dexbot 1.4.1 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +1 -0
  2. package/dist/modules/account_bots.d.ts.map +1 -1
  3. package/dist/modules/account_bots.js +29 -1
  4. package/dist/modules/account_bots.js.map +1 -1
  5. package/dist/modules/constants.d.ts +1 -0
  6. package/dist/modules/constants.d.ts.map +1 -1
  7. package/dist/modules/constants.js +5 -0
  8. package/dist/modules/constants.js.map +1 -1
  9. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  10. package/dist/modules/dexbot_cow_runtime.js +36 -7
  11. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  12. package/dist/modules/launcher/credential_daemon.d.ts.map +1 -1
  13. package/dist/modules/launcher/credential_daemon.js +3 -4
  14. package/dist/modules/launcher/credential_daemon.js.map +1 -1
  15. package/dist/modules/launcher/market_adapter_runtime.d.ts.map +1 -1
  16. package/dist/modules/launcher/market_adapter_runtime.js +2 -4
  17. package/dist/modules/launcher/market_adapter_runtime.js.map +1 -1
  18. package/dist/modules/launcher/market_adapter_watchdog.d.ts.map +1 -1
  19. package/dist/modules/launcher/market_adapter_watchdog.js +4 -6
  20. package/dist/modules/launcher/market_adapter_watchdog.js.map +1 -1
  21. package/dist/modules/logger.js +0 -1
  22. package/dist/modules/logger.js.map +1 -1
  23. package/dist/modules/order/grid.d.ts.map +1 -1
  24. package/dist/modules/order/grid.js +148 -99
  25. package/dist/modules/order/grid.js.map +1 -1
  26. package/dist/modules/order/logger.d.ts.map +1 -1
  27. package/dist/modules/order/logger.js +2 -8
  28. package/dist/modules/order/logger.js.map +1 -1
  29. package/dist/modules/order/manager.d.ts.map +1 -1
  30. package/dist/modules/order/manager.js +6 -9
  31. package/dist/modules/order/manager.js.map +1 -1
  32. package/dist/modules/order/utils/math.d.ts.map +1 -1
  33. package/dist/modules/order/utils/math.js +16 -1
  34. package/dist/modules/order/utils/math.js.map +1 -1
  35. package/dist/modules/order/utils/system.d.ts +2 -1
  36. package/dist/modules/order/utils/system.d.ts.map +1 -1
  37. package/dist/modules/order/utils/system.js +79 -21
  38. package/dist/modules/order/utils/system.js.map +1 -1
  39. package/dist/modules/order/utils/timeout.d.ts +25 -0
  40. package/dist/modules/order/utils/timeout.d.ts.map +1 -0
  41. package/dist/modules/order/utils/timeout.js +49 -0
  42. package/dist/modules/order/utils/timeout.js.map +1 -0
  43. package/dist/modules/order/utils/validate.d.ts +8 -1
  44. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  45. package/dist/modules/order/utils/validate.js +33 -9
  46. package/dist/modules/order/utils/validate.js.map +1 -1
  47. package/dist/modules/order/utils/withPoolRef.d.ts +6 -0
  48. package/dist/modules/order/utils/withPoolRef.d.ts.map +1 -0
  49. package/dist/modules/order/utils/withPoolRef.js +129 -0
  50. package/dist/modules/order/utils/withPoolRef.js.map +1 -0
  51. package/dist/modules/types.d.ts +1 -0
  52. package/dist/modules/types.d.ts.map +1 -1
  53. package/dist/unlock.d.ts.map +1 -1
  54. package/dist/unlock.js +2 -4
  55. package/dist/unlock.js.map +1 -1
  56. package/package.json +2 -1
@@ -172,6 +172,7 @@ function _snapshotFundState(manager) {
172
172
  const math_1 = require("./utils/math");
173
173
  const order_1 = require("./utils/order");
174
174
  const system_1 = require("./utils/system");
175
+ const withPoolRef_1 = require("./utils/withPoolRef");
175
176
  const market_adapter_whitelist_1 = require("../market_adapter_whitelist");
176
177
  const errors_1 = require("../utils/errors");
177
178
  function calculateGapSlots(incrementPercent, targetSpreadPercent, gridLimitsOverride) {
@@ -499,7 +500,7 @@ async function loadGrid(manager, grid, boundaryIdx = null) {
499
500
  return;
500
501
  return await manager._gridLock.acquire(async () => {
501
502
  try {
502
- await manager._initializeAssets();
503
+ await (0, system_1.withBlockchainRetry)(() => manager._initializeAssets(), 'initializeAssets', { logger: manager.logger });
503
504
  }
504
505
  catch (e) {
505
506
  manager.logger?.log?.(`Asset initialization failed during grid load: ${(0, errors_1.getErrorMessage)(e)}`, 'warn');
@@ -596,7 +597,12 @@ async function loadGrid(manager, grid, boundaryIdx = null) {
596
597
  async function initializeGrid(manager) {
597
598
  if (!manager)
598
599
  throw new Error('initializeGrid requires a manager instance');
599
- await manager._initializeAssets();
600
+ try {
601
+ await (0, system_1.withBlockchainRetry)(() => manager._initializeAssets(), 'initializeAssets', { logger: manager.logger });
602
+ }
603
+ catch (e) {
604
+ manager.logger?.log?.(`Asset initialization failed during grid init: ${(0, errors_1.getErrorMessage)(e)}`, 'warn');
605
+ }
600
606
  // FIX: Add explicit state validation to prevent cryptic errors later
601
607
  if (!manager.assets || !manager.assets.assetA || !manager.assets.assetB) {
602
608
  throw new Error('Asset initialization did not complete properly - assetA or assetB undefined');
@@ -610,7 +616,7 @@ async function initializeGrid(manager) {
610
616
  if (typeof mpRaw !== 'number' || isNaN(mpRaw)) {
611
617
  try {
612
618
  const { BitShares } = require('../bitshares_client');
613
- const derived = await (0, system_1.derivePrice)(BitShares, manager.config.assetA, manager.config.assetB, manager.config.priceMode || 'auto');
619
+ const derived = await (0, withPoolRef_1.derivePriceWithPoolRef)(BitShares, manager.config.assetA, manager.config.assetB, manager.config.priceMode || 'auto', manager.config.poolRef);
614
620
  if (derived) {
615
621
  manager.logger?.log?.(`[DIAGNOSTIC] initializeGrid: Derived new startPrice=${derived.toFixed(8)} (mode=${manager.config.priceMode || 'auto'})`, 'info');
616
622
  manager.config.startPrice = Number(derived);
@@ -649,7 +655,7 @@ async function initializeGrid(manager) {
649
655
  else if (gpMode === 'pool' || gpMode === 'book') {
650
656
  try {
651
657
  const { BitShares } = require('../bitshares_client');
652
- const derived = await (0, system_1.derivePrice)(BitShares, manager.config.assetA, manager.config.assetB, gpMode);
658
+ const derived = await (0, withPoolRef_1.derivePriceWithPoolRef)(BitShares, manager.config.assetA, manager.config.assetB, gpMode, manager.config.poolRef);
653
659
  if (derived) {
654
660
  gp = Number(derived);
655
661
  gpSource = gpMode;
@@ -870,49 +876,79 @@ async function recalculateGrid(manager, opts) {
870
876
  // Total timeout across all steps — prevents indefinite hang even if
871
877
  // an individual withBlockchainRetry step pins the event loop.
872
878
  const totalTimeoutMs = constants_1.PIPELINE_TIMING.TIMEOUT_MS * 2; // 10 min
879
+ let _resyncAborted = false;
873
880
  const work = (async () => {
874
- manager.logger?.log?.('Starting full resync...', 'info');
875
- // #1: Initialize assets (returns immediately if already loaded)
876
- await manager._initializeAssets();
877
- // #2: Fetch account totals with timeout + retry + node failover
878
- await (0, system_1.withBlockchainRetry)(() => manager.fetchAccountTotals(), 'fetchAccountTotals', { logger: manager.logger });
879
- // #3: Read open orders with timeout + retry + node failover
880
- const chainOpenOrders = await (0, system_1.withBlockchainRetry)(() => readOpenOrdersFn(), 'readOpenOrders', { logger: manager.logger });
881
- if (!Array.isArray(chainOpenOrders))
882
- return;
883
- await (0, system_1.withBlockchainRetry)(() => manager.syncFromOpenOrders(chainOpenOrders, { skipAccounting: true }), 'syncFromOpenOrders', { logger: manager.logger });
884
- manager.resetFunds();
885
- await manager.persistGrid();
886
- await initializeGrid(manager);
887
- const { reconcileGridOrders } = require('./grid_reconcile');
888
- // #5: Reconcile grid orders with timeout + retry + node failover
889
881
  try {
890
- await (0, system_1.withBlockchainRetry)(() => reconcileGridOrders({ manager, config: manager.config, account, privateKey, chainOrders, chainOpenOrders }), 'reconcileGridOrders',
891
- // 5 min: Phase 2 of reconcile does sequential creates (~3s each);
892
- // the default 30s timeout would kill mid-batch and cause duplicate-
893
- // accumulation death spirals. PIPELINE_TIMING.TIMEOUT_MS gives enough
894
- // headroom for all pending creates+updates to finish in one shot.
895
- { logger: manager.logger, timeoutMs: constants_1.PIPELINE_TIMING.TIMEOUT_MS });
882
+ manager.logger?.log?.('Starting full resync...', 'info');
883
+ if (_resyncAborted)
884
+ return;
885
+ // #1: Initialize assets with timeout + retry + node failover
886
+ try {
887
+ await (0, system_1.withBlockchainRetry)(() => manager._initializeAssets(), 'initializeAssets', { logger: manager.logger });
888
+ }
889
+ catch (e) {
890
+ manager.logger?.log?.(`Asset initialization failed during resync: ${(0, errors_1.getErrorMessage)(e)}`, 'warn');
891
+ }
892
+ if (_resyncAborted)
893
+ return;
894
+ // #2: Fetch account totals with timeout + retry + node failover
895
+ await (0, system_1.withBlockchainRetry)(() => manager.fetchAccountTotals(), 'fetchAccountTotals', { logger: manager.logger });
896
+ if (_resyncAborted)
897
+ return;
898
+ // #3: Read open orders with timeout + retry + node failover
899
+ const chainOpenOrders = await (0, system_1.withBlockchainRetry)(() => readOpenOrdersFn(), 'readOpenOrders', { logger: manager.logger });
900
+ if (_resyncAborted)
901
+ return;
902
+ if (!Array.isArray(chainOpenOrders))
903
+ return;
904
+ await (0, system_1.withBlockchainRetry)(() => manager.syncFromOpenOrders(chainOpenOrders, { skipAccounting: true }), 'syncFromOpenOrders', { logger: manager.logger });
905
+ if (_resyncAborted)
906
+ return;
907
+ manager.resetFunds();
908
+ if (_resyncAborted)
909
+ return;
910
+ await manager.persistGrid();
911
+ if (_resyncAborted)
912
+ return;
913
+ await initializeGrid(manager);
914
+ if (_resyncAborted)
915
+ return;
916
+ const { reconcileGridOrders } = require('./grid_reconcile');
917
+ // #5: Reconcile grid orders with timeout + retry + node failover
918
+ try {
919
+ await (0, system_1.withBlockchainRetry)(() => reconcileGridOrders({ manager, config: manager.config, account, privateKey, chainOrders, chainOpenOrders }), 'reconcileGridOrders',
920
+ // 5 min: Phase 2 of reconcile does sequential creates (~3s each);
921
+ // the default 30s timeout would kill mid-batch and cause duplicate-
922
+ // accumulation death spirals. PIPELINE_TIMING.TIMEOUT_MS gives enough
923
+ // headroom for all pending creates+updates to finish in one shot.
924
+ { logger: manager.logger, timeoutMs: constants_1.PIPELINE_TIMING.TIMEOUT_MS });
925
+ }
926
+ catch (err) {
927
+ manager.logger?.log?.(`Error during startup order reconciliation: ${(0, errors_1.getErrorMessage)(err)}`, 'error');
928
+ throw new Error(`Grid recalculation failed during order reconciliation: ${(0, errors_1.getErrorMessage)(err)}`);
929
+ }
930
+ if (_resyncAborted)
931
+ return;
932
+ manager.logger?.log?.('Full resync complete.', 'info');
896
933
  }
897
- catch (err) {
898
- manager.logger?.log?.(`Error during startup order reconciliation: ${(0, errors_1.getErrorMessage)(err)}`, 'error');
899
- throw new Error(`Grid recalculation failed during order reconciliation: ${(0, errors_1.getErrorMessage)(err)}`);
934
+ finally {
935
+ manager.finishBootstrap();
900
936
  }
901
- manager.logger?.log?.('Full resync complete.', 'info');
902
937
  })();
903
- try {
904
- // Swallow late rejection if timeout wins the race
905
- Promise.resolve(work).catch(() => { });
906
- return await Promise.race([
907
- work,
908
- new Promise((_, reject) => {
909
- setTimeout(() => reject(new Error(`recalculateGrid timed out after ${totalTimeoutMs}ms`)), totalTimeoutMs);
910
- })
911
- ]);
912
- }
913
- finally {
914
- manager.finishBootstrap();
915
- }
938
+ // Swallow late rejection if timeout wins the race
939
+ Promise.resolve(work).catch(() => { });
940
+ let timeoutId;
941
+ const result = await Promise.race([
942
+ work,
943
+ new Promise((_, reject) => {
944
+ timeoutId = setTimeout(() => {
945
+ _resyncAborted = true;
946
+ reject(new Error(`recalculateGrid timed out after ${totalTimeoutMs}ms`));
947
+ }, totalTimeoutMs);
948
+ })
949
+ ]);
950
+ clearTimeout(timeoutId);
951
+ return result;
916
952
  }
917
953
  /**
918
954
  * Check for grid divergence and trigger update if threshold is met.
@@ -1018,10 +1054,18 @@ async function _recalculateGridOrderSizesFromBlockchain(manager, orderType, opti
1018
1054
  const ctx = await _getSizingContext(manager, sideName);
1019
1055
  if (!ctx)
1020
1056
  return collectActions ? { actions: [], changed: false } : undefined;
1021
- // Get ALL slots for this side, sorted for calculateRotationOrderSizes
1057
+ // Get ALL slots for this side, sorted for calculateRotationOrderSizes.
1022
1058
  // SELL: sorted ASC (Market to Edge)
1023
1059
  // BUY: sorted ASC (Edge to Market)
1024
- const allSideSlots = Array.from(manager.orders.values())
1060
+ //
1061
+ // When a working grid is available (COW mode), read types from it
1062
+ // instead of manager.orders. This ensures slots whose type changed
1063
+ // due to a boundary shift (e.g. SPREAD→BUY) are included in the
1064
+ // correct side's size calculation — using manager.orders here would
1065
+ // filter by stale pre-shift types and miss the crossers, producing a
1066
+ // budget allocation that doesn't match the post-shift grid structure.
1067
+ const orderSource = collectActions ? workingGrid : manager.orders;
1068
+ const allSideSlots = Array.from(orderSource.values())
1025
1069
  .filter((o) => o.type === orderType)
1026
1070
  .sort((a, b) => a.price - b.price);
1027
1071
  if (allSideSlots.length === 0)
@@ -1133,32 +1177,37 @@ async function updateGridFromBlockchainSnapshot(manager, orderType = 'both', fro
1133
1177
  const workingGrid = new WorkingGrid(manager.orders, { baseVersion: manager._gridVersion });
1134
1178
  const allActions = [];
1135
1179
  let hasWorkingChanges = false;
1136
- // Calculate size updates for each side (via existing sizing function in COW mode)
1137
- if (orderType === constants_1.ORDER_TYPES.BUY || orderType === 'both') {
1138
- const buyResult = await _recalculateGridOrderSizesFromBlockchain(manager, constants_1.ORDER_TYPES.BUY, { workingGrid });
1139
- allActions.push(...buyResult.actions);
1140
- hasWorkingChanges = hasWorkingChanges || buyResult.changed;
1141
- }
1142
- if (orderType === constants_1.ORDER_TYPES.SELL || orderType === 'both') {
1143
- const sellResult = await _recalculateGridOrderSizesFromBlockchain(manager, constants_1.ORDER_TYPES.SELL, { workingGrid });
1144
- allActions.push(...sellResult.actions);
1145
- hasWorkingChanges = hasWorkingChanges || sellResult.changed;
1146
- }
1147
- // If the boundary is shifting, reassign slot types in the WorkingGrid now.
1148
- // This ensures the COW commit delivers consistent types + boundaryIdx in one
1149
- // atomic operation — manager.boundaryIdx must not be touched before the commit.
1150
1180
  const newBoundary = (overrideBoundaryIdx !== null) ? overrideBoundaryIdx : manager.boundaryIdx;
1181
+ // REASSIGN SLOT TYPES FIRST when the boundary is shifting.
1182
+ // _recalculateGridOrderSizesFromBlockchain needs to see the corrected
1183
+ // types so that slots which cross the boundary (e.g. SPREAD→BUY) are
1184
+ // included in the correct side's budget allocation. Running assignGridRoles
1185
+ // after the size calc means crossers keep their pre-shift sizes, producing
1186
+ // an allocation that doesn't match the post-shift grid structure.
1151
1187
  if (overrideBoundaryIdx !== null && overrideBoundaryIdx !== manager.boundaryIdx) {
1152
1188
  const gapSlots = calculateGapSlots(manager.config.incrementPercent, manager.config.targetSpreadPercent, manager.config.gridLimits);
1153
1189
  const allSlots = Array.from(workingGrid.values())
1154
1190
  .filter((s) => s.price != null)
1155
1191
  .sort((a, b) => a.price - b.price);
1156
- const updatedSlots = (0, order_1.assignGridRoles)(allSlots, newBoundary, gapSlots, constants_1.ORDER_TYPES, constants_1.ORDER_STATES);
1192
+ const updatedSlots = (0, order_1.assignGridRoles)(allSlots, newBoundary, gapSlots, constants_1.ORDER_TYPES, constants_1.ORDER_STATES, { assignOnChain: true });
1157
1193
  for (const slot of updatedSlots) {
1158
1194
  workingGrid.set(slot.id, slot);
1159
1195
  }
1160
1196
  hasWorkingChanges = true;
1161
1197
  }
1198
+ // Calculate size updates for each side (via existing sizing function in COW mode).
1199
+ // _recalculateGridOrderSizesFromBlockchain reads types from the working grid when
1200
+ // one is passed, so boundary-crossing slots are now correctly classified.
1201
+ if (orderType === constants_1.ORDER_TYPES.BUY || orderType === 'both') {
1202
+ const buyResult = await _recalculateGridOrderSizesFromBlockchain(manager, constants_1.ORDER_TYPES.BUY, { workingGrid });
1203
+ allActions.push(...buyResult.actions);
1204
+ hasWorkingChanges = hasWorkingChanges || buyResult.changed;
1205
+ }
1206
+ if (orderType === constants_1.ORDER_TYPES.SELL || orderType === 'both') {
1207
+ const sellResult = await _recalculateGridOrderSizesFromBlockchain(manager, constants_1.ORDER_TYPES.SELL, { workingGrid });
1208
+ allActions.push(...sellResult.actions);
1209
+ hasWorkingChanges = hasWorkingChanges || sellResult.changed;
1210
+ }
1162
1211
  // Return COW result only if there are changes
1163
1212
  if (allActions.length === 0 && !hasWorkingChanges) {
1164
1213
  return null;
@@ -1468,8 +1517,21 @@ async function checkSpreadCondition(manager, _BitShares, updateOrdersOnChainBatc
1468
1517
  correction = await prepareSpreadCorrectionOrders(manager, decision.side);
1469
1518
  if (!correction)
1470
1519
  return false;
1471
- const placeCount = correction.ordersToPlace?.length || 0;
1472
- const updateCount = correction.ordersToUpdate?.length || 0;
1520
+ let placeCount = correction.ordersToPlace?.length || 0;
1521
+ let updateCount = correction.ordersToUpdate?.length || 0;
1522
+ // STARVATION FALLBACK: If the selected side has no correctable slots (e.g.
1523
+ // all SPREAD slots already filled or misaligned), try the opposite side.
1524
+ if ((placeCount + updateCount) === 0) {
1525
+ const oppositeSide = decision.side === constants_1.ORDER_TYPES.BUY ? constants_1.ORDER_TYPES.SELL : constants_1.ORDER_TYPES.BUY;
1526
+ manager.logger?.log?.(`[SPREAD] Side ${decision.side} produced zero candidates; ` +
1527
+ `trying opposite side ${oppositeSide}.`, 'debug');
1528
+ const oppositeCorrection = await prepareSpreadCorrectionOrders(manager, oppositeSide);
1529
+ if (oppositeCorrection) {
1530
+ correction = oppositeCorrection;
1531
+ placeCount = correction.ordersToPlace?.length || 0;
1532
+ updateCount = correction.ordersToUpdate?.length || 0;
1533
+ }
1534
+ }
1473
1535
  // Capture fund snapshot under lock for pre-flight verification before broadcast
1474
1536
  fundSnapshot = _snapshotFundState(manager);
1475
1537
  return (placeCount + updateCount) > 0;
@@ -1730,29 +1792,7 @@ function determineOrderSideByFunds(manager, currentMarketPrice) {
1730
1792
  const sellInBuyUnits = (Number.isFinite(marketPrice) && marketPrice > 0)
1731
1793
  ? sellAvailable * marketPrice
1732
1794
  : sellAvailable;
1733
- // Determine market direction from current price vs grid center price.
1734
- // When the market is below the grid center, the bot should prefer SELL
1735
- // (to narrow the spread from above — sells are too high). When the market
1736
- // is above the grid center, prefer BUY (buys are too low). This prevents
1737
- // spread correction from placing orders on the retreating side, which would
1738
- // compound inventory in the wrong direction after a batch of fill-induced
1739
- // boundary shifts.
1740
- const centerPrice = manager._lastGridPricingContext?.startPrice
1741
- ?? Number(manager.config.startPrice)
1742
- ?? null;
1743
- const hasDirection = Number.isFinite(marketPrice)
1744
- && marketPrice > 0
1745
- && Number.isFinite(centerPrice)
1746
- && centerPrice > 0;
1747
- if (hasDirection && marketPrice < centerPrice) {
1748
- side = constants_1.ORDER_TYPES.SELL;
1749
- }
1750
- else if (hasDirection && marketPrice > centerPrice) {
1751
- side = constants_1.ORDER_TYPES.BUY;
1752
- }
1753
- else {
1754
- side = buyAvailable >= sellInBuyUnits ? constants_1.ORDER_TYPES.BUY : constants_1.ORDER_TYPES.SELL;
1755
- }
1795
+ side = buyAvailable >= sellInBuyUnits ? constants_1.ORDER_TYPES.BUY : constants_1.ORDER_TYPES.SELL;
1756
1796
  }
1757
1797
  else if (buyViable) {
1758
1798
  side = constants_1.ORDER_TYPES.BUY;
@@ -1852,27 +1892,27 @@ async function prepareSpreadCorrectionOrders(manager, preferredSide) {
1852
1892
  // 1. Priority: Update existing PARTIAL orders at the edge (Highest Buy / Lowest Sell).
1853
1893
  // 2. Fallback: Activate SPREAD slots at the edge (Lowest Spread for Buy / Highest Spread for Sell).
1854
1894
  const allOrders = Array.from(manager.orders.values());
1855
- let edgePartial = null;
1856
- const partials = allOrders
1857
- .filter((o) => o.type === railType && o.state === constants_1.ORDER_STATES.PARTIAL)
1858
- .sort((a, b) => railType === constants_1.ORDER_TYPES.BUY ? b.price - a.price : a.price - b.price);
1859
- if (partials.length > 0) {
1860
- edgePartial = partials[0];
1861
- manager.logger?.log?.(`[SPREAD-CORRECTION] Identified partial order at ${edgePartial.price} for update`, 'debug');
1862
- }
1863
- // Boundary-correct type computation. Used by both candidate pools below to ensure
1864
- // spread correction does not re-activate slots whose current boundary position
1865
- // places them on the wrong side or in the spread zone — doing so would compound
1866
- // inventory at prices where the bot already traded.
1895
+ // Boundary-correct type computation — hoisted before edge-partial and
1896
+ // candidate filters so all call sites (including the edge-partial filter
1897
+ // which now uses getSlotCorrectType) can access it.
1867
1898
  //
1868
- // The natural type of a slot is derived from its position in the price-sorted rail
1869
- // relative to boundaryIdx + gapSlots: indices in [0, boundaryIdx] are BUY, indices
1870
- // in [boundaryIdx + gapSlots + 1, N-1] are SELL, the middle band is SPREAD.
1899
+ // The natural type of a slot is derived from its position in the price-sorted
1900
+ // rail relative to boundaryIdx + gapSlots: indices in [0, boundaryIdx] are
1901
+ // BUY, indices in [boundaryIdx + gapSlots + 1, N-1] are SELL, the middle
1902
+ // band is SPREAD.
1871
1903
  const allSlotsByPrice = allOrders
1872
1904
  .filter((o) => o.price != null && Number.isFinite(o.price))
1873
1905
  .sort((a, b) => a.price - b.price);
1874
1906
  const slotIndexMap = new Map(allSlotsByPrice.map((o, i) => [o.id, i]));
1875
1907
  const gapSlots = calculateGapSlots(manager.config?.incrementPercent, manager.config?.targetSpreadPercent, manager.config?.gridLimits);
1908
+ // Sync boundary from current fund state to avoid stale boundaryIdx causing
1909
+ // getSlotCorrectType to misclassify slots (e.g. after fills shifted the
1910
+ // boundary but the COW commit hasn't updated it yet). This is safe under
1911
+ // the grid lock — no concurrent modifications can race with this read.
1912
+ const boundarySync = (0, system_1.syncBoundaryToFunds)(manager);
1913
+ if (boundarySync.changed && boundarySync.newIdx !== undefined) {
1914
+ manager.boundaryIdx = boundarySync.newIdx;
1915
+ }
1876
1916
  const bIdx = manager.boundaryIdx ?? 0;
1877
1917
  const buyEndIdx = bIdx;
1878
1918
  const sellStartIdx = bIdx + Number(gapSlots) + 1;
@@ -1886,6 +1926,15 @@ async function prepareSpreadCorrectionOrders(manager, preferredSide) {
1886
1926
  return constants_1.ORDER_TYPES.SELL;
1887
1927
  return constants_1.ORDER_TYPES.SPREAD;
1888
1928
  };
1929
+ let edgePartial = null;
1930
+ const partials = allOrders
1931
+ .filter((o) => getSlotCorrectType(o) === railType
1932
+ && o.state === constants_1.ORDER_STATES.PARTIAL)
1933
+ .sort((a, b) => railType === constants_1.ORDER_TYPES.BUY ? b.price - a.price : a.price - b.price);
1934
+ if (partials.length > 0) {
1935
+ edgePartial = partials[0];
1936
+ manager.logger?.log?.(`[SPREAD-CORRECTION] Identified partial order at ${edgePartial.price} for update`, 'debug');
1937
+ }
1889
1938
  // Primary candidates: SPREAD-type slots adjacent to the gap. Filter by
1890
1939
  // boundary-correct type so a SPREAD slot that, after a boundary shift, now sits
1891
1940
  // in the BUY or SELL zone is excluded — it would otherwise be placed on the