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
@@ -110,7 +110,7 @@ function _snapshotFundState(manager) {
110
110
  sellLocked: Number(manager.accountTotals?.sellLocked || 0),
111
111
  };
112
112
  }
113
- import { floatToBlockchainInt, getPrecisionByOrderType, getPrecisionsForManager, calculateOrderCreationFees, calculateOrderSizes, calculateRotationOrderSizes, calculateAvailableFundsValue, calculateGridSideDivergenceMetric, getPrecisionSlack, getMinOrderSize, getSingleDustThreshold, getGridBestPrices, calculateSpreadFromOrders, allocateFundsByWeights, calculateGapSlots as _mathGapSlots, findPriceCollision, getBtsSide, getSellStartIdx, resolveGapBand, countGapBandSpread, validatePersistedBoundary, adjustBudgetForBtsFees, clamp, } from './utils/math.js';
113
+ import { floatToBlockchainInt, getPrecisionByOrderType, getPrecisionsForManager, calculateOrderCreationFees, calculateOrderSizes, calculateRotationOrderSizes, calculateAvailableFundsValue, calculateGridSideDivergenceMetric, getPrecisionSlack, getMinOrderSize, getSingleDustThreshold, getGridBestPrices, calculateSpreadFromOrders, allocateFundsByWeights, calculateGapSlots as _mathGapSlots, priceSlotEqual, getBtsSide, getSellStartIdx, resolveGapBand, countGapBandSpread, validatePersistedBoundary, adjustBudgetForBtsFees, clamp, buildGenesisFromPriceLevels, assertSlotPriceInvariant, hashPriceLevels, } from './utils/math.js';
114
114
  import { filterOrdersByType, checkSizesBeforeMinimum, checkSizeThreshold, resolveConfiguredPriceBound, shouldFlagOutOfSpread, isOrderHealthy, isPhantomOrder, isSlotAvailable, isOrderOnChain, isOrderPlaced, hasOnChainId, isEmptyGridSlot, parseSlotIndex, calculateIdealBoundary, assignGridRoles, resolveOnChainRetypeType } from './utils/order.js';
115
115
  import { loadAmaCenterPrice, loadAmaCenterSnapshot, withBlockchainRetry } from './utils/system.js';
116
116
  import * as MathUtils from './utils/math.js';
@@ -361,6 +361,21 @@ export function createOrderGrid(config) {
361
361
  }
362
362
  // Sort all levels from lowest to highest (Master Rail order)
363
363
  priceLevels.sort((a, b) => a - b);
364
+ // Dedupe geometric levels that collide at float precision (tiny increments);
365
+ // keeps slot-N ↔ index mapping stable vs migration dedupe (grid.ts:652)
366
+ {
367
+ const seen = new Set();
368
+ const deduped = [];
369
+ for (const p of priceLevels) {
370
+ const key = Number(p).toFixed(12);
371
+ if (!seen.has(key)) {
372
+ seen.add(key);
373
+ deduped.push(p);
374
+ }
375
+ }
376
+ priceLevels.length = 0;
377
+ priceLevels.push(...deduped);
378
+ }
364
379
  if (priceLevels.length === 0) {
365
380
  throw new Error(`Grid generation produced no price levels for startPrice=${startPrice}, ` +
366
381
  `bounds=[${minPrice}, ${maxPrice}], incrementPercent=${incrementPercent}. ` +
@@ -402,7 +417,8 @@ export function createOrderGrid(config) {
402
417
  buy: Math.floor(gapSlots / 2),
403
418
  sell: gapSlots - Math.floor(gapSlots / 2)
404
419
  };
405
- return { orders: updatedOrders, boundaryIdx, initialSpreadCount, gapSlots };
420
+ const genesis = buildGenesisFromPriceLevels(startPrice, incrementPercent, gapSlots, priceLevels);
421
+ return { orders: updatedOrders, boundaryIdx, initialSpreadCount, gapSlots, priceLevels, genesis };
406
422
  }
407
423
  /**
408
424
  * Internal utility to clear all order-related manager caches.
@@ -442,10 +458,165 @@ function _clearOrderCachesLogic(manager) {
442
458
  * @param {number|null} [boundaryIdx=null] - The master boundary index.
443
459
  * @returns {Promise<void>}
444
460
  */
445
- export async function loadGrid(manager, grid, boundaryIdx = null) {
461
+ export async function loadGrid(manager, grid, boundaryIdx = null, genesisInput = null) {
446
462
  if (!Array.isArray(grid))
447
463
  return;
448
464
  return await manager._gridLock.acquire(async () => {
465
+ // Genesis determinism: if snapshot provided genesis, validate slots
466
+ // against it; if legacy snapshot has no genesis, migrate by building
467
+ // genesis from live config and re-sorting to canonical price order.
468
+ let genesis = genesisInput || manager._genesis || null;
469
+ // Validate genesis hash if present (wired for stale-pair detection #3)
470
+ // On mismatch we only warn — genesis is still used for validation (log mode) / virtualization (enforce mode);
471
+ // a stale persisted snapshot vs fresh manager._genesis scenario is covered by the log/enforce gate above
472
+ if (genesis && Array.isArray(genesis.priceLevels) && typeof genesis.priceLevelsHash === 'string') {
473
+ try {
474
+ const recomputed = hashPriceLevels(genesis.priceLevels);
475
+ if (recomputed !== genesis.priceLevelsHash) {
476
+ manager.logger?.log?.(`[GENESIS] hash mismatch persisted=${genesis.priceLevelsHash} recomputed=${recomputed} — persisted genesis hash does not match recomputed hash; continuing with persisted genesis`, 'warn');
477
+ }
478
+ }
479
+ catch { }
480
+ }
481
+ const validationMode = (() => {
482
+ try {
483
+ const raw = (typeof process !== 'undefined' && process.env?.GRID_PRICE_SLOT_VALIDATION) || 'log';
484
+ return String(raw).toLowerCase() === 'enforce' ? 'enforce' : 'log';
485
+ }
486
+ catch {
487
+ return 'log';
488
+ }
489
+ })();
490
+ if (genesis && Array.isArray(genesis.priceLevels)) {
491
+ // Validate each slot's price against genesis; virtualize only in enforce mode (plan §13)
492
+ const newGrid = [];
493
+ for (const slot of grid) {
494
+ try {
495
+ assertSlotPriceInvariant(slot, genesis);
496
+ newGrid.push(slot);
497
+ }
498
+ catch (e) {
499
+ const msg = `[GENESIS] Slot ${slot?.id} price mismatch vs genesis → ${validationMode === 'enforce' ? 'virtualize' : 'log-only'}: ${getErrorMessage(e)}`;
500
+ manager.logger?.log?.(msg, 'warn');
501
+ if (validationMode === 'enforce') {
502
+ newGrid.push({ ...slot, state: ORDER_STATES.VIRTUAL, size: 0, orderId: '' });
503
+ }
504
+ else {
505
+ newGrid.push(slot);
506
+ }
507
+ }
508
+ }
509
+ grid = newGrid;
510
+ // Ensure canonical price-sorted order matches slot-N order
511
+ // Quirk fix: previously `isPriceSorted && parsed!==i` gated re-sort, so a shuffled
512
+ // array with misaligned ids but unsorted prices incorrectly skipped re-sort.
513
+ // Now: re-sort if any parseable id != index (price order is authoritative via slot-N).
514
+ const idxOrderMatchesPriceOrder = (() => {
515
+ for (let i = 0; i < grid.length; i++) {
516
+ const parsed = parseSlotIndex(grid[i]?.id);
517
+ if (parsed === null)
518
+ return false;
519
+ if (parsed !== i)
520
+ return false;
521
+ }
522
+ return true;
523
+ })();
524
+ if (!idxOrderMatchesPriceOrder) {
525
+ const sorted = [...grid].sort((a, b) => {
526
+ const pa = parseSlotIndex(a?.id);
527
+ const pb = parseSlotIndex(b?.id);
528
+ if (pa !== null && pb !== null)
529
+ return pa - pb;
530
+ return Number(a.price) - Number(b.price);
531
+ });
532
+ // Detect if re-sort actually changes order
533
+ let changed = false;
534
+ for (let i = 0; i < grid.length; i++)
535
+ if (grid[i].id !== sorted[i].id) {
536
+ changed = true;
537
+ break;
538
+ }
539
+ if (changed) {
540
+ manager.logger?.log?.(`[GENESIS] Re-sorted persisted grid to canonical slot-N order`, 'warn');
541
+ grid = sorted;
542
+ }
543
+ }
544
+ // Guard: explicit persistedGenesis should not blindly overwrite a fresher in-memory _genesis
545
+ // from an in-process rebuild (currently unreachable but latent hazard if flows change)
546
+ if (manager._genesis && manager._genesis.priceLevelsHash && genesis.priceLevelsHash && manager._genesis.priceLevelsHash !== genesis.priceLevelsHash) {
547
+ manager.logger?.log?.(`[GENESIS] persisted genesis hash ${genesis.priceLevelsHash} differs from in-memory ${manager._genesis.priceLevelsHash} — keeping in-memory genesis`, 'warn');
548
+ }
549
+ else {
550
+ manager._genesis = genesis;
551
+ }
552
+ }
553
+ else if (grid.length > 0) {
554
+ // Migration: build genesis from live geometric rail (plan §4.1/§10)
555
+ // Do NOT derive priceLevels from persisted slot prices — a truncated
556
+ // persisted array would permanently shrink genesis. Recompute from
557
+ // startPrice/min/max/increment per createOrderGrid §2.1.
558
+ const startPrice = Number(manager.config?.startPrice);
559
+ const minPrice = Number(manager.config?.minPrice);
560
+ const maxPrice = Number(manager.config?.maxPrice);
561
+ const incPct = Number(manager.config?.incrementPercent);
562
+ if (Number.isFinite(startPrice) && Number.isFinite(minPrice) && Number.isFinite(maxPrice) && Number.isFinite(incPct)) {
563
+ try {
564
+ const stepUp = 1 + (incPct / 100);
565
+ const stepDown = 1 - (incPct / 100);
566
+ const priceLevels = [];
567
+ let upPrice = startPrice * Math.sqrt(stepUp);
568
+ while (upPrice <= maxPrice) {
569
+ priceLevels.push(upPrice);
570
+ upPrice *= stepUp;
571
+ }
572
+ let downPrice = startPrice * Math.sqrt(stepDown);
573
+ while (downPrice >= minPrice) {
574
+ priceLevels.push(downPrice);
575
+ downPrice *= stepDown;
576
+ }
577
+ priceLevels.sort((a, b) => a - b);
578
+ // Dedupe (floatToBlockchainInt equality via fixed 12-decimals)
579
+ const uniq = [];
580
+ const seen = new Set();
581
+ for (const p of priceLevels) {
582
+ const key = Number(p).toFixed(12);
583
+ if (!seen.has(key)) {
584
+ seen.add(key);
585
+ uniq.push(p);
586
+ }
587
+ }
588
+ const gapSlotsForGenesis = calculateGapSlots(incPct, manager.config?.targetSpreadPercent, manager.config?.gridLimits);
589
+ const built = buildGenesisFromPriceLevels(startPrice, incPct, gapSlotsForGenesis, uniq);
590
+ // Cross-check: if user edited startPrice/min/max/increment across restarts on a legacy snapshot,
591
+ // the new-config rail will mismatch most persisted slot prices → noisy log / mass-virtualize
592
+ // in enforce mode and the mismatched genesis would be persisted. Count failures first.
593
+ let mismatchCount = 0;
594
+ for (const slot of grid) {
595
+ try {
596
+ assertSlotPriceInvariant(slot, built);
597
+ }
598
+ catch {
599
+ mismatchCount++;
600
+ }
601
+ }
602
+ const mismatchRatio = grid.length > 0 ? mismatchCount / grid.length : 0;
603
+ if (mismatchRatio > 0.5) {
604
+ manager.logger?.log?.(`[GENESIS] Migration: ${mismatchCount}/${grid.length} slots mismatch new-config rail (ratio ${mismatchRatio.toFixed(2)}) — config may have changed since snapshot; NOT adopting migration genesis (validation would ${validationMode === 'enforce' ? 'mass-virtualize' : 'be noisy'}). Persisted grid will be kept as-is until a clean rebuild`, 'warn');
605
+ }
606
+ else {
607
+ if (mismatchCount > 0) {
608
+ manager.logger?.log?.(`[GENESIS] Migration: ${mismatchCount}/${grid.length} slots mismatch new-config rail — will be logged${validationMode === 'enforce' ? '/virtualized' : ''} on next load`, 'warn');
609
+ }
610
+ manager._genesis = built;
611
+ genesis = built;
612
+ manager.logger?.log?.(`[GENESIS] Migrated legacy grid: built genesis with ${uniq.length} levels (hash ${built.priceLevelsHash})`, 'info');
613
+ }
614
+ }
615
+ catch (e) {
616
+ manager.logger?.log?.(`[GENESIS] Migration failed: ${getErrorMessage(e)}`, 'warn');
617
+ }
618
+ }
619
+ }
449
620
  try {
450
621
  await withBlockchainRetry(() => manager._initializeAssets(), 'initializeAssets', { logger: manager.logger });
451
622
  }
@@ -517,11 +688,10 @@ export async function loadGrid(manager, grid, boundaryIdx = null) {
517
688
  manager.logger?.log?.(`Restored boundary index: ${restoredBoundary}`, 'info');
518
689
  }
519
690
  // Reassign slot types based on current boundary.
520
- // Every slot's type must match its position in the price-sorted rail
521
- // relative to the boundary + gapSlots. Stale persisted types cause
522
- // split spread zones and ILLEGAL_SPREAD_STATE errors. The subsequent
523
- // sync will detect type mismatches with chain orders (e.g. a BUY-zone
524
- // slot holding a SELL chain order) and auto-cancel + recreate them.
691
+ // Every slot's type must match its price-slot index (parseSlotIndex)
692
+ // relative to the boundary + gapSlots, not array position. After
693
+ // genesis freeze the persisted array may have been re-sorted, but
694
+ // slot-N is the canonical identity.
525
695
  if (restoredBoundary !== null) {
526
696
  const gapSlots = loadGapSlots;
527
697
  manager._gapSlots = gapSlots;
@@ -529,9 +699,22 @@ export async function loadGrid(manager, grid, boundaryIdx = null) {
529
699
  const sellStartIdx = getSellStartIdx(restoredBoundary, gapSlots);
530
700
  let reassignCount = 0;
531
701
  grid = grid.map((slot, i) => {
532
- const correctType = (i <= buyEndIdx)
702
+ const parsedIdx = parseSlotIndex(slot?.id);
703
+ // Plan §4.3: unparseable ids are invalid per decision #3 — enforce virtualizes,
704
+ // log mode keeps index fallback (legacy grids with ids like 'b1'/'planned' need type via position)
705
+ if (parsedIdx === null) {
706
+ if (validationMode === 'enforce') {
707
+ manager.logger?.log?.(`[GENESIS] unparseable slot id ${slot?.id} at index ${i} → virtualize (SPREAD)`, 'warn');
708
+ reassignCount++;
709
+ return { ...slot, state: ORDER_STATES.VIRTUAL, size: 0, orderId: '', type: ORDER_TYPES.SPREAD };
710
+ }
711
+ manager.logger?.log?.(`[GENESIS] unparseable slot id ${slot?.id} at index ${i} → log-only, using index fallback ${i} for type`, 'warn');
712
+ // fall through with idx = i so legacy sized VIRTUAL rail slots (e.g. 'planned') keep their size/type
713
+ }
714
+ const idx = parsedIdx !== null ? parsedIdx : i;
715
+ const correctType = (idx <= buyEndIdx)
533
716
  ? ORDER_TYPES.BUY
534
- : (i >= sellStartIdx)
717
+ : (idx >= sellStartIdx)
535
718
  ? ORDER_TYPES.SELL
536
719
  : ORDER_TYPES.SPREAD;
537
720
  // DEFENSIVE BACKSTOP: a VIRTUAL slot with no orderId and
@@ -570,7 +753,7 @@ export async function loadGrid(manager, grid, boundaryIdx = null) {
570
753
  // comparison false and wrongly resolves below-center
571
754
  // slots to SELL.
572
755
  if (newType === ORDER_TYPES.SPREAD && isOrderOnChain(slot)) {
573
- newType = resolveOnChainRetypeType(slot, i, buyEndIdx, ORDER_TYPES);
756
+ newType = resolveOnChainRetypeType(slot, idx, buyEndIdx, ORDER_TYPES);
574
757
  }
575
758
  reassignCount++;
576
759
  return { ...slot, type: newType };
@@ -605,6 +788,7 @@ export async function loadGrid(manager, grid, boundaryIdx = null) {
605
788
  }
606
789
  manager.pauseRecalcLogging();
607
790
  manager.pauseFundRecalc();
791
+ let sanitizedSizedVirtual = [];
608
792
  try {
609
793
  // RC-2: Use applyOrderUpdate (PRIVATE/UNLOCKED)
610
794
  for (const order of grid) {
@@ -613,8 +797,31 @@ export async function loadGrid(manager, grid, boundaryIdx = null) {
613
797
  manager.logger?.log?.(`Sanitizing corrupted order ${order.id}: ACTIVE/PARTIAL without orderId -> VIRTUAL`, 'warn');
614
798
  currentOrder = { ...order, state: ORDER_STATES.VIRTUAL };
615
799
  }
800
+ // Sized VIRTUAL slot with no on-chain id AND durable orphan
801
+ // evidence (createUncertain: set only where a CREATE
802
+ // broadcast result was lost — COW uncertain-restore and
803
+ // startup uncertain-create handlers). Unflagged sized
804
+ // VIRTUAL slots are the NORMAL planned-but-unplaced grid
805
+ // state (budgeted sizes cover the whole rail and persist
806
+ // by design), so their sizes must survive reload; only
807
+ // flagged orphans get zeroed so re-derivation or spread
808
+ // correction re-places them next cycle. One aggregate
809
+ // warn instead of per-slot spam.
810
+ if (currentOrder.state === ORDER_STATES.VIRTUAL
811
+ && !hasOnChainId(currentOrder)
812
+ && Number(currentOrder.size || 0) > 0
813
+ && currentOrder.createUncertain === true) {
814
+ sanitizedSizedVirtual.push(currentOrder.id);
815
+ currentOrder = { ...currentOrder, size: 0, createUncertain: false };
816
+ }
616
817
  await manager._applyOrderUpdate(currentOrder, 'grid-load', { skipAccounting: true });
617
818
  }
819
+ if (sanitizedSizedVirtual.length > 0) {
820
+ const preview = sanitizedSizedVirtual.slice(0, 10).join(', ');
821
+ manager.logger?.log?.(`Sanitized ${sanitizedSizedVirtual.length} creation-uncertain sized slot(s) on load ` +
822
+ `(VIRTUAL with size but no orderId -> size dropped): ${preview}` +
823
+ (sanitizedSizedVirtual.length > 10 ? ' …' : ''), 'warn');
824
+ }
618
825
  // Gap-band occupancy for the spread metric. Empty slots are
619
826
  // normalized to SPREAD (side-neutral) above, so a raw
620
827
  // `type === SPREAD` count would include every empty slot on both
@@ -901,13 +1108,15 @@ export async function initializeGrid(manager) {
901
1108
  // Continuing would create grid with 0 fund allocation, rendering it non-functional
902
1109
  throw new Error(`Cannot initialize grid without account totals: ${getErrorMessage(e)}`);
903
1110
  }
904
- const { orders, boundaryIdx, initialSpreadCount, gapSlots } = createOrderGrid({
1111
+ const { orders, boundaryIdx, initialSpreadCount, gapSlots, genesis } = createOrderGrid({
905
1112
  ...manager.config,
906
1113
  startPrice: gridStartPrice,
907
1114
  minPrice: resolvedMinP,
908
1115
  maxPrice: resolvedMaxP,
909
1116
  });
910
1117
  manager._gapSlots = gapSlots;
1118
+ if (genesis)
1119
+ manager._genesis = genesis;
911
1120
  // RC-8: Update boundary with notification to dependent systems
912
1121
  // Persist master boundary for StrategyEngine
913
1122
  if (manager.boundaryIdx !== boundaryIdx) {
@@ -1063,6 +1272,18 @@ export async function recalculateGrid(manager, opts) {
1063
1272
  manager.logger?.log?.(`Error during startup order reconciliation: ${getErrorMessage(err)}`, 'error');
1064
1273
  throw new Error(`Grid recalculation failed during order reconciliation: ${getErrorMessage(err)}`);
1065
1274
  }
1275
+ // Fix #5 (Mode D): the P3 boundary-evidence gate above may have
1276
+ // re-derived the boundary via manager._restoreBoundary. recalculateGrid's
1277
+ // earlier persistGrid (before initializeGrid) captured the stale value, so
1278
+ // without this flush the corrected boundary never reaches disk and the
1279
+ // [BOUNDARY-EVIDENCE] contradiction re-fires on every resync. Persist the
1280
+ // reconciled grid (corrected boundary included) now.
1281
+ try {
1282
+ await manager.persistGrid(undefined);
1283
+ }
1284
+ catch (persistErr) {
1285
+ manager.logger?.log?.(`Error persisting boundary-corrected grid after reconcile: ${getErrorMessage(persistErr)}`, 'warn');
1286
+ }
1066
1287
  if (_resyncAborted)
1067
1288
  return;
1068
1289
  manager.logger?.log?.('Full resync complete.', 'info');
@@ -1674,6 +1895,15 @@ export async function checkSpreadCondition(manager, _BitShares, updateOrdersOnCh
1674
1895
  // Limit spread = nominal + half increment tolerance (0.5 steps).
1675
1896
  const limitSpread = nominalSpread + (manager.config.incrementPercent * toleranceSteps);
1676
1897
  manager.logger?.log?.(`Spread too wide (${Format.formatPercent(currentSpread)} > ${Format.formatPercent(limitSpread)}), correcting with ${manager.outOfSpread} extra slot(s)...`, 'warn');
1898
+ // Refresh funds before the side decision below: the top-of-tick recalc may
1899
+ // predate fills processed since, and determineOrderSideByFunds reads
1900
+ // manager.funds/accountTotals directly (not via _getSizingContext). No-op
1901
+ // while a fill cycle holds the recalc pause; funds that move after this
1902
+ // point are covered by the TOCTOU re-plan before broadcast.
1903
+ try {
1904
+ await manager.recalculateFunds();
1905
+ }
1906
+ catch { /* best-effort */ }
1677
1907
  const decision = determineOrderSideByFunds(manager, lastPrice);
1678
1908
  if (!decision.side)
1679
1909
  return false;
@@ -1838,13 +2068,25 @@ export async function checkWindowDust(manager) {
1838
2068
  const topSellOrder = allOrders
1839
2069
  .filter((o) => o.type === ORDER_TYPES.SELL && isLiveOrder(o))
1840
2070
  .sort((a, b) => a.price - b.price)[0];
1841
- // Check if an order has a duplicate price level — an active sibling at the
1842
- // same price within tolerance. If so, cancelling won't create a grid gap.
1843
- // Only checks ACTIVE siblings. If two PARTIALs share a price with no active
1844
- // sibling, neither qualifies and the gap is left to the rebalancer.
1845
- // Uses the LARGER size of the two orders for tolerance calculation to prevent
1846
- // a tiny dust order from inflating the tolerance window.
1847
- const hasDuplicatePriceLevel = (order, assets) => findPriceCollision(allOrders, order.id, order.price, order.size, order.type, assets, (o) => o.type === order.type && o.state === ORDER_STATES.ACTIVE && !!o.orderId && o.price != null) != null;
2071
+ // Check if an order has a duplicate slot — an active sibling with same price via integer round-trip.
2072
+ const hasDuplicatePriceLevel = (order, assets) => {
2073
+ const precision = order.type === ORDER_TYPES.SELL ? assets.assetA.precision : assets.assetB.precision;
2074
+ for (const o of allOrders) {
2075
+ if (o.id === order.id)
2076
+ continue;
2077
+ if (o.type !== order.type || o.state !== ORDER_STATES.ACTIVE || !o.orderId || o.price == null)
2078
+ continue;
2079
+ try {
2080
+ if (priceSlotEqual(o.price, order.price, precision))
2081
+ return true;
2082
+ }
2083
+ catch {
2084
+ if (o.price === order.price)
2085
+ return true;
2086
+ }
2087
+ }
2088
+ return false;
2089
+ };
1848
2090
  const assets = manager.assets;
1849
2091
  const allPartials = allOrders.filter((o) => isLiveOrder(o) && o.state === ORDER_STATES.PARTIAL);
1850
2092
  const isTopBuy = (o) => topBuyOrder && o.id === topBuyOrder.id;
@@ -1998,15 +2240,26 @@ export function determineOrderSideByFunds(manager, currentMarketPrice) {
1998
2240
  const buyViable = buyAvailable > buyMinUnit;
1999
2241
  const sellViable = sellAvailable > sellMinUnit;
2000
2242
  let side = null;
2243
+ let skipReason = null;
2001
2244
  if (buyViable && sellViable) {
2002
2245
  // Normalize sell (assetA) to assetB units using market price so both sides
2003
2246
  // are comparable. Without this, a raw number comparison (e.g. 2192 BTS vs
2004
2247
  // 0.12 XRP) always picks BUY even when the sell side is larger in value.
2248
+ // When the market price is unavailable (non-finite, e.g. empty book after
2249
+ // a crash wiped one side), raw-unit comparison across assets is meaningless
2250
+ // and can arm the wrong side in exactly the scenario where price may be
2251
+ // missing. Skip the correction and let the next cycle decide with a real
2252
+ // price rather than guess.
2005
2253
  const marketPrice = Number(currentMarketPrice);
2006
- const sellInBuyUnits = (Number.isFinite(marketPrice) && marketPrice > 0)
2007
- ? sellAvailable * marketPrice
2008
- : sellAvailable;
2009
- side = buyAvailable >= sellInBuyUnits ? ORDER_TYPES.BUY : ORDER_TYPES.SELL;
2254
+ if (!Number.isFinite(marketPrice) || marketPrice <= 0) {
2255
+ skipReason =
2256
+ `market price unavailable (${String(currentMarketPrice)}) with funds on both sides ` +
2257
+ `(buy=${Format.formatAmount8(buyAvailable)}, sell=${Format.formatAmount8(sellAvailable)})`;
2258
+ }
2259
+ else {
2260
+ const sellInBuyUnits = sellAvailable * marketPrice;
2261
+ side = buyAvailable >= sellInBuyUnits ? ORDER_TYPES.BUY : ORDER_TYPES.SELL;
2262
+ }
2010
2263
  }
2011
2264
  else if (buyViable) {
2012
2265
  side = ORDER_TYPES.BUY;
@@ -2014,6 +2267,10 @@ export function determineOrderSideByFunds(manager, currentMarketPrice) {
2014
2267
  else if (sellViable) {
2015
2268
  side = ORDER_TYPES.SELL;
2016
2269
  }
2270
+ if (skipReason) {
2271
+ manager.logger?.log?.(`Spread correction skipped: ${skipReason}; refusing to compare raw cross-asset units`, 'warn');
2272
+ return { side: null, reason: skipReason };
2273
+ }
2017
2274
  if (!side) {
2018
2275
  const committedBuy = Math.max(0, Number(manager.funds?.committed?.chain?.buy || 0));
2019
2276
  const committedSell = Math.max(0, Number(manager.funds?.committed?.chain?.sell || 0));
@@ -2032,19 +2289,30 @@ export function determineOrderSideByFunds(manager, currentMarketPrice) {
2032
2289
  side = ORDER_TYPES.SELL;
2033
2290
  }
2034
2291
  else {
2035
- // Deterministic fallback when both sides hold inventory but market valuation is unavailable.
2036
- side = ORDER_TYPES.BUY;
2292
+ // Both sides hold committed inventory but market valuation is
2293
+ // unavailable — leave side null (skip) rather than default to
2294
+ // BUY. A deterministic cross-asset guess is exactly the
2295
+ // wrong-side risk above; the next cycle with a real price
2296
+ // decides. Carry the reason so the bottom skip log does not
2297
+ // misreport this branch as "no committed inventory".
2298
+ skipReason =
2299
+ `market price unavailable (${String(currentMarketPrice)}) with committed inventory on both sides ` +
2300
+ `(committed buy=${Format.formatAmount8(committedBuy)}, committed sell=${Format.formatAmount8(committedSell)})`;
2301
+ }
2302
+ if (side) {
2303
+ manager.logger?.log?.(`Spread correction using redistribution fallback on ${side} ` +
2304
+ `(free buy=${Format.formatAmount8(buyAvailable)}, free sell=${Format.formatAmount8(sellAvailable)}, ` +
2305
+ `price=${hasValidPrice ? Format.formatAmount8(marketPrice) : 'unavailable'})`, 'info');
2037
2306
  }
2038
- manager.logger?.log?.(`Spread correction using redistribution fallback on ${side} ` +
2039
- `(free buy=${Format.formatAmount8(buyAvailable)}, free sell=${Format.formatAmount8(sellAvailable)}, ` +
2040
- `price=${hasValidPrice ? Format.formatAmount8(marketPrice) : 'unavailable'})`, 'info');
2041
2307
  }
2042
2308
  }
2043
2309
  if (!side) {
2044
- manager.logger?.log?.(`Spread correction skipped: insufficient free funds and no committed inventory to redistribute ` +
2045
- `(buy=${Format.formatAmount8(buyAvailable)}, sell=${Format.formatAmount8(sellAvailable)})`, 'warn');
2310
+ manager.logger?.log?.(skipReason
2311
+ ? `Spread correction skipped: ${skipReason}`
2312
+ : `Spread correction skipped: insufficient free funds and no committed inventory to redistribute ` +
2313
+ `(buy=${Format.formatAmount8(buyAvailable)}, sell=${Format.formatAmount8(sellAvailable)})`, 'warn');
2046
2314
  }
2047
- return { side, reason: side ? `Choosing ${side}` : 'Insufficient funds or committed inventory' };
2315
+ return { side, reason: side ? `Choosing ${side}` : (skipReason || 'Insufficient funds or committed inventory') };
2048
2316
  }
2049
2317
  /**
2050
2318
  * Resolve the MIN_SPREAD_ORDERS gap reserve used by boundary promotion.
@@ -2184,9 +2452,10 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2184
2452
  .sort((a, b) => railType === ORDER_TYPES.BUY ? a.price - b.price : b.price - a.price)
2185
2453
  .slice(0, missingSlots);
2186
2454
  // Secondary candidates: orphaned virtual slots that have lost their
2187
- // order (e.g. stale-cleaned after a race condition during a crash).
2188
- // These sit inside the active window and are invisible to the
2189
- // gap-band SPREAD filter above.
2455
+ // order (e.g. stale-cleaned after a race condition during a crash, or a
2456
+ // lowest-sell slot dust-cancelled and then left unplaced after a
2457
+ // boundary shift). These sit inside the active window and are invisible
2458
+ // to the gap-band SPREAD filter above.
2190
2459
  //
2191
2460
  // Empty slots are stored SPREAD (side-neutral) after normalization, so
2192
2461
  // the stored type can be railType OR SPREAD — what matters is that the
@@ -2195,11 +2464,22 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2195
2464
  // whose boundary position has moved into the spread or opposite zone are
2196
2465
  // NOT re-activated on the stale side — doing so would compound inventory
2197
2466
  // at prices where the bot already traded.
2467
+ //
2468
+ // NOTE: we deliberately accept BOTH zero-size and sized virtual slots
2469
+ // here. A sized sell/buy slot that lost its orderId (the classic
2470
+ // boundary-oscillation orphan) has size > 0 but no on-chain order — the
2471
+ // old `size === 0` guard excluded it, so spread correction silently
2472
+ // planned 0 orders and the gap never closed. Dropping the guard is safe:
2473
+ // create targets are rebuilt from `current: 0` + the fresh ideal from
2474
+ // allocateFundsByWeights (grid.ts:2790-2800, 2861-2870), so a sized
2475
+ // orphan's stale stored size never leaks into a placement; getMinOrderSize
2476
+ // is only a collision-tolerance fallback (grid.ts:2712), not the primary
2477
+ // sizer. Orphans are excluded from the sideSlots sizing denominator too,
2478
+ // so this also removes the prior dilution of every other order.
2198
2479
  const orphanedVirtualCandidates = allOrders
2199
2480
  .filter((o) => (o.type === railType || o.type === ORDER_TYPES.SPREAD)
2200
2481
  && o.state === ORDER_STATES.VIRTUAL
2201
2482
  && !o.orderId
2202
- && Number(o.size || 0) === 0
2203
2483
  && getSlotCorrectType(o) === railType)
2204
2484
  .sort((a, b) => railType === ORDER_TYPES.BUY ? b.price - a.price : a.price - b.price)
2205
2485
  .slice(0, missingSlots);
@@ -2281,10 +2561,22 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2281
2561
  spreadCandidates = spreadCandidates.filter((c) => {
2282
2562
  if (c.price == null)
2283
2563
  return false;
2284
- // Resolve candidate size: if zero/missing, use minimum so tolerance
2285
- // doesn't collapse to zero (calculatePriceTolerance returns null for size <= 0).
2286
- const cs = (c.size && c.size > 0) ? c.size : getMinOrderSize(railType, manager.assets);
2287
- return !findPriceCollision(allOrders, c.id, c.price, cs, railType, manager.assets, (o) => isOrderPlaced(o) && o.price != null);
2564
+ const precision = railType === ORDER_TYPES.SELL ? manager.assets.assetA.precision : manager.assets.assetB.precision;
2565
+ for (const o of allOrders) {
2566
+ if (!isOrderPlaced(o) || o.price == null)
2567
+ continue;
2568
+ if (o.id === c.id)
2569
+ continue;
2570
+ try {
2571
+ if (priceSlotEqual(o.price, c.price, precision))
2572
+ return false;
2573
+ }
2574
+ catch {
2575
+ if (o.price === c.price)
2576
+ return false;
2577
+ }
2578
+ }
2579
+ return true;
2288
2580
  });
2289
2581
  const filteredCount = preFilter - spreadCandidates.length;
2290
2582
  if (filteredCount > 0) {
@@ -2296,7 +2588,7 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2296
2588
  }
2297
2589
  if (!edgePartial && spreadCandidates.length === 0) {
2298
2590
  manager.logger?.log?.(`[SPREAD-CORRECTION] No suitable partials, orphaned virtual slots, or spread slots found. Skipping.`, 'warn');
2299
- return { ordersToPlace: [], ordersToUpdate: [] };
2591
+ return { ordersToPlace: [], ordersToUpdate: [], origin: 'spread-correction' };
2300
2592
  }
2301
2593
  const orphanedIds = new Set(orphanedVirtualCandidates.map((o) => o.id));
2302
2594
  const sideSlots = allOrders
@@ -2308,7 +2600,7 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2308
2600
  ].sort((a, b) => a.price - b.price);
2309
2601
  const ctx = await _getSizingContext(manager, sideName);
2310
2602
  if (!ctx || ctx.budget <= 0 || syntheticSideSlots.length === 0) {
2311
- return { ordersToPlace: [], ordersToUpdate: [] };
2603
+ return { ordersToPlace: [], ordersToUpdate: [], origin: 'spread-correction' };
2312
2604
  }
2313
2605
  const precisionEpsilon = getPrecisionSlack(ctx.precision, 1);
2314
2606
  const idealSizes = allocateFundsByWeights(ctx.budget, syntheticSideSlots.length, manager.config.weightDistribution[sideName], manager.config.incrementPercent / 100, railType === ORDER_TYPES.BUY, 0, ctx.precision);
@@ -2345,7 +2637,7 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2345
2637
  }
2346
2638
  }
2347
2639
  if (prioritizedTargets.length === 0) {
2348
- return { ordersToPlace: [], ordersToUpdate: [] };
2640
+ return { ordersToPlace: [], ordersToUpdate: [], origin: 'spread-correction' };
2349
2641
  }
2350
2642
  const totalNeeded = prioritizedTargets.reduce((sum, t) => sum + Math.max(0, Number(t.needed || 0)), 0);
2351
2643
  let recoveredBudget = 0;
@@ -2479,6 +2771,6 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2479
2771
  if (combinedUpdates.length > 0 || ordersToPlace.length > 0) {
2480
2772
  manager.logger?.log?.(`[SPREAD-CORRECTION] Prepared updates=${combinedUpdates.length}, creates=${ordersToPlace.length}, remainingBudget=${Format.formatSizeByOrderType(Math.max(0, remainingBudget), railType, manager.assets)}`, 'debug');
2481
2773
  }
2482
- return { ordersToPlace, ordersToUpdate: combinedUpdates, ...(boundaryIdx === undefined ? {} : { boundaryIdx }) };
2774
+ return { ordersToPlace, ordersToUpdate: combinedUpdates, ...(boundaryIdx === undefined ? {} : { boundaryIdx }), origin: 'spread-correction' };
2483
2775
  }
2484
2776
  //# sourceMappingURL=grid.js.map