dexbot 1.4.24 → 1.5.0

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 (256) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +7 -2
  3. package/analysis/README.md +61 -5
  4. package/analysis/ama_fitting/package.json +1 -1
  5. package/analysis/analyze_derivatives.ts +1 -1
  6. package/analysis/bot_key_utils.ts +272 -3
  7. package/analysis/bot_usage/discover_bot_accounts.ts +64 -9
  8. package/analysis/bot_usage/kibana_bot_queries.ts +40 -1
  9. package/analysis/chart_utils.ts +6 -3
  10. package/analysis/grid_correction_check.ts +932 -0
  11. package/analysis/resolve_bot_accounts.ts +199 -0
  12. package/analysis/trade_profitability.ts +36 -8
  13. package/analysis/tradingview/README.md +25 -0
  14. package/analysis/tradingview/analyze_tradingview.ts +2 -2
  15. package/analysis/trend_detection/package.json +1 -1
  16. package/claw/modules/claw_launcher.ts +5 -4
  17. package/claw/modules/credit_runtime_adapter.ts +2 -1
  18. package/claw/modules/dexbot_profiles.ts +4 -3
  19. package/claw/package.json +1 -1
  20. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  21. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  22. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  23. package/dist/analysis/analyze_derivatives.js +1 -1
  24. package/dist/analysis/bot_key_utils.d.ts +28 -1
  25. package/dist/analysis/bot_key_utils.d.ts.map +1 -1
  26. package/dist/analysis/bot_key_utils.js +385 -3
  27. package/dist/analysis/bot_key_utils.js.map +1 -1
  28. package/dist/analysis/bot_usage/discover_bot_accounts.js +60 -10
  29. package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
  30. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +45 -1
  31. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
  32. package/dist/analysis/bot_usage/kibana_bot_queries.js +39 -1
  33. package/dist/analysis/bot_usage/kibana_bot_queries.js.map +1 -1
  34. package/dist/analysis/chart_utils.d.ts +2 -1
  35. package/dist/analysis/chart_utils.d.ts.map +1 -1
  36. package/dist/analysis/chart_utils.js +4 -1
  37. package/dist/analysis/chart_utils.js.map +1 -1
  38. package/dist/analysis/grid_correction_check.d.ts +90 -0
  39. package/dist/analysis/grid_correction_check.d.ts.map +1 -0
  40. package/dist/analysis/grid_correction_check.js +901 -0
  41. package/dist/analysis/grid_correction_check.js.map +1 -0
  42. package/dist/analysis/resolve_bot_accounts.d.ts +3 -0
  43. package/dist/analysis/resolve_bot_accounts.d.ts.map +1 -0
  44. package/dist/analysis/resolve_bot_accounts.js +216 -0
  45. package/dist/analysis/resolve_bot_accounts.js.map +1 -0
  46. package/dist/analysis/trade_profitability.d.ts.map +1 -1
  47. package/dist/analysis/trade_profitability.js +40 -8
  48. package/dist/analysis/trade_profitability.js.map +1 -1
  49. package/dist/analysis/tradingview/analyze_tradingview.js +2 -2
  50. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
  51. package/dist/bot.d.ts.map +1 -1
  52. package/dist/bot.js +2 -1
  53. package/dist/bot.js.map +1 -1
  54. package/dist/dexbot.d.ts.map +1 -1
  55. package/dist/dexbot.js +134 -22
  56. package/dist/dexbot.js.map +1 -1
  57. package/dist/market_adapter/ama_signal_runner.js +2 -2
  58. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +2 -1
  59. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js.map +1 -1
  60. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +4 -1
  61. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  62. package/dist/market_adapter/inputs/fetch_lp_data.js +3 -2
  63. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  64. package/dist/modules/account_bots.d.ts +42 -1
  65. package/dist/modules/account_bots.d.ts.map +1 -1
  66. package/dist/modules/account_bots.js +221 -22
  67. package/dist/modules/account_bots.js.map +1 -1
  68. package/dist/modules/account_orders.d.ts +7 -10
  69. package/dist/modules/account_orders.d.ts.map +1 -1
  70. package/dist/modules/account_orders.js +26 -2
  71. package/dist/modules/account_orders.js.map +1 -1
  72. package/dist/modules/bitshares_client.d.ts +4 -1
  73. package/dist/modules/bitshares_client.d.ts.map +1 -1
  74. package/dist/modules/bitshares_client.js +12 -4
  75. package/dist/modules/bitshares_client.js.map +1 -1
  76. package/dist/modules/bot_settings.d.ts.map +1 -1
  77. package/dist/modules/bot_settings.js +2 -1
  78. package/dist/modules/bot_settings.js.map +1 -1
  79. package/dist/modules/chain_orders.d.ts.map +1 -1
  80. package/dist/modules/chain_orders.js +21 -2
  81. package/dist/modules/chain_orders.js.map +1 -1
  82. package/dist/modules/cli_colors.d.ts +39 -0
  83. package/dist/modules/cli_colors.d.ts.map +1 -0
  84. package/dist/modules/cli_colors.js +42 -0
  85. package/dist/modules/cli_colors.js.map +1 -0
  86. package/dist/modules/config.d.ts.map +1 -1
  87. package/dist/modules/config.js +27 -0
  88. package/dist/modules/config.js.map +1 -1
  89. package/dist/modules/constants.d.ts +14 -1
  90. package/dist/modules/constants.d.ts.map +1 -1
  91. package/dist/modules/constants.js +66 -11
  92. package/dist/modules/constants.js.map +1 -1
  93. package/dist/modules/credit_pricing.d.ts +61 -0
  94. package/dist/modules/credit_pricing.d.ts.map +1 -0
  95. package/dist/modules/credit_pricing.js +237 -0
  96. package/dist/modules/credit_pricing.js.map +1 -0
  97. package/dist/modules/credit_runtime.d.ts +1 -0
  98. package/dist/modules/credit_runtime.d.ts.map +1 -1
  99. package/dist/modules/credit_runtime.js +49 -107
  100. package/dist/modules/credit_runtime.js.map +1 -1
  101. package/dist/modules/dexbot_class.d.ts +25 -0
  102. package/dist/modules/dexbot_class.d.ts.map +1 -1
  103. package/dist/modules/dexbot_class.js +82 -12
  104. package/dist/modules/dexbot_class.js.map +1 -1
  105. package/dist/modules/dexbot_cow_runtime.d.ts +39 -17
  106. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  107. package/dist/modules/dexbot_cow_runtime.js +875 -134
  108. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  109. package/dist/modules/dexbot_fill_runtime.d.ts +11 -1
  110. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  111. package/dist/modules/dexbot_fill_runtime.js +108 -1
  112. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  113. package/dist/modules/dexbot_maintenance_runtime.d.ts +29 -14
  114. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  115. package/dist/modules/dexbot_maintenance_runtime.js +279 -63
  116. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  117. package/dist/modules/dexbot_startup_runtime.d.ts +1 -0
  118. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  119. package/dist/modules/dexbot_startup_runtime.js +58 -7
  120. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  121. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  122. package/dist/modules/dexbot_state_recovery.js +55 -3
  123. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  124. package/dist/modules/fund_registry.d.ts +1 -1
  125. package/dist/modules/fund_registry.js +1 -1
  126. package/dist/modules/graceful_shutdown.d.ts +12 -1
  127. package/dist/modules/graceful_shutdown.d.ts.map +1 -1
  128. package/dist/modules/graceful_shutdown.js +39 -1
  129. package/dist/modules/graceful_shutdown.js.map +1 -1
  130. package/dist/modules/key_store.d.ts.map +1 -1
  131. package/dist/modules/key_store.js +38 -1
  132. package/dist/modules/key_store.js.map +1 -1
  133. package/dist/modules/launcher/adapter_requirement.d.ts +38 -0
  134. package/dist/modules/launcher/adapter_requirement.d.ts.map +1 -0
  135. package/dist/modules/launcher/adapter_requirement.js +141 -0
  136. package/dist/modules/launcher/adapter_requirement.js.map +1 -0
  137. package/dist/modules/launcher/bot_supervisor.d.ts +2 -2
  138. package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
  139. package/dist/modules/launcher/bot_supervisor.js +32 -3
  140. package/dist/modules/launcher/bot_supervisor.js.map +1 -1
  141. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  142. package/dist/modules/launcher/launch_modes.js +5 -4
  143. package/dist/modules/launcher/launch_modes.js.map +1 -1
  144. package/dist/modules/launcher/monolithic_runtime.d.ts +1 -1
  145. package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
  146. package/dist/modules/launcher/monolithic_runtime.js +12 -8
  147. package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
  148. package/dist/modules/launcher/status_reporting.d.ts +6 -6
  149. package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
  150. package/dist/modules/launcher/status_reporting.js +9 -6
  151. package/dist/modules/launcher/status_reporting.js.map +1 -1
  152. package/dist/modules/order/accounting.d.ts.map +1 -1
  153. package/dist/modules/order/accounting.js +20 -3
  154. package/dist/modules/order/accounting.js.map +1 -1
  155. package/dist/modules/order/export.d.ts +1 -1
  156. package/dist/modules/order/export.js +1 -1
  157. package/dist/modules/order/grid.d.ts +1 -1
  158. package/dist/modules/order/grid.d.ts.map +1 -1
  159. package/dist/modules/order/grid.js +335 -43
  160. package/dist/modules/order/grid.js.map +1 -1
  161. package/dist/modules/order/grid_reconcile.d.ts +4 -2
  162. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  163. package/dist/modules/order/grid_reconcile.js +242 -222
  164. package/dist/modules/order/grid_reconcile.js.map +1 -1
  165. package/dist/modules/order/grid_reconcile_internal.d.ts +11 -1
  166. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  167. package/dist/modules/order/grid_reconcile_internal.js +132 -6
  168. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  169. package/dist/modules/order/logger.d.ts +23 -1
  170. package/dist/modules/order/logger.d.ts.map +1 -1
  171. package/dist/modules/order/logger.js +60 -17
  172. package/dist/modules/order/logger.js.map +1 -1
  173. package/dist/modules/order/manager.d.ts +68 -14
  174. package/dist/modules/order/manager.d.ts.map +1 -1
  175. package/dist/modules/order/manager.js +281 -52
  176. package/dist/modules/order/manager.js.map +1 -1
  177. package/dist/modules/order/strategy.d.ts.map +1 -1
  178. package/dist/modules/order/strategy.js +25 -8
  179. package/dist/modules/order/strategy.js.map +1 -1
  180. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  181. package/dist/modules/order/sync_engine.js +262 -167
  182. package/dist/modules/order/sync_engine.js.map +1 -1
  183. package/dist/modules/order/utils/math.d.ts +47 -5
  184. package/dist/modules/order/utils/math.d.ts.map +1 -1
  185. package/dist/modules/order/utils/math.js +175 -11
  186. package/dist/modules/order/utils/math.js.map +1 -1
  187. package/dist/modules/order/utils/order.d.ts +16 -15
  188. package/dist/modules/order/utils/order.d.ts.map +1 -1
  189. package/dist/modules/order/utils/order.js +199 -32
  190. package/dist/modules/order/utils/order.js.map +1 -1
  191. package/dist/modules/order/utils/slot.d.ts +8 -0
  192. package/dist/modules/order/utils/slot.d.ts.map +1 -0
  193. package/dist/modules/order/utils/slot.js +16 -0
  194. package/dist/modules/order/utils/slot.js.map +1 -0
  195. package/dist/modules/order/utils/system.d.ts +52 -4
  196. package/dist/modules/order/utils/system.d.ts.map +1 -1
  197. package/dist/modules/order/utils/system.js +109 -16
  198. package/dist/modules/order/utils/system.js.map +1 -1
  199. package/dist/modules/order/utils/validate.d.ts +6 -9
  200. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  201. package/dist/modules/order/utils/validate.js +107 -85
  202. package/dist/modules/order/utils/validate.js.map +1 -1
  203. package/dist/modules/order/working_grid.d.ts +5 -0
  204. package/dist/modules/order/working_grid.d.ts.map +1 -1
  205. package/dist/modules/order/working_grid.js +21 -0
  206. package/dist/modules/order/working_grid.js.map +1 -1
  207. package/dist/modules/paths.d.ts +4 -2
  208. package/dist/modules/paths.d.ts.map +1 -1
  209. package/dist/modules/paths.js +79 -24
  210. package/dist/modules/paths.js.map +1 -1
  211. package/dist/modules/utils/chain_logs.d.ts +11 -0
  212. package/dist/modules/utils/chain_logs.d.ts.map +1 -0
  213. package/dist/modules/utils/chain_logs.js +23 -0
  214. package/dist/modules/utils/chain_logs.js.map +1 -0
  215. package/dist/modules/utils/sanitize_key.d.ts +7 -0
  216. package/dist/modules/utils/sanitize_key.d.ts.map +1 -1
  217. package/dist/modules/utils/sanitize_key.js +15 -0
  218. package/dist/modules/utils/sanitize_key.js.map +1 -1
  219. package/dist/pm2.d.ts +11 -1
  220. package/dist/pm2.d.ts.map +1 -1
  221. package/dist/pm2.js +66 -15
  222. package/dist/pm2.js.map +1 -1
  223. package/dist/scripts/analyze-credit.d.ts +4 -0
  224. package/dist/scripts/analyze-credit.d.ts.map +1 -0
  225. package/dist/scripts/analyze-credit.js +760 -0
  226. package/dist/scripts/analyze-credit.js.map +1 -0
  227. package/dist/scripts/analyze-orders.d.ts +1 -10
  228. package/dist/scripts/analyze-orders.d.ts.map +1 -1
  229. package/dist/scripts/analyze-orders.js +34 -36
  230. package/dist/scripts/analyze-orders.js.map +1 -1
  231. package/dist/scripts/generate_market_adapter_whitelist.d.ts +2 -5
  232. package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +1 -1
  233. package/dist/scripts/generate_market_adapter_whitelist.js +83 -1
  234. package/dist/scripts/generate_market_adapter_whitelist.js.map +1 -1
  235. package/dist/scripts/test-credit-renewal.d.ts.map +1 -1
  236. package/dist/scripts/test-credit-renewal.js +37 -5
  237. package/dist/scripts/test-credit-renewal.js.map +1 -1
  238. package/dist/scripts/tv.d.ts +11 -0
  239. package/dist/scripts/tv.d.ts.map +1 -0
  240. package/dist/scripts/tv.js +357 -0
  241. package/dist/scripts/tv.js.map +1 -0
  242. package/dist/scripts/update.js +77 -10
  243. package/dist/scripts/update.js.map +1 -1
  244. package/dist/unlock.d.ts.map +1 -1
  245. package/dist/unlock.js +20 -8
  246. package/dist/unlock.js.map +1 -1
  247. package/docs/DEXBOT_COMPARISON.md +11 -11
  248. package/docs/EVOLUTION.md +10 -8
  249. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  250. package/docs/GRID_RECALCULATION.md +6 -0
  251. package/docs/MPA_CREDIT_USAGE.md +3 -1
  252. package/docs/README.md +2 -2
  253. package/docs/WORKFLOW.md +3 -1
  254. package/market_adapter/README.md +10 -0
  255. package/package.json +3 -1
  256. package/scripts/README.md +16 -0
@@ -101,7 +101,7 @@ import * as client from '../bitshares_client.js';
101
101
  const { BitShares } = client;
102
102
  import { NATIVE_CLIENT } from '../constants.js';
103
103
  const { toFiniteNumber } = Format;
104
- import { blockchainToFloat, floatToBlockchainInt, calculatePriceTolerance, getAssetFees, getBtsSide } from './utils/math.js';
104
+ import { blockchainToFloat, floatToBlockchainInt, calculatePriceTolerance, getAssetFees, getBtsSide, slotIndexForPrice, isSlotInRail, priceSlotEqual } from './utils/math.js';
105
105
  import { parseChainOrder, findMatchingGridOrderByOpenOrder, applyChainSizeToGridOrder, convertToSpreadPlaceholder, virtualizeOrder, buildFillKey, isOrderPlaced, hasOnChainId, isOrderVirtual, resolveSpreadOrderSide, duplicateOrphanLogInfo } from './utils/order.js';
106
106
  import { resolveProcessedFillPersistenceMode } from './processed_fill_store.js';
107
107
  import { getErrorMessage } from '../utils/errors.js';
@@ -196,6 +196,58 @@ function describeNearestAdoptionCandidates(mgr, chainOrder, precision, calcToler
196
196
  * @param {Function} calcToleranceFn - calculatePriceTolerance function
197
197
  * @returns {{candidateSlotId: string, candidateSlotPrice: number, priceDiff: number, tolerance: number}|null}
198
198
  */
199
+ async function adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawChainOrders, matchedGridOrderIds, chainOrderIdsOnGrid, filledOrders, updatedOrders, skipAccounting) {
200
+ let bestMatch = { ...slot };
201
+ const wasVirtual = slot.state === ORDER_STATES.VIRTUAL;
202
+ const wasPartial = slot.state === ORDER_STATES.PARTIAL;
203
+ bestMatch.orderId = chainOrderId;
204
+ bestMatch.state = wasVirtual ? ORDER_STATES.ACTIVE : slot.state;
205
+ const bestMatchRaw = rawChainOrders.get(chainOrderId);
206
+ bestMatch.rawOnChain = bestMatchRaw ? { ...bestMatchRaw, fetchedAt: Date.now() } : bestMatchRaw;
207
+ matchedGridOrderIds.add(bestMatch.id);
208
+ if (bestMatch.rawOnChain) {
209
+ const rawDeferredFee = toFiniteNumber(bestMatch.rawOnChain.deferred_fee, null);
210
+ if (rawDeferredFee !== null && rawDeferredFee > 0) {
211
+ bestMatch.btsFeeState = { deferredFee: blockchainToFloat(rawDeferredFee, BTS_PRECISION) };
212
+ }
213
+ else if (wasVirtual && rawDeferredFee !== null && rawDeferredFee <= 0 && bestMatch.rawOnChain.for_sale > 0) {
214
+ bestMatch.state = ORDER_STATES.PARTIAL;
215
+ }
216
+ }
217
+ const precision = (bestMatch.type === ORDER_TYPES.SELL) ? mgr.assets.assetA.precision : mgr.assets.assetB.precision;
218
+ const targetInt = floatToBlockchainInt(slot.size, precision);
219
+ const chainInt = floatToBlockchainInt(chainOrder.size, precision);
220
+ if (targetInt !== chainInt) {
221
+ const updated = await applyChainSizeToGridOrder(mgr, bestMatch, chainOrder.size);
222
+ if (updated)
223
+ bestMatch = { ...bestMatch, ...updated };
224
+ if (chainInt > 0) {
225
+ if (wasVirtual && bestMatch.state !== ORDER_STATES.PARTIAL)
226
+ bestMatch.state = ORDER_STATES.ACTIVE;
227
+ else if (chainInt < targetInt)
228
+ bestMatch.state = ORDER_STATES.PARTIAL;
229
+ else if (wasPartial)
230
+ bestMatch.state = ORDER_STATES.PARTIAL;
231
+ else
232
+ bestMatch.state = ORDER_STATES.ACTIVE;
233
+ }
234
+ else {
235
+ const spreadOrder = convertToSpreadPlaceholder(bestMatch);
236
+ filledOrders.push({ ...bestMatch });
237
+ await mgr._applyOrderUpdate(spreadOrder, 'sync-pass2-filled', { skipAccounting: skipAccounting, fee: 0 });
238
+ updatedOrders.push(spreadOrder);
239
+ chainOrderIdsOnGrid.add(chainOrderId);
240
+ return true; // filled path
241
+ }
242
+ }
243
+ else if (wasPartial) {
244
+ bestMatch.state = ORDER_STATES.PARTIAL;
245
+ }
246
+ await mgr._applyOrderUpdate(bestMatch, 'sync-pass2-orphan', { skipAccounting: skipAccounting, fee: 0 });
247
+ updatedOrders.push(bestMatch);
248
+ chainOrderIdsOnGrid.add(chainOrderId);
249
+ return false;
250
+ }
199
251
  function computeOutOfToleranceDriftTag(mgr, chainOrder, calcToleranceFn) {
200
252
  if (!mgr?.orders || !chainOrder)
201
253
  return null;
@@ -508,6 +560,39 @@ class SyncEngine {
508
560
  }
509
561
  }
510
562
  mgr.logger?.log?.(`[SYNC] Starting synchronization from ${parsedChainOrders.size} blockchain orders...`, 'info');
563
+ // SUSPECT EMPTY READ GUARD (Fix C): an empty chain read while the grid
564
+ // still holds placed orders (slots with orderIds) is far more likely a
565
+ // lagging/partial node response than a genuinely emptied account — a
566
+ // 0-order read treated as authoritative has virtualized live orders
567
+ // ("phantom" resets). Refuse to reconcile on a
568
+ // suspect empty; the guard is self-expiring: after
569
+ // TIMING.SYNC_SUSPECT_EMPTY_READ_LIMIT consecutive empty reads the
570
+ // account really is empty and the sync accepts it. Any non-empty read
571
+ // resets the counter.
572
+ if (parsedChainOrders.size === 0) {
573
+ const gridOrderIds = Array.from(mgr.orders.values()).filter((o) => o?.orderId).length;
574
+ if (gridOrderIds > 0) {
575
+ const suspect = mgr._suspectEmptyReads || { count: 0, firstAt: 0 };
576
+ suspect.count += 1;
577
+ if (!suspect.firstAt)
578
+ suspect.firstAt = Date.now();
579
+ mgr._suspectEmptyReads = suspect;
580
+ const limit = Math.max(1, Number(TIMING.SYNC_SUSPECT_EMPTY_READ_LIMIT) || 3);
581
+ if (suspect.count < limit) {
582
+ mgr.logger?.log?.(`[SYNC] Suspect empty read (${suspect.count}/${limit} consecutive) with ${gridOrderIds} grid orderIds — ` +
583
+ `refusing reconciliation (phantom protection); accepting after ${limit} consecutive empties or next non-empty read`, 'warn');
584
+ return { filledOrders: [], updatedOrders: [], ordersNeedingCorrection: [], unmatchedChainOrders: [] };
585
+ }
586
+ mgr.logger?.log?.(`[SYNC] Empty read confirmed after ${suspect.count} consecutive attempts — reconciling to empty account`, 'warn');
587
+ mgr._suspectEmptyReads = { count: 0, firstAt: 0 };
588
+ }
589
+ else {
590
+ mgr._suspectEmptyReads = { count: 0, firstAt: 0 };
591
+ }
592
+ }
593
+ else {
594
+ mgr._suspectEmptyReads = { count: 0, firstAt: 0 };
595
+ }
511
596
  // Collect all order IDs that might be modified during reconciliation
512
597
  // Lock them to prevent concurrent modifications from createOrder/cancelOrder
513
598
  const orderIdsToLock = new Set();
@@ -644,10 +729,24 @@ class SyncEngine {
644
729
  continue;
645
730
  }
646
731
  else {
647
- const priceTolerance = calculatePriceTolerance(gridOrder.price, gridOrder.size, gridOrder.type, mgr.assets);
648
- const normalizedTolerance = (priceTolerance === null) ? 0 : priceTolerance;
649
- // Null tolerance is strict mode (0).
650
- if (Math.abs(chainOrder.price - gridOrder.price) > normalizedTolerance) {
732
+ const genesisForPass1 = mgr._genesis;
733
+ let isPriceEqual = false;
734
+ if (genesisForPass1 && Array.isArray(genesisForPass1.priceLevels)) {
735
+ const precision = gridOrder.type === ORDER_TYPES.SELL ? assetAPrecision : assetBPrecision;
736
+ // Genesis path: single epsilon via integer round-trip
737
+ try {
738
+ isPriceEqual = priceSlotEqual(chainOrder.price, gridOrder.price, precision);
739
+ }
740
+ catch {
741
+ isPriceEqual = chainOrder.price === gridOrder.price;
742
+ }
743
+ }
744
+ else {
745
+ const priceTolerance = calculatePriceTolerance(gridOrder.price, gridOrder.size, gridOrder.type, mgr.assets);
746
+ const normalizedTolerance = (priceTolerance === null) ? 0 : priceTolerance;
747
+ isPriceEqual = Math.abs(chainOrder.price - gridOrder.price) <= normalizedTolerance;
748
+ }
749
+ if (!isPriceEqual) {
651
750
  queueCorrection({
652
751
  gridOrder: { ...gridOrder },
653
752
  chainOrderId: gridOrder.orderId,
@@ -663,6 +762,84 @@ class SyncEngine {
663
762
  const currentSizeInt = floatToBlockchainInt(gridOrder.size, precision);
664
763
  const chainSizeInt = floatToBlockchainInt(chainOrder.size, precision);
665
764
  if (currentSizeInt !== chainSizeInt) {
765
+ // SIZE-CONSISTENCY TIEBREAK (mis-tracked duplicate): the
766
+ // tracked order's chain size disagrees with the slot's
767
+ // booked remaining size. Before adopting the chain size,
768
+ // check whether ANOTHER chain order at this price level
769
+ // matches the booked size exactly. The grid allows at most
770
+ // one order per level, so a second order whose size equals
771
+ // the booked remaining means the slot's orderId was
772
+ // mis-assigned (duplicate-price order created by a stale
773
+ // rebuild racing the fill booking) and the real order is
774
+ // the other one. Rebind the slot to it; the previously
775
+ // tracked order falls through to pass 2 as a duplicate-
776
+ // price orphan and is queued for cancellation there.
777
+ let swapMatch = null;
778
+ if (currentSizeInt > 0) {
779
+ for (const [candidateId, candidateOrder] of parsedChainOrders) {
780
+ if (candidateId === gridOrder.orderId)
781
+ continue;
782
+ if (chainOrderIdsOnGrid.has(candidateId))
783
+ continue;
784
+ if (!candidateOrder || candidateOrder.type !== chainOrder.type)
785
+ continue;
786
+ // Per-candidate price check: genesis → priceSlotEqual, else tolerance
787
+ const genesisForSwap = mgr._genesis;
788
+ let swapPriceEqual = false;
789
+ if (genesisForSwap && Array.isArray(genesisForSwap.priceLevels)) {
790
+ const precision = gridOrder.type === ORDER_TYPES.SELL ? assetAPrecision : assetBPrecision;
791
+ try {
792
+ swapPriceEqual = priceSlotEqual(candidateOrder.price, gridOrder.price, precision);
793
+ }
794
+ catch {
795
+ swapPriceEqual = candidateOrder.price === gridOrder.price;
796
+ }
797
+ }
798
+ else {
799
+ const candidateTolerance = calculatePriceTolerance(Math.min(candidateOrder.price, gridOrder.price), Math.max(candidateOrder.size, gridOrder.size), gridOrder.type, mgr.assets);
800
+ swapPriceEqual = Math.abs(candidateOrder.price - gridOrder.price) <= (candidateTolerance ?? 0);
801
+ }
802
+ if (!swapPriceEqual)
803
+ continue;
804
+ if (floatToBlockchainInt(candidateOrder.size, precision) !== currentSizeInt)
805
+ continue;
806
+ swapMatch = { id: candidateId, order: candidateOrder };
807
+ break;
808
+ }
809
+ }
810
+ if (swapMatch) {
811
+ const swapRaw = rawChainOrders.get(swapMatch.id);
812
+ const reboundOrder = {
813
+ ...gridOrder,
814
+ orderId: swapMatch.id,
815
+ rawOnChain: swapRaw
816
+ ? { ...swapRaw, fetchedAt: Date.now() }
817
+ : gridOrder.rawOnChain,
818
+ };
819
+ // Restore fee lifecycle / partial-fill evidence from the
820
+ // adopted order's deferred_fee (same convention as the
821
+ // pass-2 adoption path below: deferred_fee > 0 → fee
822
+ // state; deferred_fee === 0 with for_sale > 0 → the
823
+ // order was partially filled).
824
+ const rawDeferredFee = toFiniteNumber(reboundOrder.rawOnChain?.deferred_fee, null);
825
+ if (rawDeferredFee !== null && rawDeferredFee > 0) {
826
+ reboundOrder.btsFeeState = { deferredFee: blockchainToFloat(rawDeferredFee, BTS_PRECISION) };
827
+ }
828
+ else if (rawDeferredFee !== null && rawDeferredFee <= 0
829
+ && toFiniteNumber(reboundOrder.rawOnChain?.for_sale, 0) > 0) {
830
+ reboundOrder.state = ORDER_STATES.PARTIAL;
831
+ }
832
+ chainOrderIdsOnGrid.delete(gridOrder.orderId);
833
+ chainOrderIdsOnGrid.add(swapMatch.id);
834
+ await mgr._applyOrderUpdate(reboundOrder, 'sync-pass1-duplicate-swap', { skipAccounting: skipAccounting, fee: 0 });
835
+ updatedOrders.push(reboundOrder);
836
+ mgr.logger?.log?.(`[SYNC] Size tiebreak for ${gridOrder.id}: tracked order ${gridOrder.orderId} ` +
837
+ `(chain size ${chainOrder.size}) disagrees with booked size ${gridOrder.size}, but ` +
838
+ `duplicate-price order ${swapMatch.id} (chain size ${swapMatch.order.size}) matches ` +
839
+ `it exactly — rebinding slot to ${swapMatch.id}; ${gridOrder.orderId} falls through ` +
840
+ `to pass 2 as a duplicate-price orphan (queued for cancellation).`, 'warn');
841
+ continue;
842
+ }
666
843
  const newSize = blockchainToFloat(chainSizeInt, precision);
667
844
  const newInt = floatToBlockchainInt(newSize, precision);
668
845
  if (newInt > 0) {
@@ -755,168 +932,99 @@ class SyncEngine {
755
932
  }
756
933
  }
757
934
  // ====================================================================
758
- // PASS 2: CHAIN → GRID - Match unmatched chain orders to grid
935
+ // PASS 2: CHAIN → GRID - Nearest-slot deterministic adoption (genesis-frozen)
759
936
  // ====================================================================
760
- for (const [chainOrderId, chainOrder] of parsedChainOrders) {
937
+ const genesis = mgr._genesis;
938
+ const hasGenesis = genesis && Array.isArray(genesis.priceLevels) && genesis.priceLevels.length > 0;
939
+ const anchorPrice = hasGenesis ? genesis.startPrice : mgr.config?.startPrice;
940
+ const sortedChainEntries = [...parsedChainOrders.entries()].sort((a, b) => {
941
+ const pa = toFiniteNumber(a[1].price);
942
+ const pb = toFiniteNumber(b[1].price);
943
+ const da = Number.isFinite(pa) && Number.isFinite(anchorPrice) ? Math.abs(pa - anchorPrice) : Infinity;
944
+ const db = Number.isFinite(pb) && Number.isFinite(anchorPrice) ? Math.abs(pb - anchorPrice) : Infinity;
945
+ return da - db;
946
+ });
947
+ for (const [chainOrderId, chainOrder] of sortedChainEntries) {
761
948
  if (chainOrderIdsOnGrid.has(chainOrderId))
762
949
  continue;
763
- // The grid allows at most one order per price level. If a placed
764
- // grid order already exists at the orphan's price (within tolerance),
765
- // this orphan is a stale duplicate — skip adoption so the reconcile
766
- // layer cancels it. Size is irrelevant; any duplicate violates the
767
- // invariant. Check BEFORE any adoption attempt, including the first
768
- // match (line 684), because a VIRTUAL slot at a nearby price can
769
- // otherwise silently adopt the orphan before the duplicate guard runs.
950
+ // Genesis path: nearest-slot is single authority (no tolerance)
951
+ if (hasGenesis) {
952
+ let idx;
953
+ try {
954
+ idx = slotIndexForPrice(chainOrder.price, genesis);
955
+ }
956
+ catch {
957
+ unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'no-available-nearest-slot' });
958
+ queueCorrection({ gridOrder: { id: `slot-unknown-${chainOrderId}`, type: chainOrder.type }, chainOrderId, expectedPrice: chainOrder.price, size: chainOrder.size, type: chainOrder.type, isSurplus: true, cancelOnly: true });
959
+ continue;
960
+ }
961
+ const slotId = `slot-${idx}`;
962
+ const gapSlots = genesis.gapSlots ?? mgr._gapSlots ?? 0;
963
+ const boundaryIdx = mgr.boundaryIdx;
964
+ // Duplicate-price guard becomes slotId equality: if placed order already occupies this slot
965
+ const occupying = mgr.orders.get(slotId);
966
+ if (occupying && isOrderPlaced(occupying) && occupying.type === chainOrder.type) {
967
+ unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'duplicate-price-level', candidateSlotId: slotId });
968
+ const { level, suffix } = duplicateOrphanLogInfo(chainOrderId);
969
+ mgr.logger?.log?.(`[SYNC] Orphaned chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}, size=${chainOrder.size}) — NOT adopted: duplicates slot ${slotId} (${occupying.orderId})${suffix}`, level);
970
+ queueCorrection({ gridOrder: occupying, chainOrderId, expectedPrice: chainOrder.price, size: chainOrder.size, type: chainOrder.type, isSurplus: true, cancelOnly: true });
971
+ continue;
972
+ }
973
+ // 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).
974
+ if (boundaryIdx != null && Number.isFinite(Number(boundaryIdx))) {
975
+ const inRail = isSlotInRail(boundaryIdx, gapSlots, chainOrder.type, { id: slotId });
976
+ if (!inRail) {
977
+ unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'no-available-nearest-slot', candidateSlotId: slotId });
978
+ queueCorrection({ gridOrder: { id: slotId, type: chainOrder.type }, chainOrderId, expectedPrice: chainOrder.price, size: chainOrder.size, type: chainOrder.type, isSurplus: true, cancelOnly: true });
979
+ continue;
980
+ }
981
+ }
982
+ const slot = mgr.orders.get(slotId);
983
+ if (!slot || matchedGridOrderIds.has(slot.id) || slot.orderId) {
984
+ unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'no-available-nearest-slot', candidateSlotId: slotId });
985
+ const diag = describeNearestAdoptionCandidates(mgr, chainOrder, (chainOrder.type === ORDER_TYPES.SELL ? assetAPrecision : assetBPrecision), (p, s, t) => calculatePriceTolerance(p, s, t, mgr.assets), matchedGridOrderIds);
986
+ mgr.logger?.log?.(`[SYNC] Unmatched chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}, size=${chainOrder.size}): nearest slot ${slotId} not adoptable. Nearest candidates: ${diag}`, 'warn');
987
+ continue;
988
+ }
989
+ const typeCompat = slot.type === chainOrder.type || slot.type === ORDER_TYPES.SPREAD;
990
+ if (!typeCompat) {
991
+ unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'no-available-nearest-slot', candidateSlotId: slotId });
992
+ continue;
993
+ }
994
+ await adoptChainOrderIntoSlot(mgr, slot, chainOrder, chainOrderId, rawChainOrders, matchedGridOrderIds, chainOrderIdsOnGrid, filledOrders, updatedOrders, skipAccounting);
995
+ continue;
996
+ }
997
+ // Legacy fallback when genesis unavailable (migration)
770
998
  const duplicatePriceOrder = Array.from(mgr.orders.values()).find((o) => {
771
999
  const co = chainOrder;
772
- return o.type === co.type &&
773
- isOrderPlaced(o) &&
774
- co != null &&
775
- Math.abs(o.price - co.price) <= calculatePriceTolerance(Math.min(o.price, co.price), Math.max(o.size, co.size), o.type, mgr.assets);
1000
+ return o.type === co.type && isOrderPlaced(o) && co != null && Math.abs(o.price - co.price) <= calculatePriceTolerance(Math.min(o.price, co.price), Math.max(o.size, co.size), o.type, mgr.assets);
776
1001
  });
777
1002
  if (duplicatePriceOrder) {
778
- unmatchedChainOrders.push({
779
- chainOrderId,
780
- type: chainOrder.type,
781
- price: chainOrder.price,
782
- size: chainOrder.size,
783
- raw: rawChainOrders.get(chainOrderId),
784
- reason: 'duplicate-price-level',
785
- candidateSlotId: duplicatePriceOrder.id,
786
- });
1003
+ unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'duplicate-price-level', candidateSlotId: duplicatePriceOrder.id });
787
1004
  const { level, suffix } = duplicateOrphanLogInfo(chainOrderId);
788
- mgr.logger?.log?.(`[SYNC] Orphaned chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}, ` +
789
- `size=${chainOrder.size}) — NOT adopted: duplicates price level of active ` +
790
- `${duplicatePriceOrder.id} (${duplicatePriceOrder.orderId} at ${duplicatePriceOrder.price})${suffix}`, level);
791
- // Gap 2: Queue duplicate chain order for cancellation via the correction
792
- // mechanism (called from _performSyncFromOpenOrders). Duplicate price levels
793
- // block new placements. Cancelling proactively unblocks CREATEs.
794
- queueCorrection({
795
- gridOrder: duplicatePriceOrder,
796
- chainOrderId,
797
- expectedPrice: chainOrder.price,
798
- size: chainOrder.size,
799
- type: chainOrder.type,
800
- isSurplus: true,
801
- cancelOnly: true,
802
- });
1005
+ mgr.logger?.log?.(`[SYNC] Orphaned chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}, size=${chainOrder.size}) — NOT adopted: duplicates price level of active ${duplicatePriceOrder.id} (${duplicatePriceOrder.orderId} at ${duplicatePriceOrder.price})${suffix}`, level);
1006
+ queueCorrection({ gridOrder: duplicatePriceOrder, chainOrderId, expectedPrice: chainOrder.price, size: chainOrder.size, type: chainOrder.type, isSurplus: true, cancelOnly: true });
803
1007
  continue;
804
1008
  }
805
- const match = findMatchingGridOrderByOpenOrder({ orderId: chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size }, {
806
- orders: mgr.orders,
807
- assets: mgr.assets,
808
- calcToleranceFn: (p, s, t) => calculatePriceTolerance(p, s, t, mgr.assets),
809
- logger: mgr.logger,
810
- allowSmallerChainSize: true,
811
- requireAvailableSlot: true,
812
- excludeGridOrderIds: matchedGridOrderIds
813
- });
1009
+ const match = findMatchingGridOrderByOpenOrder({ orderId: chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size }, { orders: mgr.orders, assets: mgr.assets, calcToleranceFn: (p, s, t) => calculatePriceTolerance(p, s, t, mgr.assets), logger: mgr.logger, allowSmallerChainSize: true, requireAvailableSlot: true, excludeGridOrderIds: matchedGridOrderIds });
814
1010
  if (match && !matchedGridOrderIds.has(match.id)) {
815
- let bestMatch = { ...match };
816
- const wasVirtual = match.state === ORDER_STATES.VIRTUAL;
817
- const wasPartial = match.state === ORDER_STATES.PARTIAL;
818
- bestMatch.orderId = chainOrderId;
819
- bestMatch.state = wasVirtual ? ORDER_STATES.ACTIVE : match.state;
820
- const bestMatchRaw = rawChainOrders.get(chainOrderId);
821
- bestMatch.rawOnChain = bestMatchRaw
822
- ? { ...bestMatchRaw, fetchedAt: Date.now() }
823
- : bestMatchRaw;
824
- matchedGridOrderIds.add(bestMatch.id);
825
- // Reconstruct btsFeeState from raw chain order's deferred_fee.
826
- // After a grid reset, in-memory orders lose btsFeeState. The chain's
827
- // limit_order_object stores the original deferred_fee (or 0 after a
828
- // partial fill). Restoring it ensures correct fee lifecycle accounting
829
- // (cancel refunds, fill maker discounts) on the reconciled order.
830
- // Only set btsFeeState when deferred_fee > 0 — _resolveBtsFeeLifecycle
831
- // deletes btsFeeState when nextDeferred is 0, so setting it to 0 here
832
- // would be an unnecessary set-then-delete cycle.
833
- if (bestMatch.rawOnChain) {
834
- const rawDeferredFee = toFiniteNumber(bestMatch.rawOnChain.deferred_fee, null);
835
- if (rawDeferredFee !== null && rawDeferredFee > 0) {
836
- bestMatch.btsFeeState = { deferredFee: blockchainToFloat(rawDeferredFee, BTS_PRECISION) };
837
- }
838
- else if (wasVirtual && rawDeferredFee !== null && rawDeferredFee <= 0 && bestMatch.rawOnChain.for_sale > 0) {
839
- // Core zeros deferred_fee on every fill (fill_limit_order db_market.cpp:1895).
840
- // If deferred_fee is 0 while for_sale > 0, the order was partially filled.
841
- bestMatch.state = ORDER_STATES.PARTIAL;
842
- }
843
- }
844
- const precision = (bestMatch.type === ORDER_TYPES.SELL) ? assetAPrecision : assetBPrecision;
845
- const targetInt = floatToBlockchainInt(match.size, precision);
846
- const chainInt = floatToBlockchainInt(chainOrder.size, precision);
847
- if (targetInt !== chainInt) {
848
- const updated = await applyChainSizeToGridOrder(mgr, bestMatch, chainOrder.size);
849
- if (updated)
850
- bestMatch = { ...bestMatch, ...updated };
851
- if (chainInt > 0) {
852
- if (wasVirtual) {
853
- // Only override to ACTIVE if partial-fill check above didn't already
854
- // set PARTIAL via deferred_fee === 0. Skip override to preserve signal.
855
- if (bestMatch.state !== ORDER_STATES.PARTIAL) {
856
- bestMatch.state = ORDER_STATES.ACTIVE;
857
- }
858
- }
859
- else if (chainInt < targetInt) {
860
- bestMatch.state = ORDER_STATES.PARTIAL;
861
- }
862
- else if (wasPartial) {
863
- bestMatch.state = ORDER_STATES.PARTIAL;
864
- }
865
- else {
866
- bestMatch.state = ORDER_STATES.ACTIVE;
867
- }
868
- }
869
- else {
870
- const spreadOrder = convertToSpreadPlaceholder(bestMatch);
871
- filledOrders.push({ ...bestMatch });
872
- await mgr._applyOrderUpdate(spreadOrder, 'sync-pass2-filled', { skipAccounting: skipAccounting, fee: 0 });
873
- updatedOrders.push(spreadOrder);
874
- chainOrderIdsOnGrid.add(chainOrderId);
875
- continue;
876
- }
877
- }
878
- else if (wasPartial) {
879
- bestMatch.state = ORDER_STATES.PARTIAL;
880
- }
881
- await mgr._applyOrderUpdate(bestMatch, 'sync-pass2-orphan', { skipAccounting: skipAccounting, fee: 0 });
882
- updatedOrders.push(bestMatch);
883
- chainOrderIdsOnGrid.add(chainOrderId);
1011
+ await adoptChainOrderIntoSlot(mgr, match, chainOrder, chainOrderId, rawChainOrders, matchedGridOrderIds, chainOrderIdsOnGrid, filledOrders, updatedOrders, skipAccounting);
884
1012
  }
885
1013
  else if (match) {
886
- mgr.logger?.log?.(`Warning: Orphan chain order ${chainOrderId} matched grid order ${match.id}, ` +
887
- `but grid order was already matched to another chain order. Queuing orphan for cancellation.`, 'warn');
888
- queueCorrection({
889
- gridOrder: match,
890
- chainOrderId,
891
- expectedPrice: chainOrder.price,
892
- size: chainOrder.size,
893
- type: chainOrder.type,
894
- isSurplus: true,
895
- cancelOnly: true,
896
- });
897
- // Also push to unmatchedChainOrders so the existing auto-cancel
898
- // path at dexbot_class.ts:4262 can pick it up if the correction
899
- // mechanism does not fire first.
900
- unmatchedChainOrders.push({
901
- chainOrderId,
902
- type: chainOrder.type,
903
- price: chainOrder.price,
904
- size: chainOrder.size,
905
- raw: rawChainOrders.get(chainOrderId),
906
- reason: 'already-matched-slot',
907
- candidateSlotId: match.id,
908
- });
1014
+ mgr.logger?.log?.(`Warning: Orphan chain order ${chainOrderId} matched grid order ${match.id}, but grid order was already matched to another chain order. Queuing orphan for cancellation.`, 'warn');
1015
+ queueCorrection({ gridOrder: match, chainOrderId, expectedPrice: chainOrder.price, size: chainOrder.size, type: chainOrder.type, isSurplus: true, cancelOnly: true });
1016
+ unmatchedChainOrders.push({ chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), reason: 'already-matched-slot', candidateSlotId: match.id });
909
1017
  }
910
1018
  else {
911
- // No grid slot matched by type+price+size in the first pass.
912
- // Fallback: adopt into the nearest VIRTUAL/spread slot so the
913
- // orphan becomes visible to checkWindowDust for dust auto-cancel.
914
- // The duplicate-price-level guard already ran above so it is not
915
- // repeated here.
1019
+ // Legacy spread-orphan fallback: adopt into nearest VIRTUAL/spread slot (allowSpreadType + skipSizeMatch) with widened tolerance
916
1020
  const adoptedSlot = findMatchingGridOrderByOpenOrder({ orderId: chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size }, {
917
1021
  orders: mgr.orders,
918
1022
  assets: mgr.assets,
919
- calcToleranceFn: (p, s, t) => calculatePriceTolerance(p, s, t, mgr.assets),
1023
+ calcToleranceFn: (p, s, t) => {
1024
+ const strict = calculatePriceTolerance(p, s, t, mgr.assets);
1025
+ const mult = mgr?.config?.gridLimits?.ORPHAN_ADOPTION_TOLERANCE_MULTIPLIER || 4;
1026
+ return strict == null ? null : strict * mult;
1027
+ },
920
1028
  allowSpreadType: true,
921
1029
  skipSizeMatch: true,
922
1030
  requireAvailableSlot: true,
@@ -938,30 +1046,20 @@ class SyncEngine {
938
1046
  state: adoptedState,
939
1047
  size: chainOrder.size,
940
1048
  price: chainOrder.price,
941
- rawOnChain: adoptedRaw
942
- ? { ...adoptedRaw, fetchedAt: Date.now() }
943
- : adoptedRaw,
1049
+ rawOnChain: adoptedRaw ? { ...adoptedRaw, fetchedAt: Date.now() } : adoptedRaw,
944
1050
  ...(adoptedBtsFeeState ? { btsFeeState: adoptedBtsFeeState } : {}),
945
1051
  };
946
1052
  matchedGridOrderIds.add(adoptedSlot.id);
947
1053
  chainOrderIdsOnGrid.add(chainOrderId);
948
1054
  await mgr._applyOrderUpdate(adoptedOrder, 'sync-pass2-adopt-orphan', { skipAccounting: skipAccounting, fee: 0 });
949
1055
  updatedOrders.push(adoptedOrder);
950
- mgr.logger?.log?.(`[SYNC] Orphaned chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}, ` +
951
- `size=${chainOrder.size}) adopted into slot ${adoptedSlot.id} (was ${adoptedSlot.type})`, 'warn');
1056
+ mgr.logger?.log?.(`[SYNC] Orphaned chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}, size=${chainOrder.size}) adopted into slot ${adoptedSlot.id} (was ${adoptedSlot.type})`, 'warn');
952
1057
  }
953
1058
  else {
954
1059
  const precision = (chainOrder.type === ORDER_TYPES.SELL) ? assetAPrecision : assetBPrecision;
955
1060
  const candidateDiagnostics = describeNearestAdoptionCandidates(mgr, chainOrder, precision, (p, s, t) => calculatePriceTolerance(p, s, t, mgr.assets), matchedGridOrderIds);
956
1061
  const driftTag = computeOutOfToleranceDriftTag(mgr, chainOrder, (p, s, t) => calculatePriceTolerance(p, s, t, mgr.assets));
957
- const unmatchedEntry = {
958
- chainOrderId,
959
- type: chainOrder.type,
960
- price: chainOrder.price,
961
- size: chainOrder.size,
962
- raw: rawChainOrders.get(chainOrderId),
963
- candidateDiagnostics
964
- };
1062
+ const unmatchedEntry = { chainOrderId, type: chainOrder.type, price: chainOrder.price, size: chainOrder.size, raw: rawChainOrders.get(chainOrderId), candidateDiagnostics };
965
1063
  if (driftTag) {
966
1064
  unmatchedEntry.reason = 'price-drift-orphan';
967
1065
  unmatchedEntry.candidateSlotId = driftTag.candidateSlotId;
@@ -970,10 +1068,7 @@ class SyncEngine {
970
1068
  unmatchedEntry.tolerance = driftTag.tolerance;
971
1069
  }
972
1070
  unmatchedChainOrders.push(unmatchedEntry);
973
- mgr.logger?.log?.(`[SYNC] Unmatched chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}, ` +
974
- `size=${chainOrder.size}): no adoptable slot found` +
975
- (driftTag ? ` (price-drift-orphan slot=${driftTag.candidateSlotId}@${driftTag.candidateSlotPrice} diff=${driftTag.priceDiff} tol=${driftTag.tolerance})` : '') +
976
- `. Nearest candidates: ${candidateDiagnostics}`, 'warn');
1071
+ mgr.logger?.log?.(`[SYNC] Unmatched chain order ${chainOrderId} (${chainOrder.type}, price=${chainOrder.price}, size=${chainOrder.size}): no adoptable slot found` + (driftTag ? ` (price-drift-orphan slot=${driftTag.candidateSlotId}@${driftTag.candidateSlotPrice} diff=${driftTag.priceDiff} tol=${driftTag.tolerance})` : '') + `. Nearest candidates: ${candidateDiagnostics}`, 'warn');
977
1072
  }
978
1073
  }
979
1074
  }
@@ -1492,7 +1587,7 @@ class SyncEngine {
1492
1587
  // and the last update wins (`applyGridUpdateBatch` is last-wins
1493
1588
  // per slot), leaving a phantom residual equal to the earlier
1494
1589
  // fills' sum — exactly the fund-invariant CRITICAL seen in the
1495
- // XRP-BTS log whenever a batch contained 2+ fills of one order.
1590
+ // AAA-BBB log whenever a batch contained 2+ fills of one order.
1496
1591
  const gridUpdates = [];
1497
1592
  const filledOrders = [];
1498
1593
  const updatedOrders = [];