From 3b011705e8afef75642c159cee8b8cbe7fcb875d Mon Sep 17 00:00:00 2001 From: bitrinjani Date: Mon, 25 Dec 2017 21:11:09 +0900 Subject: [PATCH 01/10] add onSingleLeg skelton config --- src/config_default.json | 4 ++++ src/types.ts | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/config_default.json b/src/config_default.json index cfdca2d0..44009279 100644 --- a/src/config_default.json +++ b/src/config_default.json @@ -13,6 +13,10 @@ "maxNetExposure": 0.1, "maxRetryCount": 10, "orderStatusCheckInterval": 3000, + "onSingleLeg": { + "action": "Cancel", + "options": {} + }, "brokers": [ { "broker": "Coincheck", diff --git a/src/types.ts b/src/types.ts index b98e2249..c00b5f3d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -168,6 +168,22 @@ export class LoggingConfig extends Castable { @cast slack: SlackConfig; @cast line: LineConfig; } +export class OnSingleLegConfig extends Castable { + @cast action: 'Cancel' | 'Reverse' | 'Proceed'; + @cast options: CancelOption | ReverseOption | ProceedOption; +} + +export type CancelOption = {}; + +export class ReverseOption extends Castable { + @cast limitMovePercent: number; + @cast ttl: number; +} + +export class ProceedOption extends Castable { + @cast limitMovePercent: number; + @cast ttl: number; +} export class ConfigRoot extends Castable { @cast language: string; @@ -187,10 +203,11 @@ export class ConfigRoot extends Castable { @cast maxNetExposure: number; @cast maxRetryCount: number; @cast orderStatusCheckInterval: number; + @cast onSingleLeg: OnSingleLegConfig; @cast @element(BrokerConfig) brokers: BrokerConfig[]; @cast logging: LoggingConfig; } -export type OrderPair = [Order, Order]; +export type OrderPair = [Order, Order]; \ No newline at end of file From dc584a996dbed838ad598b86b31bf8ae21b3fc5b Mon Sep 17 00:00:00 2001 From: bitrinjani Date: Mon, 25 Dec 2017 21:12:42 +0900 Subject: [PATCH 02/10] disable log output from tests --- src/__tests__/Bitflyer/BrokerAdapterImpl.test.ts | 2 ++ src/__tests__/Bitflyer/BrokerApi.test.ts | 2 ++ src/__tests__/BrokerAdapterRouterImpl.test.ts | 2 ++ src/__tests__/BrokerPosition.test.ts | 2 ++ src/__tests__/Coincheck/BrokerAdapterImpl.test.ts | 2 ++ src/__tests__/Coincheck/BrokerApi.test.ts | 2 ++ src/__tests__/Coincheck/CashStrategy.test.ts | 2 ++ src/__tests__/Coincheck/MarginOpenStrategy.test.ts | 2 ++ src/__tests__/Coincheck/NetOutStrategy.test.ts | 2 ++ src/__tests__/ConfigValidatorImpl.test.ts | 2 ++ src/__tests__/JsonConfigStore.test.ts | 2 ++ src/__tests__/PositionServiceImpl.test.ts | 2 ++ src/__tests__/Quoine/BrokerAdapterImpl.test.ts | 2 ++ src/__tests__/Quoine/BrokerApi.test.ts | 2 ++ src/__tests__/QuoteAggregatorImpl.test.ts | 2 ++ src/__tests__/SpreadAnalyzerImpl.test.ts | 2 ++ src/__tests__/WebClient.test.ts | 2 ++ 17 files changed, 34 insertions(+) diff --git a/src/__tests__/Bitflyer/BrokerAdapterImpl.test.ts b/src/__tests__/Bitflyer/BrokerAdapterImpl.test.ts index a3af9640..2ff699f3 100644 --- a/src/__tests__/Bitflyer/BrokerAdapterImpl.test.ts +++ b/src/__tests__/Bitflyer/BrokerAdapterImpl.test.ts @@ -5,6 +5,8 @@ import BrokerAdapterImpl from '../../Bitflyer/BrokerAdapterImpl'; import { OrderStatus, Broker, CashMarginType, OrderSide, OrderType } from '../../types'; import nocksetup from './nocksetup'; import Order from '../../Order'; +import { options } from '../../logger'; +options.enabled = false; nocksetup(); afterAll(() => { diff --git a/src/__tests__/Bitflyer/BrokerApi.test.ts b/src/__tests__/Bitflyer/BrokerApi.test.ts index 00d73ac4..6e6fae9d 100644 --- a/src/__tests__/Bitflyer/BrokerApi.test.ts +++ b/src/__tests__/Bitflyer/BrokerApi.test.ts @@ -3,6 +3,8 @@ import * as nock from 'nock'; import * as _ from 'lodash'; import BrokerApi from '../../Bitflyer/BrokerApi'; import nocksetup from './nocksetup'; +import { options } from '../../logger'; +options.enabled = false; nocksetup(); afterAll(() => { diff --git a/src/__tests__/BrokerAdapterRouterImpl.test.ts b/src/__tests__/BrokerAdapterRouterImpl.test.ts index b7d42f1b..d65943ef 100644 --- a/src/__tests__/BrokerAdapterRouterImpl.test.ts +++ b/src/__tests__/BrokerAdapterRouterImpl.test.ts @@ -2,6 +2,8 @@ import 'reflect-metadata'; import Order from '../Order'; import { CashMarginType, OrderType, OrderSide, Broker } from '../types'; import BrokerAdapterRouterImpl from '../BrokerAdapterRouterImpl'; +import { options } from '../logger'; +options.enabled = false; const baBitflyer = { broker: Broker.Bitflyer, diff --git a/src/__tests__/BrokerPosition.test.ts b/src/__tests__/BrokerPosition.test.ts index f9588f9e..d7557fd2 100644 --- a/src/__tests__/BrokerPosition.test.ts +++ b/src/__tests__/BrokerPosition.test.ts @@ -1,5 +1,7 @@ import BrokerPosition from '../BrokerPosition'; import { Broker } from '../types'; +import { options } from '../logger'; +options.enabled = false; describe('BrokerPosition', () => { test('toString format', () => { diff --git a/src/__tests__/Coincheck/BrokerAdapterImpl.test.ts b/src/__tests__/Coincheck/BrokerAdapterImpl.test.ts index efb2a2b6..ed37af39 100644 --- a/src/__tests__/Coincheck/BrokerAdapterImpl.test.ts +++ b/src/__tests__/Coincheck/BrokerAdapterImpl.test.ts @@ -6,6 +6,8 @@ import { OrderStatus, Broker, CashMarginType, OrderSide, OrderType, ConfigRoot } import nocksetup from './nocksetup'; import Order from '../../Order'; import { NewOrderRequest } from '../../Coincheck/types'; +import { options } from '../../logger'; +options.enabled = false; nocksetup(); diff --git a/src/__tests__/Coincheck/BrokerApi.test.ts b/src/__tests__/Coincheck/BrokerApi.test.ts index bff6bbf6..ed36d04f 100644 --- a/src/__tests__/Coincheck/BrokerApi.test.ts +++ b/src/__tests__/Coincheck/BrokerApi.test.ts @@ -4,6 +4,8 @@ import * as nock from 'nock'; import * as _ from 'lodash'; import BrokerApi from '../../Coincheck/BrokerApi'; import nocksetup from './nocksetup'; +import { options } from '../../logger'; +options.enabled = false; nocksetup(); diff --git a/src/__tests__/Coincheck/CashStrategy.test.ts b/src/__tests__/Coincheck/CashStrategy.test.ts index 1d744022..60d30aba 100644 --- a/src/__tests__/Coincheck/CashStrategy.test.ts +++ b/src/__tests__/Coincheck/CashStrategy.test.ts @@ -4,6 +4,8 @@ import BrokerApi from '../../Coincheck/BrokerApi'; import nocksetup from './nocksetup'; import Order from '../../Order'; import * as nock from 'nock'; +import { options } from '../../logger'; +options.enabled = false; nocksetup(); diff --git a/src/__tests__/Coincheck/MarginOpenStrategy.test.ts b/src/__tests__/Coincheck/MarginOpenStrategy.test.ts index 9770529b..b9ad1f64 100644 --- a/src/__tests__/Coincheck/MarginOpenStrategy.test.ts +++ b/src/__tests__/Coincheck/MarginOpenStrategy.test.ts @@ -4,6 +4,8 @@ import nocksetup from './nocksetup'; import BrokerApi from '../../Coincheck/BrokerApi'; import Order from '../../Order'; import * as nock from 'nock'; +import { options } from '../../logger'; +options.enabled = false; nocksetup(); diff --git a/src/__tests__/Coincheck/NetOutStrategy.test.ts b/src/__tests__/Coincheck/NetOutStrategy.test.ts index 2b555517..ae646b47 100644 --- a/src/__tests__/Coincheck/NetOutStrategy.test.ts +++ b/src/__tests__/Coincheck/NetOutStrategy.test.ts @@ -5,6 +5,8 @@ import nocksetup from './nocksetup'; import Order from '../../Order'; import { NewOrderRequest } from '../../Coincheck/types'; import * as nock from 'nock'; +import { options } from '../../logger'; +options.enabled = false; nocksetup(); diff --git a/src/__tests__/ConfigValidatorImpl.test.ts b/src/__tests__/ConfigValidatorImpl.test.ts index eb8f43a9..1f1e4b3c 100644 --- a/src/__tests__/ConfigValidatorImpl.test.ts +++ b/src/__tests__/ConfigValidatorImpl.test.ts @@ -1,5 +1,7 @@ import ConfigValidatorImpl from '../ConfigValidatorImpl'; import { ConfigRoot } from '../types'; +import { options } from '../logger'; +options.enabled = false; const config: ConfigRoot = require('./config_test.json'); diff --git a/src/__tests__/JsonConfigStore.test.ts b/src/__tests__/JsonConfigStore.test.ts index d206cc32..d4862d33 100644 --- a/src/__tests__/JsonConfigStore.test.ts +++ b/src/__tests__/JsonConfigStore.test.ts @@ -1,6 +1,8 @@ import 'reflect-metadata'; import JsonConfigStore from '../JsonConfigStore'; import { ConfigStore, ConfigRoot, Broker } from '../types'; +import { options } from '../logger'; +options.enabled = false; test('JsonConfigStore', () => { const validator = { validate: (config: ConfigRoot) => true }; diff --git a/src/__tests__/PositionServiceImpl.test.ts b/src/__tests__/PositionServiceImpl.test.ts index 65f5e0cb..7e424659 100644 --- a/src/__tests__/PositionServiceImpl.test.ts +++ b/src/__tests__/PositionServiceImpl.test.ts @@ -3,6 +3,8 @@ import PositionServiceImpl from '../PositionServiceImpl'; import { Broker } from '../types'; import * as _ from 'lodash'; import { delay } from '../util'; +import { options } from '../logger'; +options.enabled = false; const config = { minSize: 0.01, diff --git a/src/__tests__/Quoine/BrokerAdapterImpl.test.ts b/src/__tests__/Quoine/BrokerAdapterImpl.test.ts index 7286ceec..a77ed576 100644 --- a/src/__tests__/Quoine/BrokerAdapterImpl.test.ts +++ b/src/__tests__/Quoine/BrokerAdapterImpl.test.ts @@ -5,6 +5,8 @@ import BrokerAdapterImpl from '../../Quoine/BrokerAdapterImpl'; import { OrderStatus, Broker, CashMarginType, OrderSide, OrderType } from '../../types'; import nocksetup from './nocksetup'; import Order from '../../Order'; +import { options } from '../../logger'; +options.enabled = false; nocksetup(); diff --git a/src/__tests__/Quoine/BrokerApi.test.ts b/src/__tests__/Quoine/BrokerApi.test.ts index 06e2dc58..fad57f67 100644 --- a/src/__tests__/Quoine/BrokerApi.test.ts +++ b/src/__tests__/Quoine/BrokerApi.test.ts @@ -4,6 +4,8 @@ import * as nock from 'nock'; import * as _ from 'lodash'; import BrokerApi from '../../Quoine/BrokerApi'; import nocksetup from './nocksetup'; +import { options } from '../../logger'; +options.enabled = false; nocksetup(); diff --git a/src/__tests__/QuoteAggregatorImpl.test.ts b/src/__tests__/QuoteAggregatorImpl.test.ts index 819c48c4..ba843573 100644 --- a/src/__tests__/QuoteAggregatorImpl.test.ts +++ b/src/__tests__/QuoteAggregatorImpl.test.ts @@ -4,6 +4,8 @@ import { Broker, QuoteSide, QuoteAggregator } from '../types'; import * as _ from 'lodash'; import { delay } from '../util'; import BrokerAdapterRouterImpl from '../BrokerAdapterRouterImpl'; +import { options } from '../logger'; +options.enabled = false; const config = { iterationInterval: 3000, diff --git a/src/__tests__/SpreadAnalyzerImpl.test.ts b/src/__tests__/SpreadAnalyzerImpl.test.ts index ac5a7395..93714ddc 100644 --- a/src/__tests__/SpreadAnalyzerImpl.test.ts +++ b/src/__tests__/SpreadAnalyzerImpl.test.ts @@ -3,6 +3,8 @@ import SpreadAnalyzerImpl from '../SpreadAnalyzerImpl'; import { Broker, QuoteSide, ConfigStore } from '../types'; import * as _ from 'lodash'; import Quote from '../Quote'; +import { options } from '../logger'; +options.enabled = false; const config = require('./config_test.json'); config.maxSize = 0.5; diff --git a/src/__tests__/WebClient.test.ts b/src/__tests__/WebClient.test.ts index f79e81c6..1737725d 100644 --- a/src/__tests__/WebClient.test.ts +++ b/src/__tests__/WebClient.test.ts @@ -3,6 +3,8 @@ import WebClient from '../WebClient'; import { RequestInit } from 'node-fetch'; import * as nock from 'nock'; +import { options } from '../logger'; +options.enabled = false; const baseUrl = 'http://local'; const mocky = nock(baseUrl); From 2cee4ed6105977526a8204cd973dadceb675e3d4 Mon Sep 17 00:00:00 2001 From: bitrinjani Date: Mon, 25 Dec 2017 21:13:49 +0900 Subject: [PATCH 03/10] add onSingleLeg handler. TODO: organize output message --- src/ArbitragerImpl.ts | 90 ++++++++++- src/__tests__/ArbitragerImpl.test.ts | 222 ++++++++++++++++++++++++++- src/constants.ts | 3 +- 3 files changed, 310 insertions(+), 5 deletions(-) diff --git a/src/ArbitragerImpl.ts b/src/ArbitragerImpl.ts index 7182321d..96c0e9c2 100644 --- a/src/ArbitragerImpl.ts +++ b/src/ArbitragerImpl.ts @@ -14,18 +14,22 @@ import { QuoteSide, OrderSide, LimitCheckerFactory, - OrderPair + OrderPair, + ReverseOption, + ProceedOption } from './types'; import t from './intl'; import { padEnd, hr, delay, calculateCommission, findBrokerConfig } from './util'; import Quote from './Quote'; import symbols from './symbols'; +import { fatalErrors } from './constants'; @injectable() export default class ArbitragerImpl implements Arbitrager { private readonly log = getLogger(this.constructor.name); private activePairs: OrderPair[] = []; private lastSpreadAnalysisResult: SpreadAnalysisResult; + private shouldStop: boolean = false; constructor( @inject(symbols.QuoteAggregator) private readonly quoteAggregator: QuoteAggregator, @@ -57,6 +61,10 @@ export default class ArbitragerImpl implements Arbitrager { } private async quoteUpdated(quotes: Quote[]): Promise { + if (this.shouldStop) { + await this.stop(); + return; + } this.positionService.print(); this.log.info(hr(20) + 'ARBITRAGER' + hr(20)); await this.arbitrage(quotes); @@ -111,6 +119,10 @@ export default class ArbitragerImpl implements Arbitrager { this.log.error(ex.message); this.log.debug(ex.stack); this.status = 'Order send/refresh failed'; + if (typeof ex.message === 'string' && _.some(fatalErrors, f => (ex.message as string).includes(f))) { + this.shouldStop = true; + return; + } } this.log.info(t`SleepingAfterSend`, config.sleepAfterSend); await delay(config.sleepAfterSend); @@ -156,6 +168,9 @@ export default class ArbitragerImpl implements Arbitrager { this.log.warn(t`MaxRetryCountReachedCancellingThePendingOrders`); const cancelTasks = orders.filter(o => !o.filled).map(o => this.brokerAdapterRouter.cancel(o)); await Promise.all(cancelTasks); + if (orders.filter(o => o.filled).length === 1) { + await this.handleSingleLeg(orders); + } break; } } @@ -216,6 +231,79 @@ export default class ArbitragerImpl implements Arbitrager { return order; } + private async handleSingleLeg(orders: OrderPair) { + if (this.configStore.config.onSingleLeg === undefined || this.configStore.config.onSingleLeg.action === 'Cancel') { + return; + } + + const { action, options } = this.configStore.config.onSingleLeg; + switch (action) { + case 'Reverse': + await this.reverseLeg(orders, options as ReverseOption); + return; + case 'Proceed': + await this.proceedLeg(orders, options as ProceedOption); + return; + default: + throw new Error('Invalid action.'); + } + } + + private async reverseLeg(orders: OrderPair, options: ReverseOption) { + this.log.info(`Reversing the filled leg...`); + const filledOrders = orders.filter(o => o.filled); + const target = filledOrders[0]; + const sign = target.side === OrderSide.Buy ? -1 : 1; + const price = target.price * (1 + sign * options.limitMovePercent / 100); + this.log.info(`Target leg: ${target}, target price: ${price}`); + const reversalOrder = new Order( + target.broker, + target.side === OrderSide.Buy ? OrderSide.Sell : OrderSide.Buy, + target.size, + price, + target.cashMarginType, + OrderType.Limit, + target.leverageLevel + ); + await this.sendOrderWithTtl(reversalOrder, options.ttl); + } + + private async proceedLeg(orders: OrderPair, options: ProceedOption) { + this.log.info(`Proceeding to fill another leg with a new price...`); + const unfilledOrders = orders.filter(o => !o.filled); + const target = unfilledOrders[0]; + const sign = target.side === OrderSide.Buy ? 1 : -1; + const price = target.price * (1 + sign * options.limitMovePercent / 100); + this.log.info(`Target leg: ${target}, target price: ${price}`); + const revisedOrder = new Order( + target.broker, + target.side, + target.size, + price, + target.cashMarginType, + OrderType.Limit, + target.leverageLevel + ); + await this.sendOrderWithTtl(revisedOrder, options.ttl); + } + + private async sendOrderWithTtl(order: Order, ttl: number) { + try { + this.log.info(`Sending an order with TTL ${ttl} ms...`); + await this.brokerAdapterRouter.send(order); + await delay(ttl); + await this.brokerAdapterRouter.refresh(order); + if (!order.filled) { + this.log.info(`The order was not filled within TTL ${ttl} ms. Cancelling the order.`); + await this.brokerAdapterRouter.cancel(order); + } else { + this.log.info(`The order was filled.`); + } + } catch (ex) { + this.log.warn(ex.message); + } + } + private printOrderSummary(orders: Order[]) { orders.forEach(o => { if (o.filled) { diff --git a/src/__tests__/ArbitragerImpl.test.ts b/src/__tests__/ArbitragerImpl.test.ts index 9b750df4..186f2013 100644 --- a/src/__tests__/ArbitragerImpl.test.ts +++ b/src/__tests__/ArbitragerImpl.test.ts @@ -15,6 +15,9 @@ import ArbitragerImpl from '../ArbitragerImpl'; import Quote from '../Quote'; import LimitCheckerFactoryImpl from '../LimitCheckerFactoryImpl'; import SpreadAnalyzerImpl from '../SpreadAnalyzerImpl'; +import { delay } from '../util'; +import { options } from '../logger'; +options.enabled = false; let quoteAggregator, config: ConfigRoot, @@ -402,11 +405,155 @@ describe('Arbitrager', () => { test('Send and only sell order filled', async () => { let i = 1; - baRouter.refresh = order => { + baRouter.refresh = jest.fn().mockImplementation(order => { if (order.side === OrderSide.Sell) { order.status = OrderStatus.Filled; - } - }; + } + }); + config.maxRetryCount = 3; + spreadAnalyzer.analyze.mockImplementation(() => { + return { + bestBid: new Quote(Broker.Quoine, QuoteSide.Bid, 600, 4), + bestAsk: new Quote(Broker.Coincheck, QuoteSide.Ask, 500, 1), + invertedSpread: 100, + availableVolume: 1, + targetVolume: 1, + targetProfit: 100 + }; + }); + const arbitrager = new ArbitragerImpl( + quoteAggregator, + configStore, + positionService, + baRouter, + spreadAnalyzer, + limitCheckerFactory + ); + positionService.isStarted = true; + await arbitrager.start(); + await quoteAggregator.onQuoteUpdated([]); + expect(arbitrager.status).toBe('MaxRetryCount breached'); + expect(baRouter.refresh.mock.calls.length).toBe(6); + expect(baRouter.send.mock.calls.length).toBe(2); + expect(baRouter.cancel.mock.calls.length).toBe(1); + }); + + test('Send and only sell order filled -> reverse', async () => { + config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async (order) => { + if (order.side === OrderSide.Sell) { + order.status = OrderStatus.Filled; + } + }); + config.maxRetryCount = 3; + spreadAnalyzer.analyze.mockImplementation(() => { + return { + bestBid: new Quote(Broker.Quoine, QuoteSide.Bid, 600, 4), + bestAsk: new Quote(Broker.Coincheck, QuoteSide.Ask, 500, 1), + invertedSpread: 100, + availableVolume: 1, + targetVolume: 1, + targetProfit: 100 + }; + }); + const arbitrager = new ArbitragerImpl( + quoteAggregator, + configStore, + positionService, + baRouter, + spreadAnalyzer, + limitCheckerFactory + ); + positionService.isStarted = true; + await arbitrager.start(); + await quoteAggregator.onQuoteUpdated([]); + expect(arbitrager.status).toBe('MaxRetryCount breached'); + expect(baRouter.refresh.mock.calls.length).toBe(7); + expect(baRouter.send.mock.calls.length).toBe(3); + expect(baRouter.cancel.mock.calls.length).toBe(2); + }); + + test('Send and only buy order filled -> reverse', async () => { + config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async (order) => { + if (order.side === OrderSide.Buy) { + order.status = OrderStatus.Filled; + } + }); + config.maxRetryCount = 3; + spreadAnalyzer.analyze.mockImplementation(() => { + return { + bestBid: new Quote(Broker.Quoine, QuoteSide.Bid, 600, 4), + bestAsk: new Quote(Broker.Coincheck, QuoteSide.Ask, 500, 1), + invertedSpread: 100, + availableVolume: 1, + targetVolume: 1, + targetProfit: 100 + }; + }); + const arbitrager = new ArbitragerImpl( + quoteAggregator, + configStore, + positionService, + baRouter, + spreadAnalyzer, + limitCheckerFactory + ); + positionService.isStarted = true; + await arbitrager.start(); + await quoteAggregator.onQuoteUpdated([]); + expect(arbitrager.status).toBe('MaxRetryCount breached'); + expect(baRouter.refresh.mock.calls.length).toBe(7); + expect(baRouter.send.mock.calls.length).toBe(3); + expect(baRouter.cancel.mock.calls.length).toBe(2); + }); + + test('Send and only sell order filled -> proceed', async () => { + config.onSingleLeg = { action: 'Proceed', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async (order) => { + if (order.side === OrderSide.Sell) { + order.status = OrderStatus.Filled; + } + }); + config.maxRetryCount = 3; + spreadAnalyzer.analyze.mockImplementation(() => { + return { + bestBid: new Quote(Broker.Quoine, QuoteSide.Bid, 600, 4), + bestAsk: new Quote(Broker.Coincheck, QuoteSide.Ask, 500, 1), + invertedSpread: 100, + availableVolume: 1, + targetVolume: 1, + targetProfit: 100 + }; + }); + const arbitrager = new ArbitragerImpl( + quoteAggregator, + configStore, + positionService, + baRouter, + spreadAnalyzer, + limitCheckerFactory + ); + positionService.isStarted = true; + await arbitrager.start(); + await quoteAggregator.onQuoteUpdated([]); + expect(arbitrager.status).toBe('MaxRetryCount breached'); + expect(baRouter.refresh.mock.calls.length).toBe(7); + expect(baRouter.send.mock.calls.length).toBe(3); + expect(baRouter.cancel.mock.calls.length).toBe(2); + }); + + test('Send and only buy order filled -> proceed', async () => { + config.onSingleLeg = { action: 'Proceed', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async (order) => { + if (order.side === OrderSide.Buy) { + order.status = OrderStatus.Filled; + } + }); config.maxRetryCount = 3; spreadAnalyzer.analyze.mockImplementation(() => { return { @@ -430,6 +577,45 @@ describe('Arbitrager', () => { await arbitrager.start(); await quoteAggregator.onQuoteUpdated([]); expect(arbitrager.status).toBe('MaxRetryCount breached'); + expect(baRouter.refresh.mock.calls.length).toBe(7); + expect(baRouter.send.mock.calls.length).toBe(3); + expect(baRouter.cancel.mock.calls.length).toBe(2); + }); + + test('Send and only buy order filled -> invalid action', async () => { + config.onSingleLeg = { action: 'Invalid', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async (order) => { + if (order.side === OrderSide.Buy) { + order.status = OrderStatus.Filled; + } + }); + config.maxRetryCount = 3; + spreadAnalyzer.analyze.mockImplementation(() => { + return { + bestBid: new Quote(Broker.Quoine, QuoteSide.Bid, 600, 4), + bestAsk: new Quote(Broker.Coincheck, QuoteSide.Ask, 500, 1), + invertedSpread: 100, + availableVolume: 1, + targetVolume: 1, + targetProfit: 100 + }; + }); + const arbitrager = new ArbitragerImpl( + quoteAggregator, + configStore, + positionService, + baRouter, + spreadAnalyzer, + limitCheckerFactory + ); + positionService.isStarted = true; + await arbitrager.start(); + await quoteAggregator.onQuoteUpdated([]); + expect(arbitrager.status).toBe("Order send/refresh failed"); + expect(baRouter.refresh.mock.calls.length).toBe(6); + expect(baRouter.send.mock.calls.length).toBe(2); + expect(baRouter.cancel.mock.calls.length).toBe(1); }); test('Send and not filled', async () => { @@ -487,6 +673,36 @@ describe('Arbitrager', () => { expect(arbitrager.status).toBe('Order send/refresh failed'); }); + test('Send throws', async () => { + config.maxRetryCount = 3; + spreadAnalyzer.analyze.mockImplementation(() => { + return { + bestBid: new Quote(Broker.Quoine, QuoteSide.Bid, 600, 4), + bestAsk: new Quote(Broker.Coincheck, QuoteSide.Ask, 500, 1), + invertedSpread: 100, + availableVolume: 1, + targetVolume: 1, + targetProfit: 100 + }; + }); + baRouter.send = () => { + throw new Error('Mock error: insufficient balance'); + }; + const arbitrager = new ArbitragerImpl( + quoteAggregator, + configStore, + positionService, + baRouter, + spreadAnalyzer, + limitCheckerFactory + ); + positionService.isStarted = true; + await arbitrager.start(); + await quoteAggregator.onQuoteUpdated([]); + await quoteAggregator.onQuoteUpdated([]); + expect(arbitrager.shouldStop).toBe(true); + }); + test('Send and refresh throws', async () => { config.maxRetryCount = 3; spreadAnalyzer.analyze.mockImplementation(() => { diff --git a/src/constants.ts b/src/constants.ts index 229bf84e..b869b095 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1 +1,2 @@ -export const LOT_MIN_DECIMAL_PLACE = 3; \ No newline at end of file +export const LOT_MIN_DECIMAL_PLACE = 3; +export const fatalErrors = ['insufficient balance', 'Insufficient funds', 'Too Many Requests', 'Service Unavailable']; \ No newline at end of file From ea841641f7725d74e908a4105039792869b2cee7 Mon Sep 17 00:00:00 2001 From: bitrinjani Date: Tue, 26 Dec 2017 17:32:03 +0900 Subject: [PATCH 04/10] add output messages for single-leg handler --- src/ArbitragerImpl.ts | 59 +++++++++++++++++++----------------------- src/Order.ts | 6 ++++- src/stringResources.ts | 5 +++- 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/ArbitragerImpl.ts b/src/ArbitragerImpl.ts index 96c0e9c2..68dee645 100644 --- a/src/ArbitragerImpl.ts +++ b/src/ArbitragerImpl.ts @@ -235,7 +235,6 @@ export default class ArbitragerImpl implements Arbitrager { if (this.configStore.config.onSingleLeg === undefined || this.configStore.config.onSingleLeg.action === 'Cancel') { return; } - const { action, options } = this.configStore.config.onSingleLeg; switch (action) { case 'Reverse': @@ -251,53 +250,51 @@ export default class ArbitragerImpl implements Arbitrager { private async reverseLeg(orders: OrderPair, options: ReverseOption) { this.log.info(`Reversing the filled leg...`); - const filledOrders = orders.filter(o => o.filled); - const target = filledOrders[0]; - const sign = target.side === OrderSide.Buy ? -1 : 1; - const price = target.price * (1 + sign * options.limitMovePercent / 100); - this.log.info(`Target leg: ${target}, target price: ${price}`); + const filledLeg = orders.filter(o => o.filled)[0]; + const sign = filledLeg.side === OrderSide.Buy ? -1 : 1; + const price = _.round(filledLeg.price * (1 + sign * options.limitMovePercent / 100)); + this.log.info(`Target leg: ${filledLeg}, target price: ${price}`); const reversalOrder = new Order( - target.broker, - target.side === OrderSide.Buy ? OrderSide.Sell : OrderSide.Buy, - target.size, + filledLeg.broker, + filledLeg.side === OrderSide.Buy ? OrderSide.Sell : OrderSide.Buy, + filledLeg.size, price, - target.cashMarginType, + filledLeg.cashMarginType, OrderType.Limit, - target.leverageLevel + filledLeg.leverageLevel ); await this.sendOrderWithTtl(reversalOrder, options.ttl); } private async proceedLeg(orders: OrderPair, options: ProceedOption) { - this.log.info(`Proceeding to fill another leg with a new price...`); - const unfilledOrders = orders.filter(o => !o.filled); - const target = unfilledOrders[0]; - const sign = target.side === OrderSide.Buy ? 1 : -1; - const price = target.price * (1 + sign * options.limitMovePercent / 100); - this.log.info(`Target leg: ${target}, target price: ${price}`); + const unfilledLeg = orders.filter(o => !o.filled)[0]; + const sign = unfilledLeg.side === OrderSide.Buy ? 1 : -1; + const price = _.round(unfilledLeg.price * (1 + sign * options.limitMovePercent / 100)); + this.log.info(t`ExecuteUnfilledLeg`, unfilledLeg.broker, unfilledLeg.side, unfilledLeg.size, price); const revisedOrder = new Order( - target.broker, - target.side, - target.size, + unfilledLeg.broker, + unfilledLeg.side, + unfilledLeg.size, price, - target.cashMarginType, + unfilledLeg.cashMarginType, OrderType.Limit, - target.leverageLevel + unfilledLeg.leverageLevel ); await this.sendOrderWithTtl(revisedOrder, options.ttl); } private async sendOrderWithTtl(order: Order, ttl: number) { try { - this.log.info(`Sending an order with TTL ${ttl} ms...`); + this.log.info(t`SendingOrderTtl`, ttl); + this.log.info(`${order.toShortString()} at ${order.price}`); await this.brokerAdapterRouter.send(order); await delay(ttl); await this.brokerAdapterRouter.refresh(order); - if (!order.filled) { - this.log.info(`The order was not filled within TTL ${ttl} ms. Cancelling the order.`); - await this.brokerAdapterRouter.cancel(order); + if (order.filled) { + this.log.info(order.toExecSummary()); } else { - this.log.info(`The order was filled.`); + this.log.info(t`NotFilledTtl`, ttl); + await this.brokerAdapterRouter.cancel(order); } } catch (ex) { this.log.warn(ex.message); @@ -307,9 +304,9 @@ export default class ArbitragerImpl implements Arbitrager { private printOrderSummary(orders: Order[]) { orders.forEach(o => { if (o.filled) { - this.log.info(o.toSummary()); + this.log.info(o.toExecSummary()); } else { - this.log.warn(o.toSummary()); + this.log.warn(o.toExecSummary()); } }); } @@ -335,9 +332,7 @@ export default class ArbitragerImpl implements Arbitrager { } this.log.info(t`OpenPairs`); this.activePairs.forEach(pair => { - this.log.info( - `[${pair[0].broker} ${pair[0].side} ${pair[0].size}, ${pair[1].broker} ${pair[1].side} ${pair[1].size}]` - ); + this.log.info(`[${pair[0].toShortString()}, ${pair[1].toShortString()}]`); }); } } diff --git a/src/Order.ts b/src/Order.ts index 4881da9b..7fedb327 100644 --- a/src/Order.ts +++ b/src/Order.ts @@ -46,13 +46,17 @@ export default class Order { return this.averageFilledPrice * this.filledSize; } - toSummary(): string { + toExecSummary(): string { return this.filled ? format(t`FilledSummary`, this.broker, this.side, this.filledSize, _.round(this.averageFilledPrice).toLocaleString()) : format(t`UnfilledSummary`, this.broker, this.side, this.size, this.price.toLocaleString(), this.pendingSize); } + toShortString(): string { + return `${this.broker} ${this.side} ${this.size} BTC`; + } + toString(): string { return JSON.stringify(this); } diff --git a/src/stringResources.ts b/src/stringResources.ts index e546a9fc..7d07fcec 100644 --- a/src/stringResources.ts +++ b/src/stringResources.ts @@ -45,7 +45,10 @@ export const en = { FilledSummary: 'Filled: %s %s %d BTC filled at %s', UnfilledSummary: 'Pending: %s %s %d BTC sent at %s, pending size %d BTC', FoundClosableOrders: 'Found closable orders.', - OpenPairs: 'Open pairs:' + OpenPairs: 'Open pairs:', + SendingOrderTtl: 'Sending an order with TTL %d ms...', + NotFilledTtl: 'The order was not filled within TTL %d ms. Cancelling the order.', + ExecuteUnfilledLeg: 'Trying to execute the unfilled leg %s %s %d BTC at new price: %s' }; export const ja = { From a043ed24e700651f0cf514f173ee71c7186a4e0d Mon Sep 17 00:00:00 2001 From: bitrinjani Date: Tue, 26 Dec 2017 17:33:14 +0900 Subject: [PATCH 05/10] refactor/clean/unittest --- src/ArbitragerImpl.ts | 14 ++-- src/JsonConfigStore.ts | 4 +- src/LimitCheckerFactoryImpl.ts | 4 +- src/LimitCheckerImpl.ts | 12 +-- src/PositionServiceImpl.ts | 2 +- src/QuoteAggregatorImpl.ts | 2 +- src/SpreadAnalyzerImpl.ts | 2 +- src/__tests__/ArbitragerImpl.test.ts | 112 +++++++++++++++++++++++---- src/__tests__/util.test.ts | 2 +- src/intl.ts | 4 +- src/types.ts | 2 +- 11 files changed, 121 insertions(+), 39 deletions(-) diff --git a/src/ArbitragerImpl.ts b/src/ArbitragerImpl.ts index 68dee645..5781576b 100644 --- a/src/ArbitragerImpl.ts +++ b/src/ArbitragerImpl.ts @@ -38,7 +38,7 @@ export default class ArbitragerImpl implements Arbitrager { @inject(symbols.BrokerAdapterRouter) private readonly brokerAdapterRouter: BrokerAdapterRouter, @inject(symbols.SpreadAnalyzer) private readonly spreadAnalyzer: SpreadAnalyzer, @inject(symbols.LimitCheckerFactory) private readonly limitCheckerFactory: LimitCheckerFactory - ) {} + ) {} status: string = 'Init'; @@ -98,9 +98,7 @@ export default class ArbitragerImpl implements Arbitrager { const limitChecker = this.limitCheckerFactory.create(spreadAnalysisResult, exitFlag); const limitCheckResult = limitChecker.check(); if (!limitCheckResult.success) { - if (limitCheckResult.reason) { - this.status = limitCheckResult.reason; - } + this.status = limitCheckResult.reason; return; } @@ -128,7 +126,7 @@ export default class ArbitragerImpl implements Arbitrager { await delay(config.sleepAfterSend); } - private async checkOrderState(orders: OrderPair, exitFlag: boolean = false): Promise { + private async checkOrderState(orders: OrderPair, exitFlag: boolean): Promise { const { config } = this.configStore; for (const i of _.range(1, config.maxRetryCount + 1)) { await delay(config.orderStatusCheckInterval); @@ -313,8 +311,8 @@ export default class ArbitragerImpl implements Arbitrager { private printSpreadAnalysisResult(result: SpreadAnalysisResult) { const columnWidth = 17; - this.log.info('%s: %s', padEnd(t`BestAsk`, columnWidth), result.bestAsk); - this.log.info('%s: %s', padEnd(t`BestBid`, columnWidth), result.bestBid); + this.log.info('%s: %s', padEnd(t`BestAsk`, columnWidth), result.bestAsk.toString()); + this.log.info('%s: %s', padEnd(t`BestBid`, columnWidth), result.bestBid.toString()); this.log.info('%s: %s', padEnd(t`Spread`, columnWidth), -result.invertedSpread); this.log.info('%s: %s', padEnd(t`AvailableVolume`, columnWidth), result.availableVolume); this.log.info('%s: %s', padEnd(t`TargetVolume`, columnWidth), result.targetVolume); @@ -335,4 +333,4 @@ export default class ArbitragerImpl implements Arbitrager { this.log.info(`[${pair[0].toShortString()}, ${pair[1].toShortString()}]`); }); } -} +} /* istanbul ignore next */ diff --git a/src/JsonConfigStore.ts b/src/JsonConfigStore.ts index 601736ea..5861807a 100644 --- a/src/JsonConfigStore.ts +++ b/src/JsonConfigStore.ts @@ -12,9 +12,9 @@ export default class JsonConfigStore implements ConfigStore { ) { this._config = getConfigRoot(); configValidator.validate(this._config); - } + } get config(): ConfigRoot { return this._config; } -} \ No newline at end of file +} /* istanbul ignore next */ \ No newline at end of file diff --git a/src/LimitCheckerFactoryImpl.ts b/src/LimitCheckerFactoryImpl.ts index 09344538..d95ba516 100644 --- a/src/LimitCheckerFactoryImpl.ts +++ b/src/LimitCheckerFactoryImpl.ts @@ -8,9 +8,9 @@ export default class LimitCheckerFactoryImpl implements LimitCheckerFactory { constructor( @inject(symbols.ConfigStore) private readonly configStore: ConfigStore, @inject(symbols.PositionService) private readonly positionService: PositionService - ) { } + ) { } create(spreadAnalysisResult: SpreadAnalysisResult, exitFlag: boolean): LimitChecker { return new LimitCheckerImpl(this.configStore, this.positionService, spreadAnalysisResult, exitFlag); } -} \ No newline at end of file +} /* istanbul ignore next */ \ No newline at end of file diff --git a/src/LimitCheckerImpl.ts b/src/LimitCheckerImpl.ts index 72070f26..d909cd5e 100644 --- a/src/LimitCheckerImpl.ts +++ b/src/LimitCheckerImpl.ts @@ -39,7 +39,7 @@ export default class LimitCheckerImpl implements LimitChecker { return result; } } - return { success: true }; + return { success: true, reason: '' }; } } @@ -51,7 +51,7 @@ class MaxNetExposureLimit implements LimitChecker { check() { const success = Math.abs(this.positionService.netExposure) <= this.configStore.config.maxNetExposure; if (success) { - return { success }; + return { success, reason: '' }; } const reason = 'Max exposure breached'; this.log.info(t`NetExposureIsLargerThanMaxNetExposure`); @@ -67,7 +67,7 @@ class InvertedSpreadLimit implements LimitChecker { check() { const success = this.spreadAnalysisResult.invertedSpread > 0; if (success) { - return { success }; + return { success, reason: '' }; } const reason = 'Spread not inverted'; this.log.info(t`NoArbitrageOpportunitySpreadIsNotInverted`); @@ -83,7 +83,7 @@ class MinTargetProfitLimit implements LimitChecker { check() { const success = this.isTargetProfitLargeEnough(); if (success) { - return { success }; + return { success, reason: '' }; } const reason = 'Too small profit'; this.log.info(t`TargetProfitIsSmallerThanMinProfit`); @@ -112,7 +112,7 @@ class MaxTargetProfitLimit implements LimitChecker { check() { const success = this.isProfitSmallerThanLimit(); if (success) { - return { success }; + return { success, reason: '' }; } const reason = 'Too large profit'; this.log.info(t`TargetProfitIsLargerThanMaxProfit`); @@ -140,7 +140,7 @@ class DemoModeLimit implements LimitChecker { check() { const success = !this.configStore.config.demoMode; if (success) { - return { success }; + return { success, reason: '' }; } const reason = 'Demo mode'; this.log.info(t`ThisIsDemoModeNotSendingOrders`); diff --git a/src/PositionServiceImpl.ts b/src/PositionServiceImpl.ts index e4e92f35..ac620682 100644 --- a/src/PositionServiceImpl.ts +++ b/src/PositionServiceImpl.ts @@ -92,4 +92,4 @@ export default class PositionServiceImpl implements PositionService { pos.shortAllowed = new Decimal(allowedShortSize).gte(minSize); return pos; } -} \ No newline at end of file +} /* istanbul ignore next */ \ No newline at end of file diff --git a/src/QuoteAggregatorImpl.ts b/src/QuoteAggregatorImpl.ts index 80293016..2fe5bc9b 100644 --- a/src/QuoteAggregatorImpl.ts +++ b/src/QuoteAggregatorImpl.ts @@ -96,4 +96,4 @@ export default class QuoteAggregatorImpl implements QuoteAggregator { ) .value(); } -} \ No newline at end of file +} /* istanbul ignore next */ \ No newline at end of file diff --git a/src/SpreadAnalyzerImpl.ts b/src/SpreadAnalyzerImpl.ts index 2a23cba2..fa970eab 100644 --- a/src/SpreadAnalyzerImpl.ts +++ b/src/SpreadAnalyzerImpl.ts @@ -96,4 +96,4 @@ export default class SpreadAnalyzerImpl implements SpreadAnalyzer { private isAllowedByCurrentPosition(q: Quote, pos: BrokerPosition): boolean { return q.side === QuoteSide.Bid ? pos.shortAllowed : pos.longAllowed; } -} +} /* istanbul ignore next */ diff --git a/src/__tests__/ArbitragerImpl.test.ts b/src/__tests__/ArbitragerImpl.test.ts index 186f2013..15916131 100644 --- a/src/__tests__/ArbitragerImpl.test.ts +++ b/src/__tests__/ArbitragerImpl.test.ts @@ -408,7 +408,7 @@ describe('Arbitrager', () => { baRouter.refresh = jest.fn().mockImplementation(order => { if (order.side === OrderSide.Sell) { order.status = OrderStatus.Filled; - } + } }); config.maxRetryCount = 3; spreadAnalyzer.analyze.mockImplementation(() => { @@ -433,18 +433,18 @@ describe('Arbitrager', () => { await arbitrager.start(); await quoteAggregator.onQuoteUpdated([]); expect(arbitrager.status).toBe('MaxRetryCount breached'); - expect(baRouter.refresh.mock.calls.length).toBe(6); - expect(baRouter.send.mock.calls.length).toBe(2); + expect(baRouter.refresh.mock.calls.length).toBe(6); + expect(baRouter.send.mock.calls.length).toBe(2); expect(baRouter.cancel.mock.calls.length).toBe(1); }); test('Send and only sell order filled -> reverse', async () => { config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; let i = 1; - baRouter.refresh = jest.fn().mockImplementation(async (order) => { + baRouter.refresh = jest.fn().mockImplementation(async order => { if (order.side === OrderSide.Sell) { order.status = OrderStatus.Filled; - } + } }); config.maxRetryCount = 3; spreadAnalyzer.analyze.mockImplementation(() => { @@ -477,10 +477,10 @@ describe('Arbitrager', () => { test('Send and only buy order filled -> reverse', async () => { config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; let i = 1; - baRouter.refresh = jest.fn().mockImplementation(async (order) => { + baRouter.refresh = jest.fn().mockImplementation(async order => { if (order.side === OrderSide.Buy) { order.status = OrderStatus.Filled; - } + } }); config.maxRetryCount = 3; spreadAnalyzer.analyze.mockImplementation(() => { @@ -510,13 +510,97 @@ describe('Arbitrager', () => { expect(baRouter.cancel.mock.calls.length).toBe(2); }); + test('Send and only buy order filled -> reverse -> fill', async () => { + config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; + let i = 1; + const fillBuy = async order => { + if (order.side === OrderSide.Buy) { + order.status = OrderStatus.Filled; + } + }; + baRouter.refresh = jest + .fn() + .mockImplementationOnce(fillBuy) + .mockImplementationOnce(fillBuy) + .mockImplementationOnce(async order => {order.status = OrderStatus.Filled}); + config.maxRetryCount = 1; + spreadAnalyzer.analyze.mockImplementation(() => { + return { + bestBid: new Quote(Broker.Quoine, QuoteSide.Bid, 600, 4), + bestAsk: new Quote(Broker.Coincheck, QuoteSide.Ask, 500, 1), + invertedSpread: 100, + availableVolume: 1, + targetVolume: 1, + targetProfit: 100 + }; + }); + const arbitrager = new ArbitragerImpl( + quoteAggregator, + configStore, + positionService, + baRouter, + spreadAnalyzer, + limitCheckerFactory + ); + positionService.isStarted = true; + await arbitrager.start(); + await quoteAggregator.onQuoteUpdated([]); + expect(arbitrager.status).toBe('MaxRetryCount breached'); + expect(baRouter.refresh.mock.calls.length).toBe(3); + expect(baRouter.send.mock.calls.length).toBe(3); + expect(baRouter.cancel.mock.calls.length).toBe(1); + baRouter.refresh.mockReset(); + }); + + test('Send and only buy order filled -> reverse -> send throws', async () => { + config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async order => { + if (order.side === OrderSide.Buy) { + order.status = OrderStatus.Filled; + } + }); + baRouter.send = jest + .fn() + .mockImplementationOnce(() => {}) + .mockImplementationOnce(() => {}) + .mockImplementationOnce(() => { + throw new Error(); + }); + config.maxRetryCount = 3; + spreadAnalyzer.analyze.mockReturnValue({ + bestBid: new Quote(Broker.Quoine, QuoteSide.Bid, 600, 4), + bestAsk: new Quote(Broker.Coincheck, QuoteSide.Ask, 500, 1), + invertedSpread: 100, + availableVolume: 1, + targetVolume: 1, + targetProfit: 100 + }); + const arbitrager = new ArbitragerImpl( + quoteAggregator, + configStore, + positionService, + baRouter, + spreadAnalyzer, + limitCheckerFactory + ); + positionService.isStarted = true; + await arbitrager.start(); + await quoteAggregator.onQuoteUpdated([]); + expect(arbitrager.status).toBe('MaxRetryCount breached'); + expect(baRouter.refresh.mock.calls.length).toBe(6); + expect(baRouter.send.mock.calls.length).toBe(3); + expect(baRouter.cancel.mock.calls.length).toBe(1); + baRouter.send.mockReset(); + }); + test('Send and only sell order filled -> proceed', async () => { config.onSingleLeg = { action: 'Proceed', options: { limitMovePercent: 10 } }; let i = 1; - baRouter.refresh = jest.fn().mockImplementation(async (order) => { + baRouter.refresh = jest.fn().mockImplementation(async order => { if (order.side === OrderSide.Sell) { order.status = OrderStatus.Filled; - } + } }); config.maxRetryCount = 3; spreadAnalyzer.analyze.mockImplementation(() => { @@ -549,10 +633,10 @@ describe('Arbitrager', () => { test('Send and only buy order filled -> proceed', async () => { config.onSingleLeg = { action: 'Proceed', options: { limitMovePercent: 10 } }; let i = 1; - baRouter.refresh = jest.fn().mockImplementation(async (order) => { + baRouter.refresh = jest.fn().mockImplementation(async order => { if (order.side === OrderSide.Buy) { order.status = OrderStatus.Filled; - } + } }); config.maxRetryCount = 3; spreadAnalyzer.analyze.mockImplementation(() => { @@ -585,10 +669,10 @@ describe('Arbitrager', () => { test('Send and only buy order filled -> invalid action', async () => { config.onSingleLeg = { action: 'Invalid', options: { limitMovePercent: 10 } }; let i = 1; - baRouter.refresh = jest.fn().mockImplementation(async (order) => { + baRouter.refresh = jest.fn().mockImplementation(async order => { if (order.side === OrderSide.Buy) { order.status = OrderStatus.Filled; - } + } }); config.maxRetryCount = 3; spreadAnalyzer.analyze.mockImplementation(() => { @@ -612,7 +696,7 @@ describe('Arbitrager', () => { positionService.isStarted = true; await arbitrager.start(); await quoteAggregator.onQuoteUpdated([]); - expect(arbitrager.status).toBe("Order send/refresh failed"); + expect(arbitrager.status).toBe('Order send/refresh failed'); expect(baRouter.refresh.mock.calls.length).toBe(6); expect(baRouter.send.mock.calls.length).toBe(2); expect(baRouter.cancel.mock.calls.length).toBe(1); diff --git a/src/__tests__/util.test.ts b/src/__tests__/util.test.ts index 4701bbe2..22e95561 100644 --- a/src/__tests__/util.test.ts +++ b/src/__tests__/util.test.ts @@ -55,4 +55,4 @@ test('safeQueryStringStringify', () => { const o = { a: 1, b: undefined }; const result = util.safeQueryStringStringify(o); expect(result).toBe('a=1'); -}); \ No newline at end of file +}); diff --git a/src/intl.ts b/src/intl.ts index ab788391..7de7af96 100644 --- a/src/intl.ts +++ b/src/intl.ts @@ -15,6 +15,6 @@ i18next.init({ } }); -export default function translateTaggedTemplate(strings: TemplateStringsArray, ...keys: string[]) { - return i18next.t(strings.raw[0]) || strings.raw[0]; +export default function translateTaggedTemplate(strings: TemplateStringsArray, ...keys: string[]): string { + return i18next.t(strings.raw[0]); } diff --git a/src/types.ts b/src/types.ts index c00b5f3d..c62c63be 100644 --- a/src/types.ts +++ b/src/types.ts @@ -50,7 +50,7 @@ export interface LimitChecker { export interface LimitCheckResult { success: boolean; - reason?: string; + reason: string; } export interface LimitCheckerFactory { From a6f39414b5b88706cddc2d6ee8958dd576d29141 Mon Sep 17 00:00:00 2001 From: bitrinjani Date: Tue, 26 Dec 2017 17:33:59 +0900 Subject: [PATCH 06/10] add unittests --- src/Bitflyer/BrokerAdapterImpl.ts | 2 +- src/Coincheck/BrokerAdapterImpl.ts | 2 +- src/Quoine/BrokerAdapterImpl.ts | 2 +- src/__tests__/AppRoot.test.ts | 2 + .../Bitflyer/BrokerAdapterImpl.test.ts | 170 +++++++++++- src/__tests__/Bitflyer/nocksetup.ts | 253 +++++++++++++----- .../transport/LineIntegration.test.ts | 4 +- .../transport/SlackIntegration.test.ts | 6 +- src/__tests__/util.test.ts | 11 + src/__tests__/util_fsmock.test.ts | 7 +- 10 files changed, 379 insertions(+), 80 deletions(-) diff --git a/src/Bitflyer/BrokerAdapterImpl.ts b/src/Bitflyer/BrokerAdapterImpl.ts index ba5dbeb7..88912dc0 100644 --- a/src/Bitflyer/BrokerAdapterImpl.ts +++ b/src/Bitflyer/BrokerAdapterImpl.ts @@ -176,4 +176,4 @@ export default class BrokerAdapterImpl implements BrokerAdapter { .value(); return _.concat(asks, bids); } -} \ No newline at end of file +} /* istanbul ignore next */ \ No newline at end of file diff --git a/src/Coincheck/BrokerAdapterImpl.ts b/src/Coincheck/BrokerAdapterImpl.ts index 14d78a97..ab794130 100644 --- a/src/Coincheck/BrokerAdapterImpl.ts +++ b/src/Coincheck/BrokerAdapterImpl.ts @@ -121,4 +121,4 @@ export default class BrokerAdapterImpl implements BrokerAdapter { order.status = almostEqual(order.filledSize, order.size, 1) ? OrderStatus.Filled : OrderStatus.Canceled; order.lastUpdated = new Date(); } -} \ No newline at end of file +} /* istanbul ignore next */ \ No newline at end of file diff --git a/src/Quoine/BrokerAdapterImpl.ts b/src/Quoine/BrokerAdapterImpl.ts index 489e98fc..e62843c9 100644 --- a/src/Quoine/BrokerAdapterImpl.ts +++ b/src/Quoine/BrokerAdapterImpl.ts @@ -149,4 +149,4 @@ export default class BrokerAdapterImpl implements BrokerAdapter { .value(); return _.concat(asks, bids); } -} \ No newline at end of file +} /* istanbul ignore next */ \ No newline at end of file diff --git a/src/__tests__/AppRoot.test.ts b/src/__tests__/AppRoot.test.ts index a314b842..acf86c3d 100644 --- a/src/__tests__/AppRoot.test.ts +++ b/src/__tests__/AppRoot.test.ts @@ -1,5 +1,7 @@ import { Container } from 'inversify'; import AppRoot from '../AppRoot'; +import { options } from '../logger'; +options.enabled = false; describe('AppRoot', () => { test('start and stop', async () => { diff --git a/src/__tests__/Bitflyer/BrokerAdapterImpl.test.ts b/src/__tests__/Bitflyer/BrokerAdapterImpl.test.ts index 2ff699f3..b4666ebf 100644 --- a/src/__tests__/Bitflyer/BrokerAdapterImpl.test.ts +++ b/src/__tests__/Bitflyer/BrokerAdapterImpl.test.ts @@ -2,7 +2,7 @@ import * as nock from 'nock'; import * as _ from 'lodash'; import BrokerAdapterImpl from '../../Bitflyer/BrokerAdapterImpl'; -import { OrderStatus, Broker, CashMarginType, OrderSide, OrderType } from '../../types'; +import { OrderStatus, Broker, CashMarginType, OrderSide, OrderType, TimeInForce } from '../../types'; import nocksetup from './nocksetup'; import Order from '../../Order'; import { options } from '../../logger'; @@ -14,13 +14,10 @@ afterAll(() => { }); const config = { - brokers: [ - { broker: Broker.Bitflyer, key: '', secret: '', cashMarginType: CashMarginType.Cash } - ] + brokers: [{ broker: Broker.Bitflyer, key: '', secret: '', cashMarginType: CashMarginType.Cash }] }; -describe('Coincheck BrokerAdapter', () => { - +describe('Bitflyer BrokerAdapter', () => { test('getBtcPosition', async () => { const target = new BrokerAdapterImpl({ config }); const result = await target.getBtcPosition(); @@ -86,7 +83,12 @@ describe('Coincheck BrokerAdapter', () => { test('send StopLimit order', async () => { const target = new BrokerAdapterImpl({ config }); - const order = { broker: Broker.Bitflyer, cashMarginType: CashMarginType.Cash, symbol: 'BTCJPY', type: OrderType.StopLimit }; + const order = { + broker: Broker.Bitflyer, + cashMarginType: CashMarginType.Cash, + symbol: 'BTCJPY', + type: OrderType.StopLimit + }; try { await target.send(order); } catch (ex) { @@ -97,7 +99,13 @@ describe('Coincheck BrokerAdapter', () => { test('send wrong time in force', async () => { const target = new BrokerAdapterImpl({ config }); - const order = { broker: Broker.Bitflyer, cashMarginType: CashMarginType.Cash, symbol: 'BTCJPY', type: OrderType.Market, timeInForce: 'MOCK' }; + const order = { + broker: Broker.Bitflyer, + cashMarginType: CashMarginType.Cash, + symbol: 'BTCJPY', + type: OrderType.Market, + timeInForce: 'MOCK' + }; try { await target.send(order); } catch (ex) { @@ -131,7 +139,44 @@ describe('Coincheck BrokerAdapter', () => { OrderSide.Buy, 0.1, 30000, - CashMarginType.Cash, OrderType.Limit, undefined); + CashMarginType.Cash, + OrderType.Limit, + undefined + ); + await target.send(order); + expect(order.status).toBe(OrderStatus.New); + expect(order.brokerOrderId).toBe('JRF20150707-050237-639234'); + }); + + test('send buy limit Fok', async () => { + const target = new BrokerAdapterImpl({ config }); + const order = new Order( + Broker.Bitflyer, + OrderSide.Buy, + 0.1, + 30000, + CashMarginType.Cash, + OrderType.Limit, + undefined + ); + order.timeInForce = TimeInForce.Fok; + await target.send(order); + expect(order.status).toBe(OrderStatus.New); + expect(order.brokerOrderId).toBe('JRF20150707-050237-639234'); + }); + + test('send buy limit Ioc', async () => { + const target = new BrokerAdapterImpl({ config }); + const order = new Order( + Broker.Bitflyer, + OrderSide.Buy, + 0.1, + 30000, + CashMarginType.Cash, + OrderType.Limit, + undefined + ); + order.timeInForce = TimeInForce.Ioc; await target.send(order); expect(order.status).toBe(OrderStatus.New); expect(order.brokerOrderId).toBe('JRF20150707-050237-639234'); @@ -139,15 +184,116 @@ describe('Coincheck BrokerAdapter', () => { test('refresh', async () => { const target = new BrokerAdapterImpl({ config }); - const order = { "symbol": "BTCJPY", "type": "Limit", "timeInForce": "None", "id": "438f7c7b-ed72-4719-935f-477ea043e2b0", "status": "New", "creationTime": "2017-11-03T09:20:06.687Z", "executions": [], "broker": "Bitflyer", "size": 0.01, "side": "Sell", "price": 846700, "cashMarginType": "Cash", "brokerOrderId": "JRF20171103-092007-284294", "sentTime": "2017-11-03T09:20:07.292Z", "lastUpdated": "2017-11-03T09:20:07.292Z" }; + const order = { + symbol: 'BTCJPY', + type: 'Limit', + timeInForce: 'None', + id: '438f7c7b-ed72-4719-935f-477ea043e2b0', + status: 'New', + creationTime: '2017-11-03T09:20:06.687Z', + executions: [], + broker: 'Bitflyer', + size: 0.01, + side: 'Sell', + price: 846700, + cashMarginType: 'Cash', + brokerOrderId: 'JRF20171103-092007-284294', + sentTime: '2017-11-03T09:20:07.292Z', + lastUpdated: '2017-11-03T09:20:07.292Z' + }; await target.refresh(order); expect(order.status).toBe(OrderStatus.Filled); }); + test('refresh Expired', async () => { + const target = new BrokerAdapterImpl({ config }); + const order = { + symbol: 'BTCJPY', + type: 'Limit', + timeInForce: 'None', + id: '438f7c7b-ed72-4719-935f-477ea043e2b0', + status: 'New', + creationTime: '2017-11-03T09:20:06.687Z', + executions: [], + broker: 'Bitflyer', + size: 0.01, + side: 'Sell', + price: 846700, + cashMarginType: 'Cash', + brokerOrderId: 'JRF12345', + sentTime: '2017-11-03T09:20:07.292Z', + lastUpdated: '2017-11-03T09:20:07.292Z' + }; + await target.refresh(order); + expect(order.status).toBe(OrderStatus.Expired); + }); + + test('refresh Canceled', async () => { + const target = new BrokerAdapterImpl({ config }); + const order = { + symbol: 'BTCJPY', + type: 'Limit', + timeInForce: 'None', + id: '438f7c7b-ed72-4719-935f-477ea043e2b0', + status: 'New', + creationTime: '2017-11-03T09:20:06.687Z', + executions: [], + broker: 'Bitflyer', + size: 0.01, + side: 'Sell', + price: 846700, + cashMarginType: 'Cash', + brokerOrderId: 'JRF12345', + sentTime: '2017-11-03T09:20:07.292Z', + lastUpdated: '2017-11-03T09:20:07.292Z' + }; + await target.refresh(order); + expect(order.status).toBe(OrderStatus.Canceled); + }); + + test('refresh Partially filled', async () => { + const target = new BrokerAdapterImpl({ config }); + const order = { + symbol: 'BTCJPY', + type: 'Limit', + timeInForce: 'None', + id: '438f7c7b-ed72-4719-935f-477ea043e2b0', + status: 'New', + creationTime: '2017-11-03T09:20:06.687Z', + executions: [], + broker: 'Bitflyer', + size: 0.01, + side: 'Sell', + price: 846700, + cashMarginType: 'Cash', + brokerOrderId: 'JRF12345', + sentTime: '2017-11-03T09:20:07.292Z', + lastUpdated: '2017-11-03T09:20:07.292Z' + }; + await target.refresh(order); + expect(order.status).toBe(OrderStatus.PartiallyFilled); + }); + test('refresh unknown order id', async () => { const target = new BrokerAdapterImpl({ config }); - const order = { "symbol": "BTCJPY", "type": "Limit", "timeInForce": "None", "id": "438f7c7b-ed72-4719-935f-477ea043e2b0", "status": "New", "creationTime": "2017-11-03T09:20:06.687Z", "executions": [], "broker": "Bitflyer", "size": 0.01, "side": "Sell", "price": 846700, "cashMarginType": "Cash", "brokerOrderId": "MOCK", "sentTime": "2017-11-03T09:20:07.292Z", "lastUpdated": "2017-11-03T09:20:07.292Z" }; + const order = { + symbol: 'BTCJPY', + type: 'Limit', + timeInForce: 'None', + id: '438f7c7b-ed72-4719-935f-477ea043e2b0', + status: 'New', + creationTime: '2017-11-03T09:20:06.687Z', + executions: [], + broker: 'Bitflyer', + size: 0.01, + side: 'Sell', + price: 846700, + cashMarginType: 'Cash', + brokerOrderId: 'MOCK', + sentTime: '2017-11-03T09:20:07.292Z', + lastUpdated: '2017-11-03T09:20:07.292Z' + }; await target.refresh(order); expect(order.status).toBe(OrderStatus.New); }); -}); \ No newline at end of file +}); diff --git a/src/__tests__/Bitflyer/nocksetup.ts b/src/__tests__/Bitflyer/nocksetup.ts index 073457f6..6305048d 100644 --- a/src/__tests__/Bitflyer/nocksetup.ts +++ b/src/__tests__/Bitflyer/nocksetup.ts @@ -3,91 +3,220 @@ import * as nock from 'nock'; function nocksetup() { const api = nock('https://api.bitflyer.jp'); - api.get('/v1/me/getbalance').reply(200, [{ "currency_code": "JPY", "amount": 100, "available": 100 }, { "currency_code": "BTC", "amount": 0.01084272, "available": 0.01084272 }, { "currency_code": "BCH", "amount": 0.00047989, "available": 0.00047989 }, { "currency_code": "ETH", "amount": 0, "available": 0 }, { "currency_code": "ETC", "amount": 0, "available": 0 }, { "currency_code": "LTC", "amount": 0, "available": 0 }, { "currency_code": "MONA", "amount": 0, "available": 0 }]); - api.get('/v1/me/getbalance').reply(200, [{ "currency_code": "JPY", "amount": 100, "available": 100 }, { "currency_code": "MOCK", "amount": 0.01084272, "available": 0.01084272 }, { "currency_code": "BCH", "amount": 0.00047989, "available": 0.00047989 }, { "currency_code": "ETH", "amount": 0, "available": 0 }, { "currency_code": "ETC", "amount": 0, "available": 0 }, { "currency_code": "LTC", "amount": 0, "available": 0 }, { "currency_code": "MONA", "amount": 0, "available": 0 }]); - api.post('/v1/me/sendchildorder', { - "product_code": "BTC_JPY", - "child_order_type": "LIMIT", - "side": "BUY", - "price": 30000, - "size": 0.1, - "time_in_force": "" - }).reply(200, { - "child_order_acceptance_id": "JRF20150707-050237-639234" - }); - api.post('/v1/me/cancelchildorder', { - "product_code": "BTC_JPY", - "child_order_acceptance_id": "JRF20150707-033333-099999" - }).reply(200); + api + .get('/v1/me/getbalance') + .reply(200, [ + { currency_code: 'JPY', amount: 100, available: 100 }, + { currency_code: 'BTC', amount: 0.01084272, available: 0.01084272 }, + { currency_code: 'BCH', amount: 0.00047989, available: 0.00047989 }, + { currency_code: 'ETH', amount: 0, available: 0 }, + { currency_code: 'ETC', amount: 0, available: 0 }, + { currency_code: 'LTC', amount: 0, available: 0 }, + { currency_code: 'MONA', amount: 0, available: 0 } + ]); + api + .get('/v1/me/getbalance') + .reply(200, [ + { currency_code: 'JPY', amount: 100, available: 100 }, + { currency_code: 'MOCK', amount: 0.01084272, available: 0.01084272 }, + { currency_code: 'BCH', amount: 0.00047989, available: 0.00047989 }, + { currency_code: 'ETH', amount: 0, available: 0 }, + { currency_code: 'ETC', amount: 0, available: 0 }, + { currency_code: 'LTC', amount: 0, available: 0 }, + { currency_code: 'MONA', amount: 0, available: 0 } + ]); + api + .post('/v1/me/sendchildorder', { + product_code: 'BTC_JPY', + child_order_type: 'LIMIT', + side: 'BUY', + price: 30000, + size: 0.1, + time_in_force: /.*/ + }) + .times(10) + .reply(200, { + child_order_acceptance_id: 'JRF20150707-050237-639234' + }); + api + .post('/v1/me/cancelchildorder', { + product_code: 'BTC_JPY', + child_order_acceptance_id: 'JRF20150707-033333-099999' + }) + .reply(200); api.get('/v1/me/getchildorders?child_order_acceptance_id=JRF20150707-084547-396699').reply(200, [ { - "id": 138397, - "child_order_id": "JOR20150707-084549-022519", - "product_code": "BTC_JPY", - "side": "SELL", - "child_order_type": "LIMIT", - "price": 30000, - "average_price": 0, - "size": 0.1, - "child_order_state": "CANCELED", - "expire_date": "2015-07-14T07:25:47", - "child_order_date": "2015-07-07T08:45:47", - "child_order_acceptance_id": "JRF20150707-084547-396699", - "outstanding_size": 0, - "cancel_size": 0.1, - "executed_size": 0, - "total_commission": 0 + id: 138397, + child_order_id: 'JOR20150707-084549-022519', + product_code: 'BTC_JPY', + side: 'SELL', + child_order_type: 'LIMIT', + price: 30000, + average_price: 0, + size: 0.1, + child_order_state: 'CANCELED', + expire_date: '2015-07-14T07:25:47', + child_order_date: '2015-07-07T08:45:47', + child_order_acceptance_id: 'JRF20150707-084547-396699', + outstanding_size: 0, + cancel_size: 0.1, + executed_size: 0, + total_commission: 0 } ]); - api.get('/v1/me/getchildorders?child_order_acceptance_id=JRF20171103-092007-284294').reply(200, [{"id":149550970,"child_order_id":"JOR20171103-092009-823736","product_code":"BTC_JPY","side":"SELL","child_order_type":"LIMIT","price":846700,"average_price":846700,"size":0.01,"child_order_state":"COMPLETED","expire_date":"2017-12-03T09:20:07","child_order_date":"2017-11-03T09:20:07","child_order_acceptance_id":"JRF20171103-092007-284294","outstanding_size":0,"cancel_size":0,"executed_size":0.01,"total_commission":0.000015}]); + api + .get('/v1/me/getchildorders?child_order_acceptance_id=JRF20171103-092007-284294') + .reply(200, [ + { + id: 149550970, + child_order_id: 'JOR20171103-092009-823736', + product_code: 'BTC_JPY', + side: 'SELL', + child_order_type: 'LIMIT', + price: 846700, + average_price: 846700, + size: 0.01, + child_order_state: 'COMPLETED', + expire_date: '2017-12-03T09:20:07', + child_order_date: '2017-11-03T09:20:07', + child_order_acceptance_id: 'JRF20171103-092007-284294', + outstanding_size: 0, + cancel_size: 0, + executed_size: 0.01, + total_commission: 0.000015 + } + ]); + api + .get('/v1/me/getchildorders?child_order_acceptance_id=JRF12345') + .reply(200, [ + { + id: 149550970, + child_order_id: 'JRF12345', + product_code: 'BTC_JPY', + side: 'SELL', + child_order_type: 'LIMIT', + price: 846700, + average_price: 846700, + size: 0.01, + child_order_state: 'EXPIRED', + expire_date: '2017-12-03T09:20:07', + child_order_date: '2017-11-03T09:20:07', + child_order_acceptance_id: 'JRF12345', + outstanding_size: 0, + cancel_size: 0, + executed_size: 0, + total_commission: 0.000015 + } + ]); + api + .get('/v1/me/getchildorders?child_order_acceptance_id=JRF12345') + .reply(200, [ + { + id: 149550970, + child_order_id: 'JRF12345', + product_code: 'BTC_JPY', + side: 'SELL', + child_order_type: 'LIMIT', + price: 846700, + average_price: 846700, + size: 0.01, + child_order_state: 'CANCELED', + expire_date: '2017-12-03T09:20:07', + child_order_date: '2017-11-03T09:20:07', + child_order_acceptance_id: 'JRF12345', + outstanding_size: 0, + cancel_size: 0, + executed_size: 0, + total_commission: 0.000015 + } + ]); + api + .get('/v1/me/getchildorders?child_order_acceptance_id=JRF12345') + .reply(200, [ + { + id: 149550970, + child_order_id: 'JRF12345', + product_code: 'BTC_JPY', + side: 'SELL', + child_order_type: 'LIMIT', + price: 846700, + average_price: 846700, + size: 0.01, + child_order_state: 'ABC', + expire_date: '2017-12-03T09:20:07', + child_order_date: '2017-11-03T09:20:07', + child_order_acceptance_id: 'JRF12345', + outstanding_size: 0, + cancel_size: 0, + executed_size: 0.005, + total_commission: 0.000015 + } + ]); api.get('/v1/me/getchildorders?child_order_acceptance_id=MOCK').reply(200, []); - api.get('/v1/me/getexecutions?child_order_acceptance_id=JRF20171103-092007-284294').reply(200, [{"id":64923644,"side":"SELL","price":846700,"size":0.01,"exec_date":"2017-11-03T09:20:09.12","child_order_id":"JOR20171103-092009-823736","commission":0.000015,"child_order_acceptance_id":"JRF20171103-092007-284294"}]); + api + .get('/v1/me/getexecutions?child_order_acceptance_id=JRF20171103-092007-284294') + .reply(200, [ + { + id: 64923644, + side: 'SELL', + price: 846700, + size: 0.01, + exec_date: '2017-11-03T09:20:09.12', + child_order_id: 'JOR20171103-092009-823736', + commission: 0.000015, + child_order_acceptance_id: 'JRF20171103-092007-284294' + } + ]); + api + .get('/v1/me/getexecutions?child_order_acceptance_id=JRF12345') + .times(10) + .reply(200, []); api.get('/v1/board').reply(200, { - "mid_price": 33320, - "bids": [ + mid_price: 33320, + bids: [ { - "price": 30000, - "size": 0.1 + price: 30000, + size: 0.1 }, { - "price": 25570, - "size": 3 + price: 25570, + size: 3 } ], - "asks": [ + asks: [ { - "price": 36640, - "size": 5 + price: 36640, + size: 5 }, { - "price": 36700, - "size": 1.2 + price: 36700, + size: 1.2 } ] }); api.get('/v1/board').reply(500); api.get('/v1/me/getexecutions?child_order_acceptance_id=JRF20150707-060559-396699').reply(200, [ { - "id": 37233, - "child_order_id": "JOR20150707-060559-021935", - "side": "BUY", - "price": 33470, - "size": 0.01, - "commission": 0, - "exec_date": "2015-07-07T09:57:40.397", - "child_order_acceptance_id": "JRF20150707-060559-396699" + id: 37233, + child_order_id: 'JOR20150707-060559-021935', + side: 'BUY', + price: 33470, + size: 0.01, + commission: 0, + exec_date: '2015-07-07T09:57:40.397', + child_order_acceptance_id: 'JRF20150707-060559-396699' }, { - "id": 37232, - "child_order_id": "JOR20150707-060426-021925", - "side": "BUY", - "price": 33470, - "size": 0.01, - "commission": 0, - "exec_date": "2015-07-07T09:57:40.397", - "child_order_acceptance_id": "JRF20150707-060559-396699" + id: 37232, + child_order_id: 'JOR20150707-060426-021925', + side: 'BUY', + price: 33470, + size: 0.01, + commission: 0, + exec_date: '2015-07-07T09:57:40.397', + child_order_acceptance_id: 'JRF20150707-060559-396699' } ]); } -export default nocksetup; \ No newline at end of file +export default nocksetup; diff --git a/src/__tests__/transport/LineIntegration.test.ts b/src/__tests__/transport/LineIntegration.test.ts index 343d8fb4..a406fe2f 100644 --- a/src/__tests__/transport/LineIntegration.test.ts +++ b/src/__tests__/transport/LineIntegration.test.ts @@ -7,6 +7,7 @@ const lineUrl = 'https://notify-api.line.me/api/notify'; const lineApi = nock(lineUrl); lineApi.post('').reply(200, 'ok'); lineApi.post('').reply(500, 'ng'); +lineApi.post('').replyWithError('mock error'); describe('LineIntegration', () => { test('line', () => { @@ -40,7 +41,7 @@ describe('LineIntegration', () => { line.handler('with keyword: profit'); }); - test('line exception response', () => { + test('line exception response', async () => { const config = { enabled: true, token: 'TOKEN', @@ -48,6 +49,7 @@ describe('LineIntegration', () => { } as LineConfig; const line = new LineIntegration(config); line.handler('with keyword: profit'); + await delay(0); }); afterAll(() => { diff --git a/src/__tests__/transport/SlackIntegration.test.ts b/src/__tests__/transport/SlackIntegration.test.ts index 5754afab..9ec32e76 100644 --- a/src/__tests__/transport/SlackIntegration.test.ts +++ b/src/__tests__/transport/SlackIntegration.test.ts @@ -1,10 +1,13 @@ import SlackIntegration from '../../transport/SlackIntegration'; import { SlackConfig } from '../../types'; import * as nock from 'nock'; +import * as util from '../../util'; const slackUrl = 'https://hooks.slack.com/services'; const slackApi = nock(slackUrl); slackApi.post('/xxxxxx').reply(200, 'ok'); +slackApi.post('/xxxxxx').replyWithError('mock error'); + describe('SlackIntegration', () => { test('slack', () => { const config = { @@ -19,7 +22,7 @@ describe('SlackIntegration', () => { slack.handler('with keyword: profit'); }); - test('slack exception handling', () => { + test('slack exception handling', async () => { const config = { enabled: true, url: 'https://hooks.slack.com/services/xxxxxx', @@ -30,6 +33,7 @@ describe('SlackIntegration', () => { const slack = new SlackIntegration(config); slack.handler('test message'); slack.handler('with keyword: profit'); + await util.delay(0); }); test('slack with no keyword', () => { diff --git a/src/__tests__/util.test.ts b/src/__tests__/util.test.ts index 22e95561..b3605115 100644 --- a/src/__tests__/util.test.ts +++ b/src/__tests__/util.test.ts @@ -56,3 +56,14 @@ test('safeQueryStringStringify', () => { const result = util.safeQueryStringStringify(o); expect(result).toBe('a=1'); }); + +import * as logger from '../logger'; +test('logger', () => { + const log = logger.getLogger(''); + log.warn(); + log.info(); + log.error(); + log.debug(); + const log2 = logger.getLogger(''); + expect(log).toBe(log2); +}); \ No newline at end of file diff --git a/src/__tests__/util_fsmock.test.ts b/src/__tests__/util_fsmock.test.ts index 9eab275a..cc451d51 100644 --- a/src/__tests__/util_fsmock.test.ts +++ b/src/__tests__/util_fsmock.test.ts @@ -1,7 +1,8 @@ jest.mock('fs', () => ({ existsSync: jest.fn(() => false), - readFileSync: jest.fn(() => '{"language": "test"}') + readFileSync: jest.fn(() => '{"language": "test"}'), + mkdirSync: jest.fn(() => { throw { code: 'EEXIST' }; }) })); import * as fs from 'fs'; @@ -23,4 +24,8 @@ test('getConfigRoot with process.env mock', () => { } }); +test('mkdir throws', () => { + expect(() => util.mkdir('mockdir')).not.toThrow(); +}); + afterAll(() => jest.unmock('fs')); \ No newline at end of file From 489947be79941732f4510414048f27fb00ada725 Mon Sep 17 00:00:00 2001 From: bitrinjani Date: Tue, 26 Dec 2017 17:38:14 +0900 Subject: [PATCH 07/10] add jp translation --- src/stringResources.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/stringResources.ts b/src/stringResources.ts index 7d07fcec..aa5fd9bd 100644 --- a/src/stringResources.ts +++ b/src/stringResources.ts @@ -97,5 +97,8 @@ export const ja = { FilledSummary: '>>約定済み: %s %s %d BTC 約定価格 %s', UnfilledSummary: '>>執行中: %s %s %d BTC 指値 %s, 残り数量 %d BTC', FoundClosableOrders: 'クローズ可能なオーダーを発見。', - OpenPairs: 'オープン状態のオーダーペア:' + OpenPairs: 'オープン状態のオーダーペア:', + SendingOrderTtl: 'TTL %d msのオーダーを送信中...', + NotFilledTtl: 'TTL %d msの間にオーダーは約定しませんでした。キャンセルします。', + ExecuteUnfilledLeg: '約定しなかったオーダー %s %s %d BTC を価格 %s で再送信中...' }; \ No newline at end of file From eb967fb33676e8c279c4b6f55c9fbab62287dffb Mon Sep 17 00:00:00 2001 From: bitrinjani Date: Tue, 26 Dec 2017 18:03:59 +0900 Subject: [PATCH 08/10] fix output messages --- src/ArbitragerImpl.ts | 8 +++----- src/stringResources.ts | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/ArbitragerImpl.ts b/src/ArbitragerImpl.ts index 5781576b..87be965c 100644 --- a/src/ArbitragerImpl.ts +++ b/src/ArbitragerImpl.ts @@ -247,11 +247,10 @@ export default class ArbitragerImpl implements Arbitrager { } private async reverseLeg(orders: OrderPair, options: ReverseOption) { - this.log.info(`Reversing the filled leg...`); const filledLeg = orders.filter(o => o.filled)[0]; const sign = filledLeg.side === OrderSide.Buy ? -1 : 1; const price = _.round(filledLeg.price * (1 + sign * options.limitMovePercent / 100)); - this.log.info(`Target leg: ${filledLeg}, target price: ${price}`); + this.log.info(t`ReverseFilledLeg`, filledLeg.toShortString(), price.toLocaleString()); const reversalOrder = new Order( filledLeg.broker, filledLeg.side === OrderSide.Buy ? OrderSide.Sell : OrderSide.Buy, @@ -268,7 +267,7 @@ export default class ArbitragerImpl implements Arbitrager { const unfilledLeg = orders.filter(o => !o.filled)[0]; const sign = unfilledLeg.side === OrderSide.Buy ? 1 : -1; const price = _.round(unfilledLeg.price * (1 + sign * options.limitMovePercent / 100)); - this.log.info(t`ExecuteUnfilledLeg`, unfilledLeg.broker, unfilledLeg.side, unfilledLeg.size, price); + this.log.info(t`ExecuteUnfilledLeg`, unfilledLeg.toShortString(), price.toLocaleString()); const revisedOrder = new Order( unfilledLeg.broker, unfilledLeg.side, @@ -284,12 +283,11 @@ export default class ArbitragerImpl implements Arbitrager { private async sendOrderWithTtl(order: Order, ttl: number) { try { this.log.info(t`SendingOrderTtl`, ttl); - this.log.info(`${order.toShortString()} at ${order.price}`); await this.brokerAdapterRouter.send(order); await delay(ttl); await this.brokerAdapterRouter.refresh(order); if (order.filled) { - this.log.info(order.toExecSummary()); + this.log.info(`${order.toExecSummary()}`); } else { this.log.info(t`NotFilledTtl`, ttl); await this.brokerAdapterRouter.cancel(order); diff --git a/src/stringResources.ts b/src/stringResources.ts index aa5fd9bd..ca3cb842 100644 --- a/src/stringResources.ts +++ b/src/stringResources.ts @@ -42,13 +42,14 @@ export const en = { ThisIsDemoModeNotSendingOrders: '>>This is Demo mode. Not sending orders.', AnalyzingQuotes: 'Analyzing quotes...', WaitingForPositionService: 'Waiting for Position Service...', - FilledSummary: 'Filled: %s %s %d BTC filled at %s', - UnfilledSummary: 'Pending: %s %s %d BTC sent at %s, pending size %d BTC', + FilledSummary: '>>Filled: %s %s %d BTC filled at %s', + UnfilledSummary: '>>Pending: %s %s %d BTC sent at %s, pending size %d BTC', FoundClosableOrders: 'Found closable orders.', OpenPairs: 'Open pairs:', - SendingOrderTtl: 'Sending an order with TTL %d ms...', - NotFilledTtl: 'The order was not filled within TTL %d ms. Cancelling the order.', - ExecuteUnfilledLeg: 'Trying to execute the unfilled leg %s %s %d BTC at new price: %s' + SendingOrderTtl: '>>Sending an order with TTL %d ms...', + NotFilledTtl: '>>The order was not filled within TTL %d ms. Cancelling the order.', + ExecuteUnfilledLeg: '>>Trying to execute the unfilled leg %s at new price %d', + ReverseFilledLeg: '>>Trying to reverse the filled leg %s at new price %d' }; export const ja = { @@ -98,7 +99,8 @@ export const ja = { UnfilledSummary: '>>執行中: %s %s %d BTC 指値 %s, 残り数量 %d BTC', FoundClosableOrders: 'クローズ可能なオーダーを発見。', OpenPairs: 'オープン状態のオーダーペア:', - SendingOrderTtl: 'TTL %d msのオーダーを送信中...', - NotFilledTtl: 'TTL %d msの間にオーダーは約定しませんでした。キャンセルします。', - ExecuteUnfilledLeg: '約定しなかったオーダー %s %s %d BTC を価格 %s で再送信中...' + SendingOrderTtl: '>>TTL %d msのオーダーを送信中...', + NotFilledTtl: '>>TTL %d msの間にオーダーは約定しませんでした。キャンセルします。', + ExecuteUnfilledLeg: '>>約定しなかったオーダー %s を価格 %d で再送信中...', + ReverseFilledLeg: '>>約定したオーダー %s を価格 %d で反対売買中...' }; \ No newline at end of file From 1b0f2fb37ecd05c95a7848262defb6aa3de22ed6 Mon Sep 17 00:00:00 2001 From: bitrinjani Date: Tue, 26 Dec 2017 18:25:02 +0900 Subject: [PATCH 09/10] add actionOnExit config --- src/ArbitragerImpl.ts | 16 +++++++++++----- src/stringResources.ts | 8 ++++---- src/types.ts | 1 + 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/ArbitragerImpl.ts b/src/ArbitragerImpl.ts index 87be965c..c117830f 100644 --- a/src/ArbitragerImpl.ts +++ b/src/ArbitragerImpl.ts @@ -16,7 +16,8 @@ import { LimitCheckerFactory, OrderPair, ReverseOption, - ProceedOption + ProceedOption, + OnSingleLegConfig } from './types'; import t from './intl'; import { padEnd, hr, delay, calculateCommission, findBrokerConfig } from './util'; @@ -167,7 +168,8 @@ export default class ArbitragerImpl implements Arbitrager { const cancelTasks = orders.filter(o => !o.filled).map(o => this.brokerAdapterRouter.cancel(o)); await Promise.all(cancelTasks); if (orders.filter(o => o.filled).length === 1) { - await this.handleSingleLeg(orders); + const onSingleLegConfig = config.onSingleLeg; + await this.handleSingleLeg(orders, onSingleLegConfig, exitFlag); } break; } @@ -229,11 +231,15 @@ export default class ArbitragerImpl implements Arbitrager { return order; } - private async handleSingleLeg(orders: OrderPair) { - if (this.configStore.config.onSingleLeg === undefined || this.configStore.config.onSingleLeg.action === 'Cancel') { + private async handleSingleLeg(orders: OrderPair, onSingleLegConfig: OnSingleLegConfig, exitFlag: Boolean) { + if (onSingleLegConfig === undefined) { return; } - const { action, options } = this.configStore.config.onSingleLeg; + const action = exitFlag ? onSingleLegConfig.actionOnExit : onSingleLegConfig.action; + if (action === undefined || action === 'Cancel') { + return; + } + const { options } = onSingleLegConfig; switch (action) { case 'Reverse': await this.reverseLeg(orders, options as ReverseOption); diff --git a/src/stringResources.ts b/src/stringResources.ts index ca3cb842..94d96b2f 100644 --- a/src/stringResources.ts +++ b/src/stringResources.ts @@ -48,8 +48,8 @@ export const en = { OpenPairs: 'Open pairs:', SendingOrderTtl: '>>Sending an order with TTL %d ms...', NotFilledTtl: '>>The order was not filled within TTL %d ms. Cancelling the order.', - ExecuteUnfilledLeg: '>>Trying to execute the unfilled leg %s at new price %d', - ReverseFilledLeg: '>>Trying to reverse the filled leg %s at new price %d' + ExecuteUnfilledLeg: '>>Trying to execute the unfilled leg %s at new price %s', + ReverseFilledLeg: '>>Trying to reverse the filled leg %s at new price %s' }; export const ja = { @@ -101,6 +101,6 @@ export const ja = { OpenPairs: 'オープン状態のオーダーペア:', SendingOrderTtl: '>>TTL %d msのオーダーを送信中...', NotFilledTtl: '>>TTL %d msの間にオーダーは約定しませんでした。キャンセルします。', - ExecuteUnfilledLeg: '>>約定しなかったオーダー %s を価格 %d で再送信中...', - ReverseFilledLeg: '>>約定したオーダー %s を価格 %d で反対売買中...' + ExecuteUnfilledLeg: '>>約定しなかったオーダー %s を価格 %s で再送信中...', + ReverseFilledLeg: '>>約定したオーダー %s を価格 %s で反対売買中...' }; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index c62c63be..cb93a36b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -170,6 +170,7 @@ export class LoggingConfig extends Castable { } export class OnSingleLegConfig extends Castable { @cast action: 'Cancel' | 'Reverse' | 'Proceed'; + @cast actionOnExit: 'Cancel' | 'Reverse' | 'Proceed'; @cast options: CancelOption | ReverseOption | ProceedOption; } From 90d636a050c8206f36dcc75b77e6b20b155ece31 Mon Sep 17 00:00:00 2001 From: bitrinjani Date: Tue, 26 Dec 2017 18:27:04 +0900 Subject: [PATCH 10/10] update config_default.json --- src/config_default.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/config_default.json b/src/config_default.json index 44009279..7b1c8085 100644 --- a/src/config_default.json +++ b/src/config_default.json @@ -14,8 +14,12 @@ "maxRetryCount": 10, "orderStatusCheckInterval": 3000, "onSingleLeg": { - "action": "Cancel", - "options": {} + "action": "Reverse", + "actionOnExit": "Proceed", + "options": { + "limitMovePercent": 5, + "ttl": 3000 + } }, "brokers": [ {