dexbot 1.5.3 → 1.6.1

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 (242) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +9 -7
  3. package/analysis/README.md +1 -2
  4. package/analysis/ama_fitting/fetch_lp_candles.ts +1 -9
  5. package/analysis/ama_fitting/package.json +1 -1
  6. package/analysis/analyze_derivatives.ts +1 -1
  7. package/analysis/bot_usage/discover_bot_accounts.ts +12 -6
  8. package/analysis/resolve_source.ts +9 -2
  9. package/analysis/tradingview/README.md +29 -11
  10. package/analysis/tradingview/analyze_tradingview.ts +92 -3
  11. package/analysis/tradingview/tradingview_uplot_chart_generator.ts +1120 -182
  12. package/analysis/trend_detection/package.json +1 -1
  13. package/claw/docs/DEXBOT2_TUNING_CHEAT_SHEET.md +1 -1
  14. package/claw/modules/dexbot_profiles.ts +22 -2
  15. package/claw/package.json +1 -1
  16. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  17. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  18. package/claw/skills/launcher-ops/references/launcher-workflow.md +1 -1
  19. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  20. package/claw/tests/test_dexbot_profiles.ts +65 -0
  21. package/dist/analysis/ama_fitting/fetch_lp_candles.js +1 -9
  22. package/dist/analysis/ama_fitting/fetch_lp_candles.js.map +1 -1
  23. package/dist/analysis/analyze_derivatives.js +1 -1
  24. package/dist/analysis/bot_usage/discover_bot_accounts.js +11 -6
  25. package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
  26. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +2 -0
  27. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
  28. package/dist/analysis/resolve_source.d.ts +1 -0
  29. package/dist/analysis/resolve_source.d.ts.map +1 -1
  30. package/dist/analysis/resolve_source.js +6 -2
  31. package/dist/analysis/resolve_source.js.map +1 -1
  32. package/dist/analysis/tradingview/analyze_tradingview.d.ts +5 -0
  33. package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -1
  34. package/dist/analysis/tradingview/analyze_tradingview.js +112 -3
  35. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
  36. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts +5 -1
  37. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
  38. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +1118 -182
  39. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
  40. package/dist/bot.js +1 -1
  41. package/dist/bot.js.map +1 -1
  42. package/dist/dexbot.js +4 -4
  43. package/dist/dexbot.js.map +1 -1
  44. package/dist/market_adapter/ama_signal_runner.js +2 -2
  45. package/dist/market_adapter/candle_utils.d.ts +6 -0
  46. package/dist/market_adapter/candle_utils.d.ts.map +1 -1
  47. package/dist/market_adapter/candle_utils.js +6 -0
  48. package/dist/market_adapter/candle_utils.js.map +1 -1
  49. package/dist/market_adapter/core/kibana_candles.d.ts +17 -1
  50. package/dist/market_adapter/core/kibana_candles.d.ts.map +1 -1
  51. package/dist/market_adapter/core/kibana_candles.js +101 -19
  52. package/dist/market_adapter/core/kibana_candles.js.map +1 -1
  53. package/dist/market_adapter/core/kibana_client.d.ts +2 -0
  54. package/dist/market_adapter/core/kibana_client.d.ts.map +1 -1
  55. package/dist/market_adapter/core/kibana_client.js +31 -4
  56. package/dist/market_adapter/core/kibana_client.js.map +1 -1
  57. package/dist/market_adapter/core/market_adapter_service.d.ts.map +1 -1
  58. package/dist/market_adapter/core/market_adapter_service.js +0 -6
  59. package/dist/market_adapter/core/market_adapter_service.js.map +1 -1
  60. package/dist/market_adapter/inputs/fetch_book_data.d.ts +14 -0
  61. package/dist/market_adapter/inputs/fetch_book_data.d.ts.map +1 -0
  62. package/dist/market_adapter/inputs/fetch_book_data.js +138 -0
  63. package/dist/market_adapter/inputs/fetch_book_data.js.map +1 -0
  64. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +1 -1
  65. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +16 -2
  66. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  67. package/dist/market_adapter/inputs/fetch_lp_data.js +109 -281
  68. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  69. package/dist/market_adapter/inputs/kibana_feed_source.d.ts +87 -0
  70. package/dist/market_adapter/inputs/kibana_feed_source.d.ts.map +1 -0
  71. package/dist/market_adapter/inputs/kibana_feed_source.js +570 -0
  72. package/dist/market_adapter/inputs/kibana_feed_source.js.map +1 -0
  73. package/dist/market_adapter/inputs/kibana_source.d.ts.map +1 -1
  74. package/dist/market_adapter/inputs/kibana_source.js +10 -5
  75. package/dist/market_adapter/inputs/kibana_source.js.map +1 -1
  76. package/dist/market_adapter/inputs/window_cache.d.ts +166 -0
  77. package/dist/market_adapter/inputs/window_cache.d.ts.map +1 -0
  78. package/dist/market_adapter/inputs/window_cache.js +500 -0
  79. package/dist/market_adapter/inputs/window_cache.js.map +1 -0
  80. package/dist/market_adapter/interval_utils.d.ts +8 -1
  81. package/dist/market_adapter/interval_utils.d.ts.map +1 -1
  82. package/dist/market_adapter/interval_utils.js +14 -1
  83. package/dist/market_adapter/interval_utils.js.map +1 -1
  84. package/dist/modules/account_bots.d.ts +1 -0
  85. package/dist/modules/account_bots.d.ts.map +1 -1
  86. package/dist/modules/account_bots.js +21 -5
  87. package/dist/modules/account_bots.js.map +1 -1
  88. package/dist/modules/account_orders.d.ts +25 -4
  89. package/dist/modules/account_orders.d.ts.map +1 -1
  90. package/dist/modules/account_orders.js +67 -4
  91. package/dist/modules/account_orders.js.map +1 -1
  92. package/dist/modules/bitshares-native/chain_client.d.ts +2 -0
  93. package/dist/modules/bitshares-native/chain_client.d.ts.map +1 -1
  94. package/dist/modules/bitshares-native/chain_client.js +3 -1
  95. package/dist/modules/bitshares-native/chain_client.js.map +1 -1
  96. package/dist/modules/bitshares-native/subscriptions.d.ts.map +1 -1
  97. package/dist/modules/bitshares-native/subscriptions.js +13 -3
  98. package/dist/modules/bitshares-native/subscriptions.js.map +1 -1
  99. package/dist/modules/bitshares-native/transport.d.ts +2 -0
  100. package/dist/modules/bitshares-native/transport.d.ts.map +1 -1
  101. package/dist/modules/bitshares-native/transport.js +122 -28
  102. package/dist/modules/bitshares-native/transport.js.map +1 -1
  103. package/dist/modules/bitshares_client.d.ts.map +1 -1
  104. package/dist/modules/bitshares_client.js +59 -8
  105. package/dist/modules/bitshares_client.js.map +1 -1
  106. package/dist/modules/bot_settings.d.ts.map +1 -1
  107. package/dist/modules/bot_settings.js +11 -0
  108. package/dist/modules/bot_settings.js.map +1 -1
  109. package/dist/modules/constants.d.ts +18 -0
  110. package/dist/modules/constants.d.ts.map +1 -1
  111. package/dist/modules/constants.js +90 -4
  112. package/dist/modules/constants.js.map +1 -1
  113. package/dist/modules/dexbot_class.d.ts +11 -0
  114. package/dist/modules/dexbot_class.d.ts.map +1 -1
  115. package/dist/modules/dexbot_class.js +92 -4
  116. package/dist/modules/dexbot_class.js.map +1 -1
  117. package/dist/modules/dexbot_cow_runtime.d.ts +26 -1
  118. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  119. package/dist/modules/dexbot_cow_runtime.js +157 -28
  120. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  121. package/dist/modules/dexbot_fill_runtime.d.ts +66 -1
  122. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  123. package/dist/modules/dexbot_fill_runtime.js +295 -10
  124. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  125. package/dist/modules/dexbot_maintenance_runtime.d.ts +102 -12
  126. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  127. package/dist/modules/dexbot_maintenance_runtime.js +644 -48
  128. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  129. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  130. package/dist/modules/dexbot_startup_runtime.js +13 -0
  131. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  132. package/dist/modules/dexbot_state_recovery.d.ts +15 -1
  133. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  134. package/dist/modules/dexbot_state_recovery.js +52 -8
  135. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  136. package/dist/modules/fund_registry.d.ts +1 -1
  137. package/dist/modules/fund_registry.js +1 -1
  138. package/dist/modules/market_adapter_whitelist.js +1 -1
  139. package/dist/modules/market_adapter_whitelist.js.map +1 -1
  140. package/dist/modules/node_connect_policy.d.ts +52 -0
  141. package/dist/modules/node_connect_policy.d.ts.map +1 -0
  142. package/dist/modules/node_connect_policy.js +35 -0
  143. package/dist/modules/node_connect_policy.js.map +1 -0
  144. package/dist/modules/node_manager.d.ts +32 -0
  145. package/dist/modules/node_manager.d.ts.map +1 -1
  146. package/dist/modules/node_manager.js +76 -9
  147. package/dist/modules/node_manager.js.map +1 -1
  148. package/dist/modules/order/accounting.d.ts +30 -0
  149. package/dist/modules/order/accounting.d.ts.map +1 -1
  150. package/dist/modules/order/accounting.js +176 -3
  151. package/dist/modules/order/accounting.js.map +1 -1
  152. package/dist/modules/order/export.d.ts.map +1 -1
  153. package/dist/modules/order/export.js +1 -0
  154. package/dist/modules/order/export.js.map +1 -1
  155. package/dist/modules/order/grid.d.ts.map +1 -1
  156. package/dist/modules/order/grid.js +189 -25
  157. package/dist/modules/order/grid.js.map +1 -1
  158. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  159. package/dist/modules/order/grid_reconcile.js +5 -3
  160. package/dist/modules/order/grid_reconcile.js.map +1 -1
  161. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  162. package/dist/modules/order/grid_reconcile_internal.js +216 -9
  163. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  164. package/dist/modules/order/logger.js +1 -1
  165. package/dist/modules/order/logger.js.map +1 -1
  166. package/dist/modules/order/manager.d.ts +77 -2
  167. package/dist/modules/order/manager.d.ts.map +1 -1
  168. package/dist/modules/order/manager.js +283 -14
  169. package/dist/modules/order/manager.js.map +1 -1
  170. package/dist/modules/order/strategy.d.ts +3 -0
  171. package/dist/modules/order/strategy.d.ts.map +1 -1
  172. package/dist/modules/order/strategy.js +79 -7
  173. package/dist/modules/order/strategy.js.map +1 -1
  174. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  175. package/dist/modules/order/sync_engine.js +16 -1
  176. package/dist/modules/order/sync_engine.js.map +1 -1
  177. package/dist/modules/order/utils/math.d.ts +16 -2
  178. package/dist/modules/order/utils/math.d.ts.map +1 -1
  179. package/dist/modules/order/utils/math.js +30 -3
  180. package/dist/modules/order/utils/math.js.map +1 -1
  181. package/dist/modules/order/utils/order.d.ts +258 -13
  182. package/dist/modules/order/utils/order.d.ts.map +1 -1
  183. package/dist/modules/order/utils/order.js +666 -13
  184. package/dist/modules/order/utils/order.js.map +1 -1
  185. package/dist/modules/order/utils/system.d.ts +36 -0
  186. package/dist/modules/order/utils/system.d.ts.map +1 -1
  187. package/dist/modules/order/utils/system.js +102 -6
  188. package/dist/modules/order/utils/system.js.map +1 -1
  189. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  190. package/dist/modules/order/utils/validate.js +6 -2
  191. package/dist/modules/order/utils/validate.js.map +1 -1
  192. package/dist/modules/paths.d.ts +2 -0
  193. package/dist/modules/paths.d.ts.map +1 -1
  194. package/dist/modules/paths.js +1 -0
  195. package/dist/modules/paths.js.map +1 -1
  196. package/dist/modules/runtime_settings.d.ts +13 -0
  197. package/dist/modules/runtime_settings.d.ts.map +1 -1
  198. package/dist/modules/runtime_settings.js +20 -0
  199. package/dist/modules/runtime_settings.js.map +1 -1
  200. package/dist/modules/utils/errors.d.ts +16 -0
  201. package/dist/modules/utils/errors.d.ts.map +1 -1
  202. package/dist/modules/utils/errors.js +31 -0
  203. package/dist/modules/utils/errors.js.map +1 -1
  204. package/dist/pm2.js +7 -7
  205. package/dist/scripts/analyze-credit.d.ts.map +1 -1
  206. package/dist/scripts/analyze-credit.js +13 -3
  207. package/dist/scripts/analyze-credit.js.map +1 -1
  208. package/dist/scripts/analyze-orders.js +2 -2
  209. package/dist/scripts/analyze-orders.js.map +1 -1
  210. package/dist/scripts/diagnose-kibana-candles.d.ts.map +1 -1
  211. package/dist/scripts/diagnose-kibana-candles.js +0 -1
  212. package/dist/scripts/diagnose-kibana-candles.js.map +1 -1
  213. package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +1 -1
  214. package/dist/scripts/generate_market_adapter_whitelist.js +3 -2
  215. package/dist/scripts/generate_market_adapter_whitelist.js.map +1 -1
  216. package/dist/scripts/test-credit-renewal.js +1 -1
  217. package/dist/scripts/test-credit-renewal.js.map +1 -1
  218. package/dist/scripts/tv.d.ts +24 -2
  219. package/dist/scripts/tv.d.ts.map +1 -1
  220. package/dist/scripts/tv.js +190 -65
  221. package/dist/scripts/tv.js.map +1 -1
  222. package/docs/BITSHARES_ONBOARDING.md +19 -4
  223. package/docs/COW_INVARIANTS.md +18 -1
  224. package/docs/DEXBOT_COMPARISON.md +3 -3
  225. package/docs/EVOLUTION.md +9 -7
  226. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +21 -9
  227. package/docs/GRID_RECALCULATION.md +13 -13
  228. package/docs/GRID_RECONCILE.md +12 -2
  229. package/docs/MPA_CREDIT_USAGE.md +2 -2
  230. package/docs/README.md +5 -6
  231. package/docs/architecture.md +0 -58
  232. package/docs/developer_guide.md +2 -1
  233. package/docs/docker.md +1 -1
  234. package/market_adapter/README.md +17 -10
  235. package/modules/README.md +1 -1
  236. package/package.json +1 -1
  237. package/scripts/README.md +9 -7
  238. package/tests/README.md +2 -2
  239. package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +0 -2455
  240. package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +0 -218
  241. package/analysis/tradingview/h-bts_tradingview.html +0 -1570
  242. package/analysis/tradingview/t-bts_tradingview.html +0 -1570
@@ -111,7 +111,7 @@ function _snapshotFundState(manager) {
111
111
  };
112
112
  }
113
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
- import { filterOrdersByType, checkSizesBeforeMinimum, checkSizeThreshold, resolveConfiguredPriceBound, shouldFlagOutOfSpread, isOrderHealthy, isPhantomOrder, isSlotAvailable, isOrderOnChain, isOrderPlaced, hasOnChainId, isEmptyGridSlot, parseSlotIndex, calculateIdealBoundary, assignGridRoles, resolveOnChainRetypeType } from './utils/order.js';
114
+ import { filterOrdersByType, checkSizesBeforeMinimum, checkSizeThreshold, resolveConfiguredPriceBound, shouldFlagOutOfSpread, isOrderHealthy, isPhantomOrder, isSlotAvailable, isOrderOnChain, isOrderPlaced, hasOnChainId, isEmptyGridSlot, parseSlotIndex, calculateIdealBoundary, assignGridRoles, resolveOnChainRetypeType, resolveReserveCount, reserveEdgeIdSet, resolveLiveReserveEdgeAnchorPrice } from './utils/order.js';
115
115
  import { loadAmaCenterPrice, loadAmaCenterSnapshot, withBlockchainRetry } from './utils/system.js';
116
116
  import * as MathUtils from './utils/math.js';
117
117
  import { derivePriceWithPoolRef } from './utils/withPoolRef.js';
@@ -226,8 +226,8 @@ export async function _getSizingContext(manager, side, { skipRecalc = false } =
226
226
  // BTS fees are paid for ALL order operations regardless of side, so the
227
227
  // BTS-holding side reserves fees for both buy and sell target counts.
228
228
  if (budget > 0) {
229
- const targetBuy = Math.max(0, manager.config.activeOrders?.buy ?? 1);
230
- const targetSell = Math.max(0, manager.config.activeOrders?.sell ?? 1);
229
+ const targetBuy = Math.max(0, manager.config.activeOrders?.buy ?? 1) + resolveReserveCount(manager.config, 'buy');
230
+ const targetSell = Math.max(0, manager.config.activeOrders?.sell ?? 1) + resolveReserveCount(manager.config, 'sell');
231
231
  const totalTarget = targetBuy + targetSell;
232
232
  const btsOrderType = getBtsSide(manager.config?.assetA, manager.config?.assetB);
233
233
  const isBtsSide = isBuy ? (btsOrderType === ORDER_TYPES.BUY) : (btsOrderType === ORDER_TYPES.SELL);
@@ -680,6 +680,21 @@ export async function loadGrid(manager, grid, boundaryIdx = null, genesisInput =
680
680
  manager.logger?.log?.(`[GRID-LOAD] Could not re-derive a safe boundary; continuing without one. ` +
681
681
  `The next sync cycle will reconcile grid geometry against the chain.`, 'error');
682
682
  }
683
+ // The rejected value must not survive: storeMasterGrid
684
+ // never persists a null boundary, so without an explicit
685
+ // erase the poison re-arms this rejection on every boot
686
+ // (Sep-9: boundary=96 rejected identically 3 restarts in
687
+ // a row). Best-effort — load continues boundary-less
688
+ // either way and the next fill batch re-anchors live.
689
+ try {
690
+ const acct = manager?.accountOrders;
691
+ if (acct && typeof acct.clearPersistedBoundary === 'function') {
692
+ await acct.clearPersistedBoundary();
693
+ manager.logger?.log?.(`[GRID-LOAD] Erased poisoned persisted boundary (${boundaryIdx}); ` +
694
+ `restart will load boundary-less instead of re-rejecting.`, 'warn');
695
+ }
696
+ }
697
+ catch { /* load continues boundary-less either way */ }
683
698
  }
684
699
  }
685
700
  // Restore boundary index for StrategyEngine
@@ -1080,6 +1095,65 @@ export async function initializeGrid(manager) {
1080
1095
  `short of ${Math.floor(Number(minScaleSlots))} levels; holding at ${guard.minPrice.toFixed(8)}`, 'info');
1081
1096
  resolvedMinP = guard.minPrice;
1082
1097
  }
1098
+ // P4 / unanchored rebuild anchor: when the new generation is centered
1099
+ // on a static config value (not a live market source), owed fill
1100
+ // crawls carry the only fresh market direction — fold their net
1101
+ // direction into the rebuild center before the ladder is generated.
1102
+ // A live center already contains the movement, so its owed crawls are
1103
+ // dropped as a stale generation (see the clear below).
1104
+ const owedCrawls = Array.isArray(manager._pendingFillCrawls)
1105
+ ? manager._pendingFillCrawls
1106
+ : [];
1107
+ // Whether the ladder CENTER already reflects current market movement.
1108
+ // gpSource describes the gridPrice/bounds reference, NOT the center:
1109
+ // the center is gridStartPrice, built from mp (config.startPrice).
1110
+ // Only a derived startPrice (a non-numeric mode like "pool"/"book"
1111
+ // resolved to a fresh number above) or an AMA-driven center is live:
1112
+ // with gpSource === "ama" the center itself is still mp * (1 +
1113
+ // offset), but the offset comes from the live AMA snapshot, so the
1114
+ // center moves with the market. A numeric startPrice with a live
1115
+ // gridPrice still has a static center — a live gridPrice (bounds
1116
+ // reference only) does not make that static center fresh.
1117
+ const startPriceWasDerived = typeof mpRaw !== 'number' || Number.isNaN(Number(mpRaw));
1118
+ const centerIsLive = startPriceWasDerived || gpSource === 'ama';
1119
+ if (owedCrawls.length > 0 && !centerIsLive) {
1120
+ // Reserve fills never crawl (static insurance). Classify them
1121
+ // against the OLD generation (manager.orders is replaced below)
1122
+ // with the same live anchors the runtime derivation uses, so the
1123
+ // fold can never be driven by a reserve fill.
1124
+ const oldSlots = Array.from(manager.orders?.values?.() ?? []);
1125
+ const edgeAnchors = {
1126
+ buy: resolveLiveReserveEdgeAnchorPrice(manager, 'buy'),
1127
+ sell: resolveLiveReserveEdgeAnchorPrice(manager, 'sell')
1128
+ };
1129
+ const reserveBuyIds = reserveEdgeIdSet(oldSlots, manager.config, ORDER_TYPES.BUY, edgeAnchors.buy);
1130
+ const reserveSellIds = reserveEdgeIdSet(oldSlots, manager.config, ORDER_TYPES.SELL, edgeAnchors.sell);
1131
+ let netShift = 0;
1132
+ for (const e of owedCrawls) {
1133
+ if (e?.side === ORDER_TYPES.BUY && reserveBuyIds?.has(e.slotId))
1134
+ continue;
1135
+ if (e?.side === ORDER_TYPES.SELL && reserveSellIds?.has(e.slotId))
1136
+ continue;
1137
+ if (e?.side === ORDER_TYPES.SELL)
1138
+ netShift++;
1139
+ else if (e?.side === ORDER_TYPES.BUY)
1140
+ netShift--;
1141
+ }
1142
+ const stepPct = Number(manager.config?.incrementPercent);
1143
+ if (netShift !== 0 && Number.isFinite(stepPct) && stepPct > 0) {
1144
+ // One crawl moves the boundary one slot; the geometric ladder
1145
+ // step is incrementPercent, so shift the center by netShift steps.
1146
+ const folded = gridStartPrice * Math.pow(1 + stepPct / 100, netShift);
1147
+ // Re-clamp to the post-guard resolved bounds so a fold cannot
1148
+ // push the center outside the rail it is about to generate.
1149
+ const clampMin = Number.isFinite(Number(resolvedMinP)) ? Number(resolvedMinP) : rMinP;
1150
+ const clampMax = Number.isFinite(Number(resolvedMaxP)) ? Number(resolvedMaxP) : rMaxP;
1151
+ gridStartPrice = Math.max(clampMin, Math.min(clampMax, folded));
1152
+ manager.config.startPrice = gridStartPrice;
1153
+ manager.logger?.log?.(`[BOUNDARY] Folded ${owedCrawls.length} owed fill crawl(s) (net ${netShift}) into ` +
1154
+ `static grid center -> ${gridStartPrice.toFixed(8)}`, 'info');
1155
+ }
1156
+ }
1083
1157
  manager.config.minPrice = resolvedMinP;
1084
1158
  manager.config.maxPrice = resolvedMaxP;
1085
1159
  manager._lastGridPricingContext = {
@@ -1112,6 +1186,12 @@ export async function initializeGrid(manager) {
1112
1186
  manager._gapSlots = gapSlots;
1113
1187
  if (genesis)
1114
1188
  manager._genesis = genesis;
1189
+ // A rebuilt grid is a NEW generation: the boundary below is re-derived
1190
+ // absolutely from the fresh price ladder, so owed fill crawls recorded
1191
+ // against the previous generation must not survive — their relative
1192
+ // deltas would shift the new anchor again for movement it already
1193
+ // contains (and their slot ids now name different prices).
1194
+ manager._clearPendingFillCrawls?.('grid rebuild');
1115
1195
  // RC-8: Update boundary with notification to dependent systems
1116
1196
  // Persist master boundary for StrategyEngine
1117
1197
  if (manager.boundaryIdx !== boundaryIdx) {
@@ -1309,11 +1389,17 @@ export async function recalculateGrid(manager, opts) {
1309
1389
  * @returns {any}
1310
1390
  */
1311
1391
  export function checkAndUpdateGridIfNeeded(manager) {
1312
- const threshold = manager.config?.gridLimits?.GRID_REGENERATION_PERCENTAGE;
1392
+ const rawThreshold = manager.config?.gridLimits?.GRID_REGENERATION_PERCENTAGE;
1393
+ // NOTE: a configured 0 (or non-numeric) falls back to the global default
1394
+ // instead of disabling. 0 never worked as "disable" (ratio >= 0 is always
1395
+ // true, i.e. constant regen), so no working configuration is broken by this.
1396
+ const threshold = (Number.isFinite(Number(rawThreshold)) && Number(rawThreshold) > 0)
1397
+ ? Number(rawThreshold)
1398
+ : GRID_LIMITS.GRID_REGENERATION_PERCENTAGE;
1313
1399
  const chainSnap = manager.getChainFundsSnapshot();
1314
1400
  const gridBuy = Number(manager.funds?.total?.grid?.buy || 0);
1315
1401
  const gridSell = Number(manager.funds?.total?.grid?.sell || 0);
1316
- const result = { buyUpdated: false, sellUpdated: false };
1402
+ const result = { buyUpdated: false, sellUpdated: false, buyShrink: false, sellShrink: false };
1317
1403
  const sides = [
1318
1404
  { name: 'buy', grid: gridBuy, orderType: ORDER_TYPES.BUY },
1319
1405
  { name: 'sell', grid: gridSell, orderType: ORDER_TYPES.SELL }
@@ -1321,21 +1407,50 @@ export function checkAndUpdateGridIfNeeded(manager) {
1321
1407
  for (const s of sides) {
1322
1408
  if (s.grid <= 0)
1323
1409
  continue;
1324
- const availableFunds = calculateAvailableFundsValue(s.name, manager.accountTotals, manager.funds, manager.config.assetA, manager.config.assetB, manager.config.activeOrders, manager.config.min_BTS_value, manager.config.feeParams ?? null);
1410
+ const feeActiveOrders = manager.config.activeOrders && typeof manager.config.activeOrders === 'object'
1411
+ ? {
1412
+ ...manager.config.activeOrders,
1413
+ buy: Math.max(0, Number(manager.config.activeOrders.buy) || 0) + resolveReserveCount(manager.config, 'buy'),
1414
+ sell: Math.max(0, Number(manager.config.activeOrders.sell) || 0) + resolveReserveCount(manager.config, 'sell'),
1415
+ }
1416
+ : manager.config.activeOrders;
1417
+ const availableFunds = calculateAvailableFundsValue(s.name, manager.accountTotals, manager.funds, manager.config.assetA, manager.config.assetB, feeActiveOrders, manager.config.min_BTS_value, manager.config.feeParams ?? null);
1325
1418
  // Denominator: side's allocated capital (or chain total fallback).
1326
1419
  const allocated = s.name === 'buy' ? chainSnap.allocatedBuy : chainSnap.allocatedSell;
1327
1420
  const denominator = (allocated > 0) ? allocated : (s.grid + availableFunds);
1328
1421
  const ratio = (denominator > 0) ? (availableFunds / denominator) * 100 : 0;
1329
- manager.logger?.log?.(`[DIVERGENCE] ${s.name.toUpperCase()} ratio check: availableFunds=${availableFunds.toFixed(5)}, allocated=${allocated.toFixed(5)}, ratio=${ratio.toFixed(4)}% (threshold=${threshold}%) → ${ratio >= threshold ? 'TRIGGER' : 'no trigger'}`, 'debug');
1330
- if (ratio >= threshold) {
1422
+ // DOWNSIDE: grid-tracked size exceeds the (botFunds-capped) allocation,
1423
+ // so the side must shrink toward the new budget via the same resize path.
1424
+ // s.grid is funds.total.grid (ACTIVE + PARTIAL + VIRTUAL — planned size,
1425
+ // not just on-chain committed). Deliberately NOT based on per-side
1426
+ // chain-total drops: a normal fill moves value across sides (pays one
1427
+ // asset, receives the other), so one side's total routinely drops >=3%
1428
+ // on ordinary fills — and the fill pipeline already re-sizes from the
1429
+ // post-fill budget. External removal is caught here because the
1430
+ // committed/planned grid stays put while the allocation sinks.
1431
+ // NOTE: with an explicit 0 allocation the shared fallback denominator
1432
+ // yields overAlloc ≈ +100%, i.e. a 0%-funded side holding size always
1433
+ // triggers — which is the desired unwind (zero budget sizes everything
1434
+ // to 0 and the correction pass cancels the surplus; self-clearing).
1435
+ const overAlloc = (denominator > 0) ? ((s.grid - allocated) / denominator) * 100 : 0;
1436
+ const growTrigger = ratio >= threshold;
1437
+ const shrinkTrigger = overAlloc >= threshold;
1438
+ manager.logger?.log?.(`[DIVERGENCE] ${s.name.toUpperCase()} ratio check: availableFunds=${availableFunds.toFixed(5)}, allocated=${allocated.toFixed(5)}, ratio=${ratio.toFixed(4)}% (threshold=${threshold}%) → ${growTrigger ? 'TRIGGER-GROW' : 'no trigger'} | overAlloc=${overAlloc.toFixed(4)}% → ${shrinkTrigger ? 'TRIGGER-SHRINK' : 'no trigger'}`, 'debug');
1439
+ if (growTrigger || shrinkTrigger) {
1331
1440
  // RC-3: Use Set for automatic duplicate prevention
1332
1441
  if (!(manager._gridSidesUpdated instanceof Set))
1333
1442
  manager._gridSidesUpdated = new Set();
1334
1443
  manager._gridSidesUpdated.add(s.orderType);
1335
- if (s.name === 'buy')
1444
+ if (s.name === 'buy') {
1336
1445
  result.buyUpdated = true;
1337
- else
1446
+ if (shrinkTrigger)
1447
+ result.buyShrink = true;
1448
+ }
1449
+ else {
1338
1450
  result.sellUpdated = true;
1451
+ if (shrinkTrigger)
1452
+ result.sellShrink = true;
1453
+ }
1339
1454
  }
1340
1455
  }
1341
1456
  return result;
@@ -1428,7 +1543,11 @@ export async function _recalculateGridOrderSizesFromBlockchain(manager, orderTyp
1428
1543
  // geometric progression including empties, then re-type the picked slot
1429
1544
  // to BUY/SELL before placement. The COW boundary-shift path re-types
1430
1545
  // the working grid by geometry first, so crossers stay in the correct
1431
- // side's denominator.
1546
+ // side's denominator. Reserve edge slots are deliberately excluded from
1547
+ // that startup re-derivation: they activate only with the size the
1548
+ // target-grid sizing pipeline has already written (exact values, one
1549
+ // sizing rule), and an unsized reserve waits for that pipeline instead
1550
+ // of being placed with a locally guessed size.
1432
1551
  const orderSource = collectActions ? workingGrid : manager.orders;
1433
1552
  const allSideSlots = Array.from(orderSource.values())
1434
1553
  .filter((o) => o.type === orderType)
@@ -1447,6 +1566,15 @@ export async function _recalculateGridOrderSizesFromBlockchain(manager, orderTyp
1447
1566
  // Apply new sizes to all slots on the side
1448
1567
  for (let i = 0; i < allSideSlots.length; i++) {
1449
1568
  const slot = allSideSlots[i];
1569
+ // Shelf/manual orders (non-slot-N ids, e.g. fork-kept deep-*
1570
+ // below the rail) sit outside window accounting: geometric
1571
+ // ideals must never resize them on-chain. Same gate as reserve
1572
+ // classification and startup cancel candidates (issue #27
1573
+ // follow-up). No-op upstream (grids only mint slot-N).
1574
+ // NB: they stay in the denominator above (budget math
1575
+ // unchanged); only the per-slot mutation below is skipped.
1576
+ if (parseSlotIndex(slot?.id) === null)
1577
+ continue;
1450
1578
  let newSize = newSizes[i] || 0;
1451
1579
  // FUND CAPPING FOR COMMITTED (ON-CHAIN) ORDERS:
1452
1580
  // Only ACTIVE/PARTIAL orders are constrained by available funds.
@@ -1744,8 +1872,8 @@ export async function monitorDivergence(manager, calculatedGrid, persistedGrid)
1744
1872
  const { getOrderTypeFromUpdatedFlags } = require('./utils/order');
1745
1873
  return {
1746
1874
  needsUpdate: true,
1747
- buy: { updated: ratioResult.buyUpdated, ratio: ratioResult.buyUpdated, rms: false, metric: 0 },
1748
- sell: { updated: ratioResult.sellUpdated, ratio: ratioResult.sellUpdated, rms: false, metric: 0 },
1875
+ buy: { updated: ratioResult.buyUpdated, ratio: ratioResult.buyUpdated, rms: false, metric: 0, shrink: ratioResult.buyShrink === true },
1876
+ sell: { updated: ratioResult.sellUpdated, ratio: ratioResult.sellUpdated, rms: false, metric: 0, shrink: ratioResult.sellShrink === true },
1749
1877
  orderType: getOrderTypeFromUpdatedFlags(ratioResult.buyUpdated, ratioResult.sellUpdated)
1750
1878
  };
1751
1879
  }
@@ -1756,8 +1884,8 @@ export async function monitorDivergence(manager, calculatedGrid, persistedGrid)
1756
1884
  const { getOrderTypeFromUpdatedFlags } = require('./utils/order');
1757
1885
  return {
1758
1886
  needsUpdate: buyUpdated || sellUpdated,
1759
- buy: { updated: buyUpdated, ratio: ratioResult.buyUpdated, rms: rmsResult.buy.updated, metric: rmsResult.buy.metric },
1760
- sell: { updated: sellUpdated, ratio: ratioResult.sellUpdated, rms: rmsResult.sell.updated, metric: rmsResult.sell.metric },
1887
+ buy: { updated: buyUpdated, ratio: ratioResult.buyUpdated, rms: rmsResult.buy.updated, metric: rmsResult.buy.metric, shrink: ratioResult.buyShrink === true },
1888
+ sell: { updated: sellUpdated, ratio: ratioResult.sellUpdated, rms: rmsResult.sell.updated, metric: rmsResult.sell.metric, shrink: ratioResult.sellShrink === true },
1761
1889
  orderType: getOrderTypeFromUpdatedFlags(buyUpdated, sellUpdated)
1762
1890
  };
1763
1891
  }
@@ -1768,14 +1896,21 @@ export async function monitorDivergence(manager, calculatedGrid, persistedGrid)
1768
1896
  * @returns {{onChainBuys: Array<import('./types').Order>, onChainSells: Array<import('./types').Order>}}
1769
1897
  */
1770
1898
  function _getOnChainOrders(manager) {
1899
+ // Slot-N gated: fork-kept shelf/manual orders (non-slot-N ids, e.g.
1900
+ // deep-*) sit outside window accounting — same gate as reserve
1901
+ // classification and startup cancel candidates (issue #27 follow-up).
1902
+ // Without this, a live shelf masks an empty window side (oneSideEmpty
1903
+ // stays false) and skews the spread inputs. No-op on grids that only
1904
+ // mint slot-N ids.
1905
+ const isGridSlot = (o) => o?.orderId && Number(o?.size || 0) > 0 && parseSlotIndex(o?.id) !== null;
1771
1906
  const onChainBuys = [
1772
1907
  ...manager.getOrdersByTypeAndState(ORDER_TYPES.BUY, ORDER_STATES.ACTIVE),
1773
1908
  ...manager.getOrdersByTypeAndState(ORDER_TYPES.BUY, ORDER_STATES.PARTIAL)
1774
- ].filter((o) => o?.orderId && Number(o?.size || 0) > 0);
1909
+ ].filter(isGridSlot);
1775
1910
  const onChainSells = [
1776
1911
  ...manager.getOrdersByTypeAndState(ORDER_TYPES.SELL, ORDER_STATES.ACTIVE),
1777
1912
  ...manager.getOrdersByTypeAndState(ORDER_TYPES.SELL, ORDER_STATES.PARTIAL)
1778
- ].filter((o) => o?.orderId && Number(o?.size || 0) > 0);
1913
+ ].filter(isGridSlot);
1779
1914
  return { onChainBuys, onChainSells };
1780
1915
  }
1781
1916
  /**
@@ -1847,11 +1982,11 @@ export async function checkSpreadCondition(manager, _BitShares, updateOrdersOnCh
1847
1982
  const toleranceSteps = 0.5;
1848
1983
  const buyCount = manager.getOrdersByTypeAndState(ORDER_TYPES.BUY, ORDER_STATES.ACTIVE)
1849
1984
  .concat(manager.getOrdersByTypeAndState(ORDER_TYPES.BUY, ORDER_STATES.PARTIAL))
1850
- .filter((o) => o?.orderId && Number(o?.size || 0) > 0)
1985
+ .filter((o) => o?.orderId && Number(o?.size || 0) > 0 && parseSlotIndex(o?.id) !== null)
1851
1986
  .length;
1852
1987
  const sellCount = manager.getOrdersByTypeAndState(ORDER_TYPES.SELL, ORDER_STATES.ACTIVE)
1853
1988
  .concat(manager.getOrdersByTypeAndState(ORDER_TYPES.SELL, ORDER_STATES.PARTIAL))
1854
- .filter((o) => o?.orderId && Number(o?.size || 0) > 0)
1989
+ .filter((o) => o?.orderId && Number(o?.size || 0) > 0 && parseSlotIndex(o?.id) !== null)
1855
1990
  .length;
1856
1991
  manager.outOfSpread = shouldFlagOutOfSpread(currentSpread, nominalSpread, toleranceSteps, buyCount, sellCount, manager.config.incrementPercent);
1857
1992
  if (manager.outOfSpread === 0)
@@ -1890,7 +2025,12 @@ export async function checkSpreadCondition(manager, _BitShares, updateOrdersOnCh
1890
2025
  }
1891
2026
  // Limit spread = nominal + half increment tolerance (0.5 steps).
1892
2027
  const limitSpread = nominalSpread + (manager.config.incrementPercent * toleranceSteps);
1893
- manager.logger?.log?.(`Spread too wide (${Format.formatPercent(currentSpread)} > ${Format.formatPercent(limitSpread)}), correcting with ${manager.outOfSpread} extra slot(s)...`, 'warn');
2028
+ // One-sided book: currentSpread is Infinity (no opposing quote), so
2029
+ // log the empty side instead of a bogus "0% > limit" comparison.
2030
+ const spreadDesc = oneSideEmpty
2031
+ ? `one-sided (${onChainBuys.length === 0 ? 'no buys' : 'no sells'})`
2032
+ : `${Format.formatPercent(currentSpread)} > ${Format.formatPercent(limitSpread)}`;
2033
+ manager.logger?.log?.(`Spread too wide (${spreadDesc}), correcting with ${manager.outOfSpread} extra slot(s)...`, 'warn');
1894
2034
  // Refresh funds before the side decision below: the top-of-tick recalc may
1895
2035
  // predate fills processed since, and determineOrderSideByFunds reads
1896
2036
  // manager.funds/accountTotals directly (not via _getSizingContext). No-op
@@ -2384,11 +2524,16 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2384
2524
  const missingSlots = configuredMissingSlots > 0
2385
2525
  ? Math.floor(configuredMissingSlots)
2386
2526
  : 1;
2387
- // STRATEGY: Edge-Based Correction (Safe Bridging)
2527
+ // STRATEGY: Window-Contiguous Correction (Safe Bridging)
2388
2528
  // Instead of calculating a "mid-price" (which can be dangerous in wide gaps),
2389
- // we strictly target the orders closest to the spread gap.
2529
+ // we extend the live window contiguously so a fund-constrained correction
2530
+ // never leaves an interior hole.
2390
2531
  // 1. Priority: Update existing PARTIAL orders at the edge (Highest Buy / Lowest Sell).
2391
- // 2. Fallback: Activate SPREAD slots at the edge (Lowest Spread for Buy / Highest Spread for Sell).
2532
+ // 2. Fallback: Activate empty slots window-contiguous-first (Lowest Buy-rail /
2533
+ // Highest Sell-rail, adjacent to the live window top). A rail with no
2534
+ // live orders has no window to extend, so it falls back to
2535
+ // spread-edge-first (Highest Buy / Lowest Sell) to close the spread
2536
+ // near market first.
2392
2537
  const allOrders = Array.from(manager.orders.values());
2393
2538
  // Boundary-correct type computation — hoisted before edge-partial and
2394
2539
  // candidate filters so all call sites (including the edge-partial filter
@@ -2441,11 +2586,30 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2441
2586
  // boundary-correct type so a SPREAD slot that, after a boundary shift, now sits
2442
2587
  // in the BUY or SELL zone is excluded — it would otherwise be placed on the
2443
2588
  // correction side at a price the grid already considers the opposite side.
2589
+ // Candidate ordering: window-contiguous-first when the rail has live
2590
+ // orders to extend (BUY lowest first, SELL highest first, both adjacent
2591
+ // to the live window top). A fully-empty rail has no window anchor —
2592
+ // "lowest buy" would be the rail bottom, placing deep orders while the
2593
+ // near-market gap stays open — so it falls back to spread-edge-first
2594
+ // (BUY highest, SELL lowest) to close the spread near market first.
2595
+ // The gap-band promotion path below stays edge-first by necessity
2596
+ // (boundary derivation requires contiguity), so under fund shortage
2597
+ // in-rail holes heal before band slots.
2598
+ const railHasLiveOrders = allOrders.some((o) => getSlotCorrectType(o) === railType && isOrderPlaced(o));
2599
+ const sortCandidates = (a, b) => {
2600
+ const edgeFirst = railType === ORDER_TYPES.BUY
2601
+ ? b.price - a.price
2602
+ : a.price - b.price;
2603
+ const windowFirst = railType === ORDER_TYPES.BUY
2604
+ ? a.price - b.price
2605
+ : b.price - a.price;
2606
+ return railHasLiveOrders ? windowFirst : edgeFirst;
2607
+ };
2444
2608
  const typedSpreadCandidates = allOrders
2445
2609
  .filter((o) => o.type === ORDER_TYPES.SPREAD
2446
2610
  && isSlotAvailable(o)
2447
2611
  && getSlotCorrectType(o) === railType)
2448
- .sort((a, b) => railType === ORDER_TYPES.BUY ? a.price - b.price : b.price - a.price)
2612
+ .sort(sortCandidates)
2449
2613
  .slice(0, missingSlots);
2450
2614
  // Secondary candidates: orphaned virtual slots that have lost their
2451
2615
  // order (e.g. stale-cleaned after a race condition during a crash, or a
@@ -2477,7 +2641,7 @@ export async function prepareSpreadCorrectionOrders(manager, preferredSide, outO
2477
2641
  && o.state === ORDER_STATES.VIRTUAL
2478
2642
  && !o.orderId
2479
2643
  && getSlotCorrectType(o) === railType)
2480
- .sort((a, b) => railType === ORDER_TYPES.BUY ? b.price - a.price : a.price - b.price)
2644
+ .sort(sortCandidates)
2481
2645
  .slice(0, missingSlots);
2482
2646
  // If the funded rail is full, the spread itself may be stale: the
2483
2647
  // nearest empty slots are still in the gap band. Promote contiguous