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