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