@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
|
@@ -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
|
|
|
@@ -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(
|
|
@@ -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,16 @@ 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;
|
|
5196
|
+
const markedAtBySymbol = /* @__PURE__ */ new Map();
|
|
5197
|
+
const entryFacts = /* @__PURE__ */ new Map();
|
|
5198
|
+
let portfolioObserved = false;
|
|
5191
5199
|
const orderLog = [];
|
|
5192
5200
|
const positionLog = [];
|
|
5193
5201
|
const orderLogByStrategy = /* @__PURE__ */ new Map();
|
|
@@ -5425,14 +5433,51 @@ var createPortfolioReplayConnector = (connector) => {
|
|
|
5425
5433
|
const { strategyName: _strategyName, ...nextPosition } = position;
|
|
5426
5434
|
return nextPosition;
|
|
5427
5435
|
}),
|
|
5436
|
+
getPortfolioObservation: async (asOf, reservations) => {
|
|
5437
|
+
portfolioObserved = true;
|
|
5438
|
+
const pendingIds = new Set(reservations.map((r) => r.id));
|
|
5439
|
+
for (const id of entryFacts.keys())
|
|
5440
|
+
if (!pendingIds.has(id)) entryFacts.delete(id);
|
|
5441
|
+
return {
|
|
5442
|
+
asOf,
|
|
5443
|
+
positions: Array.from(positionsBySymbol.values()).map(
|
|
5444
|
+
({ position, stopLossPrice }) => ({
|
|
5445
|
+
id: position.orderId ?? `${position.strategyName}:${position.symbol}:${position.timestamp}`,
|
|
5446
|
+
strategy: position.strategyName === UNKNOWN_STRATEGY ? null : position.strategyName,
|
|
5447
|
+
symbol: position.symbol,
|
|
5448
|
+
direction: position.direction,
|
|
5449
|
+
qty: position.qty,
|
|
5450
|
+
entryPrice: position.price,
|
|
5451
|
+
markPrice: currentPriceBySymbol.get(position.symbol) ?? null,
|
|
5452
|
+
markedAt: markedAtBySymbol.get(position.symbol) ?? null,
|
|
5453
|
+
stopLossPrice
|
|
5454
|
+
})
|
|
5455
|
+
),
|
|
5456
|
+
orders: reservations.map(
|
|
5457
|
+
(r) => entryFacts.get(r.id) ?? {
|
|
5458
|
+
id: r.id,
|
|
5459
|
+
status: "pending",
|
|
5460
|
+
filledQty: 0
|
|
5461
|
+
}
|
|
5462
|
+
)
|
|
5463
|
+
};
|
|
5464
|
+
},
|
|
5428
5465
|
getOpenPositionPnl: async () => Array.from(positionsBySymbol.values()).map(
|
|
5429
|
-
({ position }) =>
|
|
5430
|
-
|
|
5431
|
-
|
|
5466
|
+
({ position, stopLossPrice }) => ({
|
|
5467
|
+
...getPositionPnlSnapshot({
|
|
5468
|
+
position,
|
|
5469
|
+
currentPrice: currentPriceBySymbol.get(position.symbol) ?? position.price
|
|
5470
|
+
}),
|
|
5471
|
+
observedAt: markedAtBySymbol.get(position.symbol),
|
|
5472
|
+
stopLossPrice
|
|
5432
5473
|
})
|
|
5433
5474
|
),
|
|
5434
5475
|
placeOrder: async (order) => {
|
|
5435
|
-
const {
|
|
5476
|
+
const {
|
|
5477
|
+
beforeSubmit,
|
|
5478
|
+
onRejected: _onRejected,
|
|
5479
|
+
...submittedOrder
|
|
5480
|
+
} = order;
|
|
5436
5481
|
if (positionsBySymbol.has(order.symbol)) {
|
|
5437
5482
|
return false;
|
|
5438
5483
|
}
|
|
@@ -5476,6 +5521,14 @@ var createPortfolioReplayConnector = (connector) => {
|
|
|
5476
5521
|
stopLossPrice: null
|
|
5477
5522
|
};
|
|
5478
5523
|
positionsBySymbol.set(order.symbol, positionState);
|
|
5524
|
+
markedAtBySymbol.set(order.symbol, executionTimestamp ?? order.timestamp);
|
|
5525
|
+
if (portfolioObserved && order.orderId)
|
|
5526
|
+
entryFacts.set(order.orderId, {
|
|
5527
|
+
id: order.orderId,
|
|
5528
|
+
status: "filled",
|
|
5529
|
+
filledQty: order.qty,
|
|
5530
|
+
positionId: order.orderId
|
|
5531
|
+
});
|
|
5479
5532
|
logOrder({
|
|
5480
5533
|
positionState,
|
|
5481
5534
|
data: {
|
|
@@ -5559,11 +5612,26 @@ var createPortfolioReplayConnector = (connector) => {
|
|
|
5559
5612
|
getTickers: connector.getTickers,
|
|
5560
5613
|
advanceMarket: async ({ symbol, candle }) => {
|
|
5561
5614
|
executionTimestamp = candle.timestamp;
|
|
5615
|
+
markedAtBySymbol.set(symbol, candle.timestamp);
|
|
5616
|
+
equityMarks?.set(symbol, candle.close);
|
|
5562
5617
|
currentPriceBySymbol.set(symbol, candle.close);
|
|
5563
5618
|
await checkSl({ symbol, candle });
|
|
5564
5619
|
await checkTp({ symbol, candle });
|
|
5565
5620
|
currentPriceBySymbol.set(symbol, candle.close);
|
|
5566
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
|
+
},
|
|
5567
5635
|
getReplayArtifacts: () => ({
|
|
5568
5636
|
orderLog,
|
|
5569
5637
|
positionLog,
|
|
@@ -5777,6 +5845,21 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5777
5845
|
context.collectSkipEvidence
|
|
5778
5846
|
);
|
|
5779
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
|
+
};
|
|
5780
5863
|
const reportProgress = (timestamp) => {
|
|
5781
5864
|
if (adapters.progress.enabled === false) return;
|
|
5782
5865
|
adapters.progress.tick({
|
|
@@ -5820,6 +5903,7 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5820
5903
|
}
|
|
5821
5904
|
}
|
|
5822
5905
|
advanceAbortedCycle(cycleSymbols, timestamp);
|
|
5906
|
+
observeEquity();
|
|
5823
5907
|
reportProgress(timestamp);
|
|
5824
5908
|
continue;
|
|
5825
5909
|
}
|
|
@@ -5863,6 +5947,21 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5863
5947
|
result
|
|
5864
5948
|
});
|
|
5865
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
|
+
}
|
|
5866
5965
|
result.runtimeLineage = strategyRuntime.runtimeLineage;
|
|
5867
5966
|
await adapters.enrichSignal(result);
|
|
5868
5967
|
cycleSignals.push(result);
|
|
@@ -5877,12 +5976,16 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5877
5976
|
status: "completed",
|
|
5878
5977
|
durationMs: adapters.clock.now() - cycleStartedAt
|
|
5879
5978
|
});
|
|
5979
|
+
observeEquity();
|
|
5880
5980
|
reportProgress(timestamp);
|
|
5881
5981
|
}
|
|
5882
5982
|
} finally {
|
|
5883
5983
|
releaseHistoricalReplayCaches(context.plan, adapters);
|
|
5884
5984
|
}
|
|
5985
|
+
if (portfolioDiagnostics?.unpricedCycles)
|
|
5986
|
+
portfolioDiagnostics.markedMaxDrawdown = null;
|
|
5885
5987
|
return {
|
|
5988
|
+
...portfolioDiagnostics ? { portfolioDiagnostics } : {},
|
|
5886
5989
|
signals,
|
|
5887
5990
|
abortedCycles,
|
|
5888
5991
|
replayLineageScopes: skipEvidence.values(),
|
|
@@ -5938,6 +6041,7 @@ var collectHistoricalReplayResult = (context) => ({
|
|
|
5938
6041
|
abortedCycles: context.abortedCycles,
|
|
5939
6042
|
runtimeLineages: context.runtimeLineages,
|
|
5940
6043
|
replayLineageScopes: context.replayLineageScopes,
|
|
6044
|
+
...context.portfolioDiagnostics ? { portfolioDiagnostics: context.portfolioDiagnostics } : {},
|
|
5941
6045
|
...context.replayDecisionTraces?.length ? { replayDecisionTraces: context.replayDecisionTraces } : {}
|
|
5942
6046
|
});
|
|
5943
6047
|
var compactHistoricalReplayResultForPortfolio = (result) => ({
|
|
@@ -6139,6 +6243,7 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6139
6243
|
showProgress = true,
|
|
6140
6244
|
collectSkipEvidence = true,
|
|
6141
6245
|
collectSignals = true,
|
|
6246
|
+
collectPortfolioDiagnostics = false,
|
|
6142
6247
|
decisionTraceTargets
|
|
6143
6248
|
}) => {
|
|
6144
6249
|
const memory = createReplayMemoryProfile();
|
|
@@ -6148,7 +6253,8 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6148
6253
|
const projectHooks = projectConfig.hooks;
|
|
6149
6254
|
const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
|
|
6150
6255
|
const replayConnector = createPortfolioReplayConnector(
|
|
6151
|
-
preparedRun.marketConnector
|
|
6256
|
+
preparedRun.marketConnector,
|
|
6257
|
+
collectPortfolioDiagnostics
|
|
6152
6258
|
);
|
|
6153
6259
|
const connectorName = getConnectorName(preparedRun.marketConnector);
|
|
6154
6260
|
const referenceData = references ?? await loadHistoricalReplayReferences({ preparedRun, interval });
|
|
@@ -6226,6 +6332,7 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6226
6332
|
connector: replayConnector,
|
|
6227
6333
|
collectSkipEvidence,
|
|
6228
6334
|
collectSignals,
|
|
6335
|
+
collectPortfolioDiagnostics,
|
|
6229
6336
|
decisionTraceTargets,
|
|
6230
6337
|
hooks: projectHooks,
|
|
6231
6338
|
hookContext
|
|
@@ -6264,7 +6371,8 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6264
6371
|
abortedCycles: execution.abortedCycles,
|
|
6265
6372
|
runtimeLineages: plan.runtimeLineages,
|
|
6266
6373
|
replayLineageScopes: execution.replayLineageScopes,
|
|
6267
|
-
replayDecisionTraces: execution.replayDecisionTraces
|
|
6374
|
+
replayDecisionTraces: execution.replayDecisionTraces,
|
|
6375
|
+
portfolioDiagnostics: execution.portfolioDiagnostics
|
|
6268
6376
|
});
|
|
6269
6377
|
memory?.("result:collected");
|
|
6270
6378
|
return result;
|
|
@@ -6511,16 +6619,16 @@ var findRuntimeDuplicateGroups = (entries) => {
|
|
|
6511
6619
|
if (entry.source !== "runtime") {
|
|
6512
6620
|
continue;
|
|
6513
6621
|
}
|
|
6514
|
-
const
|
|
6515
|
-
const bucket = groups.get(
|
|
6622
|
+
const key2 = toRuntimeDuplicateKey(entry);
|
|
6623
|
+
const bucket = groups.get(key2) ?? [];
|
|
6516
6624
|
bucket.push(entry);
|
|
6517
|
-
groups.set(
|
|
6625
|
+
groups.set(key2, bucket);
|
|
6518
6626
|
}
|
|
6519
|
-
return [...groups.entries()].filter(([, group]) => group.length > 1).map(([
|
|
6627
|
+
return [...groups.entries()].filter(([, group]) => group.length > 1).map(([key2, group]) => {
|
|
6520
6628
|
const sortedGroup = [...group].sort(sortTradeParityEntries);
|
|
6521
6629
|
const first = sortedGroup[0];
|
|
6522
6630
|
return {
|
|
6523
|
-
key,
|
|
6631
|
+
key: key2,
|
|
6524
6632
|
strategy: first.strategy,
|
|
6525
6633
|
symbol: first.symbol,
|
|
6526
6634
|
direction: first.direction,
|
|
@@ -6559,26 +6667,26 @@ var compareTradeParityEntries = ({
|
|
|
6559
6667
|
const groupedRuntime = /* @__PURE__ */ new Map();
|
|
6560
6668
|
const groupedBacktest = /* @__PURE__ */ new Map();
|
|
6561
6669
|
for (const entry of runtimeEntries) {
|
|
6562
|
-
const
|
|
6563
|
-
const bucket = groupedRuntime.get(
|
|
6670
|
+
const key2 = toGroupKey(entry);
|
|
6671
|
+
const bucket = groupedRuntime.get(key2) ?? [];
|
|
6564
6672
|
bucket.push(entry);
|
|
6565
|
-
groupedRuntime.set(
|
|
6673
|
+
groupedRuntime.set(key2, bucket);
|
|
6566
6674
|
}
|
|
6567
6675
|
for (const entry of backtestEntries) {
|
|
6568
|
-
const
|
|
6569
|
-
const bucket = groupedBacktest.get(
|
|
6676
|
+
const key2 = toGroupKey(entry);
|
|
6677
|
+
const bucket = groupedBacktest.get(key2) ?? [];
|
|
6570
6678
|
bucket.push(entry);
|
|
6571
|
-
groupedBacktest.set(
|
|
6679
|
+
groupedBacktest.set(key2, bucket);
|
|
6572
6680
|
}
|
|
6573
6681
|
const groupKeys = /* @__PURE__ */ new Set([
|
|
6574
6682
|
...groupedRuntime.keys(),
|
|
6575
6683
|
...groupedBacktest.keys()
|
|
6576
6684
|
]);
|
|
6577
|
-
for (const
|
|
6578
|
-
const runtimeGroup = [...groupedRuntime.get(
|
|
6685
|
+
for (const key2 of groupKeys) {
|
|
6686
|
+
const runtimeGroup = [...groupedRuntime.get(key2) ?? []].sort(
|
|
6579
6687
|
(left, right) => left.timestamp - right.timestamp
|
|
6580
6688
|
);
|
|
6581
|
-
const backtestGroup = [...groupedBacktest.get(
|
|
6689
|
+
const backtestGroup = [...groupedBacktest.get(key2) ?? []].sort(
|
|
6582
6690
|
(left, right) => left.timestamp - right.timestamp
|
|
6583
6691
|
);
|
|
6584
6692
|
const unmatchedBacktest = backtestGroup.map((entry) => ({
|
|
@@ -6819,8 +6927,8 @@ var filterBucketKeysByWindow = (bucketKeys, prefix, startTime, endTime) => {
|
|
|
6819
6927
|
if (!allowedDayKeys.size) {
|
|
6820
6928
|
return [];
|
|
6821
6929
|
}
|
|
6822
|
-
return bucketKeys.filter((
|
|
6823
|
-
const suffix =
|
|
6930
|
+
return bucketKeys.filter((key2) => {
|
|
6931
|
+
const suffix = key2.slice(prefix.length);
|
|
6824
6932
|
const firstColon = suffix.indexOf(":");
|
|
6825
6933
|
if (firstColon <= 0) {
|
|
6826
6934
|
return false;
|
|
@@ -6839,7 +6947,7 @@ var loadRuntimeSignals = async (userName, {
|
|
|
6839
6947
|
startTime,
|
|
6840
6948
|
endTime
|
|
6841
6949
|
);
|
|
6842
|
-
const refs = (await Promise.all(bucketKeys.map((
|
|
6950
|
+
const refs = (await Promise.all(bucketKeys.map((key2) => (0, import_redis4.getHashJsonValues)(key2)))).flat().filter(isRuntimeSignalBucketRef);
|
|
6843
6951
|
const seen = /* @__PURE__ */ new Set();
|
|
6844
6952
|
const payloads = await Promise.all(
|
|
6845
6953
|
refs.map(async (ref) => {
|
|
@@ -6868,7 +6976,7 @@ var loadRuntimeSignalEvaluations = async (userName, {
|
|
|
6868
6976
|
startTime,
|
|
6869
6977
|
endTime
|
|
6870
6978
|
);
|
|
6871
|
-
const evaluations = (await Promise.all(bucketKeys.map((
|
|
6979
|
+
const evaluations = (await Promise.all(bucketKeys.map((key2) => (0, import_redis4.getHashJsonValues)(key2)))).flat().filter(isRuntimeSignalEvaluationRecord);
|
|
6872
6980
|
const deduped = /* @__PURE__ */ new Map();
|
|
6873
6981
|
for (const evaluation of evaluations) {
|
|
6874
6982
|
deduped.set(evaluation.evaluationId, evaluation);
|
|
@@ -7038,10 +7146,10 @@ var syncRuntimeTrades = async ({
|
|
|
7038
7146
|
)
|
|
7039
7147
|
];
|
|
7040
7148
|
await Promise.all(
|
|
7041
|
-
activeTradeKeys.map(async (
|
|
7042
|
-
const activeRef = await (0, import_redis5.getData)(
|
|
7149
|
+
activeTradeKeys.map(async (key2) => {
|
|
7150
|
+
const activeRef = await (0, import_redis5.getData)(key2, null);
|
|
7043
7151
|
activeOrderIdByKey.set(
|
|
7044
|
-
|
|
7152
|
+
key2,
|
|
7045
7153
|
typeof activeRef?.orderId === "string" ? activeRef.orderId : null
|
|
7046
7154
|
);
|
|
7047
7155
|
})
|
|
@@ -8108,9 +8216,9 @@ var filterReplayComparisonByLineage = ({
|
|
|
8108
8216
|
);
|
|
8109
8217
|
const deploymentWindows = /* @__PURE__ */ new Map();
|
|
8110
8218
|
for (const scope of comparableRuntimeLineageScopes) {
|
|
8111
|
-
const
|
|
8112
|
-
const existing = deploymentWindows.get(
|
|
8113
|
-
deploymentWindows.set(
|
|
8219
|
+
const key2 = lineageScopeKey(scope);
|
|
8220
|
+
const existing = deploymentWindows.get(key2);
|
|
8221
|
+
deploymentWindows.set(key2, {
|
|
8114
8222
|
firstTimestamp: existing == null ? scope.firstTimestamp : Math.min(existing.firstTimestamp, scope.firstTimestamp),
|
|
8115
8223
|
lastTimestamp: existing == null ? scope.lastTimestamp : Math.max(existing.lastTimestamp, scope.lastTimestamp)
|
|
8116
8224
|
});
|
|
@@ -8146,9 +8254,9 @@ var filterReplayComparisonByLineage = ({
|
|
|
8146
8254
|
...comparableRuntimeSignals,
|
|
8147
8255
|
...comparableRuntimeEvaluations
|
|
8148
8256
|
]) {
|
|
8149
|
-
const
|
|
8150
|
-
const existing = deploymentWindows.get(
|
|
8151
|
-
deploymentWindows.set(
|
|
8257
|
+
const key2 = lineageScopeKey(artifact);
|
|
8258
|
+
const existing = deploymentWindows.get(key2);
|
|
8259
|
+
deploymentWindows.set(key2, {
|
|
8152
8260
|
firstTimestamp: existing == null ? artifact.timestamp : Math.min(existing.firstTimestamp, artifact.timestamp),
|
|
8153
8261
|
lastTimestamp: existing == null ? artifact.timestamp : Math.max(existing.lastTimestamp, artifact.timestamp)
|
|
8154
8262
|
});
|
|
@@ -8233,16 +8341,16 @@ var compareExchangeEntriesToBacktest = ({
|
|
|
8233
8341
|
const groupedExchange = /* @__PURE__ */ new Map();
|
|
8234
8342
|
const groupedBacktest = /* @__PURE__ */ new Map();
|
|
8235
8343
|
for (const entry of exchangeEntries) {
|
|
8236
|
-
const
|
|
8237
|
-
const bucket = groupedExchange.get(
|
|
8344
|
+
const key2 = `${entry.symbol}::${entry.direction}`;
|
|
8345
|
+
const bucket = groupedExchange.get(key2) ?? [];
|
|
8238
8346
|
bucket.push(entry);
|
|
8239
|
-
groupedExchange.set(
|
|
8347
|
+
groupedExchange.set(key2, bucket);
|
|
8240
8348
|
}
|
|
8241
8349
|
for (const entry of backtestEntries) {
|
|
8242
|
-
const
|
|
8243
|
-
const bucket = groupedBacktest.get(
|
|
8350
|
+
const key2 = `${entry.symbol}::${entry.direction}`;
|
|
8351
|
+
const bucket = groupedBacktest.get(key2) ?? [];
|
|
8244
8352
|
bucket.push(entry);
|
|
8245
|
-
groupedBacktest.set(
|
|
8353
|
+
groupedBacktest.set(key2, bucket);
|
|
8246
8354
|
}
|
|
8247
8355
|
const matched = [];
|
|
8248
8356
|
const exchangeOnly = [];
|
|
@@ -8252,11 +8360,11 @@ var compareExchangeEntriesToBacktest = ({
|
|
|
8252
8360
|
...groupedExchange.keys(),
|
|
8253
8361
|
...groupedBacktest.keys()
|
|
8254
8362
|
]);
|
|
8255
|
-
for (const
|
|
8256
|
-
const exchangeGroup = [...groupedExchange.get(
|
|
8363
|
+
for (const key2 of groupKeys) {
|
|
8364
|
+
const exchangeGroup = [...groupedExchange.get(key2) ?? []].sort(
|
|
8257
8365
|
(left, right) => left.entryTimestamp - right.entryTimestamp
|
|
8258
8366
|
);
|
|
8259
|
-
const unmatchedBacktest = [...groupedBacktest.get(
|
|
8367
|
+
const unmatchedBacktest = [...groupedBacktest.get(key2) ?? []].sort((left, right) => left.timestamp - right.timestamp).map((entry) => ({ entry, used: false }));
|
|
8260
8368
|
for (const exchangeEntry of exchangeGroup) {
|
|
8261
8369
|
let bestIndex = -1;
|
|
8262
8370
|
let bestDiff = Number.POSITIVE_INFINITY;
|
|
@@ -8677,14 +8785,14 @@ var mergeExchangeEntryExecutions = (rows) => {
|
|
|
8677
8785
|
const mergedRows = [];
|
|
8678
8786
|
const groups = /* @__PURE__ */ new Map();
|
|
8679
8787
|
for (const row of rows) {
|
|
8680
|
-
const
|
|
8681
|
-
if (!
|
|
8788
|
+
const key2 = getExchangeExecutionMergeKey(row);
|
|
8789
|
+
if (!key2) {
|
|
8682
8790
|
mergedRows.push(row);
|
|
8683
8791
|
continue;
|
|
8684
8792
|
}
|
|
8685
|
-
const group = groups.get(
|
|
8793
|
+
const group = groups.get(key2) ?? [];
|
|
8686
8794
|
group.push(row);
|
|
8687
|
-
groups.set(
|
|
8795
|
+
groups.set(key2, group);
|
|
8688
8796
|
}
|
|
8689
8797
|
for (const group of groups.values()) {
|
|
8690
8798
|
mergedRows.push(
|
|
@@ -9175,13 +9283,13 @@ function* jsonParts(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
|
9175
9283
|
throw new TypeError("JSON evidence cannot contain toJSON hooks");
|
|
9176
9284
|
yield "{";
|
|
9177
9285
|
let first = true;
|
|
9178
|
-
for (const
|
|
9179
|
-
const field = Reflect.get(value,
|
|
9286
|
+
for (const key2 of Object.keys(value)) {
|
|
9287
|
+
const field = Reflect.get(value, key2);
|
|
9180
9288
|
if (field === void 0 || typeof field === "function" || typeof field === "symbol")
|
|
9181
9289
|
continue;
|
|
9182
9290
|
if (!first) yield ",";
|
|
9183
9291
|
first = false;
|
|
9184
|
-
yield JSON.stringify(
|
|
9292
|
+
yield JSON.stringify(key2) + ":";
|
|
9185
9293
|
yield* jsonParts(field, ancestors);
|
|
9186
9294
|
}
|
|
9187
9295
|
yield "}";
|
|
@@ -9643,10 +9751,276 @@ var writeReplayOutputReport = async ({
|
|
|
9643
9751
|
};
|
|
9644
9752
|
|
|
9645
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");
|
|
9646
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"));
|
|
9647
9771
|
var import_node_path6 = __toESM(require("path"));
|
|
9648
|
-
|
|
9649
|
-
|
|
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;
|
|
9650
10024
|
var WINDOWS = [365, 180, 90, 30, 7];
|
|
9651
10025
|
var COLORS = [
|
|
9652
10026
|
"#2563eb",
|
|
@@ -9726,7 +10100,7 @@ var calculatePortfolioMetricRow = ({
|
|
|
9726
10100
|
const pnl = selected.reduce((sum, trade) => sum + trade.pnl, 0);
|
|
9727
10101
|
const wins = selected.filter((trade) => trade.pnl > 0).length;
|
|
9728
10102
|
const losses = selected.filter((trade) => trade.pnl <= 0).length;
|
|
9729
|
-
const days = Math.max(1, (end - start) /
|
|
10103
|
+
const days = Math.max(1, (end - start) / DAY_MS5);
|
|
9730
10104
|
return {
|
|
9731
10105
|
window,
|
|
9732
10106
|
scope,
|
|
@@ -9746,7 +10120,7 @@ var windowBounds = (start, end) => [
|
|
|
9746
10120
|
{ label: "full", start, end },
|
|
9747
10121
|
...WINDOWS.map((days) => ({
|
|
9748
10122
|
label: `${days}d`,
|
|
9749
|
-
start: Math.max(start, end - days *
|
|
10123
|
+
start: Math.max(start, end - days * DAY_MS5),
|
|
9750
10124
|
end
|
|
9751
10125
|
}))
|
|
9752
10126
|
];
|
|
@@ -9821,10 +10195,11 @@ var writePortfolioReport = async ({
|
|
|
9821
10195
|
replayResult,
|
|
9822
10196
|
window,
|
|
9823
10197
|
lineage,
|
|
9824
|
-
command
|
|
10198
|
+
command,
|
|
10199
|
+
compareTo
|
|
9825
10200
|
}) => {
|
|
9826
|
-
const outputDir =
|
|
9827
|
-
await
|
|
10201
|
+
const outputDir = import_node_path8.default.resolve(projectRoot, "data/replay/output");
|
|
10202
|
+
await import_promises5.default.mkdir(outputDir, { recursive: true });
|
|
9828
10203
|
const trades = replayResult.strategies.flatMap(
|
|
9829
10204
|
(strategy) => tradesFromPositionLog(strategy.strategyName, strategy.positionLog)
|
|
9830
10205
|
);
|
|
@@ -9896,13 +10271,13 @@ var writePortfolioReport = async ({
|
|
|
9896
10271
|
});
|
|
9897
10272
|
const prefix = `${timestamp}-portfolio-backtest`;
|
|
9898
10273
|
const paths = {
|
|
9899
|
-
markdown:
|
|
9900
|
-
json:
|
|
9901
|
-
equitySvg:
|
|
9902
|
-
drawdownSvg:
|
|
9903
|
-
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`)
|
|
9904
10279
|
};
|
|
9905
|
-
const
|
|
10280
|
+
const baseReport = {
|
|
9906
10281
|
schema: "tradejs-portfolio-backtest/v1",
|
|
9907
10282
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9908
10283
|
command,
|
|
@@ -9924,9 +10299,17 @@ var writePortfolioReport = async ({
|
|
|
9924
10299
|
portfolioDrawdown: drawdownCurve(portfolioEquity),
|
|
9925
10300
|
strategies: strategySeries.map(({ name, points }) => ({ name, points }))
|
|
9926
10301
|
},
|
|
9927
|
-
trades
|
|
10302
|
+
trades,
|
|
10303
|
+
...replayResult.portfolioDiagnostics ? { portfolioDiagnostics: replayResult.portfolioDiagnostics } : {}
|
|
9928
10304
|
};
|
|
9929
|
-
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");
|
|
9930
10313
|
const semantics = "Production strategy order, at most one open position per symbol, and fixed-risk symbol batches merged by canonical realized PnL.";
|
|
9931
10314
|
const markdown = `# Portfolio Backtest
|
|
9932
10315
|
|
|
@@ -9938,9 +10321,9 @@ var writePortfolioReport = async ({
|
|
|
9938
10321
|
- Report checksum: \`${checksum}\`
|
|
9939
10322
|
- Command: \`${command}\`
|
|
9940
10323
|
|
|
9941
|
-
})
|
|
9942
10325
|
|
|
9943
|
-
})
|
|
9944
10327
|
|
|
9945
10328
|
## Portfolio metrics
|
|
9946
10329
|
|
|
@@ -9951,17 +10334,33 @@ ${markdownTable(portfolioRows)}
|
|
|
9951
10334
|
${markdownTable(strategyRows)}
|
|
9952
10335
|
`;
|
|
9953
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
|
+
` : "";
|
|
9954
10346
|
await Promise.all([
|
|
9955
|
-
|
|
9956
|
-
|
|
10347
|
+
import_promises5.default.writeFile(paths.markdown, markdown + comparisonMarkdown, "utf8"),
|
|
10348
|
+
import_promises5.default.writeFile(
|
|
9957
10349
|
paths.json,
|
|
9958
10350
|
`${JSON.stringify({ ...report, checksum }, null, 2)}
|
|
9959
10351
|
`,
|
|
9960
10352
|
"utf8"
|
|
9961
10353
|
),
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
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
|
+
)
|
|
9965
10364
|
]);
|
|
9966
10365
|
return { ...paths, checksum };
|
|
9967
10366
|
};
|
|
@@ -10004,8 +10403,8 @@ var cachedTickerSymbols = async ({
|
|
|
10004
10403
|
import_redis6.redisKeys.tickerUniverse(replayUserName, name, universe, "default"),
|
|
10005
10404
|
import_redis6.redisKeys.tickerUniverse(replayUserName, name)
|
|
10006
10405
|
]);
|
|
10007
|
-
for (const
|
|
10008
|
-
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);
|
|
10009
10408
|
const symbols = (cached?.tickers ?? []).map(
|
|
10010
10409
|
(ticker) => String(ticker.symbol ?? "").trim().toUpperCase()
|
|
10011
10410
|
).filter(Boolean);
|
|
@@ -10074,7 +10473,8 @@ var finishReplay = async ({
|
|
|
10074
10473
|
replayResult,
|
|
10075
10474
|
window,
|
|
10076
10475
|
lineage: portfolioLineage ?? {},
|
|
10077
|
-
command: process.argv.join(" ")
|
|
10476
|
+
command: process.argv.join(" "),
|
|
10477
|
+
compareTo: replayFlags.portfolioCompareTo
|
|
10078
10478
|
}) : null;
|
|
10079
10479
|
if (portfolioReport) {
|
|
10080
10480
|
console.log(import_chalk13.default.green(`Portfolio report: ${portfolioReport.html}`));
|
|
@@ -10358,6 +10758,18 @@ var replayBacktest = async () => {
|
|
|
10358
10758
|
`Invalid --portfolioBatchSize: ${String(replayFlags.portfolioBatchSize)}`
|
|
10359
10759
|
);
|
|
10360
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
|
+
}
|
|
10361
10773
|
const runtimeStrategies = replayFlags.tickers && !replayFlags.portfolioReport ? replayStrategies.map(
|
|
10362
10774
|
({ selection: _selection, ...strategy }) => strategy
|
|
10363
10775
|
) : replayStrategies;
|
|
@@ -10381,6 +10793,9 @@ var replayBacktest = async () => {
|
|
|
10381
10793
|
showProgress: !replayFlags.portfolioReport,
|
|
10382
10794
|
collectSkipEvidence: !replayFlags.portfolioReport,
|
|
10383
10795
|
collectSignals: !replayFlags.portfolioReport,
|
|
10796
|
+
collectPortfolioDiagnostics: Boolean(
|
|
10797
|
+
replayFlags.portfolioReport && tickerBatches.length === 1
|
|
10798
|
+
),
|
|
10384
10799
|
decisionTraceTargets: replayFlags.portfolioReport ? void 0 : evidenceMetadata?.decisionTraceTargets
|
|
10385
10800
|
});
|
|
10386
10801
|
return replayFlags.portfolioReport ? compactHistoricalReplayResultForPortfolio(result) : result;
|
|
@@ -10418,6 +10833,9 @@ var replayBacktest = async () => {
|
|
|
10418
10833
|
cacheOnly: Boolean(replayFlags.cacheOnly),
|
|
10419
10834
|
portfolioSemantics: "production strategy order and one concurrent position per symbol; fixed-risk symbol batches are merged by canonical realized PnL",
|
|
10420
10835
|
portfolioBatchSize,
|
|
10836
|
+
batchCount: tickerBatches.length,
|
|
10837
|
+
connector: preparedRun.connectorName,
|
|
10838
|
+
interval: replayInterval,
|
|
10421
10839
|
tickers: preparedRun.tickers,
|
|
10422
10840
|
candleCoverage: candleCoverage ?? null,
|
|
10423
10841
|
binanceMarketContextRows: portfolioMarketContextRows ?? null,
|