dexbot 1.4.24 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +7 -2
  3. package/analysis/README.md +61 -5
  4. package/analysis/ama_fitting/package.json +1 -1
  5. package/analysis/analyze_derivatives.ts +1 -1
  6. package/analysis/bot_key_utils.ts +272 -3
  7. package/analysis/bot_usage/discover_bot_accounts.ts +64 -9
  8. package/analysis/bot_usage/kibana_bot_queries.ts +40 -1
  9. package/analysis/chart_utils.ts +6 -3
  10. package/analysis/grid_correction_check.ts +932 -0
  11. package/analysis/resolve_bot_accounts.ts +199 -0
  12. package/analysis/trade_profitability.ts +36 -8
  13. package/analysis/tradingview/README.md +25 -0
  14. package/analysis/tradingview/analyze_tradingview.ts +2 -2
  15. package/analysis/trend_detection/package.json +1 -1
  16. package/claw/modules/claw_launcher.ts +5 -4
  17. package/claw/modules/credit_runtime_adapter.ts +2 -1
  18. package/claw/modules/dexbot_profiles.ts +4 -3
  19. package/claw/package.json +1 -1
  20. package/claw/runtimes/openclaw-plugin/openclaw.plugin.json +1 -1
  21. package/claw/runtimes/openclaw-plugin/package.json +1 -1
  22. package/claw/tests/test_claw_mcp_transport.ts +2 -2
  23. package/dist/analysis/analyze_derivatives.js +1 -1
  24. package/dist/analysis/bot_key_utils.d.ts +28 -1
  25. package/dist/analysis/bot_key_utils.d.ts.map +1 -1
  26. package/dist/analysis/bot_key_utils.js +385 -3
  27. package/dist/analysis/bot_key_utils.js.map +1 -1
  28. package/dist/analysis/bot_usage/discover_bot_accounts.js +60 -10
  29. package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
  30. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts +45 -1
  31. package/dist/analysis/bot_usage/kibana_bot_queries.d.ts.map +1 -1
  32. package/dist/analysis/bot_usage/kibana_bot_queries.js +39 -1
  33. package/dist/analysis/bot_usage/kibana_bot_queries.js.map +1 -1
  34. package/dist/analysis/chart_utils.d.ts +2 -1
  35. package/dist/analysis/chart_utils.d.ts.map +1 -1
  36. package/dist/analysis/chart_utils.js +4 -1
  37. package/dist/analysis/chart_utils.js.map +1 -1
  38. package/dist/analysis/grid_correction_check.d.ts +90 -0
  39. package/dist/analysis/grid_correction_check.d.ts.map +1 -0
  40. package/dist/analysis/grid_correction_check.js +901 -0
  41. package/dist/analysis/grid_correction_check.js.map +1 -0
  42. package/dist/analysis/resolve_bot_accounts.d.ts +3 -0
  43. package/dist/analysis/resolve_bot_accounts.d.ts.map +1 -0
  44. package/dist/analysis/resolve_bot_accounts.js +216 -0
  45. package/dist/analysis/resolve_bot_accounts.js.map +1 -0
  46. package/dist/analysis/trade_profitability.d.ts.map +1 -1
  47. package/dist/analysis/trade_profitability.js +40 -8
  48. package/dist/analysis/trade_profitability.js.map +1 -1
  49. package/dist/analysis/tradingview/analyze_tradingview.js +2 -2
  50. package/dist/analysis/tradingview/analyze_tradingview.js.map +1 -1
  51. package/dist/bot.d.ts.map +1 -1
  52. package/dist/bot.js +2 -1
  53. package/dist/bot.js.map +1 -1
  54. package/dist/dexbot.d.ts.map +1 -1
  55. package/dist/dexbot.js +134 -22
  56. package/dist/dexbot.js.map +1 -1
  57. package/dist/market_adapter/ama_signal_runner.js +2 -2
  58. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js +2 -1
  59. package/dist/market_adapter/inputs/fetch_cex_synthetic_data.js.map +1 -1
  60. package/dist/market_adapter/inputs/fetch_lp_data.d.ts +4 -1
  61. package/dist/market_adapter/inputs/fetch_lp_data.d.ts.map +1 -1
  62. package/dist/market_adapter/inputs/fetch_lp_data.js +3 -2
  63. package/dist/market_adapter/inputs/fetch_lp_data.js.map +1 -1
  64. package/dist/modules/account_bots.d.ts +42 -1
  65. package/dist/modules/account_bots.d.ts.map +1 -1
  66. package/dist/modules/account_bots.js +221 -22
  67. package/dist/modules/account_bots.js.map +1 -1
  68. package/dist/modules/account_orders.d.ts +7 -10
  69. package/dist/modules/account_orders.d.ts.map +1 -1
  70. package/dist/modules/account_orders.js +26 -2
  71. package/dist/modules/account_orders.js.map +1 -1
  72. package/dist/modules/bitshares_client.d.ts +4 -1
  73. package/dist/modules/bitshares_client.d.ts.map +1 -1
  74. package/dist/modules/bitshares_client.js +12 -4
  75. package/dist/modules/bitshares_client.js.map +1 -1
  76. package/dist/modules/bot_settings.d.ts.map +1 -1
  77. package/dist/modules/bot_settings.js +2 -1
  78. package/dist/modules/bot_settings.js.map +1 -1
  79. package/dist/modules/chain_orders.d.ts.map +1 -1
  80. package/dist/modules/chain_orders.js +21 -2
  81. package/dist/modules/chain_orders.js.map +1 -1
  82. package/dist/modules/cli_colors.d.ts +39 -0
  83. package/dist/modules/cli_colors.d.ts.map +1 -0
  84. package/dist/modules/cli_colors.js +42 -0
  85. package/dist/modules/cli_colors.js.map +1 -0
  86. package/dist/modules/config.d.ts.map +1 -1
  87. package/dist/modules/config.js +27 -0
  88. package/dist/modules/config.js.map +1 -1
  89. package/dist/modules/constants.d.ts +14 -1
  90. package/dist/modules/constants.d.ts.map +1 -1
  91. package/dist/modules/constants.js +66 -11
  92. package/dist/modules/constants.js.map +1 -1
  93. package/dist/modules/credit_pricing.d.ts +61 -0
  94. package/dist/modules/credit_pricing.d.ts.map +1 -0
  95. package/dist/modules/credit_pricing.js +237 -0
  96. package/dist/modules/credit_pricing.js.map +1 -0
  97. package/dist/modules/credit_runtime.d.ts +1 -0
  98. package/dist/modules/credit_runtime.d.ts.map +1 -1
  99. package/dist/modules/credit_runtime.js +49 -107
  100. package/dist/modules/credit_runtime.js.map +1 -1
  101. package/dist/modules/dexbot_class.d.ts +25 -0
  102. package/dist/modules/dexbot_class.d.ts.map +1 -1
  103. package/dist/modules/dexbot_class.js +82 -12
  104. package/dist/modules/dexbot_class.js.map +1 -1
  105. package/dist/modules/dexbot_cow_runtime.d.ts +39 -17
  106. package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
  107. package/dist/modules/dexbot_cow_runtime.js +875 -134
  108. package/dist/modules/dexbot_cow_runtime.js.map +1 -1
  109. package/dist/modules/dexbot_fill_runtime.d.ts +11 -1
  110. package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
  111. package/dist/modules/dexbot_fill_runtime.js +108 -1
  112. package/dist/modules/dexbot_fill_runtime.js.map +1 -1
  113. package/dist/modules/dexbot_maintenance_runtime.d.ts +29 -14
  114. package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
  115. package/dist/modules/dexbot_maintenance_runtime.js +279 -63
  116. package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
  117. package/dist/modules/dexbot_startup_runtime.d.ts +1 -0
  118. package/dist/modules/dexbot_startup_runtime.d.ts.map +1 -1
  119. package/dist/modules/dexbot_startup_runtime.js +58 -7
  120. package/dist/modules/dexbot_startup_runtime.js.map +1 -1
  121. package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
  122. package/dist/modules/dexbot_state_recovery.js +55 -3
  123. package/dist/modules/dexbot_state_recovery.js.map +1 -1
  124. package/dist/modules/fund_registry.d.ts +1 -1
  125. package/dist/modules/fund_registry.js +1 -1
  126. package/dist/modules/graceful_shutdown.d.ts +12 -1
  127. package/dist/modules/graceful_shutdown.d.ts.map +1 -1
  128. package/dist/modules/graceful_shutdown.js +39 -1
  129. package/dist/modules/graceful_shutdown.js.map +1 -1
  130. package/dist/modules/key_store.d.ts.map +1 -1
  131. package/dist/modules/key_store.js +38 -1
  132. package/dist/modules/key_store.js.map +1 -1
  133. package/dist/modules/launcher/adapter_requirement.d.ts +38 -0
  134. package/dist/modules/launcher/adapter_requirement.d.ts.map +1 -0
  135. package/dist/modules/launcher/adapter_requirement.js +141 -0
  136. package/dist/modules/launcher/adapter_requirement.js.map +1 -0
  137. package/dist/modules/launcher/bot_supervisor.d.ts +2 -2
  138. package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
  139. package/dist/modules/launcher/bot_supervisor.js +32 -3
  140. package/dist/modules/launcher/bot_supervisor.js.map +1 -1
  141. package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
  142. package/dist/modules/launcher/launch_modes.js +5 -4
  143. package/dist/modules/launcher/launch_modes.js.map +1 -1
  144. package/dist/modules/launcher/monolithic_runtime.d.ts +1 -1
  145. package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
  146. package/dist/modules/launcher/monolithic_runtime.js +12 -8
  147. package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
  148. package/dist/modules/launcher/status_reporting.d.ts +6 -6
  149. package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
  150. package/dist/modules/launcher/status_reporting.js +9 -6
  151. package/dist/modules/launcher/status_reporting.js.map +1 -1
  152. package/dist/modules/order/accounting.d.ts.map +1 -1
  153. package/dist/modules/order/accounting.js +20 -3
  154. package/dist/modules/order/accounting.js.map +1 -1
  155. package/dist/modules/order/export.d.ts +1 -1
  156. package/dist/modules/order/export.js +1 -1
  157. package/dist/modules/order/grid.d.ts +1 -1
  158. package/dist/modules/order/grid.d.ts.map +1 -1
  159. package/dist/modules/order/grid.js +335 -43
  160. package/dist/modules/order/grid.js.map +1 -1
  161. package/dist/modules/order/grid_reconcile.d.ts +4 -2
  162. package/dist/modules/order/grid_reconcile.d.ts.map +1 -1
  163. package/dist/modules/order/grid_reconcile.js +242 -222
  164. package/dist/modules/order/grid_reconcile.js.map +1 -1
  165. package/dist/modules/order/grid_reconcile_internal.d.ts +11 -1
  166. package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
  167. package/dist/modules/order/grid_reconcile_internal.js +132 -6
  168. package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
  169. package/dist/modules/order/logger.d.ts +23 -1
  170. package/dist/modules/order/logger.d.ts.map +1 -1
  171. package/dist/modules/order/logger.js +60 -17
  172. package/dist/modules/order/logger.js.map +1 -1
  173. package/dist/modules/order/manager.d.ts +68 -14
  174. package/dist/modules/order/manager.d.ts.map +1 -1
  175. package/dist/modules/order/manager.js +281 -52
  176. package/dist/modules/order/manager.js.map +1 -1
  177. package/dist/modules/order/strategy.d.ts.map +1 -1
  178. package/dist/modules/order/strategy.js +25 -8
  179. package/dist/modules/order/strategy.js.map +1 -1
  180. package/dist/modules/order/sync_engine.d.ts.map +1 -1
  181. package/dist/modules/order/sync_engine.js +262 -167
  182. package/dist/modules/order/sync_engine.js.map +1 -1
  183. package/dist/modules/order/utils/math.d.ts +47 -5
  184. package/dist/modules/order/utils/math.d.ts.map +1 -1
  185. package/dist/modules/order/utils/math.js +175 -11
  186. package/dist/modules/order/utils/math.js.map +1 -1
  187. package/dist/modules/order/utils/order.d.ts +16 -15
  188. package/dist/modules/order/utils/order.d.ts.map +1 -1
  189. package/dist/modules/order/utils/order.js +199 -32
  190. package/dist/modules/order/utils/order.js.map +1 -1
  191. package/dist/modules/order/utils/slot.d.ts +8 -0
  192. package/dist/modules/order/utils/slot.d.ts.map +1 -0
  193. package/dist/modules/order/utils/slot.js +16 -0
  194. package/dist/modules/order/utils/slot.js.map +1 -0
  195. package/dist/modules/order/utils/system.d.ts +52 -4
  196. package/dist/modules/order/utils/system.d.ts.map +1 -1
  197. package/dist/modules/order/utils/system.js +109 -16
  198. package/dist/modules/order/utils/system.js.map +1 -1
  199. package/dist/modules/order/utils/validate.d.ts +6 -9
  200. package/dist/modules/order/utils/validate.d.ts.map +1 -1
  201. package/dist/modules/order/utils/validate.js +107 -85
  202. package/dist/modules/order/utils/validate.js.map +1 -1
  203. package/dist/modules/order/working_grid.d.ts +5 -0
  204. package/dist/modules/order/working_grid.d.ts.map +1 -1
  205. package/dist/modules/order/working_grid.js +21 -0
  206. package/dist/modules/order/working_grid.js.map +1 -1
  207. package/dist/modules/paths.d.ts +4 -2
  208. package/dist/modules/paths.d.ts.map +1 -1
  209. package/dist/modules/paths.js +79 -24
  210. package/dist/modules/paths.js.map +1 -1
  211. package/dist/modules/utils/chain_logs.d.ts +11 -0
  212. package/dist/modules/utils/chain_logs.d.ts.map +1 -0
  213. package/dist/modules/utils/chain_logs.js +23 -0
  214. package/dist/modules/utils/chain_logs.js.map +1 -0
  215. package/dist/modules/utils/sanitize_key.d.ts +7 -0
  216. package/dist/modules/utils/sanitize_key.d.ts.map +1 -1
  217. package/dist/modules/utils/sanitize_key.js +15 -0
  218. package/dist/modules/utils/sanitize_key.js.map +1 -1
  219. package/dist/pm2.d.ts +11 -1
  220. package/dist/pm2.d.ts.map +1 -1
  221. package/dist/pm2.js +66 -15
  222. package/dist/pm2.js.map +1 -1
  223. package/dist/scripts/analyze-credit.d.ts +4 -0
  224. package/dist/scripts/analyze-credit.d.ts.map +1 -0
  225. package/dist/scripts/analyze-credit.js +760 -0
  226. package/dist/scripts/analyze-credit.js.map +1 -0
  227. package/dist/scripts/analyze-orders.d.ts +1 -10
  228. package/dist/scripts/analyze-orders.d.ts.map +1 -1
  229. package/dist/scripts/analyze-orders.js +34 -36
  230. package/dist/scripts/analyze-orders.js.map +1 -1
  231. package/dist/scripts/generate_market_adapter_whitelist.d.ts +2 -5
  232. package/dist/scripts/generate_market_adapter_whitelist.d.ts.map +1 -1
  233. package/dist/scripts/generate_market_adapter_whitelist.js +83 -1
  234. package/dist/scripts/generate_market_adapter_whitelist.js.map +1 -1
  235. package/dist/scripts/test-credit-renewal.d.ts.map +1 -1
  236. package/dist/scripts/test-credit-renewal.js +37 -5
  237. package/dist/scripts/test-credit-renewal.js.map +1 -1
  238. package/dist/scripts/tv.d.ts +11 -0
  239. package/dist/scripts/tv.d.ts.map +1 -0
  240. package/dist/scripts/tv.js +357 -0
  241. package/dist/scripts/tv.js.map +1 -0
  242. package/dist/scripts/update.js +77 -10
  243. package/dist/scripts/update.js.map +1 -1
  244. package/dist/unlock.d.ts.map +1 -1
  245. package/dist/unlock.js +20 -8
  246. package/dist/unlock.js.map +1 -1
  247. package/docs/DEXBOT_COMPARISON.md +11 -11
  248. package/docs/EVOLUTION.md +10 -8
  249. package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +1 -1
  250. package/docs/GRID_RECALCULATION.md +6 -0
  251. package/docs/MPA_CREDIT_USAGE.md +3 -1
  252. package/docs/README.md +2 -2
  253. package/docs/WORKFLOW.md +3 -1
  254. package/market_adapter/README.md +10 -0
  255. package/package.json +3 -1
  256. package/scripts/README.md +16 -0
@@ -0,0 +1,932 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * GRID CORRECTION CHECK
6
+ *
7
+ * Validates LAST-FILL-GUARD discipline (ceb53819): pivot ± halfIncrement
8
+ * Last fill @x with increment i (half=i/2) gates BOTH sides regardless of
9
+ * last side — BUY must be < x*(1-half/100), SELL > x*(1+half/100).
10
+ * e.g. x=1000, i=0.5% => BUY < 997.5 / SELL > 1002.5.
11
+ * Cold start (no previous fill) is disabled.
12
+ * Intentional offline gaps (documented, acceptable):
13
+ * - Runtime pivots on _lastFilledPrice/_lastFilledType at decision time; tool
14
+ * checks consecutive fill pairs. For batch-placed orders (multiple orders
15
+ * guarded against the same pivot in one COW batch) this diverges — inherent
16
+ * offline approximation.
17
+ * - Spread-correction bypass (cowResult.origin === 'spread-correction') is
18
+ * not simulated — every consecutive pair is checked.
19
+ *
20
+ * Mirrors modules/dexbot_cow_runtime.ts:isLastFillGuardBlocked 1:1 and
21
+ * modules/constants.ts:DEFAULT_CONFIG.incrementPercent fallback (0.5).
22
+ *
23
+ * Fetches fill_order operations from Kibana (same pipeline as
24
+ * trade_profitability.ts) and checks for price-order violations.
25
+ *
26
+ * Usage:
27
+ * node dist/analysis/grid_correction_check.js --bot-key <bot-key> --hours 168
28
+ * node dist/analysis/grid_correction_check.js --bot-key <bot-key> --start 2025-01-01 --end 2025-06-01
29
+ * node dist/analysis/grid_correction_check.js --bot-key <bot-key> --hours 720 --account 1.2.123
30
+ * node dist/analysis/grid_correction_check.js --bot-key <bot-key> --hours 720 --json results.json
31
+ * node dist/analysis/grid_correction_check.js --bot-key <bot-key> --hours 720 --csv violations.csv
32
+ * node dist/analysis/grid_correction_check.js --bot-key <bot-key> --hours 720 --increment 0.5
33
+ * node dist/analysis/grid_correction_check.js --list-bots
34
+ */
35
+
36
+ import fs from 'node:fs';
37
+ import { pathToFileURL } from 'node:url';
38
+ import * as KC from '../market_adapter/core/kibana_client.js';
39
+ import * as C from '../modules/constants.js';
40
+ import { loadBotMeta, loadBotSettings, computeBotKey, persistBotAccountId } from './bot_key_utils.js';
41
+
42
+ const { kibanaSearch, DEFAULT_CONFIG: BASE_CONFIG } = KC;
43
+
44
+ const OP_FILL_ORDER = 4;
45
+ const BTS_ID = '1.3.0';
46
+
47
+ interface AssetInfo { symbol: string; precision: number; }
48
+ const ASSETS: Record<string, AssetInfo> = {
49
+ '1.3.0': { symbol: 'BTS', precision: 5 },
50
+ '1.3.118': { symbol: 'GBP', precision: 4 },
51
+ '1.3.119': { symbol: 'JPY', precision: 2 },
52
+ '1.3.120': { symbol: 'EUR', precision: 4 },
53
+ '1.3.1325': { symbol: 'RUBLE', precision: 5 },
54
+ '1.3.2512': { symbol: 'EVRAZ', precision: 4 },
55
+ '1.3.3291': { symbol: 'TWENTIX', precision: 5 },
56
+ '1.3.4099': { symbol: 'XBTSX.STH', precision: 6 },
57
+ '1.3.4156': { symbol: 'XBTSX.DOGE', precision: 5 },
58
+ '1.3.4157': { symbol: 'XBTSX.BTC', precision: 8 },
59
+ '1.3.4159': { symbol: 'XBTSX.LTC', precision: 8 },
60
+ '1.3.4176': { symbol: 'XBTSX.DASH', precision: 8 },
61
+ '1.3.4274': { symbol: 'XBTSX.BCH', precision: 8 },
62
+ '1.3.4760': { symbol: 'XBTSX.ETH', precision: 7 },
63
+ '1.3.5537': { symbol: 'IOB.XRP', precision: 4 },
64
+ '1.3.5541': { symbol: 'XBTSX.BNB', precision: 7 },
65
+ '1.3.5589': { symbol: 'XBTSX.USDT', precision: 6 },
66
+ '1.3.5641': { symbol: 'HONEST.CNY', precision: 4 },
67
+ '1.3.5649': { symbol: 'HONEST.USD', precision: 4 },
68
+ '1.3.5650': { symbol: 'HONEST.BTC', precision: 8 },
69
+ '1.3.5659': { symbol: 'HONEST.ETH', precision: 6 },
70
+ '1.3.5870': { symbol: 'XBTSX.FIL', precision: 6 },
71
+ '1.3.5887': { symbol: 'XBTSX.RUB', precision: 4 },
72
+ '1.3.5902': { symbol: 'XBTSX.USDC', precision: 6 },
73
+ '1.3.6013': { symbol: 'XBTSX.HIVE', precision: 6 },
74
+ '1.3.6124': { symbol: 'XBTSX.AVAX', precision: 6 },
75
+ '1.3.6139': { symbol: 'XBTSX.XAUT', precision: 6 },
76
+ '1.3.6166': { symbol: 'XBTSX.MATIC', precision: 5 },
77
+ '1.3.6241': { symbol: 'XBTSX.ETC', precision: 7 },
78
+ '1.3.6268': { symbol: 'BTWTY.EOS', precision: 4 },
79
+ '1.3.6301': { symbol: 'HONEST.MONEY', precision: 8 },
80
+ '1.3.6304': { symbol: 'HONEST.ADA', precision: 8 },
81
+ '1.3.6305': { symbol: 'HONEST.DOT', precision: 8 },
82
+ '1.3.6309': { symbol: 'HONEST.ATOM', precision: 8 },
83
+ '1.3.6311': { symbol: 'HONEST.ALGO', precision: 8 },
84
+ '1.3.6312': { symbol: 'HONEST.FIL', precision: 8 },
85
+ '1.3.6313': { symbol: 'HONEST.EOS', precision: 8 },
86
+ '1.3.6315': { symbol: 'HONEST.EUR', precision: 4 },
87
+ '1.3.6316': { symbol: 'HONEST.GBP', precision: 4 },
88
+ '1.3.6317': { symbol: 'HONEST.JPY', precision: 4 },
89
+ '1.3.6444': { symbol: 'IOB.XLM', precision: 4 },
90
+ '1.3.6573': { symbol: 'XBTSX.DAI', precision: 6 },
91
+ '1.3.6620': { symbol: 'XBTSX.A', precision: 6 },
92
+ '1.3.6627': { symbol: 'XBTSX.LINK', precision: 6 },
93
+ };
94
+ const resolvedPrecisions: Record<string, number> = {};
95
+ function assetSymbol(id: string): string { return ASSETS[id]?.symbol ?? id; }
96
+ function assetPrec(id: string): number | undefined { return ASSETS[id]?.precision ?? resolvedPrecisions[id]; }
97
+ function toReal(amount: number, assetId: string): number {
98
+ const p = assetPrec(assetId);
99
+ if (p === undefined) return NaN;
100
+ return amount / Math.pow(10, p);
101
+ }
102
+
103
+ // ─── Types ────────────────────────────────────────────────────────────────────
104
+ interface FillRecord {
105
+ time: string;
106
+ blockNum: number;
107
+ opNum: number;
108
+ orderId: string;
109
+ accountId: string;
110
+ pays: { amount: number; asset_id: string };
111
+ receives: { amount: number; asset_id: string };
112
+ fee: { amount: number; asset_id: string };
113
+ isMaker: boolean;
114
+ sort: any[];
115
+ }
116
+ interface TradeFill {
117
+ time: string;
118
+ orderId: string;
119
+ direction: 'buy' | 'sell';
120
+ baseAsset: string;
121
+ quoteAsset: string;
122
+ baseAmount: number;
123
+ quoteAmount: number;
124
+ price: number;
125
+ isMaker: boolean;
126
+ sequence: number;
127
+ }
128
+ interface Violation {
129
+ index: number;
130
+ pair: string;
131
+ direction: 'buy' | 'sell';
132
+ expected: string;
133
+ prev: TradeFill;
134
+ curr: TradeFill;
135
+ priceDelta: number;
136
+ priceDeltaPct: number;
137
+ pivot: number;
138
+ halfInc: number;
139
+ threshold: number;
140
+ }
141
+ interface AggregatedOrder {
142
+ orderId: string;
143
+ direction: 'buy' | 'sell';
144
+ baseAsset: string;
145
+ quoteAsset: string;
146
+ baseAmount: number;
147
+ quoteAmount: number;
148
+ price: number;
149
+ time: string;
150
+ sequence: number;
151
+ fillCount: number;
152
+ isMaker: boolean;
153
+ }
154
+
155
+ // ─── LAST-FILL-GUARD helper (1:1 with dexbot_cow_runtime.ts) ─────────────────
156
+
157
+ /**
158
+ * LAST-FILL-GUARD helper — pivot ± halfIncrement (replaces price-tolerance).
159
+ * last fill @x with increment i: BUY < x*(1 - i/2/100), SELL > x*(1 + i/2/100)
160
+ * e.g. x=1000, i=0.5% => BUY < 997.5, SELL > 1002.5
161
+ * Cold (pivot null or lastType null) => disabled.
162
+ * @param {number} price - Target order price
163
+ * @param {string} type - buy/sell
164
+ * @param {number|null} lastPrice - Most recent fill price
165
+ * @param {string|null} lastType - Most recent fill side (buy/sell)
166
+ * @param {number|any} incrementPercent - Grid increment percent (e.g. 0.5). If not finite/<=0 falls back to DEFAULT_CONFIG.
167
+ * @returns {{blocked: boolean, pivot: number|null, halfInc: number, threshold: number|null}}
168
+ */
169
+ function isLastFillGuardBlocked(
170
+ price: any,
171
+ type: any,
172
+ lastPrice: any,
173
+ lastType: any,
174
+ incrementPercent: any,
175
+ ): { blocked: boolean; pivot: number | null; halfInc?: number; threshold?: number | null } {
176
+ const numPrice = Number(price);
177
+ if (!Number.isFinite(numPrice)) return { blocked: false, pivot: null };
178
+ if (lastPrice == null || !Number.isFinite(Number(lastPrice)) || lastType == null) return { blocked: false, pivot: null };
179
+ const pivot = Number(lastPrice);
180
+ let inc = Number(incrementPercent);
181
+ if (!Number.isFinite(inc) || inc <= 0) {
182
+ inc = Number((C as any)?.DEFAULT_CONFIG?.incrementPercent ?? 0.5);
183
+ }
184
+ if (!Number.isFinite(inc) || inc <= 0) return { blocked: false, pivot: null };
185
+ const halfInc = inc / 2;
186
+ const halfPct = halfInc / 100;
187
+ const buyThreshold = pivot * (1 - halfPct);
188
+ const sellThreshold = pivot * (1 + halfPct);
189
+ if (type === 'buy' && numPrice > buyThreshold) return { blocked: true, pivot, halfInc, threshold: buyThreshold };
190
+ if (type === 'sell' && numPrice < sellThreshold) return { blocked: true, pivot, halfInc, threshold: sellThreshold };
191
+ return { blocked: false, pivot: null, halfInc, threshold: null };
192
+ }
193
+
194
+ function resolveIncrementPercent(botMeta: any, override: number | null): number {
195
+ if (override != null && Number.isFinite(override) && override > 0) return override;
196
+ const fromBot = Number(botMeta?.incrementPercent);
197
+ if (Number.isFinite(fromBot) && fromBot > 0) return fromBot;
198
+ const fromDefault = Number((C as any)?.DEFAULT_CONFIG?.incrementPercent ?? 0.5);
199
+ if (Number.isFinite(fromDefault) && fromDefault > 0) return fromDefault;
200
+ return 0.5;
201
+ }
202
+
203
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
204
+ function printHelp() {
205
+ console.log(`\
206
+ Usage: node dist/analysis/grid_correction_check.js --bot-key <key> [options]
207
+
208
+ Validates LAST-FILL-GUARD discipline (pivot ± halfIncrement):
209
+ last fill @x with increment i (half=i/2) gates both sides —
210
+ BUY must be < x*(1-half/100), SELL > x*(1+half/100).
211
+ e.g. x=1000, i=0.5% => BUY < 997.5 / SELL > 1002.5.
212
+ Mirrors dexbot_cow_runtime:isLastFillGuardBlocked 1:1.
213
+
214
+ Required:
215
+ --bot-key <key> Bot key (e.g. my-grid-bot) or bot name (use --list-bots)
216
+
217
+ Time range (one of):
218
+ --hours <n> Lookback hours from now (default: 168)
219
+ --start <iso> Start time (ISO 8601)
220
+ --end <iso> End time (ISO 8601, default: now)
221
+
222
+ Options:
223
+ --account <id> Override account ID (default: from bot preferredAccount)
224
+ --lookup Legacy: account names always resolve via BitShares node
225
+ --refresh-account Force re-resolution of preferredAccount and update the
226
+ stored accountId when it changed (default: reuse the
227
+ stored accountId with no chain lookup)
228
+ --node <url> BitShares node URL for account / precision resolution
229
+ --increment <pct> Grid increment percent (default: from bot config or ${Number((C as any)?.DEFAULT_CONFIG?.incrementPercent ?? 0.5)})
230
+ --tolerance <pct> Deprecated alias for --increment (kept for compat, prefer --increment)
231
+ --per-fill Check at fill granularity (default: per-order aggregated)
232
+ --include-cross-pair Check consecutive fills across different pairs (default: same pair only)
233
+ --json <file> Export violations as JSON
234
+ --csv <file> Export violations as CSV
235
+ --verbose Show all consecutive pairs, not just violations
236
+ --list-bots List available bot keys and exit
237
+ --help, -h Show this help
238
+
239
+ Examples:
240
+ node dist/analysis/grid_correction_check.js --bot-key <bot-key> --hours 720
241
+ node dist/analysis/grid_correction_check.js --bot-key "<bot-key>" --start 2025-01-01 --end 2025-06-01
242
+ node dist/analysis/grid_correction_check.js --bot-key <bot-key> --hours 168 --increment 1.0
243
+ `);
244
+ }
245
+
246
+ function parseArgs() {
247
+ const args = process.argv.slice(2);
248
+ if (args.includes('--help') || args.includes('-h')) { printHelp(); process.exit(0); }
249
+ if (args.includes('--list-bots')) {
250
+ const settings = loadBotSettings();
251
+ const entries = settings?.bots ?? [];
252
+ if (!entries.length) { console.log('No bots in profiles/bots.json'); process.exit(0); }
253
+ console.log('Available bot keys:');
254
+ for (let i = 0; i < entries.length; i++) {
255
+ const k = computeBotKey(entries[i], i);
256
+ console.log(` ${k} (name: ${entries[i].name ?? '-'}, ${entries[i].assetA}/${entries[i].assetB}, account: ${entries[i].preferredAccount ?? '-'})`);
257
+ }
258
+ process.exit(0);
259
+ }
260
+
261
+ const opts: any = {
262
+ botKey: null,
263
+ hours: null,
264
+ start: null,
265
+ end: null,
266
+ account: null,
267
+ lookup: false,
268
+ refreshAccount: false,
269
+ node: C.NODE_MANAGEMENT.DEFAULT_NODES[0],
270
+ perFill: false,
271
+ includeCrossPair: false,
272
+ incrementPercent: null as number | null,
273
+ json: null,
274
+ csv: null,
275
+ verbose: false,
276
+ };
277
+
278
+ for (let i = 0; i < args.length; i++) {
279
+ switch (args[i]) {
280
+ case '--bot-key': opts.botKey = args[++i]; break;
281
+ case '--hours': opts.hours = parseInt(args[++i], 10); break;
282
+ case '--start': opts.start = args[++i]; break;
283
+ case '--end': opts.end = args[++i]; break;
284
+ case '--account': opts.account = args[++i]; break;
285
+ case '--lookup': opts.lookup = true; break;
286
+ case '--refresh-account': opts.refreshAccount = true; break;
287
+ case '--node': opts.node = args[++i]; break;
288
+ case '--per-fill': opts.perFill = true; break;
289
+ case '--include-cross-pair': opts.includeCrossPair = true; break;
290
+ case '--increment': opts.incrementPercent = parseFloat(args[++i]); break;
291
+ case '--tolerance': {
292
+ const v = parseFloat(args[++i]);
293
+ // Back-compat: old --tolerance was adverse pct before flagging; now map to increment if user still passes it
294
+ // Prefer explicit --increment; don't overwrite if already set
295
+ if (opts.incrementPercent == null) opts.incrementPercent = v > 0 ? v * 2 : null; // heuristic: old tolerance ~ halfInc, so inc ~ 2*tolerance
296
+ console.warn(`[warn] --tolerance is deprecated, use --increment <pct> (e.g. --increment ${v > 0 ? (v*2) : 0.5})`);
297
+ break;
298
+ }
299
+ case '--json': opts.json = args[++i]; break;
300
+ case '--csv': opts.csv = args[++i]; break;
301
+ case '--verbose': opts.verbose = true; break;
302
+ default:
303
+ console.error(`Unknown option: ${args[i]}`);
304
+ printHelp();
305
+ process.exit(1);
306
+ }
307
+ }
308
+
309
+ if (!opts.botKey) {
310
+ console.error('Error: --bot-key is required (use --list-bots to see available keys)');
311
+ printHelp();
312
+ process.exit(1);
313
+ }
314
+ if (!opts.hours && !opts.start) opts.hours = 168;
315
+ if (opts.incrementPercent != null && (isNaN(opts.incrementPercent) || opts.incrementPercent < 0)) {
316
+ console.error('Error: --increment must be a non-negative number');
317
+ process.exit(1);
318
+ }
319
+ return opts;
320
+ }
321
+
322
+ // ─── Time helpers ─────────────────────────────────────────────────────────────
323
+ function resolveTimeRange(opts: any): { gte: string; lte: string; label: string } {
324
+ let gte: string, lte: string;
325
+ if (opts.start) {
326
+ const s = new Date(opts.start);
327
+ if (isNaN(s.getTime())) { console.error(`Invalid --start: ${opts.start}`); process.exit(1); }
328
+ gte = s.toISOString();
329
+ } else {
330
+ const h = opts.hours ?? 168;
331
+ gte = new Date(Date.now() - h * 3600_000).toISOString();
332
+ }
333
+ if (opts.end) {
334
+ const e = new Date(opts.end);
335
+ if (isNaN(e.getTime())) { console.error(`Invalid --end: ${opts.end}`); process.exit(1); }
336
+ lte = e.toISOString();
337
+ } else {
338
+ lte = new Date().toISOString();
339
+ }
340
+ const label = `${gte} → ${lte}`;
341
+ return { gte, lte, label };
342
+ }
343
+
344
+ // ─── Account resolution ───────────────────────────────────────────────────────
345
+ async function resolveAccountId(name: string, nodeUrl: string): Promise<string | null> {
346
+ const { createReadOnlyClient } = await import('../modules/bitshares-native/index.js');
347
+ const client = createReadOnlyClient({ nodes: [nodeUrl] });
348
+ const prevLevel = process.env.LOG_LEVEL;
349
+ process.env.LOG_LEVEL = 'warn';
350
+ try {
351
+ await client.connect();
352
+ const accounts = await client.db('lookup_account_names', [[name]]);
353
+ if (Array.isArray(accounts) && accounts[0]?.id) return accounts[0].id;
354
+ return null;
355
+ } catch (e: any) {
356
+ console.warn(` [warn] Account resolution failed: ${e.message}`);
357
+ return null;
358
+ } finally {
359
+ try { client.disconnect(); } catch (_) {}
360
+ process.env.LOG_LEVEL = prevLevel;
361
+ }
362
+ }
363
+
364
+ async function resolveAccountForBot(botKey: string, overrideAccount: string | null, nodeUrl: string, doRefresh: boolean): Promise<{ accountId: string; botMeta: any }> {
365
+ const botMeta = loadBotMeta(botKey);
366
+ if (!botMeta && !overrideAccount) {
367
+ console.error(`Error: bot key '${botKey}' not found in profiles/bots.json and no --account provided.`);
368
+ console.error(`Use --list-bots to see available keys, or pass --account <1.2.x> explicitly.`);
369
+ process.exit(1);
370
+ }
371
+ // An explicit --account always wins and is never persisted.
372
+ if (overrideAccount) {
373
+ if (/^1\.2\.\d+$/.test(String(overrideAccount))) {
374
+ return { accountId: String(overrideAccount), botMeta };
375
+ }
376
+ console.log(` Resolving account name '${overrideAccount}'...`);
377
+ const resolvedOverride = await resolveAccountId(String(overrideAccount), nodeUrl);
378
+ if (!resolvedOverride) {
379
+ console.error(`Error: failed to resolve --account '${overrideAccount}' to 1.2.x`);
380
+ process.exit(1);
381
+ }
382
+ console.log(` → ${resolvedOverride}`);
383
+ return { accountId: String(resolvedOverride), botMeta };
384
+ }
385
+ const prefRaw = botMeta?.preferredAccount != null ? String(botMeta.preferredAccount) : null;
386
+ const prefIsId = !!prefRaw && /^1\.2\.\d+$/.test(prefRaw);
387
+ const storedId = /^1\.2\.\d+$/.test(String(botMeta?.accountId ?? '')) ? String(botMeta.accountId) : null;
388
+ // An explicit 1.2.x preferredAccount is authoritative: the stored accountId
389
+ // is a derived cache and must never override it. Self-heal the cache.
390
+ if (prefIsId) {
391
+ if (storedId && storedId !== prefRaw && persistBotAccountId(botKey, prefRaw as string)) {
392
+ console.log(` Updated stale stored accountId ${storedId} → ${prefRaw} from preferredAccount`);
393
+ }
394
+ return { accountId: prefRaw as string, botMeta };
395
+ }
396
+ if (!prefRaw) {
397
+ console.error(`Error: bot '${botKey}' has no preferredAccount and no --account provided.`);
398
+ process.exit(1);
399
+ }
400
+ // Name + fresh-enough cache + no refresh requested: offline-friendly fast path.
401
+ if (storedId && !doRefresh) {
402
+ console.log(` Using stored accountId ${storedId} from profiles/bots.json (no lookup needed; pass --refresh-account to re-verify)`);
403
+ return { accountId: storedId, botMeta };
404
+ }
405
+ // No cache, or --refresh-account: resolve the name on-chain.
406
+ console.log(` Resolving account name '${prefRaw}'...`);
407
+ const resolved = await resolveAccountId(prefRaw, nodeUrl);
408
+ if (!resolved) {
409
+ console.error(`Error: failed to resolve account name '${prefRaw}' to 1.2.x`);
410
+ process.exit(1);
411
+ }
412
+ console.log(` → ${resolved}`);
413
+ if (!storedId && persistBotAccountId(botKey, resolved)) {
414
+ console.log(` Stored accountId ${resolved} in profiles/bots.json for '${botKey}'`);
415
+ } else if (storedId && storedId !== resolved && persistBotAccountId(botKey, resolved)) {
416
+ console.log(` Updated stored accountId ${storedId} → ${resolved} in profiles/bots.json for '${botKey}'`);
417
+ } else if (storedId === resolved) {
418
+ console.log(` Stored accountId ${resolved} confirmed up to date`);
419
+ }
420
+ return { accountId: String(resolved), botMeta };
421
+ }
422
+
423
+ // ─── Asset precision resolution ───────────────────────────────────────────────
424
+ async function resolveAssetPrecisions(fills: FillRecord[], nodeUrl: string | null): Promise<void> {
425
+ const unknownIds = new Set<string>();
426
+ for (const f of fills) {
427
+ for (const id of [f.pays.asset_id, f.receives.asset_id, f.fee.asset_id]) {
428
+ if (id !== BTS_ID && !(id in ASSETS) && !(id in resolvedPrecisions)) unknownIds.add(id);
429
+ }
430
+ }
431
+ if (unknownIds.size === 0 || !nodeUrl) return;
432
+ const ids = [...unknownIds];
433
+ console.log(` Resolving ${ids.length} unknown asset(s) from blockchain...`);
434
+ const { createReadOnlyClient } = await import('../modules/bitshares-native/index.js');
435
+ const client = createReadOnlyClient({ nodes: [nodeUrl] });
436
+ try {
437
+ await client.connect();
438
+ const assets = await client.db('get_assets', [ids]);
439
+ if (Array.isArray(assets)) {
440
+ for (const asset of assets) {
441
+ if (asset?.id && asset.precision != null) {
442
+ resolvedPrecisions[asset.id] = asset.precision;
443
+ console.log(` ${asset.id} → ${asset.symbol || '?'} (precision ${asset.precision})`);
444
+ }
445
+ }
446
+ }
447
+ const missing = ids.filter(id => !(id in resolvedPrecisions));
448
+ if (missing.length > 0) console.warn(` [warn] ${missing.length} asset(s) not found on chain: ${missing.join(', ')}`);
449
+ } catch (e: any) {
450
+ console.warn(` [warn] Asset resolution failed: ${e.message}`);
451
+ } finally {
452
+ try { client.disconnect(); } catch (_) {}
453
+ }
454
+ }
455
+
456
+ // ─── Kibana fetch ─────────────────────────────────────────────────────────────
457
+ function buildFillQuery(accountId: string, gte: string, lte: string, size: number) {
458
+ return {
459
+ size,
460
+ track_total_hits: false,
461
+ _source: [
462
+ 'block_data.block_time',
463
+ 'block_data.block_num',
464
+ 'operation_id_num',
465
+ 'operation_history.op_object.pays',
466
+ 'operation_history.op_object.receives',
467
+ 'operation_history.op_object.fee',
468
+ 'operation_history.op_object.order_id',
469
+ 'operation_history.op_object.account_id',
470
+ 'operation_history.op_object.is_maker',
471
+ ],
472
+ query: {
473
+ bool: {
474
+ filter: [
475
+ { term: { operation_type: OP_FILL_ORDER } },
476
+ { term: { 'operation_history.op_object.account_id.keyword': accountId } },
477
+ { range: { 'block_data.block_time': { gte, lte } } },
478
+ ],
479
+ },
480
+ },
481
+ sort: [
482
+ { 'block_data.block_time': { order: 'asc' } },
483
+ { operation_id_num: { order: 'asc' } },
484
+ ],
485
+ };
486
+ }
487
+
488
+ async function fetchAllFills(config: any, accountId: string, gte: string, lte: string): Promise<FillRecord[]> {
489
+ const pageSize = 10000;
490
+ const fills: FillRecord[] = [];
491
+ let searchAfter: any[] | null = null;
492
+ const cfg = { ...BASE_CONFIG, timeout: 60000, ...config };
493
+ while (true) {
494
+ const query = buildFillQuery(accountId, gte, lte, pageSize);
495
+ if (searchAfter) (query as any).search_after = searchAfter;
496
+ const result: any = await kibanaSearch(cfg, query);
497
+ const hits = result?.hits?.hits ?? [];
498
+ if (!hits.length) break;
499
+ for (const hit of hits) {
500
+ const src = hit?._source;
501
+ const op = src?.operation_history?.op_object;
502
+ if (!op || !op.pays || !op.receives) continue;
503
+ fills.push({
504
+ time: src.block_data?.block_time ?? '',
505
+ blockNum: src.block_data?.block_num ?? 0,
506
+ opNum: Number(src.operation_id_num ?? 0),
507
+ orderId: op.order_id ?? '',
508
+ accountId: op.account_id ?? '',
509
+ pays: { amount: Number(op.pays.amount ?? 0), asset_id: op.pays.asset_id ?? '' },
510
+ receives: { amount: Number(op.receives.amount ?? 0), asset_id: op.receives.asset_id ?? '' },
511
+ fee: { amount: Number(op.fee?.amount ?? 0), asset_id: op.fee?.asset_id ?? '' },
512
+ isMaker: op.is_maker ?? false,
513
+ sort: hit.sort,
514
+ });
515
+ }
516
+ if (hits.length < pageSize) break;
517
+ searchAfter = hits[hits.length - 1].sort;
518
+ if (!Array.isArray(searchAfter)) break;
519
+ }
520
+ return fills;
521
+ }
522
+
523
+ // ─── Fill classification ─────────────────────────────────────────────────────
524
+ function classifyFills(fills: FillRecord[]): { trades: TradeFill[]; skipped: number } {
525
+ const trades: TradeFill[] = [];
526
+ let skipped = 0;
527
+ for (const f of fills) {
528
+ const pAsset = f.pays.asset_id;
529
+ const rAsset = f.receives.asset_id;
530
+ let direction: 'buy' | 'sell';
531
+ let baseAsset: string;
532
+ let quoteAsset: string;
533
+ let baseAmount: number;
534
+ let quoteAmount: number;
535
+ let price: number;
536
+
537
+ if (pAsset === BTS_ID && rAsset !== BTS_ID) {
538
+ direction = 'buy';
539
+ baseAsset = rAsset; quoteAsset = BTS_ID;
540
+ baseAmount = toReal(f.receives.amount, rAsset);
541
+ quoteAmount = toReal(f.pays.amount, BTS_ID);
542
+ if (!Number.isFinite(baseAmount) || !Number.isFinite(quoteAmount) || baseAmount <= 0 || quoteAmount <= 0) { skipped++; continue; }
543
+ price = quoteAmount / baseAmount;
544
+ } else if (rAsset === BTS_ID && pAsset !== BTS_ID) {
545
+ direction = 'sell';
546
+ baseAsset = pAsset; quoteAsset = BTS_ID;
547
+ baseAmount = toReal(f.pays.amount, pAsset);
548
+ quoteAmount = toReal(f.receives.amount, BTS_ID);
549
+ if (!Number.isFinite(baseAmount) || !Number.isFinite(quoteAmount) || baseAmount <= 0 || quoteAmount <= 0) { skipped++; continue; }
550
+ price = quoteAmount / baseAmount;
551
+ } else {
552
+ const baseForCheck = pAsset < rAsset ? pAsset : rAsset;
553
+ const quoteForCheck = pAsset < rAsset ? rAsset : pAsset;
554
+ if (assetPrec(baseForCheck) === undefined || assetPrec(quoteForCheck) === undefined) { skipped++; continue; }
555
+ const isSell = pAsset < rAsset;
556
+ direction = isSell ? 'sell' : 'buy';
557
+ baseAsset = isSell ? pAsset : rAsset;
558
+ quoteAsset = isSell ? rAsset : pAsset;
559
+ baseAmount = toReal(isSell ? f.pays.amount : f.receives.amount, baseAsset);
560
+ quoteAmount = toReal(isSell ? f.receives.amount : f.pays.amount, quoteAsset);
561
+ if (!Number.isFinite(baseAmount) || !Number.isFinite(quoteAmount) || baseAmount <= 0 || quoteAmount <= 0) { skipped++; continue; }
562
+ price = quoteAmount / baseAmount;
563
+ }
564
+
565
+ trades.push({
566
+ time: f.time,
567
+ orderId: f.orderId,
568
+ direction,
569
+ baseAsset,
570
+ quoteAsset,
571
+ baseAmount,
572
+ quoteAmount,
573
+ price,
574
+ isMaker: f.isMaker,
575
+ sequence: f.blockNum * 1e6 + f.opNum,
576
+ });
577
+ }
578
+ return { trades, skipped };
579
+ }
580
+
581
+ // ─── Per-order aggregation (collapses multi-fill orders to weighted avg price) ──
582
+ function aggregateByOrder(trades: TradeFill[]): AggregatedOrder[] {
583
+ const map = new Map<string, { trades: TradeFill[] }>();
584
+ for (const t of trades) {
585
+ const k = t.orderId || `__fill_${t.sequence}`;
586
+ if (!map.has(k)) map.set(k, { trades: [] });
587
+ map.get(k)!.trades.push(t);
588
+ }
589
+ const orders: AggregatedOrder[] = [];
590
+ for (const [orderId, group] of map) {
591
+ const first = group.trades[0];
592
+ // All fills for same order should share direction/pair, but validate
593
+ const directions = new Set(group.trades.map(t => t.direction));
594
+ const pairs = new Set(group.trades.map(t => `${t.baseAsset}:${t.quoteAsset}`));
595
+ if (directions.size > 1 || pairs.size > 1) {
596
+ // Mixed direction/pair for same orderId should not happen; keep fills separate
597
+ for (const t of group.trades) {
598
+ orders.push({
599
+ orderId: t.orderId, direction: t.direction, baseAsset: t.baseAsset, quoteAsset: t.quoteAsset,
600
+ baseAmount: t.baseAmount, quoteAmount: t.quoteAmount, price: t.price,
601
+ time: t.time, sequence: t.sequence, fillCount: 1, isMaker: t.isMaker,
602
+ });
603
+ }
604
+ continue;
605
+ }
606
+ const baseAmount = group.trades.reduce((s, t) => s + t.baseAmount, 0);
607
+ const quoteAmount = group.trades.reduce((s, t) => s + t.quoteAmount, 0);
608
+ const price = baseAmount > 0 ? quoteAmount / baseAmount : first.price;
609
+ // Use earliest time / smallest sequence for ordering; last time for display is earliest fill
610
+ const sorted = [...group.trades].sort((a, b) => a.sequence - b.sequence);
611
+ orders.push({
612
+ orderId, direction: first.direction, baseAsset: first.baseAsset, quoteAsset: first.quoteAsset,
613
+ baseAmount, quoteAmount, price,
614
+ time: sorted[0].time,
615
+ sequence: sorted[0].sequence,
616
+ fillCount: group.trades.length,
617
+ isMaker: group.trades.every(t => t.isMaker),
618
+ });
619
+ }
620
+ return orders.sort((a, b) => a.sequence - b.sequence);
621
+ }
622
+
623
+ // ─── Violation detection (LAST-FILL-GUARD 1:1) ────────────────────────────────
624
+ function detectViolations(
625
+ items: (TradeFill | AggregatedOrder)[],
626
+ includeCrossPair: boolean,
627
+ incrementPercent: number,
628
+ ): { violations: Violation[]; checkedPairs: number; sameDirectionPairs: number } {
629
+ const violations: Violation[] = [];
630
+ let checkedPairs = 0;
631
+ let sameDirectionPairs = 0;
632
+
633
+ // Helper for a single chronological sequence (already filtered to one pair or global)
634
+ function checkSequence(seq: (TradeFill | AggregatedOrder)[]) {
635
+ for (let i = 1; i < seq.length; i++) {
636
+ const prev = seq[i - 1] as any;
637
+ const curr = seq[i] as any;
638
+ // Skip same orderId (multi-fill split of one order) — aggregated mode already collapsed, but per-fill may split
639
+ if (prev.orderId && prev.orderId === curr.orderId) continue;
640
+ checkedPairs++;
641
+ if (prev.direction === curr.direction) sameDirectionPairs++;
642
+
643
+ const check = isLastFillGuardBlocked(curr.price, curr.direction, prev.price, prev.direction, incrementPercent);
644
+ if (check.blocked) {
645
+ const delta = curr.price - prev.price;
646
+ const deltaPct = prev.price !== 0 ? (delta / prev.price) * 100 : 0;
647
+ const isSell = curr.direction === 'sell';
648
+ violations.push({
649
+ index: i,
650
+ pair: `${curr.baseAsset}:${curr.quoteAsset}`,
651
+ direction: curr.direction,
652
+ expected: isSell ? `> ${check.threshold?.toFixed(6)} (pivot ${check.pivot} +${check.halfInc}%)` : `< ${check.threshold?.toFixed(6)} (pivot ${check.pivot} -${check.halfInc}%)`,
653
+ prev: prev as TradeFill,
654
+ curr: curr as TradeFill,
655
+ priceDelta: delta,
656
+ priceDeltaPct: deltaPct,
657
+ pivot: check.pivot as number,
658
+ halfInc: check.halfInc as number,
659
+ threshold: check.threshold as number,
660
+ });
661
+ }
662
+ }
663
+ }
664
+
665
+ if (includeCrossPair) {
666
+ // Global consecutive check regardless of pair
667
+ const sorted = [...items].sort((a, b) => (a as any).sequence - (b as any).sequence);
668
+ checkSequence(sorted);
669
+ } else {
670
+ // Per-pair independent sequences (bot trades one pair; cross-pair interleaving is irrelevant)
671
+ const byPair = new Map<string, (TradeFill | AggregatedOrder)[]>();
672
+ for (const it of items) {
673
+ const k = `${(it as any).baseAsset}:${(it as any).quoteAsset}`;
674
+ if (!byPair.has(k)) byPair.set(k, []);
675
+ byPair.get(k)!.push(it);
676
+ }
677
+ for (const [, seq] of byPair) {
678
+ seq.sort((a: any, b: any) => a.sequence - b.sequence);
679
+ checkSequence(seq);
680
+ }
681
+ }
682
+ // Sort violations chronologically for reporting
683
+ violations.sort((a, b) => new Date(a.curr.time).getTime() - new Date(b.curr.time).getTime());
684
+ return { violations, checkedPairs, sameDirectionPairs };
685
+ }
686
+
687
+ // ─── Reporting ────────────────────────────────────────────────────────────────
688
+ function fmt(n: number, d = 4): string {
689
+ if (!Number.isFinite(n)) return 'NaN';
690
+ return n.toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d });
691
+ }
692
+
693
+ function printReport(
694
+ trades: TradeFill[],
695
+ orders: AggregatedOrder[] | null,
696
+ violations: Violation[],
697
+ checkedPairs: number,
698
+ sameDirectionPairs: number,
699
+ skipped: number,
700
+ rangeLabel: string,
701
+ botKey: string,
702
+ accountId: string,
703
+ botMeta: any,
704
+ perFill: boolean,
705
+ includeCrossPair: boolean,
706
+ incrementPercent: number,
707
+ gte: string,
708
+ _lte: string,
709
+ ) {
710
+ const pairGroups = new Map<string, TradeFill[]>();
711
+ for (const t of trades) {
712
+ const k = `${assetSymbol(t.baseAsset)}/${assetSymbol(t.quoteAsset)}`;
713
+ if (!pairGroups.has(k)) pairGroups.set(k, []);
714
+ pairGroups.get(k)!.push(t);
715
+ }
716
+
717
+ const halfInc = incrementPercent / 2;
718
+
719
+ console.log('');
720
+ console.log('═══════════════════════════════════════════════════════════════════');
721
+ console.log(' GRID CORRECTION CHECK — LAST-FILL-GUARD (pivot ± halfIncrement)');
722
+ console.log('═══════════════════════════════════════════════════════════════════');
723
+ console.log(` Bot key: ${botKey}${botMeta?.name ? ` (name: ${botMeta.name})` : ''}`);
724
+ if (botMeta) console.log(` Pair: ${botMeta.assetA ?? '?'} / ${botMeta.assetB ?? '?'}`);
725
+ console.log(` Account: ${accountId}`);
726
+ console.log(` Range: ${rangeLabel}`);
727
+ console.log(` Mode: ${perFill ? 'per-fill' : 'per-order (aggregated)'}${includeCrossPair ? ', cross-pair enabled' : ', same-pair only'}`);
728
+ console.log(` Increment: ${incrementPercent}% (halfInc ${halfInc}%)`);
729
+ console.log(` Guard: BUY < pivot*(1-${halfInc}%) / SELL > pivot*(1+${halfInc}%) — both sides, global pivot`);
730
+ console.log('');
731
+
732
+ const buyCount = trades.filter(t => t.direction === 'buy').length;
733
+ const sellCount = trades.filter(t => t.direction === 'sell').length;
734
+ console.log(` Fills fetched: ${trades.length} (buy: ${buyCount}, sell: ${sellCount})`);
735
+ if (orders) console.log(` Orders (aggregated): ${orders.length} (from ${trades.length} fills)`);
736
+ console.log(` Pairs observed: ${[...pairGroups.keys()].join(', ') || '-'}`);
737
+ if (skipped > 0) console.log(` Skipped (precision): ${skipped}`);
738
+ console.log(` Pairs checked: ${checkedPairs} consecutive pairs (same-direction pairs: ${sameDirectionPairs})`);
739
+ console.log(` Violations: ${violations.length}${checkedPairs > 0 ? ` (${((violations.length / checkedPairs) * 100).toFixed(2)}%)` : ''}`);
740
+ console.log('');
741
+
742
+ if (violations.length === 0) {
743
+ console.log(' ✅ PASS — no grid inversions detected (all BUY < pivot-half, SELL > pivot+half).');
744
+ console.log('');
745
+ if (checkedPairs === 0) {
746
+ console.log(' Note: no consecutive pairs in range to check.');
747
+ console.log(' (Need at least two fills/orders on the same pair.)');
748
+ }
749
+ console.log('');
750
+ return;
751
+ }
752
+
753
+ console.log(` ❌ FAIL — ${violations.length} violation(s) detected:`);
754
+ console.log('');
755
+
756
+ // Per-direction breakdown (blocked side = curr direction)
757
+ const buyV = violations.filter(v => v.direction === 'buy').length;
758
+ const sellV = violations.filter(v => v.direction === 'sell').length;
759
+ console.log(` Breakdown: sell violations: ${sellV} (SELL < pivot+half), buy violations: ${buyV} (BUY > pivot-half)`);
760
+ console.log('');
761
+
762
+ // Timeline clustering by day
763
+ const dayBuckets = new Map<string, number>();
764
+ for (const v of violations) {
765
+ const day = (v.curr.time || '').slice(0, 10) || 'unknown';
766
+ dayBuckets.set(day, (dayBuckets.get(day) || 0) + 1);
767
+ }
768
+ const sortedDays = [...dayBuckets.entries()].sort((a, b) => a[0].localeCompare(b[0]));
769
+ console.log(' Incidents by day:');
770
+ for (const [day, count] of sortedDays) {
771
+ const bar = '█'.repeat(Math.min(count, 40));
772
+ console.log(` ${day} ${String(count).padStart(3)} ${bar}`);
773
+ }
774
+ // Overall incident time span
775
+ const times = violations.map(v => v.curr.time).filter(Boolean).sort();
776
+ if (times.length > 0) {
777
+ console.log(`\n Incident range: ${times[0]} → ${times[times.length - 1]}`);
778
+ // Also show in hours from start
779
+ const gteMs = new Date(gte).getTime();
780
+ const firstMs = new Date(times[0]).getTime();
781
+ const lastMs = new Date(times[times.length - 1]).getTime();
782
+ if (!isNaN(gteMs) && !isNaN(firstMs)) {
783
+ const firstH = ((firstMs - gteMs) / 3600000).toFixed(1);
784
+ const lastH = ((lastMs - gteMs) / 3600000).toFixed(1);
785
+ console.log(` (hours into range: ${firstH}h → ${lastH}h)`);
786
+ }
787
+ }
788
+ console.log('');
789
+
790
+ // Detailed violation table
791
+ console.log(' ── Violations (pivot ± halfIncrement) ──');
792
+ console.log('');
793
+ const hdr = ' # Pair Dir Prev Price Curr Price Thr Δ% Half% Prev Time Curr Time Prev Order Curr Order';
794
+ console.log(hdr);
795
+ console.log(' ' + '─'.repeat(hdr.length - 2));
796
+ for (let i = 0; i < violations.length; i++) {
797
+ const v = violations[i];
798
+ const pairLabel = `${assetSymbol(v.prev.baseAsset)}/${assetSymbol(v.prev.quoteAsset)}`.padEnd(12);
799
+ const dir = v.direction.padEnd(4);
800
+ const pPrice = fmt(v.prev.price, 6).padStart(12);
801
+ const cPrice = fmt(v.curr.price, 6).padStart(12);
802
+ const thr = fmt(v.threshold, 6).padStart(12);
803
+ const delta = (v.priceDeltaPct >= 0 ? '+' : '') + v.priceDeltaPct.toFixed(4) + '%';
804
+ const deltaStr = delta.padStart(9);
805
+ const halfStr = (v.halfInc.toFixed(3) + '%').padStart(6);
806
+ const pTime = (v.prev.time || '').slice(0, 19).replace('T', ' ').padEnd(19);
807
+ const cTime = (v.curr.time || '').slice(0, 19).replace('T', ' ').padEnd(19);
808
+ const pOrd = (v.prev.orderId || '-').slice(0, 16).padEnd(16);
809
+ const cOrd = (v.curr.orderId || '-').slice(0, 16).padEnd(16);
810
+ const marker = v.direction === 'sell' ? `SELL < ${fmt(v.threshold,4)}` : `BUY > ${fmt(v.threshold,4)}`;
811
+ console.log(` ${(String(i + 1)).padStart(2)} ${pairLabel} ${dir} ${pPrice} ${cPrice} ${thr} ${deltaStr} ${halfStr} ${pTime} ${cTime} ${pOrd} ${cOrd} ${marker}`);
812
+ }
813
+ console.log('');
814
+ console.log(` Guard: BUY must be < pivot*(1-${halfInc}%), SELL > pivot*(1+${halfInc}%) — blocked if violated (mirrors bot).`);
815
+ console.log(` Increment ${incrementPercent}% => halfInc ${halfInc}% — e.g. pivot 1000 => BUY thr ${(1000*(1-halfInc/100)).toFixed(4)}, SELL thr ${(1000*(1+halfInc/100)).toFixed(4)}`);
816
+ console.log('');
817
+ }
818
+
819
+ function exportJson(filePath: string, violations: Violation[], trades: TradeFill[], rangeLabel: string, botKey: string, accountId: string, incrementPercent: number) {
820
+ const payload = {
821
+ botKey, accountId, range: rangeLabel,
822
+ incrementPercent,
823
+ halfIncrement: incrementPercent / 2,
824
+ totalFills: trades.length,
825
+ violations: violations.map(v => ({
826
+ pair: `${assetSymbol(v.prev.baseAsset)}/${assetSymbol(v.prev.quoteAsset)}`,
827
+ pairIds: { base: v.prev.baseAsset, quote: v.prev.quoteAsset },
828
+ direction: v.direction,
829
+ expected: v.expected,
830
+ prev: { time: v.prev.time, orderId: v.prev.orderId, price: v.prev.price, baseAmount: v.prev.baseAmount, quoteAmount: v.prev.quoteAmount, isMaker: v.prev.isMaker },
831
+ curr: { time: v.curr.time, orderId: v.curr.orderId, price: v.curr.price, baseAmount: v.curr.baseAmount, quoteAmount: v.curr.quoteAmount, isMaker: v.curr.isMaker },
832
+ pivot: v.pivot,
833
+ halfInc: v.halfInc,
834
+ threshold: v.threshold,
835
+ priceDelta: v.priceDelta,
836
+ priceDeltaPct: v.priceDeltaPct,
837
+ })),
838
+ };
839
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf-8');
840
+ console.log(` JSON exported to ${filePath}`);
841
+ }
842
+
843
+ function exportCsv(filePath: string, violations: Violation[]) {
844
+ const header = 'index,pair,direction,expected,prev_time,prev_order,prev_price,curr_time,curr_order,curr_price,threshold,halfInc,pivot,delta_pct';
845
+ const rows = violations.map((v, i) =>
846
+ [
847
+ i + 1,
848
+ `${assetSymbol(v.prev.baseAsset)}/${assetSymbol(v.prev.quoteAsset)}`,
849
+ v.direction, v.expected,
850
+ v.prev.time, v.prev.orderId, v.prev.price,
851
+ v.curr.time, v.curr.orderId, v.curr.price,
852
+ v.threshold, v.halfInc, v.pivot,
853
+ v.priceDeltaPct.toFixed(6),
854
+ ].map(x => `"${String(x).replace(/"/g, '""')}"`).join(',')
855
+ );
856
+ fs.writeFileSync(filePath, [header, ...rows].join('\n') + '\n', 'utf-8');
857
+ console.log(` CSV exported to ${filePath}`);
858
+ }
859
+
860
+ // ─── Main ─────────────────────────────────────────────────────────────────────
861
+ async function main() {
862
+ const opts = parseArgs();
863
+ const { gte, lte, label } = resolveTimeRange(opts);
864
+
865
+ console.log(`\nGrid correction check — bot-key: ${opts.botKey}`);
866
+ console.log(`Range: ${label}`);
867
+
868
+ const { accountId, botMeta } = await resolveAccountForBot(opts.botKey, opts.account, opts.node, opts.refreshAccount);
869
+ console.log(`Account: ${accountId}${botMeta ? ` (${botMeta.assetA}/${botMeta.assetB})` : ''}`);
870
+ const incrementPercent = resolveIncrementPercent(botMeta, opts.incrementPercent);
871
+ console.log(`Increment: ${incrementPercent}% (halfInc ${incrementPercent/2}%)${opts.incrementPercent == null && botMeta?.incrementPercent != null ? ' — from bot config' : opts.incrementPercent != null ? ' — from --increment' : ' — default'}`);
872
+
873
+ console.log(`\nFetching fills from Kibana...`);
874
+ const fills = await fetchAllFills({}, accountId, gte, lte);
875
+ console.log(` Fetched ${fills.length} fill_order operation(s)`);
876
+
877
+ if (fills.length === 0) {
878
+ console.log('\nNo fills in range — nothing to check.');
879
+ process.exit(0);
880
+ }
881
+
882
+ await resolveAssetPrecisions(fills, opts.node);
883
+
884
+ const { trades, skipped } = classifyFills(fills);
885
+ console.log(` Classified ${trades.length} trade(s)${skipped > 0 ? `, ${skipped} skipped (unknown precision)` : ''}`);
886
+ if (trades.length < 2) {
887
+ console.log('\nFewer than 2 trade fills — no consecutive pairs to check.');
888
+ process.exit(0);
889
+ }
890
+
891
+ // Sort chronologically
892
+ trades.sort((a, b) => a.sequence - b.sequence);
893
+
894
+ if (opts.verbose) {
895
+ console.log('\n ── Trade sequence ──');
896
+ for (let i = 0; i < trades.length; i++) {
897
+ const t = trades[i];
898
+ const pair = `${assetSymbol(t.baseAsset)}/${assetSymbol(t.quoteAsset)}`;
899
+ console.log(` ${String(i + 1).padStart(3)} ${t.time.slice(0, 19)} ${t.direction.padEnd(4)} ${pair.padEnd(18)} price ${fmt(t.price, 6)} order ${t.orderId}`);
900
+ }
901
+ }
902
+
903
+ // Choose per-fill or per-order mode
904
+ let items: (TradeFill | AggregatedOrder)[];
905
+ if (opts.perFill) {
906
+ items = trades;
907
+ } else {
908
+ const orders = aggregateByOrder(trades);
909
+ console.log(` Aggregated into ${orders.length} order(s) (multi-fill orders collapsed by weighted avg price)`);
910
+ items = orders;
911
+ }
912
+
913
+ const { violations, checkedPairs, sameDirectionPairs } = detectViolations(items, opts.includeCrossPair, incrementPercent);
914
+
915
+ const ordersForReport = opts.perFill ? null : (items as AggregatedOrder[]);
916
+ printReport(trades, ordersForReport, violations, checkedPairs, sameDirectionPairs, skipped, label, opts.botKey, accountId, botMeta, opts.perFill, opts.includeCrossPair, incrementPercent, gte, lte);
917
+
918
+ if (opts.json) exportJson(opts.json, violations, trades, label, opts.botKey, accountId, incrementPercent);
919
+ if (opts.csv) exportCsv(opts.csv, violations);
920
+
921
+ process.exit(violations.length > 0 ? 2 : 0);
922
+ }
923
+
924
+ export { isLastFillGuardBlocked, classifyFills, aggregateByOrder, detectViolations, TradeFill, FillRecord, Violation, AggregatedOrder };
925
+
926
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
927
+ main().catch(e => {
928
+ console.error('\n[fatal]', (e as any)?.message ?? e);
929
+ if (process.env.DEBUG) console.error((e as any)?.stack);
930
+ process.exit(1);
931
+ });
932
+ }