dexbot 1.5.3 → 1.6.0

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 (231) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +4 -3
  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 +3 -3
  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 +13 -0
  110. package/dist/modules/constants.d.ts.map +1 -1
  111. package/dist/modules/constants.js +68 -4
  112. package/dist/modules/constants.js.map +1 -1
  113. package/dist/modules/dexbot_class.d.ts +8 -0
  114. package/dist/modules/dexbot_class.d.ts.map +1 -1
  115. package/dist/modules/dexbot_class.js +57 -2
  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 +41 -1
  122. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  123. package/dist/modules/dexbot_fill_runtime.js +135 -5
  124. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  125. package/dist/modules/dexbot_maintenance_runtime.d.ts +79 -12
  126. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  127. package/dist/modules/dexbot_maintenance_runtime.js +554 -46
  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 +36 -7
  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/node_connect_policy.d.ts +52 -0
  139. package/dist/modules/node_connect_policy.d.ts.map +1 -0
  140. package/dist/modules/node_connect_policy.js +35 -0
  141. package/dist/modules/node_connect_policy.js.map +1 -0
  142. package/dist/modules/node_manager.d.ts +32 -0
  143. package/dist/modules/node_manager.d.ts.map +1 -1
  144. package/dist/modules/node_manager.js +76 -9
  145. package/dist/modules/node_manager.js.map +1 -1
  146. package/dist/modules/order/accounting.d.ts +30 -0
  147. package/dist/modules/order/accounting.d.ts.map +1 -1
  148. package/dist/modules/order/accounting.js +176 -3
  149. package/dist/modules/order/accounting.js.map +1 -1
  150. package/dist/modules/order/export.d.ts.map +1 -1
  151. package/dist/modules/order/export.js +1 -0
  152. package/dist/modules/order/export.js.map +1 -1
  153. package/dist/modules/order/grid.d.ts.map +1 -1
  154. package/dist/modules/order/grid.js +143 -15
  155. package/dist/modules/order/grid.js.map +1 -1
  156. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  157. package/dist/modules/order/grid_reconcile.js +5 -3
  158. package/dist/modules/order/grid_reconcile.js.map +1 -1
  159. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  160. package/dist/modules/order/grid_reconcile_internal.js +182 -6
  161. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  162. package/dist/modules/order/manager.d.ts +64 -2
  163. package/dist/modules/order/manager.d.ts.map +1 -1
  164. package/dist/modules/order/manager.js +231 -14
  165. package/dist/modules/order/manager.js.map +1 -1
  166. package/dist/modules/order/strategy.d.ts +3 -0
  167. package/dist/modules/order/strategy.d.ts.map +1 -1
  168. package/dist/modules/order/strategy.js +79 -7
  169. package/dist/modules/order/strategy.js.map +1 -1
  170. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  171. package/dist/modules/order/sync_engine.js +16 -1
  172. package/dist/modules/order/sync_engine.js.map +1 -1
  173. package/dist/modules/order/utils/math.d.ts +11 -1
  174. package/dist/modules/order/utils/math.d.ts.map +1 -1
  175. package/dist/modules/order/utils/math.js +24 -1
  176. package/dist/modules/order/utils/math.js.map +1 -1
  177. package/dist/modules/order/utils/order.d.ts +258 -13
  178. package/dist/modules/order/utils/order.d.ts.map +1 -1
  179. package/dist/modules/order/utils/order.js +662 -12
  180. package/dist/modules/order/utils/order.js.map +1 -1
  181. package/dist/modules/order/utils/system.d.ts +36 -0
  182. package/dist/modules/order/utils/system.d.ts.map +1 -1
  183. package/dist/modules/order/utils/system.js +102 -6
  184. package/dist/modules/order/utils/system.js.map +1 -1
  185. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  186. package/dist/modules/order/utils/validate.js +6 -2
  187. package/dist/modules/order/utils/validate.js.map +1 -1
  188. package/dist/modules/paths.d.ts +2 -0
  189. package/dist/modules/paths.d.ts.map +1 -1
  190. package/dist/modules/paths.js +1 -0
  191. package/dist/modules/paths.js.map +1 -1
  192. package/dist/modules/runtime_settings.d.ts +13 -0
  193. package/dist/modules/runtime_settings.d.ts.map +1 -1
  194. package/dist/modules/runtime_settings.js +20 -0
  195. package/dist/modules/runtime_settings.js.map +1 -1
  196. package/dist/modules/utils/errors.d.ts +16 -0
  197. package/dist/modules/utils/errors.d.ts.map +1 -1
  198. package/dist/modules/utils/errors.js +31 -0
  199. package/dist/modules/utils/errors.js.map +1 -1
  200. package/dist/pm2.js +7 -7
  201. package/dist/scripts/analyze-credit.d.ts.map +1 -1
  202. package/dist/scripts/analyze-credit.js +13 -3
  203. package/dist/scripts/analyze-credit.js.map +1 -1
  204. package/dist/scripts/analyze-orders.js +2 -2
  205. package/dist/scripts/analyze-orders.js.map +1 -1
  206. package/dist/scripts/diagnose-kibana-candles.d.ts.map +1 -1
  207. package/dist/scripts/diagnose-kibana-candles.js +0 -1
  208. package/dist/scripts/diagnose-kibana-candles.js.map +1 -1
  209. package/dist/scripts/test-credit-renewal.js +1 -1
  210. package/dist/scripts/test-credit-renewal.js.map +1 -1
  211. package/dist/scripts/tv.d.ts +24 -2
  212. package/dist/scripts/tv.d.ts.map +1 -1
  213. package/dist/scripts/tv.js +190 -65
  214. package/dist/scripts/tv.js.map +1 -1
  215. package/docs/BITSHARES_ONBOARDING.md +9 -2
  216. package/docs/COW_INVARIANTS.md +18 -1
  217. package/docs/DEXBOT_COMPARISON.md +3 -3
  218. package/docs/EVOLUTION.md +8 -7
  219. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +21 -9
  220. package/docs/GRID_RECALCULATION.md +5 -6
  221. package/docs/GRID_RECONCILE.md +12 -2
  222. package/docs/MPA_CREDIT_USAGE.md +2 -2
  223. package/docs/README.md +5 -6
  224. package/docs/architecture.md +0 -58
  225. package/docs/developer_guide.md +2 -1
  226. package/docs/docker.md +1 -1
  227. package/market_adapter/README.md +4 -0
  228. package/modules/README.md +1 -1
  229. package/package.json +1 -1
  230. package/scripts/README.md +5 -3
  231. package/tests/README.md +2 -2
@@ -1048,7 +1048,7 @@ async function autoCancelOneUnmatchedOrphan(bot) {
1048
1048
  }
1049
1049
  const target = unmatched.find((u) => u && u.reason === 'price-drift-orphan');
1050
1050
  if (!target) {
1051
- return { cancelled: false, reason: 'no-price-drift-orphan', message: 'no price-drift orphan to cancel; other unmatched orders are adoptable' };
1051
+ return { cancelled: false, reason: 'no-price-drift-orphan', message: 'no price-drift orphan to cancel; remaining unmatched orders need no cancellation (adoptable or deferred holds)' };
1052
1052
  }
1053
1053
  const orderId = target.id || target.orderId || target.chainOrderId;
1054
1054
  if (!orderId) {
@@ -2200,6 +2200,57 @@ function resolveRefillBoundaryHold(workingBoundary, committedBoundary, skippedUp
2200
2200
  heldRefillSlotIds
2201
2201
  };
2202
2202
  }
2203
+ /**
2204
+ * Track consecutive boundary-hold batches on the manager (ops visibility).
2205
+ *
2206
+ * A single hold is normal maker discipline: the guard vetoed stale-priced
2207
+ * refills, so the committed boundary stays instead of advancing past
2208
+ * stranded rail holes. A growing run means the grid is trailing the market
2209
+ * (plans keep pricing refills against a racing guard pivot, typically while
2210
+ * fill batches run on backlogged state) and only fresh fills unstick it —
2211
+ * worth escalating so it cannot hide inside per-batch warns.
2212
+ *
2213
+ * Also records a hold signature (`_lastHeldPlanSignature`) for the
2214
+ * identical-held-plan suppression in `performSafeRebalance`, and the caller
2215
+ * escalates a long run to a guard-aware structural re-center. A fill-less
2216
+ * re-plan from unchanged master re-derives the identical plan and re-hits
2217
+ * the identical guard blocks, so it is suppressed rather than re-broadcast;
2218
+ * the heal path is a fresh fill-driven plan or the re-center.
2219
+ * @param {any} manager - Order manager (mutable tracking fields)
2220
+ * @param {boolean} held - Whether this batch held the boundary
2221
+ * @param {any} keptBoundary - Committed boundary that was kept
2222
+ * @param {any} plannedBoundary - Boundary the plan wanted
2223
+ * @param {string[]} heldSlotIds - Refill slots skipped this batch
2224
+ * @returns {number} Consecutive-hold count after this batch (0 when clear)
2225
+ */
2226
+ function trackBoundaryHold(manager, held, keptBoundary, plannedBoundary, heldSlotIds) {
2227
+ if (!manager)
2228
+ return 0;
2229
+ const prev = Number(manager._consecutiveBoundaryHolds) || 0;
2230
+ const consecutive = held ? prev + 1 : 0;
2231
+ manager._consecutiveBoundaryHolds = consecutive;
2232
+ if (held) {
2233
+ manager._lastBoundaryHoldInfo = {
2234
+ at: Date.now(),
2235
+ kept: keptBoundary,
2236
+ planned: plannedBoundary,
2237
+ slots: Array.isArray(heldSlotIds) ? [...heldSlotIds] : [],
2238
+ };
2239
+ // Signature for the identical-held-plan suppression in
2240
+ // performSafeRebalance: a fill-less replan with the same boundary,
2241
+ // pivot and fill timestamp can only reproduce this hold.
2242
+ manager._lastHeldPlanSignature = {
2243
+ boundaryIdx: keptBoundary,
2244
+ pivot: manager._lastFilledPrice ?? null,
2245
+ fillsAt: manager._lastFilledAt ?? 0,
2246
+ wire: Array.isArray(heldSlotIds) ? [...heldSlotIds] : [],
2247
+ };
2248
+ }
2249
+ else {
2250
+ manager._lastHeldPlanSignature = null;
2251
+ }
2252
+ return consecutive;
2253
+ }
2203
2254
  /**
2204
2255
  * Restore skipped update slots in the working grid to master state.
2205
2256
  * @param {import('./dexbot_class.js').DEXBot} bot
@@ -2533,8 +2584,13 @@ async function runPreBroadcastGuards(bot, cowResult) {
2533
2584
  const unmatchedChainOrders = Array.isArray(bot.manager?._lastUnmatchedChainOrders)
2534
2585
  ? bot.manager._lastUnmatchedChainOrders
2535
2586
  : [];
2587
+ // Out-of-grid holds are permanent by design (live orders held outside
2588
+ // the frozen rail): they can never be adopted and collide with nothing,
2589
+ // so they must not block CREATES — otherwise one dip-protection hold
2590
+ // freezes the whole grid. Only adoptable/cancellable orphans block.
2591
+ const blockingUnmatched = unmatchedChainOrders.filter((u) => !orderUtils.isNonBlockingUnmatchedOrder(u));
2536
2592
  const pendingBroadcasts = getPendingBroadcasts(bot);
2537
- if (hasCreateActions && (unmatchedChainOrders.length > 0 || pendingBroadcasts.length > 0)) {
2593
+ if (hasCreateActions && (blockingUnmatched.length > 0 || pendingBroadcasts.length > 0)) {
2538
2594
  if (pendingBroadcasts.length > 0) {
2539
2595
  bot.manager.logger.log(`[COW] Rejecting CREATE batch: ${pendingBroadcasts.length} pending broadcast(s) from a prior uncertain ` +
2540
2596
  `broadcast. Running recovery before placing replacement orders.`, 'error');
@@ -2562,11 +2618,11 @@ async function runPreBroadcastGuards(bot, cowResult) {
2562
2618
  }
2563
2619
  };
2564
2620
  }
2565
- const unmatchedSample = unmatchedChainOrders
2621
+ const unmatchedSample = blockingUnmatched
2566
2622
  .slice(0, 3)
2567
2623
  .map((o) => formatUnmatchedChainOrderForLog(o))
2568
2624
  .join(' | ');
2569
- bot.manager.logger.log(`[COW] ${unmatchedChainOrders.length} unmatched chain order(s) blocking CREATES ` +
2625
+ bot.manager.logger.log(`[COW] ${blockingUnmatched.length} unmatched chain order(s) blocking CREATES ` +
2570
2626
  (unmatchedSample ? `(${unmatchedSample})` : '') +
2571
2627
  ` — adopting via sync instead of cancelling`, 'info');
2572
2628
  try {
@@ -2662,6 +2718,19 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
2662
2718
  // frozen for every downstream commit path (success + uncertain-catch).
2663
2719
  // workingBoundary itself stays untouched (audit trail).
2664
2720
  let effectiveBoundary = workingBoundary;
2721
+ // True when the refill hold pinned the committed boundary over the plan's
2722
+ // target. Hoisted out of the broadcast try block: the uncertain-broadcast
2723
+ // catch commits too, and a held boundary must keep the owed fill crawls
2724
+ // there as well (see _commitWorkingGrid pending-crawl bookkeeping).
2725
+ let boundaryHeld = false;
2726
+ // Consecutive-hold re-center tuning (see the escalation block below).
2727
+ const boundaryHoldTiming = constantsModule?.TIMING || {};
2728
+ const holdResyncThreshold = Number(boundaryHoldTiming.BOUNDARY_HOLD_RESYNC_THRESHOLD) > 0
2729
+ ? Number(boundaryHoldTiming.BOUNDARY_HOLD_RESYNC_THRESHOLD)
2730
+ : 4;
2731
+ const holdResyncCooldownMs = Number(boundaryHoldTiming.BOUNDARY_HOLD_RESYNC_COOLDOWN_MS) > 0
2732
+ ? Number(boundaryHoldTiming.BOUNDARY_HOLD_RESYNC_COOLDOWN_MS)
2733
+ : 5 * 60 * 1000;
2665
2734
  if (bot.config.dryRun) {
2666
2735
  const cancelCount = actions.filter((a) => a.type === COW_ACTIONS.CANCEL).length;
2667
2736
  const createCount = actions.filter((a) => a.type === COW_ACTIONS.CREATE).length;
@@ -2775,6 +2844,16 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
2775
2844
  bot._markGridActivity('batch start');
2776
2845
  bot.manager._setRebalanceState(REBALANCE_STATES.BROADCASTING);
2777
2846
  bot.manager.startBroadcasting();
2847
+ // P3 — freeze the guard pivot once per batch: per-action refreshes
2848
+ // mutated the pivot mid-batch (02:03 pivots drifted 0.001523→0.001529
2849
+ // across 20 checks), so early actions were judged against a different
2850
+ // pivot than later ones. The batch summary still reports whether this
2851
+ // freeze moved the pivot under the plan.
2852
+ try {
2853
+ if (refreshLastFillPivotFromQueue(bot))
2854
+ lastFillGuardPivotRefreshed = true;
2855
+ }
2856
+ catch { /* best-effort */ }
2778
2857
  for (const action of actions) {
2779
2858
  if (action.type === COW_ACTIONS.CANCEL) {
2780
2859
  try {
@@ -2861,13 +2940,13 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
2861
2940
  const isCorrectionCreate = actionOrigin === 'spread-correction'
2862
2941
  || (actionOrigin == null && batchOrigin === 'spread-correction');
2863
2942
  if (!isCorrectionCreate) {
2864
- const { check, refreshed } = runLastFillGuardCheck(bot, createPrice, order.size, order.type, lastFillGuardStats);
2943
+ const { check, refreshed } = runLastFillGuardCheck(bot, createPrice, order.size, order.type, lastFillGuardStats, true);
2865
2944
  if (refreshed)
2866
2945
  lastFillGuardPivotRefreshed = true;
2867
2946
  if (check.blocked) {
2868
2947
  lastFillGuardStats.skipped++;
2869
2948
  const dir = order.type === ORDER_TYPES.BUY ? 'above' : 'below';
2870
- bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping ${order.type} CREATE for ${order.id} at ${Format.formatPrice6(createPrice)}: ${dir} last filled ${Format.formatPrice6(check.pivot)} (halfInc ${check.halfInc}% thr ${Format.formatPrice6(check.threshold)}); re-planned after market moves`, 'warn');
2949
+ bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping ${order.type} CREATE for ${order.id} at ${Format.formatPrice6(createPrice)}: ${dir} last filled ${Format.formatPrice6(check.pivot)} (halfInc ${check.halfInc}% thr ${Format.formatPrice6(check.threshold)}); re-planned after market moves`, 'debug');
2871
2950
  if (order.id)
2872
2951
  skippedCreateSlotIds.add(order.id);
2873
2952
  continue;
@@ -2938,6 +3017,15 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
2938
3017
  }
2939
3018
  const rotationSizeValidation = validateOrderSizeForExecution(bot, newSize, orderType, action.order, newSize);
2940
3019
  if (!rotationSizeValidation.isValid) {
3020
+ // Record like every sibling skip site: the skip set
3021
+ // feeds both the working-grid restore and the
3022
+ // boundary hold (a skipped refill must not let the
3023
+ // committed boundary advance past its empty slot).
3024
+ skippedUpdateCount++;
3025
+ if (action.id)
3026
+ skippedUpdateSlotIds.add(action.id);
3027
+ if (action.newGridId)
3028
+ skippedUpdateSlotIds.add(action.newGridId);
2941
3029
  bot.manager.logger.log(`Skipping rotation update ${action.id} -> ${action.newGridId}: ${rotationSizeValidation.reason}`, 'warn');
2942
3030
  continue;
2943
3031
  }
@@ -3087,7 +3175,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
3087
3175
  lastFillGuardStats.bypassed++;
3088
3176
  }
3089
3177
  else {
3090
- const { check, refreshed } = runLastFillGuardCheck(bot, newPrice, newSize, orderType, lastFillGuardStats);
3178
+ const { check, refreshed } = runLastFillGuardCheck(bot, newPrice, newSize, orderType, lastFillGuardStats, true);
3091
3179
  if (refreshed)
3092
3180
  lastFillGuardPivotRefreshed = true;
3093
3181
  if (check.blocked) {
@@ -3098,7 +3186,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
3098
3186
  if (action.newGridId)
3099
3187
  skippedUpdateSlotIds.add(action.newGridId);
3100
3188
  const dir = orderType === ORDER_TYPES.BUY ? 'above' : 'below';
3101
- bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping ${orderType} UPDATE for ${action.id} -> ${action.newGridId} at ${Format.formatPrice6(newPrice)}: ${dir} last filled ${Format.formatPrice6(check.pivot)} (halfInc ${check.halfInc}% thr ${Format.formatPrice6(check.threshold)})`, 'warn');
3189
+ bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping ${orderType} UPDATE for ${action.id} -> ${action.newGridId} at ${Format.formatPrice6(newPrice)}: ${dir} last filled ${Format.formatPrice6(check.pivot)} (halfInc ${check.halfInc}% thr ${Format.formatPrice6(check.threshold)})`, 'debug');
3102
3190
  continue;
3103
3191
  }
3104
3192
  lastFillGuardStats.passed++;
@@ -3214,10 +3302,10 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
3214
3302
  }
3215
3303
  // LAST-FILL GUARD (fallback variant): this CREATE
3216
3304
  // replaces a rotation UPDATE at a repriced level,
3217
- // so it obeys the same guard with a refreshed
3218
- // pivot — no origin bypass, same as rotations.
3305
+ // so it obeys the same guard against the frozen
3306
+ // batch pivot — no origin bypass, same as rotations.
3219
3307
  try {
3220
- const { check: fbCheck, refreshed: fbRefreshed } = runLastFillGuardCheck(bot, fbPrice, fbSize, fbType, lastFillGuardStats);
3308
+ const { check: fbCheck, refreshed: fbRefreshed } = runLastFillGuardCheck(bot, fbPrice, fbSize, fbType, lastFillGuardStats, true);
3221
3309
  if (fbRefreshed)
3222
3310
  lastFillGuardPivotRefreshed = true;
3223
3311
  if (fbCheck.blocked) {
@@ -3226,7 +3314,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
3226
3314
  bot.manager.logger.log(`[LAST-FILL-GUARD] Skipping CREATE fallback for ${targetSlotId} at ` +
3227
3315
  `${Format.formatPrice6(fbPrice)}: ${fbDir} last filled ` +
3228
3316
  `${Format.formatPrice6(fbCheck.pivot)} (halfInc ${fbCheck.halfInc}% thr ` +
3229
- `${Format.formatPrice6(fbCheck.threshold)}); re-planned after market moves`, 'warn');
3317
+ `${Format.formatPrice6(fbCheck.threshold)}); re-planned after market moves`, 'debug');
3230
3318
  continue;
3231
3319
  }
3232
3320
  lastFillGuardStats.passed++;
@@ -3268,10 +3356,10 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
3268
3356
  // are what incident reconstruction needs. Origin folds in here as
3269
3357
  // mode=bypassed(<origin>); there is no second source of truth.
3270
3358
  // Cold (guard off) is warn, not info — a disabled guard must say so.
3271
- // pivotRefreshed surfaces whether a mid-broadcast queued fill moved the
3272
- // pivot under this batch's checks (the refresh itself stays debug).
3273
- // Note the printed pivot is end-of-batch state: when pivotRefreshed is
3274
- // true, early actions were checked against the older pivot.
3359
+ // pivotRefreshed surfaces whether the batch-start freeze picked up a
3360
+ // queued fill (the refresh itself stays debug). The pivot is frozen
3361
+ // for the whole batch, so every action was checked against the same
3362
+ // pivot printed here.
3275
3363
  try {
3276
3364
  const totalGuarded = lastFillGuardStats.checked + lastFillGuardStats.bypassed;
3277
3365
  if (totalGuarded > 0) {
@@ -3312,10 +3400,42 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
3312
3400
  // the committed boundary; intersect-only, grid still commits.
3313
3401
  const refillHold = resolveRefillBoundaryHold(workingBoundary, bot.manager.boundaryIdx, skippedUpdateSlotIds, clampedUpdateSlotIds, cowResult?.refillSlotIds, skippedCreateSlotIds);
3314
3402
  effectiveBoundary = refillHold.effectiveBoundary;
3403
+ boundaryHeld = refillHold.heldRefillSlotIds.length > 0;
3315
3404
  if (refillHold.heldRefillSlotIds.length > 0) {
3316
3405
  bot.manager.logger.log(`[COW] Boundary hold: ${refillHold.heldRefillSlotIds.length} refill slot(s) skipped ` +
3317
3406
  `(${refillHold.heldRefillSlotIds.join(', ')}) — keeping ${bot.manager.boundaryIdx} over planned ${workingBoundary}`, 'warn');
3318
3407
  }
3408
+ // Consecutive-hold tracking (INV-COW-007 visibility): escalate a
3409
+ // growing run — the grid is trailing the market and only fresh
3410
+ // fills unstick it. Cleared automatically on the first clean batch.
3411
+ const consecutiveHolds = trackBoundaryHold(bot.manager, refillHold.heldRefillSlotIds.length > 0, bot.manager.boundaryIdx, workingBoundary, refillHold.heldRefillSlotIds);
3412
+ if (consecutiveHolds >= 3) {
3413
+ bot.manager.logger.log(`[COW] Boundary held ${consecutiveHolds} consecutive batches ` +
3414
+ `(keeping ${bot.manager.boundaryIdx} over planned ${workingBoundary}). ` +
3415
+ `Grid is trailing the market — refills re-price once the guard pivot settles; ` +
3416
+ `heals on the next fill-driven plan. Investigate only if the run keeps growing without new fills.`, 'warn');
3417
+ }
3418
+ // Guard-aware re-center escalation (INV-COW-007 heal path): a run of
3419
+ // holds carrying fresh fills means the grid is trailing the market and
3420
+ // will not heal from fill-less replans (they re-derive the identical
3421
+ // veto). Request a structural resync that re-derives centers on the
3422
+ // live pivot; the cooldown prevents resync storms. requestStructuralGridResync
3423
+ // re-defers while this batch is still in flight, so it runs in a clean context.
3424
+ if (boundaryHeld && consecutiveHolds >= holdResyncThreshold) {
3425
+ const freshFills = Array.isArray(cowResult?.fills) && cowResult.fills.length > 0;
3426
+ const lastResyncAt = Number(bot.manager._lastBoundaryHoldResyncAt) || 0;
3427
+ if (freshFills && (Date.now() - lastResyncAt) >= holdResyncCooldownMs) {
3428
+ bot.manager._lastBoundaryHoldResyncAt = Date.now();
3429
+ bot.manager.logger.log(`[COW] Boundary held ${consecutiveHolds} consecutive batches with fresh fills; ` +
3430
+ `requesting guard-aware structural re-center (cooldown ${Math.round(holdResyncCooldownMs / 1000)}s)`, 'warn');
3431
+ try {
3432
+ void bot.manager.requestStructuralGridResync?.('boundary-hold-trailing-market', { reason: 'boundary-hold-trailing-market' });
3433
+ }
3434
+ catch (err) {
3435
+ bot.manager.logger.log(`[COW] Structural re-center request failed (non-fatal): ${getErrorMessage(err)}`, 'warn');
3436
+ }
3437
+ }
3438
+ }
3319
3439
  if (operations.length === 0) {
3320
3440
  // Pop the working grid: in the re-plan recursion the fresh plan's
3321
3441
  // grid was pushed by performSafeRebalance, and nothing downstream
@@ -3454,7 +3574,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
3454
3574
  // options.result, so a later throw in this frame (e.g.
3455
3575
  // processBatchResults after a successful commit) cannot pop a
3456
3576
  // second time for the same grid in the batch catch below.
3457
- const commitOk = await bot.manager._commitWorkingGrid(workingGrid, workingIndexes, effectiveBoundary, { skipRecalc: true, result: cowResult });
3577
+ const commitOk = await bot.manager._commitWorkingGrid(workingGrid, workingIndexes, effectiveBoundary, { skipRecalc: true, result: cowResult, boundaryHeld });
3458
3578
  if (!commitOk) {
3459
3579
  // Master changed during broadcast (e.g. a fill landed and was
3460
3580
  // processed concurrently) so the commit was refused. The batch
@@ -3544,7 +3664,7 @@ async function updateOrdersOnChainBatchCOW(bot, cowResult, options = {}) {
3544
3664
  // _commitWorkingGrid pops on every settle path and clears
3545
3665
  // the push marker via options.result, so a later throw here
3546
3666
  // must not pop again in the batch catch below.
3547
- const pollCommitOk = await bot.manager._commitWorkingGrid(workingGrid, workingIndexes, effectiveBoundary, { skipRecalc: true, result: cowResult });
3667
+ const pollCommitOk = await bot.manager._commitWorkingGrid(workingGrid, workingIndexes, effectiveBoundary, { skipRecalc: true, result: cowResult, boundaryHeld });
3548
3668
  if (!pollCommitOk) {
3549
3669
  // Master moved while polling — same recovery as the
3550
3670
  // refused-commit path: adopt from chain, keep pending
@@ -3878,24 +3998,32 @@ async function recoverRefusedCommit(bot, chainOrders, logPrefix, adoptOpts, cont
3878
3998
  return successReturn ?? { executed: false, hadRotation: false, commitRefused: true, ...extraReturn };
3879
3999
  }
3880
4000
  /**
3881
- * Run the last-fill guard probe: refresh the pivot from still-queued fills,
3882
- * read the durable pivot, and evaluate isLastFillGuardBlocked. Consolidates
3883
- * the identical probe pattern in the CREATE, UPDATE-rotation, and
3884
- * CREATE-fallback guards (origin bypasses and skip logging stay at the
3885
- * call sites, which differ per action kind).
4001
+ * Run the last-fill guard probe: optionally refresh the pivot from
4002
+ * still-queued fills, read the durable pivot, and evaluate
4003
+ * isLastFillGuardBlocked. Consolidates the identical probe pattern in the
4004
+ * CREATE, UPDATE-rotation, and CREATE-fallback guards (origin bypasses and
4005
+ * skip logging stay at the call sites, which differ per action kind).
4006
+ * Batch callers pass skipRefresh=true: the batch-start freeze owns refreshes
4007
+ * so every action in a batch is judged against the same pivot.
3886
4008
  * @param {Object} bot
3887
4009
  * @param {number} price - Target order price
3888
4010
  * @param {number} size - Order size
3889
4011
  * @param {string} type - ORDER_TYPES.BUY/SELL
3890
4012
  * @param {Object} stats - lastFillGuardStats tracker (checked++ here)
4013
+ * @param {boolean} [skipRefresh=false] - Skip the queued-fill pivot refresh
3891
4014
  * @returns {{check: Object, refreshed: boolean}}
3892
4015
  */
3893
- function runLastFillGuardCheck(bot, price, size, type, stats) {
4016
+ function runLastFillGuardCheck(bot, price, size, type, stats, skipRefresh = false) {
3894
4017
  let refreshed = false;
3895
- try {
3896
- refreshed = !!refreshLastFillPivotFromQueue(bot);
4018
+ // The batch-start freeze (see broadcast loop head) owns pivot refreshes;
4019
+ // per-action refreshes are disabled so every action in a batch is judged
4020
+ // against the same pivot. Kept opt-in for non-batch callers.
4021
+ if (!skipRefresh) {
4022
+ try {
4023
+ refreshed = !!refreshLastFillPivotFromQueue(bot);
4024
+ }
4025
+ catch { /* best-effort */ }
3897
4026
  }
3898
- catch { /* best-effort */ }
3899
4027
  const lastPrice = bot.manager?._lastFilledPrice;
3900
4028
  const lastType = bot.manager?._lastFilledType;
3901
4029
  const inc = resolveLastFillGuardIncrement(bot);
@@ -4145,7 +4273,7 @@ async function processBatchResults(bot, result, opContexts) {
4145
4273
  updateOperationCount
4146
4274
  };
4147
4275
  }
4148
- export { isLastFillGuardBlocked, refreshLastFillPivotFromQueue, buildOutsideInPairGroupsForOrders, buildOutsideInPairGroupsForCreateEntries, extractOperationResults, findMissingCreateResultContexts, markMissingCreateResultsAsStructuralBlocker, formatUnmatchedChainOrderForLog, recordPendingBroadcast, clearPendingBroadcasts, clearPendingBroadcastsForSlots, popPushedWorkingGrid, buildChainOrderFingerprint, normalizeChainOrderForPendingMatch, findChainOrderForSlot, reconcileAfterUncertainBroadcast, reconcileAfterUncertainBroadcastImpl, autoCancelOneUnmatchedOrphan, shouldExecuteCreatePairMode, executeWithRetryOnUncertain, executeChunkedWithRetryOnUncertain, formatPartialBroadcastSummary, executeOperationsWithStrategy, validateOperationFunds, resolveIdealSizeForValidation, validateOrderSizeForExecution, buildActionsFromPlan, buildCowResultFromPlan, restoreSkippedUpdateSlotsInWorkingGrid, applyRotationTransitionsToWorkingGrid, pollChainForConfirmation, updateOrdersOnChainBatchCOW, processBatchResults, adoptPlacedBatchFromChain, resolveRefillBoundaryHold, toRefillSlotIdSet };
4276
+ export { isLastFillGuardBlocked, refreshLastFillPivotFromQueue, buildOutsideInPairGroupsForOrders, buildOutsideInPairGroupsForCreateEntries, extractOperationResults, findMissingCreateResultContexts, markMissingCreateResultsAsStructuralBlocker, formatUnmatchedChainOrderForLog, recordPendingBroadcast, clearPendingBroadcasts, clearPendingBroadcastsForSlots, popPushedWorkingGrid, buildChainOrderFingerprint, normalizeChainOrderForPendingMatch, findChainOrderForSlot, reconcileAfterUncertainBroadcast, reconcileAfterUncertainBroadcastImpl, autoCancelOneUnmatchedOrphan, shouldExecuteCreatePairMode, executeWithRetryOnUncertain, executeChunkedWithRetryOnUncertain, formatPartialBroadcastSummary, executeOperationsWithStrategy, validateOperationFunds, resolveIdealSizeForValidation, validateOrderSizeForExecution, buildActionsFromPlan, buildCowResultFromPlan, restoreSkippedUpdateSlotsInWorkingGrid, applyRotationTransitionsToWorkingGrid, pollChainForConfirmation, updateOrdersOnChainBatchCOW, processBatchResults, adoptPlacedBatchFromChain, resolveRefillBoundaryHold, toRefillSlotIdSet, trackBoundaryHold };
4149
4277
  // Exported for regression tests (issue #23 sibling): the uncertain-broadcast
4150
4278
  // discard path must never drop a placement silently when master lost the slot.
4151
4279
  export { restoreDiscardedCreates };
@@ -4179,6 +4307,7 @@ export default {
4179
4307
  restoreSkippedUpdateSlotsInWorkingGrid,
4180
4308
  resolveRefillBoundaryHold,
4181
4309
  toRefillSlotIdSet,
4310
+ trackBoundaryHold,
4182
4311
  applyRotationTransitionsToWorkingGrid,
4183
4312
  pollChainForConfirmation,
4184
4313
  updateOrdersOnChainBatchCOW,