Skip to content

Commit

Permalink
support minTargetProfitPercent. improve position limit check
Browse files Browse the repository at this point in the history
  • Loading branch information
bitrinjani committed Nov 6, 2017
1 parent 6d21047 commit 2f65449
Show file tree
Hide file tree
Showing 16 changed files with 187 additions and 43 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
"coverage": "jest --coverage"
},
"dependencies": {
"@types/decimal.js": "^0.0.31",
"@types/i18next": "^8.4.2",
"@types/jsonwebtoken": "^7.2.3",
"date-fns": "^1.28.5",
"decimal.js": "^7.3.0",
"i18next": "^10.0.1",
"inversify": "^4.3.0",
"jsonwebtoken": "^8.1.0",
Expand Down
43 changes: 24 additions & 19 deletions src/ArbitragerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,10 @@ export default class ArbitragerImpl implements Arbitrager {
async stop(): Promise<void> {
this.status = 'Stopping';
if (this.positionService) {
await this.positionService.stop();
await this.positionService.stop();
}
if (this.quoteAggregator) {
await this.quoteAggregator.stop();
}
if (this.quoteAggregator.onQuoteUpdated) {
this.quoteAggregator.onQuoteUpdated = undefined;
}
this.status = 'Stopped';
Expand Down Expand Up @@ -85,12 +83,13 @@ export default class ArbitragerImpl implements Arbitrager {
this.log.info(t('NoArbitrageOpportunitySpreadIsNotInverted'));
return;
}
if (targetVolume < config.minSize) {
this.status = 'Too small volume';
this.log.info(t('TargetVolumeIsSmallerThanMinSize'));
return;
}
if (targetProfit < config.minTargetProfit) {
const minTargetProfit = _.max([
config.minTargetProfit,
config.minTargetProfitPercent !== undefined ?
_.round((config.minTargetProfitPercent / 100) * _.mean([bestAsk.price, bestBid.price]) * targetVolume) :
0
]) as number;
if (targetProfit < minTargetProfit) {
this.status = 'Too small profit';
this.log.info(t('TargetProfitIsSmallerThanMinProfit'));
return;
Expand All @@ -114,12 +113,14 @@ export default class ArbitragerImpl implements Arbitrager {
}

private printSnapshot(result: SpreadAnalysisResult) {
this.log.info('%s: %s', padEnd(t('BestAsk'), 17), result.bestAsk); // TODO: fix double printing
this.log.info('%s: %s', padEnd(t('BestAsk'), 17), result.bestAsk);
this.log.info('%s: %s', padEnd(t('BestBid'), 17), result.bestBid);
this.log.info('%s: %s', padEnd(t('Spread'), 17), -result.invertedSpread);
this.log.info('%s: %s', padEnd(t('AvailableVolume'), 17), result.availableVolume);
this.log.info('%s: %s', padEnd(t('TargetVolume'), 17), result.targetVolume);
this.log.info('%s: %s', padEnd(t('ExpectedProfit'), 17), result.targetProfit);
const midNotional = _.mean([result.bestAsk.price, result.bestBid.price]) * result.targetVolume;
const profitPercentAgainstNotional = _.round((result.targetProfit / midNotional) * 100, 2);
this.log.info('%s: %s (%s%%)', padEnd(t('ExpectedProfit'), 17), result.targetProfit, profitPercentAgainstNotional);
}

private isMaxExposureBreached(): boolean {
Expand All @@ -144,38 +145,42 @@ export default class ArbitragerImpl implements Arbitrager {
this.log.debug(ex.stack);
}

if (buyOrder.status !== OrderStatus.Filled) {
this.log.warn(t('BuyLegIsNotFilledYetPendingSizeIs'), sellOrder.pendingSize);
if (!this.isFilled(buyOrder)) {
this.log.warn(t('BuyLegIsNotFilledYetPendingSizeIs'), buyOrder.pendingSize);
}
if (sellOrder.status !== OrderStatus.Filled) {
if (!this.isFilled(sellOrder)) {
this.log.warn(t('SellLegIsNotFilledYetPendingSizeIs'), sellOrder.pendingSize);
}

if (buyOrder.status === OrderStatus.Filled && sellOrder.status === OrderStatus.Filled) {
if (this.isFilled(buyOrder) && this.isFilled(sellOrder)) {
this.status = 'Filled';
const profit = _.round(sellOrder.filledSize * sellOrder.averageFilledPrice -
buyOrder.filledSize * buyOrder.averageFilledPrice);
this.log.info(t('BothLegsAreSuccessfullyFilled'));
this.log.info(t('BuyFillPriceIs'), buyOrder.averageFilledPrice);
this.log.info(t('SellFillPriceIs'), sellOrder.averageFilledPrice);
this.log.info(t('BuyFillPriceIs'), _.round(buyOrder.averageFilledPrice));
this.log.info(t('SellFillPriceIs'), _.round(sellOrder.averageFilledPrice));
this.log.info(t('ProfitIs'), profit);
break;
}

if (i === config.maxRetryCount) {
this.status = 'MaxRetryCount breached';
this.log.warn(t('MaxRetryCountReachedCancellingThePendingOrders'));
if (buyOrder.status !== OrderStatus.Filled) {
if (!this.isFilled(buyOrder)) {
await this.brokerAdapterRouter.cancel(buyOrder);
}
if (sellOrder.status !== OrderStatus.Filled) {
if (!this.isFilled(sellOrder)) {
await this.brokerAdapterRouter.cancel(sellOrder);
}
break;
}
}
}

private isFilled(order: Order): boolean {
return order.status === OrderStatus.Filled;
}

private async quoteUpdated(quotes: Quote[]): Promise<void> {
this.positionService.print();
this.log.info(hr(20) + 'ARBITRAGER' + hr(20));
Expand Down
4 changes: 2 additions & 2 deletions src/BrokerPosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { padStart, padEnd } from './util';

export default class BrokerPosition {
broker: Broker;
get longAllowed(): boolean { return this.allowedLongSize > 0; }
get shortAllowed(): boolean { return this.allowedShortSize > 0; }
longAllowed: boolean;
shortAllowed: boolean;
btc: number;
allowedLongSize: number;
allowedShortSize: number;
Expand Down
2 changes: 1 addition & 1 deletion src/ConfigValidatorImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default class ConfigValidatorImpl implements ConfigValidator {
this.mustBeGreaterThanZero(config.maxSize, 'maxSize');
this.mustBeGreaterThanZero(config.minSize, 'minSize');
this.throwIf(config.minSize < 0.01, 'minSize must be greater than 0.01.');
this.mustBePositive(config.minTargetProfit, 'minTargetProfit');
this.mustBeGreaterThanZero(config.minTargetProfit, 'minTargetProfit');
this.mustBePositive(config.orderStatusCheckInterval, 'orderStatusCheckInterval');
this.mustBePositive(config.positionRefreshInterval, 'positionRefreshInterval');
this.mustBePositive(config.priceMergeSize, 'priceMergeSize');
Expand Down
16 changes: 11 additions & 5 deletions src/PositionServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
} from './type';
import { getLogger } from './logger';
import * as _ from 'lodash';
// tslint:disable-next-line:import-name
import Decimal from 'decimal.js';
import BrokerPosition from './BrokerPosition';
import { hr, eRound } from './util';
import symbols from './symbols';
Expand Down Expand Up @@ -71,13 +73,17 @@ export default class PositionServiceImpl implements PositionService {
const promises = brokerConfigs
.map(async (brokerConfig: BrokerConfig): Promise<BrokerPosition> => {
const currentBtc = await this.brokerAdapterRouter.getBtcPosition(brokerConfig.broker);
const allowedLongSize = _.max([0, brokerConfig.maxLongPosition - currentBtc]) as number;
const allowedShortSize = _.max([0, currentBtc + brokerConfig.maxShortPosition]) as number;
const allowedLongSize =
_.max([0, new Decimal(brokerConfig.maxLongPosition).minus(currentBtc).toNumber()]) as number;
const allowedShortSize =
_.max([0, new Decimal(brokerConfig.maxShortPosition).plus(currentBtc).toNumber()]) as number;
const pos = new BrokerPosition();
pos.broker = brokerConfig.broker;
pos.btc = eRound(currentBtc);
pos.allowedLongSize = eRound(allowedLongSize);
pos.allowedShortSize = eRound(allowedShortSize);
pos.btc = currentBtc;
pos.allowedLongSize = allowedLongSize;
pos.allowedShortSize = allowedShortSize;
pos.longAllowed = new Decimal(allowedLongSize).gte(config.minSize);
pos.shortAllowed = new Decimal(allowedShortSize).gte(config.minSize);
return pos;
});
this._positionMap = _(await Promise.all(promises)).map(p => [p.broker, p]).fromPairs().value();
Expand Down
4 changes: 3 additions & 1 deletion src/Quoine/BrokerAdapterImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import Quote from '../Quote';
import { PriceLevelsResponse, SendOrderRequest, OrdersResponse } from './type';
import Execution from '../Execution';
import { timestampToDate } from '../util';
// tslint:disable-next-line:import-name
import Decimal from 'decimal.js';

namespace Quoine {
@injectable()
Expand Down Expand Up @@ -124,7 +126,7 @@ namespace Quoine {
order.brokerOrderId = ordersResponse.id.toString();
order.filledSize = Number(ordersResponse.filled_quantity);
order.creationTime = timestampToDate(ordersResponse.created_at);
if (order.filledSize === order.size) {
if (new Decimal(order.filledSize).eq(order.size)) {
order.status = OrderStatus.Filled;
} else if (order.filledSize > 0) {
order.status = OrderStatus.PartiallyFilled;
Expand Down
18 changes: 10 additions & 8 deletions src/SpreadAnalyzerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import Quote from './Quote';
import intl from './intl';
import BrokerPosition from './BrokerPosition';
import symbols from './symbols';
// tslint:disable-next-line:import-name
import Decimal from 'decimal.js';

const t = s => intl.t(s);
@injectable()
Expand All @@ -24,13 +26,13 @@ export default class SpreadAnalyzerImpl implements SpreadAnalyzer {
if (_.values(positionMap).length === 0) {
throw new Error('Position map is empty.');
}

const bestAsk = _(quotes).filter(q => q.side === QuoteSide.Ask)
.filter(q => this.isAllowed(q, positionMap[q.broker]))
.orderBy(['price']).first();
const bestBid = _(quotes).filter(q => q.side === QuoteSide.Bid)
.filter(q => this.isAllowed(q, positionMap[q.broker]))
.orderBy(['price'], ['desc']).first();
const filteredQuotes = _(quotes)
.filter(q => this.isAllowedByCurrentPosition(q, positionMap[q.broker]))
.filter(q => new Decimal(q.volume).gte(config.minSize))
.orderBy(['price'])
.value();
const bestAsk = _(filteredQuotes).filter(q => q.side === QuoteSide.Ask).first();
const bestBid = _(filteredQuotes).filter(q => q.side === QuoteSide.Bid).last();
if (bestBid === undefined) {
throw new Error(t('NoBestBidWasFound'));
} else if (bestAsk === undefined) {
Expand Down Expand Up @@ -73,7 +75,7 @@ export default class SpreadAnalyzerImpl implements SpreadAnalyzer {
return commissions.sum();
}

private isAllowed(q: Quote, pos: BrokerPosition): boolean {
private isAllowedByCurrentPosition(q: Quote, pos: BrokerPosition): boolean {
return q.side === QuoteSide.Bid ? pos.shortAllowed : pos.longAllowed;
}
}
39 changes: 34 additions & 5 deletions src/__tests__/ArbitragerImpl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ describe('Arbitrager', () => {
expect(arbitrager.status).toBe('Stopped');
});

test('stop without start', async () => {
const arbitrager = new ArbitragerImpl();
await arbitrager.stop();
expect(arbitrager.status).toBe('Stopped');
});

test('positionService is not ready', async () => {
const arbitrager = new ArbitragerImpl(quoteAggregator, configStore,
positionService, baRouter, spreadAnalyzer);
Expand Down Expand Up @@ -138,9 +144,31 @@ describe('Arbitrager', () => {
expect(baRouter.send).not.toBeCalled();
expect(arbitrager.status).toBe('Spread not inverted');
});

test('Too small profit', async () => {
config.minTargetProfit = 1000;
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);
positionService.isStarted = true;
await arbitrager.start();
await quoteAggregator.onQuoteUpdated([]);
expect(baRouter.send).not.toBeCalled();
expect(arbitrager.status).toBe('Too small profit');
});

test('Too small volume', async () => {
config.minSize = 2;
test('Too small profit by minTargetProfitPercent', async () => {
config.minTargetProfit = 0;
config.minTargetProfitPercent = 18.4;
spreadAnalyzer.analyze.mockImplementation(() => {
return {
bestBid: new Quote(Broker.Quoine, QuoteSide.Bid, 600, 4),
Expand All @@ -157,11 +185,12 @@ describe('Arbitrager', () => {
await arbitrager.start();
await quoteAggregator.onQuoteUpdated([]);
expect(baRouter.send).not.toBeCalled();
expect(arbitrager.status).toBe('Too small volume');
expect(arbitrager.status).toBe('Too small profit');
});

test('Too small profit', async () => {
config.minTargetProfit = 1000;
test('Too small profit by minTargetProfit', async () => {
config.minTargetProfit = 101;
config.minTargetProfitPercent = 10;
spreadAnalyzer.analyze.mockImplementation(() => {
return {
bestBid: new Quote(Broker.Quoine, QuoteSide.Bid, 600, 4),
Expand Down
2 changes: 2 additions & 0 deletions src/__tests__/BrokerPosition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ describe('BrokerPosition', () => {
target.btc = 0.01;
target.allowedLongSize = 0.05;
target.allowedShortSize = 0;
target.longAllowed = true;
target.shortAllowed = false;
expect(target.toString()).toBe('Coincheck : 0.01 BTC, LongAllowed: OK, ShortAllowed: NG');
});
});
2 changes: 1 addition & 1 deletion src/__tests__/Order.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ describe('Order', () => {
ex2.size = 0.006;
target.executions.push(ex1);
target.executions.push(ex2);
expect(target.averageFilledPrice).toBeCloseTo(1160);
expect(target.averageFilledPrice).toBe(1160);
});
});
41 changes: 40 additions & 1 deletion src/__tests__/PositionServiceImpl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import * as _ from 'lodash';
import { delay } from '../util';

const config = {
minSize: 0.01,
positionRefreshInterval: 5000,
brokers: [{
broker: Broker.Quoine,
enabled: true,
maxLongPosition: 0.3,
maxShortPosition: 0.3
maxShortPosition: 0
}, {
broker: Broker.Coincheck,
enabled: true,
Expand Down Expand Up @@ -41,6 +42,39 @@ describe('Position Service', () => {
expect(ccPos.shortAllowed).toBe(false);
expect(ccPos.allowedLongSize).toBe(1.3);
expect(ccPos.allowedShortSize).toBe(0);
const qPos = _.find(positions, x => x.broker === Broker.Quoine);
expect(qPos.btc).toBe(0.2);
expect(qPos.longAllowed).toBe(true);
expect(qPos.shortAllowed).toBe(true);
expect(qPos.allowedLongSize).toBe(0.1);
expect(qPos.allowedShortSize).toBe(0.2);
});

test('positions smaller than minSize', async () => {
const baRouter = {
getBtcPosition: broker => broker === Broker.Quoine ? 0.000002 : -0.3
};
const ps = new PositionServiceImpl(configStore, baRouter);
ps.print();
await ps.start();
const positions = _.values(ps.positionMap);
const exposure = ps.netExposure;
ps.print();
await ps.stop();
const ccPos = _.find(positions, x => x.broker === Broker.Coincheck);
expect(positions.length).toBe(2);
expect(exposure).toBe(-0.299998);
expect(ccPos.btc).toBe(-0.3);
expect(ccPos.longAllowed).toBe(true);
expect(ccPos.shortAllowed).toBe(false);
expect(ccPos.allowedLongSize).toBe(1.3);
expect(ccPos.allowedShortSize).toBe(0);
const qPos = _.find(positions, x => x.broker === Broker.Quoine);
expect(qPos.btc).toBe(0.000002);
expect(qPos.longAllowed).toBe(true);
expect(qPos.shortAllowed).toBe(false);
expect(qPos.allowedLongSize).toBe(0.299998);
expect(qPos.allowedShortSize).toBe(0.000002);
});

test('already refreshing block', async () => {
Expand All @@ -61,4 +95,9 @@ describe('Position Service', () => {
const positions = _.values(ps.positionMap);
expect(positions.length).toBe(2);
});

test('stop without start', async () => {
const ps = new PositionServiceImpl(configStore, baRouter);
ps.stop();
};
});
Loading

0 comments on commit 2f65449

Please sign in to comment.