@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
|
@@ -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
|
};
|
|
@@ -596,9 +596,9 @@ var loadRuntimeTrades = async (userName, {
|
|
|
596
596
|
const tradePrefix = import_redis.redisKeys.runtimeTrades(userName);
|
|
597
597
|
const bucketPrefix = import_redis.redisKeys.runtimeTradeBuckets(userName);
|
|
598
598
|
const keys = (await (0, import_redis.getKeys)(tradePrefix)).filter(
|
|
599
|
-
(
|
|
599
|
+
(key2) => !key2.startsWith(bucketPrefix)
|
|
600
600
|
);
|
|
601
|
-
const trades = await Promise.all(keys.map((
|
|
601
|
+
const trades = await Promise.all(keys.map((key2) => (0, import_redis.getData)(key2, null)));
|
|
602
602
|
return trades.filter(isRuntimeTradeRecord).filter(filterByWindow).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
|
|
603
603
|
};
|
|
604
604
|
|
|
@@ -1520,13 +1520,12 @@ var getReferenceSymbols = () => {
|
|
|
1520
1520
|
};
|
|
1521
1521
|
var intervalToMarketFeatureInterval = (interval) => {
|
|
1522
1522
|
const normalized = String(interval).trim().toLowerCase();
|
|
1523
|
-
if (normalized === "1" || normalized === "1m") return
|
|
1523
|
+
if (normalized === "1" || normalized === "1m") return null;
|
|
1524
1524
|
if (normalized === "5" || normalized === "5m") return "5m";
|
|
1525
1525
|
if (normalized === "60" || normalized === "1h") return "1h";
|
|
1526
1526
|
return "15m";
|
|
1527
1527
|
};
|
|
1528
1528
|
var marketIntervalToConnectorInterval = (interval) => {
|
|
1529
|
-
if (interval === "1m") return "1";
|
|
1530
1529
|
if (interval === "5m") return "5";
|
|
1531
1530
|
if (interval === "1h") return "60";
|
|
1532
1531
|
return "15";
|
|
@@ -1691,6 +1690,7 @@ var backfillBinanceMarketContext = async (params, enabled) => {
|
|
|
1691
1690
|
return skippedBackfillResult();
|
|
1692
1691
|
}
|
|
1693
1692
|
const interval = intervalToMarketFeatureInterval(params.interval);
|
|
1693
|
+
if (interval === null) return skippedBackfillResult();
|
|
1694
1694
|
const intervalMs = MARKET_FEATURE_INTERVAL_MS[interval];
|
|
1695
1695
|
const { breadthStartMs, tradeFlowStartMs, endMs } = resolveBinanceMarketContextBackfillWindow(params);
|
|
1696
1696
|
if (endMs <= Math.min(breadthStartMs, tradeFlowStartMs)) {
|
|
@@ -1885,8 +1885,8 @@ var backfillBinanceMarketContext = async (params, enabled) => {
|
|
|
1885
1885
|
})
|
|
1886
1886
|
);
|
|
1887
1887
|
const missingChunks = chunks.filter((chunk) => {
|
|
1888
|
-
const
|
|
1889
|
-
return plans.some((plan) => plan.missingChunkKeys.has(
|
|
1888
|
+
const key2 = `${chunk.startMs}:${chunk.endMs}`;
|
|
1889
|
+
return plans.some((plan) => plan.missingChunkKeys.has(key2));
|
|
1890
1890
|
});
|
|
1891
1891
|
const bar = new import_progress.default(
|
|
1892
1892
|
"breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
|
|
@@ -2242,13 +2242,13 @@ var networkErrorCodes = /* @__PURE__ */ new Set([
|
|
|
2242
2242
|
"EAI_AGAIN",
|
|
2243
2243
|
"EHOSTUNREACH"
|
|
2244
2244
|
]);
|
|
2245
|
-
var getNestedErrorValue = (error,
|
|
2245
|
+
var getNestedErrorValue = (error, key2) => {
|
|
2246
2246
|
let current = error;
|
|
2247
2247
|
const seen = /* @__PURE__ */ new Set();
|
|
2248
2248
|
while (current && typeof current === "object" && !seen.has(current)) {
|
|
2249
2249
|
seen.add(current);
|
|
2250
2250
|
const record = current;
|
|
2251
|
-
const value = record[
|
|
2251
|
+
const value = record[key2];
|
|
2252
2252
|
if (typeof value === "string" && value.trim()) {
|
|
2253
2253
|
return value.trim();
|
|
2254
2254
|
}
|
|
@@ -2381,11 +2381,11 @@ var selectBestMarket = (candidates, exchangePriority) => {
|
|
|
2381
2381
|
var buildMatches = (tickers, markets, exchangePriority) => {
|
|
2382
2382
|
const marketByTicker = /* @__PURE__ */ new Map();
|
|
2383
2383
|
for (const market of markets) {
|
|
2384
|
-
const
|
|
2385
|
-
if (!
|
|
2386
|
-
const list = marketByTicker.get(
|
|
2384
|
+
const key2 = String(market.symbol_on_exchange || "").trim().toUpperCase();
|
|
2385
|
+
if (!key2) continue;
|
|
2386
|
+
const list = marketByTicker.get(key2) ?? [];
|
|
2387
2387
|
list.push(market);
|
|
2388
|
-
marketByTicker.set(
|
|
2388
|
+
marketByTicker.set(key2, list);
|
|
2389
2389
|
}
|
|
2390
2390
|
const matches = [];
|
|
2391
2391
|
const unmatched = [];
|
|
@@ -2717,7 +2717,7 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
|
|
|
2717
2717
|
while (cursor < window.toMs) {
|
|
2718
2718
|
const toMs = Math.min(window.toMs, cursor + batchMs);
|
|
2719
2719
|
const missingRanges = batch.map((item) => {
|
|
2720
|
-
const
|
|
2720
|
+
const key2 = coverageKey({
|
|
2721
2721
|
symbol: item.symbol,
|
|
2722
2722
|
interval,
|
|
2723
2723
|
fromMs: cursor,
|
|
@@ -2732,7 +2732,7 @@ var backfillDerivativesContext = async (params, enabled, mode) => {
|
|
|
2732
2732
|
fromMs: cursor,
|
|
2733
2733
|
toMs,
|
|
2734
2734
|
nowMs: coverageNowMs,
|
|
2735
|
-
dataCoverageKeyExists: coverageKeys.has(
|
|
2735
|
+
dataCoverageKeyExists: coverageKeys.has(key2),
|
|
2736
2736
|
dataCoverageRanges: coverageRangesBySymbol.get(normalizedSymbol),
|
|
2737
2737
|
liquidationCoverageRanges: liquidationCoverageRangesBySymbol.get(normalizedSymbol),
|
|
2738
2738
|
edges: edgesBySymbol.get(normalizedSymbol)
|
|
@@ -3031,7 +3031,7 @@ var toIntOrNull = (value) => {
|
|
|
3031
3031
|
return numeric == null ? null : Math.trunc(numeric);
|
|
3032
3032
|
};
|
|
3033
3033
|
var getRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3034
|
-
var getNestedRecord = (value,
|
|
3034
|
+
var getNestedRecord = (value, key2) => getRecord(value[key2]);
|
|
3035
3035
|
var safeDivide2 = (numerator, denominator) => numerator != null && denominator != null && denominator > 0 ? numerator / denominator : null;
|
|
3036
3036
|
var sumFinite = (values) => values.reduce((sum, value) => sum + (value ?? 0), 0);
|
|
3037
3037
|
var toExchangeLiquidityRegime = ({
|
|
@@ -3138,7 +3138,7 @@ var coverageRowsToKeySet = (rows) => new Set(
|
|
|
3138
3138
|
);
|
|
3139
3139
|
var hasBackfillCoverage = (coverageKeys, params) => coverageKeys.has(coverageKey2(params));
|
|
3140
3140
|
var hasRecentBackfillCoverage = (rows, params) => {
|
|
3141
|
-
const
|
|
3141
|
+
const key2 = coverageKey2(params);
|
|
3142
3142
|
return rows.some((row) => {
|
|
3143
3143
|
if (!isMarketContextBackfillCoverageDataBearing(row)) return false;
|
|
3144
3144
|
if (coverageKey2({
|
|
@@ -3147,7 +3147,7 @@ var hasRecentBackfillCoverage = (rows, params) => {
|
|
|
3147
3147
|
interval: row.interval,
|
|
3148
3148
|
fromMs: row.fromMs,
|
|
3149
3149
|
toMs: row.toMs
|
|
3150
|
-
}) !==
|
|
3150
|
+
}) !== key2) {
|
|
3151
3151
|
return false;
|
|
3152
3152
|
}
|
|
3153
3153
|
const checkedAtMs = Number(row.checkedAtMs);
|
|
@@ -3184,27 +3184,27 @@ var createCoinMarketCapFetchStats = () => ({
|
|
|
3184
3184
|
totalCredits: 0,
|
|
3185
3185
|
byPath: /* @__PURE__ */ new Map()
|
|
3186
3186
|
});
|
|
3187
|
-
var getCoinMarketCapPathLabel = (
|
|
3188
|
-
if (
|
|
3189
|
-
if (
|
|
3190
|
-
if (
|
|
3191
|
-
if (
|
|
3192
|
-
if (
|
|
3193
|
-
if (
|
|
3194
|
-
return
|
|
3195
|
-
};
|
|
3196
|
-
var recordCoinMarketCapFetchStats = (stats,
|
|
3187
|
+
var getCoinMarketCapPathLabel = (path9) => {
|
|
3188
|
+
if (path9.includes("global-metrics")) return "global";
|
|
3189
|
+
if (path9.includes("cryptocurrency/quotes")) return "reference";
|
|
3190
|
+
if (path9.includes("exchange/quotes")) return "exchangeLiquidity";
|
|
3191
|
+
if (path9.includes("fear-and-greed")) return "fearGreed";
|
|
3192
|
+
if (path9.includes("cmc100")) return "cmc100";
|
|
3193
|
+
if (path9.includes("cmc20")) return "cmc20";
|
|
3194
|
+
return path9;
|
|
3195
|
+
};
|
|
3196
|
+
var recordCoinMarketCapFetchStats = (stats, path9, creditCount) => {
|
|
3197
3197
|
if (!stats) return;
|
|
3198
|
-
const existing = stats.byPath.get(
|
|
3198
|
+
const existing = stats.byPath.get(path9) ?? { requests: 0, credits: 0 };
|
|
3199
3199
|
existing.requests += 1;
|
|
3200
3200
|
existing.credits += creditCount ?? 0;
|
|
3201
3201
|
stats.requests += 1;
|
|
3202
3202
|
stats.totalCredits += creditCount ?? 0;
|
|
3203
|
-
stats.byPath.set(
|
|
3203
|
+
stats.byPath.set(path9, existing);
|
|
3204
3204
|
};
|
|
3205
3205
|
var formatCoinMarketCapFetchStats = (stats) => {
|
|
3206
3206
|
const byPath = [...stats.byPath.entries()].map(
|
|
3207
|
-
([
|
|
3207
|
+
([path9, item]) => `${getCoinMarketCapPathLabel(path9)}=${item.requests}req/${item.credits}cr`
|
|
3208
3208
|
).join(" ");
|
|
3209
3209
|
return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
|
|
3210
3210
|
};
|
|
@@ -3226,8 +3226,8 @@ var logBackfillSourceDone = (params) => {
|
|
|
3226
3226
|
var markBackfillCoverage = (rows) => (0, import_marketContext2.upsertMarketContextBackfillCoverage)(rows);
|
|
3227
3227
|
var coinMarketCapFetch = async (params) => {
|
|
3228
3228
|
const url = new URL(`${getBaseUrl()}${params.path}`);
|
|
3229
|
-
for (const [
|
|
3230
|
-
url.searchParams.set(
|
|
3229
|
+
for (const [key2, value] of Object.entries(params.searchParams)) {
|
|
3230
|
+
url.searchParams.set(key2, value);
|
|
3231
3231
|
}
|
|
3232
3232
|
for (let attempt = 0; attempt <= getMaxRetries(); attempt += 1) {
|
|
3233
3233
|
const waitMs = Math.max(
|
|
@@ -3271,7 +3271,7 @@ var coinMarketCapFetch = async (params) => {
|
|
|
3271
3271
|
};
|
|
3272
3272
|
var fetchCoinMarketCapIndexRows = async (params) => {
|
|
3273
3273
|
const rows = [];
|
|
3274
|
-
const maxChunkDays =
|
|
3274
|
+
const maxChunkDays = 4;
|
|
3275
3275
|
const totalChunks = Math.max(
|
|
3276
3276
|
1,
|
|
3277
3277
|
Math.ceil(
|
|
@@ -3408,10 +3408,10 @@ var coinMarketCapExchangeQuotesPayloadToLiquidityRows = (payload, interval = "1d
|
|
|
3408
3408
|
const ts = typeof tsRaw === "string" ? new Date(tsRaw) : null;
|
|
3409
3409
|
if (!ts || Number.isNaN(ts.getTime())) continue;
|
|
3410
3410
|
const volumeUsd = toFiniteNumberOrNull(usd.volume_24h) ?? toFiniteNumberOrNull(usd.volume_24h_adjusted) ?? toFiniteNumberOrNull(usd.spot_volume_usd) ?? toFiniteNumberOrNull(usd.reported_volume_24h);
|
|
3411
|
-
const
|
|
3412
|
-
const rows = rowsByTs.get(
|
|
3411
|
+
const key2 = ts.getTime();
|
|
3412
|
+
const rows = rowsByTs.get(key2) ?? [];
|
|
3413
3413
|
rows.push({ slug, name, volumeUsd });
|
|
3414
|
-
rowsByTs.set(
|
|
3414
|
+
rowsByTs.set(key2, rows);
|
|
3415
3415
|
}
|
|
3416
3416
|
}
|
|
3417
3417
|
return [...rowsByTs.entries()].sort(([left], [right]) => left - right).map(([ts, exchanges]) => {
|
|
@@ -4814,6 +4814,10 @@ import_args.default.option(
|
|
|
4814
4814
|
"Write aggregate portfolio metrics and SVG charts; skip runtime/exchange comparison",
|
|
4815
4815
|
false
|
|
4816
4816
|
);
|
|
4817
|
+
import_args.default.option(
|
|
4818
|
+
"portfolioCompareTo",
|
|
4819
|
+
"Compare a full shared replay with a prior portfolio report JSON"
|
|
4820
|
+
);
|
|
4817
4821
|
import_args.default.option(
|
|
4818
4822
|
["B", "portfolioBatchSize"],
|
|
4819
4823
|
"Maximum number of symbols held in memory by one portfolio replay batch",
|
|
@@ -4859,7 +4863,7 @@ var normalizeForStableJson = (value) => {
|
|
|
4859
4863
|
}
|
|
4860
4864
|
if (value && typeof value === "object") {
|
|
4861
4865
|
return Object.fromEntries(
|
|
4862
|
-
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([
|
|
4866
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key2, entry]) => [key2, normalizeForStableJson(entry)])
|
|
4863
4867
|
);
|
|
4864
4868
|
}
|
|
4865
4869
|
return value;
|
|
@@ -5017,9 +5021,9 @@ var buildRuntimeEvidenceLineageScopes = ({
|
|
|
5017
5021
|
if (!lineage || !Number.isFinite(timestamp)) {
|
|
5018
5022
|
return;
|
|
5019
5023
|
}
|
|
5020
|
-
const
|
|
5021
|
-
const existing = scopes.get(
|
|
5022
|
-
scopes.set(
|
|
5024
|
+
const key2 = `${deploymentId ?? "default"}::${accountId ?? "default"}::${strategy}::${symbol}::${runtimeLineageKey(lineage)}`;
|
|
5025
|
+
const existing = scopes.get(key2);
|
|
5026
|
+
scopes.set(key2, {
|
|
5023
5027
|
strategy,
|
|
5024
5028
|
symbol,
|
|
5025
5029
|
...deploymentId ? { deploymentId } : {},
|
|
@@ -5182,12 +5186,13 @@ var getPositionPnlSnapshot = ({
|
|
|
5182
5186
|
direction: position.direction
|
|
5183
5187
|
};
|
|
5184
5188
|
};
|
|
5185
|
-
var createPortfolioReplayConnector = (connector) => {
|
|
5189
|
+
var createPortfolioReplayConnector = (connector, observeEquity = false) => {
|
|
5186
5190
|
let state = {};
|
|
5187
5191
|
let amount = import_constants3.INITIAL_BACKTEST_AMOUNT;
|
|
5188
5192
|
let executionTimestamp = null;
|
|
5189
5193
|
const positionsBySymbol = /* @__PURE__ */ new Map();
|
|
5190
5194
|
const currentPriceBySymbol = /* @__PURE__ */ new Map();
|
|
5195
|
+
const equityMarks = observeEquity ? /* @__PURE__ */ new Map() : void 0;
|
|
5191
5196
|
const markedAtBySymbol = /* @__PURE__ */ new Map();
|
|
5192
5197
|
const entryFacts = /* @__PURE__ */ new Map();
|
|
5193
5198
|
let portfolioObserved = false;
|
|
@@ -5608,11 +5613,25 @@ var createPortfolioReplayConnector = (connector) => {
|
|
|
5608
5613
|
advanceMarket: async ({ symbol, candle }) => {
|
|
5609
5614
|
executionTimestamp = candle.timestamp;
|
|
5610
5615
|
markedAtBySymbol.set(symbol, candle.timestamp);
|
|
5616
|
+
equityMarks?.set(symbol, candle.close);
|
|
5611
5617
|
currentPriceBySymbol.set(symbol, candle.close);
|
|
5612
5618
|
await checkSl({ symbol, candle });
|
|
5613
5619
|
await checkTp({ symbol, candle });
|
|
5614
5620
|
currentPriceBySymbol.set(symbol, candle.close);
|
|
5615
5621
|
},
|
|
5622
|
+
getMarkedEquity: () => {
|
|
5623
|
+
if (!equityMarks) return null;
|
|
5624
|
+
let equity = amount - import_constants3.INITIAL_BACKTEST_AMOUNT;
|
|
5625
|
+
for (const { position } of positionsBySymbol.values()) {
|
|
5626
|
+
const mark = equityMarks.get(position.symbol);
|
|
5627
|
+
if (mark == null || !Number.isFinite(mark)) return null;
|
|
5628
|
+
equity += getPositionPnlSnapshot({
|
|
5629
|
+
position,
|
|
5630
|
+
currentPrice: mark
|
|
5631
|
+
}).unrealizedPnl;
|
|
5632
|
+
}
|
|
5633
|
+
return (0, import_math.round)(equity);
|
|
5634
|
+
},
|
|
5616
5635
|
getReplayArtifacts: () => ({
|
|
5617
5636
|
orderLog,
|
|
5618
5637
|
positionLog,
|
|
@@ -5826,6 +5845,21 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5826
5845
|
context.collectSkipEvidence
|
|
5827
5846
|
);
|
|
5828
5847
|
const decisionTrace = createReplayDecisionTrace(context.decisionTraceTargets);
|
|
5848
|
+
const portfolioDiagnostics = context.collectPortfolioDiagnostics ? { markedMaxDrawdown: 0, unpricedCycles: 0, admissions: [] } : void 0;
|
|
5849
|
+
let equityPeak = 0;
|
|
5850
|
+
const observeEquity = () => {
|
|
5851
|
+
if (!portfolioDiagnostics) return;
|
|
5852
|
+
const equity = context.connector.getMarkedEquity();
|
|
5853
|
+
if (equity === null) {
|
|
5854
|
+
portfolioDiagnostics.unpricedCycles++;
|
|
5855
|
+
return;
|
|
5856
|
+
}
|
|
5857
|
+
equityPeak = Math.max(equityPeak, equity);
|
|
5858
|
+
portfolioDiagnostics.markedMaxDrawdown = Math.max(
|
|
5859
|
+
portfolioDiagnostics.markedMaxDrawdown ?? 0,
|
|
5860
|
+
equityPeak - equity
|
|
5861
|
+
);
|
|
5862
|
+
};
|
|
5829
5863
|
const reportProgress = (timestamp) => {
|
|
5830
5864
|
if (adapters.progress.enabled === false) return;
|
|
5831
5865
|
adapters.progress.tick({
|
|
@@ -5869,6 +5903,7 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5869
5903
|
}
|
|
5870
5904
|
}
|
|
5871
5905
|
advanceAbortedCycle(cycleSymbols, timestamp);
|
|
5906
|
+
observeEquity();
|
|
5872
5907
|
reportProgress(timestamp);
|
|
5873
5908
|
continue;
|
|
5874
5909
|
}
|
|
@@ -5912,6 +5947,21 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5912
5947
|
result
|
|
5913
5948
|
});
|
|
5914
5949
|
if (result && typeof result !== "string") {
|
|
5950
|
+
if (portfolioDiagnostics) {
|
|
5951
|
+
const status = result.orderFailureReason === "PORTFOLIO_POSITION_LIMIT" ? "capacity-rejected" : result.orderStatus === "completed" ? "filled" : void 0;
|
|
5952
|
+
if (status)
|
|
5953
|
+
portfolioDiagnostics.admissions.push({
|
|
5954
|
+
strategy: strategyRuntime.strategyName,
|
|
5955
|
+
status,
|
|
5956
|
+
key: JSON.stringify([
|
|
5957
|
+
strategyRuntime.strategyName,
|
|
5958
|
+
result.symbol,
|
|
5959
|
+
result.direction,
|
|
5960
|
+
result.timestamp,
|
|
5961
|
+
result.signalId
|
|
5962
|
+
])
|
|
5963
|
+
});
|
|
5964
|
+
}
|
|
5915
5965
|
result.runtimeLineage = strategyRuntime.runtimeLineage;
|
|
5916
5966
|
await adapters.enrichSignal(result);
|
|
5917
5967
|
cycleSignals.push(result);
|
|
@@ -5926,12 +5976,16 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5926
5976
|
status: "completed",
|
|
5927
5977
|
durationMs: adapters.clock.now() - cycleStartedAt
|
|
5928
5978
|
});
|
|
5979
|
+
observeEquity();
|
|
5929
5980
|
reportProgress(timestamp);
|
|
5930
5981
|
}
|
|
5931
5982
|
} finally {
|
|
5932
5983
|
releaseHistoricalReplayCaches(context.plan, adapters);
|
|
5933
5984
|
}
|
|
5985
|
+
if (portfolioDiagnostics?.unpricedCycles)
|
|
5986
|
+
portfolioDiagnostics.markedMaxDrawdown = null;
|
|
5934
5987
|
return {
|
|
5988
|
+
...portfolioDiagnostics ? { portfolioDiagnostics } : {},
|
|
5935
5989
|
signals,
|
|
5936
5990
|
abortedCycles,
|
|
5937
5991
|
replayLineageScopes: skipEvidence.values(),
|
|
@@ -5987,6 +6041,7 @@ var collectHistoricalReplayResult = (context) => ({
|
|
|
5987
6041
|
abortedCycles: context.abortedCycles,
|
|
5988
6042
|
runtimeLineages: context.runtimeLineages,
|
|
5989
6043
|
replayLineageScopes: context.replayLineageScopes,
|
|
6044
|
+
...context.portfolioDiagnostics ? { portfolioDiagnostics: context.portfolioDiagnostics } : {},
|
|
5990
6045
|
...context.replayDecisionTraces?.length ? { replayDecisionTraces: context.replayDecisionTraces } : {}
|
|
5991
6046
|
});
|
|
5992
6047
|
var compactHistoricalReplayResultForPortfolio = (result) => ({
|
|
@@ -6188,6 +6243,7 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6188
6243
|
showProgress = true,
|
|
6189
6244
|
collectSkipEvidence = true,
|
|
6190
6245
|
collectSignals = true,
|
|
6246
|
+
collectPortfolioDiagnostics = false,
|
|
6191
6247
|
decisionTraceTargets
|
|
6192
6248
|
}) => {
|
|
6193
6249
|
const memory = createReplayMemoryProfile();
|
|
@@ -6197,7 +6253,8 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6197
6253
|
const projectHooks = projectConfig.hooks;
|
|
6198
6254
|
const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
|
|
6199
6255
|
const replayConnector = createPortfolioReplayConnector(
|
|
6200
|
-
preparedRun.marketConnector
|
|
6256
|
+
preparedRun.marketConnector,
|
|
6257
|
+
collectPortfolioDiagnostics
|
|
6201
6258
|
);
|
|
6202
6259
|
const connectorName = getConnectorName(preparedRun.marketConnector);
|
|
6203
6260
|
const referenceData = references ?? await loadHistoricalReplayReferences({ preparedRun, interval });
|
|
@@ -6275,6 +6332,7 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6275
6332
|
connector: replayConnector,
|
|
6276
6333
|
collectSkipEvidence,
|
|
6277
6334
|
collectSignals,
|
|
6335
|
+
collectPortfolioDiagnostics,
|
|
6278
6336
|
decisionTraceTargets,
|
|
6279
6337
|
hooks: projectHooks,
|
|
6280
6338
|
hookContext
|
|
@@ -6313,7 +6371,8 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6313
6371
|
abortedCycles: execution.abortedCycles,
|
|
6314
6372
|
runtimeLineages: plan.runtimeLineages,
|
|
6315
6373
|
replayLineageScopes: execution.replayLineageScopes,
|
|
6316
|
-
replayDecisionTraces: execution.replayDecisionTraces
|
|
6374
|
+
replayDecisionTraces: execution.replayDecisionTraces,
|
|
6375
|
+
portfolioDiagnostics: execution.portfolioDiagnostics
|
|
6317
6376
|
});
|
|
6318
6377
|
memory?.("result:collected");
|
|
6319
6378
|
return result;
|
|
@@ -6560,16 +6619,16 @@ var findRuntimeDuplicateGroups = (entries) => {
|
|
|
6560
6619
|
if (entry.source !== "runtime") {
|
|
6561
6620
|
continue;
|
|
6562
6621
|
}
|
|
6563
|
-
const
|
|
6564
|
-
const bucket = groups.get(
|
|
6622
|
+
const key2 = toRuntimeDuplicateKey(entry);
|
|
6623
|
+
const bucket = groups.get(key2) ?? [];
|
|
6565
6624
|
bucket.push(entry);
|
|
6566
|
-
groups.set(
|
|
6625
|
+
groups.set(key2, bucket);
|
|
6567
6626
|
}
|
|
6568
|
-
return [...groups.entries()].filter(([, group]) => group.length > 1).map(([
|
|
6627
|
+
return [...groups.entries()].filter(([, group]) => group.length > 1).map(([key2, group]) => {
|
|
6569
6628
|
const sortedGroup = [...group].sort(sortTradeParityEntries);
|
|
6570
6629
|
const first = sortedGroup[0];
|
|
6571
6630
|
return {
|
|
6572
|
-
key,
|
|
6631
|
+
key: key2,
|
|
6573
6632
|
strategy: first.strategy,
|
|
6574
6633
|
symbol: first.symbol,
|
|
6575
6634
|
direction: first.direction,
|
|
@@ -6608,26 +6667,26 @@ var compareTradeParityEntries = ({
|
|
|
6608
6667
|
const groupedRuntime = /* @__PURE__ */ new Map();
|
|
6609
6668
|
const groupedBacktest = /* @__PURE__ */ new Map();
|
|
6610
6669
|
for (const entry of runtimeEntries) {
|
|
6611
|
-
const
|
|
6612
|
-
const bucket = groupedRuntime.get(
|
|
6670
|
+
const key2 = toGroupKey(entry);
|
|
6671
|
+
const bucket = groupedRuntime.get(key2) ?? [];
|
|
6613
6672
|
bucket.push(entry);
|
|
6614
|
-
groupedRuntime.set(
|
|
6673
|
+
groupedRuntime.set(key2, bucket);
|
|
6615
6674
|
}
|
|
6616
6675
|
for (const entry of backtestEntries) {
|
|
6617
|
-
const
|
|
6618
|
-
const bucket = groupedBacktest.get(
|
|
6676
|
+
const key2 = toGroupKey(entry);
|
|
6677
|
+
const bucket = groupedBacktest.get(key2) ?? [];
|
|
6619
6678
|
bucket.push(entry);
|
|
6620
|
-
groupedBacktest.set(
|
|
6679
|
+
groupedBacktest.set(key2, bucket);
|
|
6621
6680
|
}
|
|
6622
6681
|
const groupKeys = /* @__PURE__ */ new Set([
|
|
6623
6682
|
...groupedRuntime.keys(),
|
|
6624
6683
|
...groupedBacktest.keys()
|
|
6625
6684
|
]);
|
|
6626
|
-
for (const
|
|
6627
|
-
const runtimeGroup = [...groupedRuntime.get(
|
|
6685
|
+
for (const key2 of groupKeys) {
|
|
6686
|
+
const runtimeGroup = [...groupedRuntime.get(key2) ?? []].sort(
|
|
6628
6687
|
(left, right) => left.timestamp - right.timestamp
|
|
6629
6688
|
);
|
|
6630
|
-
const backtestGroup = [...groupedBacktest.get(
|
|
6689
|
+
const backtestGroup = [...groupedBacktest.get(key2) ?? []].sort(
|
|
6631
6690
|
(left, right) => left.timestamp - right.timestamp
|
|
6632
6691
|
);
|
|
6633
6692
|
const unmatchedBacktest = backtestGroup.map((entry) => ({
|
|
@@ -6868,8 +6927,8 @@ var filterBucketKeysByWindow = (bucketKeys, prefix, startTime, endTime) => {
|
|
|
6868
6927
|
if (!allowedDayKeys.size) {
|
|
6869
6928
|
return [];
|
|
6870
6929
|
}
|
|
6871
|
-
return bucketKeys.filter((
|
|
6872
|
-
const suffix =
|
|
6930
|
+
return bucketKeys.filter((key2) => {
|
|
6931
|
+
const suffix = key2.slice(prefix.length);
|
|
6873
6932
|
const firstColon = suffix.indexOf(":");
|
|
6874
6933
|
if (firstColon <= 0) {
|
|
6875
6934
|
return false;
|
|
@@ -6888,7 +6947,7 @@ var loadRuntimeSignals = async (userName, {
|
|
|
6888
6947
|
startTime,
|
|
6889
6948
|
endTime
|
|
6890
6949
|
);
|
|
6891
|
-
const refs = (await Promise.all(bucketKeys.map((
|
|
6950
|
+
const refs = (await Promise.all(bucketKeys.map((key2) => (0, import_redis4.getHashJsonValues)(key2)))).flat().filter(isRuntimeSignalBucketRef);
|
|
6892
6951
|
const seen = /* @__PURE__ */ new Set();
|
|
6893
6952
|
const payloads = await Promise.all(
|
|
6894
6953
|
refs.map(async (ref) => {
|
|
@@ -6917,7 +6976,7 @@ var loadRuntimeSignalEvaluations = async (userName, {
|
|
|
6917
6976
|
startTime,
|
|
6918
6977
|
endTime
|
|
6919
6978
|
);
|
|
6920
|
-
const evaluations = (await Promise.all(bucketKeys.map((
|
|
6979
|
+
const evaluations = (await Promise.all(bucketKeys.map((key2) => (0, import_redis4.getHashJsonValues)(key2)))).flat().filter(isRuntimeSignalEvaluationRecord);
|
|
6921
6980
|
const deduped = /* @__PURE__ */ new Map();
|
|
6922
6981
|
for (const evaluation of evaluations) {
|
|
6923
6982
|
deduped.set(evaluation.evaluationId, evaluation);
|
|
@@ -7087,10 +7146,10 @@ var syncRuntimeTrades = async ({
|
|
|
7087
7146
|
)
|
|
7088
7147
|
];
|
|
7089
7148
|
await Promise.all(
|
|
7090
|
-
activeTradeKeys.map(async (
|
|
7091
|
-
const activeRef = await (0, import_redis5.getData)(
|
|
7149
|
+
activeTradeKeys.map(async (key2) => {
|
|
7150
|
+
const activeRef = await (0, import_redis5.getData)(key2, null);
|
|
7092
7151
|
activeOrderIdByKey.set(
|
|
7093
|
-
|
|
7152
|
+
key2,
|
|
7094
7153
|
typeof activeRef?.orderId === "string" ? activeRef.orderId : null
|
|
7095
7154
|
);
|
|
7096
7155
|
})
|
|
@@ -8157,9 +8216,9 @@ var filterReplayComparisonByLineage = ({
|
|
|
8157
8216
|
);
|
|
8158
8217
|
const deploymentWindows = /* @__PURE__ */ new Map();
|
|
8159
8218
|
for (const scope of comparableRuntimeLineageScopes) {
|
|
8160
|
-
const
|
|
8161
|
-
const existing = deploymentWindows.get(
|
|
8162
|
-
deploymentWindows.set(
|
|
8219
|
+
const key2 = lineageScopeKey(scope);
|
|
8220
|
+
const existing = deploymentWindows.get(key2);
|
|
8221
|
+
deploymentWindows.set(key2, {
|
|
8163
8222
|
firstTimestamp: existing == null ? scope.firstTimestamp : Math.min(existing.firstTimestamp, scope.firstTimestamp),
|
|
8164
8223
|
lastTimestamp: existing == null ? scope.lastTimestamp : Math.max(existing.lastTimestamp, scope.lastTimestamp)
|
|
8165
8224
|
});
|
|
@@ -8195,9 +8254,9 @@ var filterReplayComparisonByLineage = ({
|
|
|
8195
8254
|
...comparableRuntimeSignals,
|
|
8196
8255
|
...comparableRuntimeEvaluations
|
|
8197
8256
|
]) {
|
|
8198
|
-
const
|
|
8199
|
-
const existing = deploymentWindows.get(
|
|
8200
|
-
deploymentWindows.set(
|
|
8257
|
+
const key2 = lineageScopeKey(artifact);
|
|
8258
|
+
const existing = deploymentWindows.get(key2);
|
|
8259
|
+
deploymentWindows.set(key2, {
|
|
8201
8260
|
firstTimestamp: existing == null ? artifact.timestamp : Math.min(existing.firstTimestamp, artifact.timestamp),
|
|
8202
8261
|
lastTimestamp: existing == null ? artifact.timestamp : Math.max(existing.lastTimestamp, artifact.timestamp)
|
|
8203
8262
|
});
|
|
@@ -8282,16 +8341,16 @@ var compareExchangeEntriesToBacktest = ({
|
|
|
8282
8341
|
const groupedExchange = /* @__PURE__ */ new Map();
|
|
8283
8342
|
const groupedBacktest = /* @__PURE__ */ new Map();
|
|
8284
8343
|
for (const entry of exchangeEntries) {
|
|
8285
|
-
const
|
|
8286
|
-
const bucket = groupedExchange.get(
|
|
8344
|
+
const key2 = `${entry.symbol}::${entry.direction}`;
|
|
8345
|
+
const bucket = groupedExchange.get(key2) ?? [];
|
|
8287
8346
|
bucket.push(entry);
|
|
8288
|
-
groupedExchange.set(
|
|
8347
|
+
groupedExchange.set(key2, bucket);
|
|
8289
8348
|
}
|
|
8290
8349
|
for (const entry of backtestEntries) {
|
|
8291
|
-
const
|
|
8292
|
-
const bucket = groupedBacktest.get(
|
|
8350
|
+
const key2 = `${entry.symbol}::${entry.direction}`;
|
|
8351
|
+
const bucket = groupedBacktest.get(key2) ?? [];
|
|
8293
8352
|
bucket.push(entry);
|
|
8294
|
-
groupedBacktest.set(
|
|
8353
|
+
groupedBacktest.set(key2, bucket);
|
|
8295
8354
|
}
|
|
8296
8355
|
const matched = [];
|
|
8297
8356
|
const exchangeOnly = [];
|
|
@@ -8301,11 +8360,11 @@ var compareExchangeEntriesToBacktest = ({
|
|
|
8301
8360
|
...groupedExchange.keys(),
|
|
8302
8361
|
...groupedBacktest.keys()
|
|
8303
8362
|
]);
|
|
8304
|
-
for (const
|
|
8305
|
-
const exchangeGroup = [...groupedExchange.get(
|
|
8363
|
+
for (const key2 of groupKeys) {
|
|
8364
|
+
const exchangeGroup = [...groupedExchange.get(key2) ?? []].sort(
|
|
8306
8365
|
(left, right) => left.entryTimestamp - right.entryTimestamp
|
|
8307
8366
|
);
|
|
8308
|
-
const unmatchedBacktest = [...groupedBacktest.get(
|
|
8367
|
+
const unmatchedBacktest = [...groupedBacktest.get(key2) ?? []].sort((left, right) => left.timestamp - right.timestamp).map((entry) => ({ entry, used: false }));
|
|
8309
8368
|
for (const exchangeEntry of exchangeGroup) {
|
|
8310
8369
|
let bestIndex = -1;
|
|
8311
8370
|
let bestDiff = Number.POSITIVE_INFINITY;
|
|
@@ -8726,14 +8785,14 @@ var mergeExchangeEntryExecutions = (rows) => {
|
|
|
8726
8785
|
const mergedRows = [];
|
|
8727
8786
|
const groups = /* @__PURE__ */ new Map();
|
|
8728
8787
|
for (const row of rows) {
|
|
8729
|
-
const
|
|
8730
|
-
if (!
|
|
8788
|
+
const key2 = getExchangeExecutionMergeKey(row);
|
|
8789
|
+
if (!key2) {
|
|
8731
8790
|
mergedRows.push(row);
|
|
8732
8791
|
continue;
|
|
8733
8792
|
}
|
|
8734
|
-
const group = groups.get(
|
|
8793
|
+
const group = groups.get(key2) ?? [];
|
|
8735
8794
|
group.push(row);
|
|
8736
|
-
groups.set(
|
|
8795
|
+
groups.set(key2, group);
|
|
8737
8796
|
}
|
|
8738
8797
|
for (const group of groups.values()) {
|
|
8739
8798
|
mergedRows.push(
|
|
@@ -9224,13 +9283,13 @@ function* jsonParts(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
|
9224
9283
|
throw new TypeError("JSON evidence cannot contain toJSON hooks");
|
|
9225
9284
|
yield "{";
|
|
9226
9285
|
let first = true;
|
|
9227
|
-
for (const
|
|
9228
|
-
const field = Reflect.get(value,
|
|
9286
|
+
for (const key2 of Object.keys(value)) {
|
|
9287
|
+
const field = Reflect.get(value, key2);
|
|
9229
9288
|
if (field === void 0 || typeof field === "function" || typeof field === "symbol")
|
|
9230
9289
|
continue;
|
|
9231
9290
|
if (!first) yield ",";
|
|
9232
9291
|
first = false;
|
|
9233
|
-
yield JSON.stringify(
|
|
9292
|
+
yield JSON.stringify(key2) + ":";
|
|
9234
9293
|
yield* jsonParts(field, ancestors);
|
|
9235
9294
|
}
|
|
9236
9295
|
yield "}";
|
|
@@ -9692,10 +9751,276 @@ var writeReplayOutputReport = async ({
|
|
|
9692
9751
|
};
|
|
9693
9752
|
|
|
9694
9753
|
// src/lib/replay/portfolioReport.ts
|
|
9754
|
+
var import_promises5 = __toESM(require("fs/promises"));
|
|
9755
|
+
var import_node_path8 = __toESM(require("path"));
|
|
9756
|
+
var import_node_crypto6 = require("crypto");
|
|
9757
|
+
|
|
9758
|
+
// src/lib/replay/portfolioComparison.ts
|
|
9759
|
+
var import_node_crypto5 = require("crypto");
|
|
9760
|
+
|
|
9761
|
+
// src/lib/coreResearch/io.ts
|
|
9762
|
+
var import_node_crypto3 = require("crypto");
|
|
9763
|
+
var import_node_fs4 = require("fs");
|
|
9695
9764
|
var import_promises4 = __toESM(require("fs/promises"));
|
|
9765
|
+
var import_backtest2 = require("@tradejs/core/backtest");
|
|
9766
|
+
var import_node_path7 = __toESM(require("path"));
|
|
9767
|
+
|
|
9768
|
+
// src/lib/coreResearch/trace.ts
|
|
9769
|
+
var import_node_fs3 = require("fs");
|
|
9770
|
+
var import_node_readline = __toESM(require("readline"));
|
|
9696
9771
|
var import_node_path6 = __toESM(require("path"));
|
|
9697
|
-
|
|
9698
|
-
|
|
9772
|
+
|
|
9773
|
+
// src/lib/coreResearch/io.ts
|
|
9774
|
+
var canonicalJson = (value) => {
|
|
9775
|
+
const normalize = (current) => {
|
|
9776
|
+
if (Array.isArray(current)) return current.map(normalize);
|
|
9777
|
+
if (current && typeof current === "object") {
|
|
9778
|
+
return Object.fromEntries(
|
|
9779
|
+
Object.entries(current).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key2, entry]) => [key2, normalize(entry)])
|
|
9780
|
+
);
|
|
9781
|
+
}
|
|
9782
|
+
return current;
|
|
9783
|
+
};
|
|
9784
|
+
return JSON.stringify(normalize(value));
|
|
9785
|
+
};
|
|
9786
|
+
|
|
9787
|
+
// src/lib/coreResearch/statistics.ts
|
|
9788
|
+
var import_node_crypto4 = require("crypto");
|
|
9789
|
+
|
|
9790
|
+
// src/lib/coreResearch/metrics.ts
|
|
9791
|
+
var DAY_MS4 = 24 * 60 * 60 * 1e3;
|
|
9792
|
+
|
|
9793
|
+
// src/lib/coreResearch/statistics.ts
|
|
9794
|
+
var createSeededRandom = (seedText) => {
|
|
9795
|
+
const digest = (0, import_node_crypto4.createHash)("sha256").update(seedText).digest();
|
|
9796
|
+
let state = digest.readUInt32LE(0) || 1;
|
|
9797
|
+
return () => {
|
|
9798
|
+
state ^= state << 13;
|
|
9799
|
+
state ^= state >>> 17;
|
|
9800
|
+
state ^= state << 5;
|
|
9801
|
+
return (state >>> 0) / 4294967296;
|
|
9802
|
+
};
|
|
9803
|
+
};
|
|
9804
|
+
var percentile = (sorted, probability) => {
|
|
9805
|
+
if (!sorted.length) return Number.NaN;
|
|
9806
|
+
const index = Math.min(
|
|
9807
|
+
sorted.length - 1,
|
|
9808
|
+
Math.max(0, Math.floor(probability * sorted.length))
|
|
9809
|
+
);
|
|
9810
|
+
return sorted[index];
|
|
9811
|
+
};
|
|
9812
|
+
var calendarClusterBootstrap = (params) => {
|
|
9813
|
+
const clusterMs = params.clusterDays * DAY_MS4;
|
|
9814
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
9815
|
+
if (params.start != null && params.end != null && params.end > params.start) {
|
|
9816
|
+
const first = Math.floor(params.start / clusterMs);
|
|
9817
|
+
const last = Math.ceil(params.end / clusterMs);
|
|
9818
|
+
for (let bucket = first; bucket < last; bucket += 1) {
|
|
9819
|
+
buckets.set(bucket, { control: 0, candidate: 0 });
|
|
9820
|
+
}
|
|
9821
|
+
}
|
|
9822
|
+
const add = (trades, field) => {
|
|
9823
|
+
for (const trade of trades) {
|
|
9824
|
+
const bucket = Math.floor(trade.exitTimestamp / clusterMs);
|
|
9825
|
+
const current = buckets.get(bucket) ?? { control: 0, candidate: 0 };
|
|
9826
|
+
current[field] += trade.netProfit;
|
|
9827
|
+
buckets.set(bucket, current);
|
|
9828
|
+
}
|
|
9829
|
+
};
|
|
9830
|
+
add(params.control, "control");
|
|
9831
|
+
add(params.candidate, "candidate");
|
|
9832
|
+
const deltas = [...buckets.entries()].sort(([left], [right]) => left - right).map(([, value]) => value.candidate - value.control);
|
|
9833
|
+
if (!deltas.length) {
|
|
9834
|
+
return {
|
|
9835
|
+
method: "calendar-cluster-bootstrap",
|
|
9836
|
+
clusterDays: params.clusterDays,
|
|
9837
|
+
iterations: params.iterations,
|
|
9838
|
+
confidenceLevel: params.confidenceLevel,
|
|
9839
|
+
observedMeanPnlDelta: null,
|
|
9840
|
+
confidenceInterval: null,
|
|
9841
|
+
probabilityPositive: null,
|
|
9842
|
+
oneSidedPValue: null,
|
|
9843
|
+
holmAdjustedPValue: null
|
|
9844
|
+
};
|
|
9845
|
+
}
|
|
9846
|
+
const random = createSeededRandom(params.seed);
|
|
9847
|
+
const samples = [];
|
|
9848
|
+
for (let iteration = 0; iteration < params.iterations; iteration += 1) {
|
|
9849
|
+
let sum = 0;
|
|
9850
|
+
for (let index = 0; index < deltas.length; index += 1) {
|
|
9851
|
+
sum += deltas[Math.floor(random() * deltas.length)];
|
|
9852
|
+
}
|
|
9853
|
+
samples.push(sum / deltas.length);
|
|
9854
|
+
}
|
|
9855
|
+
samples.sort((left, right) => left - right);
|
|
9856
|
+
const alpha = 1 - params.confidenceLevel;
|
|
9857
|
+
const positive = samples.filter((value) => value > 0).length;
|
|
9858
|
+
const probabilityPositive = positive / samples.length;
|
|
9859
|
+
const observed = deltas.reduce((sum, value) => sum + value, 0) / deltas.length;
|
|
9860
|
+
const oneSidedPValue = (1 + samples.filter((value) => value - observed >= observed - 1e-12).length) / (samples.length + 1);
|
|
9861
|
+
return {
|
|
9862
|
+
method: "calendar-cluster-bootstrap",
|
|
9863
|
+
clusterDays: params.clusterDays,
|
|
9864
|
+
iterations: params.iterations,
|
|
9865
|
+
confidenceLevel: params.confidenceLevel,
|
|
9866
|
+
observedMeanPnlDelta: deltas.reduce((sum, value) => sum + value, 0) / deltas.length,
|
|
9867
|
+
confidenceInterval: [
|
|
9868
|
+
percentile(samples, alpha / 2),
|
|
9869
|
+
percentile(samples, 1 - alpha / 2)
|
|
9870
|
+
],
|
|
9871
|
+
probabilityPositive,
|
|
9872
|
+
oneSidedPValue,
|
|
9873
|
+
holmAdjustedPValue: null
|
|
9874
|
+
};
|
|
9875
|
+
};
|
|
9876
|
+
|
|
9877
|
+
// src/lib/replay/portfolioComparison.ts
|
|
9878
|
+
var key = (value) => canonicalJson(value);
|
|
9879
|
+
function longestRecovery(points) {
|
|
9880
|
+
let start;
|
|
9881
|
+
let lastPeak;
|
|
9882
|
+
let longestMs = 0;
|
|
9883
|
+
for (const point of points) {
|
|
9884
|
+
if (point.value === 0) {
|
|
9885
|
+
if (start !== void 0)
|
|
9886
|
+
longestMs = Math.max(longestMs, point.timestamp - start);
|
|
9887
|
+
start = void 0;
|
|
9888
|
+
lastPeak = point.timestamp;
|
|
9889
|
+
} else if (start === void 0) start = lastPeak ?? point.timestamp;
|
|
9890
|
+
}
|
|
9891
|
+
return { longestCompletedMs: longestMs, unresolvedSince: start ?? null };
|
|
9892
|
+
}
|
|
9893
|
+
function compareCapacityAdmissions(before, after, changed) {
|
|
9894
|
+
const count = (filled, rejected) => {
|
|
9895
|
+
const keys = new Set(
|
|
9896
|
+
filled.admissions.filter(
|
|
9897
|
+
(event) => event.strategy !== changed && event.status === "filled"
|
|
9898
|
+
).map((event) => event.key)
|
|
9899
|
+
);
|
|
9900
|
+
return new Set(
|
|
9901
|
+
rejected.admissions.filter(
|
|
9902
|
+
(event) => event.status === "capacity-rejected" && keys.has(event.key)
|
|
9903
|
+
).map((event) => event.key)
|
|
9904
|
+
).size;
|
|
9905
|
+
};
|
|
9906
|
+
return { displaced: count(before, after), restored: count(after, before) };
|
|
9907
|
+
}
|
|
9908
|
+
function comparePortfolioReports(input, candidate) {
|
|
9909
|
+
if (!input || typeof input !== "object" || !("checksum" in input)) {
|
|
9910
|
+
throw new Error("Expected a checksum-bound portfolio report");
|
|
9911
|
+
}
|
|
9912
|
+
const { checksum, ...payload } = input;
|
|
9913
|
+
if (checksum !== (0, import_node_crypto5.createHash)("sha256").update(JSON.stringify(payload)).digest("hex") || payload.schema !== "tradejs-portfolio-backtest/v1") {
|
|
9914
|
+
throw new Error("Control portfolio report checksum or schema mismatch");
|
|
9915
|
+
}
|
|
9916
|
+
const control = payload;
|
|
9917
|
+
if (control.lineage?.batchCount !== 1 || candidate.lineage.batchCount !== 1 || control.lineage.cacheOnly !== true || candidate.lineage.cacheOnly !== true) {
|
|
9918
|
+
throw new Error(
|
|
9919
|
+
"Portfolio comparison requires two cache-only reports from a single shared batch"
|
|
9920
|
+
);
|
|
9921
|
+
}
|
|
9922
|
+
for (const field of [
|
|
9923
|
+
"connector",
|
|
9924
|
+
"interval",
|
|
9925
|
+
"tickers",
|
|
9926
|
+
"candleCoverage",
|
|
9927
|
+
"binanceMarketContextRows",
|
|
9928
|
+
"maxLossValue"
|
|
9929
|
+
]) {
|
|
9930
|
+
if (candidate.lineage[field] === void 0 || key(control.lineage[field]) !== key(candidate.lineage[field])) {
|
|
9931
|
+
throw new Error(`Portfolio comparison scope mismatch: ${field}`);
|
|
9932
|
+
}
|
|
9933
|
+
}
|
|
9934
|
+
if (key(control.window) !== key(candidate.window))
|
|
9935
|
+
throw new Error("Portfolio comparison window mismatch");
|
|
9936
|
+
const before = control.lineage.strategies;
|
|
9937
|
+
const after = candidate.lineage.strategies;
|
|
9938
|
+
if (!Array.isArray(before) || !Array.isArray(after) || key(before.map((s) => s.strategyName)) !== key(after.map((s) => s.strategyName))) {
|
|
9939
|
+
throw new Error(
|
|
9940
|
+
"Portfolio comparison requires the same ordered strategies"
|
|
9941
|
+
);
|
|
9942
|
+
}
|
|
9943
|
+
const changed = after.filter((s, index) => key(s) !== key(before[index]));
|
|
9944
|
+
if (changed.length > 1)
|
|
9945
|
+
throw new Error(
|
|
9946
|
+
"Portfolio comparison allows at most one strategy replacement"
|
|
9947
|
+
);
|
|
9948
|
+
if (after.some(
|
|
9949
|
+
(s, index) => s.maxLossValue !== before[index].maxLossValue || key(s.effectiveConfig.PORTFOLIO_POLICY) !== key(before[index].effectiveConfig.PORTFOLIO_POLICY)
|
|
9950
|
+
)) {
|
|
9951
|
+
throw new Error(
|
|
9952
|
+
"Portfolio comparison requires unchanged risk and portfolio policies"
|
|
9953
|
+
);
|
|
9954
|
+
}
|
|
9955
|
+
const all = (report) => {
|
|
9956
|
+
const row = report.metrics.portfolio.find(
|
|
9957
|
+
(r) => r.window === "full" && r.cohort === "ALL"
|
|
9958
|
+
);
|
|
9959
|
+
if (!row) throw new Error("Missing full ALL portfolio metrics");
|
|
9960
|
+
return row;
|
|
9961
|
+
};
|
|
9962
|
+
const a = all(control), b = all(candidate);
|
|
9963
|
+
const toPnl = (report) => report.trades.map((trade) => ({
|
|
9964
|
+
exitTimestamp: trade.exitedAt,
|
|
9965
|
+
netProfit: trade.pnl
|
|
9966
|
+
}));
|
|
9967
|
+
const strategyDelta = candidate.metrics.strategies.filter((row) => row.window === "full").map((row) => {
|
|
9968
|
+
const previous = control.metrics.strategies.find(
|
|
9969
|
+
(r) => r.window === row.window && r.scope === row.scope && r.cohort === row.cohort
|
|
9970
|
+
);
|
|
9971
|
+
return {
|
|
9972
|
+
strategy: row.scope,
|
|
9973
|
+
cohort: row.cohort,
|
|
9974
|
+
trades: previous ? row.n - previous.n : null,
|
|
9975
|
+
pnl: previous ? row.pnl - previous.pnl : null
|
|
9976
|
+
};
|
|
9977
|
+
});
|
|
9978
|
+
const markBefore = control.portfolioDiagnostics?.markedMaxDrawdown ?? null;
|
|
9979
|
+
const markAfter = candidate.portfolioDiagnostics?.markedMaxDrawdown ?? null;
|
|
9980
|
+
const displacement = control.portfolioDiagnostics && candidate.portfolioDiagnostics ? compareCapacityAdmissions(
|
|
9981
|
+
control.portfolioDiagnostics,
|
|
9982
|
+
candidate.portfolioDiagnostics,
|
|
9983
|
+
changed[0]?.strategyName
|
|
9984
|
+
) : null;
|
|
9985
|
+
return {
|
|
9986
|
+
controlChecksum: checksum,
|
|
9987
|
+
markedDrawdown: {
|
|
9988
|
+
control: markBefore,
|
|
9989
|
+
candidate: markAfter,
|
|
9990
|
+
delta: markBefore === null || markAfter === null ? null : markAfter - markBefore
|
|
9991
|
+
},
|
|
9992
|
+
capacityDisplacement: displacement,
|
|
9993
|
+
changedStrategy: changed[0]?.strategyName ?? null,
|
|
9994
|
+
delta: {
|
|
9995
|
+
pnl: b.pnl - a.pnl,
|
|
9996
|
+
trades: b.n - a.n,
|
|
9997
|
+
realizedMaxDrawdown: b.realizedMaxDrawdown - a.realizedMaxDrawdown
|
|
9998
|
+
},
|
|
9999
|
+
recovery: {
|
|
10000
|
+
control: longestRecovery(control.curves.portfolioDrawdown),
|
|
10001
|
+
candidate: longestRecovery(candidate.curves.portfolioDrawdown)
|
|
10002
|
+
},
|
|
10003
|
+
strategyDelta,
|
|
10004
|
+
bootstrap: calendarClusterBootstrap({
|
|
10005
|
+
control: toPnl(control),
|
|
10006
|
+
candidate: toPnl(candidate),
|
|
10007
|
+
clusterDays: 7,
|
|
10008
|
+
iterations: 1e3,
|
|
10009
|
+
confidenceLevel: 0.95,
|
|
10010
|
+
seed: String(checksum),
|
|
10011
|
+
start: candidate.window.start,
|
|
10012
|
+
end: candidate.window.end
|
|
10013
|
+
}),
|
|
10014
|
+
limitations: [
|
|
10015
|
+
"Scope and recorded coverage match. Report lineage does not prove equality of every historical source byte.",
|
|
10016
|
+
"Marked drawdown uses last observed candle closes at replay cycle boundaries. It does not measure intra-candle excursions.",
|
|
10017
|
+
"Capacity displacement counts only identical signals of unchanged strategies filled in one run and explicitly rejected by the position cap in the other."
|
|
10018
|
+
]
|
|
10019
|
+
};
|
|
10020
|
+
}
|
|
10021
|
+
|
|
10022
|
+
// src/lib/replay/portfolioReport.ts
|
|
10023
|
+
var DAY_MS5 = 864e5;
|
|
9699
10024
|
var WINDOWS = [365, 180, 90, 30, 7];
|
|
9700
10025
|
var COLORS = [
|
|
9701
10026
|
"#2563eb",
|
|
@@ -9775,7 +10100,7 @@ var calculatePortfolioMetricRow = ({
|
|
|
9775
10100
|
const pnl = selected.reduce((sum, trade) => sum + trade.pnl, 0);
|
|
9776
10101
|
const wins = selected.filter((trade) => trade.pnl > 0).length;
|
|
9777
10102
|
const losses = selected.filter((trade) => trade.pnl <= 0).length;
|
|
9778
|
-
const days = Math.max(1, (end - start) /
|
|
10103
|
+
const days = Math.max(1, (end - start) / DAY_MS5);
|
|
9779
10104
|
return {
|
|
9780
10105
|
window,
|
|
9781
10106
|
scope,
|
|
@@ -9795,7 +10120,7 @@ var windowBounds = (start, end) => [
|
|
|
9795
10120
|
{ label: "full", start, end },
|
|
9796
10121
|
...WINDOWS.map((days) => ({
|
|
9797
10122
|
label: `${days}d`,
|
|
9798
|
-
start: Math.max(start, end - days *
|
|
10123
|
+
start: Math.max(start, end - days * DAY_MS5),
|
|
9799
10124
|
end
|
|
9800
10125
|
}))
|
|
9801
10126
|
];
|
|
@@ -9870,10 +10195,11 @@ var writePortfolioReport = async ({
|
|
|
9870
10195
|
replayResult,
|
|
9871
10196
|
window,
|
|
9872
10197
|
lineage,
|
|
9873
|
-
command
|
|
10198
|
+
command,
|
|
10199
|
+
compareTo
|
|
9874
10200
|
}) => {
|
|
9875
|
-
const outputDir =
|
|
9876
|
-
await
|
|
10201
|
+
const outputDir = import_node_path8.default.resolve(projectRoot, "data/replay/output");
|
|
10202
|
+
await import_promises5.default.mkdir(outputDir, { recursive: true });
|
|
9877
10203
|
const trades = replayResult.strategies.flatMap(
|
|
9878
10204
|
(strategy) => tradesFromPositionLog(strategy.strategyName, strategy.positionLog)
|
|
9879
10205
|
);
|
|
@@ -9945,13 +10271,13 @@ var writePortfolioReport = async ({
|
|
|
9945
10271
|
});
|
|
9946
10272
|
const prefix = `${timestamp}-portfolio-backtest`;
|
|
9947
10273
|
const paths = {
|
|
9948
|
-
markdown:
|
|
9949
|
-
json:
|
|
9950
|
-
equitySvg:
|
|
9951
|
-
drawdownSvg:
|
|
9952
|
-
html:
|
|
10274
|
+
markdown: import_node_path8.default.join(outputDir, `${prefix}.md`),
|
|
10275
|
+
json: import_node_path8.default.join(outputDir, `${prefix}.json`),
|
|
10276
|
+
equitySvg: import_node_path8.default.join(outputDir, `${prefix}-equity.svg`),
|
|
10277
|
+
drawdownSvg: import_node_path8.default.join(outputDir, `${prefix}-drawdown.svg`),
|
|
10278
|
+
html: import_node_path8.default.join(outputDir, `${prefix}.html`)
|
|
9953
10279
|
};
|
|
9954
|
-
const
|
|
10280
|
+
const baseReport = {
|
|
9955
10281
|
schema: "tradejs-portfolio-backtest/v1",
|
|
9956
10282
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9957
10283
|
command,
|
|
@@ -9973,9 +10299,17 @@ var writePortfolioReport = async ({
|
|
|
9973
10299
|
portfolioDrawdown: drawdownCurve(portfolioEquity),
|
|
9974
10300
|
strategies: strategySeries.map(({ name, points }) => ({ name, points }))
|
|
9975
10301
|
},
|
|
9976
|
-
trades
|
|
10302
|
+
trades,
|
|
10303
|
+
...replayResult.portfolioDiagnostics ? { portfolioDiagnostics: replayResult.portfolioDiagnostics } : {}
|
|
9977
10304
|
};
|
|
9978
|
-
const
|
|
10305
|
+
const comparison = compareTo ? comparePortfolioReports(
|
|
10306
|
+
JSON.parse(
|
|
10307
|
+
await import_promises5.default.readFile(import_node_path8.default.resolve(projectRoot, compareTo), "utf8")
|
|
10308
|
+
),
|
|
10309
|
+
baseReport
|
|
10310
|
+
) : void 0;
|
|
10311
|
+
const report = { ...baseReport, ...comparison ? { comparison } : {} };
|
|
10312
|
+
const checksum = (0, import_node_crypto6.createHash)("sha256").update(JSON.stringify(report)).digest("hex");
|
|
9979
10313
|
const semantics = "Production strategy order, at most one open position per symbol, and fixed-risk symbol batches merged by canonical realized PnL.";
|
|
9980
10314
|
const markdown = `# Portfolio Backtest
|
|
9981
10315
|
|
|
@@ -9987,9 +10321,9 @@ var writePortfolioReport = async ({
|
|
|
9987
10321
|
- Report checksum: \`${checksum}\`
|
|
9988
10322
|
- Command: \`${command}\`
|
|
9989
10323
|
|
|
9990
|
-
})
|
|
9991
10325
|
|
|
9992
|
-
})
|
|
9993
10327
|
|
|
9994
10328
|
## Portfolio metrics
|
|
9995
10329
|
|
|
@@ -10000,17 +10334,33 @@ ${markdownTable(portfolioRows)}
|
|
|
10000
10334
|
${markdownTable(strategyRows)}
|
|
10001
10335
|
`;
|
|
10002
10336
|
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>`;
|
|
10337
|
+
const comparisonMarkdown = comparison ? `
|
|
10338
|
+
## Paired portfolio comparison
|
|
10339
|
+
|
|
10340
|
+
- Net PnL delta: ${display(comparison.delta.pnl)}
|
|
10341
|
+
- Closed trades delta: ${comparison.delta.trades}
|
|
10342
|
+
- Realized MaxDD delta: ${display(comparison.delta.realizedMaxDrawdown)}
|
|
10343
|
+
|
|
10344
|
+
${comparison.limitations.join(" ")}
|
|
10345
|
+
` : "";
|
|
10003
10346
|
await Promise.all([
|
|
10004
|
-
|
|
10005
|
-
|
|
10347
|
+
import_promises5.default.writeFile(paths.markdown, markdown + comparisonMarkdown, "utf8"),
|
|
10348
|
+
import_promises5.default.writeFile(
|
|
10006
10349
|
paths.json,
|
|
10007
10350
|
`${JSON.stringify({ ...report, checksum }, null, 2)}
|
|
10008
10351
|
`,
|
|
10009
10352
|
"utf8"
|
|
10010
10353
|
),
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10354
|
+
import_promises5.default.writeFile(paths.equitySvg, equitySvg, "utf8"),
|
|
10355
|
+
import_promises5.default.writeFile(paths.drawdownSvg, drawdownSvg, "utf8"),
|
|
10356
|
+
import_promises5.default.writeFile(
|
|
10357
|
+
paths.html,
|
|
10358
|
+
comparison ? html.replace(
|
|
10359
|
+
"</body>",
|
|
10360
|
+
`<h2>Paired portfolio comparison</h2><pre>${escapeHtml(JSON.stringify(comparison, null, 2))}</pre></body>`
|
|
10361
|
+
) : html,
|
|
10362
|
+
"utf8"
|
|
10363
|
+
)
|
|
10014
10364
|
]);
|
|
10015
10365
|
return { ...paths, checksum };
|
|
10016
10366
|
};
|
|
@@ -10053,8 +10403,8 @@ var cachedTickerSymbols = async ({
|
|
|
10053
10403
|
import_redis6.redisKeys.tickerUniverse(replayUserName, name, universe, "default"),
|
|
10054
10404
|
import_redis6.redisKeys.tickerUniverse(replayUserName, name)
|
|
10055
10405
|
]);
|
|
10056
|
-
for (const
|
|
10057
|
-
const cached = await (0, import_redis6.getData)(
|
|
10406
|
+
for (const key2 of [...new Set(keys)]) {
|
|
10407
|
+
const cached = await (0, import_redis6.getData)(key2, null);
|
|
10058
10408
|
const symbols = (cached?.tickers ?? []).map(
|
|
10059
10409
|
(ticker) => String(ticker.symbol ?? "").trim().toUpperCase()
|
|
10060
10410
|
).filter(Boolean);
|
|
@@ -10123,7 +10473,8 @@ var finishReplay = async ({
|
|
|
10123
10473
|
replayResult,
|
|
10124
10474
|
window,
|
|
10125
10475
|
lineage: portfolioLineage ?? {},
|
|
10126
|
-
command: process.argv.join(" ")
|
|
10476
|
+
command: process.argv.join(" "),
|
|
10477
|
+
compareTo: replayFlags.portfolioCompareTo
|
|
10127
10478
|
}) : null;
|
|
10128
10479
|
if (portfolioReport) {
|
|
10129
10480
|
console.log(import_chalk13.default.green(`Portfolio report: ${portfolioReport.html}`));
|
|
@@ -10407,6 +10758,18 @@ var replayBacktest = async () => {
|
|
|
10407
10758
|
`Invalid --portfolioBatchSize: ${String(replayFlags.portfolioBatchSize)}`
|
|
10408
10759
|
);
|
|
10409
10760
|
}
|
|
10761
|
+
if (replayFlags.portfolioCompareTo && (!replayFlags.portfolioReport || !replayFlags.cacheOnly)) {
|
|
10762
|
+
throw new Error(
|
|
10763
|
+
"--portfolioCompareTo requires --portfolioReport and --cacheOnly"
|
|
10764
|
+
);
|
|
10765
|
+
}
|
|
10766
|
+
if (replayFlags.portfolioReport && preparedRun.tickers.length > portfolioBatchSize && (replayFlags.portfolioCompareTo || replayStrategies.some(
|
|
10767
|
+
({ strategyConfig }) => Boolean(strategyConfig.PORTFOLIO_POLICY)
|
|
10768
|
+
))) {
|
|
10769
|
+
throw new Error(
|
|
10770
|
+
"Portfolio policies and comparisons require a single shared batch; set --portfolioBatchSize to at least the number of tickers"
|
|
10771
|
+
);
|
|
10772
|
+
}
|
|
10410
10773
|
const runtimeStrategies = replayFlags.tickers && !replayFlags.portfolioReport ? replayStrategies.map(
|
|
10411
10774
|
({ selection: _selection, ...strategy }) => strategy
|
|
10412
10775
|
) : replayStrategies;
|
|
@@ -10430,6 +10793,9 @@ var replayBacktest = async () => {
|
|
|
10430
10793
|
showProgress: !replayFlags.portfolioReport,
|
|
10431
10794
|
collectSkipEvidence: !replayFlags.portfolioReport,
|
|
10432
10795
|
collectSignals: !replayFlags.portfolioReport,
|
|
10796
|
+
collectPortfolioDiagnostics: Boolean(
|
|
10797
|
+
replayFlags.portfolioReport && tickerBatches.length === 1
|
|
10798
|
+
),
|
|
10433
10799
|
decisionTraceTargets: replayFlags.portfolioReport ? void 0 : evidenceMetadata?.decisionTraceTargets
|
|
10434
10800
|
});
|
|
10435
10801
|
return replayFlags.portfolioReport ? compactHistoricalReplayResultForPortfolio(result) : result;
|
|
@@ -10467,6 +10833,9 @@ var replayBacktest = async () => {
|
|
|
10467
10833
|
cacheOnly: Boolean(replayFlags.cacheOnly),
|
|
10468
10834
|
portfolioSemantics: "production strategy order and one concurrent position per symbol; fixed-risk symbol batches are merged by canonical realized PnL",
|
|
10469
10835
|
portfolioBatchSize,
|
|
10836
|
+
batchCount: tickerBatches.length,
|
|
10837
|
+
connector: preparedRun.connectorName,
|
|
10838
|
+
interval: replayInterval,
|
|
10470
10839
|
tickers: preparedRun.tickers,
|
|
10471
10840
|
candleCoverage: candleCoverage ?? null,
|
|
10472
10841
|
binanceMarketContextRows: portfolioMarketContextRows ?? null,
|