dexbot 1.4.24 → 1.5.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 (256) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +7 -2
  3. package/analysis/README.md +61 -5
  4. package/analysis/ama_fitting/package.json +1 -1
  5. package/analysis/analyze_derivatives.ts +1 -1
  6. package/analysis/bot_key_utils.ts +272 -3
  7. package/analysis/bot_usage/discover_bot_accounts.ts +64 -9
  8. package/analysis/bot_usage/kibana_bot_queries.ts +40 -1
  9. package/analysis/chart_utils.ts +6 -3
  10. package/analysis/grid_correction_check.ts +932 -0
  11. package/analysis/resolve_bot_accounts.ts +199 -0
  12. package/analysis/trade_profitability.ts +36 -8
  13. package/analysis/tradingview/README.md +25 -0
  14. package/analysis/tradingview/analyze_tradingview.ts +2 -2
  15. package/analysis/trend_detection/package.json +1 -1
  16. package/claw/modules/claw_launcher.ts +5 -4
  17. package/claw/modules/credit_runtime_adapter.ts +2 -1
  18. package/claw/modules/dexbot_profiles.ts +4 -3
  19. package/claw/package.json +1 -1
  20. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  21. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  22. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  23. package/dist/analysis/analyze_derivatives.js +1 -1
  24. package/dist/analysis/bot_key_utils.d.ts +28 -1
  25. package/dist/analysis/bot_key_utils.d.ts.map +1 -1
  26. package/dist/analysis/bot_key_utils.js +385 -3
  27. package/dist/analysis/bot_key_utils.js.map +1 -1
  28. package/dist/analysis/bot_usage/discover_bot_accounts.js +60 -10
  29. package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
  30. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +45 -1
  31. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
  32. package/dist/analysis/bot_usage/kibana_bot_queries.js +39 -1
  33. package/dist/analysis/bot_usage/kibana_bot_queries.js.map +1 -1
  34. package/dist/analysis/chart_utils.d.ts +2 -1
  35. package/dist/analysis/chart_utils.d.ts.map +1 -1
  36. package/dist/analysis/chart_utils.js +4 -1
  37. package/dist/analysis/chart_utils.js.map +1 -1
  38. package/dist/analysis/grid_correction_check.d.ts +90 -0
  39. package/dist/analysis/grid_correction_check.d.ts.map +1 -0
  40. package/dist/analysis/grid_correction_check.js +901 -0
  41. package/dist/analysis/grid_correction_check.js.map +1 -0
  42. package/dist/analysis/resolve_bot_accounts.d.ts +3 -0
  43. package/dist/analysis/resolve_bot_accounts.d.ts.map +1 -0
  44. package/dist/analysis/resolve_bot_accounts.js +216 -0
  45. package/dist/analysis/resolve_bot_accounts.js.map +1 -0
  46. package/dist/analysis/trade_profitability.d.ts.map +1 -1
  47. package/dist/analysis/trade_profitability.js +40 -8
  48. package/dist/analysis/trade_profitability.js.map +1 -1
  49. package/dist/analysis/tradingview/analyze_tradingview.js +2 -2
  50. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
  51. package/dist/bot.d.ts.map +1 -1
  52. package/dist/bot.js +2 -1
  53. package/dist/bot.js.map +1 -1
  54. package/dist/dexbot.d.ts.map +1 -1
  55. package/dist/dexbot.js +134 -22
  56. package/dist/dexbot.js.map +1 -1
  57. package/dist/market_adapter/ama_signal_runner.js +2 -2
  58. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +2 -1
  59. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js.map +1 -1
  60. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +4 -1
  61. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  62. package/dist/market_adapter/inputs/fetch_lp_data.js +3 -2
  63. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  64. package/dist/modules/account_bots.d.ts +42 -1
  65. package/dist/modules/account_bots.d.ts.map +1 -1
  66. package/dist/modules/account_bots.js +221 -22
  67. package/dist/modules/account_bots.js.map +1 -1
  68. package/dist/modules/account_orders.d.ts +7 -10
  69. package/dist/modules/account_orders.d.ts.map +1 -1
  70. package/dist/modules/account_orders.js +26 -2
  71. package/dist/modules/account_orders.js.map +1 -1
  72. package/dist/modules/bitshares_client.d.ts +4 -1
  73. package/dist/modules/bitshares_client.d.ts.map +1 -1
  74. package/dist/modules/bitshares_client.js +12 -4
  75. package/dist/modules/bitshares_client.js.map +1 -1
  76. package/dist/modules/bot_settings.d.ts.map +1 -1
  77. package/dist/modules/bot_settings.js +2 -1
  78. package/dist/modules/bot_settings.js.map +1 -1
  79. package/dist/modules/chain_orders.d.ts.map +1 -1
  80. package/dist/modules/chain_orders.js +21 -2
  81. package/dist/modules/chain_orders.js.map +1 -1
  82. package/dist/modules/cli_colors.d.ts +39 -0
  83. package/dist/modules/cli_colors.d.ts.map +1 -0
  84. package/dist/modules/cli_colors.js +42 -0
  85. package/dist/modules/cli_colors.js.map +1 -0
  86. package/dist/modules/config.d.ts.map +1 -1
  87. package/dist/modules/config.js +27 -0
  88. package/dist/modules/config.js.map +1 -1
  89. package/dist/modules/constants.d.ts +14 -1
  90. package/dist/modules/constants.d.ts.map +1 -1
  91. package/dist/modules/constants.js +66 -11
  92. package/dist/modules/constants.js.map +1 -1
  93. package/dist/modules/credit_pricing.d.ts +61 -0
  94. package/dist/modules/credit_pricing.d.ts.map +1 -0
  95. package/dist/modules/credit_pricing.js +237 -0
  96. package/dist/modules/credit_pricing.js.map +1 -0
  97. package/dist/modules/credit_runtime.d.ts +1 -0
  98. package/dist/modules/credit_runtime.d.ts.map +1 -1
  99. package/dist/modules/credit_runtime.js +49 -107
  100. package/dist/modules/credit_runtime.js.map +1 -1
  101. package/dist/modules/dexbot_class.d.ts +25 -0
  102. package/dist/modules/dexbot_class.d.ts.map +1 -1
  103. package/dist/modules/dexbot_class.js +82 -12
  104. package/dist/modules/dexbot_class.js.map +1 -1
  105. package/dist/modules/dexbot_cow_runtime.d.ts +39 -17
  106. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  107. package/dist/modules/dexbot_cow_runtime.js +875 -134
  108. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  109. package/dist/modules/dexbot_fill_runtime.d.ts +11 -1
  110. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  111. package/dist/modules/dexbot_fill_runtime.js +108 -1
  112. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  113. package/dist/modules/dexbot_maintenance_runtime.d.ts +29 -14
  114. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  115. package/dist/modules/dexbot_maintenance_runtime.js +279 -63
  116. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  117. package/dist/modules/dexbot_startup_runtime.d.ts +1 -0
  118. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  119. package/dist/modules/dexbot_startup_runtime.js +58 -7
  120. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  121. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  122. package/dist/modules/dexbot_state_recovery.js +55 -3
  123. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  124. package/dist/modules/fund_registry.d.ts +1 -1
  125. package/dist/modules/fund_registry.js +1 -1
  126. package/dist/modules/graceful_shutdown.d.ts +12 -1
  127. package/dist/modules/graceful_shutdown.d.ts.map +1 -1
  128. package/dist/modules/graceful_shutdown.js +39 -1
  129. package/dist/modules/graceful_shutdown.js.map +1 -1
  130. package/dist/modules/key_store.d.ts.map +1 -1
  131. package/dist/modules/key_store.js +38 -1
  132. package/dist/modules/key_store.js.map +1 -1
  133. package/dist/modules/launcher/adapter_requirement.d.ts +38 -0
  134. package/dist/modules/launcher/adapter_requirement.d.ts.map +1 -0
  135. package/dist/modules/launcher/adapter_requirement.js +141 -0
  136. package/dist/modules/launcher/adapter_requirement.js.map +1 -0
  137. package/dist/modules/launcher/bot_supervisor.d.ts +2 -2
  138. package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
  139. package/dist/modules/launcher/bot_supervisor.js +32 -3
  140. package/dist/modules/launcher/bot_supervisor.js.map +1 -1
  141. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  142. package/dist/modules/launcher/launch_modes.js +5 -4
  143. package/dist/modules/launcher/launch_modes.js.map +1 -1
  144. package/dist/modules/launcher/monolithic_runtime.d.ts +1 -1
  145. package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
  146. package/dist/modules/launcher/monolithic_runtime.js +12 -8
  147. package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
  148. package/dist/modules/launcher/status_reporting.d.ts +6 -6
  149. package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
  150. package/dist/modules/launcher/status_reporting.js +9 -6
  151. package/dist/modules/launcher/status_reporting.js.map +1 -1
  152. package/dist/modules/order/accounting.d.ts.map +1 -1
  153. package/dist/modules/order/accounting.js +20 -3
  154. package/dist/modules/order/accounting.js.map +1 -1
  155. package/dist/modules/order/export.d.ts +1 -1
  156. package/dist/modules/order/export.js +1 -1
  157. package/dist/modules/order/grid.d.ts +1 -1
  158. package/dist/modules/order/grid.d.ts.map +1 -1
  159. package/dist/modules/order/grid.js +335 -43
  160. package/dist/modules/order/grid.js.map +1 -1
  161. package/dist/modules/order/grid_reconcile.d.ts +4 -2
  162. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  163. package/dist/modules/order/grid_reconcile.js +242 -222
  164. package/dist/modules/order/grid_reconcile.js.map +1 -1
  165. package/dist/modules/order/grid_reconcile_internal.d.ts +11 -1
  166. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  167. package/dist/modules/order/grid_reconcile_internal.js +132 -6
  168. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  169. package/dist/modules/order/logger.d.ts +23 -1
  170. package/dist/modules/order/logger.d.ts.map +1 -1
  171. package/dist/modules/order/logger.js +60 -17
  172. package/dist/modules/order/logger.js.map +1 -1
  173. package/dist/modules/order/manager.d.ts +68 -14
  174. package/dist/modules/order/manager.d.ts.map +1 -1
  175. package/dist/modules/order/manager.js +281 -52
  176. package/dist/modules/order/manager.js.map +1 -1
  177. package/dist/modules/order/strategy.d.ts.map +1 -1
  178. package/dist/modules/order/strategy.js +25 -8
  179. package/dist/modules/order/strategy.js.map +1 -1
  180. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  181. package/dist/modules/order/sync_engine.js +262 -167
  182. package/dist/modules/order/sync_engine.js.map +1 -1
  183. package/dist/modules/order/utils/math.d.ts +47 -5
  184. package/dist/modules/order/utils/math.d.ts.map +1 -1
  185. package/dist/modules/order/utils/math.js +175 -11
  186. package/dist/modules/order/utils/math.js.map +1 -1
  187. package/dist/modules/order/utils/order.d.ts +16 -15
  188. package/dist/modules/order/utils/order.d.ts.map +1 -1
  189. package/dist/modules/order/utils/order.js +199 -32
  190. package/dist/modules/order/utils/order.js.map +1 -1
  191. package/dist/modules/order/utils/slot.d.ts +8 -0
  192. package/dist/modules/order/utils/slot.d.ts.map +1 -0
  193. package/dist/modules/order/utils/slot.js +16 -0
  194. package/dist/modules/order/utils/slot.js.map +1 -0
  195. package/dist/modules/order/utils/system.d.ts +52 -4
  196. package/dist/modules/order/utils/system.d.ts.map +1 -1
  197. package/dist/modules/order/utils/system.js +109 -16
  198. package/dist/modules/order/utils/system.js.map +1 -1
  199. package/dist/modules/order/utils/validate.d.ts +6 -9
  200. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  201. package/dist/modules/order/utils/validate.js +107 -85
  202. package/dist/modules/order/utils/validate.js.map +1 -1
  203. package/dist/modules/order/working_grid.d.ts +5 -0
  204. package/dist/modules/order/working_grid.d.ts.map +1 -1
  205. package/dist/modules/order/working_grid.js +21 -0
  206. package/dist/modules/order/working_grid.js.map +1 -1
  207. package/dist/modules/paths.d.ts +4 -2
  208. package/dist/modules/paths.d.ts.map +1 -1
  209. package/dist/modules/paths.js +79 -24
  210. package/dist/modules/paths.js.map +1 -1
  211. package/dist/modules/utils/chain_logs.d.ts +11 -0
  212. package/dist/modules/utils/chain_logs.d.ts.map +1 -0
  213. package/dist/modules/utils/chain_logs.js +23 -0
  214. package/dist/modules/utils/chain_logs.js.map +1 -0
  215. package/dist/modules/utils/sanitize_key.d.ts +7 -0
  216. package/dist/modules/utils/sanitize_key.d.ts.map +1 -1
  217. package/dist/modules/utils/sanitize_key.js +15 -0
  218. package/dist/modules/utils/sanitize_key.js.map +1 -1
  219. package/dist/pm2.d.ts +11 -1
  220. package/dist/pm2.d.ts.map +1 -1
  221. package/dist/pm2.js +66 -15
  222. package/dist/pm2.js.map +1 -1
  223. package/dist/scripts/analyze-credit.d.ts +4 -0
  224. package/dist/scripts/analyze-credit.d.ts.map +1 -0
  225. package/dist/scripts/analyze-credit.js +760 -0
  226. package/dist/scripts/analyze-credit.js.map +1 -0
  227. package/dist/scripts/analyze-orders.d.ts +1 -10
  228. package/dist/scripts/analyze-orders.d.ts.map +1 -1
  229. package/dist/scripts/analyze-orders.js +34 -36
  230. package/dist/scripts/analyze-orders.js.map +1 -1
  231. package/dist/scripts/generate_market_adapter_whitelist.d.ts +2 -5
  232. package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +1 -1
  233. package/dist/scripts/generate_market_adapter_whitelist.js +83 -1
  234. package/dist/scripts/generate_market_adapter_whitelist.js.map +1 -1
  235. package/dist/scripts/test-credit-renewal.d.ts.map +1 -1
  236. package/dist/scripts/test-credit-renewal.js +37 -5
  237. package/dist/scripts/test-credit-renewal.js.map +1 -1
  238. package/dist/scripts/tv.d.ts +11 -0
  239. package/dist/scripts/tv.d.ts.map +1 -0
  240. package/dist/scripts/tv.js +357 -0
  241. package/dist/scripts/tv.js.map +1 -0
  242. package/dist/scripts/update.js +77 -10
  243. package/dist/scripts/update.js.map +1 -1
  244. package/dist/unlock.d.ts.map +1 -1
  245. package/dist/unlock.js +20 -8
  246. package/dist/unlock.js.map +1 -1
  247. package/docs/DEXBOT_COMPARISON.md +11 -11
  248. package/docs/EVOLUTION.md +10 -8
  249. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  250. package/docs/GRID_RECALCULATION.md +6 -0
  251. package/docs/MPA_CREDIT_USAGE.md +3 -1
  252. package/docs/README.md +2 -2
  253. package/docs/WORKFLOW.md +3 -1
  254. package/market_adapter/README.md +10 -0
  255. package/package.json +3 -1
  256. package/scripts/README.md +16 -0
@@ -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 } from '../constants.js';
30
- import { getMinOrderSize, computeChainFundTotals, hasValidAccountTotals, resolveConfigValueWithRegistry, isExplicitZeroAllocation, floatToBlockchainInt, validateBoundaryCommit, resolveGapSlots, resolveGapBand } from './utils/math.js';
30
+ import { getMinOrderSize, computeChainFundTotals, hasValidAccountTotals, resolveConfigValueWithRegistry, isExplicitZeroAllocation, floatToBlockchainInt, validateBoundaryCommit, resolveGapSlots } 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 } from './utils/order.js';
32
+ import { resolveSpreadOrderSide, parseSlotIndex, parseChainOrder } from './utils/order.js';
33
33
  import { getErrorMessage } from '../utils/errors.js';
34
34
  const { toFiniteNumber } = Format;
35
35
  // ===============================================================================
@@ -154,7 +154,9 @@ class COWRebalanceEngine {
154
154
  if (reconcileResult.aborted) {
155
155
  return buildAbortedResult(reconcileResult.reason);
156
156
  }
157
- const optimizedActions = optimizeRebalanceActions(reconcileResult.actions, masterGrid);
157
+ const optimizedActions = optimizeRebalanceActions(reconcileResult.actions, masterGrid, {
158
+ logger: (msg, level) => this.logger?.log(msg, level)
159
+ });
158
160
  // Stale-placement guard: drop placements crossing the plan's own
159
161
  // boundary (see stalePlacementDropReason) — defer them to the next
160
162
  // cycle instead of filling immediately.
@@ -198,6 +200,25 @@ class COWRebalanceEngine {
198
200
  this.logger?.log(`[COW] Rebalance planning took ${duration}ms`, 'warn');
199
201
  }
200
202
  this.logger?.log(`[COW] Plan: Actions=${optimizedActions.length}, StateUpdates=${stateUpdates.length}`, 'info');
203
+ // Per-action plan detail. UPDATE/ROTATE ops are the dangerous ones —
204
+ // their target size/price must be visible in production logs without
205
+ // on-chain archaeology (a mis-sized update op is only diagnosable from
206
+ // the chain afterwards otherwise).
207
+ for (const a of optimizedActions) {
208
+ if (!a || !a.type || !a.id)
209
+ continue;
210
+ const size = Number.isFinite(Number(a.newSize))
211
+ ? Number(a.newSize)
212
+ : Number(a.order?.size);
213
+ const price = Number.isFinite(Number(a.newPrice))
214
+ ? Number(a.newPrice)
215
+ : Number(a.order?.price);
216
+ const rotationSuffix = (a.newGridId && a.newGridId !== a.id) ? ` -> ${a.newGridId}` : '';
217
+ const line = `[COW] Plan action: ${a.type} ${a.id}${rotationSuffix} ` +
218
+ `(orderId=${a.orderId || 'none'}, size=${Number.isFinite(size) ? Format.formatAmount(size) : 'n/a'}, ` +
219
+ `price=${Number.isFinite(price) ? Format.formatPrice6(price) : 'n/a'})`;
220
+ this.logger?.log(line, a.type === COW_ACTIONS.UPDATE ? 'info' : 'debug');
221
+ }
201
222
  return buildSuccessResult({
202
223
  actions: optimizedActions,
203
224
  stateUpdates,
@@ -225,6 +246,7 @@ class OrderManager {
225
246
  _endOfBootstrapValidationDone;
226
247
  _broadcastingFlag;
227
248
  _broadcastingStartedAt;
249
+ _shuttingDown;
228
250
  _illegalStateSignal;
229
251
  _accountingFailureSignal;
230
252
  _recoveryStateValue;
@@ -333,6 +355,13 @@ class OrderManager {
333
355
  _orphanFillsCreditedAt;
334
356
  _pendingRecovery;
335
357
  _recentFillKeysSnapshot;
358
+ _lastFilledBuyPrice;
359
+ _lastFilledSellPrice;
360
+ _lastFilledPrice;
361
+ _lastFilledType;
362
+ // anchor fields removed
363
+ // Note: dedupe lives inside the anchor object (`_seenKeys`), not here.
364
+ // Kept for backwards compat if external code checks existence; not used.
336
365
  _metrics;
337
366
  _currentWorkingGridStack;
338
367
  _cowEngine;
@@ -363,6 +392,7 @@ class OrderManager {
363
392
  this._endOfBootstrapValidationDone = false;
364
393
  this._broadcastingFlag = 0;
365
394
  this._broadcastingStartedAt = 0;
395
+ this._shuttingDown = false;
366
396
  this._illegalStateSignal = null;
367
397
  this._accountingFailureSignal = null;
368
398
  this._recoveryStateValue = {
@@ -437,6 +467,10 @@ class OrderManager {
437
467
  this._pendingRecovery = null;
438
468
  this._recentFillKeysSnapshot = null;
439
469
  this._lastStaleTotalsWarnAt = {};
470
+ this._lastFilledBuyPrice = null;
471
+ this._lastFilledSellPrice = null;
472
+ this._lastFilledPrice = null;
473
+ this._lastFilledType = null;
440
474
  this._metrics = {
441
475
  fundRecalcCount: 0,
442
476
  lockAcquisitions: 0,
@@ -545,6 +579,23 @@ class OrderManager {
545
579
  isBroadcastingActive() {
546
580
  return this._broadcastingFlag > 0;
547
581
  }
582
+ /**
583
+ * Whether the owning bot has begun shutting down. Mirrors the
584
+ * bot-level `_shuttingDown` flag (set via setShuttingDown() from
585
+ * DEXBot._shutdownImpl) so manager-level wait loops can bail out
586
+ * instead of proceeding with broadcasts after shutdown completed.
587
+ * @returns {boolean}
588
+ */
589
+ isShuttingDown() {
590
+ return this._shuttingDown === true;
591
+ }
592
+ /**
593
+ * @param {boolean} value
594
+ * @returns {void}
595
+ */
596
+ setShuttingDown(value) {
597
+ this._shuttingDown = value === true;
598
+ }
548
599
  /**
549
600
  * Auto-clears a stale broadcast flag after 120s so a hung
550
601
  * broadcast cannot permanently block rebalancing.
@@ -611,9 +662,14 @@ class OrderManager {
611
662
  * @returns {void}
612
663
  */
613
664
  startBroadcasting() {
614
- if (this._broadcastingFlag === 0) {
615
- this._broadcastingStartedAt = Date.now();
616
- }
665
+ // Refresh the stale-watchdog timestamp on EVERY increment, not just
666
+ // the 0→1 transition: with refcounted holders (e.g. structural
667
+ // Phase-2 reconcile + an inner COW batch) measuring only from the
668
+ // first holder's start lets the 120s watchdog hard-reset while a
669
+ // later holder is still legitimately active. Each new holder
670
+ // restarts the window; the commit-refusal backstop bounds any
671
+ // pathological stream of short holders.
672
+ this._broadcastingStartedAt = Date.now();
617
673
  this._broadcastingFlag++;
618
674
  this.logger?.log?.('[BROADCAST] Flag incremented — fill processing will be deferred until stopBroadcasting()', 'debug');
619
675
  }
@@ -626,6 +682,11 @@ class OrderManager {
626
682
  }
627
683
  if (this._broadcastingFlag === 0) {
628
684
  this._broadcastingStartedAt = 0;
685
+ // Runtime hook (wired by the bot): the fill consumer defers while
686
+ // this flag is held and its defer branch returns without
687
+ // rescheduling, so fills enqueued during a long region would
688
+ // starve indefinitely. Fire once when the last region ends.
689
+ this._onBroadcastRegionEnd?.();
629
690
  }
630
691
  }
631
692
  /**
@@ -1372,71 +1433,187 @@ class OrderManager {
1372
1433
  `Boundary writes should only happen inside _commitWorkingGrid.`, 'warn');
1373
1434
  }
1374
1435
  this.boundaryIdx = newIdx;
1436
+ this._logBoundaryDebug('set', newIdx);
1375
1437
  }
1376
1438
  /**
1377
1439
  * Restore boundaryIdx outside the COW commit path (startup, recovery,
1378
1440
  * disk-load). This is the same write as _setBoundary but without the
1379
1441
  * lock-hold warning — loadGrid / initializeGrid are single-threaded at
1380
1442
  * startup and the boundary is being restored from a known-good snapshot,
1381
- * not committed alongside order mutations.
1443
+ * not committed alongside order mutations. Accepts null to clear the
1444
+ * boundary after a rejected persisted snapshot (P4) so re-derivation
1445
+ * (P3) starts from a clean state.
1382
1446
  */
1383
1447
  _restoreBoundary(newIdx) {
1384
1448
  this.boundaryIdx = newIdx;
1449
+ this._logBoundaryDebug('restore', newIdx);
1385
1450
  }
1386
1451
  /**
1387
- * DEFENSE-IN-DEPTH (runs after every COW commit): verify the committed
1388
- * state still has an intact gap band — implied geometry inside the slot
1389
- * array, and no placed order strictly inside the implied spread.
1390
- *
1391
- * This is a DETECTOR, not a guard: the grid is already committed when it
1392
- * runs. Violations are logged loudly and flagged via _recoveryState
1393
- * (structuralResyncRequested) so the maintenance loop reconciles against
1394
- * the chain. Purpose is any-writer detection — if a path other than the
1395
- * promotion clamp commits a boundary that invades the opposite rail or
1396
- * strands placed orders inside the spread, this fires.
1397
- */
1398
- _assertGapBandIntactPostCommit() {
1452
+ * Observability (fix #4, docs/CONSOLIDATED_ORPHAN_FIX_SUMMARY.md §2): emit the live
1453
+ * boundary whenever roles are (re)assigned, so a silent boundary crawl that
1454
+ * re-rolls slots into the wrong rail is diagnosable in real time. Debug-level
1455
+ * only; no behavioral effect.
1456
+ */
1457
+ _logBoundaryDebug(source, newIdx) {
1399
1458
  try {
1400
- const resolved = resolveGapBand(this);
1401
- if (resolved.boundaryIdx === null || resolved.sellStartIdx === null)
1459
+ const self = this;
1460
+ const boundary = newIdx == null ? self.boundaryIdx : newIdx;
1461
+ if (boundary == null)
1402
1462
  return;
1403
- const { boundaryIdx, sellStartIdx } = resolved;
1404
- const sorted = Array.from(this.orders.values())
1405
- .filter((o) => o && o.price != null && Number.isFinite(Number(o.price)))
1406
- .sort((a, b) => Number(a.price) - Number(b.price));
1407
- const maxIdx = sorted.length - 1;
1408
- const problems = [];
1409
- if (maxIdx < 0 || boundaryIdx < 0 || boundaryIdx > maxIdx) {
1410
- problems.push(`boundary ${boundaryIdx} outside slot range [0, ${maxIdx}]`);
1463
+ const gapSlots = typeof self._gapSlots === 'number' ? self._gapSlots : 0;
1464
+ const sellStart = boundary + 1 + gapSlots;
1465
+ const countActive = (type) => {
1466
+ let n = 0;
1467
+ for (const o of (self.orders?.values?.() || [])) {
1468
+ if (o && o.type === type && o.orderId)
1469
+ n++;
1470
+ }
1471
+ return n;
1472
+ };
1473
+ this.logger?.log?.(`[BOUNDARY] ${source} boundary=${boundary} sellStart=${sellStart} gapSlots=${gapSlots} ` +
1474
+ `spreadCount=${self.currentSpreadCount ?? self.initialSpreadCount ?? '?'} ` +
1475
+ `activeBuy=${countActive(ORDER_TYPES.BUY)} activeSell=${countActive(ORDER_TYPES.SELL)}`, 'debug');
1476
+ }
1477
+ catch {
1478
+ /* observability only — never throw from logging */
1479
+ }
1480
+ }
1481
+ /**
1482
+ * Record last filled price for side-gated guard: only the side of the most recent fill is gated
1483
+ * (BUY after BUY must be lower, SELL after SELL must be higher). Spread-correction bypasses.
1484
+ * Cold start (null) => disabled. Per-type fields kept for observability/back-compat.
1485
+ * @param {Array} fills
1486
+ */
1487
+ recordLastFilledPrices(fills) {
1488
+ if (!Array.isArray(fills) || fills.length === 0)
1489
+ return;
1490
+ let recorded = 0;
1491
+ let lastKind = 'unknown';
1492
+ let lastPriceSrc = 'direct';
1493
+ for (const f of fills) {
1494
+ if (!f || (f.type !== ORDER_TYPES.BUY && f.type !== ORDER_TYPES.SELL))
1495
+ continue;
1496
+ let price = Number(f.price ?? f.order?.price);
1497
+ let priceSrc = 'direct';
1498
+ if (!Number.isFinite(price) || price <= 0) {
1499
+ try {
1500
+ const slot = f.id ? this.orders?.get?.(f.id) : null;
1501
+ price = slot ? Number(slot.price) : null;
1502
+ if (Number.isFinite(price) && price > 0)
1503
+ priceSrc = 'slot-fallback';
1504
+ }
1505
+ catch {
1506
+ price = null;
1507
+ }
1411
1508
  }
1412
- if (sellStartIdx > maxIdx + 1) {
1413
- problems.push(`implied sellStart ${sellStartIdx} beyond slot range (${maxIdx})`);
1509
+ if (!Number.isFinite(price) || price <= 0)
1510
+ continue;
1511
+ this._lastFilledPrice = price;
1512
+ this._lastFilledType = f.type;
1513
+ if (f.type === ORDER_TYPES.BUY)
1514
+ this._lastFilledBuyPrice = price;
1515
+ else if (f.type === ORDER_TYPES.SELL)
1516
+ this._lastFilledSellPrice = price;
1517
+ recorded++;
1518
+ lastKind = f?.isPartial === true ? 'partial' : (f?.isPartial === false ? 'full' : 'unknown');
1519
+ lastPriceSrc = priceSrc;
1520
+ }
1521
+ // One line per fill batch (chunk): the pivot mutations above are otherwise
1522
+ // silent. src records the kind of the pivot-setting fill (full vs
1523
+ // partial — partial pivots behave differently downstream) and whether
1524
+ // its price came from the fill itself or the invisible slot fallback.
1525
+ if (recorded > 0) {
1526
+ try {
1527
+ const fmt = (v) => (v == null || !Number.isFinite(Number(v)) ? 'none' : Format.formatPrice6(Number(v)));
1528
+ this.logger?.log?.(`[FILL-PIVOT] pivot=${fmt(this._lastFilledPrice)}(${this._lastFilledType}) ` +
1529
+ `buy=${fmt(this._lastFilledBuyPrice)} sell=${fmt(this._lastFilledSellPrice)} ` +
1530
+ `n=${recorded} src=${lastKind}/${lastPriceSrc}`, 'info');
1531
+ }
1532
+ catch { /* logging is best-effort */ }
1533
+ }
1534
+ }
1535
+ /**
1536
+ * Seed last-filled guard from the live book at startup. Book-derived
1537
+ * (survives restart), analogous to the deleted MarketAnchor book-seed.
1538
+ * Closes the in-memory-only window where LAST-FILL-GUARD would be
1539
+ * disabled until the first fill arrives.
1540
+ * @param {Array} chainOpenOrders - raw chain open orders (from readOpenOrdersGuarded)
1541
+ */
1542
+ seedLastFilledPricesFromBook(chainOpenOrders) {
1543
+ // Already armed — silent (the guard has a live pivot).
1544
+ if (this._lastFilledPrice != null)
1545
+ return;
1546
+ if (this._lastFilledType != null)
1547
+ return;
1548
+ // Only seed when cold (no fills yet) — don't overwrite a live value.
1549
+ if (this._lastFilledBuyPrice != null && this._lastFilledSellPrice != null)
1550
+ return;
1551
+ if (!Array.isArray(chainOpenOrders) || chainOpenOrders.length === 0) {
1552
+ try {
1553
+ this.logger?.log?.(`[LAST-FILL-GUARD] Book seed skipped: no chain orders; guard remains DISABLED (cold) until the first fill`, 'warn');
1414
1554
  }
1415
- for (let idx = 0; idx < sorted.length; idx++) {
1416
- const o = sorted[idx];
1417
- if (!o.orderId)
1555
+ catch { }
1556
+ return;
1557
+ }
1558
+ try {
1559
+ let maxBuy = null;
1560
+ let minSell = null;
1561
+ for (const o of chainOpenOrders) {
1562
+ const parsed = parseChainOrder(o, this.assets);
1563
+ if (!parsed)
1564
+ continue;
1565
+ const price = Number(parsed.price);
1566
+ if (!Number.isFinite(price) || price <= 0)
1418
1567
  continue;
1419
- if (idx > boundaryIdx && idx < sellStartIdx) {
1420
- problems.push(`placed ${o.type || 'order'} ${o.id} @ ${o.price} sits inside gap band ` +
1421
- `(${boundaryIdx + 1}..${sellStartIdx - 1})`);
1568
+ if (parsed.type === ORDER_TYPES.BUY) {
1569
+ if (maxBuy == null || price > maxBuy)
1570
+ maxBuy = price;
1571
+ }
1572
+ else if (parsed.type === ORDER_TYPES.SELL) {
1573
+ if (minSell == null || price < minSell)
1574
+ minSell = price;
1575
+ }
1576
+ }
1577
+ if (maxBuy != null && this._lastFilledBuyPrice == null)
1578
+ this._lastFilledBuyPrice = maxBuy;
1579
+ if (minSell != null && this._lastFilledSellPrice == null)
1580
+ this._lastFilledSellPrice = minSell;
1581
+ if (this._lastFilledPrice == null) {
1582
+ if (maxBuy != null && minSell != null)
1583
+ this._lastFilledPrice = (maxBuy + minSell) / 2;
1584
+ else if (maxBuy != null)
1585
+ this._lastFilledPrice = maxBuy;
1586
+ else if (minSell != null)
1587
+ this._lastFilledPrice = minSell;
1588
+ }
1589
+ if (this._lastFilledType == null) {
1590
+ if (maxBuy != null && minSell != null) {
1591
+ // Seed is ambiguous (no real fill yet) — leave type null so guard stays disabled until first fill
1592
+ this._lastFilledType = null;
1422
1593
  }
1594
+ else if (maxBuy != null)
1595
+ this._lastFilledType = ORDER_TYPES.BUY;
1596
+ else if (minSell != null)
1597
+ this._lastFilledType = ORDER_TYPES.SELL;
1423
1598
  }
1424
- if (problems.length > 0) {
1425
- this.logger.log(`[COW] GAP-BAND INVARIANT VIOLATION after commit: ${problems.slice(0, 5).join('; ')}` +
1426
- `${problems.length > 5 ? ` (+${problems.length - 5} more)` : ''}. Requesting structural resync.`, 'error');
1427
- if (this._recoveryState) {
1428
- this._recoveryState = {
1429
- ...this._recoveryState,
1430
- lastFailureAt: Date.now(),
1431
- structuralResyncRequested: true
1432
- };
1599
+ if (maxBuy != null || minSell != null) {
1600
+ try {
1601
+ this.logger?.log?.(`[LAST-FILL-GUARD] Seeded from book: lastBuy=${maxBuy} lastSell=${minSell} lastPrice=${this._lastFilledPrice} lastType=${this._lastFilledType}`, 'info');
1433
1602
  }
1603
+ catch { }
1604
+ }
1605
+ // A startup that ends with the guard disabled must say so: the
1606
+ // ambiguous seed (both sides present, no real fill yet) leaves
1607
+ // _lastFilledType null by design, and that hole is otherwise only
1608
+ // inferable by the absence of any log line.
1609
+ if (this._lastFilledPrice == null || this._lastFilledType == null) {
1610
+ try {
1611
+ this.logger?.log?.(`[LAST-FILL-GUARD] Book seed left guard DISABLED (cold): lastBuy=${maxBuy} lastSell=${minSell} lastPrice=${this._lastFilledPrice} lastType=${this._lastFilledType}; guard arms on the first fill`, 'warn');
1612
+ }
1613
+ catch { }
1434
1614
  }
1435
1615
  }
1436
- catch (err) {
1437
- // Detector must never break the commit path.
1438
- this.logger.log(`[COW] Gap-band post-commit check failed: ${getErrorMessage(err)}`, 'warn');
1439
- }
1616
+ catch { }
1440
1617
  }
1441
1618
  /**
1442
1619
  * @returns {Object|null} The consumed signal or null
@@ -1555,13 +1732,66 @@ class OrderManager {
1555
1732
  dustThresholdPercent: this.config?.gridLimits?.PARTIAL_DUST_THRESHOLD_PERCENTAGE
1556
1733
  });
1557
1734
  }
1735
+ /**
1736
+ * Wait (bounded) until any non-COW broadcast/placement activity — the
1737
+ * refcounted _broadcastingFlag — has released. Startup/structural
1738
+ * reconcile Phase-2 placement now holds this flag across its create/update
1739
+ * groups; launching a fill-driven COW rebalance on top of it mutates the
1740
+ * master mid-broadcast, forcing a commit refusal and leaving a
1741
+ * previous-generation order set on chain as duplicate-price orphans
1742
+ * that block placements until drained.
1743
+ *
1744
+ * The wait is capped: after the timeout the rebalance proceeds anyway —
1745
+ * the existing commit-refusal + chain-adoption machinery remains the
1746
+ * backstop, so the worst case equals the pre-guard behavior.
1747
+ *
1748
+ * @param {number} [timeoutMs]
1749
+ * @returns {Promise<{waitedMs: number, timedOut: boolean}>}
1750
+ */
1751
+ async _awaitBroadcastIdle(timeoutMs = 30000) {
1752
+ if (this._shuttingDown) {
1753
+ return { waitedMs: 0, timedOut: false, shutdown: true };
1754
+ }
1755
+ if (!this.isBroadcastingActive())
1756
+ return { waitedMs: 0, timedOut: false, shutdown: false };
1757
+ const startedAt = Date.now();
1758
+ let logged = false;
1759
+ while (this.isBroadcastingActive()) {
1760
+ if (this._shuttingDown) {
1761
+ this.logger?.log?.(`[SAFE-REBALANCE] Broadcast wait aborted: shutdown in progress after ${Date.now() - startedAt}ms`, 'warn');
1762
+ return { waitedMs: Date.now() - startedAt, timedOut: false, shutdown: true };
1763
+ }
1764
+ const waited = Date.now() - startedAt;
1765
+ if (waited >= timeoutMs) {
1766
+ this.logger?.log?.(`[SAFE-REBALANCE] Broadcast still active after ${waited}ms; proceeding (commit-refusal + chain adoption remains the backstop)`, 'warn');
1767
+ return { waitedMs: waited, timedOut: true, shutdown: false };
1768
+ }
1769
+ if (!logged) {
1770
+ this.logger?.log?.('[SAFE-REBALANCE] Deferring: broadcast/placement activity active (startup/structural reconcile or another batch)', 'info');
1771
+ logged = true;
1772
+ }
1773
+ await new Promise(resolve => setTimeout(resolve, 1000));
1774
+ }
1775
+ return { waitedMs: Date.now() - startedAt, timedOut: false, shutdown: false };
1776
+ }
1558
1777
  /**
1559
1778
  * @param {Array} [fills] - Fill events triggering rebalance
1560
1779
  * @param {Set<string>} [excludeIds] - Order IDs to exclude
1780
+ * @param {Object} [options]
1781
+ * @param {boolean} [options.skipBroadcastWait] - Set by the COW re-plan path,
1782
+ * which runs INSIDE its own startBroadcasting() region and must not wait
1783
+ * on the flag it holds itself.
1561
1784
  * @returns {Promise<any>}
1562
1785
  */
1563
- async performSafeRebalance(fills = [], excludeIds = new Set()) {
1786
+ async performSafeRebalance(fills = [], excludeIds = new Set(), options = {}) {
1564
1787
  this.logger.log("[SAFE-REBALANCE] Starting with COW...", "info");
1788
+ if (!options?.skipBroadcastWait) {
1789
+ const idle = await this._awaitBroadcastIdle();
1790
+ if (idle.shutdown || this._shuttingDown) {
1791
+ this.logger.log('[SAFE-REBALANCE] Aborting rebalance: shutdown in progress', 'warn');
1792
+ return buildAbortedResult('Shutdown in progress — safe rebalance aborted');
1793
+ }
1794
+ }
1565
1795
  return await this._gridLock.acquire(async () => {
1566
1796
  return await this._applySafeRebalanceCOW(fills, excludeIds);
1567
1797
  });
@@ -1719,7 +1949,6 @@ class OrderManager {
1719
1949
  releaseStackEntry();
1720
1950
  return false;
1721
1951
  }
1722
- this._assertGapBandIntactPostCommit();
1723
1952
  try {
1724
1953
  if (!skipRecalc) {
1725
1954
  // If the last accounting failure was due to stale accountTotals,