tlab-trading-toolkit
Version:
A trading toolkit for building advanced trading bots on the GDAX platform
444 lines (427 loc) • 15.1 kB
text/typescript
/***************************************************************************************************************************
* @license *
* Copyright 2017 Coinbase, Inc. *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on *
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the *
* License for the specific language governing permissions and limitations under the License. *
***************************************************************************************************************************/
import { LiveOrderbook } from '../../src/core/LiveOrderbook';
import { GDAX_WS_FEED, GDAXFeed } from '../../src/exchanges/gdax/GDAXFeed';
import { NullLogger } from '../../src/utils/Logger';
import { EventEmitter } from 'events';
import { Big } from '../../src/lib/types';
import { GDAXL2UpdateMessage, GDAXMatchMessage, GDAXMessage, GDAXSnapshotMessage } from '../../src/exchanges/gdax/GDAXMessages';
import { OrderbookState } from '../../src/lib/Orderbook';
import { BookBuilder } from '../../src/lib/BookBuilder';
import { hooks } from '../../src/exchanges/ExchangeFeed';
import { prepareProductMap } from '../../test/helper';
import simple = require('simple-mock');
const assert = require('assert');
const mockSocket: any = new EventEmitter();
mockSocket.send = () => { /* no-op */ };
mockSocket.close = () => {
mockSocket.removeAllListeners();
};
describe('GDAX Message feed', () => {
before(async () => {
return prepareProductMap();
})
const messages: GDAXMessage[] = [
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['buy', '100.0', '10.0']]
} as GDAXL2UpdateMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['buy', '95.0', '5.0']]
} as GDAXL2UpdateMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['buy', '98.0', '2.5']]
} as GDAXL2UpdateMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['buy', '100.0', '15.0']]
} as GDAXL2UpdateMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['sell', '200.0', '15.0']]
} as GDAXL2UpdateMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['buy', '100.0', '5.0']]
} as GDAXL2UpdateMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['buy', '100.0', '11.0']]
} as GDAXL2UpdateMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['sell', '115.0', '5.0']]
} as GDAXL2UpdateMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['sell', '111.0', '1.0']]
} as GDAXL2UpdateMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['sell', '115.0', '3.0']]
} as GDAXL2UpdateMessage,
{
type: 'match',
product_id: 'BTC-USD',
sequence: 10,
time: '2017-01-01 15:00Z',
price: '100.0',
size: '4.0',
side: 'buy',
maker_order_id: '9e8aa3b8-60bd-11e7-907b-a6006ad30006', // Remaining = 2
taker_order_id: '9e8aa3b8-60bd-11e7-907b-a6006ad0010',
trade_id: '1'
} as GDAXMatchMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['buy', '100.0', '7.0']]
} as GDAXL2UpdateMessage,
{
type: 'match',
product_id: 'BTC-USD',
sequence: 12,
time: '2017-01-01 15:00Z',
price: '111.0',
size: '1.0',
side: 'sell',
maker_order_id: '9e8aa3b8-60bd-11e7-907b-a6006ad30008',
taker_order_id: '9e8aa3b8-60bd-11e7-907b-a6006ad00800',
trade_id: '2'
} as GDAXMatchMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['sell', '111.0', '0.0']]
} as GDAXL2UpdateMessage,
{
type: 'match',
product_id: 'BTC-USD',
sequence: 14,
time: '2017-01-01 15:00Z',
price: '115.0',
size: '1.0',
side: 'sell',
maker_order_id: '9e8aa3b8-60bd-11e7-907b-a6006ad30007',
taker_order_id: '9e8aa3b8-60bd-11e7-907b-a6006ad00800',
trade_id: '3'
} as GDAXMatchMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['sell', '115.0', '2.0']]
} as GDAXL2UpdateMessage,
{
type: 'match',
product_id: 'BTC-USD',
sequence: 16,
time: '2017-01-01 15:00Z',
price: '200.0',
size: '3.0',
side: 'sell',
maker_order_id: '9e8aa3b8-60bd-11e7-907b-a6006ad30004',
taker_order_id: '9e8aa3b8-60bd-11e7-907b-a6006ad01600',
trade_id: '4'
} as GDAXMatchMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['buy', '100.0', '10.0']]
} as GDAXL2UpdateMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['buy', '100.0', '10.1']]
} as GDAXL2UpdateMessage,
{
type: 'match',
product_id: 'BTC-USD',
sequence: 20,
time: '2017-01-01 15:00Z',
price: '100.0',
size: '2.0',
side: 'buy',
maker_order_id: '9e8aa3b8-60bd-11e7-907b-a6006ad30006', // Filled
taker_order_id: '9e8aa3b8-60bd-11e7-907b-a6006ad00801',
trade_id: '4'
} as GDAXMatchMessage,
{
type: 'match',
product_id: 'BTC-USD',
sequence: 21,
time: '2017-01-01 15:00Z',
price: '100.0',
size: '4.0',
side: 'buy',
maker_order_id: '9e8aa3b8-60bd-11e7-907b-a6006ad30018', // Remaining = 4
taker_order_id: '9e8aa3b8-60bd-11e7-907b-a6006ad00802',
trade_id: '5'
} as GDAXMatchMessage,
{
type: 'l2update',
product_id: 'BTC-USD',
changes: [['buy', '100.0', '5.1']]
} as GDAXL2UpdateMessage
];
const expectedState: OrderbookState = {
sequence: 17,
orderPool: {
'100.0': { id: '100.0', side: 'buy', price: Big(100), size: Big(4.1) },
'115.0': { id: '115.0', side: 'sell', price: Big(115), size: Big(2) },
'200.0': { id: '200.0', side: 'sell', price: Big(200), size: Big(15) },
'98.0': { id: '98.0', side: 'buy', price: Big(98), size: Big(2.5) },
'95.0': { id: '95.0', side: 'buy', price: Big(95), size: Big(5) }
},
bids: [
{
price: Big(100),
totalSize: Big(5.1),
orders: [{ id: '100.0', side: 'buy', price: Big(100), size: Big(5.1) }]
},
{
price: Big(98),
totalSize: Big(2.5),
orders: [{ id: '98.0', side: 'buy', price: Big(98), size: Big(2.5) }
]
},
{
price: Big(95),
totalSize: Big(5),
orders: [{ id: '95.0', side: 'buy', price: Big(95), size: Big(5) }]
}
],
asks: [
{
price: Big(200),
totalSize: Big(15),
orders: [{ id: '200.0', side: 'sell', price: Big(200), size: Big(15) }]
},
{
price: Big(115),
totalSize: Big(2),
orders: [{ id: '115.0', side: 'sell', price: Big(115), size: Big(2) }]
}
]
};
let book: LiveOrderbook;
let gdaxFeed: GDAXFeed;
before(() => {
simple.mock(hooks, 'WebSocket').callFn(() => mockSocket);
gdaxFeed = new GDAXFeed({
logger: null,
auth: null,
apiUrl: '',
wsUrl: '',
channels: null
});
book = new LiveOrderbook({
logger: NullLogger,
strictMode: true,
product: 'BTC/USD'
});
gdaxFeed.pipe(book);
});
after(() => {
gdaxFeed.disconnect();
});
it('produces a matching live GDAX book', (done) => {
book.once('LiveOrderbook.snapshot', () => {
snapshot = true;
});
let snapshot = false;
mockSocket.emit('open');
mockSocket.emit('message', JSON.stringify({
type: 'snapshot',
product_id: 'BTC-USD',
sequence: 0,
bids: [],
asks: []
} as GDAXSnapshotMessage));
for (const message of messages) {
mockSocket.emit('message', JSON.stringify(message));
}
setImmediate(() => {
assert.equal(book.sequence, 17, 'Did not process all relevant messages');
assert.ok(snapshot, 'Did not receive snapshot');
const expectedBook = new BookBuilder(NullLogger);
expectedBook.fromState(expectedState);
assert.deepEqual(book.state(), expectedBook.state());
done();
});
});
after(() => {
simple.restore();
});
});
describe('GDAX feed with channels', () => {
let book: LiveOrderbook;
let gdaxFeed: GDAXFeed;
const messages: GDAXMessage[] = [
{
type: 'snapshot',
product_id: 'BTC-EUR',
bids: [['100', '10'], ['90', '5']],
asks: [['110', '1'], ['115', '2']]
} as GDAXSnapshotMessage,
{
type: 'l2update',
product_id: 'BTC-EUR',
from_sequence: 1000,
to_sequence: 1010,
changes: [['buy', '100', '5'], ['sell', '110', '3']]
} as GDAXL2UpdateMessage,
{
type: 'l2update',
product_id: 'BTC-EUR',
from_sequence: 1010,
to_sequence: 1011,
changes: [['buy', '90', '0']]
} as GDAXL2UpdateMessage,
{
type: 'l2update',
product_id: 'BTC-EUR',
from_sequence: 1011,
to_sequence: 1025,
changes: [['sell', '200', '1']]
} as GDAXL2UpdateMessage
];
const expectedState: OrderbookState = {
sequence: 5,
orderPool: {
100: {
id: '100',
price: Big(100),
size: Big(5),
side: 'buy'
},
115: {
id: '115',
price: Big(115),
size: Big(2),
side: 'sell'
},
110: {
id: '110',
price: Big(110),
size: Big(3),
side: 'sell'
},
200: {
id: '200',
price: Big(200),
size: Big(1),
side: 'sell'
}
},
bids: [
{
price: Big(100),
totalSize: Big(5),
orders: [
{
id: '100',
price: Big(100),
size: Big(5),
side: 'buy'
}
]
}
],
asks: [
{
price: Big(110),
totalSize: Big(3),
orders: [
{
id: '110',
price: Big(110),
size: Big(3),
side: 'sell'
}
]
},
{
price: Big(115),
totalSize: Big(2),
orders: [
{
id: '115',
price: Big(115),
size: Big(2),
side: 'sell'
}
]
},
{
price: Big(200),
totalSize: Big(1),
orders: [
{
id: '200',
price: Big(200),
size: Big(1),
side: 'sell'
}
]
}
]
};
before(() => {
simple.mock(hooks, 'WebSocket').callFn(() => mockSocket);
gdaxFeed = new GDAXFeed({
logger: NullLogger,
apiUrl: '',
auth: null,
wsUrl: GDAX_WS_FEED,
channels: null
});
(gdaxFeed as any).queueing['BTC-EUR'] = false;
book = new LiveOrderbook({
logger: NullLogger,
strictMode: true,
product: 'BTC/EUR'
});
gdaxFeed.pipe(book);
});
it('produces a matching live GDAX book', (done) => {
book.once('LiveOrderbook.snapshot', () => {
snapshot = true;
});
let snapshot = false;
mockSocket.emit('open');
for (const message of messages) {
mockSocket.emit('message', JSON.stringify(message));
}
setImmediate(() => {
assert.equal(book.sequence, 5, 'Did not process all relevant messages');
assert.ok(snapshot, 'Did not receive snapshot');
const expectedBook = new BookBuilder(NullLogger);
expectedBook.fromState(expectedState);
assert.deepEqual(book.state(), expectedBook.state());
done();
});
});
});