dexbot 1.4.24 → 1.4.25

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