dexbot 1.4.24 → 1.4.25

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 (131) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +1 -0
  3. package/analysis/README.md +49 -0
  4. package/analysis/ama_fitting/package.json +1 -1
  5. package/analysis/grid_correction_check.ts +896 -0
  6. package/analysis/trend_detection/package.json +1 -1
  7. package/claw/package.json +1 -1
  8. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  9. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  10. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  11. package/dist/analysis/grid_correction_check.d.ts +90 -0
  12. package/dist/analysis/grid_correction_check.d.ts.map +1 -0
  13. package/dist/analysis/grid_correction_check.js +861 -0
  14. package/dist/analysis/grid_correction_check.js.map +1 -0
  15. package/dist/dexbot.d.ts.map +1 -1
  16. package/dist/dexbot.js +62 -6
  17. package/dist/dexbot.js.map +1 -1
  18. package/dist/modules/account_bots.d.ts.map +1 -1
  19. package/dist/modules/account_bots.js +53 -5
  20. package/dist/modules/account_bots.js.map +1 -1
  21. package/dist/modules/account_orders.d.ts +7 -10
  22. package/dist/modules/account_orders.d.ts.map +1 -1
  23. package/dist/modules/account_orders.js +26 -2
  24. package/dist/modules/account_orders.js.map +1 -1
  25. package/dist/modules/chain_orders.d.ts.map +1 -1
  26. package/dist/modules/chain_orders.js +21 -2
  27. package/dist/modules/chain_orders.js.map +1 -1
  28. package/dist/modules/config.d.ts.map +1 -1
  29. package/dist/modules/config.js +27 -0
  30. package/dist/modules/config.js.map +1 -1
  31. package/dist/modules/constants.d.ts +14 -1
  32. package/dist/modules/constants.d.ts.map +1 -1
  33. package/dist/modules/constants.js +57 -6
  34. package/dist/modules/constants.js.map +1 -1
  35. package/dist/modules/dexbot_class.d.ts +24 -0
  36. package/dist/modules/dexbot_class.d.ts.map +1 -1
  37. package/dist/modules/dexbot_class.js +66 -12
  38. package/dist/modules/dexbot_class.js.map +1 -1
  39. package/dist/modules/dexbot_cow_runtime.d.ts +22 -17
  40. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  41. package/dist/modules/dexbot_cow_runtime.js +646 -128
  42. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  43. package/dist/modules/dexbot_fill_runtime.d.ts +11 -1
  44. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  45. package/dist/modules/dexbot_fill_runtime.js +108 -1
  46. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  47. package/dist/modules/dexbot_maintenance_runtime.d.ts +17 -1
  48. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  49. package/dist/modules/dexbot_maintenance_runtime.js +215 -26
  50. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  51. package/dist/modules/dexbot_startup_runtime.d.ts +1 -0
  52. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  53. package/dist/modules/dexbot_startup_runtime.js +58 -7
  54. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  55. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  56. package/dist/modules/dexbot_state_recovery.js +55 -3
  57. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  58. package/dist/modules/graceful_shutdown.d.ts +12 -1
  59. package/dist/modules/graceful_shutdown.d.ts.map +1 -1
  60. package/dist/modules/graceful_shutdown.js +39 -1
  61. package/dist/modules/graceful_shutdown.js.map +1 -1
  62. package/dist/modules/key_store.d.ts.map +1 -1
  63. package/dist/modules/key_store.js +38 -1
  64. package/dist/modules/key_store.js.map +1 -1
  65. package/dist/modules/order/accounting.d.ts.map +1 -1
  66. package/dist/modules/order/accounting.js +20 -3
  67. package/dist/modules/order/accounting.js.map +1 -1
  68. package/dist/modules/order/grid.d.ts +1 -1
  69. package/dist/modules/order/grid.d.ts.map +1 -1
  70. package/dist/modules/order/grid.js +288 -31
  71. package/dist/modules/order/grid.js.map +1 -1
  72. package/dist/modules/order/grid_reconcile.d.ts +4 -2
  73. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  74. package/dist/modules/order/grid_reconcile.js +242 -222
  75. package/dist/modules/order/grid_reconcile.js.map +1 -1
  76. package/dist/modules/order/grid_reconcile_internal.d.ts +11 -1
  77. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  78. package/dist/modules/order/grid_reconcile_internal.js +132 -6
  79. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  80. package/dist/modules/order/manager.d.ts +68 -14
  81. package/dist/modules/order/manager.d.ts.map +1 -1
  82. package/dist/modules/order/manager.js +241 -52
  83. package/dist/modules/order/manager.js.map +1 -1
  84. package/dist/modules/order/strategy.d.ts.map +1 -1
  85. package/dist/modules/order/strategy.js +25 -8
  86. package/dist/modules/order/strategy.js.map +1 -1
  87. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  88. package/dist/modules/order/sync_engine.js +261 -166
  89. package/dist/modules/order/sync_engine.js.map +1 -1
  90. package/dist/modules/order/utils/math.d.ts +47 -5
  91. package/dist/modules/order/utils/math.d.ts.map +1 -1
  92. package/dist/modules/order/utils/math.js +175 -11
  93. package/dist/modules/order/utils/math.js.map +1 -1
  94. package/dist/modules/order/utils/order.d.ts +16 -15
  95. package/dist/modules/order/utils/order.d.ts.map +1 -1
  96. package/dist/modules/order/utils/order.js +199 -32
  97. package/dist/modules/order/utils/order.js.map +1 -1
  98. package/dist/modules/order/utils/slot.d.ts +8 -0
  99. package/dist/modules/order/utils/slot.d.ts.map +1 -0
  100. package/dist/modules/order/utils/slot.js +16 -0
  101. package/dist/modules/order/utils/slot.js.map +1 -0
  102. package/dist/modules/order/utils/system.d.ts.map +1 -1
  103. package/dist/modules/order/utils/system.js +3 -2
  104. package/dist/modules/order/utils/system.js.map +1 -1
  105. package/dist/modules/order/utils/validate.d.ts +5 -8
  106. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  107. package/dist/modules/order/utils/validate.js +64 -82
  108. package/dist/modules/order/utils/validate.js.map +1 -1
  109. package/dist/modules/order/working_grid.d.ts +5 -0
  110. package/dist/modules/order/working_grid.d.ts.map +1 -1
  111. package/dist/modules/order/working_grid.js +21 -0
  112. package/dist/modules/order/working_grid.js.map +1 -1
  113. package/dist/modules/paths.d.ts +4 -2
  114. package/dist/modules/paths.d.ts.map +1 -1
  115. package/dist/modules/paths.js +79 -24
  116. package/dist/modules/paths.js.map +1 -1
  117. package/dist/scripts/analyze-orders.d.ts.map +1 -1
  118. package/dist/scripts/analyze-orders.js +23 -2
  119. package/dist/scripts/analyze-orders.js.map +1 -1
  120. package/dist/scripts/generate_market_adapter_whitelist.d.ts +2 -5
  121. package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +1 -1
  122. package/dist/scripts/generate_market_adapter_whitelist.js +83 -1
  123. package/dist/scripts/generate_market_adapter_whitelist.js.map +1 -1
  124. package/docs/DEXBOT_COMPARISON.md +11 -11
  125. package/docs/EVOLUTION.md +9 -8
  126. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  127. package/docs/GRID_RECALCULATION.md +6 -0
  128. package/docs/README.md +2 -2
  129. package/market_adapter/README.md +10 -0
  130. package/package.json +2 -1
  131. package/scripts/README.md +4 -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');
@@ -1838,13 +2059,25 @@ export async function checkWindowDust(manager) {
1838
2059
  const topSellOrder = allOrders
1839
2060
  .filter((o) => o.type === ORDER_TYPES.SELL && isLiveOrder(o))
1840
2061
  .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;
2062
+ // Check if an order has a duplicate slot — an active sibling with same price via integer round-trip.
2063
+ const hasDuplicatePriceLevel = (order, assets) => {
2064
+ const precision = order.type === ORDER_TYPES.SELL ? assets.assetA.precision : assets.assetB.precision;
2065
+ for (const o of allOrders) {
2066
+ if (o.id === order.id)
2067
+ continue;
2068
+ if (o.type !== order.type || o.state !== ORDER_STATES.ACTIVE || !o.orderId || o.price == null)
2069
+ continue;
2070
+ try {
2071
+ if (priceSlotEqual(o.price, order.price, precision))
2072
+ return true;
2073
+ }
2074
+ catch {
2075
+ if (o.price === order.price)
2076
+ return true;
2077
+ }
2078
+ }
2079
+ return false;
2080
+ };
1848
2081
  const assets = manager.assets;
1849
2082
  const allPartials = allOrders.filter((o) => isLiveOrder(o) && o.state === ORDER_STATES.PARTIAL);
1850
2083
  const isTopBuy = (o) => topBuyOrder && o.id === topBuyOrder.id;
@@ -2184,9 +2417,10 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2184
2417
  .sort((a, b) => railType === ORDER_TYPES.BUY ? a.price - b.price : b.price - a.price)
2185
2418
  .slice(0, missingSlots);
2186
2419
  // 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.
2420
+ // order (e.g. stale-cleaned after a race condition during a crash, or a
2421
+ // lowest-sell slot dust-cancelled and then left unplaced after a
2422
+ // boundary shift). These sit inside the active window and are invisible
2423
+ // to the gap-band SPREAD filter above.
2190
2424
  //
2191
2425
  // Empty slots are stored SPREAD (side-neutral) after normalization, so
2192
2426
  // the stored type can be railType OR SPREAD — what matters is that the
@@ -2195,11 +2429,22 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2195
2429
  // whose boundary position has moved into the spread or opposite zone are
2196
2430
  // NOT re-activated on the stale side — doing so would compound inventory
2197
2431
  // at prices where the bot already traded.
2432
+ //
2433
+ // NOTE: we deliberately accept BOTH zero-size and sized virtual slots
2434
+ // here. A sized sell/buy slot that lost its orderId (the classic
2435
+ // boundary-oscillation orphan) has size > 0 but no on-chain order — the
2436
+ // old `size === 0` guard excluded it, so spread correction silently
2437
+ // planned 0 orders and the gap never closed. Dropping the guard is safe:
2438
+ // create targets are rebuilt from `current: 0` + the fresh ideal from
2439
+ // allocateFundsByWeights (grid.ts:2790-2800, 2861-2870), so a sized
2440
+ // orphan's stale stored size never leaks into a placement; getMinOrderSize
2441
+ // is only a collision-tolerance fallback (grid.ts:2712), not the primary
2442
+ // sizer. Orphans are excluded from the sideSlots sizing denominator too,
2443
+ // so this also removes the prior dilution of every other order.
2198
2444
  const orphanedVirtualCandidates = allOrders
2199
2445
  .filter((o) => (o.type === railType || o.type === ORDER_TYPES.SPREAD)
2200
2446
  && o.state === ORDER_STATES.VIRTUAL
2201
2447
  && !o.orderId
2202
- && Number(o.size || 0) === 0
2203
2448
  && getSlotCorrectType(o) === railType)
2204
2449
  .sort((a, b) => railType === ORDER_TYPES.BUY ? b.price - a.price : a.price - b.price)
2205
2450
  .slice(0, missingSlots);
@@ -2281,10 +2526,22 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2281
2526
  spreadCandidates = spreadCandidates.filter((c) => {
2282
2527
  if (c.price == null)
2283
2528
  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);
2529
+ const precision = railType === ORDER_TYPES.SELL ? manager.assets.assetA.precision : manager.assets.assetB.precision;
2530
+ for (const o of allOrders) {
2531
+ if (!isOrderPlaced(o) || o.price == null)
2532
+ continue;
2533
+ if (o.id === c.id)
2534
+ continue;
2535
+ try {
2536
+ if (priceSlotEqual(o.price, c.price, precision))
2537
+ return false;
2538
+ }
2539
+ catch {
2540
+ if (o.price === c.price)
2541
+ return false;
2542
+ }
2543
+ }
2544
+ return true;
2288
2545
  });
2289
2546
  const filteredCount = preFilter - spreadCandidates.length;
2290
2547
  if (filteredCount > 0) {
@@ -2296,7 +2553,7 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2296
2553
  }
2297
2554
  if (!edgePartial && spreadCandidates.length === 0) {
2298
2555
  manager.logger?.log?.(`[SPREAD-CORRECTION] No suitable partials, orphaned virtual slots, or spread slots found. Skipping.`, 'warn');
2299
- return { ordersToPlace: [], ordersToUpdate: [] };
2556
+ return { ordersToPlace: [], ordersToUpdate: [], origin: 'spread-correction' };
2300
2557
  }
2301
2558
  const orphanedIds = new Set(orphanedVirtualCandidates.map((o) => o.id));
2302
2559
  const sideSlots = allOrders
@@ -2308,7 +2565,7 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2308
2565
  ].sort((a, b) => a.price - b.price);
2309
2566
  const ctx = await _getSizingContext(manager, sideName);
2310
2567
  if (!ctx || ctx.budget <= 0 || syntheticSideSlots.length === 0) {
2311
- return { ordersToPlace: [], ordersToUpdate: [] };
2568
+ return { ordersToPlace: [], ordersToUpdate: [], origin: 'spread-correction' };
2312
2569
  }
2313
2570
  const precisionEpsilon = getPrecisionSlack(ctx.precision, 1);
2314
2571
  const idealSizes = allocateFundsByWeights(ctx.budget, syntheticSideSlots.length, manager.config.weightDistribution[sideName], manager.config.incrementPercent / 100, railType === ORDER_TYPES.BUY, 0, ctx.precision);
@@ -2345,7 +2602,7 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2345
2602
  }
2346
2603
  }
2347
2604
  if (prioritizedTargets.length === 0) {
2348
- return { ordersToPlace: [], ordersToUpdate: [] };
2605
+ return { ordersToPlace: [], ordersToUpdate: [], origin: 'spread-correction' };
2349
2606
  }
2350
2607
  const totalNeeded = prioritizedTargets.reduce((sum, t) => sum + Math.max(0, Number(t.needed || 0)), 0);
2351
2608
  let recoveredBudget = 0;
@@ -2479,6 +2736,6 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2479
2736
  if (combinedUpdates.length > 0 || ordersToPlace.length > 0) {
2480
2737
  manager.logger?.log?.(`[SPREAD-CORRECTION] Prepared updates=${combinedUpdates.length}, creates=${ordersToPlace.length}, remainingBudget=${Format.formatSizeByOrderType(Math.max(0, remainingBudget), railType, manager.assets)}`, 'debug');
2481
2738
  }
2482
- return { ordersToPlace, ordersToUpdate: combinedUpdates, ...(boundaryIdx === undefined ? {} : { boundaryIdx }) };
2739
+ return { ordersToPlace, ordersToUpdate: combinedUpdates, ...(boundaryIdx === undefined ? {} : { boundaryIdx }), origin: 'spread-correction' };
2483
2740
  }
2484
2741
  //# sourceMappingURL=grid.js.map