@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
|
}
|
package/middleware/orders.js
CHANGED
|
@@ -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.
|
|
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 (
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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 <
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
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
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
|
897
|
-
|
|
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']) <
|
|
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
|
-
|
|
924
|
-
|
|
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']) <
|
|
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
|
-
|
|
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
|
|
1022
|
-
for (
|
|
1023
|
-
this.exchange_pairs[
|
|
1024
|
-
|
|
1025
|
-
|
|
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 (
|
|
1056
|
+
if (all_canceled) {
|
|
1030
1057
|
console.log('original order canceled')
|
|
1031
1058
|
}
|
|
1032
|
-
this.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']) <
|
|
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]
|
|
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']) <
|
|
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]
|
|
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 >
|
|
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']) <
|
|
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]
|
|
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 >
|
|
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']) <
|
|
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]
|
|
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']) <
|
|
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]
|
|
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 =
|
|
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
|
-
|
|
1360
|
-
|
|
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
|
-
|
|
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
|
|
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']) <
|
|
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]
|
|
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']) <
|
|
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]
|
|
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]
|
|
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']) <
|
|
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]
|
|
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.
|
|
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.
|
|
21
|
+
"@icgio/icg-utils": "^1.9.55",
|
|
22
22
|
"lodash": "^4.17.23",
|
|
23
23
|
"pg": "^8.17.2"
|
|
24
24
|
}
|