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