Skip to content

Commit

Permalink
Merge 2ecc1f3 into 9b423eb
Browse files Browse the repository at this point in the history
  • Loading branch information
bitrinjani committed Dec 19, 2017
2 parents 9b423eb + 2ecc1f3 commit cd3d07a
Show file tree
Hide file tree
Showing 15 changed files with 1,107 additions and 468 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "r2",
"version": "2.0.0",
"version": "2.1.0",
"main": "index.js",
"license": "MIT",
"scripts": {
Expand Down Expand Up @@ -39,10 +39,10 @@
"@types/uuid": "^3.4.2",
"coveralls": "^3.0.0",
"cpy-cli": "^1.0.1",
"jest": "^21.2.1",
"jest": "^22.0.1",
"nock": "^9.0.24",
"rimraf": "^2.6.2",
"ts-jest": "^21.1.2",
"ts-jest": "^22.0.0",
"ts-node": "^3.3.0",
"tslint": "^5.8.0",
"tslint-config-airbnb": "^5.3.0",
Expand Down
186 changes: 134 additions & 52 deletions src/ArbitragerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,18 @@ import { injectable, inject } from 'inversify';
import * as _ from 'lodash';
import Order from './Order';
import {
BrokerAdapterRouter, ConfigStore, PositionService,
QuoteAggregator, SpreadAnalyzer, Arbitrager, SpreadAnalysisResult,
OrderType, QuoteSide, OrderSide, LimitCheckerFactory
BrokerAdapterRouter,
ConfigStore,
PositionService,
QuoteAggregator,
SpreadAnalyzer,
Arbitrager,
SpreadAnalysisResult,
OrderType,
QuoteSide,
OrderSide,
LimitCheckerFactory,
OrderPair
} from './types';
import t from './intl';
import { padEnd, hr, delay, calculateCommission, findBrokerConfig } from './util';
Expand All @@ -15,6 +24,8 @@ import symbols from './symbols';
@injectable()
export default class ArbitragerImpl implements Arbitrager {
private readonly log = getLogger(this.constructor.name);
private activePairs: OrderPair[] = [];
private lastSpreadAnalysisResult: SpreadAnalysisResult;

constructor(
@inject(symbols.QuoteAggregator) private readonly quoteAggregator: QuoteAggregator,
Expand All @@ -23,7 +34,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 @@ -45,35 +56,57 @@ export default class ArbitragerImpl implements Arbitrager {
this.status = 'Stopped';
}

private async quoteUpdated(quotes: Quote[]): Promise<void> {
this.positionService.print();
this.log.info(hr(20) + 'ARBITRAGER' + hr(20));
await this.arbitrage(quotes);
this.log.info(hr(50));
}

private async arbitrage(quotes: Quote[]): Promise<void> {
this.status = 'Arbitraging';
this.log.info(t`LookingForOpportunity`);
const { config } = this.configStore;

const exitFlag = await this.findClosable(quotes);
let spreadAnalysisResult: SpreadAnalysisResult;
try {
spreadAnalysisResult = await this.spreadAnalyzer.analyze(quotes, this.positionService.positionMap);
} catch (ex) {
this.status = 'Spread analysis failed';
this.log.warn(t`FailedToGetASpreadAnalysisResult`, ex.message);
this.log.debug(ex.stack);
return;
if (exitFlag) {
spreadAnalysisResult = this.lastSpreadAnalysisResult;
} else {
try {
spreadAnalysisResult = await this.spreadAnalyzer.analyze(quotes, this.positionService.positionMap);
} catch (ex) {
this.status = 'Spread analysis failed';
this.log.warn(t`FailedToGetASpreadAnalysisResult`, ex.message);
this.log.debug(ex.stack);
return;
}
}
this.printSpreadAnalysisResult(spreadAnalysisResult);
const limitChecker = this.limitCheckerFactory.create(spreadAnalysisResult);

if (!exitFlag) {
this.printSpreadAnalysisResult(spreadAnalysisResult);
}

const limitChecker = this.limitCheckerFactory.create(spreadAnalysisResult, exitFlag);
const limitCheckResult = limitChecker.check();
if (!limitCheckResult.success) {
if (limitCheckResult.reason) {
this.status = limitCheckResult.reason;
}
return;
}
this.log.info(t`FoundArbitrageOppotunity`);

if (exitFlag) {
this.log.info(t`FoundClosableOrders`);
} else {
this.log.info(t`FoundArbitrageOppotunity`);
}
try {
const { bestBid, bestAsk, targetVolume } = spreadAnalysisResult;
const sendTasks = [bestAsk, bestBid].map(q => this.sendOrder(q, targetVolume, OrderType.Limit));
const orders = await Promise.all(sendTasks);
const orders = (await Promise.all(sendTasks)) as OrderPair;
this.status = 'Sent';
await this.checkOrderState(orders);
await this.checkOrderState(orders, exitFlag);
} catch (ex) {
this.log.error(ex.message);
this.log.debug(ex.stack);
Expand All @@ -83,22 +116,7 @@ export default class ArbitragerImpl implements Arbitrager {
await delay(config.sleepAfterSend);
}

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`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);
this.log.info(
'%s: %s (%s%%)',
padEnd(t`ExpectedProfit`, columnWidth),
result.targetProfit,
result.profitPercentAgainstNotional
);
}

private async checkOrderState(orders: Order[]): Promise<void> {
private async checkOrderState(orders: OrderPair, exitFlag: boolean = false): Promise<void> {
const { config } = this.configStore;
for (const i of _.range(1, config.maxRetryCount + 1)) {
await delay(config.orderStatusCheckInterval);
Expand All @@ -115,7 +133,12 @@ export default class ArbitragerImpl implements Arbitrager {
this.printOrderSummary(orders);

if (orders.every(o => o.filled)) {
this.status = 'Filled';
if (exitFlag) {
this.status = 'Closed';
} else {
this.status = 'Filled';
this.activePairs.push(orders);
}
const commission = _(orders).sumBy(o => this.calculateFilledOrderCommission(o));
const profit = _.round(
_(orders).sumBy(o => (o.side === OrderSide.Sell ? 1 : -1) * o.filledNotional) - commission
Expand All @@ -138,36 +161,95 @@ export default class ArbitragerImpl implements Arbitrager {
}
}

private printOrderSummary(orders: Order[]) {
orders.forEach((o) => {
if (o.filled) {
this.log.info(o.toSummary());
} else {
this.log.warn(o.toSummary());
}
});
}

private calculateFilledOrderCommission(order: Order): number {
const brokerConfig = findBrokerConfig(this.configStore.config, order.broker);
return calculateCommission(order.averageFilledPrice, order.filledSize, brokerConfig.commissionPercent);
}

private async quoteUpdated(quotes: Quote[]): Promise<void> {
this.positionService.print();
this.log.info(hr(20) + 'ARBITRAGER' + hr(20));
await this.arbitrage(quotes);
this.log.info(hr(50));
private async findClosable(quotes: Quote[]): Promise<boolean> {
const { minExitTargetProfit, minExitTargetProfitPercent } = this.configStore.config;
if (minExitTargetProfit === undefined && minExitTargetProfitPercent === undefined) {
return false;
}
this.printActivePairs();
for (const pair of this.activePairs.slice().reverse()) {
try {
this.log.debug(`Analyzing pair: ${pair}...`);
const result = await this.spreadAnalyzer.analyze(quotes, this.positionService.positionMap, pair);
this.log.debug(`pair: ${pair}, result: ${JSON.stringify(result)}.`);
const { bestBid, bestAsk, targetVolume, targetProfit } = result;
const targetVolumeNotional = _.mean([bestAsk.price, bestBid.price]) * targetVolume;
const effectiveMinExitTargetProfit = _.max([
minExitTargetProfit,
minExitTargetProfitPercent !== undefined
? _.round(minExitTargetProfitPercent / 100 * targetVolumeNotional)
: Number.MIN_SAFE_INTEGER
]) as number;
this.log.debug(`effectiveMinExitTargetProfit: ${effectiveMinExitTargetProfit}`);
if (targetProfit >= effectiveMinExitTargetProfit) {
this.activePairs = _.without(this.activePairs, pair);
this.lastSpreadAnalysisResult = result;
return true;
}
} catch (ex) {
this.log.debug(ex.message);
}
}
return false;
}

private async sendOrder(quote: Quote, targetVolume: number, orderType: OrderType): Promise<Order> {
this.log.info(t`SendingOrderTargettingQuote`, quote);
const brokerConfig = findBrokerConfig(this.configStore.config, quote.broker);
const { cashMarginType, leverageLevel } = brokerConfig;
const { cashMarginType, leverageLevel } = brokerConfig;
const orderSide = quote.side === QuoteSide.Ask ? OrderSide.Buy : OrderSide.Sell;
const order = new Order(quote.broker, orderSide, targetVolume,
quote.price, cashMarginType, orderType, leverageLevel);
const order = new Order(
quote.broker,
orderSide,
targetVolume,
quote.price,
cashMarginType,
orderType,
leverageLevel
);
await this.brokerAdapterRouter.send(order);
return order;
}
}

private printOrderSummary(orders: Order[]) {
orders.forEach(o => {
if (o.filled) {
this.log.info(o.toSummary());
} else {
this.log.warn(o.toSummary());
}
});
}

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`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);
this.log.info(
'%s: %s (%s%%)',
padEnd(t`ExpectedProfit`, columnWidth),
result.targetProfit,
result.profitPercentAgainstNotional
);
}

private printActivePairs(): void {
if (this.activePairs.length === 0) {
return;
}
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}]`
);
});
}
}
8 changes: 8 additions & 0 deletions src/Coincheck/BrokerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import {
LeveragePosition, OrderBooksResponse, NewOrderRequest, NewOrderResponse,
CancelOrderResponse, OpenOrdersResponse, TransactionsResponse, Pagination, Transaction, LeverageBalanceResponse
} from './types';
import { setTimeout } from 'timers';

export default class BrokerApi {
private static CACHE_MS = 1000;
private leveragePositionsCache?: LeveragePosition[];
private readonly baseUrl = 'https://coincheck.com';
private readonly webClient: WebClient = new WebClient(this.baseUrl);

Expand Down Expand Up @@ -36,6 +39,9 @@ export default class BrokerApi {
}

async getAllOpenLeveragePositions(limit: number = 20): Promise<LeveragePosition[]> {
if (this.leveragePositionsCache) {
return _.cloneDeep(this.leveragePositionsCache);
}
let result: LeveragePosition[] = [];
const request: LeveragePositionsRequest = { limit, status: 'open', order: 'desc' };
let reply = await this.getLeveragePositions(request);
Expand All @@ -45,6 +51,8 @@ export default class BrokerApi {
const last = _.last(reply.data) as LeveragePosition;
reply = await this.getLeveragePositions({ ...request, starting_after: last.id });
}
this.leveragePositionsCache = result;
setTimeout(() => this.leveragePositionsCache = undefined, BrokerApi.CACHE_MS);
return result;
}

Expand Down
4 changes: 2 additions & 2 deletions src/LimitCheckerFactoryImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default class LimitCheckerFactoryImpl implements LimitCheckerFactory {
@inject(symbols.PositionService) private readonly positionService: PositionService
) { }

create(spreadAnalysisResult: SpreadAnalysisResult): LimitChecker {
return new LimitCheckerImpl(this.configStore, this.positionService, spreadAnalysisResult);
create(spreadAnalysisResult: SpreadAnalysisResult, exitFlag: boolean): LimitChecker {
return new LimitCheckerImpl(this.configStore, this.positionService, spreadAnalysisResult, exitFlag);
}
}
Loading

0 comments on commit cd3d07a

Please sign in to comment.