dexbot 1.5.1 → 1.5.3

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 (94) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/analysis/ama_fitting/package.json +1 -1
  3. package/analysis/resolve_source.ts +3 -0
  4. package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +2455 -0
  5. package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +218 -0
  6. package/analysis/tradingview/README.md +5 -1
  7. package/analysis/tradingview/analyze_tradingview.ts +27 -1
  8. package/analysis/tradingview/h-bts_tradingview.html +1570 -0
  9. package/analysis/tradingview/t-bts_tradingview.html +1570 -0
  10. package/analysis/tradingview/tradingview_uplot_chart_generator.ts +351 -22
  11. package/analysis/trend_detection/package.json +1 -1
  12. package/claw/package.json +1 -1
  13. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  14. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  15. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  16. package/dist/analysis/resolve_source.d.ts.map +1 -1
  17. package/dist/analysis/resolve_source.js +3 -0
  18. package/dist/analysis/resolve_source.js.map +1 -1
  19. package/dist/analysis/tradingview/analyze_tradingview.d.ts +3 -0
  20. package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
  21. package/dist/analysis/tradingview/analyze_tradingview.js +27 -2
  22. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
  23. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
  24. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +348 -22
  25. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
  26. package/dist/dexbot.js +5 -5
  27. package/dist/dexbot.js.map +1 -1
  28. package/dist/market_adapter/core/asymmetric_bounds.d.ts +33 -1
  29. package/dist/market_adapter/core/asymmetric_bounds.d.ts.map +1 -1
  30. package/dist/market_adapter/core/asymmetric_bounds.js +38 -1
  31. package/dist/market_adapter/core/asymmetric_bounds.js.map +1 -1
  32. package/dist/modules/dexbot_class.d.ts +2 -18
  33. package/dist/modules/dexbot_class.d.ts.map +1 -1
  34. package/dist/modules/dexbot_cow_runtime.d.ts +38 -37
  35. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  36. package/dist/modules/dexbot_cow_runtime.js +722 -557
  37. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  38. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  39. package/dist/modules/dexbot_fill_runtime.js +7 -1
  40. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  41. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  42. package/dist/modules/dexbot_maintenance_runtime.js +31 -51
  43. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  44. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  45. package/dist/modules/dexbot_startup_runtime.js +42 -3
  46. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  47. package/dist/modules/order/accounting.d.ts.map +1 -1
  48. package/dist/modules/order/accounting.js +19 -2
  49. package/dist/modules/order/accounting.js.map +1 -1
  50. package/dist/modules/order/export.d.ts +122 -15
  51. package/dist/modules/order/export.d.ts.map +1 -1
  52. package/dist/modules/order/export.js +310 -46
  53. package/dist/modules/order/export.js.map +1 -1
  54. package/dist/modules/order/grid.d.ts.map +1 -1
  55. package/dist/modules/order/grid.js +29 -38
  56. package/dist/modules/order/grid.js.map +1 -1
  57. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  58. package/dist/modules/order/grid_reconcile.js +7 -3
  59. package/dist/modules/order/grid_reconcile.js.map +1 -1
  60. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  61. package/dist/modules/order/grid_reconcile_internal.js +31 -33
  62. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  63. package/dist/modules/order/manager.d.ts +13 -6
  64. package/dist/modules/order/manager.d.ts.map +1 -1
  65. package/dist/modules/order/manager.js +22 -7
  66. package/dist/modules/order/manager.js.map +1 -1
  67. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  68. package/dist/modules/order/sync_engine.js +231 -11
  69. package/dist/modules/order/sync_engine.js.map +1 -1
  70. package/dist/modules/order/utils/math.d.ts +38 -15
  71. package/dist/modules/order/utils/math.d.ts.map +1 -1
  72. package/dist/modules/order/utils/math.js +55 -16
  73. package/dist/modules/order/utils/math.js.map +1 -1
  74. package/dist/modules/order/utils/order.d.ts +116 -20
  75. package/dist/modules/order/utils/order.d.ts.map +1 -1
  76. package/dist/modules/order/utils/order.js +340 -35
  77. package/dist/modules/order/utils/order.js.map +1 -1
  78. package/dist/modules/order/utils/system.d.ts +1 -30
  79. package/dist/modules/order/utils/system.d.ts.map +1 -1
  80. package/dist/modules/order/utils/system.js +31 -98
  81. package/dist/modules/order/utils/system.js.map +1 -1
  82. package/dist/modules/order/utils/validate.d.ts +3 -2
  83. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  84. package/dist/modules/order/utils/validate.js +49 -28
  85. package/dist/modules/order/utils/validate.js.map +1 -1
  86. package/dist/scripts/analyze-credit.d.ts.map +1 -1
  87. package/dist/scripts/analyze-credit.js +53 -33
  88. package/dist/scripts/analyze-credit.js.map +1 -1
  89. package/docs/DEXBOT_COMPARISON.md +3 -3
  90. package/docs/EVOLUTION.md +7 -5
  91. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  92. package/docs/README.md +2 -2
  93. package/docs/WORKFLOW.md +1 -1
  94. package/package.json +1 -1
@@ -95,7 +95,7 @@
95
95
  */
96
96
  import { ORDER_TYPES, ORDER_STATES, TIMING, BTS_PRECISION } from '../constants.js';
97
97
  import * as Format from './format.js';
98
- import { lookupAsset } from './utils/system.js';
98
+ import { lookupAsset, sleep, resolveAccountRef } from './utils/system.js';
99
99
  import * as chainOrders from '../chain_orders.js';
100
100
  import * as client from '../bitshares_client.js';
101
101
  const { BitShares } = client;
@@ -106,6 +106,73 @@ import { parseChainOrder, findMatchingGridOrderByOpenOrder, applyChainSizeToGrid
106
106
  import { parseSlotIndex } from './utils/slot.js';
107
107
  import { resolveProcessedFillPersistenceMode } from './processed_fill_store.js';
108
108
  import { getErrorMessage } from '../utils/errors.js';
109
+ /**
110
+ * Confirming re-read for the suspect-empty-read guard: after
111
+ * SYNC_SUSPECT_EMPTY_READ_LIMIT consecutive 0-order snapshots with placed
112
+ * grid orders still present, one more read after
113
+ * SYNC_EMPTY_READ_CONFIRM_DELAY_MS decides whether the account is genuinely
114
+ * empty. A lagging node serves consecutive empties, so the count alone is
115
+ * not authoritative.
116
+ *
117
+ * Test seam: when manager._confirmEmptyReadFn is a function it is used
118
+ * instead of the chain read (must resolve to a raw order array, or null
119
+ * when the re-read itself is ambiguous). A throwing seam is 'ambiguous'.
120
+ *
121
+ * @param {Object} mgr - OrderManager instance (accountId, config, logger).
122
+ * @returns {Promise<string>} 'confirmed' (still empty — accept),
123
+ * 'contradicted' (non-empty — reset counter, refuse this round),
124
+ * 'ambiguous' (truncated/read-error — refuse, re-confirm next round), or
125
+ * 'unavailable' (no chain identity or dry-run — caller falls back to the
126
+ * legacy count-based acceptance, preserving unit-test semantics).
127
+ */
128
+ async function confirmSuspectEmptyRead(mgr) {
129
+ try {
130
+ if (mgr?.config?.dryRun)
131
+ return 'unavailable';
132
+ const seam = mgr?._confirmEmptyReadFn;
133
+ const delay = Math.max(0, Number(TIMING.SYNC_EMPTY_READ_CONFIRM_DELAY_MS) || 0);
134
+ if (typeof seam === 'function') {
135
+ if (delay > 0)
136
+ await sleep(delay);
137
+ let fresh = null;
138
+ try {
139
+ fresh = await seam();
140
+ }
141
+ catch {
142
+ return 'ambiguous';
143
+ }
144
+ if (fresh === null || fresh === undefined)
145
+ return 'ambiguous';
146
+ if (Array.isArray(fresh) && fresh.length > 0)
147
+ return 'contradicted';
148
+ return 'confirmed';
149
+ }
150
+ let accountRef = null;
151
+ try {
152
+ accountRef = resolveAccountRef(mgr, null);
153
+ }
154
+ catch {
155
+ return 'unavailable';
156
+ }
157
+ if (!accountRef)
158
+ return 'unavailable';
159
+ if (delay > 0)
160
+ await sleep(delay);
161
+ const fresh = await chainOrders.readOpenOrdersGuarded(chainOrders, accountRef, {
162
+ log: (message, level) => mgr?.logger?.log?.(message, level),
163
+ label: 'SYNC-CONFIRM',
164
+ detail: 'suspect-empty confirm re-read',
165
+ });
166
+ if (fresh === null)
167
+ return 'ambiguous';
168
+ if (Array.isArray(fresh) && fresh.length > 0)
169
+ return 'contradicted';
170
+ return 'confirmed';
171
+ }
172
+ catch {
173
+ return 'ambiguous';
174
+ }
175
+ }
109
176
  function describeNearestAdoptionCandidates(mgr, chainOrder, precision, calcTolerance, matchedGridOrderIds = null) {
110
177
  if (!mgr?.orders || !chainOrder || typeof precision !== 'number')
111
178
  return 'candidate diagnostics unavailable';
@@ -244,8 +311,15 @@ async function adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawC
244
311
  else {
245
312
  const spreadOrder = convertToSpreadPlaceholder(bestMatch);
246
313
  const applied = await mgr._applyOrderUpdate(spreadOrder, 'sync-pass2-filled', { skipAccounting: skipAccounting, fee: 0 });
247
- if (applied === false)
314
+ if (applied === false) {
315
+ // Rejected adoption must not poison the slot for the next
316
+ // chain order: the slot was marked matched above, so release
317
+ // it — B's adoption re-fails the same validation and gets
318
+ // the existing cancelOnly handling if the rejection was
319
+ // slot-specific.
320
+ matchedGridOrderIds.delete(bestMatch.id);
248
321
  return false;
322
+ }
249
323
  filledOrders.push({ ...bestMatch });
250
324
  updatedOrders.push(spreadOrder);
251
325
  chainOrderIdsOnGrid.add(chainOrderId);
@@ -256,8 +330,12 @@ async function adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawC
256
330
  bestMatch.state = ORDER_STATES.PARTIAL;
257
331
  }
258
332
  const applied = await mgr._applyOrderUpdate(bestMatch, 'sync-pass2-orphan', { skipAccounting: skipAccounting, fee: 0 });
259
- if (applied === false)
333
+ if (applied === false) {
334
+ // Same un-poisoning as the filled path above: the slot counts as
335
+ // matched only once the update lands.
336
+ matchedGridOrderIds.delete(bestMatch.id);
260
337
  return false;
338
+ }
261
339
  updatedOrders.push(bestMatch);
262
340
  chainOrderIdsOnGrid.add(chainOrderId);
263
341
  return true;
@@ -581,7 +659,8 @@ class SyncEngine {
581
659
  // ("phantom" resets). Refuse to reconcile on a
582
660
  // suspect empty; the guard is self-expiring: after
583
661
  // TIMING.SYNC_SUSPECT_EMPTY_READ_LIMIT consecutive empty reads the
584
- // account really is empty and the sync accepts it. Any non-empty read
662
+ // account really is empty and the sync accepts it — but only after one
663
+ // confirming re-read (see below). Any non-empty read
585
664
  // resets the counter.
586
665
  if (parsedChainOrders.size === 0) {
587
666
  const gridOrderIds = Array.from(mgr.orders.values()).filter((o) => o?.orderId).length;
@@ -597,7 +676,33 @@ class SyncEngine {
597
676
  `refusing reconciliation (phantom protection); accepting after ${limit} consecutive empties or next non-empty read`, 'warn');
598
677
  return { filledOrders: [], updatedOrders: [], ordersNeedingCorrection: [], unmatchedChainOrders: [] };
599
678
  }
600
- mgr.logger?.log?.(`[SYNC] Empty read confirmed after ${suspect.count} consecutive attempts — reconciling to empty account`, 'warn');
679
+ // Limit reached: a lagging node serves CONSECUTIVE 0-order
680
+ // snapshots, so the count alone is not authoritative. Require
681
+ // one confirming re-read after SYNC_EMPTY_READ_CONFIRM_DELAY_MS
682
+ // before reconciling to empty; a contradicted re-read
683
+ // (non-empty) resets the counter and refuses this round (the
684
+ // next sync reconciles against the fresh snapshot), while an
685
+ // ambiguous re-read (truncated/read-error) refuses and retries
686
+ // the confirm on the next empty round.
687
+ const confirm = await confirmSuspectEmptyRead(mgr);
688
+ if (confirm === 'contradicted') {
689
+ mgr.logger?.log?.(`[SYNC] Suspect empty read contradicted by confirm re-read with ${gridOrderIds} grid orderIds — ` +
690
+ `resetting empty-read counter; reconciling on the next non-empty sync`, 'warn');
691
+ mgr._suspectEmptyReads = { count: 0, firstAt: 0 };
692
+ return { filledOrders: [], updatedOrders: [], ordersNeedingCorrection: [], unmatchedChainOrders: [] };
693
+ }
694
+ if (confirm === 'ambiguous') {
695
+ mgr.logger?.log?.(`[SYNC] Suspect empty read confirm re-read ambiguous with ${gridOrderIds} grid orderIds — ` +
696
+ `refusing reconciliation; re-confirming on the next empty read`, 'warn');
697
+ mgr._suspectEmptyReads = { count: limit, firstAt: suspect.firstAt || Date.now() };
698
+ return { filledOrders: [], updatedOrders: [], ordersNeedingCorrection: [], unmatchedChainOrders: [] };
699
+ }
700
+ // 'confirmed' (still empty on re-read) or 'unavailable' (no
701
+ // chain identity / dry-run — legacy count-based acceptance,
702
+ // preserving unit-test semantics): reconcile to empty.
703
+ mgr.logger?.log?.(`[SYNC] Empty read confirmed after ${suspect.count} consecutive attempts` +
704
+ (confirm === 'unavailable' ? ' (no chain identity for confirm re-read)' : ' (+ confirming re-read)') +
705
+ ` — reconciling to empty account`, 'warn');
601
706
  mgr._suspectEmptyReads = { count: 0, firstAt: 0 };
602
707
  }
603
708
  else {
@@ -843,9 +948,17 @@ class SyncEngine {
843
948
  && toFiniteNumber(reboundOrder.rawOnChain?.for_sale, 0) > 0) {
844
949
  reboundOrder.state = ORDER_STATES.PARTIAL;
845
950
  }
951
+ const applied = await mgr._applyOrderUpdate(reboundOrder, 'sync-pass1-duplicate-swap', { skipAccounting: skipAccounting, fee: 0 });
952
+ if (applied === false) {
953
+ // Rejected rebind: the slot keeps its old binding
954
+ // (set ops must run only after the apply lands), so
955
+ // the old orderId stays matched on-grid and the swap
956
+ // candidate stays unmatched → pass 2 cancels it.
957
+ mgr.logger?.log?.(`[SYNC] Size tiebreak for ${gridOrder.id}: rebind to ${swapMatch.id} rejected — slot keeps ${gridOrder.orderId}; duplicate falls to pass 2`, 'warn');
958
+ continue;
959
+ }
846
960
  chainOrderIdsOnGrid.delete(gridOrder.orderId);
847
961
  chainOrderIdsOnGrid.add(swapMatch.id);
848
- await mgr._applyOrderUpdate(reboundOrder, 'sync-pass1-duplicate-swap', { skipAccounting: skipAccounting, fee: 0 });
849
962
  updatedOrders.push(reboundOrder);
850
963
  mgr.logger?.log?.(`[SYNC] Size tiebreak for ${gridOrder.id}: tracked order ${gridOrder.orderId} ` +
851
964
  `(chain size ${chainOrder.size}) disagrees with booked size ${gridOrder.size}, but ` +
@@ -869,11 +982,23 @@ class SyncEngine {
869
982
  updatedOrder.state = (chainSizeInt < currentSizeInt)
870
983
  ? ORDER_STATES.PARTIAL
871
984
  : gridOrder.state;
872
- await mgr._applyOrderUpdate(updatedOrder, 'sync-pass1-partial', { skipAccounting: skipAccounting, fee: 0 });
985
+ const partialApplied = await mgr._applyOrderUpdate(updatedOrder, 'sync-pass1-partial', { skipAccounting: skipAccounting, fee: 0 });
986
+ if (partialApplied === false) {
987
+ // Rejected resize: the slot keeps its booked size;
988
+ // divergence re-detects on the next sync.
989
+ mgr.logger?.log?.(`[SYNC] Partial-size update for ${gridOrder.id} rejected — slot keeps booked size ${gridOrder.size}`, 'warn');
990
+ }
873
991
  }
874
992
  else {
875
993
  const spreadOrder = convertToSpreadPlaceholder(gridOrder);
876
- await mgr._applyOrderUpdate(spreadOrder, 'sync-pass1-filled', { skipAccounting: skipAccounting, fee: 0 });
994
+ const filledApplied = await mgr._applyOrderUpdate(spreadOrder, 'sync-pass1-filled', { skipAccounting: skipAccounting, fee: 0 });
995
+ if (filledApplied === false) {
996
+ // Rejected virtualization must not book a fill:
997
+ // neither the filled order nor the placeholder
998
+ // update leaves this path.
999
+ mgr.logger?.log?.(`[SYNC] Filled virtualization for ${gridOrder.id} rejected — fill NOT booked`, 'error');
1000
+ continue;
1001
+ }
877
1002
  // Push the filled order with its REAL side (chain order
878
1003
  // type), not the SPREAD placeholder: downstream fill
879
1004
  // processing (deriveTargetBoundary) derives boundary
@@ -927,7 +1052,14 @@ class SyncEngine {
927
1052
  const currentGridOrder = mgr.orders.get(gridOrder.id) || gridOrder;
928
1053
  const hadOrderId = Boolean(currentGridOrder?.orderId);
929
1054
  const spreadOrder = convertToSpreadPlaceholder(currentGridOrder);
930
- await mgr._applyOrderUpdate(spreadOrder, 'sync-cleanup-phantom', { skipAccounting: skipAccounting, fee: 0 });
1055
+ const phantomApplied = await mgr._applyOrderUpdate(spreadOrder, 'sync-cleanup-phantom', { skipAccounting: skipAccounting, fee: 0 });
1056
+ if (phantomApplied === false) {
1057
+ // Rejected virtualization books no phantom fill: the slot
1058
+ // keeps its order and the disappearance re-detects next
1059
+ // sync.
1060
+ mgr.logger?.log?.(`[SYNC] Phantom cleanup for ${gridOrder.id} rejected — fill NOT booked`, 'warn');
1061
+ continue;
1062
+ }
931
1063
  // Only genuine disappearances (had orderId) count as fills.
932
1064
  if (hadOrderId) {
933
1065
  // A SPREAD slot can carry an on-chain order (spread-correction
@@ -975,6 +1107,19 @@ class SyncEngine {
975
1107
  const slotId = `slot-${idx}`;
976
1108
  const gapSlots = genesis.gapSlots ?? mgr._gapSlots ?? 0;
977
1109
  const boundaryIdx = mgr.boundaryIdx;
1110
+ // Pre-boundary sync: gap geometry is unknown, so adoption is
1111
+ // deferred entirely — touch nothing (no adopt, no cancelOnly).
1112
+ // The orphan stays visible to the crossing guards and the
1113
+ // validate orphan layer via _lastUnmatchedChainOrders and is
1114
+ // re-evaluated once the boundary commits. Accepted cost: a
1115
+ // legitimate in-rail orphan waits one sync cycle
1116
+ // post-boundary-commit before adoption. Strictly better than
1117
+ // adopting a gap stray into the wrong slot.
1118
+ if (boundaryIdx == null || !Number.isFinite(Number(boundaryIdx))) {
1119
+ unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'boundary-unknown-deferred', candidateSlotId: slotId });
1120
+ mgr.logger?.log?.(`[SYNC] Orphaned chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}, size=${chainOrder.size}) — NOT adopted: boundary unknown, deferred until boundary commits (nearest slot ${slotId})`, 'warn');
1121
+ continue;
1122
+ }
978
1123
  // Duplicate-price guard becomes slotId equality: if placed order already occupies this slot
979
1124
  const occupying = mgr.orders.get(slotId);
980
1125
  if (occupying && isOrderPlaced(occupying) && occupying.type === chainOrder.type) {
@@ -984,8 +1129,9 @@ class SyncEngine {
984
1129
  queueCorrection({ gridOrder: occupying, chainOrderId, expectedPrice: chainOrder.price, size: chainOrder.size, type: chainOrder.type, isSurplus: true, cancelOnly: true });
985
1130
  continue;
986
1131
  }
987
- // Gap exclusion: nearest slot in SPREAD gap → no adopt. When boundaryIdx == null (pre-boundary sync) the gap is unknown so we allow adoption — the next reconcile will relocate gap stray per §10 table's cancelOnly intent (documented hole).
988
- if (boundaryIdx != null && Number.isFinite(Number(boundaryIdx))) {
1132
+ // Gap exclusion: nearest slot in SPREAD gap → no adopt (boundary
1133
+ // is known here — the pre-boundary case continued above).
1134
+ {
989
1135
  const inRail = isSlotInRail(boundaryIdx, gapSlots, chainOrder.type, { id: slotId });
990
1136
  if (!inRail) {
991
1137
  unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'no-available-nearest-slot', candidateSlotId: slotId });
@@ -1799,6 +1945,11 @@ class SyncEngine {
1799
1945
  switch (source) {
1800
1946
  case 'createOrder': {
1801
1947
  const { gridOrderId, chainOrderId, isPartialPlacement, expectedType, fee } = chainData;
1948
+ // Optional placement descriptor for unknown-id recovery. Callers
1949
+ // pass the placed slot's price/size/type so a grid reset racing
1950
+ // the broadcast can't strand the live chain order untracked.
1951
+ // Callers that omit it take the no-descriptor error path.
1952
+ const placedDescriptor = chainData.order ?? null;
1802
1953
  const runCreate = async () => {
1803
1954
  // Lock order to prevent concurrent modifications during state transition
1804
1955
  mgr.lockOrders([gridOrderId]);
@@ -1861,6 +2012,68 @@ class SyncEngine {
1861
2012
  fee: actualFee
1862
2013
  });
1863
2014
  }
2015
+ else {
2016
+ // Unknown grid id: the broadcast landed but master no
2017
+ // longer holds the slot (grid reset/regen raced the
2018
+ // CREATE, or a first placement for an id never
2019
+ // projected via projectTargetToWorkingGrid). The old
2020
+ // code dropped the linkage silently, so the next
2021
+ // cycle re-placed the same level (duplicate CREATEs,
2022
+ // funds locked xN). Materialize-or-error: rebind when
2023
+ // the chain id is already tracked, materialize the
2024
+ // slot when the caller supplied its descriptor, and
2025
+ // log an error otherwise so the order never vanishes
2026
+ // quietly again.
2027
+ const rebound = Array.from(mgr.orders.values()).find((o) => o.orderId === chainOrderId);
2028
+ if (rebound) {
2029
+ mgr.logger?.log?.(`[SYNC] createOrder for unknown grid order ${gridOrderId}: chain order ${chainOrderId} already tracked on ${rebound.id} — linkage up to date, skipping`, 'warn');
2030
+ }
2031
+ else {
2032
+ const rawType = placedDescriptor?.type ?? expectedType;
2033
+ let materializeType = (rawType === ORDER_TYPES.BUY || rawType === ORDER_TYPES.SELL)
2034
+ ? rawType
2035
+ : null;
2036
+ const descriptorPrice = toFiniteNumber(placedDescriptor?.price, NaN);
2037
+ if (!materializeType && Number.isFinite(descriptorPrice)) {
2038
+ materializeType = resolveSpreadOrderSide(descriptorPrice, mgr.config.startPrice);
2039
+ }
2040
+ const descriptorSize = toFiniteNumber(placedDescriptor?.size, NaN);
2041
+ const hasDescriptor = !!materializeType
2042
+ && Number.isFinite(descriptorPrice) && descriptorPrice > 0
2043
+ && Number.isFinite(descriptorSize) && descriptorSize > 0;
2044
+ if (!hasDescriptor) {
2045
+ mgr.logger?.log?.(`[SYNC] createOrder linkage LOST: grid order ${gridOrderId} not in master — chain order ${chainOrderId} placed but untracked (no placement descriptor; next readOpenOrders sync must adopt it as an orphan before the slot re-places)`, 'error');
2046
+ }
2047
+ else {
2048
+ // Minimal slot shape: the raw chain object
2049
+ // isn't available here, so rawOnChain and
2050
+ // slot-scheme metadata are absent. The next
2051
+ // readOpenOrders pass repopulates rawOnChain
2052
+ // from the live order (slot-scheme ids only).
2053
+ const materializedOrder = {
2054
+ id: gridOrderId,
2055
+ type: materializeType,
2056
+ price: descriptorPrice,
2057
+ size: descriptorSize,
2058
+ state: isPartialPlacement ? ORDER_STATES.PARTIAL : ORDER_STATES.ACTIVE,
2059
+ orderId: chainOrderId,
2060
+ };
2061
+ if (chainData.deferredFee !== undefined && chainData.deferredFee !== null) {
2062
+ materializedOrder.btsFeeState = { deferredFee: Math.max(0, chainData.deferredFee) };
2063
+ }
2064
+ const materialized = await mgr._applyOrderUpdate(materializedOrder, 'createOrder-unknown-id-adopt', {
2065
+ skipAccounting: chainData.skipAccounting || false,
2066
+ fee: fee,
2067
+ });
2068
+ if (materialized === false) {
2069
+ mgr.logger?.log?.(`[SYNC] createOrder linkage FAILED: grid order ${gridOrderId} not in master and materializing chain order ${chainOrderId} (${materializeType} @${descriptorPrice} x${descriptorSize}) was rejected — chain order left untracked`, 'error');
2070
+ }
2071
+ else {
2072
+ mgr.logger?.log?.(`[SYNC] createOrder for unknown grid order ${gridOrderId}: materialized ${materializeType} @${descriptorPrice} x${descriptorSize} -> ${chainOrderId} (master lost the slot mid-broadcast)`, 'warn');
2073
+ }
2074
+ }
2075
+ }
2076
+ }
1864
2077
  }
1865
2078
  finally {
1866
2079
  mgr.unlockOrders([gridOrderId]);
@@ -1912,6 +2125,13 @@ class SyncEngine {
1912
2125
  fee: btsFeeData?.cancelFee || 0
1913
2126
  });
1914
2127
  }
2128
+ else {
2129
+ // Slot vanished or was rebound between lookup and
2130
+ // lock — never skip quietly: the next
2131
+ // readOpenOrders sync reconciles the slot, but the
2132
+ // race must be visible when it happens.
2133
+ mgr.logger?.log?.(`[SYNC] cancelOrder for ${orderId}: slot ${gridOrder.id} changed underneath (missing or rebound to ${currentGridOrder?.orderId ?? 'none'}) — linkage skipped, next sync reconciles`, 'warn');
2134
+ }
1915
2135
  }
1916
2136
  finally {
1917
2137
  mgr.unlockOrders(orderIds);