diff --git a/src/ArbitragerImpl.ts b/src/ArbitragerImpl.ts index e87650c7..b43492e4 100644 --- a/src/ArbitragerImpl.ts +++ b/src/ArbitragerImpl.ts @@ -162,7 +162,8 @@ 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) { + if (orders.some(o => !o.filled) && + _(orders).sumBy(o => o.filledSize * (o.side === OrderSide.Buy ? -1 : 1)) !== 0) { const subOrders = await this.singleLegHandler.handle(orders, exitFlag); if (subOrders.length !== 0 && subOrders.every(o => o.filled)) { this.printProfit(_.concat(orders, subOrders)); diff --git a/src/SingleLegHandler.ts b/src/SingleLegHandler.ts index 212ddf2a..c0655817 100644 --- a/src/SingleLegHandler.ts +++ b/src/SingleLegHandler.ts @@ -42,39 +42,40 @@ export default class SingleLegHandler { } private async reverseLeg(orders: OrderPair, options: ReverseOption): Promise { - const filledLeg = orders.filter(o => o.filled)[0]; - const unfilledLeg = orders.filter(o => !o.filled)[0]; - const sign = filledLeg.side === OrderSide.Buy ? -1 : 1; - const price = _.round(filledLeg.price * (1 + sign * options.limitMovePercent / 100)); - const size = _.floor(filledLeg.filledSize - unfilledLeg.filledSize, LOT_MIN_DECIMAL_PLACE); - this.log.info(t`ReverseFilledLeg`, filledLeg.toShortString(), price.toLocaleString(), size); + const smallLeg = orders[0].filledSize <= orders[1].filledSize ? orders[0] : orders[1]; + const largeLeg = orders[0].filledSize <= orders[1].filledSize ? orders[1] : orders[0]; + const sign = largeLeg.side === OrderSide.Buy ? -1 : 1; + const price = _.round(largeLeg.price * (1 + sign * options.limitMovePercent / 100)); + const size = _.floor(largeLeg.filledSize - smallLeg.filledSize, LOT_MIN_DECIMAL_PLACE); + this.log.info(t`ReverseFilledLeg`, largeLeg.toShortString(), price.toLocaleString(), size); const reversalOrder = new Order( - filledLeg.broker, - filledLeg.side === OrderSide.Buy ? OrderSide.Sell : OrderSide.Buy, + largeLeg.broker, + largeLeg.side === OrderSide.Buy ? OrderSide.Sell : OrderSide.Buy, size, price, - filledLeg.cashMarginType, + largeLeg.cashMarginType, OrderType.Limit, - filledLeg.leverageLevel + largeLeg.leverageLevel ); await this.sendOrderWithTtl(reversalOrder, options.ttl); return [reversalOrder]; } private async proceedLeg(orders: OrderPair, options: ProceedOption): Promise { - 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)); - const size = _.floor(unfilledLeg.pendingSize, LOT_MIN_DECIMAL_PLACE); - this.log.info(t`ExecuteUnfilledLeg`, unfilledLeg.toShortString(), price.toLocaleString(), size); + const smallLeg = orders[0].filledSize <= orders[1].filledSize ? orders[0] : orders[1]; + const largeLeg = orders[0].filledSize <= orders[1].filledSize ? orders[1] : orders[0]; + const sign = smallLeg.side === OrderSide.Buy ? 1 : -1; + const price = _.round(smallLeg.price * (1 + sign * options.limitMovePercent / 100)); + const size = _.floor(smallLeg.pendingSize - largeLeg.pendingSize, LOT_MIN_DECIMAL_PLACE); + this.log.info(t`ExecuteUnfilledLeg`, smallLeg.toShortString(), price.toLocaleString(), size); const proceedOrder = new Order( - unfilledLeg.broker, - unfilledLeg.side, + smallLeg.broker, + smallLeg.side, size, price, - unfilledLeg.cashMarginType, + smallLeg.cashMarginType, OrderType.Limit, - unfilledLeg.leverageLevel + smallLeg.leverageLevel ); await this.sendOrderWithTtl(proceedOrder, options.ttl); return [proceedOrder]; diff --git a/src/__tests__/ArbitragerImpl.test.ts b/src/__tests__/ArbitragerImpl.test.ts index c2a199c4..d61f2a01 100644 --- a/src/__tests__/ArbitragerImpl.test.ts +++ b/src/__tests__/ArbitragerImpl.test.ts @@ -372,6 +372,39 @@ describe('Arbitrager', () => { expect(arbitrager.status).toBe('Demo mode'); }); + test('Send and both orders filled', async () => { + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(order => { + 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('Filled'); + expect(baRouter.refresh.mock.calls.length).toBe(2); + expect(baRouter.send.mock.calls.length).toBe(2); + expect(baRouter.cancel.mock.calls.length).toBe(0); + }); + test('Send and only buy order filled', async () => { let i = 1; baRouter.refresh = order => { @@ -443,8 +476,10 @@ describe('Arbitrager', () => { config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; let i = 1; baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0; if (order.side === OrderSide.Sell) { order.status = OrderStatus.Filled; + order.filledSize = 1; } }); config.maxRetryCount = 3; @@ -475,12 +510,127 @@ describe('Arbitrager', () => { expect(baRouter.cancel.mock.calls.length).toBe(2); }); + test('Send and sell order filled and buy order partial filled -> reverse', async () => { + config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0.3; + if (order.side === OrderSide.Sell) { + order.status = OrderStatus.Filled; + order.filledSize = 1; + } + }); + 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 sell order unfilled and buy order partial filled -> reverse', async () => { + config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0.3; + if (order.side === OrderSide.Sell) { + order.filledSize = 0; + } + }); + 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(3); + }); + 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 => { + order.filledSize = 0; + if (order.side === OrderSide.Buy) { + order.status = OrderStatus.Filled; + order.filledSize = 1; + } + }); + 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 buy order filled and sel order partial filled -> reverse', async () => { + config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0.3; if (order.side === OrderSide.Buy) { order.status = OrderStatus.Filled; + order.filledSize = 1; } }); config.maxRetryCount = 3; @@ -511,12 +661,162 @@ describe('Arbitrager', () => { expect(baRouter.cancel.mock.calls.length).toBe(2); }); + test('Send and buy order unfilled and sel order partial filled -> reverse', async () => { + config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0.3; + if (order.side === OrderSide.Buy) { + order.filledSize = 0; + } + }); + 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(3); + }); + + test('Send and both orders partial filled -> reverse', async () => { + config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0.7; + if (order.side === OrderSide.Buy) { + order.filledSize = 0.2; + } + }); + 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(3); + }); + + test('Send and both orders same quantity partial filled -> reverse', async () => { + config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0.8; + if (order.side === OrderSide.Buy) { + order.filledSize = 0.8; + } + }); + 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(2); + }); + + test('Send and both orders unfilled -> reverse', async () => { + config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0; + if (order.side === OrderSide.Buy) { + order.filledSize = 0; + } + }); + 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(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 => { + order.filledSize = 0; if (order.side === OrderSide.Buy) { order.status = OrderStatus.Filled; + order.filledSize = 1; } }; baRouter.refresh = jest @@ -557,8 +857,10 @@ describe('Arbitrager', () => { config.onSingleLeg = { action: 'Reverse', options: { limitMovePercent: 10 } }; let i = 1; baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0; if (order.side === OrderSide.Buy) { order.status = OrderStatus.Filled; + order.filledSize = 1; } }); baRouter.send = jest @@ -599,8 +901,10 @@ describe('Arbitrager', () => { config.onSingleLeg = { action: 'Proceed', options: { limitMovePercent: 10 } }; let i = 1; baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0; if (order.side === OrderSide.Sell) { order.status = OrderStatus.Filled; + order.filledSize = 1; } }); config.maxRetryCount = 3; @@ -635,8 +939,10 @@ describe('Arbitrager', () => { config.onSingleLeg = { action: 'Proceed', options: { limitMovePercent: 10 } }; let i = 1; baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0; if (order.side === OrderSide.Buy) { order.status = OrderStatus.Filled; + order.filledSize = 1; } }); config.maxRetryCount = 3; @@ -667,12 +973,200 @@ describe('Arbitrager', () => { expect(baRouter.cancel.mock.calls.length).toBe(2); }); + test('Send and buy order filled and sell order partial filled -> proceed', async () => { + config.onSingleLeg = { action: 'Proceed', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0.3; + if (order.side === OrderSide.Buy) { + order.status = OrderStatus.Filled; + order.filledSize = 1; + } + }); + 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 buy order unfilled and sell order partial filled -> proceed', async () => { + config.onSingleLeg = { action: 'Proceed', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0.3; + if (order.side === OrderSide.Buy) { + order.filledSize = 0; + } + }); + 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(3); + }); + + test('Send and both orders partial filled -> proceed', async () => { + config.onSingleLeg = { action: 'Proceed', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0.7; + if (order.side === OrderSide.Buy) { + order.filledSize = 0.2; + } + }); + 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(3); + }); + + test('Send and both orders same quantity partial filled -> proceed', async () => { + config.onSingleLeg = { action: 'Proceed', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0.8; + if (order.side === OrderSide.Buy) { + order.filledSize = 0.8; + } + }); + 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(2); + }); + + test('Send and both orders unfilled -> proceed', async () => { + config.onSingleLeg = { action: 'Proceed', options: { limitMovePercent: 10 } }; + let i = 1; + baRouter.refresh = jest.fn().mockImplementation(async order => { + order.filledSize = 0; + if (order.side === OrderSide.Buy) { + order.filledSize = 0; + } + }); + 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(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 => { + order.filledSize = 0; if (order.side === OrderSide.Buy) { order.status = OrderStatus.Filled; + order.filledSize = 1; } }); config.maxRetryCount = 3; diff --git a/src/__tests__/SingleLegHandler.test.ts b/src/__tests__/SingleLegHandler.test.ts index de8b6bba..d9de1f18 100644 --- a/src/__tests__/SingleLegHandler.test.ts +++ b/src/__tests__/SingleLegHandler.test.ts @@ -84,6 +84,42 @@ test('reverse partial fill', async () => { expect(subOrders[0].side).toBe(OrderSide.Sell); }); +test('reverse partial < partial', async () => { + const config = { action: 'Reverse', options: { limitMovePercent: 1, ttl: 1 } }; + const baRouter = { send: jest.fn(), refresh: jest.fn(), cancel: jest.fn() }; + const handler = new SingleLegHandler(baRouter, config); + const buyLeg = new Order('Dummy1', OrderSide.Buy, 0.1, 100, CashMarginType.Cash, OrderType.Limit, 10); + buyLeg.filledSize = 0.01; + buyLeg.status = OrderStatus.PartiallyFilled; + const sellLeg = new Order('Dummy2', OrderSide.Sell, 0.1, 110, CashMarginType.Cash, OrderType.Limit, 10); + sellLeg.filledSize = 0.04; + sellLeg.status = OrderStatus.PartiallyFilled; + const orders = [buyLeg, sellLeg]; + await handler.handle(orders, false); + const sentOrder = baRouter.send.mock.calls[0][0] as Order; + expect(sentOrder.size).toBe(0.03); + expect(sentOrder.broker).toBe('Dummy2'); + expect(sentOrder.side).toBe(OrderSide.Buy); +}); + +test('reverse partial > partial', async () => { + const config = { action: 'Reverse', options: { limitMovePercent: 1, ttl: 1 } }; + const baRouter = { send: jest.fn(), refresh: jest.fn(), cancel: jest.fn() }; + const handler = new SingleLegHandler(baRouter, config); + const buyLeg = new Order('Dummy1', OrderSide.Buy, 0.1, 100, CashMarginType.Cash, OrderType.Limit, 10); + buyLeg.filledSize = 0.07; + buyLeg.status = OrderStatus.PartiallyFilled; + const sellLeg = new Order('Dummy2', OrderSide.Sell, 0.1, 110, CashMarginType.Cash, OrderType.Limit, 10); + sellLeg.filledSize = 0.02; + sellLeg.status = OrderStatus.PartiallyFilled; + const orders = [buyLeg, sellLeg]; + await handler.handle(orders, false); + const sentOrder = baRouter.send.mock.calls[0][0] as Order; + expect(sentOrder.size).toBe(0.05); + expect(sentOrder.broker).toBe('Dummy1'); + expect(sentOrder.side).toBe(OrderSide.Sell); +}); + test('proceed partial fill', async () => { const config = { action: 'Proceed', options: { limitMovePercent: 1, ttl: 1 } }; const baRouter = { send: jest.fn(), refresh: jest.fn(), cancel: jest.fn() }; @@ -104,3 +140,39 @@ test('proceed partial fill', async () => { expect(subOrders[0].broker).toBe('Dummy2'); expect(subOrders[0].side).toBe(OrderSide.Sell); }); + +test('proceed partial < partial', async () => { + const config = { action: 'Proceed', options: { limitMovePercent: 1, ttl: 1 } }; + const baRouter = { send: jest.fn(), refresh: jest.fn(), cancel: jest.fn() }; + const handler = new SingleLegHandler(baRouter, config); + const buyLeg = new Order('Dummy1', OrderSide.Buy, 0.1, 100, CashMarginType.Cash, OrderType.Limit, 10); + buyLeg.filledSize = 0.01; + buyLeg.status = OrderStatus.PartiallyFilled; + const sellLeg = new Order('Dummy2', OrderSide.Sell, 0.1, 110, CashMarginType.Cash, OrderType.Limit, 10); + sellLeg.filledSize = 0.04; + sellLeg.status = OrderStatus.PartiallyFilled; + const orders = [buyLeg, sellLeg]; + await handler.handle(orders, false); + const sentOrder = baRouter.send.mock.calls[0][0] as Order; + expect(sentOrder.size).toBe(0.03); + expect(sentOrder.broker).toBe('Dummy1'); + expect(sentOrder.side).toBe(OrderSide.Buy); +}); + +test('proceed partial > partial', async () => { + const config = { action: 'Proceed', options: { limitMovePercent: 1, ttl: 1 } }; + const baRouter = { send: jest.fn(), refresh: jest.fn(), cancel: jest.fn() }; + const handler = new SingleLegHandler(baRouter, config); + const buyLeg = new Order('Dummy1', OrderSide.Buy, 0.1, 100, CashMarginType.Cash, OrderType.Limit, 10); + buyLeg.filledSize = 0.09; + buyLeg.status = OrderStatus.PartiallyFilled; + const sellLeg = new Order('Dummy2', OrderSide.Sell, 0.1, 110, CashMarginType.Cash, OrderType.Limit, 10); + sellLeg.filledSize = 0.05; + sellLeg.status = OrderStatus.PartiallyFilled; + const orders = [buyLeg, sellLeg]; + await handler.handle(orders, false); + const sentOrder = baRouter.send.mock.calls[0][0] as Order; + expect(sentOrder.size).toBe(0.04); + expect(sentOrder.broker).toBe('Dummy2'); + expect(sentOrder.side).toBe(OrderSide.Sell); +});