@icgio/icg-exchanges-wrapper 1.17.20 → 1.17.22
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.
- package/docs/2026-04-09-perf-cleanup.md +366 -0
- package/middleware/orders.js +163 -69
- package/package.json +3 -3
|
@@ -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.
|
package/middleware/orders.js
CHANGED
|
@@ -73,6 +73,7 @@ const MARKET_HISTORY_DEFAULT_MS = 10 * 60 * 1000
|
|
|
73
73
|
const TRADES_DEFAULT_MS = 7 * 24 * 60 * 60 * 1000
|
|
74
74
|
const NO_CALLBACK_TIMEOUT_MS = 2 * 60 * 1000
|
|
75
75
|
const PROCESS_RATES_THROTTLE_MS = 75
|
|
76
|
+
const MAX_ORDER_TRACE_ENTRIES = 20
|
|
76
77
|
const ORDER_TYPE_PRIORITY = {
|
|
77
78
|
self: 2,
|
|
78
79
|
maker_1: 3,
|
|
@@ -89,6 +90,13 @@ const getOrderPriority = (order_type, is_cancel = false) => {
|
|
|
89
90
|
return is_cancel ? priority - 0.5 : priority
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
const hrtime_diff = (start, end) => {
|
|
94
|
+
if (!Array.isArray(start) || !Array.isArray(end)) {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
return (end[0] - start[0]) * 1e9 + (end[1] - start[1])
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
module.exports = class Orders {
|
|
93
101
|
constructor(exchange_cur_quote_cur_dict, cd, settings) {
|
|
94
102
|
let { exchange_cur_quote_cur, exchange_cur_quote_cur_rates_only, pair_map_rates_only } = exchange_cur_quote_cur_dict
|
|
@@ -272,72 +280,135 @@ module.exports = class Orders {
|
|
|
272
280
|
ws_client.set_order_info_ref(this.order_info)
|
|
273
281
|
}
|
|
274
282
|
}
|
|
283
|
+
build_trace_entry(order, note) {
|
|
284
|
+
return {
|
|
285
|
+
ts: Date.now(),
|
|
286
|
+
status: order.status,
|
|
287
|
+
note,
|
|
288
|
+
f_amount: order.f_amount,
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
append_trace(order, note) {
|
|
292
|
+
if (!note || !order) return
|
|
293
|
+
if (!Array.isArray(order.trace)) {
|
|
294
|
+
order.trace = []
|
|
295
|
+
}
|
|
296
|
+
order.trace.push(this.build_trace_entry(order, note))
|
|
297
|
+
if (order.trace.length > MAX_ORDER_TRACE_ENTRIES) {
|
|
298
|
+
order.trace.splice(0, order.trace.length - MAX_ORDER_TRACE_ENTRIES)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
move_order_info_key(exchange, pair, type, from_order_id, to_order_id) {
|
|
302
|
+
if (!from_order_id || !to_order_id || from_order_id === to_order_id) {
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
const from_bucket = this.order_info?.[exchange]?.[pair]?.[type]
|
|
306
|
+
if (!from_bucket || !from_bucket[from_order_id]) {
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
const order = from_bucket[from_order_id]
|
|
310
|
+
from_bucket[to_order_id] = order
|
|
311
|
+
delete from_bucket[from_order_id]
|
|
312
|
+
if (order.order_type) {
|
|
313
|
+
const by_type = this.order_info_by_type?.[order.order_type]?.[exchange]?.[pair]?.[type]
|
|
314
|
+
if (by_type) {
|
|
315
|
+
by_type[to_order_id] = order
|
|
316
|
+
delete by_type[from_order_id]
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
275
320
|
init_account(balances, benchmarks) {
|
|
276
321
|
this.account = new Account(balances, benchmarks)
|
|
277
322
|
}
|
|
278
323
|
update_non_initial_from_balances(balances) {
|
|
279
324
|
this.account?.update_non_initial_from_balances(balances)
|
|
280
325
|
}
|
|
281
|
-
limit_trade(exchange, pair, method, price, amount, id = uuid(), order_type = '', limiter_on = true, api_key_index, order_options = {}, decision_context) {
|
|
326
|
+
limit_trade(exchange, pair, method, price, amount, id = uuid(), order_type = '', limiter_on = true, api_key_index, order_options = {}, decision_context = null) {
|
|
282
327
|
price = round_price(exchange, pair, price)
|
|
283
328
|
amount = round_amount(exchange, pair, amount)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
status
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
this.update_order_info(
|
|
311
|
-
exchange,
|
|
312
|
-
pair,
|
|
313
|
-
method,
|
|
314
|
-
id,
|
|
315
|
-
{
|
|
316
|
-
status: 'N-OPENED-TIMEOUT',
|
|
317
|
-
},
|
|
318
|
-
order_type + ' n-opened-timeout-init',
|
|
319
|
-
)
|
|
329
|
+
const internal_id = id
|
|
330
|
+
let no_callback_timeout = null
|
|
331
|
+
let init_emitted = false
|
|
332
|
+
const start_no_callback_timeout = () => {
|
|
333
|
+
if (no_callback_timeout) {
|
|
334
|
+
clearTimeout(no_callback_timeout)
|
|
335
|
+
}
|
|
336
|
+
no_callback_timeout = setTimeout(() => {
|
|
337
|
+
if (_.get(this.order_info, [exchange, pair, method, internal_id, 'status']) === 'INIT') {
|
|
338
|
+
console.log('no callback: %s %s %s %s %s', exchange, pair, method, internal_id, order_type)
|
|
339
|
+
this.update_order_info(
|
|
340
|
+
exchange,
|
|
341
|
+
pair,
|
|
342
|
+
method,
|
|
343
|
+
internal_id,
|
|
344
|
+
{
|
|
345
|
+
status: 'N-OPENED-TIMEOUT',
|
|
346
|
+
},
|
|
347
|
+
order_type + ' n-opened-timeout-init',
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
}, NO_CALLBACK_TIMEOUT_MS)
|
|
351
|
+
}
|
|
352
|
+
const emit_init = ({ t_order_sent } = {}) => {
|
|
353
|
+
if (init_emitted) {
|
|
354
|
+
return
|
|
320
355
|
}
|
|
321
|
-
|
|
356
|
+
init_emitted = true
|
|
357
|
+
const init_decision_context = decision_context ? { ...decision_context } : null
|
|
358
|
+
const t2t_ns = hrtime_diff(init_decision_context?.t_decision_hr, t_order_sent)
|
|
359
|
+
if (init_decision_context) {
|
|
360
|
+
delete init_decision_context.t_decision_hr
|
|
361
|
+
}
|
|
362
|
+
this.update_order_info(
|
|
363
|
+
exchange,
|
|
364
|
+
pair,
|
|
365
|
+
method,
|
|
366
|
+
internal_id,
|
|
367
|
+
{
|
|
368
|
+
order_type,
|
|
369
|
+
status: 'INIT',
|
|
370
|
+
id: internal_id,
|
|
371
|
+
internal_id,
|
|
372
|
+
order_id: null,
|
|
373
|
+
type: method,
|
|
374
|
+
price,
|
|
375
|
+
amount,
|
|
376
|
+
f_amount: 0,
|
|
377
|
+
f_percentage: 0,
|
|
378
|
+
t_amount: amount,
|
|
379
|
+
open_time: Date.now(),
|
|
380
|
+
close_time: null,
|
|
381
|
+
decision_context: init_decision_context,
|
|
382
|
+
t2t_ns,
|
|
383
|
+
event_hrtime: t_order_sent,
|
|
384
|
+
},
|
|
385
|
+
order_type + ' init',
|
|
386
|
+
)
|
|
387
|
+
start_no_callback_timeout()
|
|
388
|
+
}
|
|
322
389
|
let limit_trade = (cb) => {
|
|
390
|
+
const order_options_with_meta = Object.assign({}, order_options, {
|
|
391
|
+
on_sent: ({ t_order_sent }) => {
|
|
392
|
+
emit_init({ t_order_sent })
|
|
393
|
+
},
|
|
394
|
+
})
|
|
323
395
|
if (limiter_on) {
|
|
324
396
|
if (this.Exchanges[exchange].private_limiter) {
|
|
325
397
|
this.Exchanges[exchange].private_limiter.priority = getOrderPriority(order_type)
|
|
326
398
|
}
|
|
327
|
-
this.Exchanges[exchange].limit_trade(pair, method, price, amount,
|
|
399
|
+
this.Exchanges[exchange].limit_trade(pair, method, price, amount, order_options_with_meta, cb)
|
|
328
400
|
} else {
|
|
329
|
-
this.Exchanges[exchange].limit_trade_base(pair, method, price, amount,
|
|
401
|
+
this.Exchanges[exchange].limit_trade_base(pair, method, price, amount, order_options_with_meta, cb)
|
|
330
402
|
}
|
|
331
403
|
}
|
|
332
404
|
if (api_key_index) {
|
|
333
405
|
this.Exchanges[exchange].key_index = api_key_index
|
|
334
406
|
}
|
|
335
|
-
const t_order_sent = process.hrtime()
|
|
336
407
|
limit_trade((res) => {
|
|
337
|
-
const t_ack_recv = process.hrtime()
|
|
338
408
|
if (res.success) {
|
|
339
409
|
let order_id = res.body
|
|
340
|
-
|
|
410
|
+
emit_init(res.meta || {})
|
|
411
|
+
this.move_order_info_key(exchange, pair, method, internal_id, order_id)
|
|
341
412
|
this.update_order_info(
|
|
342
413
|
exchange,
|
|
343
414
|
pair,
|
|
@@ -346,7 +417,8 @@ module.exports = class Orders {
|
|
|
346
417
|
{
|
|
347
418
|
order_type,
|
|
348
419
|
status: 'N-FILLED',
|
|
349
|
-
id,
|
|
420
|
+
id: internal_id,
|
|
421
|
+
internal_id,
|
|
350
422
|
order_id,
|
|
351
423
|
type: method,
|
|
352
424
|
price,
|
|
@@ -354,31 +426,32 @@ module.exports = class Orders {
|
|
|
354
426
|
f_amount: 0,
|
|
355
427
|
f_percentage: 0,
|
|
356
428
|
t_amount: amount,
|
|
357
|
-
open_time: Date.now(),
|
|
358
429
|
close_time: null,
|
|
359
|
-
t_order_sent,
|
|
360
|
-
t_ack_recv,
|
|
430
|
+
order_rtt_ns: hrtime_diff(_.get(res, ['meta', 't_order_sent']), _.get(res, ['meta', 't_ack_recv'])),
|
|
431
|
+
event_hrtime: _.get(res, ['meta', 't_ack_recv']),
|
|
361
432
|
},
|
|
362
433
|
order_type + ' n-filled',
|
|
363
434
|
)
|
|
364
435
|
} else if (res.error === 'amount-too-small' || res.error === 'insufficient-funds') {
|
|
365
|
-
this.delete_order_info(exchange, pair, method,
|
|
436
|
+
this.delete_order_info(exchange, pair, method, internal_id, order_type + ' limit-trade-' + res.error)
|
|
366
437
|
} else if (res.error === 'connection-error') {
|
|
367
438
|
setTimeout(() => {
|
|
368
|
-
this.delete_order_info(exchange, pair, method,
|
|
439
|
+
this.delete_order_info(exchange, pair, method, internal_id, order_type + ' limit-trade-' + res.error)
|
|
369
440
|
}, this.delete_connection_error_orders_timeout)
|
|
370
441
|
} else {
|
|
371
442
|
if (order_type.startsWith('self') || order_type.startsWith('maker_2')) {
|
|
372
|
-
this.delete_order_info(exchange, pair, method,
|
|
443
|
+
this.delete_order_info(exchange, pair, method, internal_id, order_type + ' limit-trade-timeout')
|
|
373
444
|
}
|
|
374
445
|
// else if (_.get(this.order_info, [exchange, pair, method, id, 'status']) === 'INIT') { // make sure no_callback_timeout is NOT running zero
|
|
375
446
|
// this.update_order_info(exchange, pair, method, id, {
|
|
376
447
|
// status: 'N-OPENED-TIMEOUT'
|
|
377
448
|
// }, order_type + ' n-opened-timeout-callback')
|
|
378
449
|
// }
|
|
379
|
-
console.error('limit trade error: %s %s %s %s %s %s %s %j', order_type,
|
|
450
|
+
console.error('limit trade error: %s %s %s %s %s %s %s %j', order_type, internal_id, exchange, pair, method, price, amount, res)
|
|
451
|
+
}
|
|
452
|
+
if (no_callback_timeout) {
|
|
453
|
+
clearTimeout(no_callback_timeout)
|
|
380
454
|
}
|
|
381
|
-
clearTimeout(no_callback_timeout)
|
|
382
455
|
})
|
|
383
456
|
}
|
|
384
457
|
cancel_order(exchange, pair, order) {
|
|
@@ -407,6 +480,8 @@ module.exports = class Orders {
|
|
|
407
480
|
{
|
|
408
481
|
status: 'CANCELED',
|
|
409
482
|
close_time: Date.now(),
|
|
483
|
+
cancel_latency_ns: hrtime_diff(_.get(res, ['meta', 't_cancel_sent']), _.get(res, ['meta', 't_cancel_ack'])),
|
|
484
|
+
event_hrtime: _.get(res, ['meta', 't_cancel_ack']),
|
|
410
485
|
},
|
|
411
486
|
order.order_type + ' canceled',
|
|
412
487
|
)
|
|
@@ -557,8 +632,9 @@ module.exports = class Orders {
|
|
|
557
632
|
amount: 0,
|
|
558
633
|
f_amount: trade_amount,
|
|
559
634
|
f_percentage: trade_amount / order_info.t_amount,
|
|
560
|
-
close_time: trade.close_time
|
|
635
|
+
close_time: trade.close_time ? new Date(trade.close_time).getTime() : Date.now(),
|
|
561
636
|
fees: trade.fees,
|
|
637
|
+
event_hrtime: process.hrtime(),
|
|
562
638
|
},
|
|
563
639
|
order_info.order_type + ' canceled-order-filled(-more)',
|
|
564
640
|
)
|
|
@@ -597,8 +673,9 @@ module.exports = class Orders {
|
|
|
597
673
|
amount: order_info.t_amount - trade_amount,
|
|
598
674
|
f_amount: trade.amount,
|
|
599
675
|
f_percentage: trade_amount / order_info.t_amount,
|
|
600
|
-
close_time: trade.close_time
|
|
676
|
+
close_time: trade.close_time ? new Date(trade.close_time).getTime() : Date.now(),
|
|
601
677
|
fees: trade.fees,
|
|
678
|
+
event_hrtime: process.hrtime(),
|
|
602
679
|
},
|
|
603
680
|
order_info.order_type + ' order-f-filled-1',
|
|
604
681
|
)
|
|
@@ -684,6 +761,7 @@ module.exports = class Orders {
|
|
|
684
761
|
f_amount: order_info_order.t_amount,
|
|
685
762
|
f_percentage: 1,
|
|
686
763
|
close_time: Date.now(),
|
|
764
|
+
event_hrtime: process.hrtime(),
|
|
687
765
|
},
|
|
688
766
|
order_info_order.order_type + ' order-f-filled-2',
|
|
689
767
|
)
|
|
@@ -718,7 +796,8 @@ module.exports = class Orders {
|
|
|
718
796
|
amount: order.amount,
|
|
719
797
|
f_amount: order_f_amount,
|
|
720
798
|
f_percentage: order_f_amount / order_info.t_amount,
|
|
721
|
-
close_time: order.close_time
|
|
799
|
+
close_time: order.close_time ? new Date(order.close_time).getTime() : Date.now(),
|
|
800
|
+
event_hrtime: process.hrtime(),
|
|
722
801
|
},
|
|
723
802
|
order_info.order_type + ' order-f-filled-3',
|
|
724
803
|
)
|
|
@@ -749,8 +828,8 @@ module.exports = class Orders {
|
|
|
749
828
|
this.get_order_info_status_all(exchange, pair, 'both', 'INIT').map((init_order) => {
|
|
750
829
|
let init_order_price = round_price(exchange, pair, init_order.price)
|
|
751
830
|
let order_price = round_price(exchange, pair, order.price)
|
|
752
|
-
let init_order_time = init_order.open_time.getTime()
|
|
753
|
-
let order_time = order.open_time.getTime()
|
|
831
|
+
let init_order_time = typeof init_order.open_time === 'number' ? init_order.open_time : new Date(init_order.open_time).getTime()
|
|
832
|
+
let order_time = typeof order.open_time === 'number' ? order.open_time : new Date(order.open_time).getTime()
|
|
754
833
|
if (
|
|
755
834
|
init_order.type === order.type &&
|
|
756
835
|
init_order_price === order_price &&
|
|
@@ -758,7 +837,7 @@ module.exports = class Orders {
|
|
|
758
837
|
order_time - init_order_time < this.check_init_order_opened_timeout
|
|
759
838
|
) {
|
|
760
839
|
// console.log('ORDERS: %s %s %s %s INIT ORDER OPENED', init_order.order_id, order.order_id, exchange, pair)
|
|
761
|
-
this.
|
|
840
|
+
this.move_order_info_key(exchange, pair, init_order.type, init_order.internal_id || init_order.id, order.order_id)
|
|
762
841
|
this.update_order_info(
|
|
763
842
|
exchange,
|
|
764
843
|
pair,
|
|
@@ -772,8 +851,9 @@ module.exports = class Orders {
|
|
|
772
851
|
amount: order.amount,
|
|
773
852
|
f_amount: 0,
|
|
774
853
|
t_amount: order.amount,
|
|
775
|
-
open_time: order.open_time
|
|
854
|
+
open_time: order.open_time ? new Date(order.open_time).getTime() : Date.now(),
|
|
776
855
|
close_time: null,
|
|
856
|
+
event_hrtime: process.hrtime(),
|
|
777
857
|
},
|
|
778
858
|
init_order.order_type + ' init-order-opened',
|
|
779
859
|
)
|
|
@@ -794,6 +874,7 @@ module.exports = class Orders {
|
|
|
794
874
|
{
|
|
795
875
|
status: 'CANCELED',
|
|
796
876
|
close_time: Date.now(),
|
|
877
|
+
event_hrtime: process.hrtime(),
|
|
797
878
|
},
|
|
798
879
|
(order_info.order_type || '') + ' canceling-canceled-from-open-orders',
|
|
799
880
|
)
|
|
@@ -814,6 +895,8 @@ module.exports = class Orders {
|
|
|
814
895
|
const existing = this.order_info[exchange]?.[pair]?.[type]?.[order_id]
|
|
815
896
|
const old_status = existing && existing.status
|
|
816
897
|
const old_order_type = existing && existing.order_type
|
|
898
|
+
const event_hrtime = dict.event_hrtime
|
|
899
|
+
delete dict.event_hrtime
|
|
817
900
|
|
|
818
901
|
for (let key in dict) {
|
|
819
902
|
if (key === 'f_percentage') {
|
|
@@ -827,7 +910,15 @@ module.exports = class Orders {
|
|
|
827
910
|
}
|
|
828
911
|
|
|
829
912
|
const order = this.order_info[exchange][pair][type][order_id]
|
|
913
|
+
if (order.internal_id == null && order.id != null) {
|
|
914
|
+
order.internal_id = order.id
|
|
915
|
+
}
|
|
916
|
+
if (order.trace == null) {
|
|
917
|
+
order.trace = []
|
|
918
|
+
}
|
|
919
|
+
order.lifecycle_seq = existing && Number.isFinite(existing.lifecycle_seq) ? existing.lifecycle_seq + 1 : dict.lifecycle_seq || 1
|
|
830
920
|
order.last_update_ts = Date.now()
|
|
921
|
+
this.append_trace(order, note)
|
|
831
922
|
|
|
832
923
|
const new_status = order.status
|
|
833
924
|
if (old_status !== new_status) {
|
|
@@ -884,14 +975,16 @@ module.exports = class Orders {
|
|
|
884
975
|
|
|
885
976
|
// Send order update via WebSocket to dashboard backend
|
|
886
977
|
const order_data = _.get(this.order_info, [exchange, pair, type, order_id], {})
|
|
887
|
-
|
|
888
|
-
if (dict.status !== 'INIT' && order_data.status !== 'INIT' && ws_client) {
|
|
978
|
+
if (ws_client) {
|
|
889
979
|
ws_client.send_order({
|
|
890
980
|
action: 'update',
|
|
891
981
|
exchange,
|
|
892
982
|
pair,
|
|
893
983
|
side: type,
|
|
894
984
|
order_id,
|
|
985
|
+
internal_id: order_data.internal_id,
|
|
986
|
+
lifecycle_seq: order_data.lifecycle_seq,
|
|
987
|
+
event_hrtime,
|
|
895
988
|
data: order_data,
|
|
896
989
|
note,
|
|
897
990
|
})
|
|
@@ -914,14 +1007,16 @@ module.exports = class Orders {
|
|
|
914
1007
|
}
|
|
915
1008
|
console.log('DELETE %s: %s %s %s %s', note, exchange, pair, type, order_id)
|
|
916
1009
|
|
|
917
|
-
|
|
918
|
-
if (order_data.status !== 'INIT' && ws_client) {
|
|
1010
|
+
if (ws_client && order_data.internal_id) {
|
|
919
1011
|
ws_client.send_order({
|
|
920
1012
|
action: 'delete',
|
|
921
1013
|
exchange,
|
|
922
1014
|
pair,
|
|
923
1015
|
side: type,
|
|
924
1016
|
order_id,
|
|
1017
|
+
internal_id: order_data.internal_id,
|
|
1018
|
+
lifecycle_seq: (order_data.lifecycle_seq || 0) + 1,
|
|
1019
|
+
event_hrtime: process.hrtime(),
|
|
925
1020
|
data: order_data,
|
|
926
1021
|
note,
|
|
927
1022
|
})
|
|
@@ -1411,16 +1506,15 @@ module.exports = class Orders {
|
|
|
1411
1506
|
const throttled_process_rates = _.throttle(process_rates, PROCESS_RATES_THROTTLE_MS, { leading: true, trailing: true })
|
|
1412
1507
|
let rates_handler = (exchange, pair, res) => {
|
|
1413
1508
|
if (res.success) {
|
|
1414
|
-
let { asks, bids } = res.body
|
|
1509
|
+
let { asks, bids, _ts } = res.body
|
|
1415
1510
|
asks = asks.slice(0, RATES_LENGTH_MAX)
|
|
1416
1511
|
bids = bids.slice(0, RATES_LENGTH_MAX)
|
|
1417
|
-
|
|
1418
|
-
_.set(this.rates, [exchange, pair], { asks, bids, _ts })
|
|
1512
|
+
_.set(this.rates, [exchange, pair], { asks, bids, _ts, update_time: new Date() })
|
|
1419
1513
|
/*
|
|
1420
1514
|
* throttle to 20ms
|
|
1421
1515
|
*/
|
|
1422
1516
|
throttled_process_rates()
|
|
1423
|
-
} else if (_.get(this.rates, [exchange, pair, '
|
|
1517
|
+
} else if (_.get(this.rates, [exchange, pair, 'update_time']) < Date.now() - this.interval_dict[exchange].rates) {
|
|
1424
1518
|
console.log('%s %s rates expired: %j', exchange, pair, res)
|
|
1425
1519
|
if (this.rates[exchange]) delete this.rates[exchange][pair]
|
|
1426
1520
|
this.rates_observable.notify()
|
|
@@ -1529,13 +1623,13 @@ module.exports = class Orders {
|
|
|
1529
1623
|
for (let exchange in exchange_pairs) {
|
|
1530
1624
|
for (let pair of exchange_pairs[exchange]) {
|
|
1531
1625
|
if (Exchanges[exchange].ohlcv) {
|
|
1532
|
-
const ohlcv_from_timetamp = new Date().floorHours(1).getTime()
|
|
1626
|
+
const ohlcv_from_timetamp = new Date().floorHours(1).addHours(-OHLCV_DEFAULT_PAST_HOURS).getTime()
|
|
1533
1627
|
const ohlcv_to_timestamp = new Date().floorHours(1).getTime()
|
|
1534
1628
|
Exchanges[exchange].ohlcv(pair, ohlcv_from_timetamp, ohlcv_to_timestamp, (res) => {
|
|
1535
1629
|
ohlcv_handler(exchange, pair, res)
|
|
1536
1630
|
})
|
|
1537
1631
|
setInterval(() => {
|
|
1538
|
-
const ohlcv_from_timetamp = new Date().floorHours(1).getTime()
|
|
1632
|
+
const ohlcv_from_timetamp = new Date().floorHours(1).addHours(-OHLCV_DEFAULT_PAST_HOURS).getTime()
|
|
1539
1633
|
const ohlcv_to_timestamp = new Date().floorHours(1).getTime()
|
|
1540
1634
|
Exchanges[exchange].ohlcv(pair, ohlcv_from_timetamp, ohlcv_to_timestamp, (res) => {
|
|
1541
1635
|
ohlcv_handler(exchange, pair, res)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@icgio/icg-exchanges-wrapper",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.22",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"homepage": "https://github.com/icgio/icg-exchanges-wrapper#readme",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@icgio/icg-exchanges": "^1.40.
|
|
21
|
-
"@icgio/icg-utils": "^1.9.
|
|
20
|
+
"@icgio/icg-exchanges": "^1.40.45",
|
|
21
|
+
"@icgio/icg-utils": "^1.9.61",
|
|
22
22
|
"lodash": "^4.17.23",
|
|
23
23
|
"pg": "^8.17.2"
|
|
24
24
|
}
|