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
@@ -27,9 +27,9 @@ import SyncEngine from './sync_engine.js';
27
27
  import { calculateCurrentSpread, checkSpreadCondition, checkGridHealth } from './grid.js';
28
28
  import * as Format from './format.js';
29
29
  import { ORDER_TYPES, ORDER_STATES, REBALANCE_STATES, DEFAULT_CONFIG, TIMING, LOG_LEVEL, PIPELINE_TIMING, COW_PERFORMANCE, COW_ACTIONS, GRID_LIMITS } from '../constants.js';
30
- import { getMinOrderSize, computeChainFundTotals, hasValidAccountTotals, resolveConfigValueWithRegistry, isExplicitZeroAllocation, floatToBlockchainInt, validateBoundaryCommit, resolveGapSlots } from './utils/math.js';
30
+ import { getMinOrderSize, computeChainFundTotals, hasValidAccountTotals, resolveConfigValueWithRegistry, isExplicitZeroAllocation, floatToBlockchainInt, validateBoundaryCommit, resolveGapSlots, resolveGapBand, isSlotInRail } from './utils/math.js';
31
31
  import { validateOrder, validateGridForPersistence, validateWorkingGridFunds, checkFundDrift, reconcileGrid, optimizeRebalanceActions, projectTargetToWorkingGrid, buildStateUpdates, buildAbortedResult, buildSuccessResult, evaluateCommit } from './utils/validate.js';
32
- import { resolveSpreadOrderSide, parseSlotIndex, parseChainOrder, geometryTypeForSlotIndex, isOrderOnChain } from './utils/order.js';
32
+ import { resolveSpreadOrderSide, parseSlotIndex, parseChainOrder, geometryTypeForSlotIndex, isOrderOnChain, resolveReserveCount, resolveLiveReserveEdgeAnchorPrice, compareReserveEdge, collectRefillSlotIds } from './utils/order.js';
33
33
  import { getErrorMessage } from '../utils/errors.js';
34
34
  const { toFiniteNumber } = Format;
35
35
  // ===============================================================================
@@ -133,7 +133,7 @@ class COWRebalanceEngine {
133
133
  this.assets = deps.assets;
134
134
  this.config = deps.config;
135
135
  }
136
- async execute({ masterGrid, gridVersion, boundaryIdx, funds, fills = [], excludeIds = new Set(), gapSlots = null, evacStreaks = null }) {
136
+ async execute({ masterGrid, gridVersion, boundaryIdx, funds, fills = [], excludeIds = new Set(), gapSlots = null, evacStreaks = null, reserveEdgeAnchors = null }) {
137
137
  const startTime = Date.now();
138
138
  const workingGrid = new WorkingGrid(masterGrid, { baseVersion: gridVersion });
139
139
  const strategyParams = {
@@ -146,6 +146,27 @@ class COWRebalanceEngine {
146
146
  currentBoundaryIdx: boundaryIdx
147
147
  };
148
148
  const { targetGrid, boundaryIdx: targetBoundary } = this.strategy.calculateTargetGrid(strategyParams);
149
+ // P4 / unanchored plan: the strategy returns a null boundary paired
150
+ // with an EMPTY target grid only when no numeric center exists to
151
+ // recover from (e.g. GRID-LOAD rejected the persisted boundary AND
152
+ // startPrice is an unresolved mode string). Planning rotations here
153
+ // would fabricate rail-edge geometry — dust-cancel fills included,
154
+ // even though they deliberately carry isDelayedRotationTrigger when
155
+ // the boundary is known. Route to a structural resync instead of
156
+ // broadcasting. (A null boundary with a non-empty target is a
157
+ // hand-built/boundary-less plan — e.g. unit mocks — and proceeds.)
158
+ if ((targetBoundary === null || targetBoundary === undefined) && targetGrid.size === 0) {
159
+ // A genuinely empty grid (no master orders, no fills) has nothing
160
+ // to heal — plain abort. Anything else is stranded without an
161
+ // anchor and needs a rebuild.
162
+ const needsHeal = (masterGrid?.size ?? 0) > 0 || (fills?.length ?? 0) > 0;
163
+ this.logger?.log('[COW] Plan skipped: boundary unrecoverable (no numeric center)' + (needsHeal ? '; requesting structural resync' : ''), 'warn');
164
+ return {
165
+ ...buildAbortedResult('boundary-unrecoverable'),
166
+ evacReady: [],
167
+ ...(needsHeal ? { needsResync: true, resyncReason: 'boundary-unrecoverable' } : {}),
168
+ };
169
+ }
149
170
  let dustThresholdPercent = this.config?.gridLimits?.PARTIAL_DUST_THRESHOLD_PERCENTAGE;
150
171
  const reconcileResult = reconcileGrid(masterGrid, targetGrid, targetBoundary, {
151
172
  logger: (msg, level) => this.logger?.log(msg, level),
@@ -194,14 +215,33 @@ class COWRebalanceEngine {
194
215
  optimizedActions.length = 0;
195
216
  optimizedActions.push(...guarded);
196
217
  }
218
+ // Rail-edge truncation telemetry: when the planned window runs off
219
+ // the rail (sellStart past the last slot), one side can never place
220
+ // — the Sep-10 signature (boundary 209 on a 216-slot rail left 2
221
+ // sell slots, and the ordinal pairing then teleported the buy rail
222
+ // 113 slots). Warn only: the cross-guard, fund validation and
223
+ // boundary-hold still judge the plan; no geometry is refused here.
224
+ {
225
+ const railSize = targetGrid?.size ?? 0;
226
+ const gap = Number(gapSlots) || 0;
227
+ const sellStart = Number(targetBoundary) + gap + 1;
228
+ if (Number.isFinite(sellStart) && railSize > 0 && sellStart >= railSize) {
229
+ this.logger?.log(`[COW] Rail-edge plan: boundary=${targetBoundary} gap=${gap} rail=${railSize} (sellStart=${sellStart}); window truncated, guards still apply`, 'warn');
230
+ }
231
+ }
197
232
  // Refill-slot wire (boundary-hold): surviving hole-CREATEs justify this
198
233
  // plan's boundary shift (folded CANCEL+CREATE pairs already became
199
234
  // stamped UPDATEs above; stale-dropped placements are excluded — a
200
235
  // deferred placement justifies nothing). The executor holds the
201
236
  // committed boundary when a listed refill is guard-skipped.
202
- const refillSlotIds = optimizedActions
203
- .filter((a) => a?.type === COW_ACTIONS.CREATE && typeof a?.id === 'string' && a.id.length > 0)
204
- .map((a) => a.id);
237
+ // Reserve-ladder CREATEs are excluded (collectRefillSlotIds): reserves
238
+ // are static edge insurance and their fills never crawl, so a skipped
239
+ // reserve must not pin geometry either.
240
+ const refillSlotIds = collectRefillSlotIds(optimizedActions, {
241
+ config: this.config,
242
+ slots: masterGrid,
243
+ edgeAnchors: reserveEdgeAnchors
244
+ });
205
245
  projectTargetToWorkingGrid(workingGrid, targetGrid, { actions: optimizedActions });
206
246
  const precisions = {
207
247
  buyPrecision: this.assets?.assetB?.precision,
@@ -380,12 +420,20 @@ class OrderManager {
380
420
  _gridDirtyAt;
381
421
  _lastStaleTotalsWarnAt;
382
422
  _orphanFillsCreditedAt;
423
+ _fundDriftLedger;
383
424
  _pendingRecovery;
425
+ _pendingFillCrawls;
384
426
  _recentFillKeysSnapshot;
385
427
  _lastFilledBuyPrice;
386
428
  _lastFilledSellPrice;
387
429
  _lastFilledPrice;
388
430
  _lastFilledType;
431
+ _lastFilledAt;
432
+ _deferredRebalanceAt;
433
+ _lastHeldPlanSignature;
434
+ _heldPlanSuppressionCount;
435
+ _onBroadcastRegionEndListeners;
436
+ _lastBoundaryHoldResyncAt;
389
437
  _gapEvacStreaks;
390
438
  _gapEvacCancelQueued;
391
439
  // anchor fields removed
@@ -493,13 +541,21 @@ class OrderManager {
493
541
  this._gapSlots = 0;
494
542
  this._gridDirtyAt = null;
495
543
  this._orphanFillsCreditedAt = null;
544
+ this._fundDriftLedger = null;
496
545
  this._pendingRecovery = null;
546
+ this._pendingFillCrawls = [];
497
547
  this._recentFillKeysSnapshot = null;
498
548
  this._lastStaleTotalsWarnAt = {};
499
549
  this._lastFilledBuyPrice = null;
500
550
  this._lastFilledSellPrice = null;
501
551
  this._lastFilledPrice = null;
502
552
  this._lastFilledType = null;
553
+ this._lastFilledAt = 0;
554
+ this._deferredRebalanceAt = 0;
555
+ this._lastHeldPlanSignature = null;
556
+ this._heldPlanSuppressionCount = 0;
557
+ this._onBroadcastRegionEndListeners = [];
558
+ this._lastBoundaryHoldResyncAt = 0;
503
559
  this._gapEvacStreaks = new Map();
504
560
  this._gapEvacCancelQueued = new Set();
505
561
  this._metrics = {
@@ -645,6 +701,11 @@ class OrderManager {
645
701
  // The caller is released from the refcount contract.
646
702
  this._broadcastingFlag = 0;
647
703
  this._broadcastingStartedAt = 0;
704
+ // A leaked flag never reaches stopBroadcasting, so wake the same
705
+ // region-end listeners it would have: without this, fills
706
+ // enqueued during the hung region are stranded until the next
707
+ // fill event even though the flag is now clear.
708
+ this._fireBroadcastRegionEnd();
648
709
  }
649
710
  }
650
711
  }
@@ -717,8 +778,57 @@ class OrderManager {
717
778
  // this flag is held and its defer branch returns without
718
779
  // rescheduling, so fills enqueued during a long region would
719
780
  // starve indefinitely. Fire once when the last region ends.
781
+ this._fireBroadcastRegionEnd();
782
+ }
783
+ }
784
+ /**
785
+ * Notify runtime listeners that the last broadcast/placement region ended.
786
+ * Called from stopBroadcasting (refcount -> 0) and the stale-flag watchdog
787
+ * (a leaked flag hard-reset to 0). The fill consumer's defer branch returns
788
+ * without rescheduling, so this is the only wake-up for fills enqueued
789
+ * during a region; the watchdog path must fire it too or a hung broadcast's
790
+ * queued fills wait for the next fill event.
791
+ * Fans out to the legacy single listener plus every registered listener
792
+ * (see addBroadcastRegionEndListener): a second wirer must never silently
793
+ * displace the fill-queue drain. Guarded so a listener error never escapes
794
+ * into a finally/watchdog frame.
795
+ * @returns {void}
796
+ */
797
+ _fireBroadcastRegionEnd() {
798
+ try {
720
799
  this._onBroadcastRegionEnd?.();
721
800
  }
801
+ catch (err) {
802
+ this.logger?.log?.(`[BROADCAST] Region-end hook failed: ${err?.message || err}`, 'warn');
803
+ }
804
+ const listeners = this._onBroadcastRegionEndListeners;
805
+ if (Array.isArray(listeners)) {
806
+ for (const listener of listeners) {
807
+ try {
808
+ listener?.();
809
+ }
810
+ catch (err) {
811
+ this.logger?.log?.(`[BROADCAST] Region-end listener failed: ${err?.message || err}`, 'warn');
812
+ }
813
+ }
814
+ }
815
+ }
816
+ /**
817
+ * Register an additional broadcast-region-end listener without displacing
818
+ * the legacy single listener or previously registered ones. Duplicate
819
+ * registrations of the same function reference are ignored.
820
+ * @param {() => void} listener
821
+ * @returns {void}
822
+ */
823
+ addBroadcastRegionEndListener(listener) {
824
+ if (typeof listener !== 'function')
825
+ return;
826
+ if (!Array.isArray(this._onBroadcastRegionEndListeners)) {
827
+ this._onBroadcastRegionEndListeners = [];
828
+ }
829
+ const listeners = this._onBroadcastRegionEndListeners;
830
+ if (!listeners.includes(listener))
831
+ listeners.push(listener);
722
832
  }
723
833
  /**
724
834
  * Reset funds structure to zeroed values. Acquires _fundLock so the
@@ -1147,7 +1257,14 @@ class OrderManager {
1147
1257
  if (options === null || typeof options !== 'object' || Array.isArray(options)) {
1148
1258
  throw new TypeError('Commit options must be an object');
1149
1259
  }
1150
- return { skipRecalc: options.skipRecalc === true };
1260
+ return {
1261
+ skipRecalc: options.skipRecalc === true,
1262
+ // Set by the COW refill hold: the boundary passed to this commit is
1263
+ // the COMMITTED value, not the plan's target, so nothing was
1264
+ // derived into it and owed fill crawls must survive (see the
1265
+ // pending-crawl bookkeeping in _commitWorkingGrid).
1266
+ boundaryHeld: options.boundaryHeld === true
1267
+ };
1151
1268
  }
1152
1269
  async _updateOrder(order, context = 'updateOrder', options = {}) {
1153
1270
  const updateOptions = this._normalizeOrderUpdateOptions(options);
@@ -1354,14 +1471,23 @@ class OrderManager {
1354
1471
  * @param {Object} [options]
1355
1472
  * @returns {Promise<any>}
1356
1473
  */
1357
- async processFilledOrders(orders, excl, _options) {
1474
+ async processFilledOrders(orders, excl, options = {}) {
1358
1475
  // Step 1: Handle Fills (Accounting & State Updates)
1359
1476
  await this.strategy.processFillsOnly(orders, excl);
1360
1477
  // Step 2: Trigger Safe Rebalance only for actual fills.
1361
1478
  const triggerFills = orders.filter((f) => !f.isPartial || f.isDelayedRotationTrigger);
1362
1479
  const shouldRebalance = triggerFills.length > 0;
1363
1480
  if (shouldRebalance) {
1364
- const rebalanceResult = await this.performSafeRebalance(orders, excl);
1481
+ // deferIfBroadcasting: fill processing runs under
1482
+ // _fillProcessingLock, and _awaitBroadcastIdle can sleep 30s —
1483
+ // beyond the lock's 20s acquisition timeout. Defer the rebalance
1484
+ // instead of waiting in-lock; processFillsOnly already recorded
1485
+ // the boundary crawls, so a later derivation applies them. The
1486
+ // region-end hook schedules the deferred rebalance.
1487
+ const rebalanceResult = await this.performSafeRebalance(orders, excl, {
1488
+ ...(options || {}),
1489
+ deferIfBroadcasting: true
1490
+ });
1365
1491
  // Carry the fill set + exclusions on the result so the COW batch
1366
1492
  // executor can re-plan once from fresh master if the plan goes
1367
1493
  // stale between planning and broadcast.
@@ -1396,11 +1522,21 @@ class OrderManager {
1396
1522
  const buyPrecision = this.assets?.assetB?.precision;
1397
1523
  const minSellSizeInt = floatToBlockchainInt(minSellSize, sellPrecision);
1398
1524
  const minBuySizeInt = floatToBlockchainInt(minBuySize, buyPrecision);
1525
+ // Rail membership by geometry (shared isSlotInRail), mirroring the
1526
+ // reconcile pickers (_pickVirtualSlotsToActivate / _pickEdgeReserveSlots).
1527
+ // A freshly generated grid's roles never need this, but the gate keeps
1528
+ // the startup placement path and the runtime activation path from
1529
+ // diverging: a slot whose stored type is stale relative to the current
1530
+ // boundary must not be placed on the wrong rail.
1531
+ const resolved = resolveGapBand(this);
1532
+ const inRailFor = (type, o) => isSlotInRail(resolved.boundaryIdx, resolved.gapSlots, type, o);
1399
1533
  // Get closest virtual sells (lowest prices first = closest to market),
1400
1534
  // selecting up to sellCount orders that pass the minimum-size filter.
1401
1535
  // Scan the full sorted rail instead of slicing first so sub-min slots
1402
1536
  // don't consume activation budget (mirrors _pickVirtualSlotsToActivate).
1403
1537
  const sellsClosestFirst = this.getOrdersByTypeAndState(ORDER_TYPES.SELL, ORDER_STATES.VIRTUAL)
1538
+ .filter((o) => inRailFor(ORDER_TYPES.SELL, o))
1539
+ .filter((o) => parseSlotIndex(o?.id) !== null)
1404
1540
  .sort((a, b) => a.price - b.price);
1405
1541
  const validSells = [];
1406
1542
  for (const o of sellsClosestFirst) {
@@ -1415,6 +1551,8 @@ class OrderManager {
1415
1551
  // Get closest virtual buys (highest prices first = closest to market),
1416
1552
  // selecting up to buyCount orders that pass the minimum-size filter.
1417
1553
  const buysClosestFirst = this.getOrdersByTypeAndState(ORDER_TYPES.BUY, ORDER_STATES.VIRTUAL)
1554
+ .filter((o) => inRailFor(ORDER_TYPES.BUY, o))
1555
+ .filter((o) => parseSlotIndex(o?.id) !== null)
1418
1556
  .sort((a, b) => b.price - a.price);
1419
1557
  const validBuys = [];
1420
1558
  for (const o of buysClosestFirst) {
@@ -1426,7 +1564,37 @@ class OrderManager {
1426
1564
  }
1427
1565
  // Reverse for placement order (lowest first)
1428
1566
  validBuys.sort((a, b) => a.price - b.price);
1429
- return [...validSells, ...validBuys];
1567
+ // Reserve ladder: edge-pinned orders activate alongside the window
1568
+ // without consuming its budget. Buys pin at the floor (lowest first),
1569
+ // sells at the ceiling (highest first). Same min-size gate as the window.
1570
+ const pickEdgeReserves = (orderType, count, windowed, precision, minSizeInt, ascending) => {
1571
+ const picked = [];
1572
+ if (count <= 0)
1573
+ return picked;
1574
+ const windowedIds = new Set(windowed.map((o) => o.id));
1575
+ // Both edges anchor at the live grid's own edge (ladder/rail extreme).
1576
+ const edgeAnchor = resolveLiveReserveEdgeAnchorPrice(this, ascending ? 'buy' : 'sell');
1577
+ const edge = ascending ? 'floor' : 'ceiling';
1578
+ const edgeFirst = this.getOrdersByTypeAndState(orderType, ORDER_STATES.VIRTUAL)
1579
+ .filter((o) => inRailFor(orderType, o))
1580
+ .filter((o) => parseSlotIndex(o.id) !== null)
1581
+ .sort((a, b) => compareReserveEdge(a, b, edge, edgeAnchor));
1582
+ for (const o of edgeFirst) {
1583
+ if (picked.length >= count)
1584
+ break;
1585
+ if (windowedIds.has(o.id))
1586
+ continue;
1587
+ if (floatToBlockchainInt(o.size, precision) >= minSizeInt) {
1588
+ picked.push(o);
1589
+ windowedIds.add(o.id);
1590
+ }
1591
+ }
1592
+ picked.sort((a, b) => ascending ? a.price - b.price : b.price - a.price);
1593
+ return picked;
1594
+ };
1595
+ const reserveBuys = pickEdgeReserves(ORDER_TYPES.BUY, resolveReserveCount(this.config, 'buy'), validBuys, buyPrecision, minBuySizeInt, true);
1596
+ const reserveSells = pickEdgeReserves(ORDER_TYPES.SELL, resolveReserveCount(this.config, 'sell'), validSells, sellPrecision, minSellSizeInt, false);
1597
+ return [...validSells, ...reserveSells, ...validBuys, ...reserveBuys];
1430
1598
  }
1431
1599
  /**
1432
1600
  * Get orders matching the specified type and state.
@@ -1479,6 +1647,34 @@ class OrderManager {
1479
1647
  this.boundaryIdx = newIdx;
1480
1648
  this._logBoundaryDebug('restore', newIdx);
1481
1649
  }
1650
+ /**
1651
+ * Drop owed fill-crawl records (boundary bookkeeping).
1652
+ *
1653
+ * Records are pushed per shift-eligible fill at intake and consumed when a
1654
+ * derivation commits them. They must be dropped only when the crawl they
1655
+ * encode has been applied — an accepted commit that used the PLAN's target
1656
+ * boundary (or an absolute fill anchor that subsumes every owed delta).
1657
+ * A held boundary (refill guard skip), a gate-rejected boundary, and a
1658
+ * null boundary all leave the records owed, so they are not dropped there.
1659
+ *
1660
+ * Also used when the boundary is (re)anchored outside the fill path (grid
1661
+ * reset / recenter / rejected-snapshot rebuild): those records belong to
1662
+ * the previous generation, and applying their relative deltas onto a fresh
1663
+ * anchor would double-count movement the anchor already contains.
1664
+ *
1665
+ * Marks the grid dirty so the cleared array reaches disk even on paths
1666
+ * that do not otherwise persist.
1667
+ *
1668
+ * @param {string} reason - Log/debug label for the drop
1669
+ */
1670
+ _clearPendingFillCrawls(reason = 'unspecified') {
1671
+ if (!Array.isArray(this._pendingFillCrawls) || this._pendingFillCrawls.length === 0)
1672
+ return;
1673
+ const count = this._pendingFillCrawls.length;
1674
+ this._pendingFillCrawls = [];
1675
+ this._markGridDirty();
1676
+ this.logger?.log?.(`[BOUNDARY] Dropped ${count} owed fill crawl(s) (${reason})`, 'debug');
1677
+ }
1482
1678
  /**
1483
1679
  * Observability (fix #4, docs/CONSOLIDATED_ORPHAN_FIX_SUMMARY.md §2): emit the live
1484
1680
  * boundary whenever roles are (re)assigned, so a silent boundary crawl that
@@ -1549,6 +1745,7 @@ class OrderManager {
1549
1745
  this._lastFilledBuyPrice = price;
1550
1746
  else if (f.type === ORDER_TYPES.SELL)
1551
1747
  this._lastFilledSellPrice = price;
1748
+ this._lastFilledAt = Date.now();
1552
1749
  recorded++;
1553
1750
  lastKind = f?.isPartial === true ? 'partial' : (f?.isPartial === false ? 'full' : 'unknown');
1554
1751
  lastPriceSrc = priceSrc;
@@ -1912,12 +2109,60 @@ class OrderManager {
1912
2109
  */
1913
2110
  async performSafeRebalance(fills = [], excludeIds = new Set(), options = {}) {
1914
2111
  this.logger.log("[SAFE-REBALANCE] Starting with COW...", "info");
2112
+ // A fresh fill set re-enables planning and ends any held-plan
2113
+ // suppression run counted below; the next fill-less streak counts
2114
+ // from zero. Reset on entry (even for broadcast-deferred calls):
2115
+ // the fills arrived, so the identical-plan assumption is void.
2116
+ if (fills.length > 0 && (this._heldPlanSuppressionCount || 0) > 0) {
2117
+ this._heldPlanSuppressionCount = 0;
2118
+ }
1915
2119
  if (!options?.skipBroadcastWait) {
1916
- const idle = await this._awaitBroadcastIdle();
1917
- if (idle.shutdown || this._shuttingDown) {
2120
+ // Shutdown guard applies to every path here (including the
2121
+ // deferIfBroadcasting defer path, which never reaches
2122
+ // _awaitBroadcastIdle): a rebalance must not plan/broadcast while
2123
+ // the bot is shutting down. _awaitBroadcastIdle returns
2124
+ // shutdown:true for this, so keep it explicit when skipping it.
2125
+ if (this._shuttingDown) {
1918
2126
  this.logger.log('[SAFE-REBALANCE] Aborting rebalance: shutdown in progress', 'warn');
1919
2127
  return buildAbortedResult('Shutdown in progress — safe rebalance aborted');
1920
2128
  }
2129
+ // Never sleep on the broadcast flag while holding
2130
+ // _fillProcessingLock (the lock's acquisition timeout is 20s; the
2131
+ // wait is 30s). A fill-driven rebalance defers instead, leaving
2132
+ // the crawls recorded by processFillsOnly for a later derivation.
2133
+ if (options?.deferIfBroadcasting && this.isBroadcastingActive()) {
2134
+ this.logger.log('[SAFE-REBALANCE] Deferred: broadcast/placement activity active; rebalance retries after the region ends', 'info');
2135
+ this._deferredRebalanceAt = Date.now();
2136
+ return { ...buildAbortedResult('broadcast-active-deferred'), deferred: true };
2137
+ }
2138
+ // Identical-held-plan suppression: a fill-less replan whose
2139
+ // boundary, guard pivot and fill timestamp are unchanged since the
2140
+ // last hold can only reproduce the same vetoed plan (INV-COW-007).
2141
+ // Defer instead of spending another broadcast region re-deriving
2142
+ // it; a fresh fill changes _lastFilledAt and re-enables planning.
2143
+ const heldSig = this._lastHeldPlanSignature;
2144
+ if (fills.length === 0 && heldSig
2145
+ && Number(this.boundaryIdx) === Number(heldSig.boundaryIdx)
2146
+ && Number(this._lastFilledPrice) === Number(heldSig.pivot)
2147
+ && Number(this._lastFilledAt) === Number(heldSig.fillsAt)) {
2148
+ // Counted visibility: a fill-less replan is suppressed until a
2149
+ // fresh fill arrives, so a stale grid that produces no fills
2150
+ // can sit in this branch forever. The spread-correction path
2151
+ // (independent of this suppression) and the out-of-spread
2152
+ // persistence watchdog are the heal paths — surface the run
2153
+ // instead of hiding it at debug.
2154
+ this._heldPlanSuppressionCount = (Number(this._heldPlanSuppressionCount) || 0) + 1;
2155
+ const n = this._heldPlanSuppressionCount;
2156
+ this.logger.log(`[SAFE-REBALANCE] Deferred: identical held plan (no new fills since last hold; suppression #${n}); a fill-driven plan will re-derive`, (n === 1 || n % 10 === 0) ? 'warn' : 'debug');
2157
+ return { ...buildAbortedResult('held-plan-unchanged-deferred'), deferred: true };
2158
+ }
2159
+ if (!options?.deferIfBroadcasting) {
2160
+ const idle = await this._awaitBroadcastIdle();
2161
+ if (idle.shutdown || this._shuttingDown) {
2162
+ this.logger.log('[SAFE-REBALANCE] Aborting rebalance: shutdown in progress', 'warn');
2163
+ return buildAbortedResult('Shutdown in progress — safe rebalance aborted');
2164
+ }
2165
+ }
1921
2166
  }
1922
2167
  return await this._gridLock.acquire(async () => {
1923
2168
  return await this._applySafeRebalanceCOW(fills, excludeIds);
@@ -1933,6 +2178,13 @@ class OrderManager {
1933
2178
  this._gapEvacStreaks = new Map();
1934
2179
  if (!(this._gapEvacCancelQueued instanceof Set))
1935
2180
  this._gapEvacCancelQueued = new Set();
2181
+ // Live reserve edge anchors, resolved once and shared with the engine's
2182
+ // refill wire so a reserve-ladder CREATE can never be classified as a
2183
+ // hole refill (which would let a skipped reserve pin the boundary).
2184
+ const reserveEdgeAnchors = {
2185
+ buy: resolveLiveReserveEdgeAnchorPrice(this, 'buy'),
2186
+ sell: resolveLiveReserveEdgeAnchorPrice(this, 'sell')
2187
+ };
1936
2188
  const result = await cowEngine.execute({
1937
2189
  masterGrid: this.orders,
1938
2190
  gridVersion: this._gridVersion,
@@ -1941,7 +2193,8 @@ class OrderManager {
1941
2193
  fills,
1942
2194
  excludeIds,
1943
2195
  gapSlots: this._gapSlots,
1944
- evacStreaks: this._gapEvacStreaks
2196
+ evacStreaks: this._gapEvacStreaks,
2197
+ reserveEdgeAnchors
1945
2198
  });
1946
2199
  // Phase 3 safety net: act on stuck in-band orders regardless of the
1947
2200
  // plan outcome — a plan that aborted for unrelated reasons must not
@@ -2011,7 +2264,7 @@ class OrderManager {
2011
2264
  }
2012
2265
  };
2013
2266
  try {
2014
- const { skipRecalc: skipRecalcOpt } = this._normalizeCommitOptions(options);
2267
+ const { skipRecalc: skipRecalcOpt, boundaryHeld } = this._normalizeCommitOptions(options);
2015
2268
  skipRecalc = skipRecalcOpt;
2016
2269
  const stats = workingGrid.getMemoryStats();
2017
2270
  try {
@@ -2074,6 +2327,22 @@ class OrderManager {
2074
2327
  }
2075
2328
  this.orders = Object.freeze(finalMap);
2076
2329
  this._setBoundary(commitBoundary);
2330
+ // Pending-crawl bookkeeping: an accepted non-null commit
2331
+ // incorporates (incremental derivation) or subsumes (absolute
2332
+ // recovery anchor) every fill recorded so far, so all pending
2333
+ // crawl entries are consumed here. A gate-rejected commit keeps
2334
+ // the previous boundary — nothing was derived into it — so
2335
+ // pending entries survive for the next derivation. A null
2336
+ // commit likewise consumes nothing, and neither does a HELD
2337
+ // boundary (boundaryHeld): the refill hold deliberately keeps
2338
+ // the committed boundary over the plan's target, so the shift
2339
+ // those records encode was never applied and must stay owed —
2340
+ // clearing here lost the crawl permanently (no fill evidence
2341
+ // left for the next derivation, holes refilled same-side).
2342
+ if (boundaryCheck.ok && commitBoundary !== null && commitBoundary !== undefined
2343
+ && boundaryHeld !== true) {
2344
+ this._clearPendingFillCrawls('boundary commit');
2345
+ }
2077
2346
  this._gridVersion++;
2078
2347
  committed = true;
2079
2348
  // Track orderIds from successful COW commits for recovery sync protection.