dexbot 1.4.25 → 1.5.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 (217) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +6 -2
  3. package/analysis/README.md +14 -7
  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 +56 -20
  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.map +1 -1
  39. package/dist/analysis/grid_correction_check.js +60 -20
  40. package/dist/analysis/grid_correction_check.js.map +1 -1
  41. package/dist/analysis/resolve_bot_accounts.d.ts +3 -0
  42. package/dist/analysis/resolve_bot_accounts.d.ts.map +1 -0
  43. package/dist/analysis/resolve_bot_accounts.js +216 -0
  44. package/dist/analysis/resolve_bot_accounts.js.map +1 -0
  45. package/dist/analysis/trade_profitability.d.ts.map +1 -1
  46. package/dist/analysis/trade_profitability.js +40 -8
  47. package/dist/analysis/trade_profitability.js.map +1 -1
  48. package/dist/analysis/tradingview/analyze_tradingview.js +2 -2
  49. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
  50. package/dist/bot.d.ts.map +1 -1
  51. package/dist/bot.js +2 -1
  52. package/dist/bot.js.map +1 -1
  53. package/dist/dexbot.d.ts.map +1 -1
  54. package/dist/dexbot.js +73 -17
  55. package/dist/dexbot.js.map +1 -1
  56. package/dist/market_adapter/ama_signal_runner.js +2 -2
  57. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +2 -1
  58. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js.map +1 -1
  59. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +4 -1
  60. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  61. package/dist/market_adapter/inputs/fetch_lp_data.js +3 -2
  62. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  63. package/dist/modules/account_bots.d.ts +42 -1
  64. package/dist/modules/account_bots.d.ts.map +1 -1
  65. package/dist/modules/account_bots.js +168 -17
  66. package/dist/modules/account_bots.js.map +1 -1
  67. package/dist/modules/account_orders.d.ts +7 -1
  68. package/dist/modules/account_orders.d.ts.map +1 -1
  69. package/dist/modules/account_orders.js +34 -1
  70. package/dist/modules/account_orders.js.map +1 -1
  71. package/dist/modules/bitshares_client.d.ts +4 -1
  72. package/dist/modules/bitshares_client.d.ts.map +1 -1
  73. package/dist/modules/bitshares_client.js +12 -4
  74. package/dist/modules/bitshares_client.js.map +1 -1
  75. package/dist/modules/bot_settings.d.ts.map +1 -1
  76. package/dist/modules/bot_settings.js +2 -1
  77. package/dist/modules/bot_settings.js.map +1 -1
  78. package/dist/modules/cli_colors.d.ts +39 -0
  79. package/dist/modules/cli_colors.d.ts.map +1 -0
  80. package/dist/modules/cli_colors.js +42 -0
  81. package/dist/modules/cli_colors.js.map +1 -0
  82. package/dist/modules/constants.d.ts +2 -0
  83. package/dist/modules/constants.d.ts.map +1 -1
  84. package/dist/modules/constants.js +27 -11
  85. package/dist/modules/constants.js.map +1 -1
  86. package/dist/modules/credit_pricing.d.ts +61 -0
  87. package/dist/modules/credit_pricing.d.ts.map +1 -0
  88. package/dist/modules/credit_pricing.js +237 -0
  89. package/dist/modules/credit_pricing.js.map +1 -0
  90. package/dist/modules/credit_runtime.d.ts +1 -0
  91. package/dist/modules/credit_runtime.d.ts.map +1 -1
  92. package/dist/modules/credit_runtime.js +49 -107
  93. package/dist/modules/credit_runtime.js.map +1 -1
  94. package/dist/modules/dexbot_class.d.ts +3 -1
  95. package/dist/modules/dexbot_class.d.ts.map +1 -1
  96. package/dist/modules/dexbot_class.js +22 -6
  97. package/dist/modules/dexbot_class.js.map +1 -1
  98. package/dist/modules/dexbot_cow_runtime.d.ts +36 -2
  99. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  100. package/dist/modules/dexbot_cow_runtime.js +434 -20
  101. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  102. package/dist/modules/dexbot_maintenance_runtime.d.ts +15 -16
  103. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  104. package/dist/modules/dexbot_maintenance_runtime.js +69 -42
  105. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  106. package/dist/modules/dexbot_startup_runtime.d.ts +1 -0
  107. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  108. package/dist/modules/dexbot_startup_runtime.js +12 -1
  109. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  110. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  111. package/dist/modules/dexbot_state_recovery.js +7 -0
  112. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  113. package/dist/modules/fund_registry.d.ts +1 -1
  114. package/dist/modules/fund_registry.js +1 -1
  115. package/dist/modules/launcher/adapter_requirement.d.ts +38 -0
  116. package/dist/modules/launcher/adapter_requirement.d.ts.map +1 -0
  117. package/dist/modules/launcher/adapter_requirement.js +141 -0
  118. package/dist/modules/launcher/adapter_requirement.js.map +1 -0
  119. package/dist/modules/launcher/bot_supervisor.d.ts +2 -2
  120. package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
  121. package/dist/modules/launcher/bot_supervisor.js +32 -3
  122. package/dist/modules/launcher/bot_supervisor.js.map +1 -1
  123. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  124. package/dist/modules/launcher/launch_modes.js +5 -4
  125. package/dist/modules/launcher/launch_modes.js.map +1 -1
  126. package/dist/modules/launcher/monolithic_runtime.d.ts +1 -1
  127. package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
  128. package/dist/modules/launcher/monolithic_runtime.js +12 -8
  129. package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
  130. package/dist/modules/launcher/status_reporting.d.ts +6 -6
  131. package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
  132. package/dist/modules/launcher/status_reporting.js +9 -6
  133. package/dist/modules/launcher/status_reporting.js.map +1 -1
  134. package/dist/modules/order/accounting.d.ts +34 -0
  135. package/dist/modules/order/accounting.d.ts.map +1 -1
  136. package/dist/modules/order/accounting.js +182 -5
  137. package/dist/modules/order/accounting.js.map +1 -1
  138. package/dist/modules/order/export.d.ts +1 -1
  139. package/dist/modules/order/export.js +1 -1
  140. package/dist/modules/order/grid.d.ts.map +1 -1
  141. package/dist/modules/order/grid.js +68 -28
  142. package/dist/modules/order/grid.js.map +1 -1
  143. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  144. package/dist/modules/order/grid_reconcile_internal.js +72 -0
  145. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  146. package/dist/modules/order/logger.d.ts +23 -1
  147. package/dist/modules/order/logger.d.ts.map +1 -1
  148. package/dist/modules/order/logger.js +60 -17
  149. package/dist/modules/order/logger.js.map +1 -1
  150. package/dist/modules/order/manager.d.ts +41 -1
  151. package/dist/modules/order/manager.d.ts.map +1 -1
  152. package/dist/modules/order/manager.js +186 -17
  153. package/dist/modules/order/manager.js.map +1 -1
  154. package/dist/modules/order/strategy.d.ts.map +1 -1
  155. package/dist/modules/order/strategy.js +17 -2
  156. package/dist/modules/order/strategy.js.map +1 -1
  157. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  158. package/dist/modules/order/sync_engine.js +59 -9
  159. package/dist/modules/order/sync_engine.js.map +1 -1
  160. package/dist/modules/order/utils/math.d.ts +42 -1
  161. package/dist/modules/order/utils/math.d.ts.map +1 -1
  162. package/dist/modules/order/utils/math.js +91 -1
  163. package/dist/modules/order/utils/math.js.map +1 -1
  164. package/dist/modules/order/utils/order.d.ts +52 -1
  165. package/dist/modules/order/utils/order.d.ts.map +1 -1
  166. package/dist/modules/order/utils/order.js +137 -7
  167. package/dist/modules/order/utils/order.js.map +1 -1
  168. package/dist/modules/order/utils/system.d.ts +66 -4
  169. package/dist/modules/order/utils/system.d.ts.map +1 -1
  170. package/dist/modules/order/utils/system.js +156 -16
  171. package/dist/modules/order/utils/system.js.map +1 -1
  172. package/dist/modules/order/utils/validate.d.ts +30 -3
  173. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  174. package/dist/modules/order/utils/validate.js +206 -11
  175. package/dist/modules/order/utils/validate.js.map +1 -1
  176. package/dist/modules/utils/chain_logs.d.ts +11 -0
  177. package/dist/modules/utils/chain_logs.d.ts.map +1 -0
  178. package/dist/modules/utils/chain_logs.js +23 -0
  179. package/dist/modules/utils/chain_logs.js.map +1 -0
  180. package/dist/modules/utils/sanitize_key.d.ts +7 -0
  181. package/dist/modules/utils/sanitize_key.d.ts.map +1 -1
  182. package/dist/modules/utils/sanitize_key.js +15 -0
  183. package/dist/modules/utils/sanitize_key.js.map +1 -1
  184. package/dist/pm2.d.ts +11 -1
  185. package/dist/pm2.d.ts.map +1 -1
  186. package/dist/pm2.js +66 -15
  187. package/dist/pm2.js.map +1 -1
  188. package/dist/scripts/analyze-credit.d.ts +4 -0
  189. package/dist/scripts/analyze-credit.d.ts.map +1 -0
  190. package/dist/scripts/analyze-credit.js +760 -0
  191. package/dist/scripts/analyze-credit.js.map +1 -0
  192. package/dist/scripts/analyze-orders.d.ts +1 -10
  193. package/dist/scripts/analyze-orders.d.ts.map +1 -1
  194. package/dist/scripts/analyze-orders.js +11 -34
  195. package/dist/scripts/analyze-orders.js.map +1 -1
  196. package/dist/scripts/test-credit-renewal.d.ts.map +1 -1
  197. package/dist/scripts/test-credit-renewal.js +37 -5
  198. package/dist/scripts/test-credit-renewal.js.map +1 -1
  199. package/dist/scripts/tv.d.ts +11 -0
  200. package/dist/scripts/tv.d.ts.map +1 -0
  201. package/dist/scripts/tv.js +357 -0
  202. package/dist/scripts/tv.js.map +1 -0
  203. package/dist/scripts/update.js +77 -10
  204. package/dist/scripts/update.js.map +1 -1
  205. package/dist/unlock.d.ts.map +1 -1
  206. package/dist/unlock.js +20 -8
  207. package/dist/unlock.js.map +1 -1
  208. package/docs/DEXBOT_COMPARISON.md +3 -3
  209. package/docs/EVOLUTION.md +8 -6
  210. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +3 -1
  211. package/docs/GRID_RECONCILE.md +1 -0
  212. package/docs/LOGGING.md +3 -0
  213. package/docs/MPA_CREDIT_USAGE.md +3 -1
  214. package/docs/README.md +2 -2
  215. package/docs/WORKFLOW.md +3 -1
  216. package/package.json +2 -1
  217. package/scripts/README.md +12 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,60 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.5.1] - 2026-09-06 - Gap-Evacuation Guard Allowance & Rail-Typed Holes
6
+
7
+ ### 2026-09-05
8
+
9
+ - **Fix(sync)**: re-type SPREAD slots before chain-order adoption and honor the apply result — `adoptChainOrderIntoSlot` re-types the placeholder to the chain order's side before activation/precision work, returns a boolean, and callers only record bookkeeping on success; rejected adoptions land in unmatched orders and queue a cancel-only correction so the chain order cannot dangle untracked (`modules/order/sync_engine.ts`).
10
+ - **Fix(accounting)**: rebuild tracked funds from chain during state recovery — verify the balance fetch actually refreshed (`_lastFetchedAt` advanced) before trusting balances and defer the attempt otherwise; when drift persists after sync, `_recalibrateTrackedFundsFromChain` rebuilds the tracked commitment from the fresh chain read (matched slots forced to chain size, fully-consumed and stale-absent slots virtualized under the same recent-commit lag guards as the sync orphan pass, free balances never derived from totals), then re-runs `recalculateFunds` and re-checks drift (`modules/order/accounting.ts`; `tests/test_accounting_logic.ts`, `tests/test_resync_invariants.ts` stubs now simulate a refreshing snapshot).
11
+ - **Fix(cow)**: bit-identical same-price duplicate guard for CREATE slots — new `validateCreateTargetSlots` layer 5 keyed by side + blockchain-int price (`floatToBlockchainInt`, asset-aware with float fallback): first target per price wins, later duplicates reported as `same_batch_price_duplicate` and skipped by the caller; bit-exact by design so adjacent grid levels can never false-positive the way the removed tolerance layer did (`modules/order/utils/validate.ts`, `tests/test_validate_create_target_slots.ts`).
12
+ - **Docs**: record the kept last-fill-guard pivot semantics in the guard's doc comment — pivot is the latest fill of either side and never expires; last sold level floors new sells, buy fills pull the pivot down and re-open the sell side, buy-below-sell is never gated (`modules/dexbot_cow_runtime.ts`).
13
+
14
+ - **Fix(guard)**: allow violation-reducing gap-evacuation rotations past the last-fill guard — new pure `isEvacuationRotationAllowed(oldPrice, oldSize, newPrice, newSize, type, precision?)` in `modules/order/utils/math.ts` (bit-exact blockchain-int size compare, no float epsilon; outward repricing only; rail types only); COW rotation guard honors `origin='gap-evacuation'` on UPDATE actions only via the frozen plan-build B-stamp (`evacBoundary`/`evacGapSlots` stamped in `buildActionsFromPlan`, `reconcileGrid.pairRotations`, `optimizeRebalanceActions`) with fail-closed live-probe fallback (master-grid source read before rotation pre-application); lying "no origin bypass" comments rewritten (`modules/dexbot_cow_runtime.ts`, `modules/order/utils/validate.ts`, new `tests/test_gap_evacuation.ts` EVAC-001..004/009/010).
15
+ - **Fix(holes)**: keep rail-typed VIRTUAL holes across fill/rotation/load cycles — new `toRailHolePlaceholder` (rail type + booked size preserved) used by COW plan pre-apply (`buildCowResultFromPlan`), divergence-COW surplus cancel (`modules/order/utils/system.ts`), and fill-consumed rail slots (`modules/order/strategy.ts`); load-time retype (`modules/order/grid.ts`) and `assignGridRoles` type empty slots by slot-index geometry (in-rail stays BUY/SELL, only true band slots are SPREAD; unparseable ids stay SPREAD); `validateOrder` documents rail-hole preservation; expect a one-time `[GRID-TYPE-CORRECT]` backfill spike on first load of legacy grids (`tests/test_gap_evacuation.ts` EVAC-006/011; `tests/test_grid_bloat.ts` and `tests/test_boundary_restore_validation.ts` updated to the rail-hole contract).
16
+ - **Feat(evac)**: geometry-only gap-evacuation detection + per-slot streak counter — new `detectGapEvacuationCandidates` (slot idx vs frozen boundary/gapSlots, never stored type) and `updateGapEvacuationStreaks` (`manager._gapEvacStreaks`, persisted in the grid snapshot via `storeMasterGrid`/`persistGridSnapshot` and restored on startup/recovery with pruning to loaded slots; cancel markers stay in-memory by design) ticked per `reconcileGrid` plan with `[GAP-EVAC]` warn on stuck slots; new `GAP_EVACUATION_STREAK_THRESHOLD` (2) in `GRID_LIMITS` (`modules/order/utils/order.ts`, `modules/order/manager.ts`, `modules/account_orders.ts`, `modules/order/utils/system.ts`, `modules/dexbot_startup_runtime.ts`, `modules/dexbot_state_recovery.ts`, `modules/constants.ts`, `tests/test_gap_evacuation.ts` EVAC-007/008, `tests/test_gap_evac_persistence.ts` GEP-1..4).
17
+ - **Feat(evac-teeth)**: cancel-only evacuation for orders that stay stranded in the gap band — `evacReady` now propagates out of the COW engine on both success and aborted plans, and the manager queues a correction per stuck slot at `GAP_EVACUATION_CANCEL_THRESHOLD` (3, one warn cycle after the streak threshold) via `_processGapEvacuationTeeth`: re-verifies CURRENT committed geometry + live slot ownership, queues once per slot (`_gapEvacCancelQueued`, marker released when the slot resolves), and uses surplus semantics so the runner cancels the chain order and settles the slot back to a SPREAD placeholder — no re-placement, fee-light (`modules/order/manager.ts`, `modules/constants.ts`, `tests/test_gap_evacuation.ts` EVAC-012..015).
18
+ - **Diag(sync)**: orphan-adoption remap log carries slot geometry (idx/boundary/gap/sellStart/band-vs-rail) so the next re-map incident attributes the path without chain archaeology (`modules/order/sync_engine.ts`).
19
+ - **Fix(tests)**: stub-class hardening for cold-guard batch-summary interference — UNC-015/015c assert on the full warn sequence instead of last-wins capture; `test_resync_invariants` case-4 fetch stub guarantees `_lastFetchedAt` advancement past same-ms collisions (production behavior unchanged; `tests/test_uncertain_broadcast.ts`, `tests/test_resync_invariants.ts`).
20
+ - **Fix(accounting)**: guard the `updateOptimisticFreeBalance` debug line against an unresolvable order side — `formatSizeByOrderType` throws on unknown precision, so a debug-only log could crash the accounting flow; the line now falls back to raw numbers when the side is not BUY/SELL. Repairs the long-standing `test_scaled_spread_correction` Scenario B failure (pre-existing fixture crash since 1.5.0); full suite is green again (`modules/order/accounting.ts`).
21
+
22
+ ### 2026-09-06
23
+
24
+ - **Feat(reconcile)**: refill rail levels vacated by startup re-map (vacate+create atomic) — `_reconcileStartupSide` records, per PROCEEDING update, whether the vacated price exactly matches (`priceSlotEqual`) an empty, sized, in-rail slot of the same side and queues a refill CREATE in the same plan (`source startupVacatedRailRefill`); skipped updates (insufficient balance) vacate nothing, ghost prices (lattice moved) and in-band slots (evacuation/adoption paths own them) are skipped, as are slots already desired for activation (no double-place); refill targets require VIRTUAL state with no `orderId` (CANCELED slots and phantom VIRTUAL-with-orderId excluded) (`modules/order/grid_reconcile_internal.ts`, `tests/test_vacated_rail_refill.ts` VRR-1..7).
25
+ - **Feat(evac)**: persist gap-evacuation streaks across restarts — `storeMasterGrid` accepts `gapEvacStreaks` (sanitized: finite positive counts only; explicit empty map clears the stored entry, `undefined` is a no-op for backward-compatible callers), `persistGridSnapshot` forwards the manager's streak map, `loadGapEvacStreaks` mirrors the other snapshot loaders, and `restoreGapEvacStreaks` (pruned to loaded slots) is wired into the startup active-session path and the recovery full-grid reload (`modules/account_orders.ts`, `modules/order/utils/system.ts`, `modules/dexbot_startup_runtime.ts`, `modules/dexbot_state_recovery.ts`, `tests/test_gap_evac_persistence.ts` GEP-1..4).
26
+ - **Test**: incident-replay integration suite for gap-evacuation acceptance — with boundary 141 / gap 4 / increment 0.3% and three sell actives stranded in the band, the pipeline must plan exactly three stamped evacuation rotations, the plain last-fill guard must veto all three, both bypass routes (frozen B-stamp and live probe) must allow them, and the real pre-apply + working-grid commit must end with the band empty and a visible spread of ~1.5% (REP-001..004; `tests/test_incident_replay.ts`).
27
+ - **Fix(guard)**: harden gap-evacuation stamping and execution re-verification — new exported `isEvacuationStampStillValid` re-verifies B-stamped evacuation UPDATEs against live committed geometry at execution (stale stamps downgrade to the unstamped live probe, which itself now requires valid live evacuation geometry); plan-level `withOrigin` routes gap-evacuation rotations through the real `stampGapEvacuationRotation` (geometry + bit-exact non-growing size with side precision + outward repricing must prove, refusals strip the origin, sourceless rotations stay unstamped, gap-plan CREATEs no longer carry a dead origin); stamp and live probe share bit-exact blockchain-int size semantics via threaded manager assets (`modules/dexbot_cow_runtime.ts`, `modules/order/utils/validate.ts`, `modules/order/manager.ts`, `modules/order/utils/system.ts`, `tests/test_gap_evacuation.ts` EVAC-016..019).
28
+ - **Fix(sync)**: legacy fallback adoption parity — the legacy adoption branch now captures `_applyOrderUpdate`'s result and on rejection queues an adoption-rejected cancelOnly correction (mirroring the genesis branch) with bookkeeping only after a successful apply (`modules/order/sync_engine.ts`).
29
+ - **Fix(cow)**: bit-exact same-batch price duplicate key — the layer-5 duplicate guard keys on `floatToBlockchainInt(price, sidePrecision)` when assets context exists (float fallback preserved for asset-less callers) so distinct prices quantizing to the same on-chain int no longer escape (`modules/order/utils/validate.ts`, `tests/test_validate_create_target_slots.ts`).
30
+
31
+ ## [1.5.0] - 2026-09-05 - Credit Overview & Whitelist-Scoped CR, TradingView Shortcut, Reload Command, Guard & Rotation Hardening
32
+
33
+ ### 2026-09-02
34
+
35
+ - **Fix(editor)**: bold Mountain weight in selector display (`modules/account_bots.ts`).
36
+
37
+ ### 2026-09-03
38
+
39
+ - **Feat(cli)**: add `dexbot credit` live per-bot debt overview and centralize CLI color palette — new `scripts/analyze-credit.ts` (wired as `dexbot credit [<bot>]`) queries live `get_margin_positions` + paginated `get_credit_deals_by_borrower` per preferredAccount and prints summed debt/collateral per asset per bot (lending-asset filtered, shared accounts tagged, chain chatter muted, inclusive-pagination overlap dropped to avoid double-counting); new `modules/cli_colors.ts` (`CLI_COLORS`) is the single source of truth for every ANSI shade repo-wide with no visual change (`scripts/analyze-credit.ts`, `dexbot.ts`, `modules/cli_colors.ts`, `README.md`, `docs/WORKFLOW.md`, `tests/test_analyze_credit.ts`).
40
+
41
+ ### 2026-09-04
42
+
43
+ - **Feat(credit)**: whitelist-scoped CR reporting and shared credit-pricing math — new browser-safe `modules/credit_pricing.ts` holds the canonical math (collateral-map normalization, core/legacy-reversed orientation, offer conversion rates, per-deal and value-weighted average CR, fee helpers) with `credit_runtime.ts` delegating to it (conversion-rate fallback extended offer → pool-derived → universal market price incl. BTS-bridge hops); analyzer per-pair `Curr. CR` and per-bot `Avar. CR` are now scoped to whitelisted `debtPolicy.lending` pairs priced on the current offer, color-coded vs `maxCollateralRatio`, with `Excluded` split by reason and uncapped offer pagination; new `derivePriceViaBridges`/`derivePriceWithBridges` in `modules/order/utils/system.ts` (LP legs bridge only on opt-in) (`modules/credit_pricing.ts`, `modules/credit_runtime.ts`, `modules/order/utils/system.ts`, new `tests/test_credit_pricing.ts`, `tests/test_price_bridges.ts`, `tests/test_credit_conversion_fallback.ts`).
44
+ - **Feat(cli)**: add `dexbot reload` that leaves the credential daemon untouched — mirrors `restart` on every runtime surface (unlock `reload-all` recycles bots + market adapter via SIGUSR2 skipping the daemon re-unlock block, supervisor `reload`/`reload-all`, `pm2 restart` with no `ensureCredentialDaemon`, `reload dexbot-cred` rejected) so bot/adapter recycling no longer costs a password prompt (`unlock.ts`, `modules/launcher/bot_supervisor.ts`, `modules/launcher/launch_modes.ts`, `modules/launcher/monolithic_runtime.ts`, `pm2.ts`, `dexbot.ts`, `README.md`, `docs/WORKFLOW.md`).
45
+ - **Fix(identity)**: enforce case-insensitive bot-name identity across lookups — new `isSameBotName` in `modules/utils/sanitize_key.ts` (sanitized compare with blank-input guard) used by bot_settings, dexbot, unlock, bot, pm2, claw launcher/profiles/credit adapter, maintenance runtime, market-adapter inputs and helper scripts; PM2 stop/delete/restart/reload resolve to the canonical stored name, example pair-derived bot names in comments/help/fixtures replaced with a generic placeholder (published changelog history untouched).
46
+ - **Fix(update)**: run `npm install` only when dependencies actually changed — new `needsDepsInstall(preUpdateHead)` gate in `scripts/update.ts` (install only when node_modules missing/incomplete, manifests changed since pre-update commit, or manifests worktree-dirty; fail-open on git errors) plus a `lockRegenerated` double-install guard (`scripts/update.ts`).
47
+ - **Fix(adapter)**: centralize market-adapter ownership in unlock wrapper on one 1min interval — new `modules/launcher/adapter_requirement.ts` (semantic name:gridPrice fingerprint, corrupt/unreadable bots.json reported not thrown); wrapper exports `DEXBOT_ADAPTER_OWNER=wrapper` so supervised bots skip adapter sync and create no poll timer (wrapper-less modes keep the in-bot fallback); `TIMING.BOTS_CONFIG_POLL_INTERVAL_MS` now 60s shared with the watchdog interval; fixes `''`-fingerprint false-change and corrupt-skip regressions (`modules/launcher/adapter_requirement.ts`, `unlock.ts`, `modules/constants.ts`, `modules/launcher/*`, `modules/dexbot_class.ts`).
48
+ - **Feat(accounts)**: cache bot account IDs in bots.json for offline resolution — byte-preserving in-place `accountId` patch in `analysis/bot_key_utils.ts` (comment/string-aware scanner, re-parse assert, atomic write, full-rewrite fallback) with `persistBotAccountId`/`getStoredBotAccountId`/`findBotKeyByAccountRef`; new `analysis/resolve_bot_accounts.ts` batch backfill CLI (`npm run analysis:resolve-accounts`, `--dry-run`/`--refresh`/`--bot-key`/`--json`); editor stamps IDs via `ensureBotAccountId`, analysis tools and `test-credit-renewal` prefer the cache with `--refresh-account` re-verify; process-wide console floor (`setGlobalConsoleLevel`) silences node-init chatter during supervised lookups (`analysis/bot_key_utils.ts`, `analysis/resolve_bot_accounts.ts`, `modules/account_bots.ts`, `modules/order/logger.ts`, `modules/bitshares_client.ts`, `tests/test_bot_account_id.js`, `tests/test_logger_console_floor.js`).
49
+ - **Fix(guard)**: close stale-pivot race in last-fill guard and harden spread correction — `refreshLastFillPivotFromQueue()` peeks the incoming fill queue before every COW guard check (CREATE, rotation UPDATE, fallback CREATE) and `recordLastFilledPrices()` now runs per chunk so later broadcast chunks gate on the true latest fill; per-action plan-origin stamping keeps spread-correction CREATE bypass while rotation UPDATEs never bypass; `determineOrderSideByFunds()` returns null instead of cross-asset raw-unit compare when priceless with both sides funded; spread check defers on post-gate fills with funds recalc before side decision; batch-level LAST-FILL-GUARD summary + per-batch pivot log lines added (`modules/dexbot_cow_runtime.ts`, `modules/dexbot_class.ts`, `modules/order/grid.ts`, `modules/dexbot_maintenance_runtime.ts`, `modules/order/manager.ts`, new `tests/test_stale_guard_pivot_fixes.js`).
50
+
51
+ ### 2026-09-05
52
+
53
+ - **Feat(cli)**: add `dexbot tv` one-step TradingView chart command — `dexbot tv <bot|pool-id|AssetA/AssetB> [--month N]` pulls 1h candles in chunked 1-month windows and delegates rendering to the TradingView exporter, replacing the manual fetch-then-export two-step; fetcher chunking reused via exported `fetchCandlesSequentially`/`outputPath`/`buildFetchWindowsFromRange`, chain-log mute centralized in `modules/utils/chain_logs.ts`, saved-chart paths print as clickable `file://` links via `toFileUrl` (`scripts/tv.ts`, `dexbot.ts`, `market_adapter/inputs/fetch_lp_data.ts`, `modules/utils/chain_logs.ts`, `analysis/chart_utils.ts`, docs).
54
+ - **Fix(cow)**: clamp COW rotation size to booked remainder for partial surplus — new `clampRotationSizeForPartial` in `modules/order/utils/validate.ts` applied at both rotation-pairing sites (`pairRotations`, `optimizeRebalanceActions`) so rotations sourced from PARTIAL surplus target the booked remainder instead of the hole size, ending the plan→skip→restore loop that stranded holes and left non-contiguous rails (`modules/order/utils/validate.ts`, `modules/order/manager.ts`, `modules/order/utils/system.ts`, `tests/test_cow_master_plan.ts` COW-019).
55
+ - **Fix(credit)**: distinguish Avar/Curr CR lines in `dexbot credit` output — display-only: Avar. CR label orange+bold printed first, Curr. CR label grey+bold with trailing comma, values keep health colors (`scripts/analyze-credit.ts`).
56
+ - **Feat(discovery)**: split HIGH discovery tier into DEXBot2 vs DEXBot1-style by op-77 usage — DEXBot2 broadcasts native `limit_order_update` ops (type 77) for COW re-prices while DEXBot1-style only cancels+recreates, so presence of op 77 in-window assigns flavor; display-only split (score formula unchanged) with Updates column in HIGH tier, summary and `--output-json` carrying flavor/updates (`analysis/bot_usage/kibana_bot_queries.ts`, `analysis/bot_usage/discover_bot_accounts.ts`).
57
+ - **Fix(cli)**: forward `--help`/`-h` to the `credit` and `tv` sub-scripts — the CLI intercepted help flags anywhere in argv and always printed generic help, so `dexbot credit --help` / `dexbot tv --help` never reached the scripts that own their usage text; the dispatcher now resolves the requested command first and lets only scripts with offline help handling (`credit`, `tv`) keep the flag, everything else keeps the generic help (`dexbot.ts`).
58
+
5
59
  ## [1.4.25] - 2026-09-02 - Genesis-Frozen Price-Slot Determinism, Self-Trade & Fill-Guard Hardening, Grid Orphan & Recovery Hardening
6
60
 
7
61
  ### 2026-08-29
package/README.md CHANGED
@@ -235,12 +235,13 @@ Defaults in [`modules/constants.ts`](modules/constants.ts) are overridable at gl
235
235
 
236
236
  ## 🎯 Zero-Dependency Process Management
237
237
 
238
- `dexbot start` is the recommended production runtime (global install). Repo-root users can run `./unlock` instead. It runs the selected bot set as one monolithic bot process, with the credential daemon and market adapter in separate helper processes. Monolithic start/stop/restart controls apply to the whole runtime, not to individual bots.
238
+ `dexbot start` is the recommended production runtime (global install). Repo-root users can run `./unlock` instead. It runs the selected bot set as one monolithic bot process, with the credential daemon and market adapter in separate helper processes. Monolithic start/stop/reload/restart controls apply to the whole runtime, not to individual bots.
239
239
 
240
240
  ```bash
241
241
  dexbot start/stop # Stop/start the monolithic runtime
242
242
  dexbot start --dryrun # Dry-run (no transactions broadcast)
243
- dexbot restart # Restart the monolithic runtime
243
+ dexbot reload # Reload the runtime (leaves credential daemon untouched)
244
+ dexbot restart # Restart the runtime (re-unlocks credential daemon)
244
245
  dexbot delete # Shut down and clean up
245
246
  ```
246
247
 
@@ -260,6 +261,8 @@ dexbot enable {all|<bot>} # Enable bot in config
260
261
  dexbot stat # Runtime status (unlock or PM2)
261
262
  dexbot order [<bot>] # Analyze order grids
262
263
  dexbot order --export # Export as HTML to analysis/charts/
264
+ dexbot tv <bot|pool|A/B> # TradingView 1h chart with AMA overlay (default: 3 months)
265
+ dexbot credit [<bot>] # Live summed MPA + borrowed-credit positions per asset per bot
263
266
 
264
267
  dexbot update # Update DEXBot2
265
268
  dexbot clear # Clear log files
@@ -272,6 +275,7 @@ PM2 is optional — `dexbot start` is the native solution.
272
275
 
273
276
  ```bash
274
277
  dexbot pm2 [<bot>] # Start with PM2
278
+ dexbot pm2 reload {all|<bot>} # Reload managed apps, dexbot-cred untouched
275
279
  dexbot pm2 restart {all|<bot>|dexbot-cred} # Safe restart
276
280
  dexbot pm2 stop {all|<bot>} # Stop (via wrapper)
277
281
  dexbot pm2 delete {all|<bot>} # Delete (via wrapper)
@@ -24,7 +24,7 @@ Tools that inspect DEXBot trading behavior and the market data it operates on. O
24
24
  | [`grid_correction_check.ts`](#grid-correction-check-grid_correction_checkts) | "Is my grid placing orders monotonically?" — sell/buy price inversion detector | `npm run analysis:grid-check -- --bot-key <bot-key>` |
25
25
  | [`analyze_risk_profile.ts`](#risk-profile-analyzer-analyze_risk_profilets) | "How wide should my Safe Range clamps be?" | `node dist/analysis/analyze_risk_profile.js --bot-key <bot-key>` |
26
26
  | [`analyze_trade_heatmap.ts`](#trade-heatmap-analyze_trade_heatmapts) | "Where did trade volume cluster vs the AMA?" | `node dist/analysis/analyze_trade_heatmap.js --bot-key <bot-key>` |
27
- | [`tradingview/analyze_tradingview.ts`](#tradingview-chart-tradingviewanalyze_tradingviewts) | "Just give me a candle chart" | `npm run analysis:tradingview -- --source market_adapter --bot-key <bot-key>` |
27
+ | [`tradingview/analyze_tradingview.ts`](#tradingview-chart-tradingviewanalyze_tradingviewts) | "Just give me a candle chart" | `dexbot tv <bot-key>` |
28
28
  | [`analyze_dynamic_weight.ts`](#dynamic-weight-research-analyze_dynamic_weightts) | "Are buy/sell weights tuned for this regime?" | `node dist/analysis/analyze_dynamic_weight.js --bot-key <bot-key>` |
29
29
  | [`analyze_volatility.ts`](#volatility-analyze_volatilityts) | "Both weights clipped too hard / not enough?" | `node dist/analysis/analyze_volatility.js --bot-key <bot-key>` |
30
30
  | [`analyze_regime.ts`](#supporting-sub-signals) | "Is the trend/chaos gate too aggressive?" | `node dist/analysis/analyze_regime.js --bot-key <bot-key>` |
@@ -138,8 +138,8 @@ Fetches `fill_order` operations for a BitShares account from Kibana within a spe
138
138
  # Account by ID, last 7 days (default)
139
139
  node dist/analysis/trade_profitability.js 1.2.123456
140
140
 
141
- # Account by name with on-chain resolution
142
- node dist/analysis/trade_profitability.js "my-account-name" --lookup --hours 720
141
+ # Account by name (auto-resolved in the background)
142
+ node dist/analysis/trade_profitability.js "my-account-name" --hours 720
143
143
 
144
144
  # Absolute window with asset filter
145
145
  node dist/analysis/trade_profitability.js 1.2.123456 \
@@ -162,7 +162,8 @@ node dist/analysis/trade_profitability.js 1.2.123456 \
162
162
  | `--end <iso>` | — | End time |
163
163
  | `--hours <n>` | `168` (7d) | Lookback hours (alternative to start/end) |
164
164
  | `--asset <id>` | all | Filter to one base asset ID |
165
- | `--lookup` | off | Resolve account name to 1.2.x ID via BitShares node |
165
+ | `--lookup` | off | Legacy (no-op): account names always resolve automatically |
166
+ | `--refresh-account` | off | Force re-resolution and update the stored `accountId` |
166
167
  | `--node <url>` | first healthy from built-in pool (10 nodes) | BitShares node for account + asset resolution |
167
168
  | `--csv <file>` | — | Export chronologically sorted trade list |
168
169
  | `--json <file>` | — | Export full analysis with per-pair PnL data |
@@ -240,7 +241,7 @@ npm run analysis:grid-check -- --bot-key <bot-key> --per-fill --hours 168
240
241
  npm run analysis:grid-check -- --bot-key <bot-key> --hours 168 --tolerance 0.1
241
242
  ```
242
243
 
243
- Exit code `0` = pass, `2` = violations found, `1` = fatal error. Bot keys resolve via `profiles/bots.json` (`--list-bots` to enumerate); the account defaults to the bot's `preferredAccount` and can be overridden with `--account <1.2.x|name>`.
244
+ Exit code `0` = pass, `2` = violations found, `1` = fatal error. Bot keys resolve via `profiles/bots.json` (`--list-bots` to enumerate); the account defaults to the bot's stored `accountId` when present (no chain lookup — the ID is auto-saved next to `preferredAccount` after the first successful name resolution, re-verified with `--refresh-account`), otherwise `preferredAccount` is resolved on-chain, and can be overridden with `--account <1.2.x|name>`.
244
245
 
245
246
  <details><summary>Options (click to expand)</summary>
246
247
 
@@ -250,7 +251,8 @@ Exit code `0` = pass, `2` = violations found, `1` = fatal error. Bot keys resolv
250
251
  | `--hours <n>` | `168` | Lookback hours from now |
251
252
  | `--start <iso>` / `--end <iso>` | — | Absolute time window |
252
253
  | `--account <id>` | bot `preferredAccount` | Override account ID or name |
253
- | `--lookup` | off | Resolve account name to 1.2.x via BitShares node |
254
+ | `--lookup` | off | Legacy (no-op): account names always resolve via BitShares node when no stored ID exists |
255
+ | `--refresh-account` | off | Force re-resolution of `preferredAccount` and update the stored `accountId` when it changed |
254
256
  | `--node <url>` | first built-in node | Node for account/asset resolution |
255
257
  | `--per-fill` | off | Check at fill granularity instead of per-order aggregated |
256
258
  | `--include-cross-pair` | off | Also check consecutive fills across different pairs |
@@ -309,7 +311,12 @@ node dist/analysis/analyze_trade_heatmap.js \
309
311
  Generates a standalone TradingView-style HTML chart with candle OHLC, SMA, AMA, VWMA, and volume panel. See [tradingview/README.md](tradingview/README.md) for full documentation.
310
312
 
311
313
  ```bash
312
- # Bot-key (auto-resolves candle file and AMA settings)
314
+ # Recommended one-step: bot, pool, or pair (fetches candles + renders, default 3 months)
315
+ dexbot tv <bot-key>
316
+ dexbot tv 133
317
+ dexbot tv TOKENA/TOKENB
318
+
319
+ # Manual: bot-key (auto-resolves candle file and AMA settings)
313
320
  npm run analysis:tradingview -- --source market_adapter --bot-key <bot-key>
314
321
 
315
322
  # From an explicit candle file
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ama_fitting",
3
- "version": "1.4.25",
3
+ "version": "1.5.1",
4
4
  "description": "Tools for fitting AMA parameters to market data",
5
5
  "main": "../../dist/analysis/ama_fitting/optimizer_high_resolution.js",
6
6
  "scripts": {
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * node dist/analysis/analyze_derivatives.js \\
16
16
  * --source market_adapter \
17
- * --bot-key XRP-BTS
17
+ * --bot-key AAA-BBB
18
18
  *
19
19
  * Output:
20
20
  * analysis/charts/derivative_chart.html
@@ -4,7 +4,8 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { PATHS } from '../modules/paths.js';
6
6
  import { getStorage } from '../modules/storage/index.js';
7
- const { readJSON } = getStorage();
7
+ const { readJSON, writeJSON } = getStorage();
8
+ import { parseJsonWithComments } from '../modules/order/utils/system.js';
8
9
  import { MARKET_ADAPTER } from '../modules/constants.js';
9
10
  import { loadMarketProfiles } from './tradingview/tradingview_uplot_chart_generator.js';
10
11
  import { sanitizeKey } from '../modules/utils/sanitize_key.js';
@@ -22,7 +23,13 @@ function loadBotSettings(filePath = PATHS.PROFILES.BOTS_JSON) {
22
23
  try {
23
24
  return readJSON(filePath);
24
25
  } catch (_) {
25
- return null;
26
+ // The bot editor reads/writes through a comment-tolerant parser, so
27
+ // operator comments may legally exist — fall back before giving up.
28
+ try {
29
+ return parseJsonWithComments(fs.readFileSync(filePath, 'utf8'));
30
+ } catch (_) {
31
+ return null;
32
+ }
26
33
  }
27
34
  }
28
35
 
@@ -70,6 +77,268 @@ function loadBotMeta(botKey: any, filePath = PATHS.PROFILES.BOTS_JSON) {
70
77
  return loose || null;
71
78
  }
72
79
 
80
+ /**
81
+ * Locate the source span of the entryIndex-th object literal inside the
82
+ * top-level "bots" array of a (possibly comment-bearing) JSON document.
83
+ * Strings and // + block comments are skipped while scanning, so braces
84
+ * inside them never disturb the balance count. Returns null when the
85
+ * document shape is unexpected — callers fall back to a full rewrite.
86
+ */
87
+ function findBotsEntrySpan(raw: string, entryIndex: number): { start: number; end: number } | null {
88
+ const m = /"bots"\s*:\s*\[/.exec(raw);
89
+ if (!m) return null;
90
+ let i = m.index + m[0].length; // just past '['
91
+ const n = raw.length;
92
+ let count = 0;
93
+ const skipWsComments = () => {
94
+ while (i < n) {
95
+ const c = raw[i];
96
+ if (c === ' ' || c === '\t' || c === '\n' || c === '\r') { i++; continue; }
97
+ if (c === '/' && raw[i + 1] === '/') { while (i < n && raw[i] !== '\n') i++; continue; }
98
+ if (c === '/' && raw[i + 1] === '*') { i += 2; while (i < n && !(raw[i] === '*' && raw[i + 1] === '/')) i++; i += 2; continue; }
99
+ break;
100
+ }
101
+ };
102
+ const skipString = () => {
103
+ const q = raw[i];
104
+ i++;
105
+ while (i < n) {
106
+ const c = raw[i];
107
+ if (c === '\\') { i += 2; continue; }
108
+ if (c === q) { i++; return; }
109
+ i++;
110
+ }
111
+ };
112
+ // Skip one JSON value starting at raw[i]; false on malformed input.
113
+ const skipValue = (): boolean => {
114
+ skipWsComments();
115
+ if (i >= n) return false;
116
+ const c = raw[i];
117
+ if (c === '"' || c === "'") { skipString(); return true; }
118
+ if (c === '{' || c === '[') {
119
+ const open = c;
120
+ const close = c === '{' ? '}' : ']';
121
+ i++;
122
+ let depth = 1;
123
+ while (i < n && depth > 0) {
124
+ const d = raw[i];
125
+ if (d === '"' || d === "'") { skipString(); continue; }
126
+ if (d === '/' && raw[i + 1] === '/') { while (i < n && raw[i] !== '\n') i++; continue; }
127
+ if (d === '/' && raw[i + 1] === '*') { i += 2; while (i < n && !(raw[i] === '*' && raw[i + 1] === '/')) i++; i += 2; continue; }
128
+ if (d === open) depth++;
129
+ else if (d === close) depth--;
130
+ i++;
131
+ }
132
+ return depth === 0;
133
+ }
134
+ while (i < n && raw[i] !== ',' && raw[i] !== ']' && raw[i] !== '}') i++;
135
+ return true;
136
+ };
137
+ for (;;) {
138
+ skipWsComments();
139
+ if (i >= n) return null;
140
+ if (raw[i] === ']') return null; // array ended before entryIndex
141
+ if (raw[i] === '{') {
142
+ const start = i;
143
+ if (!skipValue()) return null;
144
+ if (count === entryIndex) return { start, end: i };
145
+ count++;
146
+ } else if (!skipValue()) {
147
+ return null;
148
+ }
149
+ skipWsComments();
150
+ if (i < n && raw[i] === ',') { i++; continue; }
151
+ if (i < n && raw[i] === ']') return null;
152
+ return null; // malformed separator
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Locate the string value of a top-level key inside one bot-entry source
158
+ * span. Unlike a regex, this scan skips over // and block comments as well
159
+ * as string literals, so a key-looking fragment inside a comment (e.g.
160
+ * `// "accountId": "1.2.9999" (old)`) can never be mistaken for the real
161
+ * key. Returns the span of the quoted value (including quotes), or null
162
+ * when the key is absent or its value is not a string.
163
+ */
164
+ function findEntryKeyValueSpan(entrySrc: string, key: string): { valueStart: number; valueEnd: number } | null {
165
+ const n = entrySrc.length;
166
+ const isWs = (c: string) => c === ' ' || c === '\t' || c === '\n' || c === '\r';
167
+ let i = 0;
168
+ let depth = 0;
169
+ while (i < n) {
170
+ const c = entrySrc[i];
171
+ if (isWs(c)) { i++; continue; }
172
+ if (c === '/' && entrySrc[i + 1] === '/') { while (i < n && entrySrc[i] !== '\n') i++; continue; }
173
+ if (c === '/' && entrySrc[i + 1] === '*') { i += 2; while (i < n && !(entrySrc[i] === '*' && entrySrc[i + 1] === '/')) i++; i += 2; continue; }
174
+ if (c === '{' || c === '[') { depth++; i++; continue; }
175
+ if (c === '}' || c === ']') { depth--; i++; continue; }
176
+ if (c !== '"') { i++; continue; }
177
+ // String literal: read it (honoring backslash escapes).
178
+ let j = i + 1;
179
+ let str = '';
180
+ while (j < n) {
181
+ const d = entrySrc[j];
182
+ if (d === '\\') { str += entrySrc.slice(j, j + 2); j += 2; continue; }
183
+ if (d === '"') break;
184
+ str += d; j++;
185
+ }
186
+ if (j >= n) return null;
187
+ const closingQuote = j;
188
+ if (depth !== 1 || str !== key) { i = closingQuote + 1; continue; }
189
+ // Candidate key at top level: expect `:` then a quoted value.
190
+ let k = closingQuote + 1;
191
+ while (k < n && isWs(entrySrc[k])) k++;
192
+ if (entrySrc[k] !== ':') { i = closingQuote + 1; continue; }
193
+ k++;
194
+ while (k < n && isWs(entrySrc[k])) k++;
195
+ if (entrySrc[k] !== '"') { i = closingQuote + 1; continue; }
196
+ let m = k + 1;
197
+ while (m < n) {
198
+ const e = entrySrc[m];
199
+ if (e === '\\') { m += 2; continue; }
200
+ if (e === '"') return { valueStart: k, valueEnd: m + 1 };
201
+ m++;
202
+ }
203
+ return null;
204
+ }
205
+ return null;
206
+ }
207
+
208
+ /**
209
+ * Byte-preserving accountId update: replaces the value in place, or inserts
210
+ * `"accountId"` right after `"preferredAccount"` keeping the line indent, so
211
+ * comments, key order and formatting elsewhere in the file survive. The
212
+ * patched document is re-parsed (comments allowed) and the value asserted
213
+ * before the atomic tmp+rename write. Returns false on any uncertainty —
214
+ * callers fall back to a full rewrite.
215
+ */
216
+ function patchAccountIdInRaw(filePath: string, entryIndex: number, accountId: string): boolean {
217
+ let raw: string;
218
+ try {
219
+ raw = fs.readFileSync(filePath, 'utf8');
220
+ } catch (_) {
221
+ return false;
222
+ }
223
+ const span = findBotsEntrySpan(raw, entryIndex);
224
+ if (!span) return false;
225
+ const entrySrc = raw.slice(span.start, span.end);
226
+ let patchedEntry: string | null = null;
227
+ const idSpan = findEntryKeyValueSpan(entrySrc, 'accountId');
228
+ if (idSpan) {
229
+ patchedEntry = entrySrc.slice(0, idSpan.valueStart)
230
+ + `"${accountId}"`
231
+ + entrySrc.slice(idSpan.valueEnd);
232
+ } else {
233
+ const prefSpan = findEntryKeyValueSpan(entrySrc, 'preferredAccount');
234
+ if (!prefSpan) return false;
235
+ const lineStart = entrySrc.lastIndexOf('\n', prefSpan.valueEnd) + 1;
236
+ const indentMatch = /^[ \t]*/.exec(entrySrc.slice(lineStart));
237
+ const indent = indentMatch ? indentMatch[0] : ' ';
238
+ patchedEntry = entrySrc.slice(0, prefSpan.valueEnd)
239
+ + `,\n${indent}"accountId": "${accountId}"`
240
+ + entrySrc.slice(prefSpan.valueEnd);
241
+ }
242
+ if (!patchedEntry || patchedEntry === entrySrc) return false;
243
+ const patched = raw.slice(0, span.start) + patchedEntry + raw.slice(span.end);
244
+ try {
245
+ const reparsed = parseJsonWithComments(patched);
246
+ const checkEntries = Array.isArray(reparsed?.bots) ? reparsed.bots : [];
247
+ if (checkEntries[entryIndex]?.accountId !== accountId) return false;
248
+ } catch (_) {
249
+ return false;
250
+ }
251
+ const tmpPath = `${filePath}.tmp-persist-${process.pid}-${Date.now()}`;
252
+ try {
253
+ fs.writeFileSync(tmpPath, patched, 'utf8');
254
+ fs.renameSync(tmpPath, filePath);
255
+ return true;
256
+ } catch (_) {
257
+ try { fs.unlinkSync(tmpPath); } catch (_) { /* best-effort cleanup */ }
258
+ return false;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Persist a resolved account name → ID mapping onto the bot entry in
264
+ * profiles/bots.json (`accountId` next to `preferredAccount`), so analysis
265
+ * tools can skip the chain lookup on subsequent runs. No-op when the stored
266
+ * value already matches. Prefers a byte-preserving textual patch (comments,
267
+ * key order and formatting survive); falls back to a full rewrite using the
268
+ * same atomic writeJSON serialization the bot editor uses. Returns true when
269
+ * the file was updated.
270
+ */
271
+ function persistBotAccountId(botKey: any, accountId: string, filePath = PATHS.PROFILES.BOTS_JSON): boolean {
272
+ if (!botKey || !/^1\.2\.\d+$/.test(String(accountId))) return false;
273
+ let settings: any = null;
274
+ try {
275
+ settings = loadBotSettings(filePath);
276
+ } catch (_) {
277
+ return false;
278
+ }
279
+ const entries = Array.isArray(settings?.bots) ? settings.bots : [];
280
+ const normalizedKey = String(botKey).toLowerCase();
281
+ const index = entries.findIndex((bot: any, i: number) => computeBotKey(bot, i) === normalizedKey);
282
+ if (index < 0 || entries[index]?.accountId === accountId) return false;
283
+ try {
284
+ if (patchAccountIdInRaw(filePath, index, String(accountId))) return true;
285
+ } catch (_) {
286
+ // Fall through to the full rewrite below.
287
+ }
288
+ entries[index].accountId = String(accountId);
289
+ try {
290
+ writeJSON(filePath, settings);
291
+ return true;
292
+ } catch (_) {
293
+ return false;
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Return the stored chain account ID for a bot key, or null when none is
299
+ * stamped yet. When expectedRef is given, the stored ID is only returned if
300
+ * the entry's current preferredAccount still matches it — a rename must
301
+ * never silently reuse a stale ID.
302
+ */
303
+ function getStoredBotAccountId(botKey: any, expectedRef?: string | null, filePath = PATHS.PROFILES.BOTS_JSON): string | null {
304
+ if (!botKey) return null;
305
+ let meta: any = null;
306
+ try {
307
+ meta = loadBotMeta(botKey, filePath);
308
+ } catch (_) {
309
+ return null;
310
+ }
311
+ const stored = meta && /^1\.2\.\d+$/.test(String(meta.accountId ?? '')) ? String(meta.accountId) : null;
312
+ if (!stored) return null;
313
+ if (expectedRef != null && String(meta.preferredAccount ?? '').toLowerCase() !== String(expectedRef).toLowerCase()) return null;
314
+ return stored;
315
+ }
316
+
317
+ /**
318
+ * Find the bot entry whose preferredAccount matches a raw account reference
319
+ * (case-insensitive). Used by tools that accept a bare account name to locate
320
+ * the stored accountId and the persist target. Returns null for 1.2.x input
321
+ * or when no bot claims the name.
322
+ */
323
+ function findBotKeyByAccountRef(ref: any, filePath = PATHS.PROFILES.BOTS_JSON): { botKey: string; meta: any } | null {
324
+ if (!ref || /^1\.2\.\d+$/.test(String(ref))) return null;
325
+ let settings: any = null;
326
+ try {
327
+ settings = loadBotSettings(filePath);
328
+ } catch (_) {
329
+ return null;
330
+ }
331
+ const entries = Array.isArray(settings?.bots) ? settings.bots : [];
332
+ const want = String(ref).toLowerCase();
333
+ for (let i = 0; i < entries.length; i++) {
334
+ const entry = entries[i];
335
+ if (entry && String(entry.preferredAccount ?? '').toLowerCase() === want) {
336
+ return { botKey: computeBotKey(entry, i), meta: entry };
337
+ }
338
+ }
339
+ return null;
340
+ }
341
+
73
342
  function resolveAmaConfig(botKey: any) {
74
343
  const botMeta = loadBotMeta(botKey);
75
344
  // Unknown bot key: fall back to the global erSmoothPeriod default (same
@@ -153,5 +422,5 @@ function resolveAmaKey(botKey: any) {
153
422
  return 'AMA3';
154
423
  }
155
424
 
156
- export { loadBotSettings, sanitizeKey, computeBotKey, resolveBotKey, candleFileForBot, resolveCandleFile, loadBotMeta, resolveAmaConfig, resolveAmaKey }
425
+ export { loadBotSettings, sanitizeKey, computeBotKey, resolveBotKey, candleFileForBot, resolveCandleFile, loadBotMeta, getStoredBotAccountId, findBotKeyByAccountRef, persistBotAccountId, resolveAmaConfig, resolveAmaKey }
157
426