dexbot 1.4.17 → 1.4.19

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 (247) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -0
  3. package/analysis/README.md +43 -43
  4. package/analysis/ama_fitting/README.md +8 -8
  5. package/analysis/ama_fitting/package.json +1 -1
  6. package/analysis/bot_fitting/README.md +4 -4
  7. package/analysis/bot_fitting/backtest_ama_sweep.ts +1 -1
  8. package/analysis/bot_usage/discover_bot_accounts.ts +11 -11
  9. package/analysis/tradingview/README.md +4 -4
  10. package/analysis/trend_detection/DYNAMIC_WEIGHT_RESEARCH.md +2 -2
  11. package/analysis/trend_detection/SIGNAL_DOCUMENTATION.md +1 -1
  12. package/analysis/trend_detection/package.json +1 -1
  13. package/claw/README.md +45 -45
  14. package/claw/docs/AI_BOT_LIBRARY_API.md +6 -6
  15. package/claw/examples/short_mpa_bts_strategy.ts +3 -3
  16. package/claw/package.json +21 -21
  17. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  18. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  19. package/claw/scripts/claw_bridge.ts +2 -2
  20. package/claw/scripts/claw_skill_md.ts +1 -1
  21. package/claw/skills/launcher-ops/references/launcher-workflow.md +5 -5
  22. package/claw/skills/margin-trading/references/honest-asset-list.md +1 -1
  23. package/claw/skills/memu-memory/SKILL.md +3 -3
  24. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  25. package/dist/analysis/ama_fitting/analyze_ama_price_changes.d.ts +3 -0
  26. package/dist/analysis/ama_fitting/analyze_ama_price_changes.d.ts.map +1 -0
  27. package/dist/analysis/ama_fitting/analyze_ama_price_changes.js +148 -0
  28. package/dist/analysis/ama_fitting/analyze_ama_price_changes.js.map +1 -0
  29. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.d.ts +12 -0
  30. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.d.ts.map +1 -0
  31. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js +389 -0
  32. package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js.map +1 -0
  33. package/dist/analysis/ama_fitting/calibrate_convergence_er.d.ts +3 -0
  34. package/dist/analysis/ama_fitting/calibrate_convergence_er.d.ts.map +1 -0
  35. package/dist/analysis/ama_fitting/calibrate_convergence_er.js +200 -0
  36. package/dist/analysis/ama_fitting/calibrate_convergence_er.js.map +1 -0
  37. package/dist/analysis/ama_fitting/fetch_lp_candles.d.ts +3 -0
  38. package/dist/analysis/ama_fitting/fetch_lp_candles.d.ts.map +1 -0
  39. package/dist/analysis/ama_fitting/fetch_lp_candles.js +213 -0
  40. package/dist/analysis/ama_fitting/fetch_lp_candles.js.map +1 -0
  41. package/dist/analysis/ama_fitting/generate_unified_comparison_chart.d.ts +25 -0
  42. package/dist/analysis/ama_fitting/generate_unified_comparison_chart.d.ts.map +1 -0
  43. package/dist/analysis/ama_fitting/generate_unified_comparison_chart.js +247 -0
  44. package/dist/analysis/ama_fitting/generate_unified_comparison_chart.js.map +1 -0
  45. package/dist/analysis/ama_fitting/optimizer_high_resolution.d.ts +35 -0
  46. package/dist/analysis/ama_fitting/optimizer_high_resolution.d.ts.map +1 -0
  47. package/dist/analysis/ama_fitting/optimizer_high_resolution.js +782 -0
  48. package/dist/analysis/ama_fitting/optimizer_high_resolution.js.map +1 -0
  49. package/dist/analysis/analyze_derivatives.d.ts +86 -0
  50. package/dist/analysis/analyze_derivatives.d.ts.map +1 -0
  51. package/dist/analysis/analyze_derivatives.js +285 -0
  52. package/dist/analysis/analyze_derivatives.js.map +1 -0
  53. package/dist/analysis/analyze_dynamic_weight.d.ts +14 -0
  54. package/dist/analysis/analyze_dynamic_weight.d.ts.map +1 -0
  55. package/dist/analysis/analyze_dynamic_weight.js +197 -0
  56. package/dist/analysis/analyze_dynamic_weight.js.map +1 -0
  57. package/dist/analysis/analyze_kalman.d.ts +13 -0
  58. package/dist/analysis/analyze_kalman.d.ts.map +1 -0
  59. package/dist/analysis/analyze_kalman.js +118 -0
  60. package/dist/analysis/analyze_kalman.js.map +1 -0
  61. package/dist/analysis/analyze_regime.d.ts +20 -0
  62. package/dist/analysis/analyze_regime.d.ts.map +1 -0
  63. package/dist/analysis/analyze_regime.js +139 -0
  64. package/dist/analysis/analyze_regime.js.map +1 -0
  65. package/dist/analysis/analyze_regime_windows.d.ts +19 -0
  66. package/dist/analysis/analyze_regime_windows.d.ts.map +1 -0
  67. package/dist/analysis/analyze_regime_windows.js +414 -0
  68. package/dist/analysis/analyze_regime_windows.js.map +1 -0
  69. package/dist/analysis/analyze_risk_profile.d.ts +3 -0
  70. package/dist/analysis/analyze_risk_profile.d.ts.map +1 -0
  71. package/dist/analysis/analyze_risk_profile.js +186 -0
  72. package/dist/analysis/analyze_risk_profile.js.map +1 -0
  73. package/dist/analysis/analyze_trade_heatmap.d.ts +3 -0
  74. package/dist/analysis/analyze_trade_heatmap.d.ts.map +1 -0
  75. package/dist/analysis/analyze_trade_heatmap.js +358 -0
  76. package/dist/analysis/analyze_trade_heatmap.js.map +1 -0
  77. package/dist/analysis/analyze_volatility.d.ts +22 -0
  78. package/dist/analysis/analyze_volatility.d.ts.map +1 -0
  79. package/dist/analysis/analyze_volatility.js +165 -0
  80. package/dist/analysis/analyze_volatility.js.map +1 -0
  81. package/dist/analysis/bot_fitting/backtest_ama_sweep.d.ts +91 -0
  82. package/dist/analysis/bot_fitting/backtest_ama_sweep.d.ts.map +1 -0
  83. package/dist/analysis/bot_fitting/backtest_ama_sweep.js +792 -0
  84. package/dist/analysis/bot_fitting/backtest_ama_sweep.js.map +1 -0
  85. package/dist/analysis/bot_fitting/backtest_bot_fitting.d.ts +2 -0
  86. package/dist/analysis/bot_fitting/backtest_bot_fitting.d.ts.map +1 -0
  87. package/dist/analysis/bot_fitting/backtest_bot_fitting.js +380 -0
  88. package/dist/analysis/bot_fitting/backtest_bot_fitting.js.map +1 -0
  89. package/dist/analysis/bot_fitting/shared_utils.d.ts +26 -0
  90. package/dist/analysis/bot_fitting/shared_utils.d.ts.map +1 -0
  91. package/dist/analysis/bot_fitting/shared_utils.js +42 -0
  92. package/dist/analysis/bot_fitting/shared_utils.js.map +1 -0
  93. package/dist/analysis/bot_key_utils.d.ts +11 -0
  94. package/dist/analysis/bot_key_utils.d.ts.map +1 -0
  95. package/dist/analysis/bot_key_utils.js +153 -0
  96. package/dist/analysis/bot_key_utils.js.map +1 -0
  97. package/dist/analysis/bot_usage/discover_bot_accounts.d.ts +3 -0
  98. package/dist/analysis/bot_usage/discover_bot_accounts.d.ts.map +1 -0
  99. package/dist/analysis/bot_usage/discover_bot_accounts.js +499 -0
  100. package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -0
  101. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +531 -0
  102. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -0
  103. package/dist/analysis/bot_usage/kibana_bot_queries.js +399 -0
  104. package/dist/analysis/bot_usage/kibana_bot_queries.js.map +1 -0
  105. package/dist/analysis/chart_css.d.ts +17 -0
  106. package/dist/analysis/chart_css.d.ts.map +1 -0
  107. package/dist/analysis/chart_css.js +49 -0
  108. package/dist/analysis/chart_css.js.map +1 -0
  109. package/dist/analysis/chart_ui.d.ts +45 -0
  110. package/dist/analysis/chart_ui.d.ts.map +1 -0
  111. package/dist/analysis/chart_ui.js +97 -0
  112. package/dist/analysis/chart_ui.js.map +1 -0
  113. package/dist/analysis/chart_utils.d.ts +27 -0
  114. package/dist/analysis/chart_utils.d.ts.map +1 -0
  115. package/dist/analysis/chart_utils.js +153 -0
  116. package/dist/analysis/chart_utils.js.map +1 -0
  117. package/dist/analysis/derivative_chart_generator.d.ts +12 -0
  118. package/dist/analysis/derivative_chart_generator.d.ts.map +1 -0
  119. package/dist/analysis/derivative_chart_generator.js +891 -0
  120. package/dist/analysis/derivative_chart_generator.js.map +1 -0
  121. package/dist/analysis/math_utils.d.ts +22 -0
  122. package/dist/analysis/math_utils.d.ts.map +1 -0
  123. package/dist/analysis/math_utils.js +43 -0
  124. package/dist/analysis/math_utils.js.map +1 -0
  125. package/dist/analysis/price_sources.d.ts +40 -0
  126. package/dist/analysis/price_sources.d.ts.map +1 -0
  127. package/dist/analysis/price_sources.js +94 -0
  128. package/dist/analysis/price_sources.js.map +1 -0
  129. package/dist/analysis/resolve_source.d.ts +25 -0
  130. package/dist/analysis/resolve_source.d.ts.map +1 -0
  131. package/dist/analysis/resolve_source.js +69 -0
  132. package/dist/analysis/resolve_source.js.map +1 -0
  133. package/dist/analysis/trade_profitability.d.ts +115 -0
  134. package/dist/analysis/trade_profitability.d.ts.map +1 -0
  135. package/dist/analysis/trade_profitability.js +1240 -0
  136. package/dist/analysis/trade_profitability.js.map +1 -0
  137. package/dist/analysis/tradingview/analyze_tradingview.d.ts +31 -0
  138. package/dist/analysis/tradingview/analyze_tradingview.d.ts.map +1 -0
  139. package/dist/analysis/tradingview/analyze_tradingview.js +146 -0
  140. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -0
  141. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts +5 -0
  142. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -0
  143. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +1714 -0
  144. package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -0
  145. package/dist/analysis/trend_detection/derivative_analyzer.d.ts +247 -0
  146. package/dist/analysis/trend_detection/derivative_analyzer.d.ts.map +1 -0
  147. package/dist/analysis/trend_detection/derivative_analyzer.js +901 -0
  148. package/dist/analysis/trend_detection/derivative_analyzer.js.map +1 -0
  149. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts +3 -0
  150. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts.map +1 -0
  151. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js +1361 -0
  152. package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js.map +1 -0
  153. package/dist/analysis/trend_detection/hurst_analyzer.d.ts +6 -0
  154. package/dist/analysis/trend_detection/hurst_analyzer.d.ts.map +1 -0
  155. package/dist/analysis/trend_detection/hurst_analyzer.js +7 -0
  156. package/dist/analysis/trend_detection/hurst_analyzer.js.map +1 -0
  157. package/dist/analysis/trend_detection/kalman_chart_generator.d.ts +3 -0
  158. package/dist/analysis/trend_detection/kalman_chart_generator.d.ts.map +1 -0
  159. package/dist/analysis/trend_detection/kalman_chart_generator.js +360 -0
  160. package/dist/analysis/trend_detection/kalman_chart_generator.js.map +1 -0
  161. package/dist/analysis/trend_detection/kalman_trend_analyzer.d.ts +8 -0
  162. package/dist/analysis/trend_detection/kalman_trend_analyzer.d.ts.map +1 -0
  163. package/dist/analysis/trend_detection/kalman_trend_analyzer.js +8 -0
  164. package/dist/analysis/trend_detection/kalman_trend_analyzer.js.map +1 -0
  165. package/dist/analysis/trend_detection/kalman_velocity_smoothing.d.ts +6 -0
  166. package/dist/analysis/trend_detection/kalman_velocity_smoothing.d.ts.map +1 -0
  167. package/dist/analysis/trend_detection/kalman_velocity_smoothing.js +7 -0
  168. package/dist/analysis/trend_detection/kalman_velocity_smoothing.js.map +1 -0
  169. package/dist/analysis/trend_detection/permutation_entropy_analyzer.d.ts +6 -0
  170. package/dist/analysis/trend_detection/permutation_entropy_analyzer.d.ts.map +1 -0
  171. package/dist/analysis/trend_detection/permutation_entropy_analyzer.js +7 -0
  172. package/dist/analysis/trend_detection/permutation_entropy_analyzer.js.map +1 -0
  173. package/dist/analysis/trend_detection/regime_chart_generator.d.ts +3 -0
  174. package/dist/analysis/trend_detection/regime_chart_generator.d.ts.map +1 -0
  175. package/dist/analysis/trend_detection/regime_chart_generator.js +310 -0
  176. package/dist/analysis/trend_detection/regime_chart_generator.js.map +1 -0
  177. package/dist/analysis/trend_detection/tests/test_kalman_trend.d.ts +2 -0
  178. package/dist/analysis/trend_detection/tests/test_kalman_trend.d.ts.map +1 -0
  179. package/dist/analysis/trend_detection/tests/test_kalman_trend.js +128 -0
  180. package/dist/analysis/trend_detection/tests/test_kalman_trend.js.map +1 -0
  181. package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.d.ts +2 -0
  182. package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.d.ts.map +1 -0
  183. package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.js +37 -0
  184. package/dist/analysis/trend_detection/tests/test_kalman_velocity_smoothing.js.map +1 -0
  185. package/dist/analysis/trend_detection/volatility_chart_generator.d.ts +3 -0
  186. package/dist/analysis/trend_detection/volatility_chart_generator.d.ts.map +1 -0
  187. package/dist/analysis/trend_detection/volatility_chart_generator.js +646 -0
  188. package/dist/analysis/trend_detection/volatility_chart_generator.js.map +1 -0
  189. package/dist/bot.js +1 -1
  190. package/dist/bot.js.map +1 -1
  191. package/dist/market_adapter/ama_signal_runner.js +1 -1
  192. package/dist/market_adapter/ama_signal_runner.js.map +1 -1
  193. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +1 -1
  194. package/dist/modules/account_bots.d.ts +1 -0
  195. package/dist/modules/account_bots.d.ts.map +1 -1
  196. package/dist/modules/account_bots.js +27 -5
  197. package/dist/modules/account_bots.js.map +1 -1
  198. package/dist/modules/constants.d.ts +1 -0
  199. package/dist/modules/constants.d.ts.map +1 -1
  200. package/dist/modules/constants.js +12 -3
  201. package/dist/modules/constants.js.map +1 -1
  202. package/dist/modules/dexbot_class.d.ts +22 -0
  203. package/dist/modules/dexbot_class.d.ts.map +1 -1
  204. package/dist/modules/dexbot_class.js +27 -1
  205. package/dist/modules/dexbot_class.js.map +1 -1
  206. package/dist/modules/dexbot_cow_runtime.d.ts +48 -1
  207. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  208. package/dist/modules/dexbot_cow_runtime.js +168 -4
  209. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  210. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  211. package/dist/modules/order/grid_reconcile_internal.js +6 -27
  212. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  213. package/dist/modules/order/strategy.d.ts.map +1 -1
  214. package/dist/modules/order/strategy.js +6 -15
  215. package/dist/modules/order/strategy.js.map +1 -1
  216. package/dist/modules/order/utils/math.d.ts +29 -1
  217. package/dist/modules/order/utils/math.d.ts.map +1 -1
  218. package/dist/modules/order/utils/math.js +46 -1
  219. package/dist/modules/order/utils/math.js.map +1 -1
  220. package/dist/modules/order/utils/system.d.ts +2 -0
  221. package/dist/modules/order/utils/system.d.ts.map +1 -1
  222. package/dist/modules/order/utils/system.js +30 -2
  223. package/dist/modules/order/utils/system.js.map +1 -1
  224. package/dist/modules/runtime_settings.d.ts +1 -0
  225. package/dist/modules/runtime_settings.d.ts.map +1 -1
  226. package/dist/modules/runtime_settings.js +13 -2
  227. package/dist/modules/runtime_settings.js.map +1 -1
  228. package/dist/scripts/generate_lp_chart.js +1 -1
  229. package/dist/scripts/test-credit-renewal.js +1 -1
  230. package/docs/DEXBOT_COMPARISON.md +3 -3
  231. package/docs/EVOLUTION.md +26 -95
  232. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  233. package/docs/GRID_RECALCULATION.md +2 -2
  234. package/docs/LIFECYCLE.md +207 -0
  235. package/docs/README.md +9 -1
  236. package/docs/developer_guide.md +4 -3
  237. package/market_adapter/README.md +23 -23
  238. package/package.json +18 -21
  239. package/scripts/README.md +12 -12
  240. package/scripts/bots +5 -7
  241. package/scripts/clear-market-adapter.sh +1 -1
  242. package/scripts/dexbot +5 -7
  243. package/scripts/keys +5 -7
  244. package/scripts/pm2 +5 -7
  245. package/scripts/unlock +5 -7
  246. package/scripts/update.js +6 -7
  247. package/scripts/repo-stats.html +0 -480
@@ -0,0 +1,1240 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ import fs from 'node:fs';
4
+ import { pathToFileURL } from 'node:url';
5
+ import * as KC from '../market_adapter/core/kibana_client.js';
6
+ import * as C from '../modules/constants.js';
7
+ const { kibanaSearch, DEFAULT_CONFIG: BASE_CONFIG } = KC;
8
+ /**
9
+ * TRADE PROFITABILITY ANALYZER
10
+ *
11
+ * Fetches fill_order operations for a BitShares account from Kibana
12
+ * within a specified time range, then analyzes profitability using
13
+ * FIFO or sequential (LIFO) inventory tracking per asset pair.
14
+ *
15
+ * Usage:
16
+ * tsx analysis/trade_profitability.ts 1.2.3 --start 2025-01-01 --end 2025-06-01
17
+ * tsx analysis/trade_profitability.ts 1.2.3 --hours 720
18
+ * tsx analysis/trade_profitability.ts 1.2.3 --start 2025-01-01T00:00:00Z --end 2025-06-01T00:00:00Z
19
+ * tsx analysis/trade_profitability.ts 1.2.3 --hours 168 --asset 1.3.113
20
+ * tsx analysis/trade_profitability.ts 1.2.3 --hours 168 --csv trades.csv
21
+ * tsx analysis/trade_profitability.ts 1.2.3 --hours 168 --json results.json
22
+ * tsx analysis/trade_profitability.ts "account-name" --lookup
23
+ * tsx analysis/trade_profitability.ts 1.2.3 --hours 168 --trades
24
+ * tsx analysis/trade_profitability.ts 1.2.3 --hours 168 --match-mode fifo
25
+ */
26
+ // ─── Constants ────────────────────────────────────────────────────────────────
27
+ const OP_FILL_ORDER = 4;
28
+ const BTS_ID = '1.3.0';
29
+ let BLOCKCHAIN_FEE_PER_FILL = 0.09652; // BTS — flat blockchain operation fee (not market fee); override with --fee-per-order
30
+ const ASSETS = {
31
+ '1.3.0': { symbol: 'BTS', precision: 5 },
32
+ '1.3.118': { symbol: 'GBP', precision: 4 },
33
+ '1.3.119': { symbol: 'JPY', precision: 2 },
34
+ '1.3.120': { symbol: 'EUR', precision: 4 },
35
+ '1.3.1325': { symbol: 'RUBLE', precision: 5 },
36
+ '1.3.2512': { symbol: 'EVRAZ', precision: 4 },
37
+ '1.3.3291': { symbol: 'TWENTIX', precision: 5 },
38
+ '1.3.4099': { symbol: 'XBTSX.STH', precision: 6 },
39
+ '1.3.4156': { symbol: 'XBTSX.DOGE', precision: 5 },
40
+ '1.3.4157': { symbol: 'XBTSX.BTC', precision: 8 },
41
+ '1.3.4159': { symbol: 'XBTSX.LTC', precision: 8 },
42
+ '1.3.4176': { symbol: 'XBTSX.DASH', precision: 8 },
43
+ '1.3.4274': { symbol: 'XBTSX.BCH', precision: 8 },
44
+ '1.3.4760': { symbol: 'XBTSX.ETH', precision: 7 },
45
+ '1.3.5537': { symbol: 'IOB.XRP', precision: 4 },
46
+ '1.3.5541': { symbol: 'XBTSX.BNB', precision: 7 },
47
+ '1.3.5589': { symbol: 'XBTSX.USDT', precision: 6 },
48
+ '1.3.5641': { symbol: 'HONEST.CNY', precision: 4 },
49
+ '1.3.5649': { symbol: 'HONEST.USD', precision: 4 },
50
+ '1.3.5650': { symbol: 'HONEST.BTC', precision: 8 },
51
+ '1.3.5659': { symbol: 'HONEST.ETH', precision: 6 },
52
+ '1.3.5870': { symbol: 'XBTSX.FIL', precision: 6 },
53
+ '1.3.5887': { symbol: 'XBTSX.RUB', precision: 4 },
54
+ '1.3.5902': { symbol: 'XBTSX.USDC', precision: 6 },
55
+ '1.3.6013': { symbol: 'XBTSX.HIVE', precision: 6 },
56
+ '1.3.6124': { symbol: 'XBTSX.AVAX', precision: 6 },
57
+ '1.3.6139': { symbol: 'XBTSX.XAUT', precision: 6 },
58
+ '1.3.6166': { symbol: 'XBTSX.MATIC', precision: 5 },
59
+ '1.3.6241': { symbol: 'XBTSX.ETC', precision: 7 },
60
+ '1.3.6268': { symbol: 'BTWTY.EOS', precision: 4 },
61
+ '1.3.6301': { symbol: 'HONEST.MONEY', precision: 8 },
62
+ '1.3.6304': { symbol: 'HONEST.ADA', precision: 8 },
63
+ '1.3.6305': { symbol: 'HONEST.DOT', precision: 8 },
64
+ '1.3.6309': { symbol: 'HONEST.ATOM', precision: 8 },
65
+ '1.3.6311': { symbol: 'HONEST.ALGO', precision: 8 },
66
+ '1.3.6312': { symbol: 'HONEST.FIL', precision: 8 },
67
+ '1.3.6313': { symbol: 'HONEST.EOS', precision: 8 },
68
+ '1.3.6315': { symbol: 'HONEST.EUR', precision: 4 },
69
+ '1.3.6316': { symbol: 'HONEST.GBP', precision: 4 },
70
+ '1.3.6317': { symbol: 'HONEST.JPY', precision: 4 },
71
+ '1.3.6444': { symbol: 'IOB.XLM', precision: 4 },
72
+ '1.3.6573': { symbol: 'XBTSX.DAI', precision: 6 },
73
+ '1.3.6620': { symbol: 'XBTSX.A', precision: 6 },
74
+ '1.3.6627': { symbol: 'XBTSX.LINK', precision: 6 },
75
+ };
76
+ const resolvedPrecisions = {};
77
+ function assetSymbol(id) {
78
+ return ASSETS[id]?.symbol ?? id;
79
+ }
80
+ function assetPrec(id) {
81
+ return ASSETS[id]?.precision ?? resolvedPrecisions[id];
82
+ }
83
+ function getPrec(id) {
84
+ return assetPrec(id);
85
+ }
86
+ function toReal(amount, assetId) {
87
+ const p = getPrec(assetId);
88
+ if (p === undefined)
89
+ return NaN;
90
+ return amount / Math.pow(10, p);
91
+ }
92
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
93
+ function printHelp() {
94
+ console.log(`\
95
+ Usage: tsx analysis/trade_profitability.ts <accountId> [options]
96
+
97
+ Analyzes filled orders for a BitShares account, computing realized PnL
98
+ via FIFO or sequential (LIFO) inventory tracking.
99
+
100
+ Arguments:
101
+ accountId BitShares account ID (1.2.x) or name (with --lookup)
102
+
103
+ Options:
104
+ --start <iso> Start time (ISO 8601, e.g. 2025-01-01 or 2025-01-01T00:00:00Z)
105
+ --end <iso> End time (ISO 8601)
106
+ --hours <n> Lookback hours from now (alternative to --start/--end)
107
+ --asset <assetId> Filter to one base asset (e.g. 1.3.113 for bitUSD)
108
+ --lookup Resolve account name to ID via BitShares node
109
+ --node <url> BitShares node URL (default: first healthy from built-in pool)
110
+ --csv <file> Export trade list as CSV
111
+ --json <file> Export full analysis as JSON
112
+ --trades Show per-order PnL detail (hidden by default)
113
+ --match-mode <mode> Matching mode: sequential (default, LIFO) or fifo
114
+ --fee-per-order <bts> Blockchain fee per limit_order_create op in BTS (default: 0.09652)
115
+ --verbose Print extra debug info
116
+ --help, -h Show this help
117
+
118
+ Examples:
119
+ tsx analysis/trade_profitability.ts 1.2.123456 --hours 720
120
+ tsx analysis/trade_profitability.ts 1.2.123456 --start 2025-01-01 --end 2025-06-01
121
+ tsx analysis/trade_profitability.ts "my-bot-account" --lookup --hours 168
122
+ tsx analysis/trade_profitability.ts 1.2.123456 --hours 720 --asset 1.3.113 --csv trades.csv
123
+ tsx analysis/trade_profitability.ts 1.2.123456 --hours 720 --match-mode sequential`);
124
+ }
125
+ function parseArgs() {
126
+ const args = process.argv.slice(2);
127
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
128
+ printHelp();
129
+ process.exit(0);
130
+ }
131
+ const opts = {
132
+ accountId: args[0],
133
+ hours: null,
134
+ start: null,
135
+ end: null,
136
+ asset: null,
137
+ lookup: false,
138
+ node: C.NODE_MANAGEMENT.DEFAULT_NODES[0],
139
+ csv: null,
140
+ json: null,
141
+ matchMode: 'sequential',
142
+ showPnlDetail: false,
143
+ verbose: false,
144
+ feePerOrder: BLOCKCHAIN_FEE_PER_FILL,
145
+ };
146
+ for (let i = 1; i < args.length; i++) {
147
+ switch (args[i]) {
148
+ case '--hours':
149
+ opts.hours = parseInt(args[++i], 10);
150
+ break;
151
+ case '--start':
152
+ opts.start = args[++i];
153
+ break;
154
+ case '--end':
155
+ opts.end = args[++i];
156
+ break;
157
+ case '--asset':
158
+ opts.asset = args[++i];
159
+ break;
160
+ case '--lookup':
161
+ opts.lookup = true;
162
+ break;
163
+ case '--node':
164
+ opts.node = args[++i];
165
+ break;
166
+ case '--csv':
167
+ opts.csv = args[++i];
168
+ break;
169
+ case '--json':
170
+ opts.json = args[++i];
171
+ break;
172
+ case '--trades':
173
+ opts.showPnlDetail = true;
174
+ break;
175
+ case '--fee-per-order':
176
+ opts.feePerOrder = parseFloat(args[++i]);
177
+ break;
178
+ case '--match-mode': {
179
+ const m = args[++i];
180
+ if (m !== 'sequential' && m !== 'fifo') {
181
+ console.error(`Invalid --match-mode: ${m} (expected: sequential | fifo)`);
182
+ process.exit(1);
183
+ }
184
+ opts.matchMode = m;
185
+ break;
186
+ }
187
+ case '--verbose':
188
+ opts.verbose = true;
189
+ break;
190
+ default:
191
+ console.error(`Unknown option: ${args[i]}`);
192
+ process.exit(1);
193
+ }
194
+ }
195
+ if (!opts.hours && !opts.start) {
196
+ opts.hours = 168; // default: 7 days
197
+ }
198
+ return opts;
199
+ }
200
+ // ─── Account name resolution ─────────────────────────────────────────────────
201
+ async function resolveAccountId(name, nodeUrl) {
202
+ const { createReadOnlyClient } = await import('../modules/bitshares-native/index.js');
203
+ const client = createReadOnlyClient({ nodes: [nodeUrl] });
204
+ // Suppress transport INFO logs during ephemeral connection:
205
+ // bitshares-native transport logger (new Logger('Transport')) writes
206
+ // "[timestamp] [INFO] [Transport] ..." — silence by raising log level.
207
+ const prevLevel = process.env.LOG_LEVEL;
208
+ process.env.LOG_LEVEL = 'warn';
209
+ try {
210
+ await client.connect();
211
+ const accounts = await client.db('lookup_account_names', [[name]]);
212
+ if (Array.isArray(accounts) && accounts[0]?.id) {
213
+ return accounts[0].id;
214
+ }
215
+ return null;
216
+ }
217
+ catch (e) {
218
+ console.warn(` [warn] Account resolution failed: ${e.message}`);
219
+ return null;
220
+ }
221
+ finally {
222
+ try {
223
+ client.disconnect();
224
+ }
225
+ catch (_) { }
226
+ process.env.LOG_LEVEL = prevLevel;
227
+ }
228
+ }
229
+ // ─── On-chain asset precision resolution ────────────────────────────────────
230
+ /**
231
+ * Collects all unique non-BTS asset IDs from fills, resolves unknown
232
+ * precisions from the blockchain, and populates the runtime cache.
233
+ */
234
+ async function resolveAssetPrecisions(fills, nodeUrl) {
235
+ const unknownIds = new Set();
236
+ for (const f of fills) {
237
+ for (const id of [f.pays.asset_id, f.receives.asset_id, f.fee.asset_id]) {
238
+ if (id !== BTS_ID && !(id in ASSETS) && !(id in resolvedPrecisions)) {
239
+ unknownIds.add(id);
240
+ }
241
+ }
242
+ }
243
+ if (unknownIds.size === 0 || !nodeUrl)
244
+ return;
245
+ const ids = [...unknownIds];
246
+ console.log(` Resolving ${ids.length} unknown asset(s) from blockchain...`);
247
+ const { createReadOnlyClient } = await import('../modules/bitshares-native/index.js');
248
+ const client = createReadOnlyClient({ nodes: [nodeUrl] });
249
+ try {
250
+ await client.connect();
251
+ const assets = await client.db('get_assets', [ids]);
252
+ if (Array.isArray(assets)) {
253
+ for (const asset of assets) {
254
+ if (asset?.id && asset.precision != null) {
255
+ resolvedPrecisions[asset.id] = asset.precision;
256
+ console.log(` ${asset.id} → ${asset.symbol || '?'} (precision ${asset.precision})`);
257
+ }
258
+ }
259
+ }
260
+ const missing = ids.filter(id => !(id in resolvedPrecisions));
261
+ if (missing.length > 0) {
262
+ console.warn(` [warn] ${missing.length} asset(s) not found on chain: ${missing.join(', ')}. Fills referencing them will be skipped.`);
263
+ }
264
+ }
265
+ catch (e) {
266
+ console.warn(` [warn] Asset resolution failed: ${e.message}. Fills with unknown assets will be skipped.`);
267
+ }
268
+ finally {
269
+ try {
270
+ client.disconnect();
271
+ }
272
+ catch (_) { }
273
+ }
274
+ }
275
+ // ─── Kibana Query ────────────────────────────────────────────────────────────
276
+ function buildFillQuery(accountId, gte, lte, size) {
277
+ return {
278
+ size,
279
+ track_total_hits: false,
280
+ _source: [
281
+ 'block_data.block_time',
282
+ 'block_data.block_num',
283
+ 'operation_id_num',
284
+ 'operation_history.op_object.pays',
285
+ 'operation_history.op_object.receives',
286
+ 'operation_history.op_object.fee',
287
+ 'operation_history.op_object.order_id',
288
+ 'operation_history.op_object.account_id',
289
+ 'operation_history.op_object.is_maker',
290
+ ],
291
+ query: {
292
+ bool: {
293
+ filter: [
294
+ { term: { operation_type: OP_FILL_ORDER } },
295
+ { term: { 'operation_history.op_object.account_id.keyword': accountId } },
296
+ { range: { 'block_data.block_time': { gte, lte } } },
297
+ ],
298
+ },
299
+ },
300
+ sort: [
301
+ { 'block_data.block_time': { order: 'asc' } },
302
+ { operation_id_num: { order: 'asc' } },
303
+ ],
304
+ };
305
+ }
306
+ async function fetchAllFills(config, accountId, gte, lte) {
307
+ const pageSize = 10000;
308
+ const fills = [];
309
+ let searchAfter = null;
310
+ const cfg = { ...BASE_CONFIG, timeout: 60000, ...config };
311
+ while (true) {
312
+ const query = buildFillQuery(accountId, gte, lte, pageSize);
313
+ if (searchAfter)
314
+ query.search_after = searchAfter;
315
+ const result = await kibanaSearch(cfg, query);
316
+ const hits = result?.hits?.hits ?? [];
317
+ if (!hits.length)
318
+ break;
319
+ for (const hit of hits) {
320
+ const src = hit?._source;
321
+ const op = src?.operation_history?.op_object;
322
+ if (!op || !op.pays || !op.receives)
323
+ continue;
324
+ fills.push({
325
+ time: src.block_data?.block_time ?? '',
326
+ blockNum: src.block_data?.block_num ?? 0,
327
+ opNum: Number(src.operation_id_num ?? 0),
328
+ orderId: op.order_id ?? '',
329
+ accountId: op.account_id ?? '',
330
+ pays: { amount: Number(op.pays.amount ?? 0), asset_id: op.pays.asset_id ?? '' },
331
+ receives: { amount: Number(op.receives.amount ?? 0), asset_id: op.receives.asset_id ?? '' },
332
+ fee: { amount: Number(op.fee?.amount ?? 0), asset_id: op.fee?.asset_id ?? '' },
333
+ isMaker: op.is_maker ?? false,
334
+ sort: hit.sort,
335
+ });
336
+ }
337
+ if (hits.length < pageSize)
338
+ break;
339
+ searchAfter = hits[hits.length - 1].sort;
340
+ if (!Array.isArray(searchAfter))
341
+ break;
342
+ }
343
+ return fills;
344
+ }
345
+ // ─── Fill Classification ─────────────────────────────────────────────────────
346
+ function classifyFills(fills, filterAsset) {
347
+ const trades = [];
348
+ const pairs = new Set();
349
+ let skipped = 0;
350
+ for (const f of fills) {
351
+ const pAsset = f.pays.asset_id;
352
+ const rAsset = f.receives.asset_id;
353
+ let direction;
354
+ let baseAsset;
355
+ let quoteAsset;
356
+ let baseAmount;
357
+ let quoteAmount;
358
+ let price;
359
+ let marketFeeReal;
360
+ let marketFeeAsset;
361
+ const feePrec = getPrec(f.fee.asset_id);
362
+ if (feePrec === undefined || isNaN(f.fee.amount)) {
363
+ skipped++;
364
+ continue;
365
+ }
366
+ const feeReal = f.fee.amount / Math.pow(10, feePrec);
367
+ if (pAsset === BTS_ID && rAsset !== BTS_ID) {
368
+ direction = 'buy';
369
+ baseAsset = rAsset;
370
+ quoteAsset = BTS_ID;
371
+ baseAmount = toReal(f.receives.amount, rAsset);
372
+ quoteAmount = toReal(f.pays.amount, BTS_ID);
373
+ if (isNaN(baseAmount) || isNaN(quoteAmount)) {
374
+ skipped++;
375
+ continue;
376
+ }
377
+ price = quoteAmount / baseAmount;
378
+ marketFeeReal = feeReal;
379
+ marketFeeAsset = f.fee.asset_id;
380
+ }
381
+ else if (rAsset === BTS_ID && pAsset !== BTS_ID) {
382
+ direction = 'sell';
383
+ baseAsset = pAsset;
384
+ quoteAsset = BTS_ID;
385
+ baseAmount = toReal(f.pays.amount, pAsset);
386
+ quoteAmount = toReal(f.receives.amount, BTS_ID);
387
+ if (isNaN(baseAmount) || isNaN(quoteAmount)) {
388
+ skipped++;
389
+ continue;
390
+ }
391
+ price = quoteAmount / baseAmount;
392
+ marketFeeReal = feeReal;
393
+ marketFeeAsset = f.fee.asset_id;
394
+ }
395
+ else {
396
+ // Non-BTS cross-pair: use consistent ordering (lower asset ID = base)
397
+ const baseForCheck = pAsset < rAsset ? pAsset : rAsset;
398
+ const quoteForCheck = pAsset < rAsset ? rAsset : pAsset;
399
+ // Skip if either asset precision is unknown
400
+ if (getPrec(baseForCheck) === undefined || getPrec(quoteForCheck) === undefined) {
401
+ skipped++;
402
+ continue;
403
+ }
404
+ const isSell = pAsset < rAsset;
405
+ direction = isSell ? 'sell' : 'buy';
406
+ baseAsset = isSell ? pAsset : rAsset;
407
+ quoteAsset = isSell ? rAsset : pAsset;
408
+ baseAmount = toReal(isSell ? f.pays.amount : f.receives.amount, baseAsset);
409
+ quoteAmount = toReal(isSell ? f.receives.amount : f.pays.amount, quoteAsset);
410
+ if (isNaN(baseAmount) || isNaN(quoteAmount)) {
411
+ skipped++;
412
+ continue;
413
+ }
414
+ price = quoteAmount / baseAmount;
415
+ marketFeeReal = feeReal;
416
+ marketFeeAsset = f.fee.asset_id;
417
+ }
418
+ if (filterAsset && baseAsset !== filterAsset)
419
+ continue;
420
+ // Validate market fee asset: fee is always deducted from receives
421
+ // (base for buys, quote for sells). Warn if unexpected.
422
+ if (marketFeeReal > 0 && marketFeeAsset !== '' && marketFeeAsset !== rAsset) {
423
+ console.warn(` [warn] Fill ${f.orderId}: fee asset ${marketFeeAsset} ≠ receives asset ${rAsset}. Market fee PnL may be incorrect.`);
424
+ }
425
+ const pairKey = `${baseAsset}:${quoteAsset}`;
426
+ pairs.add(pairKey);
427
+ trades.push({
428
+ time: f.time,
429
+ orderId: f.orderId,
430
+ direction,
431
+ baseAsset,
432
+ quoteAsset,
433
+ baseAmount,
434
+ quoteAmount,
435
+ price,
436
+ isMaker: f.isMaker,
437
+ sequence: f.blockNum * 1e6 + f.opNum,
438
+ marketFeeReal,
439
+ marketFeeAsset,
440
+ });
441
+ }
442
+ if (skipped > 0) {
443
+ console.warn(` [warn] ${skipped} fill(s) skipped due to unknown asset precision.`);
444
+ }
445
+ return { trades, pairs };
446
+ }
447
+ // ─── PnL Calculation (FIFO / Sequential) ─────────────────────────────────────
448
+ function analyzePair(trades, matchMode = 'sequential') {
449
+ const buys = trades.filter(t => t.direction === 'buy').sort((a, b) => a.sequence - b.sequence);
450
+ const sells = trades.filter(t => t.direction === 'sell').sort((a, b) => a.sequence - b.sequence);
451
+ const totalBuyBase = buys.reduce((s, t) => s + t.baseAmount, 0);
452
+ const totalSellBase = sells.reduce((s, t) => s + t.baseAmount, 0);
453
+ const totalBuyQuote = buys.reduce((s, t) => s + t.quoteAmount, 0);
454
+ const totalSellQuote = sells.reduce((s, t) => s + t.quoteAmount, 0);
455
+ // Merge all trades chronologically.
456
+ // FIFO: buys add to queue, sells consume oldest lots (queue front).
457
+ // Sequential: buys add to queue, sells consume newest lots (queue back / LIFO).
458
+ const all = [...trades].sort((a, b) => a.sequence - b.sequence);
459
+ const inventory = [];
460
+ const realizedPnls = [];
461
+ let unmatchedSellBase = 0;
462
+ for (const trade of all) {
463
+ const grossPrice = trade.price;
464
+ if (trade.direction === 'buy') {
465
+ // Enter lot with net amount (gross receives minus market fee on receives)
466
+ const lotAmount = trade.baseAmount - trade.marketFeeReal;
467
+ if (lotAmount < 1e-12)
468
+ continue;
469
+ const lotEffPrice = trade.quoteAmount / lotAmount;
470
+ inventory.push({
471
+ amount: lotAmount,
472
+ grossPrice: grossPrice,
473
+ effPrice: lotEffPrice,
474
+ time: trade.time,
475
+ entryOrderId: trade.orderId,
476
+ entryIsMaker: trade.isMaker,
477
+ });
478
+ }
479
+ else {
480
+ let remaining = trade.baseAmount;
481
+ while (remaining > 0.00000001 && inventory.length > 0) {
482
+ const lotIndex = matchMode === 'sequential' ? inventory.length - 1 : 0;
483
+ const lot = inventory[lotIndex];
484
+ const matched = Math.min(remaining, lot.amount);
485
+ // Gross PnL using gross prices (before market fee deduction)
486
+ const grossPnl = (grossPrice - lot.grossPrice) * matched;
487
+ const grossPnlPct = lot.grossPrice > 0 ? ((grossPrice - lot.grossPrice) / lot.grossPrice) * 100 : 0;
488
+ realizedPnls.push({
489
+ sellPrice: grossPrice,
490
+ buyPrice: lot.grossPrice,
491
+ amount: matched,
492
+ pnl: grossPnl,
493
+ pnlPct: grossPnlPct,
494
+ effPrice: lot.effPrice,
495
+ marketFeeEntry: 0,
496
+ marketFeeExit: 0,
497
+ feeBts: 0,
498
+ pnlNet: grossPnl,
499
+ pnlNetPct: grossPnlPct,
500
+ entryTime: lot.time,
501
+ exitTime: trade.time,
502
+ entryOrderId: lot.entryOrderId,
503
+ exitOrderId: trade.orderId,
504
+ entryIsMaker: lot.entryIsMaker,
505
+ exitIsMaker: trade.isMaker,
506
+ });
507
+ lot.amount -= matched;
508
+ remaining -= matched;
509
+ if (lot.amount < 0.00000001) {
510
+ if (matchMode === 'sequential') {
511
+ inventory.pop();
512
+ }
513
+ else {
514
+ inventory.shift();
515
+ }
516
+ }
517
+ }
518
+ if (remaining > 0.00000001) {
519
+ unmatchedSellBase += remaining;
520
+ }
521
+ }
522
+ }
523
+ // ─── Market fee allocation (aggregated per order) ──────────────────────
524
+ // A limit order may be filled across multiple fill_order_operations with
525
+ // the same orderId. Aggregate all fills' market fees per order, then
526
+ // allocate pro-rata using the fill's net acquired amount as denominator.
527
+ // (Market fee is paid on acquisition; the portion tied to unsold inventory
528
+ // is not yet realised.)
529
+ const entryOrderFees = new Map();
530
+ const exitOrderFees = new Map();
531
+ for (const t of buys) {
532
+ const e = entryOrderFees.get(t.orderId) || { feeInQuote: 0, totalAcquired: 0 };
533
+ if (t.marketFeeReal > 0) {
534
+ // Buy-side fee is in base → convert to quote using effective price
535
+ const netBase = t.baseAmount - t.marketFeeReal;
536
+ e.feeInQuote += netBase > 0 ? t.marketFeeReal * (t.quoteAmount / netBase) : 0;
537
+ }
538
+ e.totalAcquired += t.baseAmount - t.marketFeeReal;
539
+ entryOrderFees.set(t.orderId, e);
540
+ }
541
+ for (const t of sells) {
542
+ const e = exitOrderFees.get(t.orderId) || { feeInQuote: 0, totalDisposed: 0 };
543
+ if (t.marketFeeReal > 0)
544
+ e.feeInQuote += t.marketFeeReal; // Sell-side fee is already in quote
545
+ e.totalDisposed += t.baseAmount;
546
+ exitOrderFees.set(t.orderId, e);
547
+ }
548
+ for (const r of realizedPnls) {
549
+ const eObj = entryOrderFees.get(r.entryOrderId);
550
+ if (eObj && eObj.totalAcquired > 0)
551
+ r.marketFeeEntry = eObj.feeInQuote * (r.amount / eObj.totalAcquired);
552
+ const xObj = exitOrderFees.get(r.exitOrderId);
553
+ if (xObj && xObj.totalDisposed > 0)
554
+ r.marketFeeExit = xObj.feeInQuote * (r.amount / xObj.totalDisposed);
555
+ r.pnlNet = r.pnl - r.marketFeeEntry - r.marketFeeExit;
556
+ r.pnlNetPct = r.buyPrice > 0 ? (r.pnlNet / (r.buyPrice * r.amount)) * 100 : 0;
557
+ }
558
+ // ─── Blockchain fee allocation ─────────────────────────────────────────
559
+ const buyOrderIds = new Set(buys.map(t => t.orderId));
560
+ const sellOrderIds = new Set(sells.map(t => t.orderId));
561
+ const entryTotalMatched = new Map();
562
+ const exitTotalMatched = new Map();
563
+ for (const r of realizedPnls) {
564
+ entryTotalMatched.set(r.entryOrderId, (entryTotalMatched.get(r.entryOrderId) || 0) + r.amount);
565
+ exitTotalMatched.set(r.exitOrderId, (exitTotalMatched.get(r.exitOrderId) || 0) + r.amount);
566
+ }
567
+ const totalBlockchainFees = (buyOrderIds.size + sellOrderIds.size) * BLOCKCHAIN_FEE_PER_FILL;
568
+ for (const r of realizedPnls) {
569
+ const eTotal = entryTotalMatched.get(r.entryOrderId) || 1;
570
+ const xTotal = exitTotalMatched.get(r.exitOrderId) || 1;
571
+ r.feeBts = BLOCKCHAIN_FEE_PER_FILL * (r.amount / eTotal) + BLOCKCHAIN_FEE_PER_FILL * (r.amount / xTotal);
572
+ r.pnlNet -= r.feeBts;
573
+ const costBasis = r.effPrice * r.amount;
574
+ r.pnlNetPct = costBasis > 0 ? (r.pnlNet / costBasis) * 100 : 0;
575
+ }
576
+ const totalBuyBaseNet = buys.reduce((s, t) => s + t.baseAmount - t.marketFeeReal, 0);
577
+ const totalRealizedPnl = realizedPnls.reduce((s, r) => s + r.pnl, 0);
578
+ const totalMarketFees = realizedPnls.reduce((s, r) => s + r.marketFeeEntry + r.marketFeeExit, 0);
579
+ const totalRealizedPnlNet = realizedPnls.reduce((s, r) => s + r.pnlNet, 0);
580
+ const netPosition = totalBuyBaseNet - totalSellBase;
581
+ const baseAsset = trades[0]?.baseAsset ?? '';
582
+ const quoteAsset = trades[0]?.quoteAsset ?? '';
583
+ return {
584
+ baseAsset,
585
+ quoteAsset,
586
+ buys,
587
+ sells,
588
+ realizedPnls,
589
+ totalBuyBase,
590
+ totalSellBase,
591
+ totalBuyQuote,
592
+ totalSellQuote,
593
+ unmatchedSellBase,
594
+ totalRealizedPnl,
595
+ totalMarketFees,
596
+ totalBlockchainFees,
597
+ totalRealizedPnlNet,
598
+ netPosition,
599
+ };
600
+ }
601
+ // ─── Output Helpers ──────────────────────────────────────────────────────────
602
+ function fmt(n, decimals = 4) {
603
+ if (!Number.isFinite(n))
604
+ return 'NaN';
605
+ return n.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
606
+ }
607
+ function fmtPct(n) {
608
+ if (!Number.isFinite(n))
609
+ return 'NaN%';
610
+ return (n >= 0 ? '+' : '') + n.toFixed(2) + '%';
611
+ }
612
+ function fmtAsset(id) {
613
+ return assetSymbol(id);
614
+ }
615
+ function printSummary(pairs, _accountId, _start, _end, _matchMode = 'sequential') {
616
+ console.log('');
617
+ for (const pair of pairs) {
618
+ const pairLabel = `${fmtAsset(pair.baseAsset)}/${fmtAsset(pair.quoteAsset)}`;
619
+ const totalBought = pair.totalBuyBase;
620
+ const totalSold = pair.totalSellBase;
621
+ const avgBuy = pair.totalBuyBase > 0 ? pair.totalBuyQuote / pair.totalBuyBase : 0;
622
+ const avgSell = pair.totalSellBase > 0 ? pair.totalSellQuote / pair.totalSellBase : 0;
623
+ console.log(` ── ${pairLabel}`);
624
+ console.log(` Buys: ${fmt(totalBought, 4)} @ ${fmt(avgBuy, 6)} = ${fmt(pair.totalBuyQuote, 4)} ${fmtAsset(pair.quoteAsset)}`);
625
+ console.log(` Sells: ${fmt(totalSold, 4)} @ ${fmt(avgSell, 6)} = ${fmt(pair.totalSellQuote, 4)} ${fmtAsset(pair.quoteAsset)}`);
626
+ console.log(` Net traded: ${fmt(pair.netPosition, 4)} ${fmtAsset(pair.baseAsset)} (window flow)`);
627
+ console.log(` Trades: ${pair.realizedPnls.length} matched lots, ${pair.buys.length} buys, ${pair.sells.length} sells`);
628
+ if (pair.unmatchedSellBase > 0.0001) {
629
+ console.log(` Unmatched: ${fmt(pair.unmatchedSellBase, 4)} ${fmtAsset(pair.baseAsset)} (sold without prior buy in window — expected if inventory predates window)`);
630
+ }
631
+ console.log(` Gross PnL: ${fmtAsset(pair.quoteAsset)} ${fmt(pair.totalRealizedPnl, 4)}`);
632
+ const mktFee = pair.totalMarketFees;
633
+ const blkFee = pair.totalBlockchainFees;
634
+ if (mktFee > 0.0001) {
635
+ console.log(` Market fees: ${fmtAsset(pair.quoteAsset)} ${fmt(-mktFee, 4)}`);
636
+ }
637
+ if (blkFee > 0.0001) {
638
+ console.log(` Blockchain: BTS ${fmt(-blkFee, 4)}`);
639
+ }
640
+ console.log(` Net PnL: ${fmtAsset(pair.quoteAsset)} ${fmt(pair.totalRealizedPnlNet, 4)}`);
641
+ if (pair.quoteAsset !== BTS_ID) {
642
+ console.log(` ⚠ Non-BTS quote — PnL is in ${fmtAsset(pair.quoteAsset)}, not BTS`);
643
+ }
644
+ console.log('');
645
+ }
646
+ // Grand totals grouped by quote asset
647
+ const quoteGroups = new Map();
648
+ for (const p of pairs) {
649
+ const q = p.quoteAsset;
650
+ if (!quoteGroups.has(q))
651
+ quoteGroups.set(q, []);
652
+ quoteGroups.get(q).push(p);
653
+ }
654
+ if (pairs.length > 1 && quoteGroups.size > 0) {
655
+ for (const [quoteAsset, group] of quoteGroups) {
656
+ const groupPnl = group.reduce((s, p) => s + p.totalRealizedPnl, 0);
657
+ const groupMktFees = group.reduce((s, p) => s + p.totalMarketFees, 0);
658
+ const groupBlkFees = group.reduce((s, p) => s + p.totalBlockchainFees, 0);
659
+ const groupNet = group.reduce((s, p) => s + p.totalRealizedPnlNet, 0);
660
+ const groupVol = group.reduce((s, p) => s + p.totalBuyQuote + p.totalSellQuote, 0);
661
+ const qSymbol = fmtAsset(quoteAsset);
662
+ console.log(` ── TOTAL (${qSymbol}) — ${group.length} pair(s)`);
663
+ console.log(` Gross PnL: ${fmtAsset(quoteAsset)} ${fmt(groupPnl, 4)}`);
664
+ if (groupMktFees > 0.0001) {
665
+ console.log(` Market fees: ${fmtAsset(quoteAsset)} ${fmt(-groupMktFees, 4)}`);
666
+ }
667
+ if (groupBlkFees > 0.0001) {
668
+ console.log(` Blockchain: BTS ${fmt(-groupBlkFees, 4)}`);
669
+ }
670
+ console.log(` Net PnL: ${fmtAsset(quoteAsset)} ${fmt(groupNet, 4)}`);
671
+ console.log(` Volume: ${fmt(groupVol, 4)} ${qSymbol}`);
672
+ console.log('');
673
+ }
674
+ }
675
+ console.log(` Note: PnL uses gross prices. Net PnL deducts market fees (charged by`);
676
+ console.log(` asset issuer on receives, both issuer and network portions) and`);
677
+ console.log(` blockchain operation fees (BTS per limit_order_create). Market`);
678
+ console.log(` fees are converted to quote asset. Buy lots are entered at net`);
679
+ console.log(` receives (gross minus buy-side market fee) so inventory matching`);
680
+ console.log(` reflects what the account actually held. If inventory predates the`);
681
+ console.log(` window or crosses asset pairs, the matched lots may not reflect`);
682
+ console.log(` true trade economics.`);
683
+ console.log('');
684
+ }
685
+ function printPnlDetail(pairs) {
686
+ for (const pair of pairs) {
687
+ if (pair.realizedPnls.length === 0)
688
+ continue;
689
+ const pairLabel = `${fmtAsset(pair.baseAsset)}/${fmtAsset(pair.quoteAsset)}`;
690
+ console.log('');
691
+ console.log(` ── ${pairLabel} — Realized PnL Detail`);
692
+ console.log('');
693
+ const hdr = ' # Buy Price EffBuy Sell Price Amount PnL PnL% MktFee OpFeeBTS Net PnL Net% Legs Entry Time Exit Time';
694
+ console.log(hdr);
695
+ console.log(' ' + '─'.repeat(hdr.length - 1));
696
+ for (let i = 0; i < pair.realizedPnls.length; i++) {
697
+ const r = pair.realizedPnls[i];
698
+ const idx = String(i + 1).padStart(2);
699
+ const bp = fmt(r.buyPrice, 8).padStart(11);
700
+ const ep = fmt(r.effPrice, 8).padStart(11);
701
+ const sp = fmt(r.sellPrice, 8).padStart(11);
702
+ const amt = fmt(r.amount, 4).padStart(9);
703
+ const pnlStr = fmt(r.pnl, 6).padStart(9);
704
+ const pctStr = fmtPct(r.pnlPct).padStart(8);
705
+ const mktFeeStr = fmt(r.marketFeeEntry + r.marketFeeExit, 4).padStart(8);
706
+ const feeStr = fmt(r.feeBts, 4).padStart(8);
707
+ const netStr = fmt(r.pnlNet, 6).padStart(10);
708
+ const netPctStr = fmtPct(r.pnlNetPct).padStart(8);
709
+ const mk = (r.entryIsMaker ? 'M' : 'T') + '/' + (r.exitIsMaker ? 'M' : 'T') + ' ';
710
+ const et = (r.entryTime || '').slice(0, 22).padEnd(22);
711
+ const xt = (r.exitTime || '').slice(0, 22).padEnd(22);
712
+ console.log(` ${idx} ${bp} ${ep} ${sp} ${amt} ${pnlStr} ${pctStr} ${mktFeeStr} ${feeStr} ${netStr} ${netPctStr} ${mk} ${et} ${xt}`);
713
+ }
714
+ console.log('');
715
+ }
716
+ }
717
+ function percentile(sorted, p) {
718
+ const n = sorted.length;
719
+ if (n === 0)
720
+ return 0;
721
+ if (n === 1)
722
+ return sorted[0];
723
+ const k = (p / 100) * (n - 1);
724
+ const f = Math.floor(k);
725
+ const c = Math.ceil(k);
726
+ if (f === c)
727
+ return sorted[f];
728
+ return sorted[f] * (c - k) + sorted[c] * (k - f);
729
+ }
730
+ function computeMetrics(pair) {
731
+ const pnls = pair.realizedPnls;
732
+ const total = pnls.length;
733
+ if (total === 0) {
734
+ return {
735
+ totalLots: 0, winRate: 0, profitFactor: 0,
736
+ avgWin: 0, avgLoss: 0, avgWinLossRatio: 0,
737
+ expectancyBts: 0, expectancyPct: 0, expectancyR: 0,
738
+ netExpectancyBts: 0,
739
+ dailyPnlRatio: 0, dailyDownsideRatio: 0,
740
+ maxConsecWins: 0, maxConsecLosses: 0,
741
+ avgHoldHours: 0, limitOrderRatio: 0,
742
+ bestTradePct: 0, worstTradePct: 0,
743
+ mddPct: 0,
744
+ mddHadStablePeak: false,
745
+ isOngoingRecovery: false,
746
+ currentDrawdownDays: 0,
747
+ medianPnlPct: 0, p25PnlPct: 0, p75PnlPct: 0,
748
+ feeDragPct: 0, maxRecoveryDays: 0,
749
+ medianR: 0, pctRGreater1: 0, pctRGreater2: 0, pctRLessNeg1: 0,
750
+ sellOrdersFilled: 0, fillsPerOrderMean: 0, fillsPerOrderMedian: 0,
751
+ fillsPerOrderMax: 0, oneShotOrderRatio: 0, fillsPerDay: 0, avgVolumePerDay: 0,
752
+ };
753
+ }
754
+ const wins = pnls.filter(r => r.pnl > 0);
755
+ const losses = pnls.filter(r => r.pnl < 0);
756
+ const winRate = wins.length / total;
757
+ const makerEntryCount = pnls.filter(r => r.entryIsMaker).length;
758
+ const makerExitCount = pnls.filter(r => r.exitIsMaker).length;
759
+ const totalMakerLegs = makerEntryCount + makerExitCount;
760
+ const grossProfit = wins.reduce((s, r) => s + r.pnl, 0);
761
+ const grossLoss = Math.abs(losses.reduce((s, r) => s + r.pnl, 0));
762
+ const profitFactor = grossLoss > 0 ? grossProfit / grossLoss : grossProfit > 0 ? Infinity : 0;
763
+ const totalFeeDrag = pair.totalMarketFees + pair.totalBlockchainFees;
764
+ const feeDragPct = grossProfit > 0 ? (totalFeeDrag / grossProfit) * 100 : 0;
765
+ const avgWin = wins.length > 0 ? grossProfit / wins.length : 0;
766
+ const avgLoss = losses.length > 0 ? losses.reduce((s, r) => s + r.pnl, 0) / losses.length : 0;
767
+ const avgWinPct = wins.length > 0 ? wins.reduce((s, r) => s + r.pnlPct, 0) / wins.length : 0;
768
+ const avgLossPct = losses.length > 0 ? losses.reduce((s, r) => s + r.pnlPct, 0) / losses.length : 0;
769
+ const avgWinLossRatio = avgLoss !== 0 ? avgWin / Math.abs(avgLoss) : avgWin > 0 ? Infinity : 0;
770
+ const expectancyBts = (winRate * avgWin) + ((1 - winRate) * avgLoss);
771
+ const expectancyPct = (winRate * avgWinPct) + ((1 - winRate) * avgLossPct);
772
+ const rMultiple = Math.abs(avgLoss);
773
+ const expectancyR = rMultiple > 0
774
+ ? winRate * (avgWin / rMultiple) - (1 - winRate)
775
+ : Infinity;
776
+ const netExpectancyBts = pair.totalRealizedPnlNet / total;
777
+ // Daily-binned net PnL for mean/std ratio (dimensionful — not a Sharpe ratio)
778
+ const dayBuckets = {};
779
+ for (const r of pnls) {
780
+ const day = r.exitTime.slice(0, 10);
781
+ dayBuckets[day] = (dayBuckets[day] || 0) + r.pnlNet;
782
+ }
783
+ const dailyRets = Object.values(dayBuckets);
784
+ const nDays = dailyRets.length;
785
+ const meanDailyRet = nDays > 0 ? dailyRets.reduce((s, v) => s + v, 0) / nDays : 0;
786
+ const dailyVar = nDays > 0
787
+ ? dailyRets.reduce((s, v) => s + (v - meanDailyRet) ** 2, 0) / nDays
788
+ : 0;
789
+ const dailyStd = Math.sqrt(dailyVar);
790
+ const annFactor = Math.sqrt(365);
791
+ const dailyPnlRatio = dailyStd > 0 ? (meanDailyRet / dailyStd) * annFactor : 0;
792
+ // Downside deviation uses only negative returns; same N denominator
793
+ const downsideVar = nDays > 0
794
+ ? dailyRets.reduce((s, v) => s + (v < 0 ? v * v : 0), 0) / nDays
795
+ : 0;
796
+ const downsideStd = Math.sqrt(downsideVar);
797
+ const dailyDownsideRatio = downsideStd > 0 ? (meanDailyRet / downsideStd) * annFactor : 0;
798
+ // Fills-per-order distribution (grouped by sell order)
799
+ const fillCounts = [];
800
+ const orderMap = new Map();
801
+ for (const r of pnls) {
802
+ orderMap.set(r.exitOrderId, (orderMap.get(r.exitOrderId) || 0) + 1);
803
+ }
804
+ for (const c of orderMap.values())
805
+ fillCounts.push(c);
806
+ const sellOrdersFilled = orderMap.size;
807
+ const fillsPerOrderMean = sellOrdersFilled > 0
808
+ ? fillCounts.reduce((s, v) => s + v, 0) / sellOrdersFilled
809
+ : 0;
810
+ const fillsPerOrderMax = sellOrdersFilled > 0
811
+ ? fillCounts.reduce((a, b) => Math.max(a, b), 0)
812
+ : 0;
813
+ const sortedCounts = [...fillCounts].sort((a, b) => a - b);
814
+ const fillsPerOrderMedian = sellOrdersFilled > 0 ? percentile(sortedCounts, 50) : 0;
815
+ const oneShotOrderRatio = sellOrdersFilled > 0
816
+ ? fillCounts.filter(c => c === 1).length / sellOrdersFilled
817
+ : 0;
818
+ const fillsPerDay = nDays > 0 ? total / nDays : 0;
819
+ const avgVolumePerDay = nDays > 0 ? (pair.totalBuyQuote + pair.totalSellQuote) / nDays : 0;
820
+ // Avg hold duration
821
+ let totalHours = 0;
822
+ let hourCount = 0;
823
+ for (const r of pnls) {
824
+ const entry = new Date(r.entryTime).getTime();
825
+ const exit = new Date(r.exitTime).getTime();
826
+ if (!isNaN(entry) && !isNaN(exit) && exit > entry) {
827
+ totalHours += (exit - entry) / 3600000;
828
+ hourCount++;
829
+ }
830
+ }
831
+ const avgHoldHours = hourCount > 0 ? totalHours / hourCount : 0;
832
+ // Max consecutive wins / losses (aggregated by exit order)
833
+ const orderPnls = new Map();
834
+ for (const r of pnls) {
835
+ const existing = orderPnls.get(r.exitOrderId);
836
+ if (existing) {
837
+ existing.pnl += r.pnl;
838
+ }
839
+ else {
840
+ orderPnls.set(r.exitOrderId, { pnl: r.pnl, time: r.exitTime });
841
+ }
842
+ }
843
+ const orderResults = [...orderPnls.values()].sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime());
844
+ let consecW = 0, consecL = 0;
845
+ let maxW = 0, maxL = 0;
846
+ for (const { pnl } of orderResults) {
847
+ if (pnl > 0) {
848
+ consecW++;
849
+ consecL = 0;
850
+ maxW = Math.max(maxW, consecW);
851
+ }
852
+ else if (pnl < 0) {
853
+ consecL++;
854
+ consecW = 0;
855
+ maxL = Math.max(maxL, consecL);
856
+ }
857
+ else {
858
+ consecW = 0;
859
+ consecL = 0;
860
+ }
861
+ }
862
+ // ─── Max Drawdown + Recovery Time ───────────────────────────────────
863
+ const STABILITY_TRADES = 3;
864
+ const MIN_TRADES_FOR_PEAK = 10; // backstop for monotonic equity curves
865
+ const chronological = [...pnls].sort((a, b) => new Date(a.exitTime).getTime() - new Date(b.exitTime).getTime());
866
+ let equity = 0, peak = 0, mddPct = 0;
867
+ let maxRecoveryDays = 0;
868
+ let isOngoingRecovery = false;
869
+ let currentDrawdownDays = 0;
870
+ let drawdownStartTime = 0;
871
+ let troughTime = 0;
872
+ let currentTroughEquity = Infinity;
873
+ let hadStablePeak = false;
874
+ let tradesSincePeakSet = 0;
875
+ let hasPrePeakEquity = false;
876
+ let prePeakMinEquity = 0;
877
+ let totalTradesProcessed = 0;
878
+ for (const r of chronological) {
879
+ totalTradesProcessed++;
880
+ equity += r.pnlNet;
881
+ if (equity > peak) {
882
+ if (troughTime > 0 && hadStablePeak) {
883
+ const recoveryDays = (new Date(r.exitTime).getTime() - troughTime) / 86400000;
884
+ if (recoveryDays > maxRecoveryDays)
885
+ maxRecoveryDays = recoveryDays;
886
+ }
887
+ peak = equity;
888
+ tradesSincePeakSet = 0;
889
+ currentTroughEquity = Infinity;
890
+ troughTime = 0;
891
+ drawdownStartTime = 0;
892
+ }
893
+ else {
894
+ tradesSincePeakSet++;
895
+ if (tradesSincePeakSet >= STABILITY_TRADES) {
896
+ hadStablePeak = true;
897
+ }
898
+ }
899
+ if (!hadStablePeak && totalTradesProcessed >= MIN_TRADES_FOR_PEAK) {
900
+ hadStablePeak = true;
901
+ }
902
+ if (hadStablePeak && equity < peak && peak > 0) {
903
+ if (drawdownStartTime === 0)
904
+ drawdownStartTime = new Date(r.exitTime).getTime();
905
+ if (equity < currentTroughEquity) {
906
+ currentTroughEquity = equity;
907
+ troughTime = new Date(r.exitTime).getTime();
908
+ }
909
+ const dd = (equity - peak) / peak;
910
+ if (dd < mddPct)
911
+ mddPct = dd;
912
+ }
913
+ if (peak > 0 && !hadStablePeak) {
914
+ if (!hasPrePeakEquity || equity < prePeakMinEquity) {
915
+ prePeakMinEquity = equity;
916
+ hasPrePeakEquity = true;
917
+ }
918
+ }
919
+ }
920
+ if (drawdownStartTime > 0 && hadStablePeak) {
921
+ const lastTime = new Date(chronological[chronological.length - 1].exitTime).getTime();
922
+ currentDrawdownDays = (lastTime - drawdownStartTime) / 86400000;
923
+ isOngoingRecovery = true;
924
+ if (currentDrawdownDays > maxRecoveryDays) {
925
+ maxRecoveryDays = currentDrawdownDays;
926
+ }
927
+ }
928
+ if (hadStablePeak) {
929
+ mddPct *= 100;
930
+ }
931
+ else {
932
+ mddPct = hasPrePeakEquity ? prePeakMinEquity : 0;
933
+ }
934
+ // Payoff distribution stats
935
+ const pnlPcts = [...pnls.map(r => r.pnlPct)].sort((a, b) => a - b);
936
+ const medianPnlPct = percentile(pnlPcts, 50);
937
+ const p25PnlPct = percentile(pnlPcts, 25);
938
+ const p75PnlPct = percentile(pnlPcts, 75);
939
+ const absAvgLoss = Math.abs(avgLoss);
940
+ let rValues = [];
941
+ if (absAvgLoss > 0) {
942
+ rValues = pnls.map(r => r.pnl / absAvgLoss).sort((a, b) => a - b);
943
+ }
944
+ const medianR = rValues.length > 0 ? percentile(rValues, 50) : 0;
945
+ const pctRGreater1 = rValues.length > 0 ? rValues.filter(r => r > 1).length / rValues.length : 0;
946
+ const pctRGreater2 = rValues.length > 0 ? rValues.filter(r => r > 2).length / rValues.length : 0;
947
+ const pctRLessNeg1 = rValues.length > 0 ? rValues.filter(r => r < -1).length / rValues.length : 0;
948
+ const bestTradePct = pnlPcts.length > 0 ? pnlPcts[pnlPcts.length - 1] : 0;
949
+ const worstTradePct = pnlPcts.length > 0 ? pnlPcts[0] : 0;
950
+ return {
951
+ totalLots: total,
952
+ winRate,
953
+ profitFactor,
954
+ avgWin,
955
+ avgLoss,
956
+ avgWinLossRatio,
957
+ expectancyBts,
958
+ expectancyPct,
959
+ expectancyR,
960
+ netExpectancyBts,
961
+ dailyPnlRatio,
962
+ dailyDownsideRatio,
963
+ feeDragPct,
964
+ maxConsecWins: maxW,
965
+ maxConsecLosses: maxL,
966
+ avgHoldHours,
967
+ limitOrderRatio: total > 0 ? totalMakerLegs / (total * 2) : 0,
968
+ bestTradePct,
969
+ worstTradePct,
970
+ mddPct,
971
+ mddHadStablePeak: hadStablePeak,
972
+ isOngoingRecovery,
973
+ currentDrawdownDays,
974
+ maxRecoveryDays,
975
+ medianPnlPct,
976
+ p25PnlPct,
977
+ p75PnlPct,
978
+ medianR,
979
+ pctRGreater1,
980
+ pctRGreater2,
981
+ pctRLessNeg1,
982
+ sellOrdersFilled,
983
+ fillsPerOrderMean,
984
+ fillsPerOrderMedian,
985
+ fillsPerOrderMax,
986
+ oneShotOrderRatio,
987
+ fillsPerDay,
988
+ avgVolumePerDay,
989
+ };
990
+ }
991
+ function printMetrics(pairs) {
992
+ for (const pair of pairs) {
993
+ if (pair.realizedPnls.length === 0)
994
+ continue;
995
+ const m = computeMetrics(pair);
996
+ const pairLabel = `${fmtAsset(pair.baseAsset)}/${fmtAsset(pair.quoteAsset)}`;
997
+ console.log('');
998
+ console.log(` ── ${pairLabel} — Performance Metrics`);
999
+ console.log('');
1000
+ // Edge
1001
+ console.log(` Win Rate: ${(m.winRate * 100).toFixed(1)}%`);
1002
+ console.log(` Profit Factor: ${m.profitFactor === Infinity ? '∞' : m.profitFactor.toFixed(2)}`);
1003
+ const mktFeeStr = pair.totalMarketFees > 0.0001 ? ` (market ${fmt(pair.totalMarketFees, 4)})` : '';
1004
+ const blkFeeStr = pair.totalBlockchainFees > 0.0001 ? ` (op ${fmt(pair.totalBlockchainFees, 4)} BTS)` : '';
1005
+ console.log(` Fee Drag: ${m.feeDragPct > 0 ? m.feeDragPct.toFixed(2) + '% of gross profit' + mktFeeStr + blkFeeStr : '—'}`);
1006
+ console.log(` Avg Win / Avg Loss: ${m.avgWinLossRatio === Infinity ? '∞' : m.avgWinLossRatio.toFixed(2)}`);
1007
+ const qSymbol = fmtAsset(pair.quoteAsset);
1008
+ console.log(` Expectancy (gross): ${fmt(m.expectancyBts, 4)} ${qSymbol} (${fmtPct(m.expectancyPct)}) per trade`);
1009
+ const rDisplay = m.expectancyR === Infinity ? '∞' : m.expectancyR.toFixed(3) + 'R';
1010
+ console.log(` Expectancy (R): ${rDisplay}`);
1011
+ console.log(` Expectancy (net): ${fmt(m.netExpectancyBts, 4)} ${qSymbol} per trade`);
1012
+ console.log('');
1013
+ // Edge quality
1014
+ console.log(` Median R: ${m.medianR.toFixed(2)}`);
1015
+ console.log(` Trades > 1R / > 2R: ${(m.pctRGreater1 * 100).toFixed(1)}% / ${(m.pctRGreater2 * 100).toFixed(1)}%`);
1016
+ console.log(` Trades < -1R: ${(m.pctRLessNeg1 * 100).toFixed(1)}%`);
1017
+ console.log('');
1018
+ // PnL distribution
1019
+ console.log(` Median PnL: ${fmtPct(m.medianPnlPct)}`);
1020
+ console.log(` P25 / P75: ${fmtPct(m.p25PnlPct)} / ${fmtPct(m.p75PnlPct)}`);
1021
+ console.log(` Best / Worst Trade: ${fmtPct(m.bestTradePct)} / ${fmtPct(m.worstTradePct)}`);
1022
+ console.log('');
1023
+ // Risk-adjusted (dimensionful — based on absolute daily PnL, not % returns)
1024
+ console.log(` Sharpe (ann): ${m.dailyPnlRatio.toFixed(2)}`);
1025
+ console.log(` Sortino (ann): ${m.dailyDownsideRatio.toFixed(2)}`);
1026
+ console.log('');
1027
+ // Tail risk
1028
+ if (m.mddHadStablePeak) {
1029
+ console.log(` Max Drawdown: ${fmtPct(m.mddPct)}`);
1030
+ }
1031
+ else {
1032
+ console.log(` Min Equity: ${fmt(m.mddPct, 4)} ${fmtAsset(pair.quoteAsset)}`);
1033
+ }
1034
+ const recLabel = m.maxRecoveryDays > 0 ? m.maxRecoveryDays.toFixed(1) + ' days' + (m.isOngoingRecovery ? ' (ongoing)' : '') : '—';
1035
+ console.log(` Max Recovery Time: ${recLabel}`);
1036
+ if (m.currentDrawdownDays > 0) {
1037
+ console.log(` Current Drawdown: ${m.currentDrawdownDays.toFixed(1)} days (active)`);
1038
+ }
1039
+ console.log('');
1040
+ // Behavioral
1041
+ console.log(` Max Consecutive W/L: ${m.maxConsecWins} / ${m.maxConsecLosses}`);
1042
+ const holdDisplay = m.avgHoldHours > 0 ? m.avgHoldHours.toFixed(1) : '—';
1043
+ console.log(` Avg hold time: ${holdDisplay} hours`);
1044
+ console.log(` Maker / Taker: ${(m.limitOrderRatio * 100).toFixed(1)}% / ${((1 - m.limitOrderRatio) * 100).toFixed(1)}%`);
1045
+ console.log('');
1046
+ // Activity
1047
+ console.log(` Sell orders filled: ${m.sellOrdersFilled}`);
1048
+ console.log(` Partial fills/order: ${m.fillsPerOrderMean.toFixed(2)} mean, ${m.fillsPerOrderMedian.toFixed(1)} med, ${m.fillsPerOrderMax} max`);
1049
+ console.log(` One-shot orders: ${(m.oneShotOrderRatio * 100).toFixed(0)}%`);
1050
+ console.log(` Fills/day: ${m.fillsPerDay.toFixed(2)}`);
1051
+ console.log(` Avg vol/day: ${fmt(m.avgVolumePerDay, 2)} ${fmtAsset(pair.quoteAsset)}`);
1052
+ console.log('');
1053
+ }
1054
+ // Grand totals grouped by quote asset
1055
+ const quoteGroups = new Map();
1056
+ for (const p of pairs) {
1057
+ const q = p.quoteAsset;
1058
+ if (!quoteGroups.has(q))
1059
+ quoteGroups.set(q, []);
1060
+ quoteGroups.get(q).push(p);
1061
+ }
1062
+ if (pairs.length > 1 && quoteGroups.size > 0) {
1063
+ for (const [quoteAsset, group] of quoteGroups) {
1064
+ const totalLots = group.reduce((s, p) => s + p.realizedPnls.length, 0);
1065
+ const totalNet = group.reduce((s, p) => s + p.totalRealizedPnlNet, 0);
1066
+ const totalMktFees = group.reduce((s, p) => s + p.totalMarketFees, 0);
1067
+ const totalBlkFees = group.reduce((s, p) => s + p.totalBlockchainFees, 0);
1068
+ const qSymbol = fmtAsset(quoteAsset);
1069
+ console.log(` ── TOTAL (${qSymbol}) — ${group.length} pair(s)`);
1070
+ console.log(` Lots: ${totalLots}`);
1071
+ const feeParts = [];
1072
+ if (totalMktFees > 0.0001)
1073
+ feeParts.push(`market ${fmt(totalMktFees, 4)} ${qSymbol}`);
1074
+ if (totalBlkFees > 0.0001)
1075
+ feeParts.push(`op ${fmt(totalBlkFees, 4)} BTS`);
1076
+ const feeSuffix = feeParts.length > 0 ? ` (${feeParts.join(', ')})` : '';
1077
+ console.log(` Net PnL: ${fmtAsset(quoteAsset)} ${fmt(totalNet, 4)}${feeSuffix}`);
1078
+ console.log('');
1079
+ }
1080
+ }
1081
+ }
1082
+ // ─── CSV Export ──────────────────────────────────────────────────────────────
1083
+ function exportCsv(pairs, filePath) {
1084
+ const esc = (v) => { const s = String(v); return /[,"\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; };
1085
+ const lines = ['time,orderId,direction,baseAsset,quoteAsset,baseAmount,quoteAmount,price,isMaker,marketFeeReal,marketFeeAsset'];
1086
+ for (const pair of pairs) {
1087
+ for (const t of [...pair.buys, ...pair.sells].sort((a, b) => a.sequence - b.sequence)) {
1088
+ lines.push([
1089
+ esc(t.time),
1090
+ esc(t.orderId),
1091
+ esc(t.direction),
1092
+ esc(t.baseAsset),
1093
+ esc(t.quoteAsset),
1094
+ t.baseAmount,
1095
+ t.quoteAmount,
1096
+ t.price,
1097
+ t.isMaker ? '1' : '0',
1098
+ t.marketFeeReal,
1099
+ esc(t.marketFeeAsset),
1100
+ ].join(','));
1101
+ }
1102
+ }
1103
+ fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
1104
+ console.log(` Trades exported to ${filePath}`);
1105
+ }
1106
+ // ─── JSON Export ─────────────────────────────────────────────────────────────
1107
+ function exportJson(accountId, start, end, pairs, filePath) {
1108
+ const data = {
1109
+ accountId,
1110
+ period: { start, end },
1111
+ pairs: pairs.map(p => ({
1112
+ baseAsset: p.baseAsset,
1113
+ quoteAsset: p.quoteAsset,
1114
+ summary: {
1115
+ totalBuyBase: p.totalBuyBase,
1116
+ totalSellBase: p.totalSellBase,
1117
+ totalBuyQuote: p.totalBuyQuote,
1118
+ totalSellQuote: p.totalSellQuote,
1119
+ netPosition: p.netPosition,
1120
+ totalRealizedPnl: p.totalRealizedPnl,
1121
+ totalMarketFees: p.totalMarketFees,
1122
+ totalBlockchainFees: p.totalBlockchainFees,
1123
+ totalRealizedPnlNet: p.totalRealizedPnlNet,
1124
+ },
1125
+ realizedPnls: p.realizedPnls.map(r => ({
1126
+ ...r,
1127
+ marketFeeEntry: r.marketFeeEntry,
1128
+ marketFeeExit: r.marketFeeExit,
1129
+ feeBts: r.feeBts,
1130
+ pnlNet: r.pnlNet,
1131
+ pnlNetPct: r.pnlNetPct,
1132
+ })),
1133
+ totalBuys: p.buys.length,
1134
+ totalSells: p.sells.length,
1135
+ })),
1136
+ };
1137
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
1138
+ console.log(` Full analysis exported to ${filePath}`);
1139
+ }
1140
+ // ─── Main ────────────────────────────────────────────────────────────────────
1141
+ async function run() {
1142
+ const opts = parseArgs();
1143
+ let accountId = opts.accountId;
1144
+ if (opts.lookup) {
1145
+ const resolved = await resolveAccountId(accountId, opts.node);
1146
+ if (!resolved) {
1147
+ console.error(` Could not resolve "${accountId}" to an account ID`);
1148
+ process.exit(1);
1149
+ }
1150
+ accountId = resolved;
1151
+ }
1152
+ // Build time range
1153
+ const now = new Date();
1154
+ let gte, lte;
1155
+ if (opts.start && opts.end) {
1156
+ gte = opts.start;
1157
+ lte = opts.end;
1158
+ }
1159
+ else if (opts.start) {
1160
+ gte = opts.start;
1161
+ lte = now.toISOString();
1162
+ }
1163
+ else {
1164
+ const hours = opts.hours || 168;
1165
+ const start = new Date(now.getTime() - hours * 3600 * 1000);
1166
+ gte = start.toISOString();
1167
+ lte = now.toISOString();
1168
+ }
1169
+ // Ensure times end with Z for ES
1170
+ if (!gte.endsWith('Z'))
1171
+ gte += gte.includes('T') ? 'Z' : 'T00:00:00Z';
1172
+ if (!lte.endsWith('Z'))
1173
+ lte += lte.includes('T') ? 'Z' : 'T00:00:00Z';
1174
+ const KIBANA_CFG = { timeout: 60000 };
1175
+ const fills = await fetchAllFills(KIBANA_CFG, accountId, gte, lte);
1176
+ if (fills.length === 0) {
1177
+ console.log(' No fills found in the specified time range.');
1178
+ process.exit(0);
1179
+ }
1180
+ // Resolve unknown asset precisions from blockchain
1181
+ await resolveAssetPrecisions(fills, opts.node);
1182
+ // Classify fills
1183
+ const { trades, pairs } = classifyFills(fills, opts.asset);
1184
+ if (trades.length === 0) {
1185
+ console.log(' No trades could be classified (check asset filter or time range).');
1186
+ process.exit(0);
1187
+ }
1188
+ console.log(`Account: ${accountId}`);
1189
+ console.log(`Period: ${gte.slice(0, 10)} → ${lte.slice(0, 10)}`);
1190
+ console.log(`Pairs: ${pairs.size}, ${trades.length} classified trades`);
1191
+ const hasCrossPair = [...pairs].some(p => p.split(':')[1] !== BTS_ID);
1192
+ if (hasCrossPair) {
1193
+ console.log(` Note: cross-pair trades (non-BTS quote) are included but PnL is in the pair's quote asset.`);
1194
+ }
1195
+ console.log('');
1196
+ // Override blockchain fee from CLI if provided
1197
+ if (opts.feePerOrder)
1198
+ BLOCKCHAIN_FEE_PER_FILL = opts.feePerOrder;
1199
+ // Analyze each pair
1200
+ const pairMap = new Map();
1201
+ for (const t of trades) {
1202
+ const pairKey = `${t.baseAsset}:${t.quoteAsset}`;
1203
+ if (!pairMap.has(pairKey))
1204
+ pairMap.set(pairKey, []);
1205
+ pairMap.get(pairKey).push(t);
1206
+ }
1207
+ const analyses = [];
1208
+ for (const [key, pairTrades] of pairMap) {
1209
+ const analysis = analyzePair(pairTrades, opts.matchMode);
1210
+ analyses.push(analysis);
1211
+ if (opts.verbose) {
1212
+ const [base, quote] = key.split(':');
1213
+ console.log(` ${fmtAsset(base)}/${fmtAsset(quote)}: ${pairTrades.filter(t => t.direction === 'buy').length} buys, ${pairTrades.filter(t => t.direction === 'sell').length} sells, ${analysis.realizedPnls.length} matched lots`);
1214
+ }
1215
+ }
1216
+ // Sort pairs by volume (total quote)
1217
+ analyses.sort((a, b) => (b.totalBuyQuote + b.totalSellQuote) - (a.totalBuyQuote + a.totalSellQuote));
1218
+ // Output: summaries → grand total → per-match detail
1219
+ printSummary(analyses, accountId, gte, lte, opts.matchMode);
1220
+ if (opts.showPnlDetail) {
1221
+ printPnlDetail(analyses);
1222
+ }
1223
+ printMetrics(analyses);
1224
+ if (opts.csv) {
1225
+ exportCsv(analyses, opts.csv);
1226
+ }
1227
+ if (opts.json) {
1228
+ exportJson(accountId, gte, lte, analyses, opts.json);
1229
+ }
1230
+ }
1231
+ export { analyzePair, classifyFills, computeMetrics };
1232
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
1233
+ run().then(() => process.exit(0)).catch(e => {
1234
+ console.error('\n[fatal]', e.message);
1235
+ if (process.env.DEBUG)
1236
+ console.error(e.stack);
1237
+ process.exit(1);
1238
+ });
1239
+ }
1240
+ //# sourceMappingURL=trade_profitability.js.map