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
@@ -3,11 +3,16 @@
3
3
 
4
4
  import fs from 'node:fs';
5
5
  import { pathToFileURL } from 'node:url';
6
- import * as KC from '../market_adapter/core/kibana_client.js';
7
- import * as C from '../modules/constants.js';
8
- import { findBotKeyByAccountRef, getStoredBotAccountId, persistBotAccountId } from './bot_key_utils.js';
9
-
10
- const { kibanaSearch, DEFAULT_CONFIG: BASE_CONFIG } = KC;
6
+ import { resolveAccountRef } from './account_resolver.js';
7
+ import {
8
+ BTS_ID,
9
+ assetPrec as getPrec,
10
+ assetSymbol,
11
+ fetchAllFills,
12
+ resolveAssetPrecisions,
13
+ toReal,
14
+ FillRecord,
15
+ } from './fills_source.js';
11
16
 
12
17
  /**
13
18
  * TRADE PROFITABILITY ANALYZER
@@ -30,99 +35,10 @@ const { kibanaSearch, DEFAULT_CONFIG: BASE_CONFIG } = KC;
30
35
 
31
36
  // ─── Constants ────────────────────────────────────────────────────────────────
32
37
 
33
- const OP_FILL_ORDER = 4;
34
- const BTS_ID = '1.3.0';
35
38
  let BLOCKCHAIN_FEE_PER_FILL = 0.09652; // BTS — flat blockchain operation fee (not market fee); override with --fee-per-order
36
39
 
37
- interface AssetInfo {
38
- symbol: string;
39
- precision: number;
40
- }
41
-
42
- const ASSETS: Record<string, AssetInfo> = {
43
- '1.3.0': { symbol: 'BTS', precision: 5 },
44
- '1.3.118': { symbol: 'GBP', precision: 4 },
45
- '1.3.119': { symbol: 'JPY', precision: 2 },
46
- '1.3.120': { symbol: 'EUR', precision: 4 },
47
- '1.3.1325': { symbol: 'RUBLE', precision: 5 },
48
- '1.3.2512': { symbol: 'EVRAZ', precision: 4 },
49
- '1.3.3291': { symbol: 'TWENTIX', precision: 5 },
50
- '1.3.4099': { symbol: 'XBTSX.STH', precision: 6 },
51
- '1.3.4156': { symbol: 'XBTSX.DOGE', precision: 5 },
52
- '1.3.4157': { symbol: 'XBTSX.BTC', precision: 8 },
53
- '1.3.4159': { symbol: 'XBTSX.LTC', precision: 8 },
54
- '1.3.4176': { symbol: 'XBTSX.DASH', precision: 8 },
55
- '1.3.4274': { symbol: 'XBTSX.BCH', precision: 8 },
56
- '1.3.4760': { symbol: 'XBTSX.ETH', precision: 7 },
57
- '1.3.5537': { symbol: 'IOB.XRP', precision: 4 },
58
- '1.3.5541': { symbol: 'XBTSX.BNB', precision: 7 },
59
- '1.3.5589': { symbol: 'XBTSX.USDT', precision: 6 },
60
- '1.3.5641': { symbol: 'HONEST.CNY', precision: 4 },
61
- '1.3.5649': { symbol: 'HONEST.USD', precision: 4 },
62
- '1.3.5650': { symbol: 'HONEST.BTC', precision: 8 },
63
- '1.3.5659': { symbol: 'HONEST.ETH', precision: 6 },
64
- '1.3.5870': { symbol: 'XBTSX.FIL', precision: 6 },
65
- '1.3.5887': { symbol: 'XBTSX.RUB', precision: 4 },
66
- '1.3.5902': { symbol: 'XBTSX.USDC', precision: 6 },
67
- '1.3.6013': { symbol: 'XBTSX.HIVE', precision: 6 },
68
- '1.3.6124': { symbol: 'XBTSX.AVAX', precision: 6 },
69
- '1.3.6139': { symbol: 'XBTSX.XAUT', precision: 6 },
70
- '1.3.6166': { symbol: 'XBTSX.MATIC', precision: 5 },
71
- '1.3.6241': { symbol: 'XBTSX.ETC', precision: 7 },
72
- '1.3.6268': { symbol: 'BTWTY.EOS', precision: 4 },
73
- '1.3.6301': { symbol: 'HONEST.MONEY', precision: 8 },
74
- '1.3.6304': { symbol: 'HONEST.ADA', precision: 8 },
75
- '1.3.6305': { symbol: 'HONEST.DOT', precision: 8 },
76
- '1.3.6309': { symbol: 'HONEST.ATOM', precision: 8 },
77
- '1.3.6311': { symbol: 'HONEST.ALGO', precision: 8 },
78
- '1.3.6312': { symbol: 'HONEST.FIL', precision: 8 },
79
- '1.3.6313': { symbol: 'HONEST.EOS', precision: 8 },
80
- '1.3.6315': { symbol: 'HONEST.EUR', precision: 4 },
81
- '1.3.6316': { symbol: 'HONEST.GBP', precision: 4 },
82
- '1.3.6317': { symbol: 'HONEST.JPY', precision: 4 },
83
- '1.3.6444': { symbol: 'IOB.XLM', precision: 4 },
84
- '1.3.6573': { symbol: 'XBTSX.DAI', precision: 6 },
85
- '1.3.6620': { symbol: 'XBTSX.A', precision: 6 },
86
- '1.3.6627': { symbol: 'XBTSX.LINK', precision: 6 },
87
- };
88
- const resolvedPrecisions: Record<string, number> = {};
89
-
90
- function assetSymbol(id: string): string {
91
- return ASSETS[id]?.symbol ?? id;
92
- }
93
- function assetPrec(id: string): number | undefined {
94
- return ASSETS[id]?.precision ?? resolvedPrecisions[id];
95
- }
96
- function getPrec(id: string): number | undefined {
97
- return assetPrec(id);
98
- }
99
-
100
- function toReal(amount: number, assetId: string): number {
101
- const p = getPrec(assetId);
102
- if (p === undefined) return NaN;
103
- return amount / Math.pow(10, p);
104
- }
105
-
106
40
  // ─── Types ────────────────────────────────────────────────────────────────────
107
41
 
108
- interface AssetAmount {
109
- amount: number;
110
- asset_id: string;
111
- }
112
-
113
- interface FillRecord {
114
- time: string;
115
- blockNum: number;
116
- opNum: number;
117
- orderId: string;
118
- accountId: string;
119
- pays: AssetAmount;
120
- receives: AssetAmount;
121
- fee: AssetAmount;
122
- isMaker: boolean;
123
- sort: any[];
124
- }
125
-
126
42
  interface TradeFill {
127
43
  time: string;
128
44
  orderId: string;
@@ -204,7 +120,6 @@ Options:
204
120
  --asset <assetId> Filter to one base asset (e.g. 1.3.113 for bitUSD)
205
121
  --lookup Legacy (no-op): account names always resolve automatically
206
122
  --refresh-account Force re-resolution and update the stored accountId
207
- --node <url> BitShares node URL (default: first healthy from built-in pool)
208
123
  --csv <file> Export trade list as CSV
209
124
  --json <file> Export full analysis as JSON
210
125
  --trades Show per-order PnL detail (hidden by default)
@@ -236,7 +151,6 @@ function parseArgs() {
236
151
  asset: null,
237
152
  lookup: false,
238
153
  refreshAccount: false,
239
- node: C.NODE_MANAGEMENT.DEFAULT_NODES[0],
240
154
  csv: null,
241
155
  json: null,
242
156
  matchMode: 'sequential',
@@ -253,7 +167,6 @@ function parseArgs() {
253
167
  case '--asset': opts.asset = args[++i]; break;
254
168
  case '--lookup': opts.lookup = true; break;
255
169
  case '--refresh-account': opts.refreshAccount = true; break;
256
- case '--node': opts.node = args[++i]; break;
257
170
  case '--csv': opts.csv = args[++i]; break;
258
171
  case '--json': opts.json = args[++i]; break;
259
172
  case '--trades': opts.showPnlDetail = true; break;
@@ -281,150 +194,6 @@ function parseArgs() {
281
194
  return opts;
282
195
  }
283
196
 
284
- // ─── Account name resolution ─────────────────────────────────────────────────
285
-
286
- async function resolveAccountId(name: string, nodeUrl: string): Promise<string | null> {
287
- const { createReadOnlyClient } = await import('../modules/bitshares-native/index.js');
288
- const client = createReadOnlyClient({ nodes: [nodeUrl] });
289
- // Suppress transport INFO logs during ephemeral connection:
290
- // bitshares-native transport logger (new Logger('Transport')) writes
291
- // "[timestamp] [INFO] [Transport] ..." — silence by raising log level.
292
- const prevLevel = process.env.LOG_LEVEL;
293
- process.env.LOG_LEVEL = 'warn';
294
- try {
295
- await client.connect();
296
- const accounts = await client.db('lookup_account_names', [[name]]);
297
- if (Array.isArray(accounts) && accounts[0]?.id) {
298
- return accounts[0].id;
299
- }
300
- return null;
301
- } catch (e: any) {
302
- console.warn(` [warn] Account resolution failed: ${e.message}`);
303
- return null;
304
- } finally {
305
- try { client.disconnect(); } catch (_) {}
306
- process.env.LOG_LEVEL = prevLevel;
307
- }
308
- }
309
-
310
- // ─── On-chain asset precision resolution ────────────────────────────────────
311
-
312
- /**
313
- * Collects all unique non-BTS asset IDs from fills, resolves unknown
314
- * precisions from the blockchain, and populates the runtime cache.
315
- */
316
- async function resolveAssetPrecisions(fills: FillRecord[], nodeUrl: string | null): Promise<void> {
317
- const unknownIds = new Set<string>();
318
- for (const f of fills) {
319
- for (const id of [f.pays.asset_id, f.receives.asset_id, f.fee.asset_id]) {
320
- if (id !== BTS_ID && !(id in ASSETS) && !(id in resolvedPrecisions)) {
321
- unknownIds.add(id);
322
- }
323
- }
324
- }
325
- if (unknownIds.size === 0 || !nodeUrl) return;
326
-
327
- const ids = [...unknownIds];
328
- console.log(` Resolving ${ids.length} unknown asset(s) from blockchain...`);
329
-
330
- const { createReadOnlyClient } = await import('../modules/bitshares-native/index.js');
331
- const client = createReadOnlyClient({ nodes: [nodeUrl] });
332
- try {
333
- await client.connect();
334
- const assets = await client.db('get_assets', [ids]);
335
- if (Array.isArray(assets)) {
336
- for (const asset of assets) {
337
- if (asset?.id && asset.precision != null) {
338
- resolvedPrecisions[asset.id] = asset.precision;
339
- console.log(` ${asset.id} → ${asset.symbol || '?'} (precision ${asset.precision})`);
340
- }
341
- }
342
- }
343
- const missing = ids.filter(id => !(id in resolvedPrecisions));
344
- if (missing.length > 0) {
345
- console.warn(` [warn] ${missing.length} asset(s) not found on chain: ${missing.join(', ')}. Fills referencing them will be skipped.`);
346
- }
347
- } catch (e: any) {
348
- console.warn(` [warn] Asset resolution failed: ${e.message}. Fills with unknown assets will be skipped.`);
349
- } finally {
350
- try { client.disconnect(); } catch (_) {}
351
- }
352
- }
353
-
354
- // ─── Kibana Query ────────────────────────────────────────────────────────────
355
-
356
- function buildFillQuery(accountId: string, gte: string, lte: string, size: number) {
357
- return {
358
- size,
359
- track_total_hits: false,
360
- _source: [
361
- 'block_data.block_time',
362
- 'block_data.block_num',
363
- 'operation_id_num',
364
- 'operation_history.op_object.pays',
365
- 'operation_history.op_object.receives',
366
- 'operation_history.op_object.fee',
367
- 'operation_history.op_object.order_id',
368
- 'operation_history.op_object.account_id',
369
- 'operation_history.op_object.is_maker',
370
- ],
371
- query: {
372
- bool: {
373
- filter: [
374
- { term: { operation_type: OP_FILL_ORDER } },
375
- { term: { 'operation_history.op_object.account_id.keyword': accountId } },
376
- { range: { 'block_data.block_time': { gte, lte } } },
377
- ],
378
- },
379
- },
380
- sort: [
381
- { 'block_data.block_time': { order: 'asc' } },
382
- { operation_id_num: { order: 'asc' } },
383
- ],
384
- };
385
- }
386
-
387
- async function fetchAllFills(config: any, accountId: string, gte: string, lte: string): Promise<FillRecord[]> {
388
- const pageSize = 10000;
389
- const fills: FillRecord[] = [];
390
- let searchAfter: any[] | null = null;
391
- const cfg = { ...BASE_CONFIG, timeout: 60000, ...config };
392
-
393
- while (true) {
394
- const query = buildFillQuery(accountId, gte, lte, pageSize);
395
- if (searchAfter) (query as any).search_after = searchAfter;
396
-
397
- const result: any = await kibanaSearch(cfg, query);
398
- const hits = result?.hits?.hits ?? [];
399
- if (!hits.length) break;
400
-
401
- for (const hit of hits) {
402
- const src = hit?._source;
403
- const op = src?.operation_history?.op_object;
404
- if (!op || !op.pays || !op.receives) continue;
405
-
406
- fills.push({
407
- time: src.block_data?.block_time ?? '',
408
- blockNum: src.block_data?.block_num ?? 0,
409
- opNum: Number(src.operation_id_num ?? 0),
410
- orderId: op.order_id ?? '',
411
- accountId: op.account_id ?? '',
412
- pays: { amount: Number(op.pays.amount ?? 0), asset_id: op.pays.asset_id ?? '' },
413
- receives: { amount: Number(op.receives.amount ?? 0), asset_id: op.receives.asset_id ?? '' },
414
- fee: { amount: Number(op.fee?.amount ?? 0), asset_id: op.fee?.asset_id ?? '' },
415
- isMaker: op.is_maker ?? false,
416
- sort: hit.sort,
417
- });
418
- }
419
-
420
- if (hits.length < pageSize) break;
421
- searchAfter = hits[hits.length - 1].sort;
422
- if (!Array.isArray(searchAfter)) break;
423
- }
424
-
425
- return fills;
426
- }
427
-
428
197
  // ─── Fill Classification ─────────────────────────────────────────────────────
429
198
 
430
199
  /**
@@ -829,6 +598,12 @@ function printPnlDetail(pairs: PairAnalysis[]) {
829
598
 
830
599
  // ─── Performance Metrics ──────────────────────────────────────────────────────
831
600
 
601
+ interface WindowRange {
602
+ /** The analysis window actually queried — drives annualisation. */
603
+ startMs: number;
604
+ endMs: number;
605
+ }
606
+
832
607
  interface TradingMetrics {
833
608
  totalLots: number;
834
609
  winRate: number;
@@ -840,8 +615,22 @@ interface TradingMetrics {
840
615
  expectancyPct: number;
841
616
  expectancyR: number;
842
617
  netExpectancyBts: number;
843
- dailyPnlRatio: number;
844
- dailyDownsideRatio: number;
618
+ // Risk-adjusted ratios annualised from the analysis window (see
619
+ // --hours/--start/--end). Binned 1d for windows >= 3 days, else 1h; every
620
+ // period in the window is zero-filled so flat periods count as 0 PnL and
621
+ // the window scales cleanly to a year. Only whole periods are scored: a
622
+ // trailing partial period is excluded from ratios AND projection alike,
623
+ // so both share the same denominator.
624
+ sharpeAnn: number; // NaN when undefined (no dispersion / < 2 periods)
625
+ sortinoAnn: number; // Infinity when the window has no losing periods
626
+ sharpeAnnSE: number; // standard error of sharpeAnn (estimation uncertainty)
627
+ periodLabel: string; // '1h' | '1d'
628
+ periodCount: number; // whole periods scored, including zero-PnL ones
629
+ periodSpanDays: number; // queried window length
630
+ scoredSpanDays: number; // whole-period span actually scored (nPeriods × bin)
631
+ annualFactor: number; // sqrt(periods per year)
632
+ projectedNetPnlPerDay: number;
633
+ projectedNetPnlAnn: number;
845
634
  maxConsecWins: number;
846
635
  maxConsecLosses: number;
847
636
  avgHoldHours: number;
@@ -849,6 +638,7 @@ interface TradingMetrics {
849
638
  bestTradePct: number;
850
639
  worstTradePct: number;
851
640
  mddPct: number;
641
+ mddAbsBts: number;
852
642
  mddHadStablePeak: boolean;
853
643
  isOngoingRecovery: boolean;
854
644
  currentDrawdownDays: number;
@@ -882,7 +672,7 @@ function percentile(sorted: number[], p: number): number {
882
672
  return sorted[f] * (c - k) + sorted[c] * (k - f);
883
673
  }
884
674
 
885
- function computeMetrics(pair: PairAnalysis): TradingMetrics {
675
+ function computeMetrics(pair: PairAnalysis, window?: WindowRange): TradingMetrics {
886
676
  const pnls = pair.realizedPnls;
887
677
  const total = pnls.length;
888
678
  if (total === 0) {
@@ -891,11 +681,15 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
891
681
  avgWin: 0, avgLoss: 0, avgWinLossRatio: 0,
892
682
  expectancyBts: 0, expectancyPct: 0, expectancyR: 0,
893
683
  netExpectancyBts: 0,
894
- dailyPnlRatio: 0, dailyDownsideRatio: 0,
684
+ sharpeAnn: NaN, sortinoAnn: NaN, sharpeAnnSE: NaN,
685
+ periodLabel: '—', periodCount: 0, periodSpanDays: 0, scoredSpanDays: 0,
686
+ annualFactor: 0,
687
+ projectedNetPnlPerDay: 0, projectedNetPnlAnn: 0,
895
688
  maxConsecWins: 0, maxConsecLosses: 0,
896
689
  avgHoldHours: 0, limitOrderRatio: 0,
897
690
  bestTradePct: 0, worstTradePct: 0,
898
691
  mddPct: 0,
692
+ mddAbsBts: 0,
899
693
  mddHadStablePeak: false,
900
694
  isOngoingRecovery: false,
901
695
  currentDrawdownDays: 0,
@@ -937,29 +731,91 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
937
731
 
938
732
  const netExpectancyBts = pair.totalRealizedPnlNet / total;
939
733
 
940
- // Daily-binned net PnL for mean/std ratio (dimensionful — not a Sharpe ratio)
941
- const dayBuckets: Record<string, number> = {};
734
+ // ─── Risk-adjusted ratios (window-aware annualisation) ────────────────
735
+ // Bin the window's net PnL into calendar periods and annualise with the
736
+ // matching periods-per-year. Every period in the window is represented
737
+ // (zero-filled): flat periods must count as 0 PnL, otherwise a bot that
738
+ // trades a few days a week is scored as if it traded every day, and the
739
+ // window cannot be scaled honestly to a year.
740
+ const HOUR_MS = 3600_000;
741
+ const DAY_MS = 86_400_000;
742
+ const times = pnls.map(r => Date.parse(r.exitTime)).filter(t => Number.isFinite(t));
743
+ const firstMs = window?.startMs ?? (times.length > 0 ? Math.min(...times) : 0);
744
+ const lastMs = window?.endMs ?? (times.length > 0 ? Math.max(...times) : firstMs + DAY_MS);
745
+ const spanMs = Math.max(lastMs - firstMs, window ? HOUR_MS : DAY_MS);
746
+ const periodSpanDays = spanMs / DAY_MS;
747
+ // Daily buckets need a handful of observations before a daily std means
748
+ // anything; shorter windows fall back to hourly bins so they still
749
+ // annualise instead of dividing by a one-sample std.
750
+ const useHourly = periodSpanDays < 3;
751
+ const periodMs = useHourly ? HOUR_MS : DAY_MS;
752
+ const periodsPerYear = useHourly ? 8760 : 365;
753
+ const periodLabel = useHourly ? '1h' : '1d';
754
+
755
+ // Score only whole periods: a trailing partial period (window not an exact
756
+ // multiple of the bin) would otherwise be a deflated observation. It is
757
+ // excluded from ratios, projection AND activity rates alike, so every
758
+ // window-derived metric shares one basis. Bins are aligned to the window
759
+ // start, so an exact-multiple window scores everything.
760
+ const nPeriods = Math.max(1, Math.floor(spanMs / periodMs));
761
+ const scoredSpanDays = nPeriods * periodMs / DAY_MS;
762
+ const scoredEndMs = firstMs + nPeriods * periodMs;
763
+ const exactWindow = spanMs === nPeriods * periodMs;
764
+ // A fill belongs to the scored window if it falls inside the whole
765
+ // periods. On an exact-multiple window the end boundary is inclusive (the
766
+ // ES range query includes `lte`); with a partial tail that boundary fill
767
+ // is trailing and excluded along with the rest of the tail.
768
+ const inScoredWindow = (t: number) =>
769
+ Number.isFinite(t) && t >= firstMs
770
+ && (t < scoredEndMs || (exactWindow && t === scoredEndMs));
771
+ const periodPnl = new Array<number>(nPeriods).fill(0);
772
+ let scoredFillCount = 0;
942
773
  for (const r of pnls) {
943
- const day = r.exitTime.slice(0, 10);
944
- dayBuckets[day] = (dayBuckets[day] || 0) + r.pnlNet;
774
+ const t = Date.parse(r.exitTime);
775
+ if (!inScoredWindow(t)) continue;
776
+ let idx = Math.floor((t - firstMs) / periodMs);
777
+ if (idx === nPeriods) idx = nPeriods - 1; // inclusive end boundary
778
+ periodPnl[idx] += r.pnlNet;
779
+ scoredFillCount++;
945
780
  }
946
- const dailyRets = Object.values(dayBuckets);
947
- const nDays = dailyRets.length;
948
781
 
949
- const meanDailyRet = nDays > 0 ? dailyRets.reduce((s, v) => s + v, 0) / nDays : 0;
950
- const dailyVar = nDays > 0
951
- ? dailyRets.reduce((s, v) => s + (v - meanDailyRet) ** 2, 0) / nDays
782
+ const meanPeriod = periodPnl.reduce((s, v) => s + v, 0) / nPeriods;
783
+ // Sample variance (n-1): the window is a sample, not the whole population.
784
+ const periodVar = nPeriods > 1
785
+ ? periodPnl.reduce((s, v) => s + (v - meanPeriod) ** 2, 0) / (nPeriods - 1)
952
786
  : 0;
953
- const dailyStd = Math.sqrt(dailyVar);
954
- const annFactor = Math.sqrt(365);
955
- const dailyPnlRatio = dailyStd > 0 ? (meanDailyRet / dailyStd) * annFactor : 0;
956
-
957
- // Downside deviation uses only negative returns; same N denominator
958
- const downsideVar = nDays > 0
959
- ? dailyRets.reduce((s, v) => s + (v < 0 ? v * v : 0), 0) / nDays
787
+ const periodStd = Math.sqrt(periodVar);
788
+ const annualFactor = Math.sqrt(periodsPerYear);
789
+ const sharpeAnn = periodStd > 0 ? (meanPeriod / periodStd) * annualFactor : NaN;
790
+
791
+ // Estimation uncertainty of the annualised Sharpe (Lo 2002, i.i.d. returns).
792
+ const srPerPeriod = periodStd > 0 ? meanPeriod / periodStd : NaN;
793
+ const sharpeAnnSE = nPeriods > 1 && Number.isFinite(srPerPeriod)
794
+ ? Math.sqrt((1 + 0.5 * srPerPeriod * srPerPeriod) / nPeriods) * annualFactor
795
+ : NaN;
796
+
797
+ // Target downside deviation (MAR = 0), sample denominator (n-1) to match
798
+ // the Sharpe convention above — the two ratios stay internally
799
+ // comparable (many textbook Sortinos divide by N; we deliberately
800
+ // don't mix conventions). With no losing periods Sortino is undefined —
801
+ // reporting 0 would read as "terrible", the opposite of truth.
802
+ const downsideVar = nPeriods > 1
803
+ ? periodPnl.reduce((s, v) => s + (v < 0 ? v * v : 0), 0) / (nPeriods - 1)
960
804
  : 0;
961
805
  const downsideStd = Math.sqrt(downsideVar);
962
- const dailyDownsideRatio = downsideStd > 0 ? (meanDailyRet / downsideStd) * annFactor : 0;
806
+ const sortinoAnn = nPeriods < 2
807
+ ? NaN
808
+ : (downsideStd > 0
809
+ ? (meanPeriod / downsideStd) * annualFactor
810
+ : (meanPeriod > 0 ? Infinity : NaN));
811
+
812
+ // "This window repeated all year" — linear projection of the scored
813
+ // window's net PnL. Uses the same whole-period basis as the ratios
814
+ // above (not the raw window total), so Sharpe and projection can never
815
+ // disagree about what the window contains.
816
+ const scoredPnl = periodPnl.reduce((s, v) => s + v, 0);
817
+ const projectedNetPnlPerDay = scoredPnl / scoredSpanDays;
818
+ const projectedNetPnlAnn = projectedNetPnlPerDay * 365;
963
819
 
964
820
  // Fills-per-order distribution (grouped by sell order)
965
821
  const fillCounts: number[] = [];
@@ -980,8 +836,13 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
980
836
  const oneShotOrderRatio = sellOrdersFilled > 0
981
837
  ? fillCounts.filter(c => c === 1).length / sellOrdersFilled
982
838
  : 0;
983
- const fillsPerDay = nDays > 0 ? total / nDays : 0;
984
- const avgVolumePerDay = nDays > 0 ? (pair.totalBuyQuote + pair.totalSellQuote) / nDays : 0;
839
+ // Activity rates share the scored whole-period basis: numerator and
840
+ // denominator both cover exactly the lots/fills the ratios scored. A fill
841
+ // with an unparseable timestamp has no period, so it is excluded here too.
842
+ const scoredNotional = [...pair.buys, ...pair.sells]
843
+ .reduce((s, f) => s + (inScoredWindow(Date.parse(f.time)) ? f.quoteAmount : 0), 0);
844
+ const fillsPerDay = scoredSpanDays > 0 ? scoredFillCount / scoredSpanDays : 0;
845
+ const avgVolumePerDay = scoredSpanDays > 0 ? scoredNotional / scoredSpanDays : 0;
985
846
 
986
847
  // Avg hold duration
987
848
  let totalHours = 0;
@@ -1023,7 +884,7 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
1023
884
  const chronological = [...pnls].sort((a, b) =>
1024
885
  new Date(a.exitTime).getTime() - new Date(b.exitTime).getTime()
1025
886
  );
1026
- let equity = 0, peak = 0, mddPct = 0;
887
+ let equity = 0, peak = 0, mddPct = 0, mddAbsBts = 0;
1027
888
  let maxRecoveryDays = 0;
1028
889
  let isOngoingRecovery = false;
1029
890
  let currentDrawdownDays = 0;
@@ -1067,6 +928,8 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
1067
928
  }
1068
929
  const dd = (equity - peak) / peak;
1069
930
  if (dd < mddPct) mddPct = dd;
931
+ const ddAbs = peak - equity;
932
+ if (ddAbs > mddAbsBts) mddAbsBts = ddAbs;
1070
933
  }
1071
934
 
1072
935
  if (peak > 0 && !hadStablePeak) {
@@ -1090,6 +953,7 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
1090
953
  mddPct *= 100;
1091
954
  } else {
1092
955
  mddPct = hasPrePeakEquity ? prePeakMinEquity : 0;
956
+ mddAbsBts = 0;
1093
957
  }
1094
958
 
1095
959
  // Payoff distribution stats
@@ -1121,8 +985,16 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
1121
985
  expectancyPct,
1122
986
  expectancyR,
1123
987
  netExpectancyBts,
1124
- dailyPnlRatio,
1125
- dailyDownsideRatio,
988
+ sharpeAnn,
989
+ sortinoAnn,
990
+ sharpeAnnSE,
991
+ periodLabel,
992
+ periodCount: nPeriods,
993
+ periodSpanDays,
994
+ scoredSpanDays,
995
+ annualFactor,
996
+ projectedNetPnlPerDay,
997
+ projectedNetPnlAnn,
1126
998
  feeDragPct,
1127
999
  maxConsecWins: maxW,
1128
1000
  maxConsecLosses: maxL,
@@ -1131,6 +1003,7 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
1131
1003
  bestTradePct,
1132
1004
  worstTradePct,
1133
1005
  mddPct,
1006
+ mddAbsBts,
1134
1007
  mddHadStablePeak: hadStablePeak,
1135
1008
  isOngoingRecovery,
1136
1009
  currentDrawdownDays,
@@ -1152,11 +1025,11 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
1152
1025
  };
1153
1026
  }
1154
1027
 
1155
- function printMetrics(pairs: PairAnalysis[]) {
1028
+ function printMetrics(pairs: PairAnalysis[], window?: WindowRange) {
1156
1029
  for (const pair of pairs) {
1157
1030
  if (pair.realizedPnls.length === 0) continue;
1158
1031
 
1159
- const m = computeMetrics(pair);
1032
+ const m = computeMetrics(pair, window);
1160
1033
  const pairLabel = `${fmtAsset(pair.baseAsset)}/${fmtAsset(pair.quoteAsset)}`;
1161
1034
 
1162
1035
  console.log('');
@@ -1185,13 +1058,24 @@ function printMetrics(pairs: PairAnalysis[]) {
1185
1058
  console.log(` P25 / P75: ${fmtPct(m.p25PnlPct)} / ${fmtPct(m.p75PnlPct)}`);
1186
1059
  console.log(` Best / Worst Trade: ${fmtPct(m.bestTradePct)} / ${fmtPct(m.worstTradePct)}`);
1187
1060
  console.log('');
1188
- // Risk-adjusted (dimensionful — based on absolute daily PnL, not % returns)
1189
- console.log(` Sharpe (ann): ${m.dailyPnlRatio.toFixed(2)}`);
1190
- console.log(` Sortino (ann): ${m.dailyDownsideRatio.toFixed(2)}`);
1061
+ // Risk-adjusted — annualised from the analysis window, zero-filled bins
1062
+ const ratioStr = (v: number) => Number.isFinite(v) ? v.toFixed(2) : (v === Infinity ? '∞' : 'n/a');
1063
+ const seStr = Number.isFinite(m.sharpeAnnSE) ? ` ± ${m.sharpeAnnSE.toFixed(2)}` : '';
1064
+ const confidence = m.periodSpanDays < 30 ? 'low confidence' : 'ok';
1065
+ console.log(` Sharpe (ann): ${ratioStr(m.sharpeAnn)}${seStr} [${m.periodLabel} bins, n=${m.periodCount}, ${confidence}]`);
1066
+ const sortinoNote = m.sortinoAnn === Infinity ? ' (no losing periods)' : '';
1067
+ console.log(` Sortino (ann): ${ratioStr(m.sortinoAnn)}${sortinoNote}`);
1068
+ if (m.periodLabel === '1h') {
1069
+ console.log(` ⚠ window < 3 days: hourly bins over an unrepresentative sample — treat the annualised ratios as indicative only and never rank them against 1d-binned runs`);
1070
+ }
1071
+ const spanStr = m.scoredSpanDays < m.periodSpanDays - 1e-9
1072
+ ? `${m.scoredSpanDays.toFixed(1)}d scored of ${m.periodSpanDays.toFixed(1)}d window`
1073
+ : `${m.periodSpanDays.toFixed(1)}d`;
1074
+ console.log(` Projected net PnL: ${fmt(m.projectedNetPnlAnn, 2)} ${qSymbol}/yr (${fmt(m.projectedNetPnlPerDay, 2)}/day over ${spanStr})`);
1191
1075
  console.log('');
1192
1076
  // Tail risk
1193
1077
  if (m.mddHadStablePeak) {
1194
- console.log(` Max Drawdown: ${fmtPct(m.mddPct)}`);
1078
+ console.log(` Max Drawdown: ${fmt(m.mddAbsBts, 4)} ${qSymbol} (${fmtPct(m.mddPct)} of peak cumulative profit)`);
1195
1079
  } else {
1196
1080
  console.log(` Min Equity: ${fmt(m.mddPct, 4)} ${fmtAsset(pair.quoteAsset)}`);
1197
1081
  }
@@ -1334,36 +1218,17 @@ async function run() {
1334
1218
  let accountId = opts.accountId;
1335
1219
 
1336
1220
  if (!/^1\.2\.\d+$/.test(String(accountId))) {
1337
- // Background resolution: the Kibana query below filters on the 1.2.x
1338
- // account_id field, so a name must always resolve first (a raw name
1339
- // would silently return zero fills).
1340
- // Stored ID first: when the name belongs to a bot in profiles/bots.json
1341
- // and its stamped accountId still matches, no chain lookup is needed.
1342
- let matchedKey: string | null = null;
1343
- let stored: string | null = null;
1344
- try {
1345
- const match = findBotKeyByAccountRef(accountId);
1346
- if (match) {
1347
- matchedKey = match.botKey;
1348
- stored = getStoredBotAccountId(match.botKey, accountId);
1349
- }
1350
- } catch (_) {
1351
- // bots.json issues must never break resolution; fall through to chain.
1352
- }
1353
- if (stored && !opts.refreshAccount) {
1354
- console.log(` Using stored accountId ${stored} from profiles/bots.json (no lookup needed; pass --refresh-account to re-verify)`);
1355
- accountId = stored;
1356
- } else {
1357
- const resolved = await resolveAccountId(accountId, opts.node);
1358
- if (!resolved) {
1359
- console.error(` Could not resolve "${accountId}" to an account ID`);
1360
- process.exit(1);
1361
- }
1362
- accountId = resolved;
1363
- if (matchedKey && persistBotAccountId(matchedKey, resolved)) {
1364
- console.log(` Stored accountId ${resolved} in profiles/bots.json`);
1365
- }
1221
+ // The Kibana query below filters on the 1.2.x account_id field, so a
1222
+ // name must always resolve first (a raw name would silently return
1223
+ // zero fills). Shared resolver: reuses a stored accountId from
1224
+ // profiles/bots.json when one matches the name and stamps the result
1225
+ // back onto the bot entry on a fresh lookup.
1226
+ const resolved = await resolveAccountRef(accountId, { refresh: opts.refreshAccount });
1227
+ if (!resolved.accountId) {
1228
+ console.error(` Could not resolve "${accountId}" to an account ID`);
1229
+ process.exit(1);
1366
1230
  }
1231
+ accountId = resolved.accountId;
1367
1232
  }
1368
1233
 
1369
1234
  // Build time range
@@ -1393,7 +1258,7 @@ async function run() {
1393
1258
  }
1394
1259
 
1395
1260
  // Resolve unknown asset precisions from blockchain
1396
- await resolveAssetPrecisions(fills, opts.node);
1261
+ await resolveAssetPrecisions(fills);
1397
1262
 
1398
1263
  // Classify fills
1399
1264
  const { trades, pairs } = classifyFills(fills, opts.asset);
@@ -1451,7 +1316,7 @@ async function run() {
1451
1316
  printPnlDetail(analyses);
1452
1317
  }
1453
1318
 
1454
- printMetrics(analyses);
1319
+ printMetrics(analyses, { startMs: Date.parse(gte), endMs: Date.parse(lte) });
1455
1320
 
1456
1321
  if (opts.csv) {
1457
1322
  exportCsv(analyses, opts.csv);
@@ -203,7 +203,7 @@ market_adapter/data/lp/<pair>/lp_pool_<id>_<interval>.json
203
203
 
204
204
  ## Notes
205
205
 
206
- - The chart uses vendored `uPlot` from `analysis/uplot/` in the generated HTML (no CDN dependency).
206
+ - The chart embeds the vendored `uPlot` runtime inline (no CDN, no sibling `uplot/` dir, no DEXBot2 install needed). Each export is a single self-contained HTML file that renders anywhere, even after being copied or mailed to a machine without DEXBot2.
207
207
  - The displayed indicators are computed from the 1h base candles and then sampled onto the selected timeframe.
208
208
  - The current volume-weighted overlay is a rolling `VWMA`, not a session-reset VWAP.
209
209
  - SMA is disabled by default.
@@ -218,7 +218,7 @@ market_adapter/data/lp/<pair>/lp_pool_<id>_<interval>.json
218
218
  - Mouse: drag the candles to pan time + price (price drag sets a manual range); wheel zooms time, except over the price axis where it zooms price. Shift+wheel zooms price anywhere over the price pane (cursor-anchored). Dragging the price-axis gutter scales price, dragging the time-axis gutter scales the timeframe; double-click the price axis to return to autofit. While the price range is manual, timeframe moves no longer refit it.
219
219
  - Indicator, timeframe, scale, and overlay-visibility changes are persisted in browser `localStorage` per pool/pair chart (`dexbot2-tradingview-uplot-v3:<pool>:<A>_<B>:<baseSecs|base>`); cursor sync between the price/volume panes uses a separate constant key.
220
220
  - The price axis defaults to log base `10`, with a toolbar switch for `Log` / `Linear`.
221
- - If you regenerate the HTML and then open it later, no CDN access is needed — `uPlot` is loaded from the vendored local copy at `analysis/uplot/`.
221
+ - If you regenerate the HTML and then open it later, no CDN access is needed — the `uPlot` library (JS + CSS) is inlined into the file itself, so it renders fully offline and is independent of where the file lives on disk.
222
222
 
223
223
  ## Typical Workflow
224
224