@tradejs/cli 3.1.29-beta.291 → 3.1.29-beta.296
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/dist/cli.js +7150 -6352
- package/dist/lib/binanceMarketContextBackfill.js +2 -2
- package/dist/lib/coinMarketCapContextBackfill.js +1 -1
- package/dist/lib/coreResearch.js +261 -115
- package/dist/lib/marketContextPrepare.js +3 -3
- package/dist/lib/researchVisuals.js +27 -6
- package/dist/lib/strategyResearch.js +234 -245
- package/dist/scripts/aiPocketSearch.js +3343 -3029
- package/dist/scripts/backtest.js +192 -69
- package/dist/scripts/binanceMarketIngest.js +3 -3
- package/dist/scripts/coreResearch.js +263 -115
- package/dist/scripts/replay.js +483 -114
- package/dist/scripts/replayRunner.js +483 -114
- package/dist/scripts/researchVisuals.js +27 -6
- package/dist/scripts/runtimeParity.js +3 -3
- package/dist/scripts/signals.js +3 -3
- package/dist/scripts/signalsDaemon.js +3 -3
- package/dist/scripts/strategyRelease.js +6 -37
- package/dist/scripts/strategyResearch.js +239 -249
- package/package.json +7 -7
- package/dist/lib/researchMetricSnapshot.js +0 -74
package/dist/scripts/replay.js
CHANGED
|
@@ -11,9 +11,9 @@ var __export = (target, all) => {
|
|
|
11
11
|
};
|
|
12
12
|
var __copyProps = (to, from, except, desc) => {
|
|
13
13
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let
|
|
15
|
-
if (!__hasOwnProp.call(to,
|
|
16
|
-
__defProp(to,
|
|
14
|
+
for (let key2 of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key2) && key2 !== except)
|
|
16
|
+
__defProp(to, key2, { get: () => from[key2], enumerable: !(desc = __getOwnPropDesc(from, key2)) || desc.enumerable });
|
|
17
17
|
}
|
|
18
18
|
return to;
|
|
19
19
|
};
|
|
@@ -591,9 +591,9 @@ var loadRuntimeTrades = async (userName, {
|
|
|
591
591
|
const tradePrefix = import_redis.redisKeys.runtimeTrades(userName);
|
|
592
592
|
const bucketPrefix = import_redis.redisKeys.runtimeTradeBuckets(userName);
|
|
593
593
|
const keys = (await (0, import_redis.getKeys)(tradePrefix)).filter(
|
|
594
|
-
(
|
|
594
|
+
(key2) => !key2.startsWith(bucketPrefix)
|
|
595
595
|
);
|
|
596
|
-
const trades = await Promise.all(keys.map((
|
|
596
|
+
const trades = await Promise.all(keys.map((key2) => (0, import_redis.getData)(key2, null)));
|
|
597
597
|
return trades.filter(isRuntimeTradeRecord).filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
|
|
598
598
|
};
|
|
599
599
|
|
|
@@ -1515,13 +1515,12 @@ var getReferenceSymbols = () => {
|
|
|
1515
1515
|
};
|
|
1516
1516
|
var intervalToMarketFeatureInterval = (interval) => {
|
|
1517
1517
|
const normalized = String(interval).trim().toLowerCase();
|
|
1518
|
-
if (normalized === "1" || normalized === "1m") return
|
|
1518
|
+
if (normalized === "1" || normalized === "1m") return null;
|
|
1519
1519
|
if (normalized === "5" || normalized === "5m") return "5m";
|
|
1520
1520
|
if (normalized === "60" || normalized === "1h") return "1h";
|
|
1521
1521
|
return "15m";
|
|
1522
1522
|
};
|
|
1523
1523
|
var marketIntervalToConnectorInterval = (interval) => {
|
|
1524
|
-
if (interval === "1m") return "1";
|
|
1525
1524
|
if (interval === "5m") return "5";
|
|
1526
1525
|
if (interval === "1h") return "60";
|
|
1527
1526
|
return "15";
|
|
@@ -1686,6 +1685,7 @@ var backfillBinanceMarketContext = async (params, enabled) => {
|
|
|
1686
1685
|
return skippedBackfillResult();
|
|
1687
1686
|
}
|
|
1688
1687
|
const interval = intervalToMarketFeatureInterval(params.interval);
|
|
1688
|
+
if (interval === null) return skippedBackfillResult();
|
|
1689
1689
|
const intervalMs = MARKET_FEATURE_INTERVAL_MS[interval];
|
|
1690
1690
|
const { breadthStartMs, tradeFlowStartMs, endMs } = resolveBinanceMarketContextBackfillWindow(params);
|
|
1691
1691
|
if (endMs <= Math.min(breadthStartMs, tradeFlowStartMs)) {
|
|
@@ -1880,8 +1880,8 @@ var backfillBinanceMarketContext = async (params, enabled) => {
|
|
|
1880
1880
|
})
|
|
1881
1881
|
);
|
|
1882
1882
|
const missingChunks = chunks.filter((chunk) => {
|
|
1883
|
-
const
|
|
1884
|
-
return plans.some((plan) => plan.missingChunkKeys.has(
|
|
1883
|
+
const key2 = `${chunk.startMs}:${chunk.endMs}`;
|
|
1884
|
+
return plans.some((plan) => plan.missingChunkKeys.has(key2));
|
|
1885
1885
|
});
|
|
1886
1886
|
const bar = new import_progress.default(
|
|
1887
1887
|
"breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
|
|
@@ -2237,13 +2237,13 @@ var networkErrorCodes = /* @__PURE__ */ new Set([
|
|
|
2237
2237
|
"EAI_AGAIN",
|
|
2238
2238
|
"EHOSTUNREACH"
|
|
2239
2239
|
]);
|
|
2240
|
-
var getNestedErrorValue = (error,
|
|
2240
|
+
var getNestedErrorValue = (error, key2) => {
|
|
2241
2241
|
let current = error;
|
|
2242
2242
|
const seen = /* @__PURE__ */ new Set();
|
|
2243
2243
|
while (current && typeof current === "object" && !seen.has(current)) {
|
|
2244
2244
|
seen.add(current);
|
|
2245
2245
|
const record = current;
|
|
2246
|
-
const value = record[
|
|
2246
|
+
const value = record[key2];
|
|
2247
2247
|
if (typeof value === "string" && value.trim()) {
|
|
2248
2248
|
return value.trim();
|
|
2249
2249
|
}
|
|
@@ -2376,11 +2376,11 @@ var selectBestMarket = (candidates, exchangePriority) => {
|
|
|
2376
2376
|
var buildMatches = (tickers, markets, exchangePriority) => {
|
|
2377
2377
|
const marketByTicker = /* @__PURE__ */ new Map();
|
|
2378
2378
|
for (const market of markets) {
|
|
2379
|
-
const
|
|
2380
|
-
if (!
|
|
2381
|
-
const list = marketByTicker.get(
|
|
2379
|
+
const key2 = String(market.symbol_on_exchange || "").trim().toUpperCase();
|
|
2380
|
+
if (!key2) continue;
|
|
2381
|
+
const list = marketByTicker.get(key2) ?? [];
|
|
2382
2382
|
list.push(market);
|
|
2383
|
-
marketByTicker.set(
|
|
2383
|
+
marketByTicker.set(key2, list);
|
|
2384
2384
|
}
|
|
2385
2385
|
const matches = [];
|
|
2386
2386
|
const unmatched = [];
|
|
@@ -2712,7 +2712,7 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
|
|
|
2712
2712
|
while (cursor < window.toMs) {
|
|
2713
2713
|
const toMs = Math.min(window.toMs, cursor + batchMs);
|
|
2714
2714
|
const missingRanges = batch.map((item) => {
|
|
2715
|
-
const
|
|
2715
|
+
const key2 = coverageKey({
|
|
2716
2716
|
symbol: item.symbol,
|
|
2717
2717
|
interval,
|
|
2718
2718
|
fromMs: cursor,
|
|
@@ -2727,7 +2727,7 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
|
|
|
2727
2727
|
fromMs: cursor,
|
|
2728
2728
|
toMs,
|
|
2729
2729
|
nowMs: coverageNowMs,
|
|
2730
|
-
dataCoverageKeyExists: coverageKeys.has(
|
|
2730
|
+
dataCoverageKeyExists: coverageKeys.has(key2),
|
|
2731
2731
|
dataCoverageRanges: coverageRangesBySymbol.get(normalizedSymbol),
|
|
2732
2732
|
liquidationCoverageRanges: liquidationCoverageRangesBySymbol.get(normalizedSymbol),
|
|
2733
2733
|
edges: edgesBySymbol.get(normalizedSymbol)
|
|
@@ -3026,7 +3026,7 @@ var toIntOrNull = (value) => {
|
|
|
3026
3026
|
return numeric == null ? null : Math.trunc(numeric);
|
|
3027
3027
|
};
|
|
3028
3028
|
var getRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3029
|
-
var getNestedRecord = (value,
|
|
3029
|
+
var getNestedRecord = (value, key2) => getRecord(value[key2]);
|
|
3030
3030
|
var safeDivide2 = (numerator, denominator) => numerator != null && denominator != null && denominator > 0 ? numerator / denominator : null;
|
|
3031
3031
|
var sumFinite = (values) => values.reduce((sum, value) => sum + (value ?? 0), 0);
|
|
3032
3032
|
var toExchangeLiquidityRegime = ({
|
|
@@ -3133,7 +3133,7 @@ var coverageRowsToKeySet = (rows) => new Set(
|
|
|
3133
3133
|
);
|
|
3134
3134
|
var hasBackfillCoverage = (coverageKeys, params) => coverageKeys.has(coverageKey2(params));
|
|
3135
3135
|
var hasRecentBackfillCoverage = (rows, params) => {
|
|
3136
|
-
const
|
|
3136
|
+
const key2 = coverageKey2(params);
|
|
3137
3137
|
return rows.some((row) => {
|
|
3138
3138
|
if (!isMarketContextBackfillCoverageDataBearing(row)) return false;
|
|
3139
3139
|
if (coverageKey2({
|
|
@@ -3142,7 +3142,7 @@ var hasRecentBackfillCoverage = (rows, params) => {
|
|
|
3142
3142
|
interval: row.interval,
|
|
3143
3143
|
fromMs: row.fromMs,
|
|
3144
3144
|
toMs: row.toMs
|
|
3145
|
-
}) !==
|
|
3145
|
+
}) !== key2) {
|
|
3146
3146
|
return false;
|
|
3147
3147
|
}
|
|
3148
3148
|
const checkedAtMs = Number(row.checkedAtMs);
|
|
@@ -3179,27 +3179,27 @@ var createCoinMarketCapFetchStats = () => ({
|
|
|
3179
3179
|
totalCredits: 0,
|
|
3180
3180
|
byPath: /* @__PURE__ */ new Map()
|
|
3181
3181
|
});
|
|
3182
|
-
var getCoinMarketCapPathLabel = (
|
|
3183
|
-
if (
|
|
3184
|
-
if (
|
|
3185
|
-
if (
|
|
3186
|
-
if (
|
|
3187
|
-
if (
|
|
3188
|
-
if (
|
|
3189
|
-
return
|
|
3190
|
-
};
|
|
3191
|
-
var recordCoinMarketCapFetchStats = (stats,
|
|
3182
|
+
var getCoinMarketCapPathLabel = (path9) => {
|
|
3183
|
+
if (path9.includes("global-metrics")) return "global";
|
|
3184
|
+
if (path9.includes("cryptocurrency/quotes")) return "reference";
|
|
3185
|
+
if (path9.includes("exchange/quotes")) return "exchangeLiquidity";
|
|
3186
|
+
if (path9.includes("fear-and-greed")) return "fearGreed";
|
|
3187
|
+
if (path9.includes("cmc100")) return "cmc100";
|
|
3188
|
+
if (path9.includes("cmc20")) return "cmc20";
|
|
3189
|
+
return path9;
|
|
3190
|
+
};
|
|
3191
|
+
var recordCoinMarketCapFetchStats = (stats, path9, creditCount) => {
|
|
3192
3192
|
if (!stats) return;
|
|
3193
|
-
const existing = stats.byPath.get(
|
|
3193
|
+
const existing = stats.byPath.get(path9) ?? { requests: 0, credits: 0 };
|
|
3194
3194
|
existing.requests += 1;
|
|
3195
3195
|
existing.credits += creditCount ?? 0;
|
|
3196
3196
|
stats.requests += 1;
|
|
3197
3197
|
stats.totalCredits += creditCount ?? 0;
|
|
3198
|
-
stats.byPath.set(
|
|
3198
|
+
stats.byPath.set(path9, existing);
|
|
3199
3199
|
};
|
|
3200
3200
|
var formatCoinMarketCapFetchStats = (stats) => {
|
|
3201
3201
|
const byPath = [...stats.byPath.entries()].map(
|
|
3202
|
-
([
|
|
3202
|
+
([path9, item]) => `${getCoinMarketCapPathLabel(path9)}=${item.requests}req/${item.credits}cr`
|
|
3203
3203
|
).join(" ");
|
|
3204
3204
|
return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
|
|
3205
3205
|
};
|
|
@@ -3221,8 +3221,8 @@ var logBackfillSourceDone = (params) => {
|
|
|
3221
3221
|
var markBackfillCoverage = (rows) => (0, import_marketContext2.upsertMarketContextBackfillCoverage)(rows);
|
|
3222
3222
|
var coinMarketCapFetch = async (params) => {
|
|
3223
3223
|
const url = new URL(`${getBaseUrl()}${params.path}`);
|
|
3224
|
-
for (const [
|
|
3225
|
-
url.searchParams.set(
|
|
3224
|
+
for (const [key2, value] of Object.entries(params.searchParams)) {
|
|
3225
|
+
url.searchParams.set(key2, value);
|
|
3226
3226
|
}
|
|
3227
3227
|
for (let attempt = 0; attempt <= getMaxRetries(); attempt += 1) {
|
|
3228
3228
|
const waitMs = Math.max(
|
|
@@ -3266,7 +3266,7 @@ var coinMarketCapFetch = async (params) => {
|
|
|
3266
3266
|
};
|
|
3267
3267
|
var fetchCoinMarketCapIndexRows = async (params) => {
|
|
3268
3268
|
const rows = [];
|
|
3269
|
-
const maxChunkDays =
|
|
3269
|
+
const maxChunkDays = 4;
|
|
3270
3270
|
const totalChunks = Math.max(
|
|
3271
3271
|
1,
|
|
3272
3272
|
Math.ceil(
|
|
@@ -3403,10 +3403,10 @@ var coinMarketCapExchangeQuotesPayloadToLiquidityRows = (payload, interval = "1d
|
|
|
3403
3403
|
const ts = typeof tsRaw === "string" ? new Date(tsRaw) : null;
|
|
3404
3404
|
if (!ts || Number.isNaN(ts.getTime())) continue;
|
|
3405
3405
|
const volumeUsd = toFiniteNumberOrNull(usd.volume_24h) ?? toFiniteNumberOrNull(usd.volume_24h_adjusted) ?? toFiniteNumberOrNull(usd.spot_volume_usd) ?? toFiniteNumberOrNull(usd.reported_volume_24h);
|
|
3406
|
-
const
|
|
3407
|
-
const rows = rowsByTs.get(
|
|
3406
|
+
const key2 = ts.getTime();
|
|
3407
|
+
const rows = rowsByTs.get(key2) ?? [];
|
|
3408
3408
|
rows.push({ slug, name, volumeUsd });
|
|
3409
|
-
rowsByTs.set(
|
|
3409
|
+
rowsByTs.set(key2, rows);
|
|
3410
3410
|
}
|
|
3411
3411
|
}
|
|
3412
3412
|
return [...rowsByTs.entries()].sort(([left], [right]) => left - right).map(([ts, exchanges]) => {
|
|
@@ -4809,6 +4809,10 @@ import_args.default.option(
|
|
|
4809
4809
|
"Write aggregate portfolio metrics and SVG charts; skip runtime/exchange comparison",
|
|
4810
4810
|
false
|
|
4811
4811
|
);
|
|
4812
|
+
import_args.default.option(
|
|
4813
|
+
"portfolioCompareTo",
|
|
4814
|
+
"Compare a full shared replay with a prior portfolio report JSON"
|
|
4815
|
+
);
|
|
4812
4816
|
import_args.default.option(
|
|
4813
4817
|
["B", "portfolioBatchSize"],
|
|
4814
4818
|
"Maximum number of symbols held in memory by one portfolio replay batch",
|
|
@@ -4854,7 +4858,7 @@ var normalizeForStableJson = (value) => {
|
|
|
4854
4858
|
}
|
|
4855
4859
|
if (value && typeof value === "object") {
|
|
4856
4860
|
return Object.fromEntries(
|
|
4857
|
-
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([
|
|
4861
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key2, entry]) => [key2, normalizeForStableJson(entry)])
|
|
4858
4862
|
);
|
|
4859
4863
|
}
|
|
4860
4864
|
return value;
|
|
@@ -5012,9 +5016,9 @@ var buildRuntimeEvidenceLineageScopes = ({
|
|
|
5012
5016
|
if (!lineage || !Number.isFinite(timestamp)) {
|
|
5013
5017
|
return;
|
|
5014
5018
|
}
|
|
5015
|
-
const
|
|
5016
|
-
const existing = scopes.get(
|
|
5017
|
-
scopes.set(
|
|
5019
|
+
const key2 = `${deploymentId ?? "default"}::${accountId ?? "default"}::${strategy}::${symbol}::${runtimeLineageKey(lineage)}`;
|
|
5020
|
+
const existing = scopes.get(key2);
|
|
5021
|
+
scopes.set(key2, {
|
|
5018
5022
|
strategy,
|
|
5019
5023
|
symbol,
|
|
5020
5024
|
...deploymentId ? { deploymentId } : {},
|
|
@@ -5177,12 +5181,13 @@ var getPositionPnlSnapshot = ({
|
|
|
5177
5181
|
direction: position.direction
|
|
5178
5182
|
};
|
|
5179
5183
|
};
|
|
5180
|
-
var createPortfolioReplayConnector = (connector) => {
|
|
5184
|
+
var createPortfolioReplayConnector = (connector, observeEquity = false) => {
|
|
5181
5185
|
let state = {};
|
|
5182
5186
|
let amount = import_constants3.INITIAL_BACKTEST_AMOUNT;
|
|
5183
5187
|
let executionTimestamp = null;
|
|
5184
5188
|
const positionsBySymbol = /* @__PURE__ */ new Map();
|
|
5185
5189
|
const currentPriceBySymbol = /* @__PURE__ */ new Map();
|
|
5190
|
+
const equityMarks = observeEquity ? /* @__PURE__ */ new Map() : void 0;
|
|
5186
5191
|
const markedAtBySymbol = /* @__PURE__ */ new Map();
|
|
5187
5192
|
const entryFacts = /* @__PURE__ */ new Map();
|
|
5188
5193
|
let portfolioObserved = false;
|
|
@@ -5603,11 +5608,25 @@ var createPortfolioReplayConnector = (connector) => {
|
|
|
5603
5608
|
advanceMarket: async ({ symbol, candle }) => {
|
|
5604
5609
|
executionTimestamp = candle.timestamp;
|
|
5605
5610
|
markedAtBySymbol.set(symbol, candle.timestamp);
|
|
5611
|
+
equityMarks?.set(symbol, candle.close);
|
|
5606
5612
|
currentPriceBySymbol.set(symbol, candle.close);
|
|
5607
5613
|
await checkSl({ symbol, candle });
|
|
5608
5614
|
await checkTp({ symbol, candle });
|
|
5609
5615
|
currentPriceBySymbol.set(symbol, candle.close);
|
|
5610
5616
|
},
|
|
5617
|
+
getMarkedEquity: () => {
|
|
5618
|
+
if (!equityMarks) return null;
|
|
5619
|
+
let equity = amount - import_constants3.INITIAL_BACKTEST_AMOUNT;
|
|
5620
|
+
for (const { position } of positionsBySymbol.values()) {
|
|
5621
|
+
const mark = equityMarks.get(position.symbol);
|
|
5622
|
+
if (mark == null || !Number.isFinite(mark)) return null;
|
|
5623
|
+
equity += getPositionPnlSnapshot({
|
|
5624
|
+
position,
|
|
5625
|
+
currentPrice: mark
|
|
5626
|
+
}).unrealizedPnl;
|
|
5627
|
+
}
|
|
5628
|
+
return (0, import_math.round)(equity);
|
|
5629
|
+
},
|
|
5611
5630
|
getReplayArtifacts: () => ({
|
|
5612
5631
|
orderLog,
|
|
5613
5632
|
positionLog,
|
|
@@ -5821,6 +5840,21 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5821
5840
|
context.collectSkipEvidence
|
|
5822
5841
|
);
|
|
5823
5842
|
const decisionTrace = createReplayDecisionTrace(context.decisionTraceTargets);
|
|
5843
|
+
const portfolioDiagnostics = context.collectPortfolioDiagnostics ? { markedMaxDrawdown: 0, unpricedCycles: 0, admissions: [] } : void 0;
|
|
5844
|
+
let equityPeak = 0;
|
|
5845
|
+
const observeEquity = () => {
|
|
5846
|
+
if (!portfolioDiagnostics) return;
|
|
5847
|
+
const equity = context.connector.getMarkedEquity();
|
|
5848
|
+
if (equity === null) {
|
|
5849
|
+
portfolioDiagnostics.unpricedCycles++;
|
|
5850
|
+
return;
|
|
5851
|
+
}
|
|
5852
|
+
equityPeak = Math.max(equityPeak, equity);
|
|
5853
|
+
portfolioDiagnostics.markedMaxDrawdown = Math.max(
|
|
5854
|
+
portfolioDiagnostics.markedMaxDrawdown ?? 0,
|
|
5855
|
+
equityPeak - equity
|
|
5856
|
+
);
|
|
5857
|
+
};
|
|
5824
5858
|
const reportProgress = (timestamp) => {
|
|
5825
5859
|
if (adapters.progress.enabled === false) return;
|
|
5826
5860
|
adapters.progress.tick({
|
|
@@ -5864,6 +5898,7 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5864
5898
|
}
|
|
5865
5899
|
}
|
|
5866
5900
|
advanceAbortedCycle(cycleSymbols, timestamp);
|
|
5901
|
+
observeEquity();
|
|
5867
5902
|
reportProgress(timestamp);
|
|
5868
5903
|
continue;
|
|
5869
5904
|
}
|
|
@@ -5907,6 +5942,21 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5907
5942
|
result
|
|
5908
5943
|
});
|
|
5909
5944
|
if (result && typeof result !== "string") {
|
|
5945
|
+
if (portfolioDiagnostics) {
|
|
5946
|
+
const status = result.orderFailureReason === "PORTFOLIO_POSITION_LIMIT" ? "capacity-rejected" : result.orderStatus === "completed" ? "filled" : void 0;
|
|
5947
|
+
if (status)
|
|
5948
|
+
portfolioDiagnostics.admissions.push({
|
|
5949
|
+
strategy: strategyRuntime.strategyName,
|
|
5950
|
+
status,
|
|
5951
|
+
key: JSON.stringify([
|
|
5952
|
+
strategyRuntime.strategyName,
|
|
5953
|
+
result.symbol,
|
|
5954
|
+
result.direction,
|
|
5955
|
+
result.timestamp,
|
|
5956
|
+
result.signalId
|
|
5957
|
+
])
|
|
5958
|
+
});
|
|
5959
|
+
}
|
|
5910
5960
|
result.runtimeLineage = strategyRuntime.runtimeLineage;
|
|
5911
5961
|
await adapters.enrichSignal(result);
|
|
5912
5962
|
cycleSignals.push(result);
|
|
@@ -5921,12 +5971,16 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5921
5971
|
status: "completed",
|
|
5922
5972
|
durationMs: adapters.clock.now() - cycleStartedAt
|
|
5923
5973
|
});
|
|
5974
|
+
observeEquity();
|
|
5924
5975
|
reportProgress(timestamp);
|
|
5925
5976
|
}
|
|
5926
5977
|
} finally {
|
|
5927
5978
|
releaseHistoricalReplayCaches(context.plan, adapters);
|
|
5928
5979
|
}
|
|
5980
|
+
if (portfolioDiagnostics?.unpricedCycles)
|
|
5981
|
+
portfolioDiagnostics.markedMaxDrawdown = null;
|
|
5929
5982
|
return {
|
|
5983
|
+
...portfolioDiagnostics ? { portfolioDiagnostics } : {},
|
|
5930
5984
|
signals,
|
|
5931
5985
|
abortedCycles,
|
|
5932
5986
|
replayLineageScopes: skipEvidence.values(),
|
|
@@ -5982,6 +6036,7 @@ var collectHistoricalReplayResult = (context) => ({
|
|
|
5982
6036
|
abortedCycles: context.abortedCycles,
|
|
5983
6037
|
runtimeLineages: context.runtimeLineages,
|
|
5984
6038
|
replayLineageScopes: context.replayLineageScopes,
|
|
6039
|
+
...context.portfolioDiagnostics ? { portfolioDiagnostics: context.portfolioDiagnostics } : {},
|
|
5985
6040
|
...context.replayDecisionTraces?.length ? { replayDecisionTraces: context.replayDecisionTraces } : {}
|
|
5986
6041
|
});
|
|
5987
6042
|
var compactHistoricalReplayResultForPortfolio = (result) => ({
|
|
@@ -6183,6 +6238,7 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6183
6238
|
showProgress = true,
|
|
6184
6239
|
collectSkipEvidence = true,
|
|
6185
6240
|
collectSignals = true,
|
|
6241
|
+
collectPortfolioDiagnostics = false,
|
|
6186
6242
|
decisionTraceTargets
|
|
6187
6243
|
}) => {
|
|
6188
6244
|
const memory = createReplayMemoryProfile();
|
|
@@ -6192,7 +6248,8 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6192
6248
|
const projectHooks = projectConfig.hooks;
|
|
6193
6249
|
const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
|
|
6194
6250
|
const replayConnector = createPortfolioReplayConnector(
|
|
6195
|
-
preparedRun.marketConnector
|
|
6251
|
+
preparedRun.marketConnector,
|
|
6252
|
+
collectPortfolioDiagnostics
|
|
6196
6253
|
);
|
|
6197
6254
|
const connectorName = getConnectorName(preparedRun.marketConnector);
|
|
6198
6255
|
const referenceData = references ?? await loadHistoricalReplayReferences({ preparedRun, interval });
|
|
@@ -6270,6 +6327,7 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6270
6327
|
connector: replayConnector,
|
|
6271
6328
|
collectSkipEvidence,
|
|
6272
6329
|
collectSignals,
|
|
6330
|
+
collectPortfolioDiagnostics,
|
|
6273
6331
|
decisionTraceTargets,
|
|
6274
6332
|
hooks: projectHooks,
|
|
6275
6333
|
hookContext
|
|
@@ -6308,7 +6366,8 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6308
6366
|
abortedCycles: execution.abortedCycles,
|
|
6309
6367
|
runtimeLineages: plan.runtimeLineages,
|
|
6310
6368
|
replayLineageScopes: execution.replayLineageScopes,
|
|
6311
|
-
replayDecisionTraces: execution.replayDecisionTraces
|
|
6369
|
+
replayDecisionTraces: execution.replayDecisionTraces,
|
|
6370
|
+
portfolioDiagnostics: execution.portfolioDiagnostics
|
|
6312
6371
|
});
|
|
6313
6372
|
memory?.("result:collected");
|
|
6314
6373
|
return result;
|
|
@@ -6555,16 +6614,16 @@ var findRuntimeDuplicateGroups = (entries) => {
|
|
|
6555
6614
|
if (entry.source !== "runtime") {
|
|
6556
6615
|
continue;
|
|
6557
6616
|
}
|
|
6558
|
-
const
|
|
6559
|
-
const bucket = groups.get(
|
|
6617
|
+
const key2 = toRuntimeDuplicateKey(entry);
|
|
6618
|
+
const bucket = groups.get(key2) ?? [];
|
|
6560
6619
|
bucket.push(entry);
|
|
6561
|
-
groups.set(
|
|
6620
|
+
groups.set(key2, bucket);
|
|
6562
6621
|
}
|
|
6563
|
-
return [...groups.entries()].filter(([, group]) => group.length > 1).map(([
|
|
6622
|
+
return [...groups.entries()].filter(([, group]) => group.length > 1).map(([key2, group]) => {
|
|
6564
6623
|
const sortedGroup = [...group].sort(sortTradeParityEntries);
|
|
6565
6624
|
const first = sortedGroup[0];
|
|
6566
6625
|
return {
|
|
6567
|
-
key,
|
|
6626
|
+
key: key2,
|
|
6568
6627
|
strategy: first.strategy,
|
|
6569
6628
|
symbol: first.symbol,
|
|
6570
6629
|
direction: first.direction,
|
|
@@ -6603,26 +6662,26 @@ var compareTradeParityEntries = ({
|
|
|
6603
6662
|
const groupedRuntime = /* @__PURE__ */ new Map();
|
|
6604
6663
|
const groupedBacktest = /* @__PURE__ */ new Map();
|
|
6605
6664
|
for (const entry of runtimeEntries) {
|
|
6606
|
-
const
|
|
6607
|
-
const bucket = groupedRuntime.get(
|
|
6665
|
+
const key2 = toGroupKey(entry);
|
|
6666
|
+
const bucket = groupedRuntime.get(key2) ?? [];
|
|
6608
6667
|
bucket.push(entry);
|
|
6609
|
-
groupedRuntime.set(
|
|
6668
|
+
groupedRuntime.set(key2, bucket);
|
|
6610
6669
|
}
|
|
6611
6670
|
for (const entry of backtestEntries) {
|
|
6612
|
-
const
|
|
6613
|
-
const bucket = groupedBacktest.get(
|
|
6671
|
+
const key2 = toGroupKey(entry);
|
|
6672
|
+
const bucket = groupedBacktest.get(key2) ?? [];
|
|
6614
6673
|
bucket.push(entry);
|
|
6615
|
-
groupedBacktest.set(
|
|
6674
|
+
groupedBacktest.set(key2, bucket);
|
|
6616
6675
|
}
|
|
6617
6676
|
const groupKeys = /* @__PURE__ */ new Set([
|
|
6618
6677
|
...groupedRuntime.keys(),
|
|
6619
6678
|
...groupedBacktest.keys()
|
|
6620
6679
|
]);
|
|
6621
|
-
for (const
|
|
6622
|
-
const runtimeGroup = [...groupedRuntime.get(
|
|
6680
|
+
for (const key2 of groupKeys) {
|
|
6681
|
+
const runtimeGroup = [...groupedRuntime.get(key2) ?? []].sort(
|
|
6623
6682
|
(left, right) => left.timestamp - right.timestamp
|
|
6624
6683
|
);
|
|
6625
|
-
const backtestGroup = [...groupedBacktest.get(
|
|
6684
|
+
const backtestGroup = [...groupedBacktest.get(key2) ?? []].sort(
|
|
6626
6685
|
(left, right) => left.timestamp - right.timestamp
|
|
6627
6686
|
);
|
|
6628
6687
|
const unmatchedBacktest = backtestGroup.map((entry) => ({
|
|
@@ -6863,8 +6922,8 @@ var filterBucketKeysByWindow = (bucketKeys, prefix, startTime, endTime) => {
|
|
|
6863
6922
|
if (!allowedDayKeys.size) {
|
|
6864
6923
|
return [];
|
|
6865
6924
|
}
|
|
6866
|
-
return bucketKeys.filter((
|
|
6867
|
-
const suffix =
|
|
6925
|
+
return bucketKeys.filter((key2) => {
|
|
6926
|
+
const suffix = key2.slice(prefix.length);
|
|
6868
6927
|
const firstColon = suffix.indexOf(":");
|
|
6869
6928
|
if (firstColon <= 0) {
|
|
6870
6929
|
return false;
|
|
@@ -6883,7 +6942,7 @@ var loadRuntimeSignals = async (userName, {
|
|
|
6883
6942
|
startTime,
|
|
6884
6943
|
endTime
|
|
6885
6944
|
);
|
|
6886
|
-
const refs = (await Promise.all(bucketKeys.map((
|
|
6945
|
+
const refs = (await Promise.all(bucketKeys.map((key2) => (0, import_redis4.getHashJsonValues)(key2)))).flat().filter(isRuntimeSignalBucketRef);
|
|
6887
6946
|
const seen = /* @__PURE__ */ new Set();
|
|
6888
6947
|
const payloads = await Promise.all(
|
|
6889
6948
|
refs.map(async (ref) => {
|
|
@@ -6912,7 +6971,7 @@ var loadRuntimeSignalEvaluations = async (userName, {
|
|
|
6912
6971
|
startTime,
|
|
6913
6972
|
endTime
|
|
6914
6973
|
);
|
|
6915
|
-
const evaluations = (await Promise.all(bucketKeys.map((
|
|
6974
|
+
const evaluations = (await Promise.all(bucketKeys.map((key2) => (0, import_redis4.getHashJsonValues)(key2)))).flat().filter(isRuntimeSignalEvaluationRecord);
|
|
6916
6975
|
const deduped = /* @__PURE__ */ new Map();
|
|
6917
6976
|
for (const evaluation of evaluations) {
|
|
6918
6977
|
deduped.set(evaluation.evaluationId, evaluation);
|
|
@@ -7082,10 +7141,10 @@ var syncRuntimeTrades = async ({
|
|
|
7082
7141
|
)
|
|
7083
7142
|
];
|
|
7084
7143
|
await Promise.all(
|
|
7085
|
-
activeTradeKeys.map(async (
|
|
7086
|
-
const activeRef = await (0, import_redis5.getData)(
|
|
7144
|
+
activeTradeKeys.map(async (key2) => {
|
|
7145
|
+
const activeRef = await (0, import_redis5.getData)(key2, null);
|
|
7087
7146
|
activeOrderIdByKey.set(
|
|
7088
|
-
|
|
7147
|
+
key2,
|
|
7089
7148
|
typeof activeRef?.orderId === "string" ? activeRef.orderId : null
|
|
7090
7149
|
);
|
|
7091
7150
|
})
|
|
@@ -8152,9 +8211,9 @@ var filterReplayComparisonByLineage = ({
|
|
|
8152
8211
|
);
|
|
8153
8212
|
const deploymentWindows = /* @__PURE__ */ new Map();
|
|
8154
8213
|
for (const scope of comparableRuntimeLineageScopes) {
|
|
8155
|
-
const
|
|
8156
|
-
const existing = deploymentWindows.get(
|
|
8157
|
-
deploymentWindows.set(
|
|
8214
|
+
const key2 = lineageScopeKey(scope);
|
|
8215
|
+
const existing = deploymentWindows.get(key2);
|
|
8216
|
+
deploymentWindows.set(key2, {
|
|
8158
8217
|
firstTimestamp: existing == null ? scope.firstTimestamp : Math.min(existing.firstTimestamp, scope.firstTimestamp),
|
|
8159
8218
|
lastTimestamp: existing == null ? scope.lastTimestamp : Math.max(existing.lastTimestamp, scope.lastTimestamp)
|
|
8160
8219
|
});
|
|
@@ -8190,9 +8249,9 @@ var filterReplayComparisonByLineage = ({
|
|
|
8190
8249
|
...comparableRuntimeSignals,
|
|
8191
8250
|
...comparableRuntimeEvaluations
|
|
8192
8251
|
]) {
|
|
8193
|
-
const
|
|
8194
|
-
const existing = deploymentWindows.get(
|
|
8195
|
-
deploymentWindows.set(
|
|
8252
|
+
const key2 = lineageScopeKey(artifact);
|
|
8253
|
+
const existing = deploymentWindows.get(key2);
|
|
8254
|
+
deploymentWindows.set(key2, {
|
|
8196
8255
|
firstTimestamp: existing == null ? artifact.timestamp : Math.min(existing.firstTimestamp, artifact.timestamp),
|
|
8197
8256
|
lastTimestamp: existing == null ? artifact.timestamp : Math.max(existing.lastTimestamp, artifact.timestamp)
|
|
8198
8257
|
});
|
|
@@ -8277,16 +8336,16 @@ var compareExchangeEntriesToBacktest = ({
|
|
|
8277
8336
|
const groupedExchange = /* @__PURE__ */ new Map();
|
|
8278
8337
|
const groupedBacktest = /* @__PURE__ */ new Map();
|
|
8279
8338
|
for (const entry of exchangeEntries) {
|
|
8280
|
-
const
|
|
8281
|
-
const bucket = groupedExchange.get(
|
|
8339
|
+
const key2 = `${entry.symbol}::${entry.direction}`;
|
|
8340
|
+
const bucket = groupedExchange.get(key2) ?? [];
|
|
8282
8341
|
bucket.push(entry);
|
|
8283
|
-
groupedExchange.set(
|
|
8342
|
+
groupedExchange.set(key2, bucket);
|
|
8284
8343
|
}
|
|
8285
8344
|
for (const entry of backtestEntries) {
|
|
8286
|
-
const
|
|
8287
|
-
const bucket = groupedBacktest.get(
|
|
8345
|
+
const key2 = `${entry.symbol}::${entry.direction}`;
|
|
8346
|
+
const bucket = groupedBacktest.get(key2) ?? [];
|
|
8288
8347
|
bucket.push(entry);
|
|
8289
|
-
groupedBacktest.set(
|
|
8348
|
+
groupedBacktest.set(key2, bucket);
|
|
8290
8349
|
}
|
|
8291
8350
|
const matched = [];
|
|
8292
8351
|
const exchangeOnly = [];
|
|
@@ -8296,11 +8355,11 @@ var compareExchangeEntriesToBacktest = ({
|
|
|
8296
8355
|
...groupedExchange.keys(),
|
|
8297
8356
|
...groupedBacktest.keys()
|
|
8298
8357
|
]);
|
|
8299
|
-
for (const
|
|
8300
|
-
const exchangeGroup = [...groupedExchange.get(
|
|
8358
|
+
for (const key2 of groupKeys) {
|
|
8359
|
+
const exchangeGroup = [...groupedExchange.get(key2) ?? []].sort(
|
|
8301
8360
|
(left, right) => left.entryTimestamp - right.entryTimestamp
|
|
8302
8361
|
);
|
|
8303
|
-
const unmatchedBacktest = [...groupedBacktest.get(
|
|
8362
|
+
const unmatchedBacktest = [...groupedBacktest.get(key2) ?? []].sort((left, right) => left.timestamp - right.timestamp).map((entry) => ({ entry, used: false }));
|
|
8304
8363
|
for (const exchangeEntry of exchangeGroup) {
|
|
8305
8364
|
let bestIndex = -1;
|
|
8306
8365
|
let bestDiff = Number.POSITIVE_INFINITY;
|
|
@@ -8721,14 +8780,14 @@ var mergeExchangeEntryExecutions = (rows) => {
|
|
|
8721
8780
|
const mergedRows = [];
|
|
8722
8781
|
const groups = /* @__PURE__ */ new Map();
|
|
8723
8782
|
for (const row of rows) {
|
|
8724
|
-
const
|
|
8725
|
-
if (!
|
|
8783
|
+
const key2 = getExchangeExecutionMergeKey(row);
|
|
8784
|
+
if (!key2) {
|
|
8726
8785
|
mergedRows.push(row);
|
|
8727
8786
|
continue;
|
|
8728
8787
|
}
|
|
8729
|
-
const group = groups.get(
|
|
8788
|
+
const group = groups.get(key2) ?? [];
|
|
8730
8789
|
group.push(row);
|
|
8731
|
-
groups.set(
|
|
8790
|
+
groups.set(key2, group);
|
|
8732
8791
|
}
|
|
8733
8792
|
for (const group of groups.values()) {
|
|
8734
8793
|
mergedRows.push(
|
|
@@ -9219,13 +9278,13 @@ function* jsonParts(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
|
9219
9278
|
throw new TypeError("JSON evidence cannot contain toJSON hooks");
|
|
9220
9279
|
yield "{";
|
|
9221
9280
|
let first = true;
|
|
9222
|
-
for (const
|
|
9223
|
-
const field = Reflect.get(value,
|
|
9281
|
+
for (const key2 of Object.keys(value)) {
|
|
9282
|
+
const field = Reflect.get(value, key2);
|
|
9224
9283
|
if (field === void 0 || typeof field === "function" || typeof field === "symbol")
|
|
9225
9284
|
continue;
|
|
9226
9285
|
if (!first) yield ",";
|
|
9227
9286
|
first = false;
|
|
9228
|
-
yield JSON.stringify(
|
|
9287
|
+
yield JSON.stringify(key2) + ":";
|
|
9229
9288
|
yield* jsonParts(field, ancestors);
|
|
9230
9289
|
}
|
|
9231
9290
|
yield "}";
|
|
@@ -9687,10 +9746,276 @@ var writeReplayOutputReport = async ({
|
|
|
9687
9746
|
};
|
|
9688
9747
|
|
|
9689
9748
|
// src/lib/replay/portfolioReport.ts
|
|
9749
|
+
var import_promises5 = __toESM(require("fs/promises"));
|
|
9750
|
+
var import_node_path8 = __toESM(require("path"));
|
|
9751
|
+
var import_node_crypto6 = require("crypto");
|
|
9752
|
+
|
|
9753
|
+
// src/lib/replay/portfolioComparison.ts
|
|
9754
|
+
var import_node_crypto5 = require("crypto");
|
|
9755
|
+
|
|
9756
|
+
// src/lib/coreResearch/io.ts
|
|
9757
|
+
var import_node_crypto3 = require("crypto");
|
|
9758
|
+
var import_node_fs4 = require("fs");
|
|
9690
9759
|
var import_promises4 = __toESM(require("fs/promises"));
|
|
9760
|
+
var import_backtest2 = require("@tradejs/core/backtest");
|
|
9761
|
+
var import_node_path7 = __toESM(require("path"));
|
|
9762
|
+
|
|
9763
|
+
// src/lib/coreResearch/trace.ts
|
|
9764
|
+
var import_node_fs3 = require("fs");
|
|
9765
|
+
var import_node_readline = __toESM(require("readline"));
|
|
9691
9766
|
var import_node_path6 = __toESM(require("path"));
|
|
9692
|
-
|
|
9693
|
-
|
|
9767
|
+
|
|
9768
|
+
// src/lib/coreResearch/io.ts
|
|
9769
|
+
var canonicalJson = (value) => {
|
|
9770
|
+
const normalize = (current) => {
|
|
9771
|
+
if (Array.isArray(current)) return current.map(normalize);
|
|
9772
|
+
if (current && typeof current === "object") {
|
|
9773
|
+
return Object.fromEntries(
|
|
9774
|
+
Object.entries(current).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key2, entry]) => [key2, normalize(entry)])
|
|
9775
|
+
);
|
|
9776
|
+
}
|
|
9777
|
+
return current;
|
|
9778
|
+
};
|
|
9779
|
+
return JSON.stringify(normalize(value));
|
|
9780
|
+
};
|
|
9781
|
+
|
|
9782
|
+
// src/lib/coreResearch/statistics.ts
|
|
9783
|
+
var import_node_crypto4 = require("crypto");
|
|
9784
|
+
|
|
9785
|
+
// src/lib/coreResearch/metrics.ts
|
|
9786
|
+
var DAY_MS4 = 24 * 60 * 60 * 1e3;
|
|
9787
|
+
|
|
9788
|
+
// src/lib/coreResearch/statistics.ts
|
|
9789
|
+
var createSeededRandom = (seedText) => {
|
|
9790
|
+
const digest = (0, import_node_crypto4.createHash)("sha256").update(seedText).digest();
|
|
9791
|
+
let state = digest.readUInt32LE(0) || 1;
|
|
9792
|
+
return () => {
|
|
9793
|
+
state ^= state << 13;
|
|
9794
|
+
state ^= state >>> 17;
|
|
9795
|
+
state ^= state << 5;
|
|
9796
|
+
return (state >>> 0) / 4294967296;
|
|
9797
|
+
};
|
|
9798
|
+
};
|
|
9799
|
+
var percentile = (sorted, probability) => {
|
|
9800
|
+
if (!sorted.length) return Number.NaN;
|
|
9801
|
+
const index = Math.min(
|
|
9802
|
+
sorted.length - 1,
|
|
9803
|
+
Math.max(0, Math.floor(probability * sorted.length))
|
|
9804
|
+
);
|
|
9805
|
+
return sorted[index];
|
|
9806
|
+
};
|
|
9807
|
+
var calendarClusterBootstrap = (params) => {
|
|
9808
|
+
const clusterMs = params.clusterDays * DAY_MS4;
|
|
9809
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
9810
|
+
if (params.start != null && params.end != null && params.end > params.start) {
|
|
9811
|
+
const first = Math.floor(params.start / clusterMs);
|
|
9812
|
+
const last = Math.ceil(params.end / clusterMs);
|
|
9813
|
+
for (let bucket = first; bucket < last; bucket += 1) {
|
|
9814
|
+
buckets.set(bucket, { control: 0, candidate: 0 });
|
|
9815
|
+
}
|
|
9816
|
+
}
|
|
9817
|
+
const add = (trades, field) => {
|
|
9818
|
+
for (const trade of trades) {
|
|
9819
|
+
const bucket = Math.floor(trade.exitTimestamp / clusterMs);
|
|
9820
|
+
const current = buckets.get(bucket) ?? { control: 0, candidate: 0 };
|
|
9821
|
+
current[field] += trade.netProfit;
|
|
9822
|
+
buckets.set(bucket, current);
|
|
9823
|
+
}
|
|
9824
|
+
};
|
|
9825
|
+
add(params.control, "control");
|
|
9826
|
+
add(params.candidate, "candidate");
|
|
9827
|
+
const deltas = [...buckets.entries()].sort(([left], [right]) => left - right).map(([, value]) => value.candidate - value.control);
|
|
9828
|
+
if (!deltas.length) {
|
|
9829
|
+
return {
|
|
9830
|
+
method: "calendar-cluster-bootstrap",
|
|
9831
|
+
clusterDays: params.clusterDays,
|
|
9832
|
+
iterations: params.iterations,
|
|
9833
|
+
confidenceLevel: params.confidenceLevel,
|
|
9834
|
+
observedMeanPnlDelta: null,
|
|
9835
|
+
confidenceInterval: null,
|
|
9836
|
+
probabilityPositive: null,
|
|
9837
|
+
oneSidedPValue: null,
|
|
9838
|
+
holmAdjustedPValue: null
|
|
9839
|
+
};
|
|
9840
|
+
}
|
|
9841
|
+
const random = createSeededRandom(params.seed);
|
|
9842
|
+
const samples = [];
|
|
9843
|
+
for (let iteration = 0; iteration < params.iterations; iteration += 1) {
|
|
9844
|
+
let sum = 0;
|
|
9845
|
+
for (let index = 0; index < deltas.length; index += 1) {
|
|
9846
|
+
sum += deltas[Math.floor(random() * deltas.length)];
|
|
9847
|
+
}
|
|
9848
|
+
samples.push(sum / deltas.length);
|
|
9849
|
+
}
|
|
9850
|
+
samples.sort((left, right) => left - right);
|
|
9851
|
+
const alpha = 1 - params.confidenceLevel;
|
|
9852
|
+
const positive = samples.filter((value) => value > 0).length;
|
|
9853
|
+
const probabilityPositive = positive / samples.length;
|
|
9854
|
+
const observed = deltas.reduce((sum, value) => sum + value, 0) / deltas.length;
|
|
9855
|
+
const oneSidedPValue = (1 + samples.filter((value) => value - observed >= observed - 1e-12).length) / (samples.length + 1);
|
|
9856
|
+
return {
|
|
9857
|
+
method: "calendar-cluster-bootstrap",
|
|
9858
|
+
clusterDays: params.clusterDays,
|
|
9859
|
+
iterations: params.iterations,
|
|
9860
|
+
confidenceLevel: params.confidenceLevel,
|
|
9861
|
+
observedMeanPnlDelta: deltas.reduce((sum, value) => sum + value, 0) / deltas.length,
|
|
9862
|
+
confidenceInterval: [
|
|
9863
|
+
percentile(samples, alpha / 2),
|
|
9864
|
+
percentile(samples, 1 - alpha / 2)
|
|
9865
|
+
],
|
|
9866
|
+
probabilityPositive,
|
|
9867
|
+
oneSidedPValue,
|
|
9868
|
+
holmAdjustedPValue: null
|
|
9869
|
+
};
|
|
9870
|
+
};
|
|
9871
|
+
|
|
9872
|
+
// src/lib/replay/portfolioComparison.ts
|
|
9873
|
+
var key = (value) => canonicalJson(value);
|
|
9874
|
+
function longestRecovery(points) {
|
|
9875
|
+
let start;
|
|
9876
|
+
let lastPeak;
|
|
9877
|
+
let longestMs = 0;
|
|
9878
|
+
for (const point of points) {
|
|
9879
|
+
if (point.value === 0) {
|
|
9880
|
+
if (start !== void 0)
|
|
9881
|
+
longestMs = Math.max(longestMs, point.timestamp - start);
|
|
9882
|
+
start = void 0;
|
|
9883
|
+
lastPeak = point.timestamp;
|
|
9884
|
+
} else if (start === void 0) start = lastPeak ?? point.timestamp;
|
|
9885
|
+
}
|
|
9886
|
+
return { longestCompletedMs: longestMs, unresolvedSince: start ?? null };
|
|
9887
|
+
}
|
|
9888
|
+
function compareCapacityAdmissions(before, after, changed) {
|
|
9889
|
+
const count = (filled, rejected) => {
|
|
9890
|
+
const keys = new Set(
|
|
9891
|
+
filled.admissions.filter(
|
|
9892
|
+
(event) => event.strategy !== changed && event.status === "filled"
|
|
9893
|
+
).map((event) => event.key)
|
|
9894
|
+
);
|
|
9895
|
+
return new Set(
|
|
9896
|
+
rejected.admissions.filter(
|
|
9897
|
+
(event) => event.status === "capacity-rejected" && keys.has(event.key)
|
|
9898
|
+
).map((event) => event.key)
|
|
9899
|
+
).size;
|
|
9900
|
+
};
|
|
9901
|
+
return { displaced: count(before, after), restored: count(after, before) };
|
|
9902
|
+
}
|
|
9903
|
+
function comparePortfolioReports(input, candidate) {
|
|
9904
|
+
if (!input || typeof input !== "object" || !("checksum" in input)) {
|
|
9905
|
+
throw new Error("Expected a checksum-bound portfolio report");
|
|
9906
|
+
}
|
|
9907
|
+
const { checksum, ...payload } = input;
|
|
9908
|
+
if (checksum !== (0, import_node_crypto5.createHash)("sha256").update(JSON.stringify(payload)).digest("hex") || payload.schema !== "tradejs-portfolio-backtest/v1") {
|
|
9909
|
+
throw new Error("Control portfolio report checksum or schema mismatch");
|
|
9910
|
+
}
|
|
9911
|
+
const control = payload;
|
|
9912
|
+
if (control.lineage?.batchCount !== 1 || candidate.lineage.batchCount !== 1 || control.lineage.cacheOnly !== true || candidate.lineage.cacheOnly !== true) {
|
|
9913
|
+
throw new Error(
|
|
9914
|
+
"Portfolio comparison requires two cache-only reports from a single shared batch"
|
|
9915
|
+
);
|
|
9916
|
+
}
|
|
9917
|
+
for (const field of [
|
|
9918
|
+
"connector",
|
|
9919
|
+
"interval",
|
|
9920
|
+
"tickers",
|
|
9921
|
+
"candleCoverage",
|
|
9922
|
+
"binanceMarketContextRows",
|
|
9923
|
+
"maxLossValue"
|
|
9924
|
+
]) {
|
|
9925
|
+
if (candidate.lineage[field] === void 0 || key(control.lineage[field]) !== key(candidate.lineage[field])) {
|
|
9926
|
+
throw new Error(`Portfolio comparison scope mismatch: ${field}`);
|
|
9927
|
+
}
|
|
9928
|
+
}
|
|
9929
|
+
if (key(control.window) !== key(candidate.window))
|
|
9930
|
+
throw new Error("Portfolio comparison window mismatch");
|
|
9931
|
+
const before = control.lineage.strategies;
|
|
9932
|
+
const after = candidate.lineage.strategies;
|
|
9933
|
+
if (!Array.isArray(before) || !Array.isArray(after) || key(before.map((s) => s.strategyName)) !== key(after.map((s) => s.strategyName))) {
|
|
9934
|
+
throw new Error(
|
|
9935
|
+
"Portfolio comparison requires the same ordered strategies"
|
|
9936
|
+
);
|
|
9937
|
+
}
|
|
9938
|
+
const changed = after.filter((s, index) => key(s) !== key(before[index]));
|
|
9939
|
+
if (changed.length > 1)
|
|
9940
|
+
throw new Error(
|
|
9941
|
+
"Portfolio comparison allows at most one strategy replacement"
|
|
9942
|
+
);
|
|
9943
|
+
if (after.some(
|
|
9944
|
+
(s, index) => s.maxLossValue !== before[index].maxLossValue || key(s.effectiveConfig.PORTFOLIO_POLICY) !== key(before[index].effectiveConfig.PORTFOLIO_POLICY)
|
|
9945
|
+
)) {
|
|
9946
|
+
throw new Error(
|
|
9947
|
+
"Portfolio comparison requires unchanged risk and portfolio policies"
|
|
9948
|
+
);
|
|
9949
|
+
}
|
|
9950
|
+
const all = (report) => {
|
|
9951
|
+
const row = report.metrics.portfolio.find(
|
|
9952
|
+
(r) => r.window === "full" && r.cohort === "ALL"
|
|
9953
|
+
);
|
|
9954
|
+
if (!row) throw new Error("Missing full ALL portfolio metrics");
|
|
9955
|
+
return row;
|
|
9956
|
+
};
|
|
9957
|
+
const a = all(control), b = all(candidate);
|
|
9958
|
+
const toPnl = (report) => report.trades.map((trade) => ({
|
|
9959
|
+
exitTimestamp: trade.exitedAt,
|
|
9960
|
+
netProfit: trade.pnl
|
|
9961
|
+
}));
|
|
9962
|
+
const strategyDelta = candidate.metrics.strategies.filter((row) => row.window === "full").map((row) => {
|
|
9963
|
+
const previous = control.metrics.strategies.find(
|
|
9964
|
+
(r) => r.window === row.window && r.scope === row.scope && r.cohort === row.cohort
|
|
9965
|
+
);
|
|
9966
|
+
return {
|
|
9967
|
+
strategy: row.scope,
|
|
9968
|
+
cohort: row.cohort,
|
|
9969
|
+
trades: previous ? row.n - previous.n : null,
|
|
9970
|
+
pnl: previous ? row.pnl - previous.pnl : null
|
|
9971
|
+
};
|
|
9972
|
+
});
|
|
9973
|
+
const markBefore = control.portfolioDiagnostics?.markedMaxDrawdown ?? null;
|
|
9974
|
+
const markAfter = candidate.portfolioDiagnostics?.markedMaxDrawdown ?? null;
|
|
9975
|
+
const displacement = control.portfolioDiagnostics && candidate.portfolioDiagnostics ? compareCapacityAdmissions(
|
|
9976
|
+
control.portfolioDiagnostics,
|
|
9977
|
+
candidate.portfolioDiagnostics,
|
|
9978
|
+
changed[0]?.strategyName
|
|
9979
|
+
) : null;
|
|
9980
|
+
return {
|
|
9981
|
+
controlChecksum: checksum,
|
|
9982
|
+
markedDrawdown: {
|
|
9983
|
+
control: markBefore,
|
|
9984
|
+
candidate: markAfter,
|
|
9985
|
+
delta: markBefore === null || markAfter === null ? null : markAfter - markBefore
|
|
9986
|
+
},
|
|
9987
|
+
capacityDisplacement: displacement,
|
|
9988
|
+
changedStrategy: changed[0]?.strategyName ?? null,
|
|
9989
|
+
delta: {
|
|
9990
|
+
pnl: b.pnl - a.pnl,
|
|
9991
|
+
trades: b.n - a.n,
|
|
9992
|
+
realizedMaxDrawdown: b.realizedMaxDrawdown - a.realizedMaxDrawdown
|
|
9993
|
+
},
|
|
9994
|
+
recovery: {
|
|
9995
|
+
control: longestRecovery(control.curves.portfolioDrawdown),
|
|
9996
|
+
candidate: longestRecovery(candidate.curves.portfolioDrawdown)
|
|
9997
|
+
},
|
|
9998
|
+
strategyDelta,
|
|
9999
|
+
bootstrap: calendarClusterBootstrap({
|
|
10000
|
+
control: toPnl(control),
|
|
10001
|
+
candidate: toPnl(candidate),
|
|
10002
|
+
clusterDays: 7,
|
|
10003
|
+
iterations: 1e3,
|
|
10004
|
+
confidenceLevel: 0.95,
|
|
10005
|
+
seed: String(checksum),
|
|
10006
|
+
start: candidate.window.start,
|
|
10007
|
+
end: candidate.window.end
|
|
10008
|
+
}),
|
|
10009
|
+
limitations: [
|
|
10010
|
+
"Scope and recorded coverage match. Report lineage does not prove equality of every historical source byte.",
|
|
10011
|
+
"Marked drawdown uses last observed candle closes at replay cycle boundaries. It does not measure intra-candle excursions.",
|
|
10012
|
+
"Capacity displacement counts only identical signals of unchanged strategies filled in one run and explicitly rejected by the position cap in the other."
|
|
10013
|
+
]
|
|
10014
|
+
};
|
|
10015
|
+
}
|
|
10016
|
+
|
|
10017
|
+
// src/lib/replay/portfolioReport.ts
|
|
10018
|
+
var DAY_MS5 = 864e5;
|
|
9694
10019
|
var WINDOWS = [365, 180, 90, 30, 7];
|
|
9695
10020
|
var COLORS = [
|
|
9696
10021
|
"#2563eb",
|
|
@@ -9770,7 +10095,7 @@ var calculatePortfolioMetricRow = ({
|
|
|
9770
10095
|
const pnl = selected.reduce((sum, trade) => sum + trade.pnl, 0);
|
|
9771
10096
|
const wins = selected.filter((trade) => trade.pnl > 0).length;
|
|
9772
10097
|
const losses = selected.filter((trade) => trade.pnl <= 0).length;
|
|
9773
|
-
const days = Math.max(1, (end - start) /
|
|
10098
|
+
const days = Math.max(1, (end - start) / DAY_MS5);
|
|
9774
10099
|
return {
|
|
9775
10100
|
window,
|
|
9776
10101
|
scope,
|
|
@@ -9790,7 +10115,7 @@ var windowBounds = (start, end) => [
|
|
|
9790
10115
|
{ label: "full", start, end },
|
|
9791
10116
|
...WINDOWS.map((days) => ({
|
|
9792
10117
|
label: `${days}d`,
|
|
9793
|
-
start: Math.max(start, end - days *
|
|
10118
|
+
start: Math.max(start, end - days * DAY_MS5),
|
|
9794
10119
|
end
|
|
9795
10120
|
}))
|
|
9796
10121
|
];
|
|
@@ -9865,10 +10190,11 @@ var writePortfolioReport = async ({
|
|
|
9865
10190
|
replayResult,
|
|
9866
10191
|
window,
|
|
9867
10192
|
lineage,
|
|
9868
|
-
command
|
|
10193
|
+
command,
|
|
10194
|
+
compareTo
|
|
9869
10195
|
}) => {
|
|
9870
|
-
const outputDir =
|
|
9871
|
-
await
|
|
10196
|
+
const outputDir = import_node_path8.default.resolve(projectRoot, "data/replay/output");
|
|
10197
|
+
await import_promises5.default.mkdir(outputDir, { recursive: true });
|
|
9872
10198
|
const trades = replayResult.strategies.flatMap(
|
|
9873
10199
|
(strategy) => tradesFromPositionLog(strategy.strategyName, strategy.positionLog)
|
|
9874
10200
|
);
|
|
@@ -9940,13 +10266,13 @@ var writePortfolioReport = async ({
|
|
|
9940
10266
|
});
|
|
9941
10267
|
const prefix = `${timestamp}-portfolio-backtest`;
|
|
9942
10268
|
const paths = {
|
|
9943
|
-
markdown:
|
|
9944
|
-
json:
|
|
9945
|
-
equitySvg:
|
|
9946
|
-
drawdownSvg:
|
|
9947
|
-
html:
|
|
10269
|
+
markdown: import_node_path8.default.join(outputDir, `${prefix}.md`),
|
|
10270
|
+
json: import_node_path8.default.join(outputDir, `${prefix}.json`),
|
|
10271
|
+
equitySvg: import_node_path8.default.join(outputDir, `${prefix}-equity.svg`),
|
|
10272
|
+
drawdownSvg: import_node_path8.default.join(outputDir, `${prefix}-drawdown.svg`),
|
|
10273
|
+
html: import_node_path8.default.join(outputDir, `${prefix}.html`)
|
|
9948
10274
|
};
|
|
9949
|
-
const
|
|
10275
|
+
const baseReport = {
|
|
9950
10276
|
schema: "tradejs-portfolio-backtest/v1",
|
|
9951
10277
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9952
10278
|
command,
|
|
@@ -9968,9 +10294,17 @@ var writePortfolioReport = async ({
|
|
|
9968
10294
|
portfolioDrawdown: drawdownCurve(portfolioEquity),
|
|
9969
10295
|
strategies: strategySeries.map(({ name, points }) => ({ name, points }))
|
|
9970
10296
|
},
|
|
9971
|
-
trades
|
|
10297
|
+
trades,
|
|
10298
|
+
...replayResult.portfolioDiagnostics ? { portfolioDiagnostics: replayResult.portfolioDiagnostics } : {}
|
|
9972
10299
|
};
|
|
9973
|
-
const
|
|
10300
|
+
const comparison = compareTo ? comparePortfolioReports(
|
|
10301
|
+
JSON.parse(
|
|
10302
|
+
await import_promises5.default.readFile(import_node_path8.default.resolve(projectRoot, compareTo), "utf8")
|
|
10303
|
+
),
|
|
10304
|
+
baseReport
|
|
10305
|
+
) : void 0;
|
|
10306
|
+
const report = { ...baseReport, ...comparison ? { comparison } : {} };
|
|
10307
|
+
const checksum = (0, import_node_crypto6.createHash)("sha256").update(JSON.stringify(report)).digest("hex");
|
|
9974
10308
|
const semantics = "Production strategy order, at most one open position per symbol, and fixed-risk symbol batches merged by canonical realized PnL.";
|
|
9975
10309
|
const markdown = `# Portfolio Backtest
|
|
9976
10310
|
|
|
@@ -9982,9 +10316,9 @@ var writePortfolioReport = async ({
|
|
|
9982
10316
|
- Report checksum: \`${checksum}\`
|
|
9983
10317
|
- Command: \`${command}\`
|
|
9984
10318
|
|
|
9985
|
-
})
|
|
9986
10320
|
|
|
9987
|
-
})
|
|
9988
10322
|
|
|
9989
10323
|
## Portfolio metrics
|
|
9990
10324
|
|
|
@@ -9995,17 +10329,33 @@ ${markdownTable(portfolioRows)}
|
|
|
9995
10329
|
${markdownTable(strategyRows)}
|
|
9996
10330
|
`;
|
|
9997
10331
|
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Portfolio Backtest</title><style>body{font-family:Inter,system-ui,sans-serif;margin:32px;color:#0f172a}h1,h2{margin-top:32px}.meta{color:#475569}table{border-collapse:collapse;width:100%;font-size:13px}th,td{padding:8px;border:1px solid #e2e8f0;text-align:right}th:nth-child(-n+3),td:nth-child(-n+3){text-align:left}.chart{overflow:auto;border:1px solid #e2e8f0;margin:16px 0}</style></head><body><h1>Portfolio Backtest</h1><p class="meta">${escapeHtml(report.window.startIso)} .. ${escapeHtml(report.window.endIso)} \xB7 ${report.counts.strategies} strategies \xB7 ${trades.length} completed trades \xB7 MAX_LOSS_VALUE=${escapeHtml(String(lineage.maxLossValue))}</p><p class="meta">${escapeHtml(semantics)}</p><div class="chart">${equitySvg}</div><div class="chart">${drawdownSvg}</div><h2>Portfolio metrics</h2>${metricTable(portfolioRows)}<h2>Strategy metrics</h2>${metricTable(strategyRows)}<h2>Lineage</h2><pre>${escapeHtml(JSON.stringify(lineage, null, 2))}</pre></body></html>`;
|
|
10332
|
+
const comparisonMarkdown = comparison ? `
|
|
10333
|
+
## Paired portfolio comparison
|
|
10334
|
+
|
|
10335
|
+
- Net PnL delta: ${display(comparison.delta.pnl)}
|
|
10336
|
+
- Closed trades delta: ${comparison.delta.trades}
|
|
10337
|
+
- Realized MaxDD delta: ${display(comparison.delta.realizedMaxDrawdown)}
|
|
10338
|
+
|
|
10339
|
+
${comparison.limitations.join(" ")}
|
|
10340
|
+
` : "";
|
|
9998
10341
|
await Promise.all([
|
|
9999
|
-
|
|
10000
|
-
|
|
10342
|
+
import_promises5.default.writeFile(paths.markdown, markdown + comparisonMarkdown, "utf8"),
|
|
10343
|
+
import_promises5.default.writeFile(
|
|
10001
10344
|
paths.json,
|
|
10002
10345
|
`${JSON.stringify({ ...report, checksum }, null, 2)}
|
|
10003
10346
|
`,
|
|
10004
10347
|
"utf8"
|
|
10005
10348
|
),
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10349
|
+
import_promises5.default.writeFile(paths.equitySvg, equitySvg, "utf8"),
|
|
10350
|
+
import_promises5.default.writeFile(paths.drawdownSvg, drawdownSvg, "utf8"),
|
|
10351
|
+
import_promises5.default.writeFile(
|
|
10352
|
+
paths.html,
|
|
10353
|
+
comparison ? html.replace(
|
|
10354
|
+
"</body>",
|
|
10355
|
+
`<h2>Paired portfolio comparison</h2><pre>${escapeHtml(JSON.stringify(comparison, null, 2))}</pre></body>`
|
|
10356
|
+
) : html,
|
|
10357
|
+
"utf8"
|
|
10358
|
+
)
|
|
10009
10359
|
]);
|
|
10010
10360
|
return { ...paths, checksum };
|
|
10011
10361
|
};
|
|
@@ -10048,8 +10398,8 @@ var cachedTickerSymbols = async ({
|
|
|
10048
10398
|
import_redis6.redisKeys.tickerUniverse(replayUserName, name, universe, "default"),
|
|
10049
10399
|
import_redis6.redisKeys.tickerUniverse(replayUserName, name)
|
|
10050
10400
|
]);
|
|
10051
|
-
for (const
|
|
10052
|
-
const cached = await (0, import_redis6.getData)(
|
|
10401
|
+
for (const key2 of [...new Set(keys)]) {
|
|
10402
|
+
const cached = await (0, import_redis6.getData)(key2, null);
|
|
10053
10403
|
const symbols = (cached?.tickers ?? []).map(
|
|
10054
10404
|
(ticker) => String(ticker.symbol ?? "").trim().toUpperCase()
|
|
10055
10405
|
).filter(Boolean);
|
|
@@ -10118,7 +10468,8 @@ var finishReplay = async ({
|
|
|
10118
10468
|
replayResult,
|
|
10119
10469
|
window,
|
|
10120
10470
|
lineage: portfolioLineage ?? {},
|
|
10121
|
-
command: process.argv.join(" ")
|
|
10471
|
+
command: process.argv.join(" "),
|
|
10472
|
+
compareTo: replayFlags.portfolioCompareTo
|
|
10122
10473
|
}) : null;
|
|
10123
10474
|
if (portfolioReport) {
|
|
10124
10475
|
console.log(import_chalk13.default.green(`Portfolio report: ${portfolioReport.html}`));
|
|
@@ -10402,6 +10753,18 @@ var replayBacktest = async () => {
|
|
|
10402
10753
|
`Invalid --portfolioBatchSize: ${String(replayFlags.portfolioBatchSize)}`
|
|
10403
10754
|
);
|
|
10404
10755
|
}
|
|
10756
|
+
if (replayFlags.portfolioCompareTo && (!replayFlags.portfolioReport || !replayFlags.cacheOnly)) {
|
|
10757
|
+
throw new Error(
|
|
10758
|
+
"--portfolioCompareTo requires --portfolioReport and --cacheOnly"
|
|
10759
|
+
);
|
|
10760
|
+
}
|
|
10761
|
+
if (replayFlags.portfolioReport && preparedRun.tickers.length > portfolioBatchSize && (replayFlags.portfolioCompareTo || replayStrategies.some(
|
|
10762
|
+
({ strategyConfig }) => Boolean(strategyConfig.PORTFOLIO_POLICY)
|
|
10763
|
+
))) {
|
|
10764
|
+
throw new Error(
|
|
10765
|
+
"Portfolio policies and comparisons require a single shared batch; set --portfolioBatchSize to at least the number of tickers"
|
|
10766
|
+
);
|
|
10767
|
+
}
|
|
10405
10768
|
const runtimeStrategies = replayFlags.tickers && !replayFlags.portfolioReport ? replayStrategies.map(
|
|
10406
10769
|
({ selection: _selection, ...strategy }) => strategy
|
|
10407
10770
|
) : replayStrategies;
|
|
@@ -10425,6 +10788,9 @@ var replayBacktest = async () => {
|
|
|
10425
10788
|
showProgress: !replayFlags.portfolioReport,
|
|
10426
10789
|
collectSkipEvidence: !replayFlags.portfolioReport,
|
|
10427
10790
|
collectSignals: !replayFlags.portfolioReport,
|
|
10791
|
+
collectPortfolioDiagnostics: Boolean(
|
|
10792
|
+
replayFlags.portfolioReport && tickerBatches.length === 1
|
|
10793
|
+
),
|
|
10428
10794
|
decisionTraceTargets: replayFlags.portfolioReport ? void 0 : evidenceMetadata?.decisionTraceTargets
|
|
10429
10795
|
});
|
|
10430
10796
|
return replayFlags.portfolioReport ? compactHistoricalReplayResultForPortfolio(result) : result;
|
|
@@ -10462,6 +10828,9 @@ var replayBacktest = async () => {
|
|
|
10462
10828
|
cacheOnly: Boolean(replayFlags.cacheOnly),
|
|
10463
10829
|
portfolioSemantics: "production strategy order and one concurrent position per symbol; fixed-risk symbol batches are merged by canonical realized PnL",
|
|
10464
10830
|
portfolioBatchSize,
|
|
10831
|
+
batchCount: tickerBatches.length,
|
|
10832
|
+
connector: preparedRun.connectorName,
|
|
10833
|
+
interval: replayInterval,
|
|
10465
10834
|
tickers: preparedRun.tickers,
|
|
10466
10835
|
candleCoverage: candleCoverage ?? null,
|
|
10467
10836
|
binanceMarketContextRows: portfolioMarketContextRows ?? null,
|