Skip to content

Commit

Permalink
Merge pull request #25 from bitrinjani/feature-singleleg-handling
Browse files Browse the repository at this point in the history
Feature singleleg handling
  • Loading branch information
bitrinjani committed Dec 26, 2017
2 parents 3b01e15 + 90d636a commit 5654da5
Show file tree
Hide file tree
Showing 40 changed files with 875 additions and 118 deletions.
113 changes: 99 additions & 14 deletions src/ArbitragerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,23 @@ import {
QuoteSide,
OrderSide,
LimitCheckerFactory,
OrderPair
OrderPair,
ReverseOption,
ProceedOption,
OnSingleLegConfig
} 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,
Expand All @@ -34,7 +39,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';

Expand All @@ -57,6 +62,10 @@ export default class ArbitragerImpl implements Arbitrager {
}

private async quoteUpdated(quotes: Quote[]): Promise<void> {
if (this.shouldStop) {
await this.stop();
return;
}
this.positionService.print();
this.log.info(hr(20) + 'ARBITRAGER' + hr(20));
await this.arbitrage(quotes);
Expand Down Expand Up @@ -90,9 +99,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;
}

Expand All @@ -111,12 +118,16 @@ 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);
}

private async checkOrderState(orders: OrderPair, exitFlag: boolean = false): Promise<void> {
private async checkOrderState(orders: OrderPair, exitFlag: boolean): Promise<void> {
const { config } = this.configStore;
for (const i of _.range(1, config.maxRetryCount + 1)) {
await delay(config.orderStatusCheckInterval);
Expand Down Expand Up @@ -156,6 +167,10 @@ 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) {
const onSingleLegConfig = config.onSingleLeg;
await this.handleSingleLeg(orders, onSingleLegConfig, exitFlag);
}
break;
}
}
Expand Down Expand Up @@ -216,20 +231,92 @@ export default class ArbitragerImpl implements Arbitrager {
return order;
}

private async handleSingleLeg(orders: OrderPair, onSingleLegConfig: OnSingleLegConfig, exitFlag: Boolean) {
if (onSingleLegConfig === undefined) {
return;
}
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);
return;
case 'Proceed':
await this.proceedLeg(orders, options as ProceedOption);
return;
default:
throw new Error('Invalid action.');
}
}

private async reverseLeg(orders: OrderPair, options: ReverseOption) {
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(t`ReverseFilledLeg`, filledLeg.toShortString(), price.toLocaleString());
const reversalOrder = new Order(
filledLeg.broker,
filledLeg.side === OrderSide.Buy ? OrderSide.Sell : OrderSide.Buy,
filledLeg.size,
price,
filledLeg.cashMarginType,
OrderType.Limit,
filledLeg.leverageLevel
);
await this.sendOrderWithTtl(reversalOrder, options.ttl);
}

private async proceedLeg(orders: OrderPair, options: ProceedOption) {
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.toShortString(), price.toLocaleString());
const revisedOrder = new Order(
unfilledLeg.broker,
unfilledLeg.side,
unfilledLeg.size,
price,
unfilledLeg.cashMarginType,
OrderType.Limit,
unfilledLeg.leverageLevel
);
await this.sendOrderWithTtl(revisedOrder, options.ttl);
}

private async sendOrderWithTtl(order: Order, ttl: number) {
try {
this.log.info(t`SendingOrderTtl`, ttl);
await this.brokerAdapterRouter.send(order);
await delay(ttl);
await this.brokerAdapterRouter.refresh(order);
if (order.filled) {
this.log.info(`${order.toExecSummary()}`);
} else {
this.log.info(t`NotFilledTtl`, ttl);
await this.brokerAdapterRouter.cancel(order);
}
} catch (ex) {
this.log.warn(ex.message);
}
}

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());
}
});
}

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);
Expand All @@ -247,9 +334,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()}]`);
});
}
}
} /* istanbul ignore next */
2 changes: 1 addition & 1 deletion src/Bitflyer/BrokerAdapterImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,4 @@ export default class BrokerAdapterImpl implements BrokerAdapter {
.value();
return _.concat(asks, bids);
}
}
} /* istanbul ignore next */
2 changes: 1 addition & 1 deletion src/Coincheck/BrokerAdapterImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
} /* istanbul ignore next */
4 changes: 2 additions & 2 deletions src/JsonConfigStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export default class JsonConfigStore implements ConfigStore {
) {
this._config = getConfigRoot();
configValidator.validate(this._config);
}
}

get config(): ConfigRoot {
return this._config;
}
}
} /* istanbul ignore next */
4 changes: 2 additions & 2 deletions src/LimitCheckerFactoryImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
} /* istanbul ignore next */
12 changes: 6 additions & 6 deletions src/LimitCheckerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default class LimitCheckerImpl implements LimitChecker {
return result;
}
}
return { success: true };
return { success: true, reason: '' };
}
}

Expand All @@ -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`);
Expand All @@ -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`);
Expand All @@ -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`);
Expand Down Expand Up @@ -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`);
Expand Down Expand Up @@ -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`);
Expand Down
6 changes: 5 additions & 1 deletion src/Order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/PositionServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,4 @@ export default class PositionServiceImpl implements PositionService {
pos.shortAllowed = new Decimal(allowedShortSize).gte(minSize);
return pos;
}
}
} /* istanbul ignore next */
2 changes: 1 addition & 1 deletion src/Quoine/BrokerAdapterImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,4 @@ export default class BrokerAdapterImpl implements BrokerAdapter {
.value();
return _.concat(asks, bids);
}
}
} /* istanbul ignore next */
2 changes: 1 addition & 1 deletion src/QuoteAggregatorImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,4 @@ export default class QuoteAggregatorImpl implements QuoteAggregator {
)
.value();
}
}
} /* istanbul ignore next */
2 changes: 1 addition & 1 deletion src/SpreadAnalyzerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
2 changes: 2 additions & 0 deletions src/__tests__/AppRoot.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down

0 comments on commit 5654da5

Please sign in to comment.