@tradejs/cli 3.1.29-beta.290 → 3.1.29-beta.292
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 +12283 -10394
- package/dist/lib/aiPocketSearch.js +37 -3
- package/dist/lib/aiTrainDataset.js +1 -0
- package/dist/lib/aiTrainEvaluationDump.js +7 -2
- package/dist/lib/aiTrainResearch.js +11 -7
- package/dist/lib/coreResearch.js +273 -119
- package/dist/lib/researchMetricSnapshot.js +74 -0
- package/dist/lib/researchVisuals.js +931 -0
- package/dist/lib/strategyRelease.js +8 -2
- package/dist/lib/strategyResearch.js +334 -199
- package/dist/lib/tickerProfiles.js +76 -0
- package/dist/scripts/aiPocketSearch.js +2959 -2610
- package/dist/scripts/aiTrain.js +29 -17
- package/dist/scripts/coreResearch.js +275 -119
- package/dist/scripts/replay.js +533 -115
- package/dist/scripts/replayRunner.js +533 -115
- package/dist/scripts/researchVisuals.js +976 -0
- package/dist/scripts/strategyRelease.js +59 -7
- package/dist/scripts/strategyResearch.js +339 -203
- package/dist/scripts/tickerProfiles.js +114 -0
- package/package.json +8 -7
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
|
|
|
@@ -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(
|
|
@@ -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,16 @@ 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;
|
|
5191
|
+
const markedAtBySymbol = /* @__PURE__ */ new Map();
|
|
5192
|
+
const entryFacts = /* @__PURE__ */ new Map();
|
|
5193
|
+
let portfolioObserved = false;
|
|
5186
5194
|
const orderLog = [];
|
|
5187
5195
|
const positionLog = [];
|
|
5188
5196
|
const orderLogByStrategy = /* @__PURE__ */ new Map();
|
|
@@ -5420,14 +5428,51 @@ var createPortfolioReplayConnector = (connector) => {
|
|
|
5420
5428
|
const { strategyName: _strategyName, ...nextPosition } = position;
|
|
5421
5429
|
return nextPosition;
|
|
5422
5430
|
}),
|
|
5431
|
+
getPortfolioObservation: async (asOf, reservations) => {
|
|
5432
|
+
portfolioObserved = true;
|
|
5433
|
+
const pendingIds = new Set(reservations.map((r) => r.id));
|
|
5434
|
+
for (const id of entryFacts.keys())
|
|
5435
|
+
if (!pendingIds.has(id)) entryFacts.delete(id);
|
|
5436
|
+
return {
|
|
5437
|
+
asOf,
|
|
5438
|
+
positions: Array.from(positionsBySymbol.values()).map(
|
|
5439
|
+
({ position, stopLossPrice }) => ({
|
|
5440
|
+
id: position.orderId ?? `${position.strategyName}:${position.symbol}:${position.timestamp}`,
|
|
5441
|
+
strategy: position.strategyName === UNKNOWN_STRATEGY ? null : position.strategyName,
|
|
5442
|
+
symbol: position.symbol,
|
|
5443
|
+
direction: position.direction,
|
|
5444
|
+
qty: position.qty,
|
|
5445
|
+
entryPrice: position.price,
|
|
5446
|
+
markPrice: currentPriceBySymbol.get(position.symbol) ?? null,
|
|
5447
|
+
markedAt: markedAtBySymbol.get(position.symbol) ?? null,
|
|
5448
|
+
stopLossPrice
|
|
5449
|
+
})
|
|
5450
|
+
),
|
|
5451
|
+
orders: reservations.map(
|
|
5452
|
+
(r) => entryFacts.get(r.id) ?? {
|
|
5453
|
+
id: r.id,
|
|
5454
|
+
status: "pending",
|
|
5455
|
+
filledQty: 0
|
|
5456
|
+
}
|
|
5457
|
+
)
|
|
5458
|
+
};
|
|
5459
|
+
},
|
|
5423
5460
|
getOpenPositionPnl: async () => Array.from(positionsBySymbol.values()).map(
|
|
5424
|
-
({ position }) =>
|
|
5425
|
-
|
|
5426
|
-
|
|
5461
|
+
({ position, stopLossPrice }) => ({
|
|
5462
|
+
...getPositionPnlSnapshot({
|
|
5463
|
+
position,
|
|
5464
|
+
currentPrice: currentPriceBySymbol.get(position.symbol) ?? position.price
|
|
5465
|
+
}),
|
|
5466
|
+
observedAt: markedAtBySymbol.get(position.symbol),
|
|
5467
|
+
stopLossPrice
|
|
5427
5468
|
})
|
|
5428
5469
|
),
|
|
5429
5470
|
placeOrder: async (order) => {
|
|
5430
|
-
const {
|
|
5471
|
+
const {
|
|
5472
|
+
beforeSubmit,
|
|
5473
|
+
onRejected: _onRejected,
|
|
5474
|
+
...submittedOrder
|
|
5475
|
+
} = order;
|
|
5431
5476
|
if (positionsBySymbol.has(order.symbol)) {
|
|
5432
5477
|
return false;
|
|
5433
5478
|
}
|
|
@@ -5471,6 +5516,14 @@ var createPortfolioReplayConnector = (connector) => {
|
|
|
5471
5516
|
stopLossPrice: null
|
|
5472
5517
|
};
|
|
5473
5518
|
positionsBySymbol.set(order.symbol, positionState);
|
|
5519
|
+
markedAtBySymbol.set(order.symbol, executionTimestamp ?? order.timestamp);
|
|
5520
|
+
if (portfolioObserved && order.orderId)
|
|
5521
|
+
entryFacts.set(order.orderId, {
|
|
5522
|
+
id: order.orderId,
|
|
5523
|
+
status: "filled",
|
|
5524
|
+
filledQty: order.qty,
|
|
5525
|
+
positionId: order.orderId
|
|
5526
|
+
});
|
|
5474
5527
|
logOrder({
|
|
5475
5528
|
positionState,
|
|
5476
5529
|
data: {
|
|
@@ -5554,11 +5607,26 @@ var createPortfolioReplayConnector = (connector) => {
|
|
|
5554
5607
|
getTickers: connector.getTickers,
|
|
5555
5608
|
advanceMarket: async ({ symbol, candle }) => {
|
|
5556
5609
|
executionTimestamp = candle.timestamp;
|
|
5610
|
+
markedAtBySymbol.set(symbol, candle.timestamp);
|
|
5611
|
+
equityMarks?.set(symbol, candle.close);
|
|
5557
5612
|
currentPriceBySymbol.set(symbol, candle.close);
|
|
5558
5613
|
await checkSl({ symbol, candle });
|
|
5559
5614
|
await checkTp({ symbol, candle });
|
|
5560
5615
|
currentPriceBySymbol.set(symbol, candle.close);
|
|
5561
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
|
+
},
|
|
5562
5630
|
getReplayArtifacts: () => ({
|
|
5563
5631
|
orderLog,
|
|
5564
5632
|
positionLog,
|
|
@@ -5772,6 +5840,21 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5772
5840
|
context.collectSkipEvidence
|
|
5773
5841
|
);
|
|
5774
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
|
+
};
|
|
5775
5858
|
const reportProgress = (timestamp) => {
|
|
5776
5859
|
if (adapters.progress.enabled === false) return;
|
|
5777
5860
|
adapters.progress.tick({
|
|
@@ -5815,6 +5898,7 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5815
5898
|
}
|
|
5816
5899
|
}
|
|
5817
5900
|
advanceAbortedCycle(cycleSymbols, timestamp);
|
|
5901
|
+
observeEquity();
|
|
5818
5902
|
reportProgress(timestamp);
|
|
5819
5903
|
continue;
|
|
5820
5904
|
}
|
|
@@ -5858,6 +5942,21 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5858
5942
|
result
|
|
5859
5943
|
});
|
|
5860
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
|
+
}
|
|
5861
5960
|
result.runtimeLineage = strategyRuntime.runtimeLineage;
|
|
5862
5961
|
await adapters.enrichSignal(result);
|
|
5863
5962
|
cycleSignals.push(result);
|
|
@@ -5872,12 +5971,16 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5872
5971
|
status: "completed",
|
|
5873
5972
|
durationMs: adapters.clock.now() - cycleStartedAt
|
|
5874
5973
|
});
|
|
5974
|
+
observeEquity();
|
|
5875
5975
|
reportProgress(timestamp);
|
|
5876
5976
|
}
|
|
5877
5977
|
} finally {
|
|
5878
5978
|
releaseHistoricalReplayCaches(context.plan, adapters);
|
|
5879
5979
|
}
|
|
5980
|
+
if (portfolioDiagnostics?.unpricedCycles)
|
|
5981
|
+
portfolioDiagnostics.markedMaxDrawdown = null;
|
|
5880
5982
|
return {
|
|
5983
|
+
...portfolioDiagnostics ? { portfolioDiagnostics } : {},
|
|
5881
5984
|
signals,
|
|
5882
5985
|
abortedCycles,
|
|
5883
5986
|
replayLineageScopes: skipEvidence.values(),
|
|
@@ -5933,6 +6036,7 @@ var collectHistoricalReplayResult = (context) => ({
|
|
|
5933
6036
|
abortedCycles: context.abortedCycles,
|
|
5934
6037
|
runtimeLineages: context.runtimeLineages,
|
|
5935
6038
|
replayLineageScopes: context.replayLineageScopes,
|
|
6039
|
+
...context.portfolioDiagnostics ? { portfolioDiagnostics: context.portfolioDiagnostics } : {},
|
|
5936
6040
|
...context.replayDecisionTraces?.length ? { replayDecisionTraces: context.replayDecisionTraces } : {}
|
|
5937
6041
|
});
|
|
5938
6042
|
var compactHistoricalReplayResultForPortfolio = (result) => ({
|
|
@@ -6134,6 +6238,7 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6134
6238
|
showProgress = true,
|
|
6135
6239
|
collectSkipEvidence = true,
|
|
6136
6240
|
collectSignals = true,
|
|
6241
|
+
collectPortfolioDiagnostics = false,
|
|
6137
6242
|
decisionTraceTargets
|
|
6138
6243
|
}) => {
|
|
6139
6244
|
const memory = createReplayMemoryProfile();
|
|
@@ -6143,7 +6248,8 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6143
6248
|
const projectHooks = projectConfig.hooks;
|
|
6144
6249
|
const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
|
|
6145
6250
|
const replayConnector = createPortfolioReplayConnector(
|
|
6146
|
-
preparedRun.marketConnector
|
|
6251
|
+
preparedRun.marketConnector,
|
|
6252
|
+
collectPortfolioDiagnostics
|
|
6147
6253
|
);
|
|
6148
6254
|
const connectorName = getConnectorName(preparedRun.marketConnector);
|
|
6149
6255
|
const referenceData = references ?? await loadHistoricalReplayReferences({ preparedRun, interval });
|
|
@@ -6221,6 +6327,7 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6221
6327
|
connector: replayConnector,
|
|
6222
6328
|
collectSkipEvidence,
|
|
6223
6329
|
collectSignals,
|
|
6330
|
+
collectPortfolioDiagnostics,
|
|
6224
6331
|
decisionTraceTargets,
|
|
6225
6332
|
hooks: projectHooks,
|
|
6226
6333
|
hookContext
|
|
@@ -6259,7 +6366,8 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6259
6366
|
abortedCycles: execution.abortedCycles,
|
|
6260
6367
|
runtimeLineages: plan.runtimeLineages,
|
|
6261
6368
|
replayLineageScopes: execution.replayLineageScopes,
|
|
6262
|
-
replayDecisionTraces: execution.replayDecisionTraces
|
|
6369
|
+
replayDecisionTraces: execution.replayDecisionTraces,
|
|
6370
|
+
portfolioDiagnostics: execution.portfolioDiagnostics
|
|
6263
6371
|
});
|
|
6264
6372
|
memory?.("result:collected");
|
|
6265
6373
|
return result;
|
|
@@ -6506,16 +6614,16 @@ var findRuntimeDuplicateGroups = (entries) => {
|
|
|
6506
6614
|
if (entry.source !== "runtime") {
|
|
6507
6615
|
continue;
|
|
6508
6616
|
}
|
|
6509
|
-
const
|
|
6510
|
-
const bucket = groups.get(
|
|
6617
|
+
const key2 = toRuntimeDuplicateKey(entry);
|
|
6618
|
+
const bucket = groups.get(key2) ?? [];
|
|
6511
6619
|
bucket.push(entry);
|
|
6512
|
-
groups.set(
|
|
6620
|
+
groups.set(key2, bucket);
|
|
6513
6621
|
}
|
|
6514
|
-
return [...groups.entries()].filter(([, group]) => group.length > 1).map(([
|
|
6622
|
+
return [...groups.entries()].filter(([, group]) => group.length > 1).map(([key2, group]) => {
|
|
6515
6623
|
const sortedGroup = [...group].sort(sortTradeParityEntries);
|
|
6516
6624
|
const first = sortedGroup[0];
|
|
6517
6625
|
return {
|
|
6518
|
-
key,
|
|
6626
|
+
key: key2,
|
|
6519
6627
|
strategy: first.strategy,
|
|
6520
6628
|
symbol: first.symbol,
|
|
6521
6629
|
direction: first.direction,
|
|
@@ -6554,26 +6662,26 @@ var compareTradeParityEntries = ({
|
|
|
6554
6662
|
const groupedRuntime = /* @__PURE__ */ new Map();
|
|
6555
6663
|
const groupedBacktest = /* @__PURE__ */ new Map();
|
|
6556
6664
|
for (const entry of runtimeEntries) {
|
|
6557
|
-
const
|
|
6558
|
-
const bucket = groupedRuntime.get(
|
|
6665
|
+
const key2 = toGroupKey(entry);
|
|
6666
|
+
const bucket = groupedRuntime.get(key2) ?? [];
|
|
6559
6667
|
bucket.push(entry);
|
|
6560
|
-
groupedRuntime.set(
|
|
6668
|
+
groupedRuntime.set(key2, bucket);
|
|
6561
6669
|
}
|
|
6562
6670
|
for (const entry of backtestEntries) {
|
|
6563
|
-
const
|
|
6564
|
-
const bucket = groupedBacktest.get(
|
|
6671
|
+
const key2 = toGroupKey(entry);
|
|
6672
|
+
const bucket = groupedBacktest.get(key2) ?? [];
|
|
6565
6673
|
bucket.push(entry);
|
|
6566
|
-
groupedBacktest.set(
|
|
6674
|
+
groupedBacktest.set(key2, bucket);
|
|
6567
6675
|
}
|
|
6568
6676
|
const groupKeys = /* @__PURE__ */ new Set([
|
|
6569
6677
|
...groupedRuntime.keys(),
|
|
6570
6678
|
...groupedBacktest.keys()
|
|
6571
6679
|
]);
|
|
6572
|
-
for (const
|
|
6573
|
-
const runtimeGroup = [...groupedRuntime.get(
|
|
6680
|
+
for (const key2 of groupKeys) {
|
|
6681
|
+
const runtimeGroup = [...groupedRuntime.get(key2) ?? []].sort(
|
|
6574
6682
|
(left, right) => left.timestamp - right.timestamp
|
|
6575
6683
|
);
|
|
6576
|
-
const backtestGroup = [...groupedBacktest.get(
|
|
6684
|
+
const backtestGroup = [...groupedBacktest.get(key2) ?? []].sort(
|
|
6577
6685
|
(left, right) => left.timestamp - right.timestamp
|
|
6578
6686
|
);
|
|
6579
6687
|
const unmatchedBacktest = backtestGroup.map((entry) => ({
|
|
@@ -6814,8 +6922,8 @@ var filterBucketKeysByWindow = (bucketKeys, prefix, startTime, endTime) => {
|
|
|
6814
6922
|
if (!allowedDayKeys.size) {
|
|
6815
6923
|
return [];
|
|
6816
6924
|
}
|
|
6817
|
-
return bucketKeys.filter((
|
|
6818
|
-
const suffix =
|
|
6925
|
+
return bucketKeys.filter((key2) => {
|
|
6926
|
+
const suffix = key2.slice(prefix.length);
|
|
6819
6927
|
const firstColon = suffix.indexOf(":");
|
|
6820
6928
|
if (firstColon <= 0) {
|
|
6821
6929
|
return false;
|
|
@@ -6834,7 +6942,7 @@ var loadRuntimeSignals = async (userName, {
|
|
|
6834
6942
|
startTime,
|
|
6835
6943
|
endTime
|
|
6836
6944
|
);
|
|
6837
|
-
const refs = (await Promise.all(bucketKeys.map((
|
|
6945
|
+
const refs = (await Promise.all(bucketKeys.map((key2) => (0, import_redis4.getHashJsonValues)(key2)))).flat().filter(isRuntimeSignalBucketRef);
|
|
6838
6946
|
const seen = /* @__PURE__ */ new Set();
|
|
6839
6947
|
const payloads = await Promise.all(
|
|
6840
6948
|
refs.map(async (ref) => {
|
|
@@ -6863,7 +6971,7 @@ var loadRuntimeSignalEvaluations = async (userName, {
|
|
|
6863
6971
|
startTime,
|
|
6864
6972
|
endTime
|
|
6865
6973
|
);
|
|
6866
|
-
const evaluations = (await Promise.all(bucketKeys.map((
|
|
6974
|
+
const evaluations = (await Promise.all(bucketKeys.map((key2) => (0, import_redis4.getHashJsonValues)(key2)))).flat().filter(isRuntimeSignalEvaluationRecord);
|
|
6867
6975
|
const deduped = /* @__PURE__ */ new Map();
|
|
6868
6976
|
for (const evaluation of evaluations) {
|
|
6869
6977
|
deduped.set(evaluation.evaluationId, evaluation);
|
|
@@ -7033,10 +7141,10 @@ var syncRuntimeTrades = async ({
|
|
|
7033
7141
|
)
|
|
7034
7142
|
];
|
|
7035
7143
|
await Promise.all(
|
|
7036
|
-
activeTradeKeys.map(async (
|
|
7037
|
-
const activeRef = await (0, import_redis5.getData)(
|
|
7144
|
+
activeTradeKeys.map(async (key2) => {
|
|
7145
|
+
const activeRef = await (0, import_redis5.getData)(key2, null);
|
|
7038
7146
|
activeOrderIdByKey.set(
|
|
7039
|
-
|
|
7147
|
+
key2,
|
|
7040
7148
|
typeof activeRef?.orderId === "string" ? activeRef.orderId : null
|
|
7041
7149
|
);
|
|
7042
7150
|
})
|
|
@@ -8103,9 +8211,9 @@ var filterReplayComparisonByLineage = ({
|
|
|
8103
8211
|
);
|
|
8104
8212
|
const deploymentWindows = /* @__PURE__ */ new Map();
|
|
8105
8213
|
for (const scope of comparableRuntimeLineageScopes) {
|
|
8106
|
-
const
|
|
8107
|
-
const existing = deploymentWindows.get(
|
|
8108
|
-
deploymentWindows.set(
|
|
8214
|
+
const key2 = lineageScopeKey(scope);
|
|
8215
|
+
const existing = deploymentWindows.get(key2);
|
|
8216
|
+
deploymentWindows.set(key2, {
|
|
8109
8217
|
firstTimestamp: existing == null ? scope.firstTimestamp : Math.min(existing.firstTimestamp, scope.firstTimestamp),
|
|
8110
8218
|
lastTimestamp: existing == null ? scope.lastTimestamp : Math.max(existing.lastTimestamp, scope.lastTimestamp)
|
|
8111
8219
|
});
|
|
@@ -8141,9 +8249,9 @@ var filterReplayComparisonByLineage = ({
|
|
|
8141
8249
|
...comparableRuntimeSignals,
|
|
8142
8250
|
...comparableRuntimeEvaluations
|
|
8143
8251
|
]) {
|
|
8144
|
-
const
|
|
8145
|
-
const existing = deploymentWindows.get(
|
|
8146
|
-
deploymentWindows.set(
|
|
8252
|
+
const key2 = lineageScopeKey(artifact);
|
|
8253
|
+
const existing = deploymentWindows.get(key2);
|
|
8254
|
+
deploymentWindows.set(key2, {
|
|
8147
8255
|
firstTimestamp: existing == null ? artifact.timestamp : Math.min(existing.firstTimestamp, artifact.timestamp),
|
|
8148
8256
|
lastTimestamp: existing == null ? artifact.timestamp : Math.max(existing.lastTimestamp, artifact.timestamp)
|
|
8149
8257
|
});
|
|
@@ -8228,16 +8336,16 @@ var compareExchangeEntriesToBacktest = ({
|
|
|
8228
8336
|
const groupedExchange = /* @__PURE__ */ new Map();
|
|
8229
8337
|
const groupedBacktest = /* @__PURE__ */ new Map();
|
|
8230
8338
|
for (const entry of exchangeEntries) {
|
|
8231
|
-
const
|
|
8232
|
-
const bucket = groupedExchange.get(
|
|
8339
|
+
const key2 = `${entry.symbol}::${entry.direction}`;
|
|
8340
|
+
const bucket = groupedExchange.get(key2) ?? [];
|
|
8233
8341
|
bucket.push(entry);
|
|
8234
|
-
groupedExchange.set(
|
|
8342
|
+
groupedExchange.set(key2, bucket);
|
|
8235
8343
|
}
|
|
8236
8344
|
for (const entry of backtestEntries) {
|
|
8237
|
-
const
|
|
8238
|
-
const bucket = groupedBacktest.get(
|
|
8345
|
+
const key2 = `${entry.symbol}::${entry.direction}`;
|
|
8346
|
+
const bucket = groupedBacktest.get(key2) ?? [];
|
|
8239
8347
|
bucket.push(entry);
|
|
8240
|
-
groupedBacktest.set(
|
|
8348
|
+
groupedBacktest.set(key2, bucket);
|
|
8241
8349
|
}
|
|
8242
8350
|
const matched = [];
|
|
8243
8351
|
const exchangeOnly = [];
|
|
@@ -8247,11 +8355,11 @@ var compareExchangeEntriesToBacktest = ({
|
|
|
8247
8355
|
...groupedExchange.keys(),
|
|
8248
8356
|
...groupedBacktest.keys()
|
|
8249
8357
|
]);
|
|
8250
|
-
for (const
|
|
8251
|
-
const exchangeGroup = [...groupedExchange.get(
|
|
8358
|
+
for (const key2 of groupKeys) {
|
|
8359
|
+
const exchangeGroup = [...groupedExchange.get(key2) ?? []].sort(
|
|
8252
8360
|
(left, right) => left.entryTimestamp - right.entryTimestamp
|
|
8253
8361
|
);
|
|
8254
|
-
const unmatchedBacktest = [...groupedBacktest.get(
|
|
8362
|
+
const unmatchedBacktest = [...groupedBacktest.get(key2) ?? []].sort((left, right) => left.timestamp - right.timestamp).map((entry) => ({ entry, used: false }));
|
|
8255
8363
|
for (const exchangeEntry of exchangeGroup) {
|
|
8256
8364
|
let bestIndex = -1;
|
|
8257
8365
|
let bestDiff = Number.POSITIVE_INFINITY;
|
|
@@ -8672,14 +8780,14 @@ var mergeExchangeEntryExecutions = (rows) => {
|
|
|
8672
8780
|
const mergedRows = [];
|
|
8673
8781
|
const groups = /* @__PURE__ */ new Map();
|
|
8674
8782
|
for (const row of rows) {
|
|
8675
|
-
const
|
|
8676
|
-
if (!
|
|
8783
|
+
const key2 = getExchangeExecutionMergeKey(row);
|
|
8784
|
+
if (!key2) {
|
|
8677
8785
|
mergedRows.push(row);
|
|
8678
8786
|
continue;
|
|
8679
8787
|
}
|
|
8680
|
-
const group = groups.get(
|
|
8788
|
+
const group = groups.get(key2) ?? [];
|
|
8681
8789
|
group.push(row);
|
|
8682
|
-
groups.set(
|
|
8790
|
+
groups.set(key2, group);
|
|
8683
8791
|
}
|
|
8684
8792
|
for (const group of groups.values()) {
|
|
8685
8793
|
mergedRows.push(
|
|
@@ -9170,13 +9278,13 @@ function* jsonParts(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
|
9170
9278
|
throw new TypeError("JSON evidence cannot contain toJSON hooks");
|
|
9171
9279
|
yield "{";
|
|
9172
9280
|
let first = true;
|
|
9173
|
-
for (const
|
|
9174
|
-
const field = Reflect.get(value,
|
|
9281
|
+
for (const key2 of Object.keys(value)) {
|
|
9282
|
+
const field = Reflect.get(value, key2);
|
|
9175
9283
|
if (field === void 0 || typeof field === "function" || typeof field === "symbol")
|
|
9176
9284
|
continue;
|
|
9177
9285
|
if (!first) yield ",";
|
|
9178
9286
|
first = false;
|
|
9179
|
-
yield JSON.stringify(
|
|
9287
|
+
yield JSON.stringify(key2) + ":";
|
|
9180
9288
|
yield* jsonParts(field, ancestors);
|
|
9181
9289
|
}
|
|
9182
9290
|
yield "}";
|
|
@@ -9638,10 +9746,276 @@ var writeReplayOutputReport = async ({
|
|
|
9638
9746
|
};
|
|
9639
9747
|
|
|
9640
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");
|
|
9641
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"));
|
|
9642
9766
|
var import_node_path6 = __toESM(require("path"));
|
|
9643
|
-
|
|
9644
|
-
|
|
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;
|
|
9645
10019
|
var WINDOWS = [365, 180, 90, 30, 7];
|
|
9646
10020
|
var COLORS = [
|
|
9647
10021
|
"#2563eb",
|
|
@@ -9721,7 +10095,7 @@ var calculatePortfolioMetricRow = ({
|
|
|
9721
10095
|
const pnl = selected.reduce((sum, trade) => sum + trade.pnl, 0);
|
|
9722
10096
|
const wins = selected.filter((trade) => trade.pnl > 0).length;
|
|
9723
10097
|
const losses = selected.filter((trade) => trade.pnl <= 0).length;
|
|
9724
|
-
const days = Math.max(1, (end - start) /
|
|
10098
|
+
const days = Math.max(1, (end - start) / DAY_MS5);
|
|
9725
10099
|
return {
|
|
9726
10100
|
window,
|
|
9727
10101
|
scope,
|
|
@@ -9741,7 +10115,7 @@ var windowBounds = (start, end) => [
|
|
|
9741
10115
|
{ label: "full", start, end },
|
|
9742
10116
|
...WINDOWS.map((days) => ({
|
|
9743
10117
|
label: `${days}d`,
|
|
9744
|
-
start: Math.max(start, end - days *
|
|
10118
|
+
start: Math.max(start, end - days * DAY_MS5),
|
|
9745
10119
|
end
|
|
9746
10120
|
}))
|
|
9747
10121
|
];
|
|
@@ -9816,10 +10190,11 @@ var writePortfolioReport = async ({
|
|
|
9816
10190
|
replayResult,
|
|
9817
10191
|
window,
|
|
9818
10192
|
lineage,
|
|
9819
|
-
command
|
|
10193
|
+
command,
|
|
10194
|
+
compareTo
|
|
9820
10195
|
}) => {
|
|
9821
|
-
const outputDir =
|
|
9822
|
-
await
|
|
10196
|
+
const outputDir = import_node_path8.default.resolve(projectRoot, "data/replay/output");
|
|
10197
|
+
await import_promises5.default.mkdir(outputDir, { recursive: true });
|
|
9823
10198
|
const trades = replayResult.strategies.flatMap(
|
|
9824
10199
|
(strategy) => tradesFromPositionLog(strategy.strategyName, strategy.positionLog)
|
|
9825
10200
|
);
|
|
@@ -9891,13 +10266,13 @@ var writePortfolioReport = async ({
|
|
|
9891
10266
|
});
|
|
9892
10267
|
const prefix = `${timestamp}-portfolio-backtest`;
|
|
9893
10268
|
const paths = {
|
|
9894
|
-
markdown:
|
|
9895
|
-
json:
|
|
9896
|
-
equitySvg:
|
|
9897
|
-
drawdownSvg:
|
|
9898
|
-
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`)
|
|
9899
10274
|
};
|
|
9900
|
-
const
|
|
10275
|
+
const baseReport = {
|
|
9901
10276
|
schema: "tradejs-portfolio-backtest/v1",
|
|
9902
10277
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9903
10278
|
command,
|
|
@@ -9919,9 +10294,17 @@ var writePortfolioReport = async ({
|
|
|
9919
10294
|
portfolioDrawdown: drawdownCurve(portfolioEquity),
|
|
9920
10295
|
strategies: strategySeries.map(({ name, points }) => ({ name, points }))
|
|
9921
10296
|
},
|
|
9922
|
-
trades
|
|
10297
|
+
trades,
|
|
10298
|
+
...replayResult.portfolioDiagnostics ? { portfolioDiagnostics: replayResult.portfolioDiagnostics } : {}
|
|
9923
10299
|
};
|
|
9924
|
-
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");
|
|
9925
10308
|
const semantics = "Production strategy order, at most one open position per symbol, and fixed-risk symbol batches merged by canonical realized PnL.";
|
|
9926
10309
|
const markdown = `# Portfolio Backtest
|
|
9927
10310
|
|
|
@@ -9933,9 +10316,9 @@ var writePortfolioReport = async ({
|
|
|
9933
10316
|
- Report checksum: \`${checksum}\`
|
|
9934
10317
|
- Command: \`${command}\`
|
|
9935
10318
|
|
|
9936
|
-
})
|
|
9937
10320
|
|
|
9938
|
-
})
|
|
9939
10322
|
|
|
9940
10323
|
## Portfolio metrics
|
|
9941
10324
|
|
|
@@ -9946,17 +10329,33 @@ ${markdownTable(portfolioRows)}
|
|
|
9946
10329
|
${markdownTable(strategyRows)}
|
|
9947
10330
|
`;
|
|
9948
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
|
+
` : "";
|
|
9949
10341
|
await Promise.all([
|
|
9950
|
-
|
|
9951
|
-
|
|
10342
|
+
import_promises5.default.writeFile(paths.markdown, markdown + comparisonMarkdown, "utf8"),
|
|
10343
|
+
import_promises5.default.writeFile(
|
|
9952
10344
|
paths.json,
|
|
9953
10345
|
`${JSON.stringify({ ...report, checksum }, null, 2)}
|
|
9954
10346
|
`,
|
|
9955
10347
|
"utf8"
|
|
9956
10348
|
),
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
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
|
+
)
|
|
9960
10359
|
]);
|
|
9961
10360
|
return { ...paths, checksum };
|
|
9962
10361
|
};
|
|
@@ -9999,8 +10398,8 @@ var cachedTickerSymbols = async ({
|
|
|
9999
10398
|
import_redis6.redisKeys.tickerUniverse(replayUserName, name, universe, "default"),
|
|
10000
10399
|
import_redis6.redisKeys.tickerUniverse(replayUserName, name)
|
|
10001
10400
|
]);
|
|
10002
|
-
for (const
|
|
10003
|
-
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);
|
|
10004
10403
|
const symbols = (cached?.tickers ?? []).map(
|
|
10005
10404
|
(ticker) => String(ticker.symbol ?? "").trim().toUpperCase()
|
|
10006
10405
|
).filter(Boolean);
|
|
@@ -10069,7 +10468,8 @@ var finishReplay = async ({
|
|
|
10069
10468
|
replayResult,
|
|
10070
10469
|
window,
|
|
10071
10470
|
lineage: portfolioLineage ?? {},
|
|
10072
|
-
command: process.argv.join(" ")
|
|
10471
|
+
command: process.argv.join(" "),
|
|
10472
|
+
compareTo: replayFlags.portfolioCompareTo
|
|
10073
10473
|
}) : null;
|
|
10074
10474
|
if (portfolioReport) {
|
|
10075
10475
|
console.log(import_chalk13.default.green(`Portfolio report: ${portfolioReport.html}`));
|
|
@@ -10353,6 +10753,18 @@ var replayBacktest = async () => {
|
|
|
10353
10753
|
`Invalid --portfolioBatchSize: ${String(replayFlags.portfolioBatchSize)}`
|
|
10354
10754
|
);
|
|
10355
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
|
+
}
|
|
10356
10768
|
const runtimeStrategies = replayFlags.tickers && !replayFlags.portfolioReport ? replayStrategies.map(
|
|
10357
10769
|
({ selection: _selection, ...strategy }) => strategy
|
|
10358
10770
|
) : replayStrategies;
|
|
@@ -10376,6 +10788,9 @@ var replayBacktest = async () => {
|
|
|
10376
10788
|
showProgress: !replayFlags.portfolioReport,
|
|
10377
10789
|
collectSkipEvidence: !replayFlags.portfolioReport,
|
|
10378
10790
|
collectSignals: !replayFlags.portfolioReport,
|
|
10791
|
+
collectPortfolioDiagnostics: Boolean(
|
|
10792
|
+
replayFlags.portfolioReport && tickerBatches.length === 1
|
|
10793
|
+
),
|
|
10379
10794
|
decisionTraceTargets: replayFlags.portfolioReport ? void 0 : evidenceMetadata?.decisionTraceTargets
|
|
10380
10795
|
});
|
|
10381
10796
|
return replayFlags.portfolioReport ? compactHistoricalReplayResultForPortfolio(result) : result;
|
|
@@ -10413,6 +10828,9 @@ var replayBacktest = async () => {
|
|
|
10413
10828
|
cacheOnly: Boolean(replayFlags.cacheOnly),
|
|
10414
10829
|
portfolioSemantics: "production strategy order and one concurrent position per symbol; fixed-risk symbol batches are merged by canonical realized PnL",
|
|
10415
10830
|
portfolioBatchSize,
|
|
10831
|
+
batchCount: tickerBatches.length,
|
|
10832
|
+
connector: preparedRun.connectorName,
|
|
10833
|
+
interval: replayInterval,
|
|
10416
10834
|
tickers: preparedRun.tickers,
|
|
10417
10835
|
candleCoverage: candleCoverage ?? null,
|
|
10418
10836
|
binanceMarketContextRows: portfolioMarketContextRows ?? null,
|