dexbot 1.4.25 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +28 -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/bitshares_client.d.ts +4 -1
  68. package/dist/modules/bitshares_client.d.ts.map +1 -1
  69. package/dist/modules/bitshares_client.js +12 -4
  70. package/dist/modules/bitshares_client.js.map +1 -1
  71. package/dist/modules/bot_settings.d.ts.map +1 -1
  72. package/dist/modules/bot_settings.js +2 -1
  73. package/dist/modules/bot_settings.js.map +1 -1
  74. package/dist/modules/cli_colors.d.ts +39 -0
  75. package/dist/modules/cli_colors.d.ts.map +1 -0
  76. package/dist/modules/cli_colors.js +42 -0
  77. package/dist/modules/cli_colors.js.map +1 -0
  78. package/dist/modules/constants.d.ts.map +1 -1
  79. package/dist/modules/constants.js +15 -11
  80. package/dist/modules/constants.js.map +1 -1
  81. package/dist/modules/credit_pricing.d.ts +61 -0
  82. package/dist/modules/credit_pricing.d.ts.map +1 -0
  83. package/dist/modules/credit_pricing.js +237 -0
  84. package/dist/modules/credit_pricing.js.map +1 -0
  85. package/dist/modules/credit_runtime.d.ts +1 -0
  86. package/dist/modules/credit_runtime.d.ts.map +1 -1
  87. package/dist/modules/credit_runtime.js +49 -107
  88. package/dist/modules/credit_runtime.js.map +1 -1
  89. package/dist/modules/dexbot_class.d.ts +2 -1
  90. package/dist/modules/dexbot_class.d.ts.map +1 -1
  91. package/dist/modules/dexbot_class.js +22 -6
  92. package/dist/modules/dexbot_class.js.map +1 -1
  93. package/dist/modules/dexbot_cow_runtime.d.ts +19 -2
  94. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  95. package/dist/modules/dexbot_cow_runtime.js +249 -26
  96. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  97. package/dist/modules/dexbot_maintenance_runtime.d.ts +15 -16
  98. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  99. package/dist/modules/dexbot_maintenance_runtime.js +69 -42
  100. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  101. package/dist/modules/fund_registry.d.ts +1 -1
  102. package/dist/modules/fund_registry.js +1 -1
  103. package/dist/modules/launcher/adapter_requirement.d.ts +38 -0
  104. package/dist/modules/launcher/adapter_requirement.d.ts.map +1 -0
  105. package/dist/modules/launcher/adapter_requirement.js +141 -0
  106. package/dist/modules/launcher/adapter_requirement.js.map +1 -0
  107. package/dist/modules/launcher/bot_supervisor.d.ts +2 -2
  108. package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
  109. package/dist/modules/launcher/bot_supervisor.js +32 -3
  110. package/dist/modules/launcher/bot_supervisor.js.map +1 -1
  111. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  112. package/dist/modules/launcher/launch_modes.js +5 -4
  113. package/dist/modules/launcher/launch_modes.js.map +1 -1
  114. package/dist/modules/launcher/monolithic_runtime.d.ts +1 -1
  115. package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
  116. package/dist/modules/launcher/monolithic_runtime.js +12 -8
  117. package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
  118. package/dist/modules/launcher/status_reporting.d.ts +6 -6
  119. package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
  120. package/dist/modules/launcher/status_reporting.js +9 -6
  121. package/dist/modules/launcher/status_reporting.js.map +1 -1
  122. package/dist/modules/order/export.d.ts +1 -1
  123. package/dist/modules/order/export.js +1 -1
  124. package/dist/modules/order/grid.d.ts.map +1 -1
  125. package/dist/modules/order/grid.js +47 -12
  126. package/dist/modules/order/grid.js.map +1 -1
  127. package/dist/modules/order/logger.d.ts +23 -1
  128. package/dist/modules/order/logger.d.ts.map +1 -1
  129. package/dist/modules/order/logger.js +60 -17
  130. package/dist/modules/order/logger.js.map +1 -1
  131. package/dist/modules/order/manager.d.ts.map +1 -1
  132. package/dist/modules/order/manager.js +43 -3
  133. package/dist/modules/order/manager.js.map +1 -1
  134. package/dist/modules/order/sync_engine.js +1 -1
  135. package/dist/modules/order/utils/system.d.ts +52 -4
  136. package/dist/modules/order/utils/system.d.ts.map +1 -1
  137. package/dist/modules/order/utils/system.js +106 -14
  138. package/dist/modules/order/utils/system.js.map +1 -1
  139. package/dist/modules/order/utils/validate.d.ts +1 -1
  140. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  141. package/dist/modules/order/utils/validate.js +43 -3
  142. package/dist/modules/order/utils/validate.js.map +1 -1
  143. package/dist/modules/utils/chain_logs.d.ts +11 -0
  144. package/dist/modules/utils/chain_logs.d.ts.map +1 -0
  145. package/dist/modules/utils/chain_logs.js +23 -0
  146. package/dist/modules/utils/chain_logs.js.map +1 -0
  147. package/dist/modules/utils/sanitize_key.d.ts +7 -0
  148. package/dist/modules/utils/sanitize_key.d.ts.map +1 -1
  149. package/dist/modules/utils/sanitize_key.js +15 -0
  150. package/dist/modules/utils/sanitize_key.js.map +1 -1
  151. package/dist/pm2.d.ts +11 -1
  152. package/dist/pm2.d.ts.map +1 -1
  153. package/dist/pm2.js +66 -15
  154. package/dist/pm2.js.map +1 -1
  155. package/dist/scripts/analyze-credit.d.ts +4 -0
  156. package/dist/scripts/analyze-credit.d.ts.map +1 -0
  157. package/dist/scripts/analyze-credit.js +760 -0
  158. package/dist/scripts/analyze-credit.js.map +1 -0
  159. package/dist/scripts/analyze-orders.d.ts +1 -10
  160. package/dist/scripts/analyze-orders.d.ts.map +1 -1
  161. package/dist/scripts/analyze-orders.js +11 -34
  162. package/dist/scripts/analyze-orders.js.map +1 -1
  163. package/dist/scripts/test-credit-renewal.d.ts.map +1 -1
  164. package/dist/scripts/test-credit-renewal.js +37 -5
  165. package/dist/scripts/test-credit-renewal.js.map +1 -1
  166. package/dist/scripts/tv.d.ts +11 -0
  167. package/dist/scripts/tv.d.ts.map +1 -0
  168. package/dist/scripts/tv.js +357 -0
  169. package/dist/scripts/tv.js.map +1 -0
  170. package/dist/scripts/update.js +77 -10
  171. package/dist/scripts/update.js.map +1 -1
  172. package/dist/unlock.d.ts.map +1 -1
  173. package/dist/unlock.js +20 -8
  174. package/dist/unlock.js.map +1 -1
  175. package/docs/DEXBOT_COMPARISON.md +3 -3
  176. package/docs/EVOLUTION.md +7 -6
  177. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  178. package/docs/MPA_CREDIT_USAGE.md +3 -1
  179. package/docs/README.md +2 -2
  180. package/docs/WORKFLOW.md +3 -1
  181. package/package.json +2 -1
  182. package/scripts/README.md +12 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.5.0] - 2026-09-05 - Credit Overview & Whitelist-Scoped CR, TradingView Shortcut, Reload Command, Guard & Rotation Hardening
6
+
7
+ ### 2026-09-02
8
+
9
+ - **Fix(editor)**: bold Mountain weight in selector display (`modules/account_bots.ts`).
10
+
11
+ ### 2026-09-03
12
+
13
+ - **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`).
14
+
15
+ ### 2026-09-04
16
+
17
+ - **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`).
18
+ - **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`).
19
+ - **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).
20
+ - **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`).
21
+ - **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`).
22
+ - **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`).
23
+ - **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`).
24
+
25
+ ### 2026-09-05
26
+
27
+ - **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).
28
+ - **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).
29
+ - **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`).
30
+ - **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`).
31
+ - **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`).
32
+
5
33
  ## [1.4.25] - 2026-09-02 - Genesis-Frozen Price-Slot Determinism, Self-Trade & Fill-Guard Hardening, Grid Orphan & Recovery Hardening
6
34
 
7
35
  ### 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.0",
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
 
@@ -18,12 +18,16 @@ const { writeJSON } = getStorage();
18
18
  * Q1 — top 100 accounts by limit_order_create count
19
19
  * Q2 — top 100 accounts by limit_order_cancel count
20
20
  * Q3 — top 100 accounts by fill_order count
21
+ * Q4 — top 200 accounts by limit_order_update count (op 77, DEXBot2 fingerprint)
21
22
  * 2. Merge & pre-filter:
22
23
  * creates ≥ MIN_CREATES (grid analysis determines DEXBot candidacy)
23
24
  * 3. Grid analysis (parallel, batches of 5):
24
25
  * Fetch 200 raw orders per candidate → per-session geometric spacing test
25
26
  * 4. BitShares: resolve account IDs → names (batch db.get_objects call)
26
- * 5. Rank by DEX score and print table
27
+ * 5. Rank by DEX score and print table.
28
+ * HIGH tier (80+) is split by op-77 usage:
29
+ * updates ≥ 1 → DEXBot2 (native limit_order_update in-place re-price)
30
+ * updates = 0 → DEXBot1-style (cancel-only, cancel + recreate)
27
31
  *
28
32
  * Usage:
29
33
  * node dist/analysis/bot_usage/discover_bot_accounts.js
@@ -51,6 +55,7 @@ import {
51
55
  buildTopSellerAccountsQuery,
52
56
  buildTopCancellerAccountsQuery,
53
57
  buildTopFilledAccountsQuery,
58
+ buildTopUpdaterAccountsQuery,
54
59
  DEFAULT_CONFIG,
55
60
  } from './kibana_bot_queries.js';
56
61
  import { NODE_MANAGEMENT } from '../../modules/constants.js';
@@ -61,6 +66,7 @@ interface AccountCounts {
61
66
  creates: number;
62
67
  cancels: number;
63
68
  fills: number;
69
+ updates: number;
64
70
  }
65
71
 
66
72
  interface CandidateInfo {
@@ -68,6 +74,8 @@ interface CandidateInfo {
68
74
  creates: number;
69
75
  cancels: number;
70
76
  fills: number;
77
+ updates: number;
78
+ flavor: 'DEXBot2' | 'DEXBot1' | '';
71
79
  cancelRatio: number;
72
80
  fillRate: number;
73
81
  gridScore: number;
@@ -323,18 +331,21 @@ async function run() {
323
331
 
324
332
  console.log('Phase 1: Querying Kibana for top active accounts...');
325
333
 
326
- const [createRes, cancelRes, fillRes]: any[] = await Promise.all([
334
+ const [createRes, cancelRes, fillRes, updateRes]: any[] = await Promise.all([
327
335
  withRetry(() => kibanaSearch(KIBANA_CFG, buildTopSellerAccountsQuery(lookbackH, 200, opts.minCreates)),
328
336
  'top-seller query'),
329
337
  withRetry(() => kibanaSearch(KIBANA_CFG, buildTopCancellerAccountsQuery(lookbackH, 200, 5)),
330
338
  'top-canceller query'),
331
339
  withRetry(() => kibanaSearch(KIBANA_CFG, buildTopFilledAccountsQuery(lookbackH, 200, 3)),
332
340
  'top-fills query'),
341
+ withRetry(() => kibanaSearch(KIBANA_CFG, buildTopUpdaterAccountsQuery(lookbackH, 200, 1)),
342
+ 'top-updater query'),
333
343
  ]);
334
344
 
335
345
  const createBuckets = createRes?.aggregations?.by_account?.buckets ?? [];
336
346
  const cancelBuckets = cancelRes?.aggregations?.by_account?.buckets ?? [];
337
347
  const fillBuckets = fillRes?.aggregations?.by_account?.buckets ?? [];
348
+ const updateBuckets = updateRes?.aggregations?.by_account?.buckets ?? [];
338
349
 
339
350
  if (opts.verbose) {
340
351
  console.log(` [verbose] createRes keys: ${Object.keys(createRes ?? {}).join(', ')}`);
@@ -344,6 +355,7 @@ async function run() {
344
355
  console.log(` Creates: ${createBuckets.length} accounts with ≥${opts.minCreates} creates`);
345
356
  console.log(` Cancels: ${cancelBuckets.length} accounts with ≥5 cancels`);
346
357
  console.log(` Fills: ${fillBuckets.length} accounts with ≥3 fills`);
358
+ console.log(` Updates: ${updateBuckets.length} accounts with ≥1 limit_order_update (op 77)`);
347
359
 
348
360
  if (createBuckets.length === 0) {
349
361
  console.log('');
@@ -360,17 +372,23 @@ async function run() {
360
372
  console.log('\nPhase 2: Merging and filtering candidates...');
361
373
 
362
374
  const accounts: Record<string, AccountCounts> = {};
363
- for (const b of createBuckets) accounts[b.key] = { creates: b.doc_count, cancels: 0, fills: 0 };
375
+ for (const b of createBuckets) accounts[b.key] = { creates: b.doc_count, cancels: 0, fills: 0, updates: 0 };
364
376
  for (const b of cancelBuckets) {
365
- if (!accounts[b.key]) accounts[b.key] = { creates: 0, cancels: 0, fills: 0 };
377
+ if (!accounts[b.key]) accounts[b.key] = { creates: 0, cancels: 0, fills: 0, updates: 0 };
366
378
  accounts[b.key].cancels = b.doc_count;
367
379
  }
368
380
  for (const b of fillBuckets) {
369
- if (!accounts[b.key]) accounts[b.key] = { creates: 0, cancels: 0, fills: 0 };
381
+ if (!accounts[b.key]) accounts[b.key] = { creates: 0, cancels: 0, fills: 0, updates: 0 };
370
382
  accounts[b.key].fills = b.doc_count;
371
383
  }
384
+ for (const b of updateBuckets) {
385
+ if (!accounts[b.key]) accounts[b.key] = { creates: 0, cancels: 0, fills: 0, updates: 0 };
386
+ accounts[b.key].updates = b.doc_count;
387
+ }
372
388
 
373
389
  // Pre-filter: must have creates ≥ minCreates (grid analysis determines DEXBot candidacy)
390
+ // Flavor: any limit_order_update (op 77) in-window → DEXBot2 (native in-place
391
+ // re-price); zero updates → DEXBot1-style (cancel-only: cancel + recreate).
374
392
  const candidates: CandidateInfo[] = (Object.entries(accounts) as [string, AccountCounts][])
375
393
  .filter(([, s]) => s.creates >= opts.minCreates)
376
394
  .map(([id, s]) => ({
@@ -378,6 +396,8 @@ async function run() {
378
396
  creates: s.creates,
379
397
  cancels: s.cancels,
380
398
  fills: s.fills,
399
+ updates: s.updates,
400
+ flavor: (s.updates >= 1 ? 'DEXBot2' : 'DEXBot1') as 'DEXBot2' | 'DEXBot1',
381
401
  cancelRatio: s.cancels / Math.max(s.creates, 1),
382
402
  fillRate: s.creates > 0 ? (s.fills / s.creates * 100) : 0,
383
403
  } as CandidateInfo))
@@ -492,9 +512,41 @@ async function run() {
492
512
  console.log('════════════════════════════════════════════════════════════════════════════════════');
493
513
  console.log('');
494
514
 
495
- // Print by DEX score tier
515
+ // Print by DEX score tier. HIGH tier (80+) splits by update-order usage:
516
+ // DEXBot2 — broadcast limit_order_update (op 77) in-window (in-place re-price)
517
+ // DEXBot1-style — cancel-only, no op 77 (cancel + recreate)
518
+ const highV2 = results.filter(r => r.dexScore >= 80 && r.updates >= 1);
519
+ const highV1 = results.filter(r => r.dexScore >= 80 && r.updates < 1);
520
+
521
+ const hdrHigh = ' # Name ID Creates Fills Cancel Fill% C/C MaxBatch Incr% Grid DEX Updates';
522
+ function printHighGroup(label: string, rows: CandidateInfo[]) {
523
+ if (!rows.length) return;
524
+ console.log(` ── ${label}`);
525
+ console.log('');
526
+ console.log(hdrHigh);
527
+ console.log(' ' + '─'.repeat(hdrHigh.length - 1));
528
+ for (const r of rows) {
529
+ const rank = String(results.indexOf(r) + 1).padStart(2);
530
+ const name = r.name.padEnd(22).slice(0, 22);
531
+ const id = r.id.padEnd(14);
532
+ const creates = String(r.creates).padStart(7);
533
+ const fills = String(r.fills).padStart(6);
534
+ const cancels = String(r.cancels).padStart(7);
535
+ const fr = (r.fillRate.toFixed(1) + '%').padStart(5);
536
+ const cr = r.cancelRatio.toFixed(2).padStart(4);
537
+ const batch = String(r.maxBatch || '-').padStart(9);
538
+ const inc = (r.impliedInc != null ? r.impliedInc.toFixed(2) + '%' : 'n/a').padStart(6);
539
+ const grid = String(r.gridScore).padStart(5);
540
+ const dex = String(r.dexScore).padStart(4);
541
+ const upd = String(r.updates).padStart(7);
542
+ console.log(` ${rank} ${name} ${id} ${creates} ${fills} ${cancels} ${fr} ${cr} ${batch} ${inc} ${grid} ${dex} ${upd}`);
543
+ }
544
+ console.log('');
545
+ }
546
+ printHighGroup('HIGH confidence — DEXBot2 (80+, uses limit_order_update op 77)', highV2);
547
+ printHighGroup('HIGH confidence — DEXBot1-style (80+, cancel-only, no op 77)', highV1);
548
+
496
549
  const tiers = [
497
- { label: 'HIGH confidence (80+) — almost certainly DEXBot/DEXBot2', min: 80, max: 101 },
498
550
  { label: 'MEDIUM confidence (50–79) — likely a grid bot', min: 50, max: 80 },
499
551
  { label: 'LOW confidence (25–49) — some automation detected', min: 25, max: 50 },
500
552
  { label: 'WEAK signal (<25) — create/cancel pattern, no grid', min: 0, max: 25 },
@@ -531,13 +583,13 @@ async function run() {
531
583
 
532
584
  // ── Summary ───────────────────────────────────────────────────────────────
533
585
 
534
- const high = results.filter(r => r.dexScore >= 80).length;
586
+ const high = highV2.length + highV1.length;
535
587
  const medium = results.filter(r => r.dexScore >= 50 && r.dexScore < 80).length;
536
588
  const low = results.filter(r => r.dexScore >= 25 && r.dexScore < 50).length;
537
589
 
538
590
  console.log('════════════════════════════════════════════════════════════════════════════════════');
539
591
  console.log(` Total candidates scanned: ${results.length}`);
540
- console.log(` HIGH (80+): ${high} accounts — almost certainly DEXBot/DEXBot2`);
592
+ console.log(` HIGH (80+): ${high} accounts — DEXBot2: ${highV2.length} (op 77 updates) / DEXBot1-style: ${highV1.length} (cancel-only)`);
541
593
  console.log(` MEDIUM (50+): ${medium} accounts — likely grid bots`);
542
594
  console.log(` LOW (25+): ${low} accounts — weak signal`);
543
595
  console.log('');
@@ -545,6 +597,7 @@ async function run() {
545
597
  console.log(' C/C = cancel/create ratio (grid bots: ~1.0)');
546
598
  console.log(' Incr% = implied grid increment from price spacing');
547
599
  console.log(' Grid = grid quality 0-100 | DEX = DEXBot confidence 0-100');
600
+ console.log(' Updates = limit_order_update (op 77) count — DEXBot2 only; 0 = cancel-only (DEXBot1-style)');
548
601
  console.log('');
549
602
 
550
603
  // ── Export JSON ────────────────────────────────────────────────────────────
@@ -553,9 +606,11 @@ async function run() {
553
606
  const exportData = results.map(r => ({
554
607
  id: r.id,
555
608
  name: r.name,
609
+ flavor: r.flavor,
556
610
  creates: r.creates,
557
611
  fills: r.fills,
558
612
  cancels: r.cancels,
613
+ updates: r.updates,
559
614
  fillPct: r.fillRate,
560
615
  cancelRatio: r.cancelRatio,
561
616
  maxBatch: r.maxBatch,