dexbot 1.6.2 → 1.6.4
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 +53 -3
- package/README.md +11 -7
- package/analysis/README.md +20 -14
- package/analysis/account_resolver.ts +171 -0
- package/analysis/ama_fitting/analyze_lambda_vs_slow.ts +2 -5
- package/analysis/ama_fitting/package.json +1 -1
- package/analysis/bot_usage/discover_bot_accounts.ts +20 -27
- package/analysis/chain_pool.ts +47 -0
- package/analysis/chart_utils.ts +33 -20
- package/analysis/derivative_chart_generator.ts +2 -3
- package/analysis/fills_source.ts +234 -0
- package/analysis/grid_correction_check.ts +104 -244
- package/analysis/results/ama_sweep_results_lp_pool_133_1h.json +2455 -0
- package/analysis/results/bot_fitting_results_lp_pool_133_1h.json +218 -0
- package/analysis/trade_profitability.ts +169 -304
- package/analysis/tradingview/README.md +2 -2
- package/analysis/tradingview/h-bts_tradingview.html +1570 -0
- package/analysis/tradingview/t-bts_tradingview.html +1570 -0
- package/analysis/tradingview/tradingview_uplot_chart_generator.ts +3 -4
- package/analysis/trend_detection/dynamic_weight_chart_generator.ts +2 -3
- package/analysis/trend_detection/kalman_chart_generator.ts +2 -3
- package/analysis/trend_detection/package.json +1 -1
- package/analysis/trend_detection/regime_chart_generator.ts +2 -3
- package/analysis/trend_detection/volatility_chart_generator.ts +2 -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/account_resolver.d.ts +43 -0
- package/dist/analysis/account_resolver.d.ts.map +1 -0
- package/dist/analysis/account_resolver.js +137 -0
- package/dist/analysis/account_resolver.js.map +1 -0
- package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.d.ts.map +1 -1
- package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js +2 -4
- package/dist/analysis/ama_fitting/analyze_lambda_vs_slow.js.map +1 -1
- package/dist/analysis/bot_usage/discover_bot_accounts.js +20 -28
- package/dist/analysis/bot_usage/discover_bot_accounts.js.map +1 -1
- package/dist/analysis/chain_pool.d.ts +10 -0
- package/dist/analysis/chain_pool.d.ts.map +1 -0
- package/dist/analysis/chain_pool.js +47 -0
- package/dist/analysis/chain_pool.js.map +1 -0
- package/dist/analysis/chart_utils.d.ts +14 -1
- package/dist/analysis/chart_utils.d.ts.map +1 -1
- package/dist/analysis/chart_utils.js +32 -22
- package/dist/analysis/chart_utils.js.map +1 -1
- package/dist/analysis/derivative_chart_generator.d.ts.map +1 -1
- package/dist/analysis/derivative_chart_generator.js +2 -3
- package/dist/analysis/derivative_chart_generator.js.map +1 -1
- package/dist/analysis/fills_source.d.ts +79 -0
- package/dist/analysis/fills_source.d.ts.map +1 -0
- package/dist/analysis/fills_source.js +186 -0
- package/dist/analysis/fills_source.js.map +1 -0
- package/dist/analysis/grid_correction_check.d.ts +4 -20
- package/dist/analysis/grid_correction_check.d.ts.map +1 -1
- package/dist/analysis/grid_correction_check.js +95 -248
- package/dist/analysis/grid_correction_check.js.map +1 -1
- package/dist/analysis/trade_profitability.d.ts +18 -19
- package/dist/analysis/trade_profitability.d.ts.map +1 -1
- package/dist/analysis/trade_profitability.js +136 -280
- package/dist/analysis/trade_profitability.js.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.d.ts.map +1 -1
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js +3 -4
- package/dist/analysis/tradingview/tradingview_uplot_chart_generator.js.map +1 -1
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.d.ts.map +1 -1
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js +2 -3
- package/dist/analysis/trend_detection/dynamic_weight_chart_generator.js.map +1 -1
- package/dist/analysis/trend_detection/kalman_chart_generator.d.ts.map +1 -1
- package/dist/analysis/trend_detection/kalman_chart_generator.js +2 -3
- package/dist/analysis/trend_detection/kalman_chart_generator.js.map +1 -1
- package/dist/analysis/trend_detection/regime_chart_generator.d.ts.map +1 -1
- package/dist/analysis/trend_detection/regime_chart_generator.js +2 -3
- package/dist/analysis/trend_detection/regime_chart_generator.js.map +1 -1
- package/dist/analysis/trend_detection/volatility_chart_generator.d.ts.map +1 -1
- package/dist/analysis/trend_detection/volatility_chart_generator.js +2 -3
- package/dist/analysis/trend_detection/volatility_chart_generator.js.map +1 -1
- package/dist/market_adapter/lp_chart_core.d.ts.map +1 -1
- package/dist/market_adapter/lp_chart_core.js +2 -3
- package/dist/market_adapter/lp_chart_core.js.map +1 -1
- package/dist/modules/account_bots.d.ts +1 -18
- package/dist/modules/account_bots.d.ts.map +1 -1
- package/dist/modules/account_bots.js +1 -1
- package/dist/modules/account_bots.js.map +1 -1
- package/dist/modules/bitshares-native/serial/operations.d.ts +1 -84
- package/dist/modules/bitshares-native/serial/operations.d.ts.map +1 -1
- package/dist/modules/bitshares-native/serial/operations.js +1 -38
- package/dist/modules/bitshares-native/serial/operations.js.map +1 -1
- package/dist/modules/cli_colors.d.ts +0 -1
- package/dist/modules/cli_colors.d.ts.map +1 -1
- package/dist/modules/constants.js +5 -5
- package/dist/modules/constants.js.map +1 -1
- package/dist/modules/credential_policy.d.ts +1 -25
- package/dist/modules/credential_policy.d.ts.map +1 -1
- package/dist/modules/credential_policy.js +1 -1
- package/dist/modules/credential_policy.js.map +1 -1
- package/dist/modules/credential_runtime.d.ts +1 -6
- package/dist/modules/credential_runtime.d.ts.map +1 -1
- package/dist/modules/credential_runtime.js +1 -1
- package/dist/modules/credential_runtime.js.map +1 -1
- package/dist/modules/credential_session_cache.d.ts +1 -5
- package/dist/modules/credential_session_cache.d.ts.map +1 -1
- package/dist/modules/credential_session_cache.js +1 -1
- package/dist/modules/credential_session_cache.js.map +1 -1
- package/dist/modules/crypto/pure_secp256k1.d.ts +1 -2
- package/dist/modules/crypto/pure_secp256k1.d.ts.map +1 -1
- package/dist/modules/crypto/pure_secp256k1.js +1 -1
- package/dist/modules/crypto/pure_secp256k1.js.map +1 -1
- package/dist/modules/dexbot_class.d.ts +9 -8
- package/dist/modules/dexbot_class.d.ts.map +1 -1
- package/dist/modules/dexbot_class.js +9 -8
- package/dist/modules/dexbot_class.js.map +1 -1
- package/dist/modules/dexbot_cow_runtime.d.ts +87 -7
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +489 -14
- package/dist/modules/dexbot_cow_runtime.js.map +1 -1
- package/dist/modules/dexbot_fill_runtime.d.ts +1 -17
- package/dist/modules/dexbot_fill_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_fill_runtime.js +1 -1
- package/dist/modules/dexbot_fill_runtime.js.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts +1 -1
- package/dist/modules/dexbot_maintenance_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_maintenance_runtime.js +21 -5
- package/dist/modules/dexbot_maintenance_runtime.js.map +1 -1
- package/dist/modules/dexbot_state_recovery.d.ts +1 -7
- package/dist/modules/dexbot_state_recovery.d.ts.map +1 -1
- package/dist/modules/dexbot_state_recovery.js +1 -1
- package/dist/modules/dexbot_state_recovery.js.map +1 -1
- package/dist/modules/launcher/adapter_requirement.d.ts +1 -5
- package/dist/modules/launcher/adapter_requirement.d.ts.map +1 -1
- package/dist/modules/launcher/adapter_requirement.js +1 -1
- package/dist/modules/launcher/adapter_requirement.js.map +1 -1
- package/dist/modules/launcher/bot_supervisor.d.ts +1 -6
- package/dist/modules/launcher/bot_supervisor.d.ts.map +1 -1
- package/dist/modules/launcher/bot_supervisor.js +1 -1
- package/dist/modules/launcher/bot_supervisor.js.map +1 -1
- package/dist/modules/launcher/foreign_cred_daemon.d.ts +1 -3
- package/dist/modules/launcher/foreign_cred_daemon.d.ts.map +1 -1
- package/dist/modules/launcher/foreign_cred_daemon.js +1 -1
- package/dist/modules/launcher/foreign_cred_daemon.js.map +1 -1
- package/dist/modules/launcher/launch_modes.d.ts +1 -2
- package/dist/modules/launcher/launch_modes.d.ts.map +1 -1
- package/dist/modules/launcher/launch_modes.js +1 -1
- package/dist/modules/launcher/launch_modes.js.map +1 -1
- package/dist/modules/launcher/market_adapter_runtime.d.ts +1 -3
- package/dist/modules/launcher/market_adapter_runtime.d.ts.map +1 -1
- package/dist/modules/launcher/market_adapter_runtime.js +1 -1
- package/dist/modules/launcher/market_adapter_runtime.js.map +1 -1
- package/dist/modules/launcher/monolithic_runtime.d.ts +1 -5
- package/dist/modules/launcher/monolithic_runtime.d.ts.map +1 -1
- package/dist/modules/launcher/monolithic_runtime.js +1 -1
- package/dist/modules/launcher/monolithic_runtime.js.map +1 -1
- package/dist/modules/launcher/status_reporting.d.ts +1 -2
- package/dist/modules/launcher/status_reporting.d.ts.map +1 -1
- package/dist/modules/launcher/status_reporting.js +1 -1
- package/dist/modules/launcher/status_reporting.js.map +1 -1
- package/dist/modules/order/export.d.ts +1 -8
- package/dist/modules/order/export.d.ts.map +1 -1
- package/dist/modules/order/export.js +1 -1
- package/dist/modules/order/export.js.map +1 -1
- package/dist/modules/order/grid.d.ts +7 -0
- package/dist/modules/order/grid.d.ts.map +1 -1
- package/dist/modules/order/grid.js +52 -98
- package/dist/modules/order/grid.js.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.d.ts +1 -10
- package/dist/modules/order/grid_reconcile_internal.d.ts.map +1 -1
- package/dist/modules/order/grid_reconcile_internal.js +2 -2
- package/dist/modules/order/grid_reconcile_internal.js.map +1 -1
- package/dist/modules/order/manager.d.ts +10 -2
- package/dist/modules/order/manager.d.ts.map +1 -1
- package/dist/modules/order/manager.js +20 -7
- package/dist/modules/order/manager.js.map +1 -1
- package/dist/modules/order/sync_engine.d.ts.map +1 -1
- package/dist/modules/order/sync_engine.js +25 -20
- package/dist/modules/order/sync_engine.js.map +1 -1
- package/dist/modules/order/utils/math.d.ts +1 -10
- package/dist/modules/order/utils/math.d.ts.map +1 -1
- package/dist/modules/order/utils/math.js +1 -1
- package/dist/modules/order/utils/math.js.map +1 -1
- package/dist/modules/order/utils/order.d.ts +45 -7
- package/dist/modules/order/utils/order.d.ts.map +1 -1
- package/dist/modules/order/utils/order.js +160 -15
- package/dist/modules/order/utils/order.js.map +1 -1
- package/dist/modules/order/utils/system.d.ts +0 -15
- package/dist/modules/order/utils/system.d.ts.map +1 -1
- package/dist/modules/order/utils/system.js +2 -2
- package/dist/modules/order/utils/system.js.map +1 -1
- package/dist/modules/process_discovery.d.ts +0 -3
- package/dist/modules/process_discovery.d.ts.map +1 -1
- package/dist/modules/process_discovery.js +1 -1
- package/dist/modules/process_discovery.js.map +1 -1
- package/dist/modules/utils/chain_logs.d.ts +1 -2
- package/dist/modules/utils/chain_logs.d.ts.map +1 -1
- package/dist/modules/utils/chain_logs.js +1 -1
- package/dist/modules/utils/chain_logs.js.map +1 -1
- package/dist/scripts/analyze-git.js +2 -2
- package/dist/scripts/analyze-git.js.map +1 -1
- package/dist/scripts/update.js +46 -31
- package/dist/scripts/update.js.map +1 -1
- package/docs/BITSHARES_ONBOARDING.md +110 -40
- package/docs/COPY_ON_WRITE_MASTER_PLAN.md +10 -207
- package/docs/COW_INVARIANTS.md +16 -10
- package/docs/DEXBOT_COMPARISON.md +3 -3
- package/docs/EVOLUTION.md +75 -29
- package/docs/FUND_MOVEMENT_AND_ACCOUNTING.md +6 -6
- package/docs/GRID_RECALCULATION.md +14 -6
- package/docs/GRID_RECONCILE.md +28 -25
- package/docs/LIFECYCLE.md +12 -11
- package/docs/README.md +48 -42
- package/docs/architecture.md +19 -30
- package/docs/developer_guide.md +3 -3
- package/package.json +1 -1
- package/scripts/git-viewer.sh +1 -1
- package/tests/README.md +10 -1
|
@@ -1,20 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
amount: number;
|
|
4
|
-
asset_id: string;
|
|
5
|
-
}
|
|
6
|
-
interface FillRecord {
|
|
7
|
-
time: string;
|
|
8
|
-
blockNum: number;
|
|
9
|
-
opNum: number;
|
|
10
|
-
orderId: string;
|
|
11
|
-
accountId: string;
|
|
12
|
-
pays: AssetAmount;
|
|
13
|
-
receives: AssetAmount;
|
|
14
|
-
fee: AssetAmount;
|
|
15
|
-
isMaker: boolean;
|
|
16
|
-
sort: any[];
|
|
17
|
-
}
|
|
2
|
+
import { FillRecord } from './fills_source.js';
|
|
18
3
|
interface TradeFill {
|
|
19
4
|
time: string;
|
|
20
5
|
orderId: string;
|
|
@@ -70,6 +55,11 @@ declare function classifyFills(fills: FillRecord[], filterAsset: string | null):
|
|
|
70
55
|
pairs: Set<string>;
|
|
71
56
|
};
|
|
72
57
|
declare function analyzePair(trades: TradeFill[], matchMode?: 'fifo' | 'sequential'): PairAnalysis;
|
|
58
|
+
interface WindowRange {
|
|
59
|
+
/** The analysis window actually queried — drives annualisation. */
|
|
60
|
+
startMs: number;
|
|
61
|
+
endMs: number;
|
|
62
|
+
}
|
|
73
63
|
interface TradingMetrics {
|
|
74
64
|
totalLots: number;
|
|
75
65
|
winRate: number;
|
|
@@ -81,8 +71,16 @@ interface TradingMetrics {
|
|
|
81
71
|
expectancyPct: number;
|
|
82
72
|
expectancyR: number;
|
|
83
73
|
netExpectancyBts: number;
|
|
84
|
-
|
|
85
|
-
|
|
74
|
+
sharpeAnn: number;
|
|
75
|
+
sortinoAnn: number;
|
|
76
|
+
sharpeAnnSE: number;
|
|
77
|
+
periodLabel: string;
|
|
78
|
+
periodCount: number;
|
|
79
|
+
periodSpanDays: number;
|
|
80
|
+
scoredSpanDays: number;
|
|
81
|
+
annualFactor: number;
|
|
82
|
+
projectedNetPnlPerDay: number;
|
|
83
|
+
projectedNetPnlAnn: number;
|
|
86
84
|
maxConsecWins: number;
|
|
87
85
|
maxConsecLosses: number;
|
|
88
86
|
avgHoldHours: number;
|
|
@@ -90,6 +88,7 @@ interface TradingMetrics {
|
|
|
90
88
|
bestTradePct: number;
|
|
91
89
|
worstTradePct: number;
|
|
92
90
|
mddPct: number;
|
|
91
|
+
mddAbsBts: number;
|
|
93
92
|
mddHadStablePeak: boolean;
|
|
94
93
|
isOngoingRecovery: boolean;
|
|
95
94
|
currentDrawdownDays: number;
|
|
@@ -110,6 +109,6 @@ interface TradingMetrics {
|
|
|
110
109
|
pctRGreater2: number;
|
|
111
110
|
pctRLessNeg1: number;
|
|
112
111
|
}
|
|
113
|
-
declare function computeMetrics(pair: PairAnalysis): TradingMetrics;
|
|
112
|
+
declare function computeMetrics(pair: PairAnalysis, window?: WindowRange): TradingMetrics;
|
|
114
113
|
export { analyzePair, classifyFills, computeMetrics, TradeFill, FillRecord, RealizedPnl, PairAnalysis, TradingMetrics };
|
|
115
114
|
//# sourceMappingURL=trade_profitability.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trade_profitability.d.ts","sourceRoot":"","sources":["../../analysis/trade_profitability.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"trade_profitability.d.ts","sourceRoot":"","sources":["../../analysis/trade_profitability.ts"],"names":[],"mappings":";AAMA,OAAO,EAOH,UAAU,EACb,MAAM,mBAAmB,CAAC;AA2B3B,UAAU,SAAS;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;CAC1B;AAWD,UAAU,WAAW;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;CACxB;AAED,UAAU,YAAY;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;CACvB;AA2GD,iBAAS,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG;IAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CA2FnH;AAID,iBAAS,WAAW,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAE,MAAM,GAAG,YAA2B,GAAG,YAAY,CAyKvG;AAgID,UAAU,WAAW;IACjB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,cAAc;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IAOzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAElB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACxB;AAaD,iBAAS,cAAc,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,cAAc,CA+VhF;AAgTD,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,CAAC"}
|
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import { findBotKeyByAccountRef, getStoredBotAccountId, persistBotAccountId } from './bot_key_utils.js';
|
|
8
|
-
const { kibanaSearch, DEFAULT_CONFIG: BASE_CONFIG } = KC;
|
|
5
|
+
import { resolveAccountRef } from './account_resolver.js';
|
|
6
|
+
import { BTS_ID, assetPrec as getPrec, assetSymbol, fetchAllFills, resolveAssetPrecisions, toReal, } from './fills_source.js';
|
|
9
7
|
/**
|
|
10
8
|
* TRADE PROFITABILITY ANALYZER
|
|
11
9
|
*
|
|
@@ -25,71 +23,7 @@ const { kibanaSearch, DEFAULT_CONFIG: BASE_CONFIG } = KC;
|
|
|
25
23
|
* node dist/analysis/trade_profitability.js 1.2.3 --hours 168 --match-mode fifo
|
|
26
24
|
*/
|
|
27
25
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
28
|
-
const OP_FILL_ORDER = 4;
|
|
29
|
-
const BTS_ID = '1.3.0';
|
|
30
26
|
let BLOCKCHAIN_FEE_PER_FILL = 0.09652; // BTS — flat blockchain operation fee (not market fee); override with --fee-per-order
|
|
31
|
-
const ASSETS = {
|
|
32
|
-
'1.3.0': { symbol: 'BTS', precision: 5 },
|
|
33
|
-
'1.3.118': { symbol: 'GBP', precision: 4 },
|
|
34
|
-
'1.3.119': { symbol: 'JPY', precision: 2 },
|
|
35
|
-
'1.3.120': { symbol: 'EUR', precision: 4 },
|
|
36
|
-
'1.3.1325': { symbol: 'RUBLE', precision: 5 },
|
|
37
|
-
'1.3.2512': { symbol: 'EVRAZ', precision: 4 },
|
|
38
|
-
'1.3.3291': { symbol: 'TWENTIX', precision: 5 },
|
|
39
|
-
'1.3.4099': { symbol: 'XBTSX.STH', precision: 6 },
|
|
40
|
-
'1.3.4156': { symbol: 'XBTSX.DOGE', precision: 5 },
|
|
41
|
-
'1.3.4157': { symbol: 'XBTSX.BTC', precision: 8 },
|
|
42
|
-
'1.3.4159': { symbol: 'XBTSX.LTC', precision: 8 },
|
|
43
|
-
'1.3.4176': { symbol: 'XBTSX.DASH', precision: 8 },
|
|
44
|
-
'1.3.4274': { symbol: 'XBTSX.BCH', precision: 8 },
|
|
45
|
-
'1.3.4760': { symbol: 'XBTSX.ETH', precision: 7 },
|
|
46
|
-
'1.3.5537': { symbol: 'IOB.XRP', precision: 4 },
|
|
47
|
-
'1.3.5541': { symbol: 'XBTSX.BNB', precision: 7 },
|
|
48
|
-
'1.3.5589': { symbol: 'XBTSX.USDT', precision: 6 },
|
|
49
|
-
'1.3.5641': { symbol: 'HONEST.CNY', precision: 4 },
|
|
50
|
-
'1.3.5649': { symbol: 'HONEST.USD', precision: 4 },
|
|
51
|
-
'1.3.5650': { symbol: 'HONEST.BTC', precision: 8 },
|
|
52
|
-
'1.3.5659': { symbol: 'HONEST.ETH', precision: 6 },
|
|
53
|
-
'1.3.5870': { symbol: 'XBTSX.FIL', precision: 6 },
|
|
54
|
-
'1.3.5887': { symbol: 'XBTSX.RUB', precision: 4 },
|
|
55
|
-
'1.3.5902': { symbol: 'XBTSX.USDC', precision: 6 },
|
|
56
|
-
'1.3.6013': { symbol: 'XBTSX.HIVE', precision: 6 },
|
|
57
|
-
'1.3.6124': { symbol: 'XBTSX.AVAX', precision: 6 },
|
|
58
|
-
'1.3.6139': { symbol: 'XBTSX.XAUT', precision: 6 },
|
|
59
|
-
'1.3.6166': { symbol: 'XBTSX.MATIC', precision: 5 },
|
|
60
|
-
'1.3.6241': { symbol: 'XBTSX.ETC', precision: 7 },
|
|
61
|
-
'1.3.6268': { symbol: 'BTWTY.EOS', precision: 4 },
|
|
62
|
-
'1.3.6301': { symbol: 'HONEST.MONEY', precision: 8 },
|
|
63
|
-
'1.3.6304': { symbol: 'HONEST.ADA', precision: 8 },
|
|
64
|
-
'1.3.6305': { symbol: 'HONEST.DOT', precision: 8 },
|
|
65
|
-
'1.3.6309': { symbol: 'HONEST.ATOM', precision: 8 },
|
|
66
|
-
'1.3.6311': { symbol: 'HONEST.ALGO', precision: 8 },
|
|
67
|
-
'1.3.6312': { symbol: 'HONEST.FIL', precision: 8 },
|
|
68
|
-
'1.3.6313': { symbol: 'HONEST.EOS', precision: 8 },
|
|
69
|
-
'1.3.6315': { symbol: 'HONEST.EUR', precision: 4 },
|
|
70
|
-
'1.3.6316': { symbol: 'HONEST.GBP', precision: 4 },
|
|
71
|
-
'1.3.6317': { symbol: 'HONEST.JPY', precision: 4 },
|
|
72
|
-
'1.3.6444': { symbol: 'IOB.XLM', precision: 4 },
|
|
73
|
-
'1.3.6573': { symbol: 'XBTSX.DAI', precision: 6 },
|
|
74
|
-
'1.3.6620': { symbol: 'XBTSX.A', precision: 6 },
|
|
75
|
-
'1.3.6627': { symbol: 'XBTSX.LINK', precision: 6 },
|
|
76
|
-
};
|
|
77
|
-
const resolvedPrecisions = {};
|
|
78
|
-
function assetSymbol(id) {
|
|
79
|
-
return ASSETS[id]?.symbol ?? id;
|
|
80
|
-
}
|
|
81
|
-
function assetPrec(id) {
|
|
82
|
-
return ASSETS[id]?.precision ?? resolvedPrecisions[id];
|
|
83
|
-
}
|
|
84
|
-
function getPrec(id) {
|
|
85
|
-
return assetPrec(id);
|
|
86
|
-
}
|
|
87
|
-
function toReal(amount, assetId) {
|
|
88
|
-
const p = getPrec(assetId);
|
|
89
|
-
if (p === undefined)
|
|
90
|
-
return NaN;
|
|
91
|
-
return amount / Math.pow(10, p);
|
|
92
|
-
}
|
|
93
27
|
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
94
28
|
function printHelp() {
|
|
95
29
|
console.log(`\
|
|
@@ -108,7 +42,6 @@ Options:
|
|
|
108
42
|
--asset <assetId> Filter to one base asset (e.g. 1.3.113 for bitUSD)
|
|
109
43
|
--lookup Legacy (no-op): account names always resolve automatically
|
|
110
44
|
--refresh-account Force re-resolution and update the stored accountId
|
|
111
|
-
--node <url> BitShares node URL (default: first healthy from built-in pool)
|
|
112
45
|
--csv <file> Export trade list as CSV
|
|
113
46
|
--json <file> Export full analysis as JSON
|
|
114
47
|
--trades Show per-order PnL detail (hidden by default)
|
|
@@ -138,7 +71,6 @@ function parseArgs() {
|
|
|
138
71
|
asset: null,
|
|
139
72
|
lookup: false,
|
|
140
73
|
refreshAccount: false,
|
|
141
|
-
node: C.NODE_MANAGEMENT.DEFAULT_NODES[0],
|
|
142
74
|
csv: null,
|
|
143
75
|
json: null,
|
|
144
76
|
matchMode: 'sequential',
|
|
@@ -166,9 +98,6 @@ function parseArgs() {
|
|
|
166
98
|
case '--refresh-account':
|
|
167
99
|
opts.refreshAccount = true;
|
|
168
100
|
break;
|
|
169
|
-
case '--node':
|
|
170
|
-
opts.node = args[++i];
|
|
171
|
-
break;
|
|
172
101
|
case '--csv':
|
|
173
102
|
opts.csv = args[++i];
|
|
174
103
|
break;
|
|
@@ -203,151 +132,6 @@ function parseArgs() {
|
|
|
203
132
|
}
|
|
204
133
|
return opts;
|
|
205
134
|
}
|
|
206
|
-
// ─── Account name resolution ─────────────────────────────────────────────────
|
|
207
|
-
async function resolveAccountId(name, nodeUrl) {
|
|
208
|
-
const { createReadOnlyClient } = await import('../modules/bitshares-native/index.js');
|
|
209
|
-
const client = createReadOnlyClient({ nodes: [nodeUrl] });
|
|
210
|
-
// Suppress transport INFO logs during ephemeral connection:
|
|
211
|
-
// bitshares-native transport logger (new Logger('Transport')) writes
|
|
212
|
-
// "[timestamp] [INFO] [Transport] ..." — silence by raising log level.
|
|
213
|
-
const prevLevel = process.env.LOG_LEVEL;
|
|
214
|
-
process.env.LOG_LEVEL = 'warn';
|
|
215
|
-
try {
|
|
216
|
-
await client.connect();
|
|
217
|
-
const accounts = await client.db('lookup_account_names', [[name]]);
|
|
218
|
-
if (Array.isArray(accounts) && accounts[0]?.id) {
|
|
219
|
-
return accounts[0].id;
|
|
220
|
-
}
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
catch (e) {
|
|
224
|
-
console.warn(` [warn] Account resolution failed: ${e.message}`);
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
finally {
|
|
228
|
-
try {
|
|
229
|
-
client.disconnect();
|
|
230
|
-
}
|
|
231
|
-
catch (_) { }
|
|
232
|
-
process.env.LOG_LEVEL = prevLevel;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
// ─── On-chain asset precision resolution ────────────────────────────────────
|
|
236
|
-
/**
|
|
237
|
-
* Collects all unique non-BTS asset IDs from fills, resolves unknown
|
|
238
|
-
* precisions from the blockchain, and populates the runtime cache.
|
|
239
|
-
*/
|
|
240
|
-
async function resolveAssetPrecisions(fills, nodeUrl) {
|
|
241
|
-
const unknownIds = new Set();
|
|
242
|
-
for (const f of fills) {
|
|
243
|
-
for (const id of [f.pays.asset_id, f.receives.asset_id, f.fee.asset_id]) {
|
|
244
|
-
if (id !== BTS_ID && !(id in ASSETS) && !(id in resolvedPrecisions)) {
|
|
245
|
-
unknownIds.add(id);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
if (unknownIds.size === 0 || !nodeUrl)
|
|
250
|
-
return;
|
|
251
|
-
const ids = [...unknownIds];
|
|
252
|
-
console.log(` Resolving ${ids.length} unknown asset(s) from blockchain...`);
|
|
253
|
-
const { createReadOnlyClient } = await import('../modules/bitshares-native/index.js');
|
|
254
|
-
const client = createReadOnlyClient({ nodes: [nodeUrl] });
|
|
255
|
-
try {
|
|
256
|
-
await client.connect();
|
|
257
|
-
const assets = await client.db('get_assets', [ids]);
|
|
258
|
-
if (Array.isArray(assets)) {
|
|
259
|
-
for (const asset of assets) {
|
|
260
|
-
if (asset?.id && asset.precision != null) {
|
|
261
|
-
resolvedPrecisions[asset.id] = asset.precision;
|
|
262
|
-
console.log(` ${asset.id} → ${asset.symbol || '?'} (precision ${asset.precision})`);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
const missing = ids.filter(id => !(id in resolvedPrecisions));
|
|
267
|
-
if (missing.length > 0) {
|
|
268
|
-
console.warn(` [warn] ${missing.length} asset(s) not found on chain: ${missing.join(', ')}. Fills referencing them will be skipped.`);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
catch (e) {
|
|
272
|
-
console.warn(` [warn] Asset resolution failed: ${e.message}. Fills with unknown assets will be skipped.`);
|
|
273
|
-
}
|
|
274
|
-
finally {
|
|
275
|
-
try {
|
|
276
|
-
client.disconnect();
|
|
277
|
-
}
|
|
278
|
-
catch (_) { }
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
// ─── Kibana Query ────────────────────────────────────────────────────────────
|
|
282
|
-
function buildFillQuery(accountId, gte, lte, size) {
|
|
283
|
-
return {
|
|
284
|
-
size,
|
|
285
|
-
track_total_hits: false,
|
|
286
|
-
_source: [
|
|
287
|
-
'block_data.block_time',
|
|
288
|
-
'block_data.block_num',
|
|
289
|
-
'operation_id_num',
|
|
290
|
-
'operation_history.op_object.pays',
|
|
291
|
-
'operation_history.op_object.receives',
|
|
292
|
-
'operation_history.op_object.fee',
|
|
293
|
-
'operation_history.op_object.order_id',
|
|
294
|
-
'operation_history.op_object.account_id',
|
|
295
|
-
'operation_history.op_object.is_maker',
|
|
296
|
-
],
|
|
297
|
-
query: {
|
|
298
|
-
bool: {
|
|
299
|
-
filter: [
|
|
300
|
-
{ term: { operation_type: OP_FILL_ORDER } },
|
|
301
|
-
{ term: { 'operation_history.op_object.account_id.keyword': accountId } },
|
|
302
|
-
{ range: { 'block_data.block_time': { gte, lte } } },
|
|
303
|
-
],
|
|
304
|
-
},
|
|
305
|
-
},
|
|
306
|
-
sort: [
|
|
307
|
-
{ 'block_data.block_time': { order: 'asc' } },
|
|
308
|
-
{ operation_id_num: { order: 'asc' } },
|
|
309
|
-
],
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
async function fetchAllFills(config, accountId, gte, lte) {
|
|
313
|
-
const pageSize = 10000;
|
|
314
|
-
const fills = [];
|
|
315
|
-
let searchAfter = null;
|
|
316
|
-
const cfg = { ...BASE_CONFIG, timeout: 60000, ...config };
|
|
317
|
-
while (true) {
|
|
318
|
-
const query = buildFillQuery(accountId, gte, lte, pageSize);
|
|
319
|
-
if (searchAfter)
|
|
320
|
-
query.search_after = searchAfter;
|
|
321
|
-
const result = await kibanaSearch(cfg, query);
|
|
322
|
-
const hits = result?.hits?.hits ?? [];
|
|
323
|
-
if (!hits.length)
|
|
324
|
-
break;
|
|
325
|
-
for (const hit of hits) {
|
|
326
|
-
const src = hit?._source;
|
|
327
|
-
const op = src?.operation_history?.op_object;
|
|
328
|
-
if (!op || !op.pays || !op.receives)
|
|
329
|
-
continue;
|
|
330
|
-
fills.push({
|
|
331
|
-
time: src.block_data?.block_time ?? '',
|
|
332
|
-
blockNum: src.block_data?.block_num ?? 0,
|
|
333
|
-
opNum: Number(src.operation_id_num ?? 0),
|
|
334
|
-
orderId: op.order_id ?? '',
|
|
335
|
-
accountId: op.account_id ?? '',
|
|
336
|
-
pays: { amount: Number(op.pays.amount ?? 0), asset_id: op.pays.asset_id ?? '' },
|
|
337
|
-
receives: { amount: Number(op.receives.amount ?? 0), asset_id: op.receives.asset_id ?? '' },
|
|
338
|
-
fee: { amount: Number(op.fee?.amount ?? 0), asset_id: op.fee?.asset_id ?? '' },
|
|
339
|
-
isMaker: op.is_maker ?? false,
|
|
340
|
-
sort: hit.sort,
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
if (hits.length < pageSize)
|
|
344
|
-
break;
|
|
345
|
-
searchAfter = hits[hits.length - 1].sort;
|
|
346
|
-
if (!Array.isArray(searchAfter))
|
|
347
|
-
break;
|
|
348
|
-
}
|
|
349
|
-
return fills;
|
|
350
|
-
}
|
|
351
135
|
// ─── Fill Classification ─────────────────────────────────────────────────────
|
|
352
136
|
/**
|
|
353
137
|
* A fill needs strictly positive, finite amounts on both legs. A zero base
|
|
@@ -742,7 +526,7 @@ function percentile(sorted, p) {
|
|
|
742
526
|
return sorted[f];
|
|
743
527
|
return sorted[f] * (c - k) + sorted[c] * (k - f);
|
|
744
528
|
}
|
|
745
|
-
function computeMetrics(pair) {
|
|
529
|
+
function computeMetrics(pair, window) {
|
|
746
530
|
const pnls = pair.realizedPnls;
|
|
747
531
|
const total = pnls.length;
|
|
748
532
|
if (total === 0) {
|
|
@@ -751,11 +535,15 @@ function computeMetrics(pair) {
|
|
|
751
535
|
avgWin: 0, avgLoss: 0, avgWinLossRatio: 0,
|
|
752
536
|
expectancyBts: 0, expectancyPct: 0, expectancyR: 0,
|
|
753
537
|
netExpectancyBts: 0,
|
|
754
|
-
|
|
538
|
+
sharpeAnn: NaN, sortinoAnn: NaN, sharpeAnnSE: NaN,
|
|
539
|
+
periodLabel: '—', periodCount: 0, periodSpanDays: 0, scoredSpanDays: 0,
|
|
540
|
+
annualFactor: 0,
|
|
541
|
+
projectedNetPnlPerDay: 0, projectedNetPnlAnn: 0,
|
|
755
542
|
maxConsecWins: 0, maxConsecLosses: 0,
|
|
756
543
|
avgHoldHours: 0, limitOrderRatio: 0,
|
|
757
544
|
bestTradePct: 0, worstTradePct: 0,
|
|
758
545
|
mddPct: 0,
|
|
546
|
+
mddAbsBts: 0,
|
|
759
547
|
mddHadStablePeak: false,
|
|
760
548
|
isOngoingRecovery: false,
|
|
761
549
|
currentDrawdownDays: 0,
|
|
@@ -789,27 +577,87 @@ function computeMetrics(pair) {
|
|
|
789
577
|
? winRate * (avgWin / rMultiple) - (1 - winRate)
|
|
790
578
|
: Infinity;
|
|
791
579
|
const netExpectancyBts = pair.totalRealizedPnlNet / total;
|
|
792
|
-
//
|
|
793
|
-
|
|
580
|
+
// ─── Risk-adjusted ratios (window-aware annualisation) ────────────────
|
|
581
|
+
// Bin the window's net PnL into calendar periods and annualise with the
|
|
582
|
+
// matching periods-per-year. Every period in the window is represented
|
|
583
|
+
// (zero-filled): flat periods must count as 0 PnL, otherwise a bot that
|
|
584
|
+
// trades a few days a week is scored as if it traded every day, and the
|
|
585
|
+
// window cannot be scaled honestly to a year.
|
|
586
|
+
const HOUR_MS = 3600_000;
|
|
587
|
+
const DAY_MS = 86_400_000;
|
|
588
|
+
const times = pnls.map(r => Date.parse(r.exitTime)).filter(t => Number.isFinite(t));
|
|
589
|
+
const firstMs = window?.startMs ?? (times.length > 0 ? Math.min(...times) : 0);
|
|
590
|
+
const lastMs = window?.endMs ?? (times.length > 0 ? Math.max(...times) : firstMs + DAY_MS);
|
|
591
|
+
const spanMs = Math.max(lastMs - firstMs, window ? HOUR_MS : DAY_MS);
|
|
592
|
+
const periodSpanDays = spanMs / DAY_MS;
|
|
593
|
+
// Daily buckets need a handful of observations before a daily std means
|
|
594
|
+
// anything; shorter windows fall back to hourly bins so they still
|
|
595
|
+
// annualise instead of dividing by a one-sample std.
|
|
596
|
+
const useHourly = periodSpanDays < 3;
|
|
597
|
+
const periodMs = useHourly ? HOUR_MS : DAY_MS;
|
|
598
|
+
const periodsPerYear = useHourly ? 8760 : 365;
|
|
599
|
+
const periodLabel = useHourly ? '1h' : '1d';
|
|
600
|
+
// Score only whole periods: a trailing partial period (window not an exact
|
|
601
|
+
// multiple of the bin) would otherwise be a deflated observation. It is
|
|
602
|
+
// excluded from ratios, projection AND activity rates alike, so every
|
|
603
|
+
// window-derived metric shares one basis. Bins are aligned to the window
|
|
604
|
+
// start, so an exact-multiple window scores everything.
|
|
605
|
+
const nPeriods = Math.max(1, Math.floor(spanMs / periodMs));
|
|
606
|
+
const scoredSpanDays = nPeriods * periodMs / DAY_MS;
|
|
607
|
+
const scoredEndMs = firstMs + nPeriods * periodMs;
|
|
608
|
+
const exactWindow = spanMs === nPeriods * periodMs;
|
|
609
|
+
// A fill belongs to the scored window if it falls inside the whole
|
|
610
|
+
// periods. On an exact-multiple window the end boundary is inclusive (the
|
|
611
|
+
// ES range query includes `lte`); with a partial tail that boundary fill
|
|
612
|
+
// is trailing and excluded along with the rest of the tail.
|
|
613
|
+
const inScoredWindow = (t) => Number.isFinite(t) && t >= firstMs
|
|
614
|
+
&& (t < scoredEndMs || (exactWindow && t === scoredEndMs));
|
|
615
|
+
const periodPnl = new Array(nPeriods).fill(0);
|
|
616
|
+
let scoredFillCount = 0;
|
|
794
617
|
for (const r of pnls) {
|
|
795
|
-
const
|
|
796
|
-
|
|
618
|
+
const t = Date.parse(r.exitTime);
|
|
619
|
+
if (!inScoredWindow(t))
|
|
620
|
+
continue;
|
|
621
|
+
let idx = Math.floor((t - firstMs) / periodMs);
|
|
622
|
+
if (idx === nPeriods)
|
|
623
|
+
idx = nPeriods - 1; // inclusive end boundary
|
|
624
|
+
periodPnl[idx] += r.pnlNet;
|
|
625
|
+
scoredFillCount++;
|
|
797
626
|
}
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
const
|
|
801
|
-
|
|
802
|
-
? dailyRets.reduce((s, v) => s + (v - meanDailyRet) ** 2, 0) / nDays
|
|
627
|
+
const meanPeriod = periodPnl.reduce((s, v) => s + v, 0) / nPeriods;
|
|
628
|
+
// Sample variance (n-1): the window is a sample, not the whole population.
|
|
629
|
+
const periodVar = nPeriods > 1
|
|
630
|
+
? periodPnl.reduce((s, v) => s + (v - meanPeriod) ** 2, 0) / (nPeriods - 1)
|
|
803
631
|
: 0;
|
|
804
|
-
const
|
|
805
|
-
const
|
|
806
|
-
const
|
|
807
|
-
//
|
|
808
|
-
const
|
|
809
|
-
|
|
632
|
+
const periodStd = Math.sqrt(periodVar);
|
|
633
|
+
const annualFactor = Math.sqrt(periodsPerYear);
|
|
634
|
+
const sharpeAnn = periodStd > 0 ? (meanPeriod / periodStd) * annualFactor : NaN;
|
|
635
|
+
// Estimation uncertainty of the annualised Sharpe (Lo 2002, i.i.d. returns).
|
|
636
|
+
const srPerPeriod = periodStd > 0 ? meanPeriod / periodStd : NaN;
|
|
637
|
+
const sharpeAnnSE = nPeriods > 1 && Number.isFinite(srPerPeriod)
|
|
638
|
+
? Math.sqrt((1 + 0.5 * srPerPeriod * srPerPeriod) / nPeriods) * annualFactor
|
|
639
|
+
: NaN;
|
|
640
|
+
// Target downside deviation (MAR = 0), sample denominator (n-1) to match
|
|
641
|
+
// the Sharpe convention above — the two ratios stay internally
|
|
642
|
+
// comparable (many textbook Sortinos divide by N; we deliberately
|
|
643
|
+
// don't mix conventions). With no losing periods Sortino is undefined —
|
|
644
|
+
// reporting 0 would read as "terrible", the opposite of truth.
|
|
645
|
+
const downsideVar = nPeriods > 1
|
|
646
|
+
? periodPnl.reduce((s, v) => s + (v < 0 ? v * v : 0), 0) / (nPeriods - 1)
|
|
810
647
|
: 0;
|
|
811
648
|
const downsideStd = Math.sqrt(downsideVar);
|
|
812
|
-
const
|
|
649
|
+
const sortinoAnn = nPeriods < 2
|
|
650
|
+
? NaN
|
|
651
|
+
: (downsideStd > 0
|
|
652
|
+
? (meanPeriod / downsideStd) * annualFactor
|
|
653
|
+
: (meanPeriod > 0 ? Infinity : NaN));
|
|
654
|
+
// "This window repeated all year" — linear projection of the scored
|
|
655
|
+
// window's net PnL. Uses the same whole-period basis as the ratios
|
|
656
|
+
// above (not the raw window total), so Sharpe and projection can never
|
|
657
|
+
// disagree about what the window contains.
|
|
658
|
+
const scoredPnl = periodPnl.reduce((s, v) => s + v, 0);
|
|
659
|
+
const projectedNetPnlPerDay = scoredPnl / scoredSpanDays;
|
|
660
|
+
const projectedNetPnlAnn = projectedNetPnlPerDay * 365;
|
|
813
661
|
// Fills-per-order distribution (grouped by sell order)
|
|
814
662
|
const fillCounts = [];
|
|
815
663
|
const orderMap = new Map();
|
|
@@ -830,8 +678,13 @@ function computeMetrics(pair) {
|
|
|
830
678
|
const oneShotOrderRatio = sellOrdersFilled > 0
|
|
831
679
|
? fillCounts.filter(c => c === 1).length / sellOrdersFilled
|
|
832
680
|
: 0;
|
|
833
|
-
|
|
834
|
-
|
|
681
|
+
// Activity rates share the scored whole-period basis: numerator and
|
|
682
|
+
// denominator both cover exactly the lots/fills the ratios scored. A fill
|
|
683
|
+
// with an unparseable timestamp has no period, so it is excluded here too.
|
|
684
|
+
const scoredNotional = [...pair.buys, ...pair.sells]
|
|
685
|
+
.reduce((s, f) => s + (inScoredWindow(Date.parse(f.time)) ? f.quoteAmount : 0), 0);
|
|
686
|
+
const fillsPerDay = scoredSpanDays > 0 ? scoredFillCount / scoredSpanDays : 0;
|
|
687
|
+
const avgVolumePerDay = scoredSpanDays > 0 ? scoredNotional / scoredSpanDays : 0;
|
|
835
688
|
// Avg hold duration
|
|
836
689
|
let totalHours = 0;
|
|
837
690
|
let hourCount = 0;
|
|
@@ -878,7 +731,7 @@ function computeMetrics(pair) {
|
|
|
878
731
|
const STABILITY_TRADES = 3;
|
|
879
732
|
const MIN_TRADES_FOR_PEAK = 10; // backstop for monotonic equity curves
|
|
880
733
|
const chronological = [...pnls].sort((a, b) => new Date(a.exitTime).getTime() - new Date(b.exitTime).getTime());
|
|
881
|
-
let equity = 0, peak = 0, mddPct = 0;
|
|
734
|
+
let equity = 0, peak = 0, mddPct = 0, mddAbsBts = 0;
|
|
882
735
|
let maxRecoveryDays = 0;
|
|
883
736
|
let isOngoingRecovery = false;
|
|
884
737
|
let currentDrawdownDays = 0;
|
|
@@ -924,6 +777,9 @@ function computeMetrics(pair) {
|
|
|
924
777
|
const dd = (equity - peak) / peak;
|
|
925
778
|
if (dd < mddPct)
|
|
926
779
|
mddPct = dd;
|
|
780
|
+
const ddAbs = peak - equity;
|
|
781
|
+
if (ddAbs > mddAbsBts)
|
|
782
|
+
mddAbsBts = ddAbs;
|
|
927
783
|
}
|
|
928
784
|
if (peak > 0 && !hadStablePeak) {
|
|
929
785
|
if (!hasPrePeakEquity || equity < prePeakMinEquity) {
|
|
@@ -945,6 +801,7 @@ function computeMetrics(pair) {
|
|
|
945
801
|
}
|
|
946
802
|
else {
|
|
947
803
|
mddPct = hasPrePeakEquity ? prePeakMinEquity : 0;
|
|
804
|
+
mddAbsBts = 0;
|
|
948
805
|
}
|
|
949
806
|
// Payoff distribution stats
|
|
950
807
|
const pnlPcts = [...pnls.map(r => r.pnlPct)].sort((a, b) => a - b);
|
|
@@ -973,8 +830,16 @@ function computeMetrics(pair) {
|
|
|
973
830
|
expectancyPct,
|
|
974
831
|
expectancyR,
|
|
975
832
|
netExpectancyBts,
|
|
976
|
-
|
|
977
|
-
|
|
833
|
+
sharpeAnn,
|
|
834
|
+
sortinoAnn,
|
|
835
|
+
sharpeAnnSE,
|
|
836
|
+
periodLabel,
|
|
837
|
+
periodCount: nPeriods,
|
|
838
|
+
periodSpanDays,
|
|
839
|
+
scoredSpanDays,
|
|
840
|
+
annualFactor,
|
|
841
|
+
projectedNetPnlPerDay,
|
|
842
|
+
projectedNetPnlAnn,
|
|
978
843
|
feeDragPct,
|
|
979
844
|
maxConsecWins: maxW,
|
|
980
845
|
maxConsecLosses: maxL,
|
|
@@ -983,6 +848,7 @@ function computeMetrics(pair) {
|
|
|
983
848
|
bestTradePct,
|
|
984
849
|
worstTradePct,
|
|
985
850
|
mddPct,
|
|
851
|
+
mddAbsBts,
|
|
986
852
|
mddHadStablePeak: hadStablePeak,
|
|
987
853
|
isOngoingRecovery,
|
|
988
854
|
currentDrawdownDays,
|
|
@@ -1003,11 +869,11 @@ function computeMetrics(pair) {
|
|
|
1003
869
|
avgVolumePerDay,
|
|
1004
870
|
};
|
|
1005
871
|
}
|
|
1006
|
-
function printMetrics(pairs) {
|
|
872
|
+
function printMetrics(pairs, window) {
|
|
1007
873
|
for (const pair of pairs) {
|
|
1008
874
|
if (pair.realizedPnls.length === 0)
|
|
1009
875
|
continue;
|
|
1010
|
-
const m = computeMetrics(pair);
|
|
876
|
+
const m = computeMetrics(pair, window);
|
|
1011
877
|
const pairLabel = `${fmtAsset(pair.baseAsset)}/${fmtAsset(pair.quoteAsset)}`;
|
|
1012
878
|
console.log('');
|
|
1013
879
|
console.log(` ── ${pairLabel} — Performance Metrics`);
|
|
@@ -1035,13 +901,24 @@ function printMetrics(pairs) {
|
|
|
1035
901
|
console.log(` P25 / P75: ${fmtPct(m.p25PnlPct)} / ${fmtPct(m.p75PnlPct)}`);
|
|
1036
902
|
console.log(` Best / Worst Trade: ${fmtPct(m.bestTradePct)} / ${fmtPct(m.worstTradePct)}`);
|
|
1037
903
|
console.log('');
|
|
1038
|
-
// Risk-adjusted
|
|
1039
|
-
|
|
1040
|
-
|
|
904
|
+
// Risk-adjusted — annualised from the analysis window, zero-filled bins
|
|
905
|
+
const ratioStr = (v) => Number.isFinite(v) ? v.toFixed(2) : (v === Infinity ? '∞' : 'n/a');
|
|
906
|
+
const seStr = Number.isFinite(m.sharpeAnnSE) ? ` ± ${m.sharpeAnnSE.toFixed(2)}` : '';
|
|
907
|
+
const confidence = m.periodSpanDays < 30 ? 'low confidence' : 'ok';
|
|
908
|
+
console.log(` Sharpe (ann): ${ratioStr(m.sharpeAnn)}${seStr} [${m.periodLabel} bins, n=${m.periodCount}, ${confidence}]`);
|
|
909
|
+
const sortinoNote = m.sortinoAnn === Infinity ? ' (no losing periods)' : '';
|
|
910
|
+
console.log(` Sortino (ann): ${ratioStr(m.sortinoAnn)}${sortinoNote}`);
|
|
911
|
+
if (m.periodLabel === '1h') {
|
|
912
|
+
console.log(` ⚠ window < 3 days: hourly bins over an unrepresentative sample — treat the annualised ratios as indicative only and never rank them against 1d-binned runs`);
|
|
913
|
+
}
|
|
914
|
+
const spanStr = m.scoredSpanDays < m.periodSpanDays - 1e-9
|
|
915
|
+
? `${m.scoredSpanDays.toFixed(1)}d scored of ${m.periodSpanDays.toFixed(1)}d window`
|
|
916
|
+
: `${m.periodSpanDays.toFixed(1)}d`;
|
|
917
|
+
console.log(` Projected net PnL: ${fmt(m.projectedNetPnlAnn, 2)} ${qSymbol}/yr (${fmt(m.projectedNetPnlPerDay, 2)}/day over ${spanStr})`);
|
|
1041
918
|
console.log('');
|
|
1042
919
|
// Tail risk
|
|
1043
920
|
if (m.mddHadStablePeak) {
|
|
1044
|
-
console.log(` Max Drawdown: ${fmtPct(m.mddPct)}`);
|
|
921
|
+
console.log(` Max Drawdown: ${fmt(m.mddAbsBts, 4)} ${qSymbol} (${fmtPct(m.mddPct)} of peak cumulative profit)`);
|
|
1045
922
|
}
|
|
1046
923
|
else {
|
|
1047
924
|
console.log(` Min Equity: ${fmt(m.mddPct, 4)} ${fmtAsset(pair.quoteAsset)}`);
|
|
@@ -1176,38 +1053,17 @@ async function run() {
|
|
|
1176
1053
|
const opts = parseArgs();
|
|
1177
1054
|
let accountId = opts.accountId;
|
|
1178
1055
|
if (!/^1\.2\.\d+$/.test(String(accountId))) {
|
|
1179
|
-
//
|
|
1180
|
-
//
|
|
1181
|
-
//
|
|
1182
|
-
//
|
|
1183
|
-
//
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
if (match) {
|
|
1189
|
-
matchedKey = match.botKey;
|
|
1190
|
-
stored = getStoredBotAccountId(match.botKey, accountId);
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
catch (_) {
|
|
1194
|
-
// bots.json issues must never break resolution; fall through to chain.
|
|
1195
|
-
}
|
|
1196
|
-
if (stored && !opts.refreshAccount) {
|
|
1197
|
-
console.log(` Using stored accountId ${stored} from profiles/bots.json (no lookup needed; pass --refresh-account to re-verify)`);
|
|
1198
|
-
accountId = stored;
|
|
1199
|
-
}
|
|
1200
|
-
else {
|
|
1201
|
-
const resolved = await resolveAccountId(accountId, opts.node);
|
|
1202
|
-
if (!resolved) {
|
|
1203
|
-
console.error(` Could not resolve "${accountId}" to an account ID`);
|
|
1204
|
-
process.exit(1);
|
|
1205
|
-
}
|
|
1206
|
-
accountId = resolved;
|
|
1207
|
-
if (matchedKey && persistBotAccountId(matchedKey, resolved)) {
|
|
1208
|
-
console.log(` Stored accountId ${resolved} in profiles/bots.json`);
|
|
1209
|
-
}
|
|
1056
|
+
// The Kibana query below filters on the 1.2.x account_id field, so a
|
|
1057
|
+
// name must always resolve first (a raw name would silently return
|
|
1058
|
+
// zero fills). Shared resolver: reuses a stored accountId from
|
|
1059
|
+
// profiles/bots.json when one matches the name and stamps the result
|
|
1060
|
+
// back onto the bot entry on a fresh lookup.
|
|
1061
|
+
const resolved = await resolveAccountRef(accountId, { refresh: opts.refreshAccount });
|
|
1062
|
+
if (!resolved.accountId) {
|
|
1063
|
+
console.error(` Could not resolve "${accountId}" to an account ID`);
|
|
1064
|
+
process.exit(1);
|
|
1210
1065
|
}
|
|
1066
|
+
accountId = resolved.accountId;
|
|
1211
1067
|
}
|
|
1212
1068
|
// Build time range
|
|
1213
1069
|
const now = new Date();
|
|
@@ -1233,7 +1089,7 @@ async function run() {
|
|
|
1233
1089
|
process.exit(0);
|
|
1234
1090
|
}
|
|
1235
1091
|
// Resolve unknown asset precisions from blockchain
|
|
1236
|
-
await resolveAssetPrecisions(fills
|
|
1092
|
+
await resolveAssetPrecisions(fills);
|
|
1237
1093
|
// Classify fills
|
|
1238
1094
|
const { trades, pairs } = classifyFills(fills, opts.asset);
|
|
1239
1095
|
if (trades.length === 0) {
|
|
@@ -1281,7 +1137,7 @@ async function run() {
|
|
|
1281
1137
|
if (opts.showPnlDetail) {
|
|
1282
1138
|
printPnlDetail(analyses);
|
|
1283
1139
|
}
|
|
1284
|
-
printMetrics(analyses);
|
|
1140
|
+
printMetrics(analyses, { startMs: Date.parse(gte), endMs: Date.parse(lte) });
|
|
1285
1141
|
if (opts.csv) {
|
|
1286
1142
|
exportCsv(analyses, opts.csv);
|
|
1287
1143
|
}
|