@icgio/icg-exchanges-wrapper 1.17.21 → 1.17.23

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.
Files changed (2) hide show
  1. package/middleware/orders.js +212 -66
  2. package/package.json +2 -2
@@ -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,36 @@ 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
+
100
+ const is_finite_number = (value) => value != null && Number.isFinite(Number(value))
101
+
102
+ const normalize_market_ts = (raw_market_ts) => {
103
+ if (!raw_market_ts || typeof raw_market_ts !== 'object') {
104
+ return {}
105
+ }
106
+ return {
107
+ ts_ms_venue_event: is_finite_number(raw_market_ts.ts_ms_venue_event) ? Number(raw_market_ts.ts_ms_venue_event) : null,
108
+ ts_ms_venue_send: is_finite_number(raw_market_ts.ts_ms_venue_send) ? Number(raw_market_ts.ts_ms_venue_send) : null,
109
+ ts_ms_local_received: is_finite_number(raw_market_ts.ts_ms_local_received) ? Number(raw_market_ts.ts_ms_local_received) : null,
110
+ }
111
+ }
112
+
113
+ const public_decision_context = (decision_context) => {
114
+ if (!decision_context || typeof decision_context !== 'object') {
115
+ return null
116
+ }
117
+ const public_context = _.cloneDeep(decision_context)
118
+ delete public_context.hr_rate_root
119
+ delete public_context.hr_decision
120
+ return public_context
121
+ }
122
+
92
123
  module.exports = class Orders {
93
124
  constructor(exchange_cur_quote_cur_dict, cd, settings) {
94
125
  let { exchange_cur_quote_cur, exchange_cur_quote_cur_rates_only, pair_map_rates_only } = exchange_cur_quote_cur_dict
@@ -272,60 +303,124 @@ module.exports = class Orders {
272
303
  ws_client.set_order_info_ref(this.order_info)
273
304
  }
274
305
  }
306
+ build_trace_entry(order, note) {
307
+ return {
308
+ ts: Date.now(),
309
+ status: order.status,
310
+ note,
311
+ f_amount: order.f_amount,
312
+ }
313
+ }
314
+ append_trace(order, note) {
315
+ if (!note || !order) return
316
+ if (!Array.isArray(order.trace)) {
317
+ order.trace = []
318
+ }
319
+ order.trace.push(this.build_trace_entry(order, note))
320
+ if (order.trace.length > MAX_ORDER_TRACE_ENTRIES) {
321
+ order.trace.splice(0, order.trace.length - MAX_ORDER_TRACE_ENTRIES)
322
+ }
323
+ }
324
+ move_order_info_key(exchange, pair, type, from_order_id, to_order_id) {
325
+ if (!from_order_id || !to_order_id || from_order_id === to_order_id) {
326
+ return
327
+ }
328
+ const from_bucket = this.order_info?.[exchange]?.[pair]?.[type]
329
+ if (!from_bucket || !from_bucket[from_order_id]) {
330
+ return
331
+ }
332
+ const order = from_bucket[from_order_id]
333
+ from_bucket[to_order_id] = order
334
+ delete from_bucket[from_order_id]
335
+ if (order.order_type) {
336
+ const by_type = this.order_info_by_type?.[order.order_type]?.[exchange]?.[pair]?.[type]
337
+ if (by_type) {
338
+ by_type[to_order_id] = order
339
+ delete by_type[from_order_id]
340
+ }
341
+ }
342
+ }
275
343
  init_account(balances, benchmarks) {
276
344
  this.account = new Account(balances, benchmarks)
277
345
  }
278
346
  update_non_initial_from_balances(balances) {
279
347
  this.account?.update_non_initial_from_balances(balances)
280
348
  }
281
- limit_trade(exchange, pair, method, price, amount, id = uuid(), order_type = '', limiter_on = true, api_key_index, order_options = {}) {
349
+ limit_trade(exchange, pair, method, price, amount, id = uuid(), order_type = '', limiter_on = true, api_key_index, order_options = {}, decision_context = null) {
282
350
  price = round_price(exchange, pair, price)
283
351
  amount = round_amount(exchange, pair, amount)
284
- // console.log('%s %s %s %s %s %s at %s', limiter_on, order_type, exchange, pair, method, amount, price)
285
- this.update_order_info(
286
- exchange,
287
- pair,
288
- method,
289
- id,
290
- {
291
- order_type,
292
- status: 'INIT',
293
- id,
294
- order_id: id,
295
- type: method,
296
- price,
297
- amount,
298
- f_amount: 0,
299
- f_percentage: 0,
300
- t_amount: amount,
301
- open_time: new Date(),
302
- close_time: null,
303
- },
304
- order_type + ' init',
305
- )
306
- let no_callback_timeout = setTimeout(() => {
307
- if (_.get(this.order_info, [exchange, pair, method, id, 'status']) === 'INIT') {
308
- console.log('no callback: %s %s %s %s %s', exchange, pair, method, id, order_type)
309
- this.update_order_info(
310
- exchange,
311
- pair,
312
- method,
313
- id,
314
- {
315
- status: 'N-OPENED-TIMEOUT',
316
- },
317
- order_type + ' n-opened-timeout-init',
318
- )
352
+ const internal_id = id
353
+ let no_callback_timeout = null
354
+ let init_emitted = false
355
+ const start_no_callback_timeout = () => {
356
+ if (no_callback_timeout) {
357
+ clearTimeout(no_callback_timeout)
358
+ }
359
+ no_callback_timeout = setTimeout(() => {
360
+ if (_.get(this.order_info, [exchange, pair, method, internal_id, 'status']) === 'INIT') {
361
+ console.log('no callback: %s %s %s %s %s', exchange, pair, method, internal_id, order_type)
362
+ this.update_order_info(
363
+ exchange,
364
+ pair,
365
+ method,
366
+ internal_id,
367
+ {
368
+ status: 'N-OPENED-TIMEOUT',
369
+ },
370
+ order_type + ' n-opened-timeout-init',
371
+ )
372
+ }
373
+ }, NO_CALLBACK_TIMEOUT_MS)
374
+ }
375
+ const emit_init = ({ t_order_sent } = {}) => {
376
+ if (init_emitted) {
377
+ return
319
378
  }
320
- }, NO_CALLBACK_TIMEOUT_MS)
379
+ init_emitted = true
380
+ const init_decision_context = public_decision_context(decision_context)
381
+ const du_ns_decision_to_send = hrtime_diff(decision_context?.hr_decision, t_order_sent)
382
+ const du_ns_rate_to_send = hrtime_diff(decision_context?.hr_rate_root, t_order_sent)
383
+ this.update_order_info(
384
+ exchange,
385
+ pair,
386
+ method,
387
+ internal_id,
388
+ {
389
+ order_type,
390
+ status: 'INIT',
391
+ id: internal_id,
392
+ internal_id,
393
+ order_id: null,
394
+ type: method,
395
+ price,
396
+ amount,
397
+ f_amount: 0,
398
+ f_percentage: 0,
399
+ t_amount: amount,
400
+ open_time: Date.now(),
401
+ close_time: null,
402
+ decision_context: init_decision_context,
403
+ du_ns_decision_to_send,
404
+ du_ns_rate_to_send,
405
+ event_hrtime: t_order_sent,
406
+ },
407
+ order_type + ' init',
408
+ )
409
+ start_no_callback_timeout()
410
+ }
321
411
  let limit_trade = (cb) => {
412
+ const order_options_with_meta = Object.assign({}, order_options, {
413
+ on_sent: ({ t_order_sent }) => {
414
+ emit_init({ t_order_sent })
415
+ },
416
+ })
322
417
  if (limiter_on) {
323
418
  if (this.Exchanges[exchange].private_limiter) {
324
419
  this.Exchanges[exchange].private_limiter.priority = getOrderPriority(order_type)
325
420
  }
326
- this.Exchanges[exchange].limit_trade(pair, method, price, amount, order_options, cb)
421
+ this.Exchanges[exchange].limit_trade(pair, method, price, amount, order_options_with_meta, cb)
327
422
  } else {
328
- this.Exchanges[exchange].limit_trade_base(pair, method, price, amount, order_options, cb)
423
+ this.Exchanges[exchange].limit_trade_base(pair, method, price, amount, order_options_with_meta, cb)
329
424
  }
330
425
  }
331
426
  if (api_key_index) {
@@ -334,7 +429,8 @@ module.exports = class Orders {
334
429
  limit_trade((res) => {
335
430
  if (res.success) {
336
431
  let order_id = res.body
337
- this.delete_order_info(exchange, pair, method, id, order_type + ' n-filled')
432
+ emit_init(res.meta || {})
433
+ this.move_order_info_key(exchange, pair, method, internal_id, order_id)
338
434
  this.update_order_info(
339
435
  exchange,
340
436
  pair,
@@ -343,7 +439,8 @@ module.exports = class Orders {
343
439
  {
344
440
  order_type,
345
441
  status: 'N-FILLED',
346
- id,
442
+ id: internal_id,
443
+ internal_id,
347
444
  order_id,
348
445
  type: method,
349
446
  price,
@@ -351,33 +448,38 @@ module.exports = class Orders {
351
448
  f_amount: 0,
352
449
  f_percentage: 0,
353
450
  t_amount: amount,
354
- open_time: new Date(),
355
451
  close_time: null,
452
+ du_ns_rate_to_ack: hrtime_diff(decision_context?.hr_rate_root, _.get(res, ['meta', 't_ack_recv'])),
453
+ du_ns_order_rtt: hrtime_diff(_.get(res, ['meta', 't_order_sent']), _.get(res, ['meta', 't_ack_recv'])),
454
+ event_hrtime: _.get(res, ['meta', 't_ack_recv']),
356
455
  },
357
456
  order_type + ' n-filled',
358
457
  )
359
458
  } else if (res.error === 'amount-too-small' || res.error === 'insufficient-funds') {
360
- this.delete_order_info(exchange, pair, method, id, order_type + ' limit-trade-' + res.error)
459
+ this.delete_order_info(exchange, pair, method, internal_id, order_type + ' limit-trade-' + res.error)
361
460
  } else if (res.error === 'connection-error') {
362
461
  setTimeout(() => {
363
- this.delete_order_info(exchange, pair, method, id, order_type + ' limit-trade-' + res.error)
462
+ this.delete_order_info(exchange, pair, method, internal_id, order_type + ' limit-trade-' + res.error)
364
463
  }, this.delete_connection_error_orders_timeout)
365
464
  } else {
366
465
  if (order_type.startsWith('self') || order_type.startsWith('maker_2')) {
367
- this.delete_order_info(exchange, pair, method, id, order_type + ' limit-trade-timeout')
466
+ this.delete_order_info(exchange, pair, method, internal_id, order_type + ' limit-trade-timeout')
368
467
  }
369
468
  // else if (_.get(this.order_info, [exchange, pair, method, id, 'status']) === 'INIT') { // make sure no_callback_timeout is NOT running zero
370
469
  // this.update_order_info(exchange, pair, method, id, {
371
470
  // status: 'N-OPENED-TIMEOUT'
372
471
  // }, order_type + ' n-opened-timeout-callback')
373
472
  // }
374
- console.error('limit trade error: %s %s %s %s %s %s %s %j', order_type, id, exchange, pair, method, price, amount, res)
473
+ console.error('limit trade error: %s %s %s %s %s %s %s %j', order_type, internal_id, exchange, pair, method, price, amount, res)
474
+ }
475
+ if (no_callback_timeout) {
476
+ clearTimeout(no_callback_timeout)
375
477
  }
376
- clearTimeout(no_callback_timeout)
377
478
  })
378
479
  }
379
- cancel_order(exchange, pair, order) {
480
+ cancel_order(exchange, pair, order, decision_context = null) {
380
481
  let original_status = order.status
482
+ const cancel_decision_context = public_decision_context(decision_context) || _.cloneDeep(order.decision_context) || null
381
483
  this.update_order_info(
382
484
  exchange,
383
485
  pair,
@@ -385,6 +487,7 @@ module.exports = class Orders {
385
487
  order.order_id,
386
488
  {
387
489
  status: 'CANCELING',
490
+ decision_context: cancel_decision_context,
388
491
  },
389
492
  order.order_type + ' canceling',
390
493
  )
@@ -401,7 +504,11 @@ module.exports = class Orders {
401
504
  order.order_id,
402
505
  {
403
506
  status: 'CANCELED',
404
- close_time: new Date(),
507
+ close_time: Date.now(),
508
+ decision_context: cancel_decision_context,
509
+ du_ns_cancel_latency: hrtime_diff(_.get(res, ['meta', 't_cancel_sent']), _.get(res, ['meta', 't_cancel_ack'])),
510
+ du_ns_rate_to_cancel_ack: hrtime_diff(decision_context?.hr_rate_root, _.get(res, ['meta', 't_cancel_ack'])),
511
+ event_hrtime: _.get(res, ['meta', 't_cancel_ack']),
405
512
  },
406
513
  order.order_type + ' canceled',
407
514
  )
@@ -420,7 +527,7 @@ module.exports = class Orders {
420
527
  amount: 0,
421
528
  f_amount: order.amount,
422
529
  f_percentage: 1,
423
- close_time: new Date(),
530
+ close_time: Date.now(),
424
531
  },
425
532
  order.order_type + ' cannot-be-canceled',
426
533
  )
@@ -552,8 +659,9 @@ module.exports = class Orders {
552
659
  amount: 0,
553
660
  f_amount: trade_amount,
554
661
  f_percentage: trade_amount / order_info.t_amount,
555
- close_time: trade.close_time || new Date(),
662
+ close_time: trade.close_time ? new Date(trade.close_time).getTime() : Date.now(),
556
663
  fees: trade.fees,
664
+ event_hrtime: process.hrtime(),
557
665
  },
558
666
  order_info.order_type + ' canceled-order-filled(-more)',
559
667
  )
@@ -592,8 +700,9 @@ module.exports = class Orders {
592
700
  amount: order_info.t_amount - trade_amount,
593
701
  f_amount: trade.amount,
594
702
  f_percentage: trade_amount / order_info.t_amount,
595
- close_time: trade.close_time || new Date(),
703
+ close_time: trade.close_time ? new Date(trade.close_time).getTime() : Date.now(),
596
704
  fees: trade.fees,
705
+ event_hrtime: process.hrtime(),
597
706
  },
598
707
  order_info.order_type + ' order-f-filled-1',
599
708
  )
@@ -678,7 +787,8 @@ module.exports = class Orders {
678
787
  amount: 0,
679
788
  f_amount: order_info_order.t_amount,
680
789
  f_percentage: 1,
681
- close_time: new Date(),
790
+ close_time: Date.now(),
791
+ event_hrtime: process.hrtime(),
682
792
  },
683
793
  order_info_order.order_type + ' order-f-filled-2',
684
794
  )
@@ -713,7 +823,8 @@ module.exports = class Orders {
713
823
  amount: order.amount,
714
824
  f_amount: order_f_amount,
715
825
  f_percentage: order_f_amount / order_info.t_amount,
716
- close_time: order.close_time || new Date(),
826
+ close_time: order.close_time ? new Date(order.close_time).getTime() : Date.now(),
827
+ event_hrtime: process.hrtime(),
717
828
  },
718
829
  order_info.order_type + ' order-f-filled-3',
719
830
  )
@@ -744,8 +855,8 @@ module.exports = class Orders {
744
855
  this.get_order_info_status_all(exchange, pair, 'both', 'INIT').map((init_order) => {
745
856
  let init_order_price = round_price(exchange, pair, init_order.price)
746
857
  let order_price = round_price(exchange, pair, order.price)
747
- let init_order_time = init_order.open_time.getTime()
748
- let order_time = order.open_time.getTime()
858
+ let init_order_time = typeof init_order.open_time === 'number' ? init_order.open_time : new Date(init_order.open_time).getTime()
859
+ let order_time = typeof order.open_time === 'number' ? order.open_time : new Date(order.open_time).getTime()
749
860
  if (
750
861
  init_order.type === order.type &&
751
862
  init_order_price === order_price &&
@@ -753,7 +864,7 @@ module.exports = class Orders {
753
864
  order_time - init_order_time < this.check_init_order_opened_timeout
754
865
  ) {
755
866
  // console.log('ORDERS: %s %s %s %s INIT ORDER OPENED', init_order.order_id, order.order_id, exchange, pair)
756
- this.delete_order_info(exchange, pair, init_order.type, init_order.order_id, init_order.order_type + ' init-order-opened')
867
+ this.move_order_info_key(exchange, pair, init_order.type, init_order.internal_id || init_order.id, order.order_id)
757
868
  this.update_order_info(
758
869
  exchange,
759
870
  pair,
@@ -767,8 +878,9 @@ module.exports = class Orders {
767
878
  amount: order.amount,
768
879
  f_amount: 0,
769
880
  t_amount: order.amount,
770
- open_time: order.open_time || new Date(),
881
+ open_time: order.open_time ? new Date(order.open_time).getTime() : Date.now(),
771
882
  close_time: null,
883
+ event_hrtime: process.hrtime(),
772
884
  },
773
885
  init_order.order_type + ' init-order-opened',
774
886
  )
@@ -788,7 +900,8 @@ module.exports = class Orders {
788
900
  order_info.order_id,
789
901
  {
790
902
  status: 'CANCELED',
791
- close_time: new Date(),
903
+ close_time: Date.now(),
904
+ event_hrtime: process.hrtime(),
792
905
  },
793
906
  (order_info.order_type || '') + ' canceling-canceled-from-open-orders',
794
907
  )
@@ -809,6 +922,8 @@ module.exports = class Orders {
809
922
  const existing = this.order_info[exchange]?.[pair]?.[type]?.[order_id]
810
923
  const old_status = existing && existing.status
811
924
  const old_order_type = existing && existing.order_type
925
+ const event_hrtime = dict.event_hrtime
926
+ delete dict.event_hrtime
812
927
 
813
928
  for (let key in dict) {
814
929
  if (key === 'f_percentage') {
@@ -822,7 +937,15 @@ module.exports = class Orders {
822
937
  }
823
938
 
824
939
  const order = this.order_info[exchange][pair][type][order_id]
940
+ if (order.internal_id == null && order.id != null) {
941
+ order.internal_id = order.id
942
+ }
943
+ if (order.trace == null) {
944
+ order.trace = []
945
+ }
946
+ order.lifecycle_seq = existing && Number.isFinite(existing.lifecycle_seq) ? existing.lifecycle_seq + 1 : dict.lifecycle_seq || 1
825
947
  order.last_update_ts = Date.now()
948
+ this.append_trace(order, note)
826
949
 
827
950
  const new_status = order.status
828
951
  if (old_status !== new_status) {
@@ -879,14 +1002,16 @@ module.exports = class Orders {
879
1002
 
880
1003
  // Send order update via WebSocket to dashboard backend
881
1004
  const order_data = _.get(this.order_info, [exchange, pair, type, order_id], {})
882
- // Only send N-FILLED and later statuses (skip INIT which has temporary order_id)
883
- if (dict.status !== 'INIT' && order_data.status !== 'INIT' && ws_client) {
1005
+ if (ws_client) {
884
1006
  ws_client.send_order({
885
1007
  action: 'update',
886
1008
  exchange,
887
1009
  pair,
888
1010
  side: type,
889
1011
  order_id,
1012
+ internal_id: order_data.internal_id,
1013
+ lifecycle_seq: order_data.lifecycle_seq,
1014
+ event_hrtime,
890
1015
  data: order_data,
891
1016
  note,
892
1017
  })
@@ -909,14 +1034,16 @@ module.exports = class Orders {
909
1034
  }
910
1035
  console.log('DELETE %s: %s %s %s %s', note, exchange, pair, type, order_id)
911
1036
 
912
- // Send delete event via WebSocket (only for non-INIT orders)
913
- if (order_data.status !== 'INIT' && ws_client) {
1037
+ if (ws_client && order_data.internal_id) {
914
1038
  ws_client.send_order({
915
1039
  action: 'delete',
916
1040
  exchange,
917
1041
  pair,
918
1042
  side: type,
919
1043
  order_id,
1044
+ internal_id: order_data.internal_id,
1045
+ lifecycle_seq: (order_data.lifecycle_seq || 0) + 1,
1046
+ event_hrtime: process.hrtime(),
920
1047
  data: order_data,
921
1048
  note,
922
1049
  })
@@ -1331,7 +1458,7 @@ module.exports = class Orders {
1331
1458
  for (let order_id in order_info[type]) {
1332
1459
  let { open_time } = order_info[type][order_id]
1333
1460
  // console.log(order_id, open_time, open_time < new Date() - 30 * 60 * 1000)
1334
- if (open_time < new Date() - QUERY_ORDER_TIME_IN_MS) {
1461
+ if (open_time < Date.now() - QUERY_ORDER_TIME_IN_MS) {
1335
1462
  // console.log('query_order_maker_1: %s %s %s', exchange, pair, order_id)
1336
1463
  Exchanges[exchange].query_order(pair, order_id, (res) => {
1337
1464
  count++
@@ -1406,10 +1533,29 @@ module.exports = class Orders {
1406
1533
  const throttled_process_rates = _.throttle(process_rates, PROCESS_RATES_THROTTLE_MS, { leading: true, trailing: true })
1407
1534
  let rates_handler = (exchange, pair, res) => {
1408
1535
  if (res.success) {
1409
- let { asks, bids } = res.body
1536
+ const hr_rate_root = process.hrtime()
1537
+ const ts_ms_local_rate_root = Date.now()
1538
+ let { asks, bids, market_ts } = res.body
1539
+ const raw_market_ts = market_ts && typeof market_ts === 'object' ? market_ts : null
1540
+ const normalized_market_ts = normalize_market_ts(raw_market_ts)
1541
+ const prev_local_received = _.get(this.rates, [exchange, pair, 'market_ts', 'ts_ms_local_received'])
1542
+ const du_ms_interarrival_local_received =
1543
+ is_finite_number(prev_local_received) && is_finite_number(normalized_market_ts.ts_ms_local_received)
1544
+ ? Number(normalized_market_ts.ts_ms_local_received) - Number(prev_local_received)
1545
+ : null
1410
1546
  asks = asks.slice(0, RATES_LENGTH_MAX)
1411
1547
  bids = bids.slice(0, RATES_LENGTH_MAX)
1412
- _.set(this.rates, [exchange, pair], { asks, bids, update_time: new Date() })
1548
+ _.set(this.rates, [exchange, pair], {
1549
+ asks,
1550
+ bids,
1551
+ market_ts: normalized_market_ts,
1552
+ hr_rate_root,
1553
+ hr_recv: Array.isArray(raw_market_ts?.hr_recv) ? raw_market_ts.hr_recv : null,
1554
+ hr_parsed: Array.isArray(raw_market_ts?.hr_parsed) ? raw_market_ts.hr_parsed : null,
1555
+ ts_ms_local_rate_root,
1556
+ du_ms_interarrival_local_received,
1557
+ update_time: new Date(),
1558
+ })
1413
1559
  /*
1414
1560
  * throttle to 20ms
1415
1561
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icgio/icg-exchanges-wrapper",
3
- "version": "1.17.21",
3
+ "version": "1.17.23",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "homepage": "https://github.com/icgio/icg-exchanges-wrapper#readme",
19
19
  "dependencies": {
20
- "@icgio/icg-exchanges": "^1.40.44",
20
+ "@icgio/icg-exchanges": "^1.40.46",
21
21
  "@icgio/icg-utils": "^1.9.61",
22
22
  "lodash": "^4.17.23",
23
23
  "pg": "^8.17.2"