dexbot 1.6.3 → 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 +38 -2
- package/README.md +11 -7
- package/analysis/README.md +18 -12
- 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 +38 -251
- package/analysis/trade_profitability.ts +169 -304
- package/analysis/tradingview/README.md +2 -2
- 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 +1 -21
- package/dist/analysis/grid_correction_check.d.ts.map +1 -1
- package/dist/analysis/grid_correction_check.js +30 -257
- 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 +8 -7
- 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 +7 -6
- package/dist/modules/dexbot_cow_runtime.d.ts.map +1 -1
- package/dist/modules/dexbot_cow_runtime.js +8 -7
- 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 +15 -4
- 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 +17 -6
- 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 +8 -15
- 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/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 -30
- 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
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
import { resolveAccountRef } from './account_resolver.js';
|
|
7
|
+
import {
|
|
8
|
+
BTS_ID,
|
|
9
|
+
assetPrec as getPrec,
|
|
10
|
+
assetSymbol,
|
|
11
|
+
fetchAllFills,
|
|
12
|
+
resolveAssetPrecisions,
|
|
13
|
+
toReal,
|
|
14
|
+
FillRecord,
|
|
15
|
+
} from './fills_source.js';
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* TRADE PROFITABILITY ANALYZER
|
|
@@ -30,99 +35,10 @@ const { kibanaSearch, DEFAULT_CONFIG: BASE_CONFIG } = KC;
|
|
|
30
35
|
|
|
31
36
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
32
37
|
|
|
33
|
-
const OP_FILL_ORDER = 4;
|
|
34
|
-
const BTS_ID = '1.3.0';
|
|
35
38
|
let BLOCKCHAIN_FEE_PER_FILL = 0.09652; // BTS — flat blockchain operation fee (not market fee); override with --fee-per-order
|
|
36
39
|
|
|
37
|
-
interface AssetInfo {
|
|
38
|
-
symbol: string;
|
|
39
|
-
precision: number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const ASSETS: Record<string, AssetInfo> = {
|
|
43
|
-
'1.3.0': { symbol: 'BTS', precision: 5 },
|
|
44
|
-
'1.3.118': { symbol: 'GBP', precision: 4 },
|
|
45
|
-
'1.3.119': { symbol: 'JPY', precision: 2 },
|
|
46
|
-
'1.3.120': { symbol: 'EUR', precision: 4 },
|
|
47
|
-
'1.3.1325': { symbol: 'RUBLE', precision: 5 },
|
|
48
|
-
'1.3.2512': { symbol: 'EVRAZ', precision: 4 },
|
|
49
|
-
'1.3.3291': { symbol: 'TWENTIX', precision: 5 },
|
|
50
|
-
'1.3.4099': { symbol: 'XBTSX.STH', precision: 6 },
|
|
51
|
-
'1.3.4156': { symbol: 'XBTSX.DOGE', precision: 5 },
|
|
52
|
-
'1.3.4157': { symbol: 'XBTSX.BTC', precision: 8 },
|
|
53
|
-
'1.3.4159': { symbol: 'XBTSX.LTC', precision: 8 },
|
|
54
|
-
'1.3.4176': { symbol: 'XBTSX.DASH', precision: 8 },
|
|
55
|
-
'1.3.4274': { symbol: 'XBTSX.BCH', precision: 8 },
|
|
56
|
-
'1.3.4760': { symbol: 'XBTSX.ETH', precision: 7 },
|
|
57
|
-
'1.3.5537': { symbol: 'IOB.XRP', precision: 4 },
|
|
58
|
-
'1.3.5541': { symbol: 'XBTSX.BNB', precision: 7 },
|
|
59
|
-
'1.3.5589': { symbol: 'XBTSX.USDT', precision: 6 },
|
|
60
|
-
'1.3.5641': { symbol: 'HONEST.CNY', precision: 4 },
|
|
61
|
-
'1.3.5649': { symbol: 'HONEST.USD', precision: 4 },
|
|
62
|
-
'1.3.5650': { symbol: 'HONEST.BTC', precision: 8 },
|
|
63
|
-
'1.3.5659': { symbol: 'HONEST.ETH', precision: 6 },
|
|
64
|
-
'1.3.5870': { symbol: 'XBTSX.FIL', precision: 6 },
|
|
65
|
-
'1.3.5887': { symbol: 'XBTSX.RUB', precision: 4 },
|
|
66
|
-
'1.3.5902': { symbol: 'XBTSX.USDC', precision: 6 },
|
|
67
|
-
'1.3.6013': { symbol: 'XBTSX.HIVE', precision: 6 },
|
|
68
|
-
'1.3.6124': { symbol: 'XBTSX.AVAX', precision: 6 },
|
|
69
|
-
'1.3.6139': { symbol: 'XBTSX.XAUT', precision: 6 },
|
|
70
|
-
'1.3.6166': { symbol: 'XBTSX.MATIC', precision: 5 },
|
|
71
|
-
'1.3.6241': { symbol: 'XBTSX.ETC', precision: 7 },
|
|
72
|
-
'1.3.6268': { symbol: 'BTWTY.EOS', precision: 4 },
|
|
73
|
-
'1.3.6301': { symbol: 'HONEST.MONEY', precision: 8 },
|
|
74
|
-
'1.3.6304': { symbol: 'HONEST.ADA', precision: 8 },
|
|
75
|
-
'1.3.6305': { symbol: 'HONEST.DOT', precision: 8 },
|
|
76
|
-
'1.3.6309': { symbol: 'HONEST.ATOM', precision: 8 },
|
|
77
|
-
'1.3.6311': { symbol: 'HONEST.ALGO', precision: 8 },
|
|
78
|
-
'1.3.6312': { symbol: 'HONEST.FIL', precision: 8 },
|
|
79
|
-
'1.3.6313': { symbol: 'HONEST.EOS', precision: 8 },
|
|
80
|
-
'1.3.6315': { symbol: 'HONEST.EUR', precision: 4 },
|
|
81
|
-
'1.3.6316': { symbol: 'HONEST.GBP', precision: 4 },
|
|
82
|
-
'1.3.6317': { symbol: 'HONEST.JPY', precision: 4 },
|
|
83
|
-
'1.3.6444': { symbol: 'IOB.XLM', precision: 4 },
|
|
84
|
-
'1.3.6573': { symbol: 'XBTSX.DAI', precision: 6 },
|
|
85
|
-
'1.3.6620': { symbol: 'XBTSX.A', precision: 6 },
|
|
86
|
-
'1.3.6627': { symbol: 'XBTSX.LINK', precision: 6 },
|
|
87
|
-
};
|
|
88
|
-
const resolvedPrecisions: Record<string, number> = {};
|
|
89
|
-
|
|
90
|
-
function assetSymbol(id: string): string {
|
|
91
|
-
return ASSETS[id]?.symbol ?? id;
|
|
92
|
-
}
|
|
93
|
-
function assetPrec(id: string): number | undefined {
|
|
94
|
-
return ASSETS[id]?.precision ?? resolvedPrecisions[id];
|
|
95
|
-
}
|
|
96
|
-
function getPrec(id: string): number | undefined {
|
|
97
|
-
return assetPrec(id);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function toReal(amount: number, assetId: string): number {
|
|
101
|
-
const p = getPrec(assetId);
|
|
102
|
-
if (p === undefined) return NaN;
|
|
103
|
-
return amount / Math.pow(10, p);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
40
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
107
41
|
|
|
108
|
-
interface AssetAmount {
|
|
109
|
-
amount: number;
|
|
110
|
-
asset_id: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
interface FillRecord {
|
|
114
|
-
time: string;
|
|
115
|
-
blockNum: number;
|
|
116
|
-
opNum: number;
|
|
117
|
-
orderId: string;
|
|
118
|
-
accountId: string;
|
|
119
|
-
pays: AssetAmount;
|
|
120
|
-
receives: AssetAmount;
|
|
121
|
-
fee: AssetAmount;
|
|
122
|
-
isMaker: boolean;
|
|
123
|
-
sort: any[];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
42
|
interface TradeFill {
|
|
127
43
|
time: string;
|
|
128
44
|
orderId: string;
|
|
@@ -204,7 +120,6 @@ Options:
|
|
|
204
120
|
--asset <assetId> Filter to one base asset (e.g. 1.3.113 for bitUSD)
|
|
205
121
|
--lookup Legacy (no-op): account names always resolve automatically
|
|
206
122
|
--refresh-account Force re-resolution and update the stored accountId
|
|
207
|
-
--node <url> BitShares node URL (default: first healthy from built-in pool)
|
|
208
123
|
--csv <file> Export trade list as CSV
|
|
209
124
|
--json <file> Export full analysis as JSON
|
|
210
125
|
--trades Show per-order PnL detail (hidden by default)
|
|
@@ -236,7 +151,6 @@ function parseArgs() {
|
|
|
236
151
|
asset: null,
|
|
237
152
|
lookup: false,
|
|
238
153
|
refreshAccount: false,
|
|
239
|
-
node: C.NODE_MANAGEMENT.DEFAULT_NODES[0],
|
|
240
154
|
csv: null,
|
|
241
155
|
json: null,
|
|
242
156
|
matchMode: 'sequential',
|
|
@@ -253,7 +167,6 @@ function parseArgs() {
|
|
|
253
167
|
case '--asset': opts.asset = args[++i]; break;
|
|
254
168
|
case '--lookup': opts.lookup = true; break;
|
|
255
169
|
case '--refresh-account': opts.refreshAccount = true; break;
|
|
256
|
-
case '--node': opts.node = args[++i]; break;
|
|
257
170
|
case '--csv': opts.csv = args[++i]; break;
|
|
258
171
|
case '--json': opts.json = args[++i]; break;
|
|
259
172
|
case '--trades': opts.showPnlDetail = true; break;
|
|
@@ -281,150 +194,6 @@ function parseArgs() {
|
|
|
281
194
|
return opts;
|
|
282
195
|
}
|
|
283
196
|
|
|
284
|
-
// ─── Account name resolution ─────────────────────────────────────────────────
|
|
285
|
-
|
|
286
|
-
async function resolveAccountId(name: string, nodeUrl: string): Promise<string | null> {
|
|
287
|
-
const { createReadOnlyClient } = await import('../modules/bitshares-native/index.js');
|
|
288
|
-
const client = createReadOnlyClient({ nodes: [nodeUrl] });
|
|
289
|
-
// Suppress transport INFO logs during ephemeral connection:
|
|
290
|
-
// bitshares-native transport logger (new Logger('Transport')) writes
|
|
291
|
-
// "[timestamp] [INFO] [Transport] ..." — silence by raising log level.
|
|
292
|
-
const prevLevel = process.env.LOG_LEVEL;
|
|
293
|
-
process.env.LOG_LEVEL = 'warn';
|
|
294
|
-
try {
|
|
295
|
-
await client.connect();
|
|
296
|
-
const accounts = await client.db('lookup_account_names', [[name]]);
|
|
297
|
-
if (Array.isArray(accounts) && accounts[0]?.id) {
|
|
298
|
-
return accounts[0].id;
|
|
299
|
-
}
|
|
300
|
-
return null;
|
|
301
|
-
} catch (e: any) {
|
|
302
|
-
console.warn(` [warn] Account resolution failed: ${e.message}`);
|
|
303
|
-
return null;
|
|
304
|
-
} finally {
|
|
305
|
-
try { client.disconnect(); } catch (_) {}
|
|
306
|
-
process.env.LOG_LEVEL = prevLevel;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ─── On-chain asset precision resolution ────────────────────────────────────
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Collects all unique non-BTS asset IDs from fills, resolves unknown
|
|
314
|
-
* precisions from the blockchain, and populates the runtime cache.
|
|
315
|
-
*/
|
|
316
|
-
async function resolveAssetPrecisions(fills: FillRecord[], nodeUrl: string | null): Promise<void> {
|
|
317
|
-
const unknownIds = new Set<string>();
|
|
318
|
-
for (const f of fills) {
|
|
319
|
-
for (const id of [f.pays.asset_id, f.receives.asset_id, f.fee.asset_id]) {
|
|
320
|
-
if (id !== BTS_ID && !(id in ASSETS) && !(id in resolvedPrecisions)) {
|
|
321
|
-
unknownIds.add(id);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
if (unknownIds.size === 0 || !nodeUrl) return;
|
|
326
|
-
|
|
327
|
-
const ids = [...unknownIds];
|
|
328
|
-
console.log(` Resolving ${ids.length} unknown asset(s) from blockchain...`);
|
|
329
|
-
|
|
330
|
-
const { createReadOnlyClient } = await import('../modules/bitshares-native/index.js');
|
|
331
|
-
const client = createReadOnlyClient({ nodes: [nodeUrl] });
|
|
332
|
-
try {
|
|
333
|
-
await client.connect();
|
|
334
|
-
const assets = await client.db('get_assets', [ids]);
|
|
335
|
-
if (Array.isArray(assets)) {
|
|
336
|
-
for (const asset of assets) {
|
|
337
|
-
if (asset?.id && asset.precision != null) {
|
|
338
|
-
resolvedPrecisions[asset.id] = asset.precision;
|
|
339
|
-
console.log(` ${asset.id} → ${asset.symbol || '?'} (precision ${asset.precision})`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
const missing = ids.filter(id => !(id in resolvedPrecisions));
|
|
344
|
-
if (missing.length > 0) {
|
|
345
|
-
console.warn(` [warn] ${missing.length} asset(s) not found on chain: ${missing.join(', ')}. Fills referencing them will be skipped.`);
|
|
346
|
-
}
|
|
347
|
-
} catch (e: any) {
|
|
348
|
-
console.warn(` [warn] Asset resolution failed: ${e.message}. Fills with unknown assets will be skipped.`);
|
|
349
|
-
} finally {
|
|
350
|
-
try { client.disconnect(); } catch (_) {}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// ─── Kibana Query ────────────────────────────────────────────────────────────
|
|
355
|
-
|
|
356
|
-
function buildFillQuery(accountId: string, gte: string, lte: string, size: number) {
|
|
357
|
-
return {
|
|
358
|
-
size,
|
|
359
|
-
track_total_hits: false,
|
|
360
|
-
_source: [
|
|
361
|
-
'block_data.block_time',
|
|
362
|
-
'block_data.block_num',
|
|
363
|
-
'operation_id_num',
|
|
364
|
-
'operation_history.op_object.pays',
|
|
365
|
-
'operation_history.op_object.receives',
|
|
366
|
-
'operation_history.op_object.fee',
|
|
367
|
-
'operation_history.op_object.order_id',
|
|
368
|
-
'operation_history.op_object.account_id',
|
|
369
|
-
'operation_history.op_object.is_maker',
|
|
370
|
-
],
|
|
371
|
-
query: {
|
|
372
|
-
bool: {
|
|
373
|
-
filter: [
|
|
374
|
-
{ term: { operation_type: OP_FILL_ORDER } },
|
|
375
|
-
{ term: { 'operation_history.op_object.account_id.keyword': accountId } },
|
|
376
|
-
{ range: { 'block_data.block_time': { gte, lte } } },
|
|
377
|
-
],
|
|
378
|
-
},
|
|
379
|
-
},
|
|
380
|
-
sort: [
|
|
381
|
-
{ 'block_data.block_time': { order: 'asc' } },
|
|
382
|
-
{ operation_id_num: { order: 'asc' } },
|
|
383
|
-
],
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
async function fetchAllFills(config: any, accountId: string, gte: string, lte: string): Promise<FillRecord[]> {
|
|
388
|
-
const pageSize = 10000;
|
|
389
|
-
const fills: FillRecord[] = [];
|
|
390
|
-
let searchAfter: any[] | null = null;
|
|
391
|
-
const cfg = { ...BASE_CONFIG, timeout: 60000, ...config };
|
|
392
|
-
|
|
393
|
-
while (true) {
|
|
394
|
-
const query = buildFillQuery(accountId, gte, lte, pageSize);
|
|
395
|
-
if (searchAfter) (query as any).search_after = searchAfter;
|
|
396
|
-
|
|
397
|
-
const result: any = await kibanaSearch(cfg, query);
|
|
398
|
-
const hits = result?.hits?.hits ?? [];
|
|
399
|
-
if (!hits.length) break;
|
|
400
|
-
|
|
401
|
-
for (const hit of hits) {
|
|
402
|
-
const src = hit?._source;
|
|
403
|
-
const op = src?.operation_history?.op_object;
|
|
404
|
-
if (!op || !op.pays || !op.receives) continue;
|
|
405
|
-
|
|
406
|
-
fills.push({
|
|
407
|
-
time: src.block_data?.block_time ?? '',
|
|
408
|
-
blockNum: src.block_data?.block_num ?? 0,
|
|
409
|
-
opNum: Number(src.operation_id_num ?? 0),
|
|
410
|
-
orderId: op.order_id ?? '',
|
|
411
|
-
accountId: op.account_id ?? '',
|
|
412
|
-
pays: { amount: Number(op.pays.amount ?? 0), asset_id: op.pays.asset_id ?? '' },
|
|
413
|
-
receives: { amount: Number(op.receives.amount ?? 0), asset_id: op.receives.asset_id ?? '' },
|
|
414
|
-
fee: { amount: Number(op.fee?.amount ?? 0), asset_id: op.fee?.asset_id ?? '' },
|
|
415
|
-
isMaker: op.is_maker ?? false,
|
|
416
|
-
sort: hit.sort,
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (hits.length < pageSize) break;
|
|
421
|
-
searchAfter = hits[hits.length - 1].sort;
|
|
422
|
-
if (!Array.isArray(searchAfter)) break;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return fills;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
197
|
// ─── Fill Classification ─────────────────────────────────────────────────────
|
|
429
198
|
|
|
430
199
|
/**
|
|
@@ -829,6 +598,12 @@ function printPnlDetail(pairs: PairAnalysis[]) {
|
|
|
829
598
|
|
|
830
599
|
// ─── Performance Metrics ──────────────────────────────────────────────────────
|
|
831
600
|
|
|
601
|
+
interface WindowRange {
|
|
602
|
+
/** The analysis window actually queried — drives annualisation. */
|
|
603
|
+
startMs: number;
|
|
604
|
+
endMs: number;
|
|
605
|
+
}
|
|
606
|
+
|
|
832
607
|
interface TradingMetrics {
|
|
833
608
|
totalLots: number;
|
|
834
609
|
winRate: number;
|
|
@@ -840,8 +615,22 @@ interface TradingMetrics {
|
|
|
840
615
|
expectancyPct: number;
|
|
841
616
|
expectancyR: number;
|
|
842
617
|
netExpectancyBts: number;
|
|
843
|
-
|
|
844
|
-
|
|
618
|
+
// Risk-adjusted ratios annualised from the analysis window (see
|
|
619
|
+
// --hours/--start/--end). Binned 1d for windows >= 3 days, else 1h; every
|
|
620
|
+
// period in the window is zero-filled so flat periods count as 0 PnL and
|
|
621
|
+
// the window scales cleanly to a year. Only whole periods are scored: a
|
|
622
|
+
// trailing partial period is excluded from ratios AND projection alike,
|
|
623
|
+
// so both share the same denominator.
|
|
624
|
+
sharpeAnn: number; // NaN when undefined (no dispersion / < 2 periods)
|
|
625
|
+
sortinoAnn: number; // Infinity when the window has no losing periods
|
|
626
|
+
sharpeAnnSE: number; // standard error of sharpeAnn (estimation uncertainty)
|
|
627
|
+
periodLabel: string; // '1h' | '1d'
|
|
628
|
+
periodCount: number; // whole periods scored, including zero-PnL ones
|
|
629
|
+
periodSpanDays: number; // queried window length
|
|
630
|
+
scoredSpanDays: number; // whole-period span actually scored (nPeriods × bin)
|
|
631
|
+
annualFactor: number; // sqrt(periods per year)
|
|
632
|
+
projectedNetPnlPerDay: number;
|
|
633
|
+
projectedNetPnlAnn: number;
|
|
845
634
|
maxConsecWins: number;
|
|
846
635
|
maxConsecLosses: number;
|
|
847
636
|
avgHoldHours: number;
|
|
@@ -849,6 +638,7 @@ interface TradingMetrics {
|
|
|
849
638
|
bestTradePct: number;
|
|
850
639
|
worstTradePct: number;
|
|
851
640
|
mddPct: number;
|
|
641
|
+
mddAbsBts: number;
|
|
852
642
|
mddHadStablePeak: boolean;
|
|
853
643
|
isOngoingRecovery: boolean;
|
|
854
644
|
currentDrawdownDays: number;
|
|
@@ -882,7 +672,7 @@ function percentile(sorted: number[], p: number): number {
|
|
|
882
672
|
return sorted[f] * (c - k) + sorted[c] * (k - f);
|
|
883
673
|
}
|
|
884
674
|
|
|
885
|
-
function computeMetrics(pair: PairAnalysis): TradingMetrics {
|
|
675
|
+
function computeMetrics(pair: PairAnalysis, window?: WindowRange): TradingMetrics {
|
|
886
676
|
const pnls = pair.realizedPnls;
|
|
887
677
|
const total = pnls.length;
|
|
888
678
|
if (total === 0) {
|
|
@@ -891,11 +681,15 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
|
|
|
891
681
|
avgWin: 0, avgLoss: 0, avgWinLossRatio: 0,
|
|
892
682
|
expectancyBts: 0, expectancyPct: 0, expectancyR: 0,
|
|
893
683
|
netExpectancyBts: 0,
|
|
894
|
-
|
|
684
|
+
sharpeAnn: NaN, sortinoAnn: NaN, sharpeAnnSE: NaN,
|
|
685
|
+
periodLabel: '—', periodCount: 0, periodSpanDays: 0, scoredSpanDays: 0,
|
|
686
|
+
annualFactor: 0,
|
|
687
|
+
projectedNetPnlPerDay: 0, projectedNetPnlAnn: 0,
|
|
895
688
|
maxConsecWins: 0, maxConsecLosses: 0,
|
|
896
689
|
avgHoldHours: 0, limitOrderRatio: 0,
|
|
897
690
|
bestTradePct: 0, worstTradePct: 0,
|
|
898
691
|
mddPct: 0,
|
|
692
|
+
mddAbsBts: 0,
|
|
899
693
|
mddHadStablePeak: false,
|
|
900
694
|
isOngoingRecovery: false,
|
|
901
695
|
currentDrawdownDays: 0,
|
|
@@ -937,29 +731,91 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
|
|
|
937
731
|
|
|
938
732
|
const netExpectancyBts = pair.totalRealizedPnlNet / total;
|
|
939
733
|
|
|
940
|
-
//
|
|
941
|
-
|
|
734
|
+
// ─── Risk-adjusted ratios (window-aware annualisation) ────────────────
|
|
735
|
+
// Bin the window's net PnL into calendar periods and annualise with the
|
|
736
|
+
// matching periods-per-year. Every period in the window is represented
|
|
737
|
+
// (zero-filled): flat periods must count as 0 PnL, otherwise a bot that
|
|
738
|
+
// trades a few days a week is scored as if it traded every day, and the
|
|
739
|
+
// window cannot be scaled honestly to a year.
|
|
740
|
+
const HOUR_MS = 3600_000;
|
|
741
|
+
const DAY_MS = 86_400_000;
|
|
742
|
+
const times = pnls.map(r => Date.parse(r.exitTime)).filter(t => Number.isFinite(t));
|
|
743
|
+
const firstMs = window?.startMs ?? (times.length > 0 ? Math.min(...times) : 0);
|
|
744
|
+
const lastMs = window?.endMs ?? (times.length > 0 ? Math.max(...times) : firstMs + DAY_MS);
|
|
745
|
+
const spanMs = Math.max(lastMs - firstMs, window ? HOUR_MS : DAY_MS);
|
|
746
|
+
const periodSpanDays = spanMs / DAY_MS;
|
|
747
|
+
// Daily buckets need a handful of observations before a daily std means
|
|
748
|
+
// anything; shorter windows fall back to hourly bins so they still
|
|
749
|
+
// annualise instead of dividing by a one-sample std.
|
|
750
|
+
const useHourly = periodSpanDays < 3;
|
|
751
|
+
const periodMs = useHourly ? HOUR_MS : DAY_MS;
|
|
752
|
+
const periodsPerYear = useHourly ? 8760 : 365;
|
|
753
|
+
const periodLabel = useHourly ? '1h' : '1d';
|
|
754
|
+
|
|
755
|
+
// Score only whole periods: a trailing partial period (window not an exact
|
|
756
|
+
// multiple of the bin) would otherwise be a deflated observation. It is
|
|
757
|
+
// excluded from ratios, projection AND activity rates alike, so every
|
|
758
|
+
// window-derived metric shares one basis. Bins are aligned to the window
|
|
759
|
+
// start, so an exact-multiple window scores everything.
|
|
760
|
+
const nPeriods = Math.max(1, Math.floor(spanMs / periodMs));
|
|
761
|
+
const scoredSpanDays = nPeriods * periodMs / DAY_MS;
|
|
762
|
+
const scoredEndMs = firstMs + nPeriods * periodMs;
|
|
763
|
+
const exactWindow = spanMs === nPeriods * periodMs;
|
|
764
|
+
// A fill belongs to the scored window if it falls inside the whole
|
|
765
|
+
// periods. On an exact-multiple window the end boundary is inclusive (the
|
|
766
|
+
// ES range query includes `lte`); with a partial tail that boundary fill
|
|
767
|
+
// is trailing and excluded along with the rest of the tail.
|
|
768
|
+
const inScoredWindow = (t: number) =>
|
|
769
|
+
Number.isFinite(t) && t >= firstMs
|
|
770
|
+
&& (t < scoredEndMs || (exactWindow && t === scoredEndMs));
|
|
771
|
+
const periodPnl = new Array<number>(nPeriods).fill(0);
|
|
772
|
+
let scoredFillCount = 0;
|
|
942
773
|
for (const r of pnls) {
|
|
943
|
-
const
|
|
944
|
-
|
|
774
|
+
const t = Date.parse(r.exitTime);
|
|
775
|
+
if (!inScoredWindow(t)) continue;
|
|
776
|
+
let idx = Math.floor((t - firstMs) / periodMs);
|
|
777
|
+
if (idx === nPeriods) idx = nPeriods - 1; // inclusive end boundary
|
|
778
|
+
periodPnl[idx] += r.pnlNet;
|
|
779
|
+
scoredFillCount++;
|
|
945
780
|
}
|
|
946
|
-
const dailyRets = Object.values(dayBuckets);
|
|
947
|
-
const nDays = dailyRets.length;
|
|
948
781
|
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
782
|
+
const meanPeriod = periodPnl.reduce((s, v) => s + v, 0) / nPeriods;
|
|
783
|
+
// Sample variance (n-1): the window is a sample, not the whole population.
|
|
784
|
+
const periodVar = nPeriods > 1
|
|
785
|
+
? periodPnl.reduce((s, v) => s + (v - meanPeriod) ** 2, 0) / (nPeriods - 1)
|
|
952
786
|
: 0;
|
|
953
|
-
const
|
|
954
|
-
const
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
//
|
|
958
|
-
const
|
|
959
|
-
|
|
787
|
+
const periodStd = Math.sqrt(periodVar);
|
|
788
|
+
const annualFactor = Math.sqrt(periodsPerYear);
|
|
789
|
+
const sharpeAnn = periodStd > 0 ? (meanPeriod / periodStd) * annualFactor : NaN;
|
|
790
|
+
|
|
791
|
+
// Estimation uncertainty of the annualised Sharpe (Lo 2002, i.i.d. returns).
|
|
792
|
+
const srPerPeriod = periodStd > 0 ? meanPeriod / periodStd : NaN;
|
|
793
|
+
const sharpeAnnSE = nPeriods > 1 && Number.isFinite(srPerPeriod)
|
|
794
|
+
? Math.sqrt((1 + 0.5 * srPerPeriod * srPerPeriod) / nPeriods) * annualFactor
|
|
795
|
+
: NaN;
|
|
796
|
+
|
|
797
|
+
// Target downside deviation (MAR = 0), sample denominator (n-1) to match
|
|
798
|
+
// the Sharpe convention above — the two ratios stay internally
|
|
799
|
+
// comparable (many textbook Sortinos divide by N; we deliberately
|
|
800
|
+
// don't mix conventions). With no losing periods Sortino is undefined —
|
|
801
|
+
// reporting 0 would read as "terrible", the opposite of truth.
|
|
802
|
+
const downsideVar = nPeriods > 1
|
|
803
|
+
? periodPnl.reduce((s, v) => s + (v < 0 ? v * v : 0), 0) / (nPeriods - 1)
|
|
960
804
|
: 0;
|
|
961
805
|
const downsideStd = Math.sqrt(downsideVar);
|
|
962
|
-
const
|
|
806
|
+
const sortinoAnn = nPeriods < 2
|
|
807
|
+
? NaN
|
|
808
|
+
: (downsideStd > 0
|
|
809
|
+
? (meanPeriod / downsideStd) * annualFactor
|
|
810
|
+
: (meanPeriod > 0 ? Infinity : NaN));
|
|
811
|
+
|
|
812
|
+
// "This window repeated all year" — linear projection of the scored
|
|
813
|
+
// window's net PnL. Uses the same whole-period basis as the ratios
|
|
814
|
+
// above (not the raw window total), so Sharpe and projection can never
|
|
815
|
+
// disagree about what the window contains.
|
|
816
|
+
const scoredPnl = periodPnl.reduce((s, v) => s + v, 0);
|
|
817
|
+
const projectedNetPnlPerDay = scoredPnl / scoredSpanDays;
|
|
818
|
+
const projectedNetPnlAnn = projectedNetPnlPerDay * 365;
|
|
963
819
|
|
|
964
820
|
// Fills-per-order distribution (grouped by sell order)
|
|
965
821
|
const fillCounts: number[] = [];
|
|
@@ -980,8 +836,13 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
|
|
|
980
836
|
const oneShotOrderRatio = sellOrdersFilled > 0
|
|
981
837
|
? fillCounts.filter(c => c === 1).length / sellOrdersFilled
|
|
982
838
|
: 0;
|
|
983
|
-
|
|
984
|
-
|
|
839
|
+
// Activity rates share the scored whole-period basis: numerator and
|
|
840
|
+
// denominator both cover exactly the lots/fills the ratios scored. A fill
|
|
841
|
+
// with an unparseable timestamp has no period, so it is excluded here too.
|
|
842
|
+
const scoredNotional = [...pair.buys, ...pair.sells]
|
|
843
|
+
.reduce((s, f) => s + (inScoredWindow(Date.parse(f.time)) ? f.quoteAmount : 0), 0);
|
|
844
|
+
const fillsPerDay = scoredSpanDays > 0 ? scoredFillCount / scoredSpanDays : 0;
|
|
845
|
+
const avgVolumePerDay = scoredSpanDays > 0 ? scoredNotional / scoredSpanDays : 0;
|
|
985
846
|
|
|
986
847
|
// Avg hold duration
|
|
987
848
|
let totalHours = 0;
|
|
@@ -1023,7 +884,7 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
|
|
|
1023
884
|
const chronological = [...pnls].sort((a, b) =>
|
|
1024
885
|
new Date(a.exitTime).getTime() - new Date(b.exitTime).getTime()
|
|
1025
886
|
);
|
|
1026
|
-
let equity = 0, peak = 0, mddPct = 0;
|
|
887
|
+
let equity = 0, peak = 0, mddPct = 0, mddAbsBts = 0;
|
|
1027
888
|
let maxRecoveryDays = 0;
|
|
1028
889
|
let isOngoingRecovery = false;
|
|
1029
890
|
let currentDrawdownDays = 0;
|
|
@@ -1067,6 +928,8 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
|
|
|
1067
928
|
}
|
|
1068
929
|
const dd = (equity - peak) / peak;
|
|
1069
930
|
if (dd < mddPct) mddPct = dd;
|
|
931
|
+
const ddAbs = peak - equity;
|
|
932
|
+
if (ddAbs > mddAbsBts) mddAbsBts = ddAbs;
|
|
1070
933
|
}
|
|
1071
934
|
|
|
1072
935
|
if (peak > 0 && !hadStablePeak) {
|
|
@@ -1090,6 +953,7 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
|
|
|
1090
953
|
mddPct *= 100;
|
|
1091
954
|
} else {
|
|
1092
955
|
mddPct = hasPrePeakEquity ? prePeakMinEquity : 0;
|
|
956
|
+
mddAbsBts = 0;
|
|
1093
957
|
}
|
|
1094
958
|
|
|
1095
959
|
// Payoff distribution stats
|
|
@@ -1121,8 +985,16 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
|
|
|
1121
985
|
expectancyPct,
|
|
1122
986
|
expectancyR,
|
|
1123
987
|
netExpectancyBts,
|
|
1124
|
-
|
|
1125
|
-
|
|
988
|
+
sharpeAnn,
|
|
989
|
+
sortinoAnn,
|
|
990
|
+
sharpeAnnSE,
|
|
991
|
+
periodLabel,
|
|
992
|
+
periodCount: nPeriods,
|
|
993
|
+
periodSpanDays,
|
|
994
|
+
scoredSpanDays,
|
|
995
|
+
annualFactor,
|
|
996
|
+
projectedNetPnlPerDay,
|
|
997
|
+
projectedNetPnlAnn,
|
|
1126
998
|
feeDragPct,
|
|
1127
999
|
maxConsecWins: maxW,
|
|
1128
1000
|
maxConsecLosses: maxL,
|
|
@@ -1131,6 +1003,7 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
|
|
|
1131
1003
|
bestTradePct,
|
|
1132
1004
|
worstTradePct,
|
|
1133
1005
|
mddPct,
|
|
1006
|
+
mddAbsBts,
|
|
1134
1007
|
mddHadStablePeak: hadStablePeak,
|
|
1135
1008
|
isOngoingRecovery,
|
|
1136
1009
|
currentDrawdownDays,
|
|
@@ -1152,11 +1025,11 @@ function computeMetrics(pair: PairAnalysis): TradingMetrics {
|
|
|
1152
1025
|
};
|
|
1153
1026
|
}
|
|
1154
1027
|
|
|
1155
|
-
function printMetrics(pairs: PairAnalysis[]) {
|
|
1028
|
+
function printMetrics(pairs: PairAnalysis[], window?: WindowRange) {
|
|
1156
1029
|
for (const pair of pairs) {
|
|
1157
1030
|
if (pair.realizedPnls.length === 0) continue;
|
|
1158
1031
|
|
|
1159
|
-
const m = computeMetrics(pair);
|
|
1032
|
+
const m = computeMetrics(pair, window);
|
|
1160
1033
|
const pairLabel = `${fmtAsset(pair.baseAsset)}/${fmtAsset(pair.quoteAsset)}`;
|
|
1161
1034
|
|
|
1162
1035
|
console.log('');
|
|
@@ -1185,13 +1058,24 @@ function printMetrics(pairs: PairAnalysis[]) {
|
|
|
1185
1058
|
console.log(` P25 / P75: ${fmtPct(m.p25PnlPct)} / ${fmtPct(m.p75PnlPct)}`);
|
|
1186
1059
|
console.log(` Best / Worst Trade: ${fmtPct(m.bestTradePct)} / ${fmtPct(m.worstTradePct)}`);
|
|
1187
1060
|
console.log('');
|
|
1188
|
-
// Risk-adjusted
|
|
1189
|
-
|
|
1190
|
-
|
|
1061
|
+
// Risk-adjusted — annualised from the analysis window, zero-filled bins
|
|
1062
|
+
const ratioStr = (v: number) => Number.isFinite(v) ? v.toFixed(2) : (v === Infinity ? '∞' : 'n/a');
|
|
1063
|
+
const seStr = Number.isFinite(m.sharpeAnnSE) ? ` ± ${m.sharpeAnnSE.toFixed(2)}` : '';
|
|
1064
|
+
const confidence = m.periodSpanDays < 30 ? 'low confidence' : 'ok';
|
|
1065
|
+
console.log(` Sharpe (ann): ${ratioStr(m.sharpeAnn)}${seStr} [${m.periodLabel} bins, n=${m.periodCount}, ${confidence}]`);
|
|
1066
|
+
const sortinoNote = m.sortinoAnn === Infinity ? ' (no losing periods)' : '';
|
|
1067
|
+
console.log(` Sortino (ann): ${ratioStr(m.sortinoAnn)}${sortinoNote}`);
|
|
1068
|
+
if (m.periodLabel === '1h') {
|
|
1069
|
+
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`);
|
|
1070
|
+
}
|
|
1071
|
+
const spanStr = m.scoredSpanDays < m.periodSpanDays - 1e-9
|
|
1072
|
+
? `${m.scoredSpanDays.toFixed(1)}d scored of ${m.periodSpanDays.toFixed(1)}d window`
|
|
1073
|
+
: `${m.periodSpanDays.toFixed(1)}d`;
|
|
1074
|
+
console.log(` Projected net PnL: ${fmt(m.projectedNetPnlAnn, 2)} ${qSymbol}/yr (${fmt(m.projectedNetPnlPerDay, 2)}/day over ${spanStr})`);
|
|
1191
1075
|
console.log('');
|
|
1192
1076
|
// Tail risk
|
|
1193
1077
|
if (m.mddHadStablePeak) {
|
|
1194
|
-
console.log(` Max Drawdown: ${fmtPct(m.mddPct)}`);
|
|
1078
|
+
console.log(` Max Drawdown: ${fmt(m.mddAbsBts, 4)} ${qSymbol} (${fmtPct(m.mddPct)} of peak cumulative profit)`);
|
|
1195
1079
|
} else {
|
|
1196
1080
|
console.log(` Min Equity: ${fmt(m.mddPct, 4)} ${fmtAsset(pair.quoteAsset)}`);
|
|
1197
1081
|
}
|
|
@@ -1334,36 +1218,17 @@ async function run() {
|
|
|
1334
1218
|
let accountId = opts.accountId;
|
|
1335
1219
|
|
|
1336
1220
|
if (!/^1\.2\.\d+$/.test(String(accountId))) {
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
1339
|
-
//
|
|
1340
|
-
//
|
|
1341
|
-
//
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
if (match) {
|
|
1347
|
-
matchedKey = match.botKey;
|
|
1348
|
-
stored = getStoredBotAccountId(match.botKey, accountId);
|
|
1349
|
-
}
|
|
1350
|
-
} catch (_) {
|
|
1351
|
-
// bots.json issues must never break resolution; fall through to chain.
|
|
1352
|
-
}
|
|
1353
|
-
if (stored && !opts.refreshAccount) {
|
|
1354
|
-
console.log(` Using stored accountId ${stored} from profiles/bots.json (no lookup needed; pass --refresh-account to re-verify)`);
|
|
1355
|
-
accountId = stored;
|
|
1356
|
-
} else {
|
|
1357
|
-
const resolved = await resolveAccountId(accountId, opts.node);
|
|
1358
|
-
if (!resolved) {
|
|
1359
|
-
console.error(` Could not resolve "${accountId}" to an account ID`);
|
|
1360
|
-
process.exit(1);
|
|
1361
|
-
}
|
|
1362
|
-
accountId = resolved;
|
|
1363
|
-
if (matchedKey && persistBotAccountId(matchedKey, resolved)) {
|
|
1364
|
-
console.log(` Stored accountId ${resolved} in profiles/bots.json`);
|
|
1365
|
-
}
|
|
1221
|
+
// The Kibana query below filters on the 1.2.x account_id field, so a
|
|
1222
|
+
// name must always resolve first (a raw name would silently return
|
|
1223
|
+
// zero fills). Shared resolver: reuses a stored accountId from
|
|
1224
|
+
// profiles/bots.json when one matches the name and stamps the result
|
|
1225
|
+
// back onto the bot entry on a fresh lookup.
|
|
1226
|
+
const resolved = await resolveAccountRef(accountId, { refresh: opts.refreshAccount });
|
|
1227
|
+
if (!resolved.accountId) {
|
|
1228
|
+
console.error(` Could not resolve "${accountId}" to an account ID`);
|
|
1229
|
+
process.exit(1);
|
|
1366
1230
|
}
|
|
1231
|
+
accountId = resolved.accountId;
|
|
1367
1232
|
}
|
|
1368
1233
|
|
|
1369
1234
|
// Build time range
|
|
@@ -1393,7 +1258,7 @@ async function run() {
|
|
|
1393
1258
|
}
|
|
1394
1259
|
|
|
1395
1260
|
// Resolve unknown asset precisions from blockchain
|
|
1396
|
-
await resolveAssetPrecisions(fills
|
|
1261
|
+
await resolveAssetPrecisions(fills);
|
|
1397
1262
|
|
|
1398
1263
|
// Classify fills
|
|
1399
1264
|
const { trades, pairs } = classifyFills(fills, opts.asset);
|
|
@@ -1451,7 +1316,7 @@ async function run() {
|
|
|
1451
1316
|
printPnlDetail(analyses);
|
|
1452
1317
|
}
|
|
1453
1318
|
|
|
1454
|
-
printMetrics(analyses);
|
|
1319
|
+
printMetrics(analyses, { startMs: Date.parse(gte), endMs: Date.parse(lte) });
|
|
1455
1320
|
|
|
1456
1321
|
if (opts.csv) {
|
|
1457
1322
|
exportCsv(analyses, opts.csv);
|
|
@@ -203,7 +203,7 @@ market_adapter/data/lp/<pair>/lp_pool_<id>_<interval>.json
|
|
|
203
203
|
|
|
204
204
|
## Notes
|
|
205
205
|
|
|
206
|
-
- The chart
|
|
206
|
+
- The chart embeds the vendored `uPlot` runtime inline (no CDN, no sibling `uplot/` dir, no DEXBot2 install needed). Each export is a single self-contained HTML file that renders anywhere, even after being copied or mailed to a machine without DEXBot2.
|
|
207
207
|
- The displayed indicators are computed from the 1h base candles and then sampled onto the selected timeframe.
|
|
208
208
|
- The current volume-weighted overlay is a rolling `VWMA`, not a session-reset VWAP.
|
|
209
209
|
- SMA is disabled by default.
|
|
@@ -218,7 +218,7 @@ market_adapter/data/lp/<pair>/lp_pool_<id>_<interval>.json
|
|
|
218
218
|
- Mouse: drag the candles to pan time + price (price drag sets a manual range); wheel zooms time, except over the price axis where it zooms price. Shift+wheel zooms price anywhere over the price pane (cursor-anchored). Dragging the price-axis gutter scales price, dragging the time-axis gutter scales the timeframe; double-click the price axis to return to autofit. While the price range is manual, timeframe moves no longer refit it.
|
|
219
219
|
- Indicator, timeframe, scale, and overlay-visibility changes are persisted in browser `localStorage` per pool/pair chart (`dexbot2-tradingview-uplot-v3:<pool>:<A>_<B>:<baseSecs|base>`); cursor sync between the price/volume panes uses a separate constant key.
|
|
220
220
|
- The price axis defaults to log base `10`, with a toolbar switch for `Log` / `Linear`.
|
|
221
|
-
- If you regenerate the HTML and then open it later, no CDN access is needed — `uPlot` is
|
|
221
|
+
- If you regenerate the HTML and then open it later, no CDN access is needed — the `uPlot` library (JS + CSS) is inlined into the file itself, so it renders fully offline and is independent of where the file lives on disk.
|
|
222
222
|
|
|
223
223
|
## Typical Workflow
|
|
224
224
|
|