This repository was archived by the owner on Sep 9, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 183
/
Copy pathKrakenOrderBook.js
281 lines (264 loc) · 8.4 KB
/
KrakenOrderBook.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
const crc = require("crc");
const KrakenOrderBookPoint = require("./KrakenOrderBookPoint");
/**
* Prototype implementation of an order book for Kraken. This should be
* used with a feed that originiates with data provided by Kraken
* spec: https://docs.kraken.com/websockets/#message-book.
*
* ```javascript
* const client = new KrakenClient();
* const market = { id: "XXBTZUSD", base: "BTC", quote: "USD" };
* client.subscribeLevel2Updates(market);
*
* let ob;
* client.on("l2snapshot", snap => {
* const asks = snap.asks.map(p => new KrakenOrderBookPoint(p.price, p.size, p.timestamp));
* const bids = snap.bids.map(p => new KrakenOrderBookPoint(p.price, p.size, p.timestamp));
* ob = new KrakenOrderBook(asks, bids);
* });
*
* client.on("l2update", update => {
* for (let a of update.asks) {
* ob.update(false, a.price, a.size, a.timestamp);
* }
* for (let b of update.bids) {
* ob.update(true, b.price, b.size, b.timestamp);
* }
* });
* ```
*
* @remarks
*
* This implementation uses sorted arrays to store ask and bid values.
* The rationale is that after each batch of updates a snapshot operation
* must be performed which will require ordering the values the book.
*
* This uses binary search to find the mutation index for the array.
* This means we have worst case time complexity of:
*
* updates: O(log n)
* insert: O(n)
* delete: O(n)
* snapshots: O(1)
*
* Because most order books operate towards the tip of the book,
* highest bid and lowest ask, we can get average time complexity
* improvements by using sorted arrays such that the bulk of the
* operations occur towards the end of the array. This reduces the
* number of operations required in shift operation.
*
* We will perform further research to determine the optimal data
* structure before releasing a non-prototype version for other
* exchanges.
*/
class KrakenOrderBook {
/**
* Creates an order book from the points provided. Can be used with a
* snapshot to immeidately construct an orderbook. All input values
* are expected to be KrakenOrderBookPoint instances.
*
* ```javascript
* const asks = asks.map(p => new KrakenOrderBookPoint(p.price, p.size, p.timestamp));
* const bids = bids.map(p => new KrakenOrderBookPoint(p.price, p.size, p.timestamp));
* ob = new KrakenOrderBook(asks, bids);
* ```
*
* @param {KrakenOrderBookPoint[]} asks
* @param {KrakenOrderBookPoint[]} bids
*/
constructor(asks = [], bids = []) {
/**
* Best ask value is the lowest price. This means that most activity
* will happen towards the lowest price and we need to sort desc.
*/
this.asks = asks.slice().sort(sortDesc);
/**
* Best bid value is the highest price. this means that most
* activity will happen towards the highest price and we need to
* sort asc.
*/
this.bids = bids.slice().sort(sortAsc);
}
/**
* Updates the orderbook with a new price level entry. This value will
* either be and insertion, update, or deletion. The bid parameter
* determines which side of the book the update falls on.
*
* ```javascript
* client.on("l2update", update => {
* for (let a of update.asks) {
* ob.update(false, a.price, a.size, a.timestamp);
* }
* for (let b of update.bids) {
* ob.update(true, b.price, b.size, b.timestamp);
* }
* });
* ```
* @param {boolean} bid
* @param {string} price
* @param {string} size
* @param {string} timestamp
*/
update(bid, price, size, timestamp) {
let arr;
let index;
// The best bids are the highest priced, meaning the tail of array
// using the best bids would be sorted ascending
if (bid) {
arr = this.bids;
index = findIndexAsc(arr, price);
}
// The best asks are the lowest priced, meaning the tail of array
// with the best asks would be sorted descending
else {
arr = this.asks;
index = findIndexDesc(arr, price);
}
// We perform an update when the index of hte current value has
// the same price as the update we are now processing.
if (arr[index] && arr[index].price === price) {
// Only perform an update if the new value has a newer timestamp
// than the existing value
if (timestamp <= arr[index].timestamp) {
return;
}
// Remove the value when the size is 0
if (Number(size) === 0) {
arr.splice(index, 1);
return;
}
// Otherwise we perform an update by changing the size and timestamp
arr[index].size = size;
arr[index].timestamp = timestamp;
}
// Otherwise we are performing an insert, which we will construct
// a new point. Because we are using splice, which should have a
// worst case runtime of O(N), we
// O()
else if (Number(size) > 0) {
const point = new KrakenOrderBookPoint(price, size, timestamp);
arr.splice(index, 0, point);
}
}
/**
* Captures a simple snapshot of best asks and bids up to the
* requested depth.
* @param {number} depth
* @returns {{ asks, bids }}
*/
snapshot(depth) {
let asks = [];
for (let i = this.asks.length - 1; i >= this.asks.length - depth; i--) {
let val = this.asks[i];
if (val) asks.push(val);
}
let bids = [];
for (let i = this.bids.length - 1; i >= this.bids.length - depth; i--) {
let val = this.bids[i];
if (val) bids.push(val);
}
return {
asks,
bids,
};
}
/**
* Returns the checksum of the order book based on the algorithm
* specified in https://docs.kraken.com/websockets/#book-checksum
* @returns {string}
*/
checksum() {
const snap = this.snapshot(10);
const data = checksumString(snap.asks, snap.bids);
return crc.crc32(data).toString(10);
}
}
/**
* Performs a binary search of a sorted array for the insert or update
* position of the value and operates on a KrakenOrderBookPoint value
* @param {KrakenOrderBookPoint[]} arr
* @param {string} key
* @param {number} l
* @param {number} r
*/
function findIndexAsc(arr, key, l = 0, r = arr.length) {
const mid = Math.floor((l + r) / 2);
if (l === r) return mid;
if (arr[mid] && arr[mid].price === key) return mid;
if (arr[mid] && arr[mid].price > key) return findIndexAsc(arr, key, l, mid);
if (arr[mid] && arr[mid].price < key) return findIndexAsc(arr, key, mid + 1, r);
}
/**
* Performs a binary search of a sorted array for the insert or update
* position of the value and operates on a KrakenOrderBookPoint value
* @param {KrakenOrderBookPoint[]} arr
* @param {string} key
* @param {number} l
* @param {number} r
*/
function findIndexDesc(arr, key, l = 0, r = arr.length) {
const mid = Math.floor((l + r) / 2);
if (l === r) return mid;
if (arr[mid] && arr[mid].price === key) return mid;
if (arr[mid] && arr[mid].price < key) return findIndexDesc(arr, key, l, mid);
if (arr[mid] && arr[mid].price > key) return findIndexDesc(arr, key, mid + 1, r);
}
/**
* Converts a raw number value into the format for crc checksum. This
* format removes the . from the number, and strips all prefixed 0's
* before the first real digit. For examples '0.00050000' => '50000'
* @param {string} val
*/
function crcNum(val) {
let result = "";
let chars = val.split("");
let start = true;
for (let char of chars) {
if (start && char === "0") continue;
if (char === ".") continue;
start = false;
result += char;
}
return result;
}
/**
* Creates the checksum string from the bid and ask values based on the
* algorithm. This converts string values into the stripped number
* format in `crcNum` and concatenates the price and size for asks then
* bids.
* @param {KrakenOrderBookPoint[]} asks
* @param {KrakenOrderBookPoint[]} bids
*/
function checksumString(asks, bids) {
let data = "";
for (let ask of asks) {
data += crcNum(ask.price);
data += crcNum(ask.size);
}
for (let bid of bids) {
data += crcNum(bid.price);
data += crcNum(bid.size);
}
return data;
}
/**
* Sorts points from high to low
* @param {KrakenOrderBookPoint} a
* @param {KrakenOrderBookPoint} b
*/
function sortDesc(a, b) {
if (a.price > b.price) return -1;
if (a.price < b.price) return 1;
return 0;
}
/**
* Sorts points from low to high
* @param {KrakenOrderBookPoint} a
* @param {KrakenOrderBookPoint} b
*/
function sortAsc(a, b) {
if (a.price < b.price) return -1;
if (a.price > b.price) return 1;
return 0;
}
module.exports = KrakenOrderBook;