Skip to content

Commit

Permalink
add noTradePeriods config - #39
Browse files Browse the repository at this point in the history
  • Loading branch information
bitrinjani committed Jan 11, 2018
1 parent 99f7ae4 commit 5eae8c4
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 6 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"inversify": "^4.3.0",
"jsonwebtoken": "^8.1.0",
"lodash": "^4.17.4",
"luxon": "^0.2.12",
"mkdirp": "^0.5.1",
"node-fetch": "^1.7.3",
"pino": "^4.10.2",
Expand All @@ -41,6 +42,7 @@
"@types/jest": "^21.1.4",
"@types/jsonwebtoken": "^7.2.3",
"@types/lodash": "^4.14.86",
"@types/luxon": "^0.2.2",
"@types/nock": "^8.2.1",
"@types/node": "^8.0.53",
"@types/node-fetch": "^1.6.7",
Expand Down
22 changes: 20 additions & 2 deletions src/QuoteAggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import * as _ from 'lodash';
import symbols from './symbols';
import QuoteImpl from './QuoteImpl';
import BrokerAdapterRouter from './BrokerAdapterRouter';
import { DateTime, Interval } from 'luxon';

@injectable()
export default class QuoteAggregator {
export default class QuoteAggregator {
private readonly log = getLogger(this.constructor.name);
private timer;
private isRunning: boolean;
Expand Down Expand Up @@ -71,11 +72,28 @@ export default class QuoteAggregator {

private getEnabledBrokers(): Broker[] {
return _(this.configStore.config.brokers)
.filter((b: BrokerConfig) => b.enabled)
.filter(b => b.enabled)
.filter(b => this.timeFilter(b))
.map(b => b.broker)
.value();
}

private timeFilter(brokerConfig: BrokerConfig): boolean {
if (_.isEmpty(brokerConfig.noTradePeriods)) {
return true;
}
const current = DateTime.local();
const outOfPeriod = period => {
const interval = Interval.fromISO(`${period[0]}/${period[1]}`);
if (!interval.isValid) {
this.log.warn('Invalid noTradePeriods. Ignoring the config.');
return true;
}
return !interval.contains(current);
};
return brokerConfig.noTradePeriods.every(outOfPeriod);
}

private fold(quotes: Quote[], step: number): Quote[] {
return _(quotes)
.groupBy((q: Quote) => {
Expand Down
134 changes: 132 additions & 2 deletions src/__tests__/QuoteAggregatorImpl.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'reflect-metadata';
import QuoteAggregator from '../QuoteAggregator';
import { Broker, QuoteSide, QuoteAggregator } from '../types';
import { Broker, QuoteSide } from '../types';
import * as _ from 'lodash';
import { delay } from '../util';
import BrokerAdapterRouter from '../BrokerAdapterRouter';
import { options } from '../logger';
import { DateTime } from 'luxon';
options.enabled = false;

const config = {
Expand Down Expand Up @@ -57,13 +58,139 @@ describe('Quote Aggregator', () => {
const baList = [bitflyerBa, coincheckBa, quoineBa];
const baRouter = new BrokerAdapterRouter(baList);
const aggregator: QuoteAggregator = new QuoteAggregator(configStore, baRouter);
const mustBeCalled = jest.fn();
aggregator.onQuoteUpdated = async quotes => {
expect(quotes.length).toBe(3);
console.log(quotes[0].toString());
expect(quotes[0].toString()).toBe('Bitflyer Ask 500,000 0.1');
mustBeCalled();
};
await aggregator.start();
await delay(0);
await aggregator.stop();
expect(mustBeCalled.mock.calls.length).toBeGreaterThanOrEqual(1);
});

test('folding with noTradePeriods', async () => {
configStore.config.iterationInterval = 10;
const current = DateTime.local();
const start = current.minus({ minutes: 5 });
const end = current.plus({ minutes: 5 });
configStore.config.brokers[0].noTradePeriods = [
[start.toLocaleString(DateTime.TIME_24_SIMPLE), end.toLocaleString(DateTime.TIME_24_SIMPLE)]
];
const bitflyerBa = {
broker: 'Bitflyer',
fetchQuotes: () =>
Promise.resolve([
{ broker: 'Bitflyer', side: QuoteSide.Ask, price: 500000, volume: 0.1 },
{ broker: 'Bitflyer', side: QuoteSide.Ask, price: 500001, volume: 0.1 }
])
};
const coincheckBa = {
broker: 'Coincheck',
fetchQuotes: () =>
Promise.resolve([
{ broker: 'Coincheck', side: QuoteSide.Bid, price: 490001, volume: 0.02 },
{ broker: 'Coincheck', side: QuoteSide.Bid, price: 490000, volume: 0.2 }
])
};
const quoineBa = {
broker: 'Quoine',
fetchQuotes: () => Promise.resolve([])
};
const baList = [bitflyerBa, coincheckBa, quoineBa];
const baRouter = new BrokerAdapterRouter(baList);
const aggregator: QuoteAggregator = new QuoteAggregator(configStore, baRouter);
const mustBeCalled = jest.fn();
aggregator.onQuoteUpdated = async quotes => {
expect(quotes.length).toBe(1);
mustBeCalled();
};
await aggregator.start();
await delay(0);
await aggregator.stop();
expect(mustBeCalled.mock.calls.length).toBeGreaterThanOrEqual(1);
});

test('folding with noTradePeriods -> no matching period', async () => {
configStore.config.iterationInterval = 10;
const current = DateTime.local();
const start = current.plus({ minutes: 5 });
const end = current.plus({ minutes: 15 });
configStore.config.brokers[0].noTradePeriods = [
[start.toLocaleString(DateTime.TIME_24_SIMPLE), end.toLocaleString(DateTime.TIME_24_SIMPLE)]
];
const bitflyerBa = {
broker: 'Bitflyer',
fetchQuotes: () =>
Promise.resolve([
{ broker: 'Bitflyer', side: QuoteSide.Ask, price: 500000, volume: 0.1 },
{ broker: 'Bitflyer', side: QuoteSide.Ask, price: 500001, volume: 0.1 }
])
};
const coincheckBa = {
broker: 'Coincheck',
fetchQuotes: () =>
Promise.resolve([
{ broker: 'Coincheck', side: QuoteSide.Bid, price: 490001, volume: 0.02 },
{ broker: 'Coincheck', side: QuoteSide.Bid, price: 490000, volume: 0.2 }
])
};
const quoineBa = {
broker: 'Quoine',
fetchQuotes: () => Promise.resolve([])
};
const baList = [bitflyerBa, coincheckBa, quoineBa];
const baRouter = new BrokerAdapterRouter(baList);
const aggregator: QuoteAggregator = new QuoteAggregator(configStore, baRouter);
const mustBeCalled = jest.fn();
aggregator.onQuoteUpdated = async quotes => {
expect(quotes.length).toBe(3);
mustBeCalled();
};
await aggregator.start();
await delay(0);
await aggregator.stop();
expect(mustBeCalled.mock.calls.length).toBeGreaterThanOrEqual(1);
});

test('folding with noTradePeriods -> invalid period', async () => {
configStore.config.iterationInterval = 10;
configStore.config.brokers[0].noTradePeriods = [
['00_00', '01_00']
];
const bitflyerBa = {
broker: 'Bitflyer',
fetchQuotes: () =>
Promise.resolve([
{ broker: 'Bitflyer', side: QuoteSide.Ask, price: 500000, volume: 0.1 },
{ broker: 'Bitflyer', side: QuoteSide.Ask, price: 500001, volume: 0.1 }
])
};
const coincheckBa = {
broker: 'Coincheck',
fetchQuotes: () =>
Promise.resolve([
{ broker: 'Coincheck', side: QuoteSide.Bid, price: 490001, volume: 0.02 },
{ broker: 'Coincheck', side: QuoteSide.Bid, price: 490000, volume: 0.2 }
])
};
const quoineBa = {
broker: 'Quoine',
fetchQuotes: () => Promise.resolve([])
};
const baList = [bitflyerBa, coincheckBa, quoineBa];
const baRouter = new BrokerAdapterRouter(baList);
const aggregator: QuoteAggregator = new QuoteAggregator(configStore, baRouter);
const mustBeCalled = jest.fn();
aggregator.onQuoteUpdated = async quotes => {
expect(quotes.length).toBe(3);
mustBeCalled();
};
await aggregator.start();
await delay(0);
await aggregator.stop();
expect(mustBeCalled.mock.calls.length).toBeGreaterThanOrEqual(1);
});

test('folding when a broker is disabled', async () => {
Expand Down Expand Up @@ -92,12 +219,15 @@ describe('Quote Aggregator', () => {
const baList = [bitflyerBa, coincheckBa, quoineBa];
const baRouter = new BrokerAdapterRouter(baList);
const aggregator: QuoteAggregator = new QuoteAggregator(configStore, baRouter);
const mustBeCalled = jest.fn();
aggregator.onQuoteUpdated = async quotes => {
expect(quotes.length).toBe(1);
mustBeCalled();
};
await aggregator.start();
await delay(0);
await aggregator.stop();
expect(mustBeCalled.mock.calls.length).toBeGreaterThanOrEqual(1);
});

test('onQuoteUpdated', async () => {
Expand Down
5 changes: 3 additions & 2 deletions src/config_default.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"maxSize": 0.01,
"minSize": 0.01,
"minTargetProfitPercent": 1.0,
"exitNetProfitRatio": 20,
"exitNetProfitRatio": 20,
"maxTargetVolumePercent": 50.0,
"iterationInterval": 3000,
"positionRefreshInterval": 5000,
Expand Down Expand Up @@ -40,7 +40,8 @@
"maxLongPosition": 0.03,
"maxShortPosition": 0,
"cashMarginType": "Cash",
"commissionPercent": 0
"commissionPercent": 0,
"noTradePeriods": [["04:00", "04:15"]]
},
{
"broker": "Quoine",
Expand Down
1 change: 1 addition & 0 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class BrokerConfig extends Castable implements BrokerConfigType {
@cast cashMarginType: CashMarginType;
@cast leverageLevel: number;
@cast commissionPercent: number;
@cast @element(Array, String) noTradePeriods: string[][];
}

export class SlackConfig extends Castable {
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
version "4.14.86"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.86.tgz#6788d3d75032c46322ff1c144c948cf8117c405b"

"@types/luxon@^0.2.2":
version "0.2.2"
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-0.2.2.tgz#ca83c1026980b111254ef78587ee027483e57df4"

"@types/nock@^8.2.1":
version "8.2.1"
resolved "https://registry.yarnpkg.com/@types/nock/-/nock-8.2.1.tgz#1fbe5bdecb943c109a778553fa4d2401cb9394b4"
Expand Down Expand Up @@ -2119,6 +2123,10 @@ lru-cache@^4.0.1:
pseudomap "^1.0.2"
yallist "^2.1.2"

luxon@^0.2.12:
version "0.2.12"
resolved "https://registry.yarnpkg.com/luxon/-/luxon-0.2.12.tgz#a5edc7a9439ceb605bd400420c4bad657d25a161"

make-error@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.0.tgz#52ad3a339ccf10ce62b4040b708fe707244b8b96"
Expand Down

0 comments on commit 5eae8c4

Please sign in to comment.