dexbot 1.6.3 → 1.6.4

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 (204) hide show
  1. package/CHANGELOG.md +38 -2
  2. package/README.md +11 -7
  3. package/analysis/README.md +18 -12
  4. package/analysis/account_resolver.ts +171 -0
  5. package/analysis/ama_fitting/analyze_lambda_vs_slow.ts +2 -5
  6. package/analysis/ama_fitting/package.json +1 -1
  7. package/analysis/bot_usage/discover_bot_accounts.ts +20 -27
  8. package/analysis/chain_pool.ts +47 -0
  9. package/analysis/chart_utils.ts +33 -20
  10. package/analysis/derivative_chart_generator.ts +2 -3
  11. package/analysis/fills_source.ts +234 -0
  12. package/analysis/grid_correction_check.ts +38 -251
  13. package/analysis/trade_profitability.ts +169 -304
  14. package/analysis/tradingview/README.md +2 -2
  15. package/analysis/tradingview/tradingview_uplot_chart_generator.ts +3 -4
  16. package/analysis/trend_detection/dynamic_weight_chart_generator.ts +2 -3
  17. package/analysis/trend_detection/kalman_chart_generator.ts +2 -3
  18. package/analysis/trend_detection/package.json +1 -1
  19. package/analysis/trend_detection/regime_chart_generator.ts +2 -3
  20. package/analysis/trend_detection/volatility_chart_generator.ts +2 -3
  21. package/claw/package.json +1 -1
  22. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  23. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  24. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  25. package/dist/analysis/account_resolver.d.ts +43 -0
  26. package/dist/analysis/account_resolver.d.ts.map +1 -0
  27. package/dist/analysis/account_resolver.js +137 -0
  28. package/dist/analysis/account_resolver.js.map +1 -0
  29. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.d.ts.map +1 -1
  30. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js +2 -4
  31. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js.map +1 -1
  32. package/dist/analysis/bot_usage/discover_bot_accounts.js +20 -28
  33. package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
  34. package/dist/analysis/chain_pool.d.ts +10 -0
  35. package/dist/analysis/chain_pool.d.ts.map +1 -0
  36. package/dist/analysis/chain_pool.js +47 -0
  37. package/dist/analysis/chain_pool.js.map +1 -0
  38. package/dist/analysis/chart_utils.d.ts +14 -1
  39. package/dist/analysis/chart_utils.d.ts.map +1 -1
  40. package/dist/analysis/chart_utils.js +32 -22
  41. package/dist/analysis/chart_utils.js.map +1 -1
  42. package/dist/analysis/derivative_chart_generator.d.ts.map +1 -1
  43. package/dist/analysis/derivative_chart_generator.js +2 -3
  44. package/dist/analysis/derivative_chart_generator.js.map +1 -1
  45. package/dist/analysis/fills_source.d.ts +79 -0
  46. package/dist/analysis/fills_source.d.ts.map +1 -0
  47. package/dist/analysis/fills_source.js +186 -0
  48. package/dist/analysis/fills_source.js.map +1 -0
  49. package/dist/analysis/grid_correction_check.d.ts +1 -21
  50. package/dist/analysis/grid_correction_check.d.ts.map +1 -1
  51. package/dist/analysis/grid_correction_check.js +30 -257
  52. package/dist/analysis/grid_correction_check.js.map +1 -1
  53. package/dist/analysis/trade_profitability.d.ts +18 -19
  54. package/dist/analysis/trade_profitability.d.ts.map +1 -1
  55. package/dist/analysis/trade_profitability.js +136 -280
  56. package/dist/analysis/trade_profitability.js.map +1 -1
  57. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
  58. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +3 -4
  59. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
  60. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts.map +1 -1
  61. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js +2 -3
  62. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js.map +1 -1
  63. package/dist/analysis/trend_detection/kalman_chart_generator.d.ts.map +1 -1
  64. package/dist/analysis/trend_detection/kalman_chart_generator.js +2 -3
  65. package/dist/analysis/trend_detection/kalman_chart_generator.js.map +1 -1
  66. package/dist/analysis/trend_detection/regime_chart_generator.d.ts.map +1 -1
  67. package/dist/analysis/trend_detection/regime_chart_generator.js +2 -3
  68. package/dist/analysis/trend_detection/regime_chart_generator.js.map +1 -1
  69. package/dist/analysis/trend_detection/volatility_chart_generator.d.ts.map +1 -1
  70. package/dist/analysis/trend_detection/volatility_chart_generator.js +2 -3
  71. package/dist/analysis/trend_detection/volatility_chart_generator.js.map +1 -1
  72. package/dist/market_adapter/lp_chart_core.d.ts.map +1 -1
  73. package/dist/market_adapter/lp_chart_core.js +2 -3
  74. package/dist/market_adapter/lp_chart_core.js.map +1 -1
  75. package/dist/modules/account_bots.d.ts +1 -18
  76. package/dist/modules/account_bots.d.ts.map +1 -1
  77. package/dist/modules/account_bots.js +1 -1
  78. package/dist/modules/account_bots.js.map +1 -1
  79. package/dist/modules/bitshares-native/serial/operations.d.ts +1 -84
  80. package/dist/modules/bitshares-native/serial/operations.d.ts.map +1 -1
  81. package/dist/modules/bitshares-native/serial/operations.js +1 -38
  82. package/dist/modules/bitshares-native/serial/operations.js.map +1 -1
  83. package/dist/modules/cli_colors.d.ts +0 -1
  84. package/dist/modules/cli_colors.d.ts.map +1 -1
  85. package/dist/modules/constants.js +5 -5
  86. package/dist/modules/constants.js.map +1 -1
  87. package/dist/modules/credential_policy.d.ts +1 -25
  88. package/dist/modules/credential_policy.d.ts.map +1 -1
  89. package/dist/modules/credential_policy.js +1 -1
  90. package/dist/modules/credential_policy.js.map +1 -1
  91. package/dist/modules/credential_runtime.d.ts +1 -6
  92. package/dist/modules/credential_runtime.d.ts.map +1 -1
  93. package/dist/modules/credential_runtime.js +1 -1
  94. package/dist/modules/credential_runtime.js.map +1 -1
  95. package/dist/modules/credential_session_cache.d.ts +1 -5
  96. package/dist/modules/credential_session_cache.d.ts.map +1 -1
  97. package/dist/modules/credential_session_cache.js +1 -1
  98. package/dist/modules/credential_session_cache.js.map +1 -1
  99. package/dist/modules/crypto/pure_secp256k1.d.ts +1 -2
  100. package/dist/modules/crypto/pure_secp256k1.d.ts.map +1 -1
  101. package/dist/modules/crypto/pure_secp256k1.js +1 -1
  102. package/dist/modules/crypto/pure_secp256k1.js.map +1 -1
  103. package/dist/modules/dexbot_class.d.ts +8 -7
  104. package/dist/modules/dexbot_class.d.ts.map +1 -1
  105. package/dist/modules/dexbot_class.js +9 -8
  106. package/dist/modules/dexbot_class.js.map +1 -1
  107. package/dist/modules/dexbot_cow_runtime.d.ts +7 -6
  108. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  109. package/dist/modules/dexbot_cow_runtime.js +8 -7
  110. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  111. package/dist/modules/dexbot_fill_runtime.d.ts +1 -17
  112. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  113. package/dist/modules/dexbot_fill_runtime.js +1 -1
  114. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  115. package/dist/modules/dexbot_maintenance_runtime.d.ts +1 -1
  116. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  117. package/dist/modules/dexbot_maintenance_runtime.js +15 -4
  118. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  119. package/dist/modules/dexbot_state_recovery.d.ts +1 -7
  120. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  121. package/dist/modules/dexbot_state_recovery.js +1 -1
  122. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  123. package/dist/modules/launcher/adapter_requirement.d.ts +1 -5
  124. package/dist/modules/launcher/adapter_requirement.d.ts.map +1 -1
  125. package/dist/modules/launcher/adapter_requirement.js +1 -1
  126. package/dist/modules/launcher/adapter_requirement.js.map +1 -1
  127. package/dist/modules/launcher/bot_supervisor.d.ts +1 -6
  128. package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
  129. package/dist/modules/launcher/bot_supervisor.js +1 -1
  130. package/dist/modules/launcher/bot_supervisor.js.map +1 -1
  131. package/dist/modules/launcher/foreign_cred_daemon.d.ts +1 -3
  132. package/dist/modules/launcher/foreign_cred_daemon.d.ts.map +1 -1
  133. package/dist/modules/launcher/foreign_cred_daemon.js +1 -1
  134. package/dist/modules/launcher/foreign_cred_daemon.js.map +1 -1
  135. package/dist/modules/launcher/launch_modes.d.ts +1 -2
  136. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  137. package/dist/modules/launcher/launch_modes.js +1 -1
  138. package/dist/modules/launcher/launch_modes.js.map +1 -1
  139. package/dist/modules/launcher/market_adapter_runtime.d.ts +1 -3
  140. package/dist/modules/launcher/market_adapter_runtime.d.ts.map +1 -1
  141. package/dist/modules/launcher/market_adapter_runtime.js +1 -1
  142. package/dist/modules/launcher/market_adapter_runtime.js.map +1 -1
  143. package/dist/modules/launcher/monolithic_runtime.d.ts +1 -5
  144. package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
  145. package/dist/modules/launcher/monolithic_runtime.js +1 -1
  146. package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
  147. package/dist/modules/launcher/status_reporting.d.ts +1 -2
  148. package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
  149. package/dist/modules/launcher/status_reporting.js +1 -1
  150. package/dist/modules/launcher/status_reporting.js.map +1 -1
  151. package/dist/modules/order/export.d.ts +1 -8
  152. package/dist/modules/order/export.d.ts.map +1 -1
  153. package/dist/modules/order/export.js +1 -1
  154. package/dist/modules/order/export.js.map +1 -1
  155. package/dist/modules/order/grid.d.ts +7 -0
  156. package/dist/modules/order/grid.d.ts.map +1 -1
  157. package/dist/modules/order/grid.js +52 -98
  158. package/dist/modules/order/grid.js.map +1 -1
  159. package/dist/modules/order/grid_reconcile_internal.d.ts +1 -10
  160. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  161. package/dist/modules/order/grid_reconcile_internal.js +2 -2
  162. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  163. package/dist/modules/order/manager.d.ts +10 -2
  164. package/dist/modules/order/manager.d.ts.map +1 -1
  165. package/dist/modules/order/manager.js +17 -6
  166. package/dist/modules/order/manager.js.map +1 -1
  167. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  168. package/dist/modules/order/sync_engine.js +8 -15
  169. package/dist/modules/order/sync_engine.js.map +1 -1
  170. package/dist/modules/order/utils/math.d.ts +1 -10
  171. package/dist/modules/order/utils/math.d.ts.map +1 -1
  172. package/dist/modules/order/utils/math.js +1 -1
  173. package/dist/modules/order/utils/math.js.map +1 -1
  174. package/dist/modules/order/utils/system.d.ts +0 -15
  175. package/dist/modules/order/utils/system.d.ts.map +1 -1
  176. package/dist/modules/order/utils/system.js +2 -2
  177. package/dist/modules/order/utils/system.js.map +1 -1
  178. package/dist/modules/process_discovery.d.ts +0 -3
  179. package/dist/modules/process_discovery.d.ts.map +1 -1
  180. package/dist/modules/process_discovery.js +1 -1
  181. package/dist/modules/process_discovery.js.map +1 -1
  182. package/dist/modules/utils/chain_logs.d.ts +1 -2
  183. package/dist/modules/utils/chain_logs.d.ts.map +1 -1
  184. package/dist/modules/utils/chain_logs.js +1 -1
  185. package/dist/modules/utils/chain_logs.js.map +1 -1
  186. package/dist/scripts/analyze-git.js +2 -2
  187. package/dist/scripts/analyze-git.js.map +1 -1
  188. package/dist/scripts/update.js +46 -31
  189. package/dist/scripts/update.js.map +1 -1
  190. package/docs/BITSHARES_ONBOARDING.md +110 -40
  191. package/docs/COPY_ON_WRITE_MASTER_PLAN.md +10 -207
  192. package/docs/COW_INVARIANTS.md +16 -10
  193. package/docs/DEXBOT_COMPARISON.md +3 -3
  194. package/docs/EVOLUTION.md +75 -30
  195. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +6 -6
  196. package/docs/GRID_RECALCULATION.md +14 -6
  197. package/docs/GRID_RECONCILE.md +28 -25
  198. package/docs/LIFECYCLE.md +12 -11
  199. package/docs/README.md +48 -42
  200. package/docs/architecture.md +19 -30
  201. package/docs/developer_guide.md +3 -3
  202. package/package.json +1 -1
  203. package/scripts/git-viewer.sh +1 -1
  204. package/tests/README.md +10 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.6.4] - 2026-09-22 - Fund-Driven Spread Correction, GapSlots+1 Batch Cap, Analysis Shared Modules
6
+
7
+ ### 2026-09-21
8
+
9
+ - **Fix(grid)**: include VIRTUAL orders in RMS structural divergence — `compareGrids()` filtered each side's RMS metric to ACTIVE orders only, so persisted-vs-ideal drift in VIRTUAL slots was invisible. VIRTUAL slots carry the planned reservation for unplaced rail slots (`funds.virtual`), and Available = ChainFree − Virtual − fees, so an over-reserved or stale virtual pins spending power without ever tripping a structural check. `filterForRms` now covers ACTIVE + VIRTUAL (PARTIAL/SPREAD stay excluded); ideal sizing already covers every slot of the side, so no other logic changes. A side whose only divergence is in VIRTUAL reservations now yields an RMS metric and can trigger the `rms_structural_grid_resync` full resync; the 14.3% threshold is unchanged. Docs: divergence-filter wording corrected in `docs/architecture.md` and `docs/GRID_RECALCULATION.md` (`modules/order/grid.ts`, `tests/test_grid_comparison.ts`; full suite 291 pass / 0 fail).
10
+
11
+ - **Feat(spread)**: make spread correction purely fund-driven with a balance refresh fallback — `prepareSpreadCorrectionOrders` / `determineOrderSideByFunds` shrank resting orders to manufacture budget (self-funded tail recycling plus a generic redistribution donor loop). Those paths moved inventory within a rail and were gamed by stale-size snapshots: a donor's "current" size could lag a fill, so the recovered budget was phantom and the grid churned instead of healing. All shrink paths removed: corrections are funded only by free available/chainFree; when no side has funds the check returns `fundsExhausted` and the maintenance runtime refreshes account totals + open orders on the next tick via `getTargetedSyncReason` (a stale zero balance is re-read, not met with inventory recycling). New `tests/test_spread_pure_fund_driven.ts` (zero free → no side/create/shrink; funded → spend ≤ free and top-up only); SGP-6 now expects a skip (`modules/order/grid.ts`, `modules/dexbot_maintenance_runtime.ts`, `tests/test_targeted_drift_reconcile.ts`).
12
+
13
+ - **Fix(sync)**: stop false sync-lock "disappeared" logs and correct shadow-lock metric — `_doSyncFromOpenOrders` collected slot ids and chain order ids into `orderIdsToLock`, then re-verified each with `mgr.orders.has(id)`. `mgr.orders` is keyed by slot id only, so every placed order's chain id failed the check and was logged as "disappeared between collection and locking" (~40 lines per sync, 3,461 in one production log) — phantom lines once mistaken for a COW race during a fill-storm investigation, and chain ids were silently dropped from the lock set. Collection and locking are synchronous, so the re-verification guarded an impossible window; the full collected set is now locked. `shadowOrderIds` aliases one order under two keys, so `size` double-counted live orders; new `OrderManager.getActiveShadowLockCount()` feeds the `shadowLocks` / `shadowLocksActive` metrics. New `tests/test_sync_lock_id_verification.ts` (`modules/order/sync_engine.ts`, `modules/order/manager.ts`, `modules/dexbot_maintenance_runtime.ts`).
14
+
15
+ - **Feat(batch)**: raise fill/broadcast batch cap to gapSlots + 1 — `_getGapSlotBatchSize()`, the single source of truth for the fill-batch chunk size and the per-broadcast op cap, now returns the resolved gap-slot count + 1 instead of the count itself, giving one slot of headroom so a queue or operation set that previously spilled into an extra sequential broadcast chunk completes in a single cycle/transaction (1..gapSlots+1 fills → one unified batch; the non-finite/non-positive fallback still returns 1). Docs, constants/cow-runtime comments, and the batch-sizing tests updated to the new contract (`modules/dexbot_class.ts`, `modules/constants.ts`, `modules/dexbot_cow_runtime.ts`, `tests/test_cow_ops_per_broadcast.ts`, `tests/test_fill_batch_chunking.ts`, `tests/sim_batching.ts`).
16
+
17
+ ### 2026-09-20
18
+
19
+ - **Docs(invariant)**: make grid-price invariant doc a fully present-tense contract — drops the mixed-state "What is still open" section (the blocking check has seen live traffic: 75 judgeable checks, 0 violations across four live bot logs), folds each item into its owning section, documents `runFinalPivotGate` as the seventh gate, adds a key-constants table, and cross-links the doc from `LIFECYCLE.md`, `COW_INVARIANTS.md` (INV-GRID-004), `GRID_RECONCILE.md`, `tests/README.md`, and `docs/README.md`; repoints dead `CONSOLIDATED_ORPHAN_FIX_SUMMARY.md` references in `CHANGELOG.md` to the retrospective. Docs-only, no code or test behavior (`docs/GRID_PRICE_INVARIANT.md`, `docs/ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md`).
20
+
21
+ - **Refactor(code)**: purge dead code and internal-only exports across modules — drops the unreachable `processed_transaction` serializer chain in `bitshares-native/serial/operations.ts` (export list 98 → 17 names), the unused `CliColors` type, the never-read `_recentlyRotatedOrderIds` field, plus 56 internal-only exports across account/launcher/order/credential/crypto modules. Symbol visibility only, no behavioral impact (`npx tsc --noEmit` clean; `npm test` 290 pass, 0 fail).
22
+
23
+ - **Docs(engine)**: consolidate order-engine docs and move legacy narrative to the history hubs — replaces `docs/CONSOLIDATED_ORPHAN_FIX_SUMMARY.md` with the synthesis hub `docs/ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md` (§0–§8 numbering preserved: code comments and tests cite it), folds the grid-price-invariant history into retrospective Appendix A, and refreshes drifted refs/symbols across `COW_INVARIANTS.md`, `GRID_RECONCILE.md`, `LIFECYCLE.md`, `FUND_MOVEMENT_AND_ACCOUNTING.md`, and `architecture.md`. History (COW three-era/build-step narrative, memory-only tracking) moves into `docs/EVOLUTION.md`; docs/comment-only change, no runtime behavior.
24
+
25
+ ### 2026-09-18
26
+
27
+ - **Fix(range)**: restore suizidal range threshold to 1.45x — reverts the 1.40 widening so sub-1.45x ranges flag red (suizidal) again and the tight orange zone is 1.45x–1.55x; single-sourced via `RANGE_QUALITY`, bot-editor legend and live range coloring only (`modules/constants.ts`). Trivial: TradingView toolbar tag shortened to `Offset` (tooltip keeps full name).
28
+
29
+ ### 2026-09-16
30
+
31
+ - **Docs(onboarding)**: first-run troubleshooting — npm 12 install-script blocking + manual-build fallback, `dexbot` unknown-command diagnosis (failed link vs stale shell cache), `npm link` EACCES recovery, and pacman `-Syu` for Arch-derivative installs (`docs/BITSHARES_ONBOARDING.md`, `README.md`, `scripts/git-viewer.sh`). Docs-only.
32
+
33
+ - **Refactor(analysis)**: centralize account/node handling, drop `--node` flags — new `analysis/chain_pool.ts` (single read-only chain entry point), `analysis/account_resolver.ts` (single preferredAccount/override decision tree), and `analysis/fills_source.ts` (shared asset-precision table + paginated fill fetch); `trade_profitability.ts` and `grid_correction_check.ts` use the shared modules. Name → accountId resolution always goes through the full built-in pool; breaks anyone passing the undocumented `--node` flag; no production runtime path touched. New `tests/test_analysis_account_resolver.ts`; full suite 290 pass, 0 fail, 15 live skipped.
34
+
35
+ - **Feat(analysis)**: window-aware annualisation of profitability metrics — Sharpe/Sortino now bin the queried window into whole periods (daily for ≥3-day windows, else hourly), zero-filled so flat periods count as 0 PnL, with sample (n−1) variance and √periods-per-year scaling; Sharpe prints with Lo-2002 estimation error, Sortino returns Infinity with no losing periods; new "Projected net PnL" (scored net / scored days × 365); trailing partial periods excluded from ratios, projection, and activity rates via one shared predicate (`analysis/trade_profitability.ts`, `tests/test_trade_profitability_fees.ts`, `analysis/README.md`).
36
+
37
+ - **Feat(charts)**: inline vendored uPlot into generated chart HTML — every generated chart embeds the vendored uPlot JS+CSS via `uplotInlineTags()`, so each export is a single self-contained document rendering anywhere with no CDN, install, or sibling-dir dependency (~53KB per chart; tests assert the banner is present and no `../uplot/` reference remains).
38
+
39
+ - **Fix(update)**: don't fake pm2 process list or claim restarts that failed — when `pm2 jlist` fails the selective restart step no longer substitutes config-active bots for the process list, and `restarted` is set only when at least one `pm2 restart` succeeds, so the monolithic auto-start fallback and the manual-start notice fire correctly on installs without pm2 (`scripts/update.ts`).
40
+
5
41
  ## [1.6.3] - 2026-09-14 - Correction-Queue Staleness Guard, Grid-Checker Price Epochs, Final Pivot Gate
6
42
 
7
43
  ### 2026-09-15
@@ -18,7 +54,7 @@ All notable changes to this project will be documented in this file.
18
54
 
19
55
  ### 2026-09-14
20
56
 
21
- - **Fix(grid)**: enforce grid-price invariant at emission sites + escalate persistent corruption and stranded holds — the engine had two sources of truth for a slot's price — the genesis ladder (`priceForSlot(idx, genesis)`) and the mutable `slot.price` field — and every violation was the second winning over the first. Five mechanisms fed it: orphan adoption overwrote the slot's identity price (S1), a pre-broadcast "freshness" step re-substituted that corrupted price at debug level (S2), guards checked range membership but never grid membership (S3), the fill-guard pivot was written from unvalidated fill prices (S4), and guard bypasses skipped even that (S5). New `checkGridPriceInvariant` / `reportGridPriceInvariant` (`modules/order/utils/order.ts`) require the emitted price for a slot to equal its genesis level (fail-open on everything unjudgeable: no genesis, unparseable id, out-of-ladder index, non-finite price, checker error), enforced blocking at all six emission sites: CREATE, UPDATE (rotation), CREATE-FALLBACK (`modules/dexbot_cow_runtime.ts`); RECONCILE-CREATE, RECONCILE-UPDATE, STARTUP-CREATE (`modules/order/grid_reconcile_internal.ts`). Rotation UPDATE derives the emitted price from the destination slot's genesis level (`deriveRotationPrice`) instead of trusting the planner's carried `newPrice`; both pre-broadcast price substitutions removed (drift reported at warn, never adopted); legacy orphan adoption no longer writes `chainOrder.price` into `slot.price` and gains a rail guard; materialize path derives slot price (and side) from the ladder; `loadGrid` repairs a mismatched slot price from the genesis ladder in both validation modes; fill-guard pivot validated onto the ladder (`resolveOnGridPivot`: near-ladder pivots snap, far off-ladder fail open and are counted as `pivotOffGrid`). Persistent invariant rejection escalates after `GRID_PRICE_INVARIANT_RESYNC_THRESHOLD` consecutive rejecting batches per slot (bot-scoped streak, reset on clean check, 15m cooldown) to `requestStructuralGridResync('grid-price-invariant-violation')`. Out-of-bounds policy: hold and surface — grid geometry is not invalidated by the market leaving it (the removed 5% placement gate is documented as a design deadlock, not a tuning problem). New `docs/GRID_PRICE_INVARIANT.md` plus `docs/README.md` index entry and `CONSOLIDATED_ORPHAN_FIX_SUMMARY.md` status refresh. Tests: new `tests/test_grid_price_invariant_guard.ts` (GPI-001..015) and `tests/test_grid_price_invariant_wiring.ts` (GPI-WIRE-001..008) plus `tests/test_startup_and_guard_log_hygiene.ts` (GPI-LOG-001); every behavioral fix mutation-tested (11 escalation/guard mutations verified); audited over 1,153 slots across five geometries with zero rejections; full suite 282/282 pass (`modules/order/utils/order.ts`, `modules/dexbot_cow_runtime.ts`, `modules/order/grid_reconcile_internal.ts`, `modules/order/grid.ts`).
57
+ - **Fix(grid)**: enforce grid-price invariant at emission sites + escalate persistent corruption and stranded holds — the engine had two sources of truth for a slot's price — the genesis ladder (`priceForSlot(idx, genesis)`) and the mutable `slot.price` field — and every violation was the second winning over the first. Five mechanisms fed it: orphan adoption overwrote the slot's identity price (S1), a pre-broadcast "freshness" step re-substituted that corrupted price at debug level (S2), guards checked range membership but never grid membership (S3), the fill-guard pivot was written from unvalidated fill prices (S4), and guard bypasses skipped even that (S5). New `checkGridPriceInvariant` / `reportGridPriceInvariant` (`modules/order/utils/order.ts`) require the emitted price for a slot to equal its genesis level (fail-open on everything unjudgeable: no genesis, unparseable id, out-of-ladder index, non-finite price, checker error), enforced blocking at all six emission sites: CREATE, UPDATE (rotation), CREATE-FALLBACK (`modules/dexbot_cow_runtime.ts`); RECONCILE-CREATE, RECONCILE-UPDATE, STARTUP-CREATE (`modules/order/grid_reconcile_internal.ts`). Rotation UPDATE derives the emitted price from the destination slot's genesis level (`deriveRotationPrice`) instead of trusting the planner's carried `newPrice`; both pre-broadcast price substitutions removed (drift reported at warn, never adopted); legacy orphan adoption no longer writes `chainOrder.price` into `slot.price` and gains a rail guard; materialize path derives slot price (and side) from the ladder; `loadGrid` repairs a mismatched slot price from the genesis ladder in both validation modes; fill-guard pivot validated onto the ladder (`resolveOnGridPivot`: near-ladder pivots snap, far off-ladder fail open and are counted as `pivotOffGrid`). Persistent invariant rejection escalates after `GRID_PRICE_INVARIANT_RESYNC_THRESHOLD` consecutive rejecting batches per slot (bot-scoped streak, reset on clean check, 15m cooldown) to `requestStructuralGridResync('grid-price-invariant-violation')`. Out-of-bounds policy: hold and surface — grid geometry is not invalidated by the market leaving it (the removed 5% placement gate is documented as a design deadlock, not a tuning problem). New `docs/GRID_PRICE_INVARIANT.md` plus `docs/README.md` index entry and a `CONSOLIDATED_ORPHAN_FIX_SUMMARY.md` status refresh (now absorbed into `docs/ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md`). Tests: new `tests/test_grid_price_invariant_guard.ts` (GPI-001..015) and `tests/test_grid_price_invariant_wiring.ts` (GPI-WIRE-001..008) plus `tests/test_startup_and_guard_log_hygiene.ts` (GPI-LOG-001); every behavioral fix mutation-tested (11 escalation/guard mutations verified); audited over 1,153 slots across five geometries with zero rejections; full suite 282/282 pass (`modules/order/utils/order.ts`, `modules/dexbot_cow_runtime.ts`, `modules/order/grid_reconcile_internal.ts`, `modules/order/grid.ts`).
22
58
 
23
59
  - **Fix(fill)**: retire deferred-retry counter on queue settle + pin COW finally drain — the fill-consumer deferral counter (`_deferredFillRetryWaits`) only retired when a pending retry timer fired against an already-empty queue, so a queue drained by any other path left the counter elevated and the deferral log cadence stuck at warn (every 12th defer) after the backlog cleared. The counter now retires in `consumeFillQueue` at both settle points (top-of-run empty early-return and tail-after-drain); `_deferredFillRetryTimer` declared/initialized on the bot class and cleared on shutdown so the unref'd timer cannot leak across lifecycle restarts. New RETRY-007 pins the counter reset on a clean drain (7 → 0 on settle); RETRY-006 pins the COW batch finally (injected `_ensureCredentialDaemonWritable` throw after `_batchInFlight++` / `_cowBroadcastInFlight` asserts the finally releases both and reschedules the fill consumer exactly once; the COW layer converts the throw into a handled `{executed:false}` abort via `_handleBatchHardAbort`) (`modules/dexbot_fill_runtime.ts`, `modules/dexbot_class.ts`, `tests/test_fill_defer_retry.ts`; neighbors green, `tsc --noEmit` clean).
24
60
 
@@ -38,7 +74,7 @@ All notable changes to this project will be documented in this file.
38
74
 
39
75
  ### 2026-09-13
40
76
 
41
- - **Fix(recovery)**: give the GRID-PRICE-INVARIANT guard and the deferred-hold policy a self-healing exit — both had the same shape (state the guard refuses to emit, but nothing repairs it) and both now escalate to the existing structural resync rather than running forever. A slot rejected as off-grid is skipped and warned, which is correct for a one-off, but the recurring planner carries the slot's price straight from `manager.orders`, so an in-process corruption is re-planned, re-rejected and re-warned every cycle with nothing able to heal it short of a restart — the slot is dead while the bot looks healthy, and the repeated warns train operators to ignore them. The guard now counts **consecutive** rejecting batches per slot (bot-scoped — the monolithic runtime (`dexbot.ts`, the `dexbot` bin) builds EVERY active bot in one process, so a module-level streak would let one bot's rejections push another to the threshold on its FIRST rejection and fire a spurious resync on a healthy bot, and the count also must not outlive a repair; GPI-WIRE-009 pins the cross-bot case) and at `TIMING.GRID_PRICE_INVARIANT_RESYNC_THRESHOLD` (3) fires `requestStructuralGridResync('grid-price-invariant-violation', {slotId, expected, actual, site, streak})`; a clean check clears the streak so escalation means "rejected N consecutive batches", not "N times ever", and `GRID_PRICE_INVARIANT_RESYNC_COOLDOWN_MS` (15m) bounds repeats. Healing in place at rejection time is deliberately NOT done — silently rewriting `slot.price` would erase the diagnostic signal distinguishing the four corruption sources. Separately, out-of-rail orphans hold locked funds and are never auto-cancelled per cycle (correct: cancelling on ambiguous evidence is irreversible), but "held indefinitely" had no exit; a hold whose signature is unchanged for `DEFERRED_HOLD_ESCALATE_MS` (24h) now escalates at `error` to `requestStructuralGridResync('deferred-hold-stale')`, which is safe because the full reset's reconcile is update-first (unmatched orders are price-updated onto rail slots, only true surplus cancelled), so funds are released without inventing a new cancellation policy. Hold age is tracked **per stranded order** (bot-scoped map keyed `id@price/size:reason`), because two simpler clocks were wrong: `manager._lastUnmatchedChainOrdersAt` is unconditionally refreshed on every sync observing any unmatched order, so it records "when we last looked" and an age gate on it could never fire (HOLD-007 pins this); and the whole-held-set signature clock looked correct but was **reset by unrelated churn** — the signature includes every entry's reason, so an unrelated hold flapping in and out restarted the clock every cycle and starved a genuinely stranded order of escalation forever (HOLD-010 pins this; a 6-hourly flap was simulated reporting a clock reset on every tick). A third defect sat in the same path: the signature-change branch `return`ed before the escalation call, so any churn skipped escalation entirely — escalation now runs on both branches, since a signature change is a reason to re-log, not to stop evaluating age. Escalation triggers only on genuinely **stranded** reasons via a narrow allow-list (`isStrandedHoldOrder`: `out-of-rail-deferred`, `out-of-grid-deferred`) rather than the broad `-deferred` non-blocking filter, because a resync cannot end a broadcast region or re-evaluate an uncommitted boundary; `broadcast-active-deferred`, `boundary-hold-trailing-market`, `boundary-unknown-deferred` and `held-plan-unchanged-deferred` are excluded (HOLD-011 pins this), and the allow-list fails closed so a future transient reason is excluded by default. Both escalations reuse the existing debounced, batch-in-flight-aware resync path; a second repair mechanism would duplicate tested machinery. New GPI-WIRE-006..009 (`tests/test_grid_price_invariant_wiring.ts`) and HOLD-006..011 (`tests/test_hold_and_center_guards.ts`); mutation-verified that removing the escalation call, removing the streak reset, disabling the cooldown, removing the hold escalation, re-gating escalation behind the signature early return, reading the hold age from the whole-set clock, and replacing the stranded allow-list with the broad `-deferred` filter are each caught (`modules/constants.ts`, `modules/dexbot_cow_runtime.ts`, `modules/dexbot_maintenance_runtime.ts`, `docs/GRID_PRICE_INVARIANT.md`, `docs/CONSOLIDATED_ORPHAN_FIX_SUMMARY.md`).
77
+ - **Fix(recovery)**: give the GRID-PRICE-INVARIANT guard and the deferred-hold policy a self-healing exit — both had the same shape (state the guard refuses to emit, but nothing repairs it) and both now escalate to the existing structural resync rather than running forever. A slot rejected as off-grid is skipped and warned, which is correct for a one-off, but the recurring planner carries the slot's price straight from `manager.orders`, so an in-process corruption is re-planned, re-rejected and re-warned every cycle with nothing able to heal it short of a restart — the slot is dead while the bot looks healthy, and the repeated warns train operators to ignore them. The guard now counts **consecutive** rejecting batches per slot (bot-scoped — the monolithic runtime (`dexbot.ts`, the `dexbot` bin) builds EVERY active bot in one process, so a module-level streak would let one bot's rejections push another to the threshold on its FIRST rejection and fire a spurious resync on a healthy bot, and the count also must not outlive a repair; GPI-WIRE-009 pins the cross-bot case) and at `TIMING.GRID_PRICE_INVARIANT_RESYNC_THRESHOLD` (3) fires `requestStructuralGridResync('grid-price-invariant-violation', {slotId, expected, actual, site, streak})`; a clean check clears the streak so escalation means "rejected N consecutive batches", not "N times ever", and `GRID_PRICE_INVARIANT_RESYNC_COOLDOWN_MS` (15m) bounds repeats. Healing in place at rejection time is deliberately NOT done — silently rewriting `slot.price` would erase the diagnostic signal distinguishing the four corruption sources. Separately, out-of-rail orphans hold locked funds and are never auto-cancelled per cycle (correct: cancelling on ambiguous evidence is irreversible), but "held indefinitely" had no exit; a hold whose signature is unchanged for `DEFERRED_HOLD_ESCALATE_MS` (24h) now escalates at `error` to `requestStructuralGridResync('deferred-hold-stale')`, which is safe because the full reset's reconcile is update-first (unmatched orders are price-updated onto rail slots, only true surplus cancelled), so funds are released without inventing a new cancellation policy. Hold age is tracked **per stranded order** (bot-scoped map keyed `id@price/size:reason`), because two simpler clocks were wrong: `manager._lastUnmatchedChainOrdersAt` is unconditionally refreshed on every sync observing any unmatched order, so it records "when we last looked" and an age gate on it could never fire (HOLD-007 pins this); and the whole-held-set signature clock looked correct but was **reset by unrelated churn** — the signature includes every entry's reason, so an unrelated hold flapping in and out restarted the clock every cycle and starved a genuinely stranded order of escalation forever (HOLD-010 pins this; a 6-hourly flap was simulated reporting a clock reset on every tick). A third defect sat in the same path: the signature-change branch `return`ed before the escalation call, so any churn skipped escalation entirely — escalation now runs on both branches, since a signature change is a reason to re-log, not to stop evaluating age. Escalation triggers only on genuinely **stranded** reasons via a narrow allow-list (`isStrandedHoldOrder`: `out-of-rail-deferred`, `out-of-grid-deferred`) rather than the broad `-deferred` non-blocking filter, because a resync cannot end a broadcast region or re-evaluate an uncommitted boundary; `broadcast-active-deferred`, `boundary-hold-trailing-market`, `boundary-unknown-deferred` and `held-plan-unchanged-deferred` are excluded (HOLD-011 pins this), and the allow-list fails closed so a future transient reason is excluded by default. Both escalations reuse the existing debounced, batch-in-flight-aware resync path; a second repair mechanism would duplicate tested machinery. New GPI-WIRE-006..009 (`tests/test_grid_price_invariant_wiring.ts`) and HOLD-006..011 (`tests/test_hold_and_center_guards.ts`); mutation-verified that removing the escalation call, removing the streak reset, disabling the cooldown, removing the hold escalation, re-gating escalation behind the signature early return, reading the hold age from the whole-set clock, and replacing the stranded allow-list with the broad `-deferred` filter are each caught (`modules/constants.ts`, `modules/dexbot_cow_runtime.ts`, `modules/dexbot_maintenance_runtime.ts`, `docs/GRID_PRICE_INVARIANT.md`, `docs/ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md` — successor of the then-cited `docs/CONSOLIDATED_ORPHAN_FIX_SUMMARY.md`).
42
78
 
43
79
  - **Fix(shelf)**: exclude shelf orders from every grid count (issue #27 follow-up) — the four shelf follow-ups gated `reserveEdgeIdSet`, `matchedExcess`, and the geometric size recalc to `parseSlotIndex(id) !== null`, but three counters feeding the same decisions never got the gate, so a live fork-kept shelf (non-slot-N id, `ACTIVE` + `orderId`) still poisoned them via the fail-open `isSlotInRail` geometry. `_countActiveOnGrid` inflated `matchedOnGrid` (suppressing `neededSlots`/creates), `chainCount` in `_reconcileStartupSide` counted shelf-bound chain orders (fabricating a `chain - target` surplus that cancelled real window orders), `countLiveGridOrders` masked window shortfalls in targeted sync, and `_getOnChainOrders` plus the spread `buyCount`/`sellCount` masked `oneSideEmpty`. All now apply the same slot-N gate (no-op on grids that only mint slot-N ids); shelf stays in the geometric-recalc denominator by design (conservative sizing direction). `_pickVirtualSlotsToActivate` and the `getInitialOrdersToActivate` window picks are gated too so a `VIRTUAL` shelf can never consume window activation budget (`modules/order/grid_reconcile_internal.ts`, `modules/dexbot_maintenance_runtime.ts`, `modules/order/grid.ts`, `modules/order/manager.ts`; `tests/test_reserve_orders.ts` shelf expectations corrected to the fixed behavior — chain 12 grid vs target 8 plans 4 rail cancels, grid count converges to 8 with 3 shelf surviving alongside — plus a new block pinning `_countActiveOnGrid`, startup create non-suppression, targeted-sync `buy 5/8` firing through shelf, and slot-N-only activation picks; `tests/test_startup_decision.ts`, `tests/test_resync_balance_fix.ts`, `tests/test_resync_duplicate_race.ts` fixtures migrated from ad-hoc ids to `slot-N` ids per the production invariant; full suite exit 0, zero failures).
44
80
 
package/README.md CHANGED
@@ -65,8 +65,8 @@ Install Git:
65
65
  sudo apt-get update
66
66
  sudo apt-get install git
67
67
 
68
- # Arch/Manjaro
69
- sudo pacman -S git
68
+ # Arch/Manjaro/CachyOS
69
+ sudo pacman -Syu git
70
70
 
71
71
  # Fedora/RHEL
72
72
  sudo dnf install git
@@ -122,6 +122,10 @@ npm install
122
122
  npm link
123
123
  ```
124
124
 
125
+ If `npm link` fails with `EACCES` or `dexbot` isn't found afterwards, see the
126
+ [first-run troubleshooting](docs/BITSHARES_ONBOARDING.md#troubleshooting-first-run-mistakes)
127
+ section in the onboarding guide.
128
+
125
129
  ### Where your data lives
126
130
 
127
131
  Both installs use the same CLI and store all user state — keys, `bots.json`, logs — in `~/.config/dexbot2/profiles` (Windows: `%USERPROFILE%\.config\dexbot2\profiles`). State lives outside the repo/package tree, so it survives reinstalls and `npm update -g`. A source checkout that already contains a populated `profiles/` directory keeps using it. Override the location with `DEXBOT_PROFILE_ROOT` (see the [developer guide](docs/developer_guide.md)).
@@ -271,7 +275,7 @@ dexbot clear # Clear log files
271
275
  dexbot default # Reset settings to defaults
272
276
  ```
273
277
 
274
- ## 🎯 PM2 Process Management
278
+ ## ⚙️ PM2 Process Management
275
279
 
276
280
  PM2 is optional — `dexbot start` is the native solution.
277
281
 
@@ -313,12 +317,12 @@ Logs are written to `logs/` in the profiles directory in all modes: the monolith
313
317
  - **[Docs Index](docs/README.md)** - Main documentation hub
314
318
  - **[Architecture](docs/architecture.md)** - System design, fill processing pipeline, and testing strategy
315
319
  - **[Lifecycle](docs/LIFECYCLE.md)** - End-to-end walkthrough: startup, fill-driven, and AMA-driven flows with diagrams
316
- - **[Evolution Report](docs/EVOLUTION.md)** - Project timeline, architecture phases, and release history
317
- - **[Developer Guide](docs/developer_guide.md)** - Development guide, environment variables, examples, and glossary
318
- - **[Workflow](docs/WORKFLOW.md)** - Project workflow and contribution guide
319
- - **[Fund Movement & Accounting](docs/FUND_MOVEMENT_AND_ACCOUNTING.md)** - Fund accounting, grid topology, and rotation mechanics
320
320
  - **[Copy-on-Write Plan](docs/COPY_ON_WRITE_MASTER_PLAN.md)** - Copy-on-Write grid architecture
321
+ - **[Fund Movement & Accounting](docs/FUND_MOVEMENT_AND_ACCOUNTING.md)** - Fund accounting, grid topology, and rotation mechanics
321
322
  - **[Claw API Boundary](claw/docs/AI_BOT_LIBRARY_API.md)** - Responsibility split between the AI layer and the DEXBot2 execution layer
323
+ - **[Developer Guide](docs/developer_guide.md)** - Development guide, environment variables, examples, and glossary
324
+ - **[Workflow](docs/WORKFLOW.md)** - Project workflow and contribution guide
325
+ - **[Evolution Report](docs/EVOLUTION.md)** - Project timeline, architecture phases, and release history
322
326
 
323
327
  ## 🤝 Contributing
324
328
 
@@ -134,6 +134,8 @@ Fetches `fill_order` operations for a BitShares account from Kibana within a spe
134
134
 
135
135
  **Pipeline:** Kibana fill query → on-chain asset precision resolution → buy/sell classification → chronological matching (sequential LIFO by default) → per-pair summary + optional per-match detail.
136
136
 
137
+ **Account resolution:** an account name (or a bare `1.2.x` ID) is resolved through the shared `account_resolver.ts` helper. When the name matches a bot in `profiles/bots.json`, the resolved ID is stamped onto that entry as `accountId`, so later runs resolve offline; `--refresh-account` forces a fresh lookup.
138
+
137
139
  ```bash
138
140
  # Account by ID, last 7 days (default)
139
141
  node dist/analysis/trade_profitability.js 1.2.123456
@@ -164,7 +166,6 @@ node dist/analysis/trade_profitability.js 1.2.123456 \
164
166
  | `--asset <id>` | all | Filter to one base asset ID |
165
167
  | `--lookup` | off | Legacy (no-op): account names always resolve automatically |
166
168
  | `--refresh-account` | off | Force re-resolution and update the stored `accountId` |
167
- | `--node <url>` | first healthy from built-in pool (10 nodes) | BitShares node for account + asset resolution |
168
169
  | `--csv <file>` | — | Export chronologically sorted trade list |
169
170
  | `--json <file>` | — | Export full analysis with per-pair PnL data |
170
171
  | `--match-mode <mode>` | `sequential` | Matching mode: `sequential` (LIFO, default) or `fifo` |
@@ -177,8 +178,8 @@ node dist/analysis/trade_profitability.js 1.2.123456 \
177
178
  **Asset precision handling:**
178
179
 
179
180
  1. Assets listed in the static `ASSETS` table (BTS, TWENTIX, XBTSX.*, HONEST.*, IOB.*, etc.) resolve instantly.
180
- 2. Unknown assets are resolved on-chain via `get_assets` when `--node` is provided, with results cached at runtime.
181
- 3. If no `--node` is given and an asset is unknown, the fill is **skipped** with a warning (no abort).
181
+ 2. Unknown assets are resolved on-chain via `get_assets` against the built-in node pool, with results cached at runtime.
182
+ 3. If resolution fails or an asset is not found on chain, the affected fills are **skipped** with a warning (no abort).
182
183
 
183
184
  **PnL methodology:**
184
185
 
@@ -206,9 +207,10 @@ node dist/analysis/trade_profitability.js 1.2.123456 \
206
207
  | `Expectancy (gross)` | How much one trade is expected to earn before fees. Positive = edge exists. The `R` version normalises this by the average loss size (reports in R-multiples instead of BTS). The `net` version subtracts fees. |
207
208
  | `Median R` | The middle R-multiple value (half of trades are above, half below). `>1R` / `>2R` = % of trades that earned more than 1× or 2× the average loss. `<-1R` = % that lost more than 1× the average loss. |
208
209
  | `PnL distribution` | Median, P25, P75, Best, Worst — the centre, spread, and extremes of per-trade return %. Not annualised, just per cycle. |
209
- | `Sharpe (ann)` | How consistent your daily net PnL is per unit of volatility. Dimensionful (based on absolute daily PnL, not % returns) — use for ranking your own runs, not comparing across account sizes. |
210
- | `Sortino (ann)` | Same method but only penalises days where you lost money (downside volatility). Higher than the Sharpe is normal; a big gap means most volatility came from winning days. |
211
- | `Max Drawdown` | Largest peak-to-trough equity decline as a % of the peak. How bad things got. |
210
+ | `Sharpe (ann)` | The window's net PnL per unit of volatility, annualised (`mean/std × √periods-per-year`). Binned daily for ≥ 3-day windows, hourly below; every period counts, flat ones as 0 PnL. Shown as `value ± estimation error [bin, n, confidence]` — short windows are low confidence, and only same-bin runs are comparable. Dimensionful (absolute PnL, not % returns). |
211
+ | `Sortino (ann)` | As Sharpe, but only losing periods feed the downside deviation. `∞` means the window had no losing periods. |
212
+ | `Projected net PnL` | Scored-window net PnL scaled linearly to a year (`÷ scored days × 365`) — same whole-period basis as Sharpe/Sortino, so a trailing partial period is excluded from both. A projection, not a forecast. |
213
+ | `Max Drawdown` | Largest peak-to-trough decline of the realised-PnL curve, in quote units (with the same decline as a % of peak cumulative profit). Realised only — open inventory isn't marked. |
212
214
  | `Max Recovery Time` | Longest time (in days) from the deepest point of a drawdown back to a new equity high. |
213
215
  | `Max Consecutive W/L` | Longest streak of winning or losing round-trips. Grouped by sell order, so one order covering multiple buy lots counts as one result. Grid bots naturally cluster wins during trends — streaks of 100-200 are not alarming. |
214
216
  | `Avg hold time` | Average time (hours) between buying an asset and selling it. |
@@ -216,14 +218,14 @@ node dist/analysis/trade_profitability.js 1.2.123456 \
216
218
  | `Sell orders filled` | Number of distinct sell orders that were filled in the period. |
217
219
  | `Partial fills/order` | How many buy lots each sell order consumed (mean, median, max). For a grid bot: 2.0 median means half the orders clear 2 grid levels; 18 max means one big sweep. |
218
220
  | `One-shot orders` | % of orders that matched exactly 1 buy lot. Low % = your grid is thick enough that orders routinely cover multiple levels. |
219
- | `Fills/day` | Average matched lots per calendar day. Raw activity speed. |
220
- | `Avg vol/day` | Average daily trading volume in the quote asset. |
221
+ | `Fills/day` | Average matched lots per scored day, on the same whole-period basis as the ratios above. Raw activity speed. |
222
+ | `Avg vol/day` | Average daily trading volume in the quote asset over that same scored window. |
221
223
 
222
224
  </details>
223
225
 
224
226
  ### Grid Correction Check (`grid_correction_check.ts`)
225
227
 
226
- Validates grid discipline from the same Kibana fill pipeline as `trade_profitability.ts`: two consecutive same-direction fills on a pair must be monotonic — sell prices rising, buy prices falling (equal is OK). An inversion means the bot placed an order below its own previous sell (or above its own previous buy), e.g. an orphaned order filling outside grid accounting. Used as the external regression gate for the orphan-fix plans in `docs/CONSOLIDATED_ORPHAN_FIX_SUMMARY.md`.
228
+ Validates grid discipline from the same Kibana fill pipeline as `trade_profitability.ts`: two consecutive same-direction fills on a pair must be monotonic — sell prices rising, buy prices falling (equal is OK). An inversion means the bot placed an order below its own previous sell (or above its own previous buy), e.g. an orphaned order filling outside grid accounting. Used as the external regression gate for the orphan-fix plans in `docs/ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md`.
227
229
 
228
230
  **Pipeline:** Kibana `fill_order` query (paginated `search_after`) → on-chain asset precision resolution → buy/sell classification → chronological sort → per-order/price-epoch aggregation (partial fills at one price collapsed to weighted-average; repriced order lifetimes kept separate) → consecutive same-direction pair comparison → violation report with daily histogram.
229
231
 
@@ -253,7 +255,6 @@ Exit code `0` = pass, `2` = violations found, `1` = fatal error. Bot keys resolv
253
255
  | `--account <id>` | bot `preferredAccount` | Override account ID or name |
254
256
  | `--lookup` | off | Legacy (no-op): account names always resolve via BitShares node when no stored ID exists |
255
257
  | `--refresh-account` | off | Force re-resolution of `preferredAccount` and update the stored `accountId` when it changed |
256
- | `--node <url>` | first built-in node | Node for account/asset resolution |
257
258
  | `--per-fill` | off | Check at fill granularity instead of per-order aggregated |
258
259
  | `--include-cross-pair` | off | Also check consecutive fills across different pairs |
259
260
  | `--tolerance <pct>` | `0` | Adverse price move (%) forgiven before flagging |
@@ -485,7 +486,12 @@ Details: [bot_fitting/README.md](bot_fitting/README.md)
485
486
  | `price_sources.ts` | Unified candle source abstraction (`json`, `market_adapter`) |
486
487
  | `chart_utils.ts` | Shared chart rendering utilities |
487
488
  | `math_utils.ts` | Shared math utilities |
488
- | `bot_key_utils.ts` | Bot-key resolution and candle file lookup |
489
+ | `bot_key_utils.ts` | Bot-key resolution, candle file lookup, `accountId` persistence (`persistBotAccountId`) |
490
+ | `account_resolver.ts` | Account resolution for all tools: `preferredAccount` / `--account` → `1.2.x`, stamping the result into `profiles/bots.json` |
491
+ | `chain_pool.ts` | Ephemeral read-only chain client over the built-in node pool (account + asset lookups) |
492
+ | `fills_source.ts` | Shared `fill_order` Kibana fetch/query and the static asset-precision table + on-chain cache |
493
+
494
+ On-chain account and asset lookups in the fill-based tools go through `account_resolver.ts` / `fills_source.ts` (both built on `chain_pool.ts`): tool scripts must not open their own read-only clients or carry their own node list. The batch backfill `resolve_bot_accounts.ts` is the exception — it reuses the production chain client over one connection.
489
495
 
490
496
  ## npm Script Shortcuts
491
497
 
@@ -519,5 +525,5 @@ npm run ama:chart:lp-local -- --data market_adapter/data/lp/<pair>/lp_pool_<id>_
519
525
  ## Related Docs
520
526
 
521
527
  - [Market Adapter](../market_adapter/README.md) — live AMA pricing, grid triggers, dynamic weights, and recalc triggers
522
- - [Consolidated Orphan-Fix Summary](../docs/CONSOLIDATED_ORPHAN_FIX_SUMMARY.md) — orphan/gap-band root-cause plans; `grid_correction_check` is their regression gate
528
+ - [Order Engine Retrospective](../docs/ORDER_ENGINE_POST_1.0_RETROSPECTIVE.md) — orphan/gap-band root-cause plans; `grid_correction_check` is their regression gate
523
529
  - [DEXBot2 Tuning Cheat Sheet](../claw/docs/DEXBOT2_TUNING_CHEAT_SHEET.md) — grid tuning reference for live bots
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * SHARED ACCOUNT RESOLUTION (analysis)
5
+ *
6
+ * Central implementation of "preferredAccount / bare account reference →
7
+ * 1.2.x accountId" for every analysis tool, including stamping the resolved ID
8
+ * back into profiles/bots.json so later runs skip the chain lookup.
9
+ *
10
+ * trade_profitability.ts and grid_correction_check.ts used to each carry a
11
+ * private copy of this decision tree, of the chain lookup and of the node
12
+ * handling — all of it lives here now.
13
+ */
14
+
15
+ import { findBotKeyByAccountRef, loadBotMeta, persistBotAccountId } from './bot_key_utils.js';
16
+ import { withReadOnlyClient } from './chain_pool.js';
17
+
18
+ const ACCOUNT_ID_RE = /^1\.2\.\d+$/;
19
+
20
+ /** Chain lookup seam: batch tools can reuse one connection across lookups. */
21
+ type NameLookup = (name: string) => Promise<string | null>;
22
+
23
+ /** Default lookup: ephemeral read-only client over the built-in node pool. */
24
+ async function lookupNameOnChain(name: string): Promise<string | null> {
25
+ try {
26
+ return await withReadOnlyClient(async (client) => {
27
+ const accounts = await client.db('lookup_account_names', [[name]]);
28
+ return Array.isArray(accounts) && accounts[0]?.id ? String(accounts[0].id) : null;
29
+ });
30
+ } catch (e: any) {
31
+ console.warn(` [warn] Account resolution failed: ${e.message}`);
32
+ return null;
33
+ }
34
+ }
35
+
36
+ type ResolveSource = 'typed-id' | 'override-id' | 'override-resolved' | 'preferred-id' | 'stored' | 'resolved';
37
+ type ResolveFailure = 'bot-not-found' | 'no-preferred-account' | 'override-unresolved' | 'name-unresolved';
38
+
39
+ interface ResolveOptions {
40
+ /** Explicit override (--account). Wins over the bot entry and is never persisted. */
41
+ overrideAccount?: string | null;
42
+ /** Ignore a stored accountId and re-resolve on chain (--refresh-account). */
43
+ refresh?: boolean;
44
+ /** Suppress informational output; failures are reported via `reason`. */
45
+ quiet?: boolean;
46
+ /** Chain lookup override (batch tools reuse one connection, tests stub it). */
47
+ lookup?: NameLookup;
48
+ /** Override profiles/bots.json (tests use a temp file). */
49
+ botsFile?: string;
50
+ }
51
+
52
+ interface ResolvedAccount {
53
+ accountId: string | null;
54
+ botKey: string | null;
55
+ botMeta: any | null;
56
+ source: ResolveSource | null;
57
+ reason: ResolveFailure | null;
58
+ }
59
+
60
+ /**
61
+ * Persist a resolved ID onto the bot entry (no-op when it already matches) and
62
+ * report the outcome. `previous` is the stored value the write replaces, if any.
63
+ */
64
+ function persistResolvedAccountId(botKey: string, accountId: string, previous: string | null, quiet: boolean, botsFile?: string): boolean {
65
+ const wrote = persistBotAccountId(botKey, accountId, botsFile);
66
+ if (quiet) return wrote;
67
+ if (wrote) {
68
+ console.log(previous && previous !== accountId
69
+ ? ` Updated stored accountId ${previous} → ${accountId} in profiles/bots.json for '${botKey}'`
70
+ : ` Stored accountId ${accountId} in profiles/bots.json for '${botKey}'`);
71
+ } else if (previous === accountId) {
72
+ console.log(` Stored accountId ${accountId} confirmed up to date`);
73
+ } else {
74
+ console.warn(` [warn] Could not store accountId ${accountId} in profiles/bots.json for '${botKey}'`);
75
+ }
76
+ return wrote;
77
+ }
78
+
79
+ /**
80
+ * Resolve the chain account for a bot entry:
81
+ * explicit override > typed 1.2.x preferredAccount > stored accountId >
82
+ * fresh chain lookup of the name.
83
+ * The resolved ID is persisted onto the entry whenever it differs from the
84
+ * stored one (an explicit override is never persisted — it is not the bot's
85
+ * preferredAccount).
86
+ */
87
+ async function resolveBotAccount(botKey: string, options: ResolveOptions = {}): Promise<ResolvedAccount> {
88
+ const { overrideAccount = null, refresh = false, quiet = false, lookup = lookupNameOnChain, botsFile } = options;
89
+ const botMeta = botKey ? loadBotMeta(botKey, botsFile) : null;
90
+
91
+ // An explicit override always wins and is never persisted.
92
+ const override = overrideAccount != null ? String(overrideAccount).trim() : '';
93
+ if (override) {
94
+ if (ACCOUNT_ID_RE.test(override)) {
95
+ return { accountId: override, botKey, botMeta, source: 'override-id', reason: null };
96
+ }
97
+ if (!quiet) console.log(` Resolving account name '${override}'...`);
98
+ const id = await lookup(override);
99
+ if (!id) return { accountId: null, botKey, botMeta, source: null, reason: 'override-unresolved' };
100
+ if (!quiet) console.log(` → ${id}`);
101
+ return { accountId: String(id), botKey, botMeta, source: 'override-resolved', reason: null };
102
+ }
103
+
104
+ if (!botMeta) return { accountId: null, botKey, botMeta: null, source: null, reason: 'bot-not-found' };
105
+
106
+ const prefRaw = botMeta.preferredAccount != null ? String(botMeta.preferredAccount).trim() : '';
107
+ if (!prefRaw) return { accountId: null, botKey, botMeta, source: null, reason: 'no-preferred-account' };
108
+
109
+ const stored = ACCOUNT_ID_RE.test(String(botMeta.accountId ?? '')) ? String(botMeta.accountId) : null;
110
+
111
+ // A typed 1.2.x preferredAccount is authoritative — the stored accountId is
112
+ // a derived cache and must never override it. Self-heal the cache.
113
+ if (ACCOUNT_ID_RE.test(prefRaw)) {
114
+ if (stored !== prefRaw) persistResolvedAccountId(botKey, prefRaw, stored, quiet, botsFile);
115
+ return { accountId: prefRaw, botKey, botMeta, source: 'preferred-id', reason: null };
116
+ }
117
+
118
+ // Name + fresh-enough cache + no refresh requested: offline-friendly fast path.
119
+ if (stored && !refresh) {
120
+ if (!quiet) console.log(` Using stored accountId ${stored} from profiles/bots.json (no lookup needed; pass --refresh-account to re-verify)`);
121
+ return { accountId: stored, botKey, botMeta, source: 'stored', reason: null };
122
+ }
123
+
124
+ if (!quiet) console.log(` Resolving account name '${prefRaw}'...`);
125
+ const id = await lookup(prefRaw);
126
+ if (!id) return { accountId: null, botKey, botMeta, source: null, reason: 'name-unresolved' };
127
+ if (!quiet) console.log(` → ${id}`);
128
+ persistResolvedAccountId(botKey, String(id), stored, quiet, botsFile);
129
+ return { accountId: String(id), botKey, botMeta, source: 'resolved', reason: null };
130
+ }
131
+
132
+ /**
133
+ * Resolve a bare account reference (1.2.x ID or name) to a chain account ID.
134
+ * When a bot in profiles/bots.json claims the name, the resolved ID is
135
+ * persisted onto that entry via resolveBotAccount.
136
+ */
137
+ async function resolveAccountRef(accountRef: string, options: ResolveOptions = {}): Promise<ResolvedAccount> {
138
+ const ref = String(accountRef ?? '').trim();
139
+ if (ACCOUNT_ID_RE.test(ref)) {
140
+ return { accountId: ref, botKey: null, botMeta: null, source: 'typed-id', reason: null };
141
+ }
142
+
143
+ // A bot claiming this name is the persist target; its preferredAccount is
144
+ // the same name, so resolveBotAccount owns the whole decision tree.
145
+ let match: { botKey: string; meta: any } | null = null;
146
+ try {
147
+ match = findBotKeyByAccountRef(ref, options.botsFile);
148
+ } catch (_) {
149
+ // bots.json issues must never break resolution; fall through to chain.
150
+ }
151
+ if (match) return resolveBotAccount(match.botKey, options);
152
+
153
+ const lookup = options.lookup ?? lookupNameOnChain;
154
+ if (!options.quiet) console.log(` Resolving account name '${ref}'...`);
155
+ const id = await lookup(ref);
156
+ if (!id) return { accountId: null, botKey: null, botMeta: null, source: null, reason: 'name-unresolved' };
157
+ if (!options.quiet) console.log(` → ${id}`);
158
+ return { accountId: String(id), botKey: null, botMeta: null, source: 'resolved', reason: null };
159
+ }
160
+
161
+ export {
162
+ ACCOUNT_ID_RE,
163
+ lookupNameOnChain,
164
+ resolveBotAccount,
165
+ resolveAccountRef,
166
+ ResolveOptions,
167
+ ResolvedAccount,
168
+ ResolveSource,
169
+ ResolveFailure,
170
+ NameLookup,
171
+ };
@@ -10,6 +10,7 @@ import { ensureDir } from '../../modules/order/utils/system.js';
10
10
  import { PATHS } from '../../modules/paths.js';
11
11
  import { MARKET_ADAPTER } from '../../modules/constants.js';
12
12
  import { getErrorMessage } from '../../modules/utils/errors.js';
13
+ import { uplotInlineTags } from '../chart_utils.js';
13
14
 
14
15
  /**
15
16
  * LAMBDA vs SLOW ANALYSIS
@@ -186,9 +187,6 @@ function generateChartHtml(results: any, metricCache: any, fixEr: any, fixFast:
186
187
  const cacheSlow = metricCache.map((m: any) => m.slow);
187
188
  const cacheMove = metricCache.map((m: any) => m.movement);
188
189
 
189
- const uplotCode = fs.readFileSync(path.join(PATHS.ANALYSIS.ASSETS_DIR, 'uPlot.iife.min.js'), 'utf8');
190
- const uplotCSS = fs.readFileSync(path.join(PATHS.ANALYSIS.ASSETS_DIR, 'uPlot.min.css'), 'utf8');
191
-
192
190
  const amaAnnotations = [
193
191
  { label: 'AMA1', lambda: 0.0031, slow: 62.1, color: '#ef5350' },
194
192
  { label: 'AMA2', lambda: 0.0025, slow: 72.0, color: '#fb8c00' },
@@ -203,7 +201,6 @@ function generateChartHtml(results: any, metricCache: any, fixEr: any, fixFast:
203
201
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
204
202
  <title>λ → Slow — ER=${fixEr} Fast=${fixFast}</title>
205
203
  <style>
206
- ${uplotCSS}
207
204
  body { font-family: system-ui, sans-serif; margin: 20px; background: #1e1e2e; color: #cdd6f4; }
208
205
  h1 { font-size: 16px; margin: 0 0 2px 0; }
209
206
  .subtitle { color: #a6adc8; font-size: 12px; margin-bottom: 12px; }
@@ -231,7 +228,7 @@ function generateChartHtml(results: any, metricCache: any, fixEr: any, fixFast:
231
228
  ${amaAnnotations.map(a => `<span class="legend-item"><span class="legend-dot" style="background:${a.color}"></span> ${a.label}</span>`).join('\n')}
232
229
  </div>
233
230
  </div>
234
- <script>${uplotCode}</script>
231
+ ${uplotInlineTags()}
235
232
  <script>
236
233
  (function() {
237
234
  const xs = ${JSON.stringify(xs)};
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ama_fitting",
3
- "version": "1.6.3",
3
+ "version": "1.6.4",
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": {
@@ -1,10 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- import { createRequire } from 'node:module';
4
- const require = createRequire(import.meta.url);
5
-
6
-
7
3
  import { getStorage } from '../../modules/storage/index.js';
4
+ import { withReadOnlyClient } from '../chain_pool.js';
8
5
  const { writeJSON } = getStorage();
9
6
 
10
7
  /**
@@ -58,7 +55,6 @@ import {
58
55
  buildTopUpdaterAccountsQuery,
59
56
  DEFAULT_CONFIG,
60
57
  } from './kibana_bot_queries.js';
61
- import { NODE_MANAGEMENT } from '../../modules/constants.js';
62
58
 
63
59
  // ─── Types ────────────────────────────────────────────────────────────────────
64
60
 
@@ -283,33 +279,30 @@ function dexScore(creates: number, fills: number, _cancels: number, gridScore: n
283
279
  // ─── Account resolution ───────────────────────────────────────────────────────
284
280
 
285
281
  async function resolveNames(ids: string[]): Promise<Record<string, string>> {
286
- const { createReadOnlyClient } = require('../../modules/bitshares-native');
287
- const client = createReadOnlyClient({ nodes: NODE_MANAGEMENT.DEFAULT_NODES });
288
-
289
282
  const map: Record<string, any> = {};
290
283
  try {
291
- await client.connect();
292
- // BitShares db.get_objects accepts an array of IDs
293
- const objects = await client.db('get_objects', [ids]);
294
- for (const obj of (objects ?? [])) {
295
- if (obj?.id && obj?.name) map[obj.id] = obj.name;
296
- }
284
+ // Ephemeral read-only client over the built-in node pool (shared helper).
285
+ await withReadOnlyClient(async (client) => {
286
+ // BitShares db.get_objects accepts an array of IDs
287
+ const objects = await client.db('get_objects', [ids]);
288
+ for (const obj of (objects ?? [])) {
289
+ if (obj?.id && obj?.name) map[obj.id] = obj.name;
290
+ }
297
291
 
298
- // Resolve extra asset precisions while connected
299
- const toCheck = ['IOB.XRP', 'HONEST.MONEY', 'XBTSX.XRP', 'XBTSX.USDT', 'USD', 'CNY'];
300
- for (const sym of toCheck) {
301
- try {
302
- const assets = await client.db('lookup_asset_symbols', [[sym]]);
303
- const a = Array.isArray(assets) ? assets[0] : null;
304
- if (a?.id && !(a.id in ASSET_PRECISION)) {
305
- (ASSET_PRECISION as Record<string, any>)[a.id] = a.precision;
306
- }
307
- } catch (_) {}
308
- }
292
+ // Resolve extra asset precisions while connected
293
+ const toCheck = ['IOB.XRP', 'HONEST.MONEY', 'XBTSX.XRP', 'XBTSX.USDT', 'USD', 'CNY'];
294
+ for (const sym of toCheck) {
295
+ try {
296
+ const assets = await client.db('lookup_asset_symbols', [[sym]]);
297
+ const a = Array.isArray(assets) ? assets[0] : null;
298
+ if (a?.id && !(a.id in ASSET_PRECISION)) {
299
+ (ASSET_PRECISION as Record<string, any>)[a.id] = a.precision;
300
+ }
301
+ } catch (_) {}
302
+ }
303
+ });
309
304
  } catch (e: any) {
310
305
  console.warn(` [warn] Name resolution failed: ${e.message}`);
311
- } finally {
312
- try { client.disconnect(); } catch (_) {}
313
306
  }
314
307
  return map;
315
308
  }
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * SHARED CHAIN POOL (analysis)
5
+ *
6
+ * Single source of truth for the node list and the ephemeral read-only
7
+ * connection used by analysis tools for small on-chain lookups (account name
8
+ * resolution, asset precision resolution).
9
+ *
10
+ * Analysis tools deliberately do NOT use the production singleton client
11
+ * (modules/bitshares_client.ts): they are short-lived processes and must never
12
+ * disturb a running bot's connection state. Instead each lookup opens a fresh
13
+ * read-only client over the built-in node pool, which the native transport
14
+ * races in parallel and then retries sequentially — so one dead node is not
15
+ * fatal.
16
+ */
17
+
18
+ import * as C from '../modules/constants.js';
19
+
20
+ /** Nodes for ephemeral read-only analysis connections (node management pool). */
21
+ function defaultNodePool(): string[] {
22
+ return [...C.NODE_MANAGEMENT.DEFAULT_NODES];
23
+ }
24
+
25
+ /**
26
+ * Run `fn` against a fresh read-only client connected over the built-in pool.
27
+ * Transport INFO logging is silenced for the duration of the short-lived
28
+ * connection and the client is always disconnected afterwards.
29
+ */
30
+ async function withReadOnlyClient<T>(fn: (client: any) => Promise<T>): Promise<T> {
31
+ const { createReadOnlyClient } = await import('../modules/bitshares-native/index.js');
32
+ const client = createReadOnlyClient({ nodes: defaultNodePool() });
33
+ // Suppress transport INFO logs during ephemeral connection:
34
+ // bitshares-native transport logger (new Logger('Transport')) writes
35
+ // "[timestamp] [INFO] [Transport] ..." — silence by raising log level.
36
+ const prevLevel = process.env.LOG_LEVEL;
37
+ process.env.LOG_LEVEL = 'warn';
38
+ try {
39
+ await client.connect();
40
+ return await fn(client);
41
+ } finally {
42
+ try { client.disconnect(); } catch (_) { /* best-effort cleanup */ }
43
+ process.env.LOG_LEVEL = prevLevel;
44
+ }
45
+ }
46
+
47
+ export { defaultNodePool, withReadOnlyClient };