@icgio/icg-exchanges-wrapper 1.17.14 → 1.17.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,366 @@
1
+ # Performance cleanup — 2026-04-09
2
+
3
+ Session of performance and dead-code cleanup work spanning three repos:
4
+
5
+ - `icg-utils` — `combine_orderbooks` rewrite + new tests
6
+ - `icg-exchanges-wrapper` — `orders.js` hot-path optimization + dead-code removal
7
+ - `icg-market-maker` — small follow-up to drop `o.trace` references
8
+
9
+ Nothing in this document has been published or deployed. Versions, dependents,
10
+ and MM client servers still need updating per the deployment notes at the
11
+ bottom.
12
+
13
+ ## Why
14
+
15
+ The maker hot path (rate update → `process_rates` → `notify()` → maker
16
+ callback → `handle_maker_*`) was the bottleneck under analysis. Several
17
+ contributing costs were identified and addressed. Expected aggregate effect:
18
+ the per-rate-tick cost on the maker hot path should drop from the
19
+ **multi-millisecond range to sub-millisecond** for typical configurations
20
+ (5 exchanges × 3 pairs × ~100 orders).
21
+
22
+ ## Changes
23
+
24
+ ### icg-utils
25
+
26
+ #### Rewrote `combine_orderbooks` (2026-04-09)
27
+
28
+ `lib/utils.js`
29
+
30
+ The original implementation:
31
+ - Used `_.get`, `_.sum`, `_.values`, `_.keys` inside the inner loop
32
+ - Mutated input tuples in place by pushing the exchange name (`tuple[2]`)
33
+ onto each level — forcing callers to `cloneDeep` upstream
34
+ - O(R × K) with high lodash constant factors
35
+
36
+ The rewrite:
37
+ - Strips all lodash from the inner loop
38
+ - Uses parallel arrays (`lists`, `indexes`, `exchanges`) keyed by an integer
39
+ index, hoists the asks/bids direction comparator out of the loop
40
+ - **Non-mutating**: `orderbooks_dict` and its inner arrays/tuples are never
41
+ touched. Callers do NOT need to deep-clone before calling.
42
+ - **Pushes input tuple references directly into the result** (Flavor 2 — see
43
+ Reference Sharing Contract below). Zero allocations in the inner loop.
44
+ - Drops the `tuple[2]` augmentation entirely. The previous algorithm needed
45
+ the exchange name on each tuple to know which pointer to advance; the
46
+ rewrite tracks this via `bestIdx` (an integer index into the parallel
47
+ arrays).
48
+
49
+ Expected speedup vs the original: ~10× per call. Combined with the upstream
50
+ `cloneDeep` removal, `process_rates` per call drops from ~3-5 ms to ~0.2-0.4
51
+ ms for a typical 5-exchange / 3-pair MM.
52
+
53
+ ##### Reference Sharing Contract
54
+
55
+ The returned `{asks, bids}` arrays contain references to the SAME tuple
56
+ objects that live inside the caller's input. **Callers MUST treat both the
57
+ input and the result as read-only.** Mutating either will corrupt the other.
58
+
59
+ This is safe in `icg-exchanges-wrapper`'s `process_rates` flow because:
60
+
61
+ 1. WS rate updates REPLACE the per-pair object via
62
+ `_.set(this.rates, [exchange, pair], { asks, bids, ... })` rather than
63
+ mutating in place — old tuple references simply become garbage when no
64
+ one holds them.
65
+ 2. The combined-rates consumer (the MM `rates_observable` subscriber) runs
66
+ synchronously inside `notify()` in the same call stack, so no other WS
67
+ handler can interleave and rebuild rates underneath us.
68
+
69
+ If you ever need result tuples that are independent of the input (e.g. to
70
+ pass to a mutating consumer like `vol_position_combined`, which expects a
71
+ `[price, amount, exchange]` 3-tuple), wrap them at the call site — do not
72
+ put that contract back into `combine_orderbooks`.
73
+
74
+ #### Added `test/combine-orderbooks.test.js` (2026-04-09)
75
+
76
+ `test/combine-orderbooks.test.js`
77
+
78
+ 13 tests covering:
79
+
80
+ - Basic merge ordering (asks ascend, bids descend, length limit honoured)
81
+ - Result tuples remain `[price, amount]` (no `[2]` augmentation)
82
+ - Input is byte-equal to its pre-call snapshot (non-mutation contract)
83
+ - Idempotent on repeated calls
84
+ - Result tuples `===` input tuples (Flavor 2 reference aliasing)
85
+ - Empty dict, empty side per exchange, single exchange, length larger than
86
+ available, no length limit
87
+ - 3-way interleaving correctness
88
+ - Tied prices are deterministic
89
+ - Fractional prices
90
+
91
+ Wired into `npm test` via `package.json` (now runs both `rate-limiter.test.js`
92
+ and `combine-orderbooks.test.js`).
93
+
94
+ ### icg-exchanges-wrapper
95
+
96
+ All changes in `middleware/orders.js` unless noted otherwise.
97
+
98
+ #### Dropped `_.cloneDeep` in `process_rates` (2026-04-09)
99
+
100
+ The deep clone existed only because the old `combine_orderbooks` mutated its
101
+ input. With the rewrite the input is never touched, so `process_rates` now
102
+ passes the live `this.rates[exchange][pair]` reference directly. Saves
103
+ ~600 µs per `process_rates` call for a typical 5-exchange / 3-pair / 20-level
104
+ MM.
105
+
106
+ #### Removed dead DEX code (2026-04-09)
107
+
108
+ `rates_with_routes` and the entire DEX trading path were referenced from six
109
+ call sites in `orders.js` and one in
110
+ `functions/get-exchanges-rates-market-history-ohlcv.js`, but no exchange in
111
+ `icg-exchanges` ever set `exchange_type === 'dex'` and no exchange implemented
112
+ `rate_with_routes` / `rate_with_routes_ws` / `limit_trade_with_routes`. The
113
+ branches were unreachable in production.
114
+
115
+ Removed:
116
+
117
+ - `this.rates_with_routes = {}` initialization
118
+ - `limit_trade`'s DEX branch (~30 lines including closest-level / route lookup)
119
+ - `if (rates_with_routes)` block in `rates_handler`, plus the
120
+ `rates_with_routes = false` parameter
121
+ - Both DEX branches in `get_rates` (with and without `ws_market`)
122
+ - DEX startup branch that pre-set `previous_orders_canceled_exchange_pair`
123
+ - The `else if (Exchange.rate_with_routes)` REST fallback in
124
+ `functions/get-exchanges-rates-market-history-ohlcv.js`
125
+
126
+ Net: ~74 lines of dead code removed.
127
+
128
+ #### Removed `update_order_info` trace block (2026-04-09)
129
+
130
+ `update_order_info` previously appended to a per-order `trace` array on every
131
+ update via `[...existing_trace, trace_entry]`. This was:
132
+
133
+ - O(N) quadratic per update — the spread copies the entire existing trace
134
+ - Unbounded memory growth per long-lived order
135
+ - Sent over WebSocket on every update, growing the WS payload over time
136
+
137
+ The trace had two consumers:
138
+
139
+ 1. **`maker.js`** read `_.last(o.trace).ts` to filter stale CANCELING orders
140
+ in `maker_2` and `maker_3`. Replaced with a new `last_update_ts` field set
141
+ via native assignment in `update_order_info`.
142
+ 2. **The dashboard "Trace Timeline" UI** showed the full status history for
143
+ any clicked order. This is real value, but the wrapper is the wrong place
144
+ to materialize it. Plan: rebuild it on the dashboard backend from the
145
+ `ws_client.send_order` events that already arrive there. See
146
+ "Future work" below.
147
+
148
+ The wrapper now does zero trace work. The dashboard "Trace Timeline" will
149
+ temporarily show "No trace available for this order" until the backend
150
+ update ships.
151
+
152
+ #### Added status / order_type indexes (2026-04-09)
153
+
154
+ Two new indexes maintained incrementally by `update_order_info` and
155
+ `delete_order_info`:
156
+
157
+ - `this.order_status_index[exchange][pair][type][status] -> Set<order_ref>`
158
+ Used by `get_order_info_status_all`.
159
+ - `this.order_info_by_type[order_type][exchange][pair][type][order_id] -> ref`
160
+ Used by `get_order_info_by_order_type`.
161
+
162
+ Maintenance happens at the end of `update_order_info` after the dict fields
163
+ are written. The function snapshots the old `status` / `order_type` BEFORE
164
+ the for-loop, then compares to the new values after, and moves the order
165
+ between buckets only if the relevant field actually changed. The common
166
+ case (a non-status update like `f_amount` or `last_update_ts`) is a no-op.
167
+
168
+ `delete_order_info` removes the order from both indexes using the order's
169
+ current `status` / `order_type` BEFORE deleting it from the canonical
170
+ `this.order_info`.
171
+
172
+ `get_order_info_status_all` was rewritten to look up the precomputed Set
173
+ and return `Array.from(...)`. Handles both single-status and array-status
174
+ queries, plus the `'both'` recursive case.
175
+
176
+ `get_order_info_by_order_type` walks only the matching `order_type`
177
+ subtree(s) and returns a freshly built top-level dict (so callers can safely
178
+ mutate the returned structure) with leaves that are still refs into
179
+ `this.order_info`.
180
+
181
+ ##### Indexing invariants
182
+
183
+ - Every order in `this.order_info[exchange][pair][type][order_id]` has at
184
+ most one entry in `order_status_index[exchange][pair][type][status]` and
185
+ at most one entry in
186
+ `order_info_by_type[order_type][exchange][pair][type][order_id]`.
187
+ - The only mutators of `this.order_info` are `update_order_info` and
188
+ `delete_order_info`. Both maintain the indexes. Anyone who writes directly
189
+ to `this.order_info` outside these two methods will desynchronize the
190
+ indexes.
191
+ - The previous `this.order_info_status` cache (used by the old
192
+ `get_order_info_status_all`) is removed.
193
+ - `Account.order_type === 'range-order'` mutations at `:1006` and `:1011`
194
+ occur on `res.body` open-orders entries from the exchange WS, NOT on
195
+ `this.order_info` orders, so they don't affect the indexes.
196
+
197
+ ##### Functional verification
198
+
199
+ A standalone in-process test exercises the indexed code paths against a
200
+ stub instance. 11 assertions cover:
201
+
202
+ 1. Insert maker_1 sell INIT → in INIT bucket, in maker_1 subtree
203
+ 2. Transition INIT → N-FILLED → moves buckets, stays in maker_1
204
+ 3. `'both'` query → unions sell + buy
205
+ 4. Array status query → unions multiple status buckets
206
+ 5. by_order_type with array → merges multiple subtrees
207
+ 6. `delete_order_info` → removed from BOTH indexes + canonical store
208
+ 7. Multiple status transitions → bucket moves are atomic
209
+ 8. `last_update_ts` is stamped (numeric, > 0)
210
+ 9. Empty queries return `[]` / `{}` (no crash)
211
+ 10. Result arrays from `get_order_info_status_all` are independent (fresh
212
+ array per call) but inner orders are the same refs
213
+ 11. Mutating the dict returned by `get_order_info_by_order_type` does not
214
+ affect the canonical store
215
+
216
+ #### Fixed `check_balances` shadow bug (2026-04-09)
217
+
218
+ The original inner loop used `for (let exchange in this.exchange_cur_quote_cur)`
219
+ which **shadowed** the outer `exchange` parameter. Every balance update
220
+ rebuilt `buy_sell_limits_against_reserve_balance` for ALL exchanges, doing
221
+ K× the necessary work where K = exchange count. The fix scopes the inner
222
+ loop to the single exchange whose balance just arrived. Also collapsed the
223
+ empty `if (max > 0) { } else { max = 0 }` blocks to `if (max < 0) max = 0`.
224
+
225
+ #### Early-exit on `previous_orders_canceled` recompute (2026-04-09)
226
+
227
+ The recompute walked every `(exchange, pair)` cell every time. Now uses a
228
+ labeled `break outer` on the first unmarked cell. Steady-state behavior
229
+ unchanged (the outer guard `if (!this.previous_orders_canceled)` still
230
+ prevents re-runs once the flag is true), but startup is faster.
231
+
232
+ #### `_.omit(obj, [key])` → `delete obj[key]` (2026-04-09)
233
+
234
+ 16 sites. `_.omit` allocates a new object copying every other key — O(N)
235
+ plus GC pressure. `delete` is O(1) and free.
236
+
237
+ Sites:
238
+
239
+ - `delete_order_info` (`this.order_info[exchange][pair][type]`)
240
+ - `check_balances` expiry (`this.balances`,
241
+ `this.buy_sell_limits_against_reserve_balance`)
242
+ - `check_positions` expiry (`this.positions`)
243
+ - `open_orders_handler` and `all_open_orders_handler` expiry
244
+ (`this.open_orders[exchange]`)
245
+ - `trades_handler` and `all_trades_handler` expiry (`this.trades[exchange]`)
246
+ - `query_order_handler` expiry (`this.query_order[exchange]`)
247
+ - `process_rates` local-variable cleanups (`orderbook_major`,
248
+ `orderbook_major_temp`) and `this.rates_combined_major`
249
+ - `rates_handler` expiry (`this.rates[exchange]`)
250
+ - `market_history_handler` expiry (`this.market_history[exchange]`)
251
+ - `ohlcv_handler` short-data and expiry (`this.ohlcv[exchange]`)
252
+
253
+ #### `new Date().getTime()` → `Date.now()` (2026-04-09)
254
+
255
+ 13 sites. `new Date().getTime()` allocates a Date object just to read its
256
+ timestamp; `Date.now()` is an intrinsic with no allocation. Pure micro-win.
257
+
258
+ ### icg-market-maker
259
+
260
+ #### Replaced `_.last(o.trace).ts` filters with `o.last_update_ts` (2026-04-09)
261
+
262
+ `scripts/maker.js`
263
+
264
+ Two filters in `maker_2` and `maker_3` (for stale CANCELING orders) replaced
265
+ with the new `o.last_update_ts` field added to `orders.js` in this session.
266
+ No semantic change.
267
+
268
+ ## Files touched
269
+
270
+ | Repo | File | Note |
271
+ |---|---|---|
272
+ | `icg-utils` | `lib/utils.js` | `combine_orderbooks` rewrite |
273
+ | `icg-utils` | `test/combine-orderbooks.test.js` | new file, 13 tests |
274
+ | `icg-utils` | `package.json` | `test` script runs both files |
275
+ | `icg-exchanges-wrapper` | `middleware/orders.js` | (a) drop `_.cloneDeep`, (b) remove DEX dead code, (c) remove trace block + add `last_update_ts`, (d) status / order_type indexes, (e) rewrite `get_order_info_status_all` and `get_order_info_by_order_type`, (f) `check_balances` shadow + scope, (g) early-exit `previous_orders_canceled` recompute, (h) `_.omit` → `delete` (16 sites), (i) `new Date().getTime()` → `Date.now()` (13 sites) |
276
+ | `icg-exchanges-wrapper` | `functions/get-exchanges-rates-market-history-ohlcv.js` | remove DEX REST fallback |
277
+ | `icg-market-maker` | `scripts/maker.js` | two `last_trace` filters → `o.last_update_ts` |
278
+
279
+ ## Net effect
280
+
281
+ Hot path per rate tick (5 exchanges × 3 pairs × ~100 orders):
282
+
283
+ | Operation | Before | After |
284
+ |---|---|---|
285
+ | `process_rates` per pair | ~1-2 ms | ~0.2-0.4 ms |
286
+ | `combine_orderbooks` per call | ~500-1000 µs | ~50-100 µs |
287
+ | `get_order_info_status_all` × ~270 calls/tick | walks all matching orders for that (exchange, pair, type), lodash-heavy | Set lookup + `Array.from`, O(matching) |
288
+ | `get_order_info_by_order_type` × ~15 calls/tick | 4-deep scan of all `order_info`, lodash-heavy | walks only matching subtree |
289
+ | `update_order_info` trace cost | O(N) spread per update | 0 |
290
+ | `check_balances` per balance event | K× redundant rebuild | 1× rebuild scoped to the right exchange |
291
+ | `previous_orders_canceled` recompute | always full walk while flag is false | early-exit on first miss |
292
+ | `_.omit` × 16 sites | O(N) new-object allocation | O(1) `delete`, no allocation |
293
+ | `Date` instances per check | 13 per tick | 0 |
294
+
295
+ ## Future work
296
+
297
+ ### Backend trace materialization
298
+
299
+ Rebuild the "Trace Timeline" data on
300
+ `clients-dashboard-backend/src/services/ws-data-service.js` from the
301
+ `ws_client.send_order` events it already receives. About 12 lines added in
302
+ `handle_order_event`. The wrapper does no trace work; the backend caps the
303
+ trace at last 20 entries; the dashboard UI is unchanged. Spans
304
+ `clients-dashboard-backend` only — neither the wrapper nor the maker need to
305
+ ship for this.
306
+
307
+ The required pattern in `handle_order_event`:
308
+
309
+ ```js
310
+ if (action === 'update') {
311
+ const existing = _.get(this.order_info[client_id], [exchange, pair, side, order_id])
312
+ const trace = (existing && existing.trace) || []
313
+ trace.push({
314
+ ts: Date.now(),
315
+ status: data.status,
316
+ note,
317
+ f_amount: data.f_amount,
318
+ })
319
+ if (trace.length > TRACE_MAX_ENTRIES) {
320
+ trace.splice(0, trace.length - TRACE_MAX_ENTRIES)
321
+ }
322
+ _.setWith(
323
+ this.order_info[client_id],
324
+ `${exchange}.${pair}.${side}.${order_id}`,
325
+ { ...data, trace, ts: Date.now() },
326
+ Object,
327
+ )
328
+ // ... rest unchanged
329
+ }
330
+ ```
331
+
332
+ Plus a small change to `handle_order_sync` to preserve existing traces
333
+ across full re-sync (otherwise reconnects wipe history).
334
+
335
+ ### Other items not done in this session
336
+
337
+ - **`Observable.notify()` try/catch** in `icg-utils/lib/observable.js`. Today
338
+ there's only one subscriber so a throw is just a crash, but the moment a
339
+ second subscriber is added, a throw in #1 silently kills #2. Two-line fix.
340
+ - **Remaining lodash hot-path cleanup**: `_.set` / `_.setWith` / `_.get` in
341
+ `update_order_info` and `rates_handler` could be replaced with native
342
+ indexing for ~5-20× speedup on those calls.
343
+ - **Empty `cancel_order_by_order` callbacks** at `orders.js:1038`, `:1045`:
344
+ failures silently ignored. Robustness, not perf.
345
+ - **Hot-path `console.log('UPDATE %s: ...')` formatting** at `orders.js:862`:
346
+ runs on every `update_order_info` call. Should be gated with a verbosity
347
+ flag or routed through `log_limiter` (like maker.js does).
348
+ - **`process_open_orders` nested scans** at `orders.js:618-766`. Same
349
+ indexing approach as the status / order_type indexes would help here too,
350
+ but it's off the hot path so lower priority.
351
+
352
+ ## Deployment notes
353
+
354
+ When this work ships:
355
+
356
+ 1. `icg-utils` → bump version → publish to npm (new `combine_orderbooks`).
357
+ 2. `icg-exchanges-wrapper` → bump `@icgio/icg-utils` dep range → `npm install`
358
+ → bump pkg version → publish.
359
+ 3. **MM client servers** → manual deploy across all 12. Per
360
+ `feedback_mm_servers_dirty_lockfile.md`, pre-stash any dirty
361
+ `package.json` / `package-lock.json` on each server before
362
+ `git pull && npm ci`. The deploy carries both the wrapper republish and
363
+ the `maker.js` change (they both touch the maker process).
364
+ 4. The dashboard "Trace Timeline" will show "No trace available" until the
365
+ `clients-dashboard-backend` update ships separately. Plan that as a
366
+ follow-up.
@@ -374,26 +374,6 @@ const get_exchanges_rates = function (Exchanges, exchange_pair_dict, cb, ws_enab
374
374
  })
375
375
 
376
376
  promises = promises.concat(rate_promises) // Add all rate promises
377
- } else if (Exchange.rate_with_routes) {
378
- let pairs_to_fetch = exchange_pair_dict[Exchange_name] || all_pairs
379
- let rate_with_routes_promises = pairs_to_fetch.map((pair) => {
380
- return new Promise((resolve, reject) => {
381
- Exchange.rate_with_routes(pair, function (res) {
382
- if (res.success) {
383
- let { asks, bids } = res.body
384
- asks = asks.map((ask) => ask.slice(0, 2))
385
- bids = bids.map((bid) => bid.slice(0, 2))
386
- set_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair, { asks, bids })
387
- } else {
388
- console.error(`getting rate with routes failed for ${Exchange_name} ${pair}:`, res)
389
- delete_pair_data(exchange_pair_rates, pair_exchange_rates, Exchange_name, pair)
390
- }
391
- resolve()
392
- })
393
- })
394
- })
395
-
396
- promises = promises.concat(rate_with_routes_promises) // Add all rate_with_routes promises
397
377
  }
398
378
  }
399
379
  }
@@ -200,7 +200,6 @@ module.exports = class Orders {
200
200
  this.trades = {}
201
201
  this.query_order = {}
202
202
  this.rates = {}
203
- this.rates_with_routes = {}
204
203
  this.rates_observable = new Observable()
205
204
  this.market_history = {}
206
205
  this.ohlcv = {}
@@ -209,7 +208,8 @@ module.exports = class Orders {
209
208
  this.buy_sell_limits_against_reserve_balance = {}
210
209
  this.order_info = {}
211
210
  this.fill_records = [] // { side, usdt_amount, timestamp, order_type }
212
- this.order_info_status = {}
211
+ this.order_status_index = {}
212
+ this.order_info_by_type = {}
213
213
  this.function_enabled = settings.function_enabled || {
214
214
  balances_check: true,
215
215
  positions_check: false,
@@ -319,39 +319,7 @@ module.exports = class Orders {
319
319
  }
320
320
  }, NO_CALLBACK_TIMEOUT_MS)
321
321
  let limit_trade = (cb) => {
322
- if (this.Exchanges[exchange].exchange_type === 'dex') {
323
- let rates_with_routes = this.rates_with_routes[exchange][pair]
324
- let routes
325
- if (method === 'buy') {
326
- let closest_level = _.minBy(rates_with_routes.asks, (ask) => {
327
- return Math.abs(ask[0] - price)
328
- })
329
- routes = [closest_level[2]]
330
- // for (let ask of rates_with_routes.asks) {
331
- // if (ask[0] === price) {
332
- // routes = [ask[2]]
333
- // }
334
- // }
335
- } else {
336
- let closest_level = _.minBy(rates_with_routes.bids, (bid) => {
337
- return Math.abs(bid[0] - price)
338
- })
339
- routes = [closest_level[2]]
340
- // for (let bid of rates_with_routes.bids) {
341
- // if (bid[0] === price) {
342
- // routes = [bid[2]]
343
- // }
344
- // }
345
- }
346
- if (routes) {
347
- if (this.Exchanges[exchange].private_limiter) {
348
- this.Exchanges[exchange].private_limiter.priority = getOrderPriority(order_type)
349
- }
350
- this.Exchanges[exchange].limit_trade_with_routes(pair, method, price, amount, routes, cb)
351
- } else {
352
- console.error('dex no routes:', rates_with_routes, price)
353
- }
354
- } else if (limiter_on) {
322
+ if (limiter_on) {
355
323
  if (this.Exchanges[exchange].private_limiter) {
356
324
  this.Exchanges[exchange].private_limiter.priority = getOrderPriority(order_type)
357
325
  }
@@ -494,33 +462,61 @@ module.exports = class Orders {
494
462
  return stats
495
463
  }
496
464
  get_order_info_by_order_type(order_type) {
497
- let order_info_temp = {}
498
- for (let exchange in this.order_info) {
499
- for (let pair in this.order_info[exchange]) {
500
- for (let type in this.order_info[exchange][pair]) {
501
- for (let order_id in this.order_info[exchange][pair][type]) {
502
- const order_type_temp = _.get(this.order_info, [exchange, pair, type, order_id, 'order_type'])
503
- if ((Array.isArray(order_type) && order_type.includes(order_type_temp)) || order_type_temp === order_type) {
504
- _.setWith(order_info_temp, `${exchange}.${pair}.${type}.${order_id}`, _.get(this.order_info, `${exchange}.${pair}.${type}.${order_id}`), Object)
465
+ const types = Array.isArray(order_type) ? order_type : [order_type]
466
+ const result = {}
467
+ // Walk only the index sub-trees for the requested order_type(s). The
468
+ // returned dict is freshly built (preserving the original semantics that
469
+ // callers may add/remove top-level keys without affecting the canonical
470
+ // store) but the leaf order objects are still refs into this.order_info.
471
+ for (const t of types) {
472
+ const sub = this.order_info_by_type[t]
473
+ if (!sub) continue
474
+ for (const exchange in sub) {
475
+ const exch_sub = sub[exchange]
476
+ let result_exch = result[exchange]
477
+ if (!result_exch) {
478
+ result_exch = {}
479
+ result[exchange] = result_exch
480
+ }
481
+ for (const pair in exch_sub) {
482
+ const pair_sub = exch_sub[pair]
483
+ let result_pair = result_exch[pair]
484
+ if (!result_pair) {
485
+ result_pair = {}
486
+ result_exch[pair] = result_pair
487
+ }
488
+ for (const inner_type in pair_sub) {
489
+ const type_sub = pair_sub[inner_type]
490
+ let result_type = result_pair[inner_type]
491
+ if (!result_type) {
492
+ result_type = {}
493
+ result_pair[inner_type] = result_type
494
+ }
495
+ for (const order_id in type_sub) {
496
+ result_type[order_id] = type_sub[order_id]
505
497
  }
506
498
  }
507
499
  }
508
500
  }
509
501
  }
510
- return order_info_temp
502
+ return result
511
503
  }
512
504
  get_order_info_status_all(exchange, pair, type, status) {
513
505
  if (type === 'both') {
514
506
  return this.get_order_info_status_all(exchange, pair, 'sell', status).concat(this.get_order_info_status_all(exchange, pair, 'buy', status))
515
507
  }
516
- _.set(this.order_info_status, [exchange, pair, type, status], [])
517
- for (let order_id in _.get(this.order_info, [exchange, pair, type], {})) {
518
- let order_status = _.get(this.order_info, [exchange, pair, type, order_id, 'status'])
519
- if (order_status === status || _.includes(status, order_status)) {
520
- this.order_info_status[exchange][pair][type][status].push(this.order_info[exchange][pair][type][order_id])
508
+ const buckets_by_status = this.order_status_index[exchange]?.[pair]?.[type]
509
+ if (!buckets_by_status) return []
510
+ if (Array.isArray(status)) {
511
+ const result = []
512
+ for (const s of status) {
513
+ const bucket = buckets_by_status[s]
514
+ if (bucket) for (const order of bucket) result.push(order)
521
515
  }
516
+ return result
522
517
  }
523
- return this.order_info_status[exchange][pair][type][status]
518
+ const bucket = buckets_by_status[status]
519
+ return bucket ? Array.from(bucket) : []
524
520
  }
525
521
  get_order_info_status_by_order_type(exchange, pair, type, status, order_type) {
526
522
  return this.get_order_info_status_all(exchange, pair, type, status).filter((o) => o.order_type === order_type)
@@ -656,7 +652,7 @@ module.exports = class Orders {
656
652
  let order_info_order = this.order_info[exchange][pair][type][order_id]
657
653
  if (
658
654
  (order_info_order.status === 'N-FILLED' || order_info_order.status === 'P-FILLED') &&
659
- order_info_order.open_time < new Date().getTime() - this.no_trades_function_f_filled_timeout
655
+ order_info_order.open_time < Date.now() - this.no_trades_function_f_filled_timeout
660
656
  ) {
661
657
  let exist = false
662
658
  open_orders[exchange][pair].open_orders.map((o) => {
@@ -803,6 +799,11 @@ module.exports = class Orders {
803
799
  console.error('UPDATE %s: order_id invalid %s %s %s %s %j', note, exchange, pair, type, order_id, dict)
804
800
  return
805
801
  }
802
+
803
+ const existing = this.order_info[exchange]?.[pair]?.[type]?.[order_id]
804
+ const old_status = existing && existing.status
805
+ const old_order_type = existing && existing.order_type
806
+
806
807
  for (let key in dict) {
807
808
  if (key === 'f_percentage') {
808
809
  dict[key] = Math.round(dict[key] * 100) / 100
@@ -814,15 +815,37 @@ module.exports = class Orders {
814
815
  _.setWith(this.order_info, `${exchange}.${pair}.${type}.${order_id}.${key}`, dict[key], Object)
815
816
  }
816
817
 
817
- // Add trace entry for order history
818
- const trace_entry = {
819
- ts: Date.now(),
820
- status: dict.status || _.get(this.order_info, [exchange, pair, type, order_id, 'status']),
821
- note,
822
- f_amount: dict.f_amount,
818
+ const order = this.order_info[exchange][pair][type][order_id]
819
+ order.last_update_ts = Date.now()
820
+
821
+ const new_status = order.status
822
+ if (old_status !== new_status) {
823
+ if (old_status) {
824
+ const old_bucket = this.order_status_index[exchange]?.[pair]?.[type]?.[old_status]
825
+ if (old_bucket) old_bucket.delete(order)
826
+ }
827
+ if (new_status) {
828
+ const by_exch = this.order_status_index[exchange] || (this.order_status_index[exchange] = {})
829
+ const by_pair = by_exch[pair] || (by_exch[pair] = {})
830
+ const by_type = by_pair[type] || (by_pair[type] = {})
831
+ const bucket = by_type[new_status] || (by_type[new_status] = new Set())
832
+ bucket.add(order)
833
+ }
834
+ }
835
+ const new_order_type = order.order_type
836
+ if (old_order_type !== new_order_type) {
837
+ if (old_order_type) {
838
+ const at = this.order_info_by_type[old_order_type]?.[exchange]?.[pair]?.[type]
839
+ if (at) delete at[order_id]
840
+ }
841
+ if (new_order_type) {
842
+ const by_type = this.order_info_by_type[new_order_type] || (this.order_info_by_type[new_order_type] = {})
843
+ const by_exch = by_type[exchange] || (by_type[exchange] = {})
844
+ const by_pair = by_exch[pair] || (by_exch[pair] = {})
845
+ const by_inner_type = by_pair[type] || (by_pair[type] = {})
846
+ by_inner_type[order_id] = order
847
+ }
823
848
  }
824
- const existing_trace = _.get(this.order_info, [exchange, pair, type, order_id, 'trace'], [])
825
- _.setWith(this.order_info, `${exchange}.${pair}.${type}.${order_id}.trace`, [...existing_trace, trace_entry], Object)
826
849
 
827
850
  console.log('UPDATE %s: %s %s %s %s %j', note, exchange, pair, type, order_id, dict)
828
851
  // start: log every f-filled trade for future analysis
@@ -865,7 +888,19 @@ module.exports = class Orders {
865
888
  }
866
889
  delete_order_info(exchange, pair, type, order_id, note) {
867
890
  const order_data = _.get(this.order_info, [exchange, pair, type, order_id], {})
868
- this.order_info[exchange][pair][type] = _.omit(this.order_info[exchange][pair][type], [order_id])
891
+
892
+ if (order_data.status) {
893
+ const bucket = this.order_status_index[exchange]?.[pair]?.[type]?.[order_data.status]
894
+ if (bucket) bucket.delete(order_data)
895
+ }
896
+ if (order_data.order_type) {
897
+ const at = this.order_info_by_type[order_data.order_type]?.[exchange]?.[pair]?.[type]
898
+ if (at) delete at[order_id]
899
+ }
900
+
901
+ if (this.order_info[exchange]?.[pair]?.[type]) {
902
+ delete this.order_info[exchange][pair][type][order_id]
903
+ }
869
904
  console.log('DELETE %s: %s %s %s %s', note, exchange, pair, type, order_id)
870
905
 
871
906
  // Send delete event via WebSocket (only for non-INIT orders)
@@ -885,28 +920,17 @@ module.exports = class Orders {
885
920
  let balances_handler = (exchange, res) => {
886
921
  if (res.success) {
887
922
  _.set(this.balances, [exchange], Object.assign(res.body, { update_time: new Date() }))
888
- for (let exchange in this.exchange_cur_quote_cur) {
889
- for (let quote_cur in this.exchange_cur_quote_cur[exchange]) {
890
- this.exchange_cur_quote_cur[exchange][quote_cur].map((cur) => {
891
- let cur_balance = _.get(this.balances, [exchange, 'available', cur], 0)
892
- let quote_cur_balance = _.get(this.balances, [exchange, 'available', quote_cur], 0)
893
- let pair = cur + quote_cur
923
+ const cur_quote_cur = this.exchange_cur_quote_cur[exchange]
924
+ if (cur_quote_cur) {
925
+ for (let quote_cur in cur_quote_cur) {
926
+ cur_quote_cur[quote_cur].map((cur) => {
927
+ const cur_balance = _.get(this.balances, [exchange, 'available', cur], 0)
928
+ const quote_cur_balance = _.get(this.balances, [exchange, 'available', quote_cur], 0)
929
+ const pair = cur + quote_cur
894
930
  let max_cur_amount = cur_balance - _.get(this.min_reserve_balance, [exchange, cur], 0)
895
931
  let max_quote_cur_amount = quote_cur_balance - _.get(this.min_reserve_balance, [exchange, quote_cur], 0)
896
- if (max_cur_amount > 0) {
897
- } else {
898
- // if (cur_balance > 0) {
899
- // console.log('balance low', exchange, cur, cur_balance, this.min_reserve_balance[exchange][cur])
900
- // }
901
- max_cur_amount = 0
902
- }
903
- if (max_quote_cur_amount > 0) {
904
- } else {
905
- // if (quote_cur_balance > 0) {
906
- // console.log('balance low', exchange, quote_cur, quote_cur_balance, this.min_reserve_balance[exchange][quote_cur])
907
- // }
908
- max_quote_cur_amount = 0
909
- }
932
+ if (max_cur_amount < 0) max_cur_amount = 0
933
+ if (max_quote_cur_amount < 0) max_quote_cur_amount = 0
910
934
  _.set(this.buy_sell_limits_against_reserve_balance, [exchange, pair], {
911
935
  sell: max_cur_amount,
912
936
  buy: max_quote_cur_amount,
@@ -918,10 +942,10 @@ module.exports = class Orders {
918
942
  this.account_init = true
919
943
  this.init_account(this.balances, this.accounting_benchmark || this.balance_benchmark) // use accounting_benchmark if it exists
920
944
  }
921
- } else if (_.get(this.balances, [exchange, 'update_time']) < new Date().getTime() - this.interval_dict[exchange].balances) {
945
+ } else if (_.get(this.balances, [exchange, 'update_time']) < Date.now() - this.interval_dict[exchange].balances) {
922
946
  console.log('%s balances expired: %j', exchange, res)
923
- this.balances = _.omit(this.balances, [exchange])
924
- this.buy_sell_limits_against_reserve_balance = _.omit(this.buy_sell_limits_against_reserve_balance, [exchange])
947
+ delete this.balances[exchange]
948
+ delete this.buy_sell_limits_against_reserve_balance[exchange]
925
949
  }
926
950
  }
927
951
  function get_balances(Exchanges, interval_dict) {
@@ -949,9 +973,9 @@ module.exports = class Orders {
949
973
  let position_all_handler = (exchange, res) => {
950
974
  if (res.success) {
951
975
  _.set(this.positions, [exchange], Object.assign(res.body, { update_time: new Date() }))
952
- } else if (_.get(this.positions, [exchange, 'update_time']) < new Date().getTime() - this.interval_dict[exchange].positions) {
976
+ } else if (_.get(this.positions, [exchange, 'update_time']) < Date.now() - this.interval_dict[exchange].positions) {
953
977
  console.log('%s positions expired: %j', exchange, res)
954
- this.positions = _.omit(this.positions, [exchange])
978
+ delete this.positions[exchange]
955
979
  }
956
980
  }
957
981
  function get_positions(Exchanges, interval_dict) {
@@ -1018,18 +1042,21 @@ module.exports = class Orders {
1018
1042
  }
1019
1043
  }
1020
1044
  if (!this.previous_orders_canceled) {
1021
- let previous_orders_canceled = true
1022
- for (let exchange in this.exchange_pairs) {
1023
- this.exchange_pairs[exchange].map((pair) => {
1024
- if (!_.get(this.previous_orders_canceled_exchange_pair, [exchange, pair])) {
1025
- previous_orders_canceled = false
1045
+ let all_canceled = true
1046
+ outer: for (const exch of Object.keys(this.exchange_pairs)) {
1047
+ const pairs = this.exchange_pairs[exch]
1048
+ const exch_marks = this.previous_orders_canceled_exchange_pair[exch]
1049
+ for (let i = 0; i < pairs.length; i++) {
1050
+ if (!exch_marks || !exch_marks[pairs[i]]) {
1051
+ all_canceled = false
1052
+ break outer
1026
1053
  }
1027
- })
1054
+ }
1028
1055
  }
1029
- if (previous_orders_canceled) {
1056
+ if (all_canceled) {
1030
1057
  console.log('original order canceled')
1031
1058
  }
1032
- this.previous_orders_canceled = previous_orders_canceled
1059
+ this.previous_orders_canceled = all_canceled
1033
1060
  }
1034
1061
  }
1035
1062
  let rate_major = _.get(this.rates_combined_major, [pair])
@@ -1053,9 +1080,9 @@ module.exports = class Orders {
1053
1080
  if (res.success) {
1054
1081
  _.set(this.open_orders, [exchange, pair], { open_orders: res.body, update_time: new Date() })
1055
1082
  previous_orders_processer(exchange, pair, res.body)
1056
- } else if (_.get(this.open_orders, [exchange, pair, 'update_time']) < new Date().getTime() - this.interval_dict[exchange].open_orders) {
1083
+ } else if (_.get(this.open_orders, [exchange, pair, 'update_time']) < Date.now() - this.interval_dict[exchange].open_orders) {
1057
1084
  console.log('%s %s open orders expired: %j', exchange, pair, res)
1058
- this.open_orders[exchange] = _.omit(this.open_orders[exchange], [pair])
1085
+ if (this.open_orders[exchange]) delete this.open_orders[exchange][pair]
1059
1086
  } else if (res.error !== 'opening') {
1060
1087
  console.error('%s %s open orders: %j', exchange, pair, res)
1061
1088
  }
@@ -1070,12 +1097,12 @@ module.exports = class Orders {
1070
1097
  } else {
1071
1098
  let error_logged = false
1072
1099
  for (let pair of this.exchange_pairs[exchange]) {
1073
- if (_.get(this.open_orders, [exchange, pair, 'update_time']) < new Date().getTime() - this.interval_dict[exchange].open_orders) {
1100
+ if (_.get(this.open_orders, [exchange, pair, 'update_time']) < Date.now() - this.interval_dict[exchange].open_orders) {
1074
1101
  if (!error_logged) {
1075
1102
  console.log('%s all open orders expired: %j', exchange, res)
1076
1103
  error_logged = true
1077
1104
  }
1078
- this.open_orders[exchange] = _.omit(this.open_orders[exchange], [pair])
1105
+ if (this.open_orders[exchange]) delete this.open_orders[exchange][pair]
1079
1106
  } else if (res.error !== 'opening' && !error_logged) {
1080
1107
  console.error('%s all open orders: %j', exchange, res)
1081
1108
  error_logged = true
@@ -1118,11 +1145,6 @@ module.exports = class Orders {
1118
1145
  })
1119
1146
  }
1120
1147
  for (let exchange in Exchanges) {
1121
- if (Exchanges[exchange].exchange_type === 'dex') {
1122
- for (let pair of exchange_pairs[exchange]) {
1123
- _.set(this.previous_orders_canceled_exchange_pair, [exchange, pair], true)
1124
- }
1125
- }
1126
1148
  if (Exchanges[exchange].get_all_open_orders_ws) {
1127
1149
  if (Exchanges[exchange].ws_status().trading === 'n-opened') Exchanges[exchange].ws_account()
1128
1150
  get_all_open_orders_ws(exchange)
@@ -1151,13 +1173,13 @@ module.exports = class Orders {
1151
1173
  this.trades_amount_past_hour,
1152
1174
  [exchange, pair],
1153
1175
  _.sumBy(
1154
- res.body.filter((t) => t.close_time > new Date().getTime() - ONE_HOUR_IN_MS || t.open_time > new Date().getTime() - ONE_HOUR_IN_MS),
1176
+ res.body.filter((t) => t.close_time > Date.now() - ONE_HOUR_IN_MS || t.open_time > Date.now() - ONE_HOUR_IN_MS),
1155
1177
  (t) => t.amount,
1156
1178
  ) || 0,
1157
1179
  )
1158
- } else if (_.get(this.trades, [exchange, pair, 'update_time']) < new Date().getTime() - this.interval_dict[exchange].trades) {
1180
+ } else if (_.get(this.trades, [exchange, pair, 'update_time']) < Date.now() - this.interval_dict[exchange].trades) {
1159
1181
  console.log('%s %s trades expired: %j', exchange, pair, res)
1160
- this.trades[exchange] = _.omit(this.trades[exchange], [pair])
1182
+ if (this.trades[exchange]) delete this.trades[exchange][pair]
1161
1183
  } else if (res.error !== 'opening') {
1162
1184
  console.error('%s %s trades: %j', exchange, pair, res)
1163
1185
  }
@@ -1171,7 +1193,7 @@ module.exports = class Orders {
1171
1193
  this.trades_amount_past_hour,
1172
1194
  [exchange, pair],
1173
1195
  _.sumBy(
1174
- trades.filter((t) => t.close_time > new Date().getTime() - ONE_HOUR_IN_MS || t.open_time > new Date().getTime() - ONE_HOUR_IN_MS),
1196
+ trades.filter((t) => t.close_time > Date.now() - ONE_HOUR_IN_MS || t.open_time > Date.now() - ONE_HOUR_IN_MS),
1175
1197
  (t) => t.amount,
1176
1198
  ) || 0,
1177
1199
  )
@@ -1179,12 +1201,12 @@ module.exports = class Orders {
1179
1201
  } else if (res.error !== 'opening') {
1180
1202
  let error_logged = false
1181
1203
  for (let pair of this.exchange_pairs[exchange]) {
1182
- if (_.get(this.trades, [exchange, pair, 'update_time']) < new Date().getTime() - this.interval_dict[exchange].trades) {
1204
+ if (_.get(this.trades, [exchange, pair, 'update_time']) < Date.now() - this.interval_dict[exchange].trades) {
1183
1205
  if (!error_logged) {
1184
1206
  console.log('%s all trades expired: %j', exchange, res)
1185
1207
  error_logged = true
1186
1208
  }
1187
- this.trades[exchange] = _.omit(this.trades[exchange], [pair])
1209
+ if (this.trades[exchange]) delete this.trades[exchange][pair]
1188
1210
  } else if (res.error !== 'opening' && !error_logged) {
1189
1211
  console.error('%s all trades: %j', exchange, res)
1190
1212
  error_logged = true
@@ -1283,9 +1305,9 @@ module.exports = class Orders {
1283
1305
  let query_order_handler = (exchange, pair, res) => {
1284
1306
  if (res.success) {
1285
1307
  _.set(this.query_order, [exchange, pair], { query_order: res.body, update_time: new Date() })
1286
- } else if (_.get(this.query_order, [exchange, pair, 'update_time']) < new Date().getTime() - this.interval_dict[exchange].query_order) {
1308
+ } else if (_.get(this.query_order, [exchange, pair, 'update_time']) < Date.now() - this.interval_dict[exchange].query_order) {
1287
1309
  console.log('%s %s query_order expired: %j', exchange, pair, res)
1288
- this.query_order[exchange] = _.omit(this.query_order[exchange], [pair])
1310
+ if (this.query_order[exchange]) delete this.query_order[exchange][pair]
1289
1311
  } else if (res.error !== 'opening') {
1290
1312
  console.error('%s %s query_order: %j', exchange, pair, res)
1291
1313
  }
@@ -1341,7 +1363,7 @@ module.exports = class Orders {
1341
1363
  let exchanges_list = _.concat(this.pair_exchanges[pair] || [], this.pair_exchanges_rates_only[pair] || [])
1342
1364
  for (let exchange of exchanges_list) {
1343
1365
  if (_.get(this.rates, [exchange, pair, 'asks'], []).length > 0 && _.get(this.rates, [exchange, pair, 'bids'], []).length > 0) {
1344
- const rates = _.cloneDeep(this.rates[exchange][pair])
1366
+ const rates = this.rates[exchange][pair]
1345
1367
 
1346
1368
  let major_rates_only_exchanges_length = _.get(this.major_exchanges, [pair], []).length + _.get(this.rates_only_exchanges, [pair], []).length
1347
1369
  if (this.major_exchanges[pair] && _.includes(this.major_exchanges[pair], exchange)) {
@@ -1356,8 +1378,8 @@ module.exports = class Orders {
1356
1378
  }
1357
1379
  }
1358
1380
  } else {
1359
- orderbook_major = _.omit(orderbook_major, [exchange])
1360
- orderbook_major_temp = _.omit(orderbook_major_temp, [exchange])
1381
+ delete orderbook_major[exchange]
1382
+ delete orderbook_major_temp[exchange]
1361
1383
  }
1362
1384
  }
1363
1385
 
@@ -1365,7 +1387,7 @@ module.exports = class Orders {
1365
1387
  if (o_3.asks.length > 0 && o_3.bids.length > 0) {
1366
1388
  _.set(this.rates_combined_major, [pair], o_3)
1367
1389
  } else {
1368
- this.rates_combined_major = _.omit(this.rates_combined_major, [pair])
1390
+ delete this.rates_combined_major[pair]
1369
1391
  }
1370
1392
 
1371
1393
  const reference_pair = this.pair_map_rates_only[pair]
@@ -1376,14 +1398,9 @@ module.exports = class Orders {
1376
1398
  this.rates_observable.notify()
1377
1399
  }
1378
1400
  const throttled_process_rates = _.throttle(process_rates, PROCESS_RATES_THROTTLE_MS, { leading: true, trailing: true })
1379
- let rates_handler = (exchange, pair, res, rates_with_routes = false) => {
1401
+ let rates_handler = (exchange, pair, res) => {
1380
1402
  if (res.success) {
1381
1403
  let { asks, bids } = res.body
1382
- if (rates_with_routes) {
1383
- _.set(this.rates_with_routes, [exchange, pair], { asks, bids, update_time: new Date() })
1384
- asks = asks.map((ask) => ask.slice(0, 2))
1385
- bids = bids.map((bid) => bid.slice(0, 2))
1386
- }
1387
1404
  asks = asks.slice(0, RATES_LENGTH_MAX)
1388
1405
  bids = bids.slice(0, RATES_LENGTH_MAX)
1389
1406
  _.set(this.rates, [exchange, pair], { asks, bids, update_time: new Date() })
@@ -1391,9 +1408,9 @@ module.exports = class Orders {
1391
1408
  * throttle to 20ms
1392
1409
  */
1393
1410
  throttled_process_rates()
1394
- } else if (_.get(this.rates, [exchange, pair, 'update_time']) < new Date().getTime() - this.interval_dict[exchange].rates) {
1411
+ } else if (_.get(this.rates, [exchange, pair, 'update_time']) < Date.now() - this.interval_dict[exchange].rates) {
1395
1412
  console.log('%s %s rates expired: %j', exchange, pair, res)
1396
- this.rates[exchange] = _.omit(this.rates[exchange], [pair])
1413
+ if (this.rates[exchange]) delete this.rates[exchange][pair]
1397
1414
  this.rates_observable.notify()
1398
1415
  }
1399
1416
  }
@@ -1412,19 +1429,6 @@ module.exports = class Orders {
1412
1429
  rates_handler(exchange, pair, res)
1413
1430
  })
1414
1431
  }
1415
- } else if (Exchanges[exchange].ws_market && Exchanges[exchange].exchange_type === 'dex') {
1416
- setInterval(() => {
1417
- if (!this.ws_market_first_time[exchange]) {
1418
- Exchanges[exchange].ws_market(exchange_pairs[exchange], this.ws_market_options)
1419
- this.ws_market_first_time[exchange] = true
1420
- } else {
1421
- for (let pair of exchange_pairs[exchange]) {
1422
- Exchanges[exchange].rate_with_routes_ws(pair, (res) => {
1423
- rates_handler(exchange, pair, res, true)
1424
- })
1425
- }
1426
- }
1427
- }, interval_dict[exchange].rates)
1428
1432
  } else if (Exchanges[exchange].exchange_type === 'spot') {
1429
1433
  for (let pair of exchange_pairs[exchange]) {
1430
1434
  setInterval(() => {
@@ -1433,14 +1437,6 @@ module.exports = class Orders {
1433
1437
  })
1434
1438
  }, interval_dict[exchange].rates)
1435
1439
  }
1436
- } else if (Exchanges[exchange].exchange_type === 'dex') {
1437
- for (let pair of exchange_pairs[exchange]) {
1438
- setInterval(() => {
1439
- Exchanges[exchange].rate_with_routes(pair, (res) => {
1440
- rates_handler(exchange, pair, res, true)
1441
- })
1442
- }, interval_dict[exchange].rates)
1443
- }
1444
1440
  }
1445
1441
  }
1446
1442
  for (let exchange in exchange_pairs_rates_only) {
@@ -1471,9 +1467,9 @@ module.exports = class Orders {
1471
1467
  let market_history_handler = (exchange, pair, res) => {
1472
1468
  if (res.success) {
1473
1469
  _.set(this.market_history, [exchange, pair], { data: res.body, update_time: new Date() })
1474
- } else if (_.get(this.market_history, [exchange, pair, 'update_time']) < new Date().getTime() - this.interval_dict[exchange].market_history) {
1470
+ } else if (_.get(this.market_history, [exchange, pair, 'update_time']) < Date.now() - this.interval_dict[exchange].market_history) {
1475
1471
  console.log('%s %s market_history expired: %j', exchange, pair, res)
1476
- this.market_history[exchange] = _.omit(this.market_history[exchange], [pair])
1472
+ if (this.market_history[exchange]) delete this.market_history[exchange][pair]
1477
1473
  }
1478
1474
  }
1479
1475
  let get_market_history = (Exchanges, exchange_pairs, interval_dict) => {
@@ -1508,13 +1504,13 @@ module.exports = class Orders {
1508
1504
  if (res.success) {
1509
1505
  if (res.body.length < OHLCV_DEFAULT_PAST_HOURS) {
1510
1506
  console.error('ohlcv length should be:', OHLCV_DEFAULT_PAST_HOURS, exchange, pair, res.body.length, _.first(res.body), _.last(res.body))
1511
- this.ohlcv[exchange] = _.omit(this.ohlcv[exchange], [pair])
1507
+ if (this.ohlcv[exchange]) delete this.ohlcv[exchange][pair]
1512
1508
  } else {
1513
1509
  _.set(this.ohlcv, [exchange, pair], { data: res.body, update_time: new Date() })
1514
1510
  }
1515
- } else if (_.get(this.ohlcv, [exchange, pair, 'update_time']) < new Date().getTime() - this.interval_dict[exchange].ohlcv) {
1511
+ } else if (_.get(this.ohlcv, [exchange, pair, 'update_time']) < Date.now() - this.interval_dict[exchange].ohlcv) {
1516
1512
  console.log('%s %s ohlcv expired: %j', exchange, pair, res)
1517
- this.ohlcv[exchange] = _.omit(this.ohlcv[exchange], [pair])
1513
+ if (this.ohlcv[exchange]) delete this.ohlcv[exchange][pair]
1518
1514
  }
1519
1515
  }
1520
1516
  let get_ohlcv = (Exchanges, exchange_pairs, interval_dict) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icgio/icg-exchanges-wrapper",
3
- "version": "1.17.14",
3
+ "version": "1.17.15",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -18,7 +18,7 @@
18
18
  "homepage": "https://github.com/icgio/icg-exchanges-wrapper#readme",
19
19
  "dependencies": {
20
20
  "@icgio/icg-exchanges": "^1.40.29",
21
- "@icgio/icg-utils": "^1.9.53",
21
+ "@icgio/icg-utils": "^1.9.55",
22
22
  "lodash": "^4.17.23",
23
23
  "pg": "^8.17.2"
24
24
  }