dexbot 1.4.8 → 1.4.10

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 (107) hide show
  1. package/README.md +4 -7
  2. package/dist/dexbot.d.ts +3 -0
  3. package/dist/dexbot.d.ts.map +1 -1
  4. package/dist/dexbot.js +21 -2
  5. package/dist/dexbot.js.map +1 -1
  6. package/dist/modules/broadcast_failure.d.ts +0 -1
  7. package/dist/modules/broadcast_failure.d.ts.map +1 -1
  8. package/dist/modules/broadcast_failure.js +0 -1
  9. package/dist/modules/broadcast_failure.js.map +1 -1
  10. package/dist/modules/chain_keys.d.ts.map +1 -1
  11. package/dist/modules/chain_keys.js +19 -1
  12. package/dist/modules/chain_keys.js.map +1 -1
  13. package/dist/modules/credential_policy.d.ts.map +1 -1
  14. package/dist/modules/credential_policy.js +135 -364
  15. package/dist/modules/credential_policy.js.map +1 -1
  16. package/dist/modules/credit_runtime.d.ts.map +1 -1
  17. package/dist/modules/credit_runtime.js +26 -0
  18. package/dist/modules/credit_runtime.js.map +1 -1
  19. package/dist/modules/daemon_node_health.d.ts +0 -1
  20. package/dist/modules/daemon_node_health.d.ts.map +1 -1
  21. package/dist/modules/daemon_node_health.js +0 -1
  22. package/dist/modules/daemon_node_health.js.map +1 -1
  23. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  24. package/dist/modules/dexbot_cow_runtime.js +7 -1
  25. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  26. package/dist/modules/dexbot_fill_runtime.d.ts +14 -5
  27. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  28. package/dist/modules/dexbot_fill_runtime.js +27 -27
  29. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  30. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  31. package/dist/modules/dexbot_maintenance_runtime.js +32 -14
  32. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  33. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  34. package/dist/modules/dexbot_startup_runtime.js +4 -11
  35. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  36. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  37. package/dist/modules/dexbot_state_recovery.js +7 -7
  38. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  39. package/dist/modules/key_store.d.ts.map +1 -1
  40. package/dist/modules/key_store.js +42 -60
  41. package/dist/modules/key_store.js.map +1 -1
  42. package/dist/modules/launcher/bot_supervisor.d.ts +2 -2
  43. package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
  44. package/dist/modules/launcher/bot_supervisor.js +6 -26
  45. package/dist/modules/launcher/bot_supervisor.js.map +1 -1
  46. package/dist/modules/launcher/foreign_cred_daemon.d.ts +17 -0
  47. package/dist/modules/launcher/foreign_cred_daemon.d.ts.map +1 -1
  48. package/dist/modules/launcher/foreign_cred_daemon.js +5 -14
  49. package/dist/modules/launcher/foreign_cred_daemon.js.map +1 -1
  50. package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
  51. package/dist/modules/launcher/monolithic_runtime.js +4 -16
  52. package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
  53. package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
  54. package/dist/modules/launcher/status_reporting.js +1 -11
  55. package/dist/modules/launcher/status_reporting.js.map +1 -1
  56. package/dist/modules/node_failure_ledger.d.ts +0 -1
  57. package/dist/modules/node_failure_ledger.d.ts.map +1 -1
  58. package/dist/modules/node_failure_ledger.js +0 -1
  59. package/dist/modules/node_failure_ledger.js.map +1 -1
  60. package/dist/modules/order/accounting.d.ts.map +1 -1
  61. package/dist/modules/order/accounting.js +5 -1
  62. package/dist/modules/order/accounting.js.map +1 -1
  63. package/dist/modules/order/grid.d.ts.map +1 -1
  64. package/dist/modules/order/grid.js +187 -22
  65. package/dist/modules/order/grid.js.map +1 -1
  66. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  67. package/dist/modules/order/grid_reconcile_internal.js +147 -35
  68. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  69. package/dist/modules/order/logger.js +1 -1
  70. package/dist/modules/order/logger.js.map +1 -1
  71. package/dist/modules/order/manager.d.ts +1 -1
  72. package/dist/modules/order/manager.d.ts.map +1 -1
  73. package/dist/modules/order/manager.js +3 -20
  74. package/dist/modules/order/manager.js.map +1 -1
  75. package/dist/modules/order/strategy.js +2 -2
  76. package/dist/modules/order/strategy.js.map +1 -1
  77. package/dist/modules/order/sync_engine.js +4 -4
  78. package/dist/modules/order/sync_engine.js.map +1 -1
  79. package/dist/modules/order/utils/math.d.ts +38 -1
  80. package/dist/modules/order/utils/math.d.ts.map +1 -1
  81. package/dist/modules/order/utils/math.js +60 -0
  82. package/dist/modules/order/utils/math.js.map +1 -1
  83. package/dist/modules/order/utils/order.d.ts +45 -1
  84. package/dist/modules/order/utils/order.d.ts.map +1 -1
  85. package/dist/modules/order/utils/order.js +84 -0
  86. package/dist/modules/order/utils/order.js.map +1 -1
  87. package/dist/modules/order/utils/system.d.ts.map +1 -1
  88. package/dist/modules/order/utils/system.js +5 -2
  89. package/dist/modules/order/utils/system.js.map +1 -1
  90. package/dist/modules/process_discovery.d.ts +1 -1
  91. package/dist/modules/process_discovery.d.ts.map +1 -1
  92. package/dist/modules/process_discovery.js +15 -14
  93. package/dist/modules/process_discovery.js.map +1 -1
  94. package/dist/modules/socket_json_client.d.ts +0 -1
  95. package/dist/modules/socket_json_client.d.ts.map +1 -1
  96. package/dist/modules/socket_json_client.js +0 -1
  97. package/dist/modules/socket_json_client.js.map +1 -1
  98. package/dist/modules/types.d.ts +0 -600
  99. package/dist/modules/types.d.ts.map +1 -1
  100. package/dist/modules/types.js +15 -0
  101. package/dist/modules/types.js.map +1 -1
  102. package/dist/scripts/run-tests.js +185 -18
  103. package/dist/scripts/run-tests.js.map +1 -1
  104. package/dist/unlock.d.ts +4 -4
  105. package/dist/unlock.js +8 -8
  106. package/dist/unlock.js.map +1 -1
  107. package/package.json +1 -1
@@ -385,9 +385,13 @@ function createOrderGrid(config) {
385
385
  if (!Number.isFinite(incrementPercent)) {
386
386
  throw new Error(`Invalid incrementPercent: ${incrementPercent}. Must be a finite number.`);
387
387
  }
388
- const minPercent = config.incrementBounds?.MIN_PERCENT;
389
- const maxPercent = config.incrementBounds?.MAX_PERCENT;
390
- if (incrementPercent < minPercent || incrementPercent > maxPercent) {
388
+ // Fall back to the canonical INCREMENT_BOUNDS when the config omits
389
+ // incrementBounds. Without this, a non-positive incrementPercent (e.g. 0)
390
+ // silently passes validation and the geometric loop below spins forever.
391
+ const incrementBounds = config.incrementBounds || constants_1.INCREMENT_BOUNDS;
392
+ const minPercent = incrementBounds.MIN_PERCENT;
393
+ const maxPercent = incrementBounds.MAX_PERCENT;
394
+ if (incrementPercent <= 0 || incrementPercent < minPercent || incrementPercent > maxPercent) {
391
395
  throw new Error(`Invalid incrementPercent: ${incrementPercent}. Must be between ` +
392
396
  `${minPercent} and ${maxPercent} (inclusive).`);
393
397
  }
@@ -544,6 +548,25 @@ async function loadGrid(manager, grid, boundaryIdx = null) {
544
548
  : (i >= sellStartIdx)
545
549
  ? constants_1.ORDER_TYPES.SELL
546
550
  : constants_1.ORDER_TYPES.SPREAD;
551
+ // DEFENSIVE BACKSTOP: a VIRTUAL slot with no orderId and
552
+ // zero size is side-neutral — it is a reusable placeholder
553
+ // that may be activated on either rail. Storing a stale
554
+ // BUY/SELL type here misleads candidate-selection code
555
+ // (e.g. spread-correction orphaned filters and reconcile
556
+ // activation), which pick by stored type instead of boundary
557
+ // geometry. Force SPREAD so the stored type can never
558
+ // pre-bias which side reuses the slot. The VIRTUAL +
559
+ // !orderId + size-0 combination already implies not on-chain
560
+ // (on-chain requires ACTIVE/PARTIAL and an orderId). This is
561
+ // a defensive backstop for legacy persisted grids; the
562
+ // boundary-shift and strategy re-plan paths use
563
+ // assignGridRoles (order.ts) with assignOnChain, where
564
+ // geometry-based typing wins.
565
+ if ((0, order_1.isEmptyGridSlot)(slot, slot, { allowNullType: true })) {
566
+ if (slot.type !== constants_1.ORDER_TYPES.SPREAD)
567
+ reassignCount++;
568
+ return { ...slot, type: constants_1.ORDER_TYPES.SPREAD };
569
+ }
547
570
  if (slot.type !== correctType) {
548
571
  let newType = correctType;
549
572
  // On-chain slots must never be reassigned to SPREAD:
@@ -606,8 +629,20 @@ async function loadGrid(manager, grid, boundaryIdx = null) {
606
629
  }
607
630
  await manager._applyOrderUpdate(currentOrder, 'grid-load', { skipAccounting: true });
608
631
  }
609
- const spreadCount = grid.filter((o) => o.type === constants_1.ORDER_TYPES.SPREAD).length;
610
- manager.targetSpreadCount = spreadCount;
632
+ // Gap-band occupancy for the spread metric. Empty slots are
633
+ // normalized to SPREAD (side-neutral) above, so a raw
634
+ // `type === SPREAD` count would include every empty slot on both
635
+ // rails. countGapBandSpread requires both SPREAD type and band
636
+ // geometry (target = gapSlots).
637
+ // Index resolution: prefer the parsed slot id (price-monotonic
638
+ // at creation) so the count is order-independent, matching the
639
+ // accountant (accounting.ts). Fall back to array position only
640
+ // for ids that are not grid slot ids.
641
+ const spreadCount = (0, math_1.countGapBandSpread)(manager, grid, (o, i) => {
642
+ const idx = (0, order_1.parseSlotIndex)(o?.id);
643
+ return idx === null ? i : idx;
644
+ });
645
+ manager.initialSpreadCount = spreadCount;
611
646
  manager.currentSpreadCount = spreadCount;
612
647
  }
613
648
  finally {
@@ -882,9 +917,14 @@ async function initializeGrid(manager) {
882
917
  await manager.resumeFundRecalc();
883
918
  manager.resumeRecalcLogging();
884
919
  }
885
- // RC-6: Spread count updates protected by grid lock
886
- manager.targetSpreadCount = initialSpreadCount.buy + initialSpreadCount.sell;
887
- manager.currentSpreadCount = manager.targetSpreadCount;
920
+ // RC-6: Spread count updates protected by grid lock.
921
+ // initializeGrid always sets initialSpreadCount = gapSlots (theoretical
922
+ // value for a fresh grid). loadGrid uses the actual SPREAD-typed slot
923
+ // count in the gap band, which may be lower after promotions absorbed
924
+ // gap slots into a rail. Both are correct for their context; callers
925
+ // should not assume the two paths agree.
926
+ manager.initialSpreadCount = initialSpreadCount.buy + initialSpreadCount.sell;
927
+ manager.currentSpreadCount = manager.initialSpreadCount;
888
928
  });
889
929
  // FIX: Use consistent optional chaining pattern for all logger calls
890
930
  manager.logger?.log?.(`Initialized grid with ${orders.length} orders.`, 'info');
@@ -1113,6 +1153,19 @@ async function _recalculateGridOrderSizesFromBlockchain(manager, orderType, opti
1113
1153
  // correct side's size calculation — using manager.orders here would
1114
1154
  // filter by stale pre-shift types and miss the crossers, producing a
1115
1155
  // budget allocation that doesn't match the post-shift grid structure.
1156
+ //
1157
+ // Empty (size-0 VIRTUAL) slots are stored SPREAD (side-neutral) after
1158
+ // normalization and are deliberately NOT included in the side's
1159
+ // ideal-size denominator here: a SPREAD slot cannot carry a size
1160
+ // (validateOrder forces SPREAD size back to 0), so including it would
1161
+ // only dilute the budget without giving the slot a usable size.
1162
+ // Activation sizing for empties is handled where re-typing happens:
1163
+ // spread-correction (prepareSpreadCorrectionOrders) and startup
1164
+ // reconcile (_deriveBudgetedSideSizes) both compute the full in-rail
1165
+ // geometric progression including empties, then re-type the picked slot
1166
+ // to BUY/SELL before placement. The COW boundary-shift path re-types
1167
+ // the working grid by geometry first, so crossers stay in the correct
1168
+ // side's denominator.
1116
1169
  const orderSource = collectActions ? workingGrid : manager.orders;
1117
1170
  const allSideSlots = Array.from(orderSource.values())
1118
1171
  .filter((o) => o.type === orderType)
@@ -1903,6 +1956,36 @@ function determineOrderSideByFunds(manager, currentMarketPrice) {
1903
1956
  }
1904
1957
  return { side, reason: side ? `Choosing ${side}` : 'Insufficient funds or committed inventory' };
1905
1958
  }
1959
+ // Collect contiguous empty gap-band slots adjacent to a rail edge for boundary
1960
+ // promotion. BUY walks upward from the boundary into the gap, SELL walks
1961
+ // downward from the sell start; both stop at the first unavailable slot.
1962
+ // Mirrors are collapsed into one directional walk (step +/- 1).
1963
+ //
1964
+ // NOTE: the promotion cap is the OPPOSITE RAIL's span, NOT the gap-band size.
1965
+ // SELL promotion moves the boundary down by the promoted count (boundary =
1966
+ // buyEndIdx - count), so capping at buyEndIdx keeps it >= 0. BUY promotion
1967
+ // moves the boundary up; capping at the SELL rail size (maxIdx+1-sellStartIdx)
1968
+ // keeps the promoted boundary at or below the last placed slot. The walk's
1969
+ // own idx < sellStartIdx / idx > buyEndIdx bounds already limit promotion to
1970
+ // the band, so this cap only binds when the opposite rail is smaller than the
1971
+ // band — i.e. an empty opposite rail (cap 0) blocks promotion entirely.
1972
+ function _collectPromotableBoundarySlots(allSlotsByPrice, railType, buyEndIdx, sellStartIdx, quota) {
1973
+ const isBuy = railType === constants_1.ORDER_TYPES.BUY;
1974
+ const maxIdx = allSlotsByPrice.length - 1;
1975
+ const maxPromotable = isBuy
1976
+ ? Math.max(0, maxIdx + 1 - sellStartIdx)
1977
+ : Math.max(0, buyEndIdx);
1978
+ const promotionQuota = Math.min(quota, maxPromotable);
1979
+ const promoted = [];
1980
+ const step = isBuy ? 1 : -1;
1981
+ for (let idx = isBuy ? buyEndIdx + 1 : sellStartIdx - 1; promoted.length < promotionQuota && (isBuy ? idx < sellStartIdx : idx > buyEndIdx); idx += step) {
1982
+ const slot = allSlotsByPrice[idx];
1983
+ if (!slot || !(0, order_1.isSlotAvailable)(slot))
1984
+ break;
1985
+ promoted.push(slot);
1986
+ }
1987
+ return promoted;
1988
+ }
1906
1989
  /**
1907
1990
  * Prepares one or more orders to correct a wide spread.
1908
1991
  * @param {import('./types').OrderManager} manager - The OrderManager instance.
@@ -1941,15 +2024,22 @@ async function prepareSpreadCorrectionOrders(manager, preferredSide, outOfSpread
1941
2024
  .filter((o) => o.price != null && Number.isFinite(o.price))
1942
2025
  .sort((a, b) => a.price - b.price);
1943
2026
  const slotIndexMap = new Map(allSlotsByPrice.map((o, i) => [o.id, i]));
1944
- const gapSlots = manager._gapSlots ?? calculateGapSlots(manager.config?.incrementPercent, manager.config?.targetSpreadPercent, manager.config?.gridLimits);
1945
2027
  // Use the committed boundary for slot classification — never a speculative
1946
2028
  // value from syncBoundaryToFunds that hasn't been persisted through the COW
1947
2029
  // pipeline. If the boundary shifts later via _commitWorkingGrid, the next
1948
2030
  // spread correction cycle will re-classify with the updated committed value.
1949
2031
  // This prevents TOCTOU-style inconsistency where slot types are chosen
1950
2032
  // against a boundary that was never atomically committed to manager.orders.
1951
- const buyEndIdx = manager.boundaryIdx ?? 0;
1952
- const sellStartIdx = (0, math_1.getSellStartIdx)(manager.boundaryIdx, gapSlots);
2033
+ const resolved = (0, math_1.resolveGapBand)(manager);
2034
+ const gapSlots = resolved.gapSlots;
2035
+ const boundaryKnown = resolved.boundaryIdx !== null && resolved.sellStartIdx !== null;
2036
+ // NOTE: `?? 0` keeps the legacy classification fallback (boundary not
2037
+ // restored yet → treat as bottom of the rail) so the correction can still
2038
+ // act on concrete BUY/SELL candidates. Boundary PROMOTION is gated on
2039
+ // `boundaryKnown` below: deriving a new boundary from a fabricated 0 would
2040
+ // silently commit a boundary that was never real.
2041
+ const buyEndIdx = resolved.boundaryIdx ?? 0;
2042
+ const sellStartIdx = resolved.sellStartIdx ?? (0, math_1.getSellStartIdx)(buyEndIdx, gapSlots);
1953
2043
  const getSlotCorrectType = (slot) => {
1954
2044
  const idx = slotIndexMap.get(slot.id);
1955
2045
  if (idx === undefined)
@@ -1979,30 +2069,68 @@ async function prepareSpreadCorrectionOrders(manager, preferredSide, outOfSpread
1979
2069
  && getSlotCorrectType(o) === railType)
1980
2070
  .sort((a, b) => railType === constants_1.ORDER_TYPES.BUY ? a.price - b.price : b.price - a.price)
1981
2071
  .slice(0, missingSlots);
1982
- // Secondary candidates: orphaned virtual slots of the correct side-type that have
1983
- // lost their order (e.g. stale-cleaned after a race condition during a crash).
1984
- // These sit inside the active window and are invisible to the SPREAD-type filter above.
2072
+ // Secondary candidates: orphaned virtual slots that have lost their
2073
+ // order (e.g. stale-cleaned after a race condition during a crash).
2074
+ // These sit inside the active window and are invisible to the
2075
+ // gap-band SPREAD filter above.
1985
2076
  //
1986
- // IMPORTANT: Filter by boundary-correct type so that filled-then-virtualized slots
1987
- // whose boundary position has moved into the spread or opposite zone are NOT
1988
- // re-activated on the stale side — doing so would compound inventory at prices
1989
- // where the bot already traded. The boundary-correct type is computed from the
1990
- // current boundary index and the slot's price position in the sorted rail.
2077
+ // Empty slots are stored SPREAD (side-neutral) after normalization, so
2078
+ // the stored type can be railType OR SPREAD — what matters is that the
2079
+ // boundary-correct type (geometry) matches the rail being corrected.
2080
+ // Filter by boundary-correct type so that filled-then-virtualized slots
2081
+ // whose boundary position has moved into the spread or opposite zone are
2082
+ // NOT re-activated on the stale side — doing so would compound inventory
2083
+ // at prices where the bot already traded.
1991
2084
  const orphanedVirtualCandidates = allOrders
1992
- .filter((o) => o.type === railType
2085
+ .filter((o) => (o.type === railType || o.type === constants_1.ORDER_TYPES.SPREAD)
1993
2086
  && o.state === constants_1.ORDER_STATES.VIRTUAL
1994
2087
  && !o.orderId
1995
2088
  && Number(o.size || 0) === 0
1996
2089
  && getSlotCorrectType(o) === railType)
1997
2090
  .sort((a, b) => railType === constants_1.ORDER_TYPES.BUY ? b.price - a.price : a.price - b.price)
1998
2091
  .slice(0, missingSlots);
2092
+ // If the funded rail is full, the spread itself may be stale: the
2093
+ // nearest empty slots are still in the gap band. Promote contiguous
2094
+ // empty gap slots from that rail edge so correction can repair the
2095
+ // boundary and place orders in one atomic COW commit.
2096
+ //
2097
+ // Requires a KNOWN committed boundary: with a null/unknown boundary the
2098
+ // rail edges are fabricated (buyEndIdx=0), so promotion would place
2099
+ // orders at arbitrary prices and return a boundary derived from that
2100
+ // fiction. Without promotion there is nothing to commit, so falling
2101
+ // back to the pre-existing candidate paths is safe.
2102
+ const promotedCandidates = [];
2103
+ if (boundaryKnown && orphanedVirtualCandidates.length + typedSpreadCandidates.length < missingSlots) {
2104
+ const rawQuota = missingSlots - orphanedVirtualCandidates.length - typedSpreadCandidates.length;
2105
+ promotedCandidates.push(..._collectPromotableBoundarySlots(allSlotsByPrice, railType, buyEndIdx, sellStartIdx, rawQuota));
2106
+ if (promotedCandidates.length > 0) {
2107
+ manager.logger?.log?.(`[SPREAD-CORRECTION] ${promotedCandidates.length} gap slot(s) available for boundary promotion on ${sideName}`, 'info');
2108
+ }
2109
+ }
1999
2110
  // Merge: prefer orphaned virtuals (they already occupy correct grid positions) then
2000
2111
  // fall back to SPREAD slots for any remaining quota.
2001
2112
  const remainingQuota = Math.max(0, missingSlots - orphanedVirtualCandidates.length);
2002
2113
  let spreadCandidates = [
2003
2114
  ...orphanedVirtualCandidates,
2004
- ...typedSpreadCandidates.slice(0, remainingQuota)
2115
+ ...typedSpreadCandidates.slice(0, remainingQuota),
2116
+ ...promotedCandidates
2005
2117
  ];
2118
+ // Dedupe by slot id: an empty in-rail slot typed SPREAD (normalized) now
2119
+ // qualifies for BOTH orphanedVirtualCandidates and typedSpreadCandidates
2120
+ // (both accept SPREAD type + rail geometry), so a single slot can be
2121
+ // planned twice. Duplicate CREATEs would inflate the sizing denominator
2122
+ // (diluting every order), mislead the plan counts, and get dropped by the
2123
+ // COW same-batch collision filter anyway. Keep the first occurrence
2124
+ // (orphaned-priority — those already occupy correct grid positions).
2125
+ {
2126
+ const seenSlotIds = new Set();
2127
+ spreadCandidates = spreadCandidates.filter((c) => {
2128
+ if (!c?.id || seenSlotIds.has(c.id))
2129
+ return false;
2130
+ seenSlotIds.add(c.id);
2131
+ return true;
2132
+ });
2133
+ }
2006
2134
  // P2: Filter out candidates whose price already has a placed order from any slot.
2007
2135
  // This prevents creating a duplicate order at the same price when a prior cycle's
2008
2136
  // order was not properly cleaned up (e.g. uncertain broadcast).
@@ -2135,6 +2263,43 @@ async function prepareSpreadCorrectionOrders(manager, preferredSide, outOfSpread
2135
2263
  });
2136
2264
  remainingBudget -= createSize;
2137
2265
  }
2266
+ let boundaryIdx;
2267
+ const placedPromotedIds = new Set(ordersToPlace
2268
+ .filter((order) => promotedCandidates.some((slot) => slot.id === order.id))
2269
+ .map((order) => order.id));
2270
+ if (placedPromotedIds.size > 0) {
2271
+ // Derive boundary from the placed promoted set's extremes (via
2272
+ // slotIndexMap), not the contiguous prefix. Any promoted-but-
2273
+ // unplaced slots sit in-rail or in-band as normal empty slots.
2274
+ let maxDist = 0;
2275
+ for (const id of placedPromotedIds) {
2276
+ const idx = slotIndexMap.get(id);
2277
+ if (idx === undefined)
2278
+ continue;
2279
+ const dist = railType === constants_1.ORDER_TYPES.BUY
2280
+ ? idx - buyEndIdx
2281
+ : sellStartIdx - idx;
2282
+ if (dist > maxDist)
2283
+ maxDist = dist;
2284
+ }
2285
+ if (maxDist > 0) {
2286
+ boundaryIdx = railType === constants_1.ORDER_TYPES.BUY
2287
+ ? buyEndIdx + maxDist
2288
+ : buyEndIdx - maxDist;
2289
+ const maxIdx = allSlotsByPrice.length - 1;
2290
+ const clamped = Math.max(0, Math.min(maxIdx, boundaryIdx));
2291
+ if (clamped !== boundaryIdx) {
2292
+ manager.logger?.log?.(`[SPREAD-CORRECTION] Boundary promotion clamped on ${sideName}: ` +
2293
+ `${boundaryIdx} -> ${clamped}`, 'warn');
2294
+ boundaryIdx = clamped;
2295
+ }
2296
+ manager.logger?.log?.(`[SPREAD-CORRECTION] Boundary promotion on ${sideName}: ` +
2297
+ `${buyEndIdx} -> ${boundaryIdx} (${placedPromotedIds.size} placed, maxDist ${maxDist})`, 'info');
2298
+ }
2299
+ else {
2300
+ boundaryIdx = undefined;
2301
+ }
2302
+ }
2138
2303
  const combinedUpdates = [...redistributionUpdates];
2139
2304
  for (const plannedUpdate of ordersToUpdate) {
2140
2305
  const id = plannedUpdate?.partialOrder?.id || plannedUpdate?.id;
@@ -2157,6 +2322,6 @@ async function prepareSpreadCorrectionOrders(manager, preferredSide, outOfSpread
2157
2322
  if (combinedUpdates.length > 0 || ordersToPlace.length > 0) {
2158
2323
  manager.logger?.log?.(`[SPREAD-CORRECTION] Prepared updates=${combinedUpdates.length}, creates=${ordersToPlace.length}, remainingBudget=${Format.formatSizeByOrderType(Math.max(0, remainingBudget), railType, manager.assets)}`, 'debug');
2159
2324
  }
2160
- return { ordersToPlace, ordersToUpdate: combinedUpdates };
2325
+ return { ordersToPlace, ordersToUpdate: combinedUpdates, ...(boundaryIdx === undefined ? {} : { boundaryIdx }) };
2161
2326
  }
2162
2327
  //# sourceMappingURL=grid.js.map