@tradejs/cli 2.0.6 → 2.0.7
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 +2448 -1374
- package/dist/lib/aiExport.js +1822 -0
- package/dist/lib/binanceMarketContextBackfill.js +97 -88
- package/dist/lib/binanceMarketData.js +6 -3
- package/dist/lib/marketContextPrepare.js +97 -88
- package/dist/lib/runEnvironment.js +48 -18
- package/dist/lib/runtimeLineage.js +255 -0
- package/dist/lib/runtimeParityDetails.js +24 -1
- package/dist/lib/runtimeSignalsLoader.js +19 -0
- package/dist/lib/runtimeSignalsStorage.js +3 -0
- package/dist/lib/runtimeTradeSync.js +40 -11
- package/dist/lib/timeWindow.js +44 -16
- package/dist/scripts/aiExport.js +200 -88
- package/dist/scripts/backtest.js +170 -107
- package/dist/scripts/binanceBreadthUniversesUpdate.js +120 -0
- package/dist/scripts/binanceMarketIngest.js +32 -33
- package/dist/scripts/mlExport.js +5 -2
- package/dist/scripts/replay.js +988 -218
- package/dist/scripts/replayRunner.js +988 -218
- package/dist/scripts/replayRuntimeEvidence.js +3 -1
- package/dist/scripts/resolveExportStrategy.js +7 -2
- package/dist/scripts/runtimeParity.js +141 -104
- package/dist/scripts/signals.js +381 -107
- package/dist/scripts/signalsDaemon.js +381 -107
- package/dist/scripts/signalsSummary.js +40 -11
- package/package.json +10 -9
package/dist/scripts/replay.js
CHANGED
|
@@ -1643,8 +1643,9 @@ module.exports = __toCommonJS(replay_exports);
|
|
|
1643
1643
|
// src/scripts/replayRunner.ts
|
|
1644
1644
|
var import_chalk12 = __toESM(require("chalk"));
|
|
1645
1645
|
var import_constants6 = require("@tradejs/core/constants");
|
|
1646
|
+
var import_data2 = require("@tradejs/core/data");
|
|
1646
1647
|
var import_time7 = require("@tradejs/core/time");
|
|
1647
|
-
var
|
|
1648
|
+
var import_redis6 = require("@tradejs/infra/redis");
|
|
1648
1649
|
|
|
1649
1650
|
// src/lib/runFormatting.ts
|
|
1650
1651
|
var import_date_fns = require("date-fns");
|
|
@@ -1706,11 +1707,30 @@ var resolveTimeWindow = ({
|
|
|
1706
1707
|
endTime,
|
|
1707
1708
|
nowMs = Date.now(),
|
|
1708
1709
|
defaultStartMs,
|
|
1709
|
-
defaultEndMs
|
|
1710
|
+
defaultEndMs,
|
|
1711
|
+
closedIntervalMs
|
|
1710
1712
|
}) => {
|
|
1711
1713
|
const explicitStart = toEpochMs(startTime);
|
|
1712
1714
|
const explicitEnd = toEpochMs(endTime);
|
|
1713
1715
|
const resolvedDays = toPositiveNumber(days);
|
|
1716
|
+
const alignToLastClosedInterval = (window2, preserveDuration) => {
|
|
1717
|
+
if (typeof closedIntervalMs !== "number" || !Number.isFinite(closedIntervalMs) || closedIntervalMs <= 0) {
|
|
1718
|
+
return window2;
|
|
1719
|
+
}
|
|
1720
|
+
const alignedEnd = Math.floor(window2.end / closedIntervalMs) * closedIntervalMs - 1;
|
|
1721
|
+
const delta = window2.end - alignedEnd;
|
|
1722
|
+
const alignedStart = preserveDuration ? window2.start - delta : window2.start;
|
|
1723
|
+
if (alignedStart >= alignedEnd) {
|
|
1724
|
+
throw new Error(
|
|
1725
|
+
`Invalid closed-candle window: start (${alignedStart}) must be less than end (${alignedEnd})`
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
return {
|
|
1729
|
+
...window2,
|
|
1730
|
+
start: Math.trunc(alignedStart),
|
|
1731
|
+
end: Math.trunc(alignedEnd)
|
|
1732
|
+
};
|
|
1733
|
+
};
|
|
1714
1734
|
if (explicitStart != null || explicitEnd != null) {
|
|
1715
1735
|
const resolvedStart = explicitStart ?? (resolvedDays != null && explicitEnd != null ? explicitEnd - resolvedDays * ONE_DAY_MS : defaultStartMs);
|
|
1716
1736
|
const resolvedEnd2 = explicitEnd ?? (resolvedDays != null && explicitStart != null ? explicitStart + resolvedDays * ONE_DAY_MS : defaultEndMs ?? nowMs);
|
|
@@ -1719,11 +1739,14 @@ var resolveTimeWindow = ({
|
|
|
1719
1739
|
`Invalid time window: startTime (${resolvedStart}) must be less than endTime (${resolvedEnd2})`
|
|
1720
1740
|
);
|
|
1721
1741
|
}
|
|
1722
|
-
return
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1742
|
+
return alignToLastClosedInterval(
|
|
1743
|
+
{
|
|
1744
|
+
start: resolvedStart,
|
|
1745
|
+
end: resolvedEnd2,
|
|
1746
|
+
source: "explicit"
|
|
1747
|
+
},
|
|
1748
|
+
explicitStart == null && resolvedDays != null
|
|
1749
|
+
);
|
|
1727
1750
|
}
|
|
1728
1751
|
if (resolvedDays != null) {
|
|
1729
1752
|
const resolvedEnd2 = defaultEndMs ?? nowMs;
|
|
@@ -1733,11 +1756,14 @@ var resolveTimeWindow = ({
|
|
|
1733
1756
|
`Invalid time window: computed start (${resolvedStart}) must be less than end (${resolvedEnd2})`
|
|
1734
1757
|
);
|
|
1735
1758
|
}
|
|
1736
|
-
return
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1759
|
+
return alignToLastClosedInterval(
|
|
1760
|
+
{
|
|
1761
|
+
start: Math.trunc(resolvedStart),
|
|
1762
|
+
end: Math.trunc(resolvedEnd2),
|
|
1763
|
+
source: "days"
|
|
1764
|
+
},
|
|
1765
|
+
true
|
|
1766
|
+
);
|
|
1741
1767
|
}
|
|
1742
1768
|
const resolvedEnd = defaultEndMs ?? nowMs;
|
|
1743
1769
|
if (defaultStartMs >= resolvedEnd) {
|
|
@@ -1745,11 +1771,14 @@ var resolveTimeWindow = ({
|
|
|
1745
1771
|
`Invalid default time window: start (${defaultStartMs}) must be less than end (${resolvedEnd})`
|
|
1746
1772
|
);
|
|
1747
1773
|
}
|
|
1748
|
-
return
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1774
|
+
return alignToLastClosedInterval(
|
|
1775
|
+
{
|
|
1776
|
+
start: Math.trunc(defaultStartMs),
|
|
1777
|
+
end: Math.trunc(resolvedEnd),
|
|
1778
|
+
source: "default"
|
|
1779
|
+
},
|
|
1780
|
+
true
|
|
1781
|
+
);
|
|
1753
1782
|
};
|
|
1754
1783
|
|
|
1755
1784
|
// src/lib/runtimeStrategyBacktest.ts
|
|
@@ -2189,7 +2218,8 @@ var prepareRunEnvironment = async ({
|
|
|
2189
2218
|
accountId,
|
|
2190
2219
|
deploymentId,
|
|
2191
2220
|
assetClasses,
|
|
2192
|
-
deployment
|
|
2221
|
+
deployment,
|
|
2222
|
+
closedIntervalMs
|
|
2193
2223
|
}) => {
|
|
2194
2224
|
const connectorName = await resolveRunConnectorName({
|
|
2195
2225
|
value: connector,
|
|
@@ -2252,7 +2282,8 @@ var prepareRunEnvironment = async ({
|
|
|
2252
2282
|
startTime,
|
|
2253
2283
|
endTime,
|
|
2254
2284
|
defaultStartMs: (0, import_time2.getTimestamp)(import_constants.BACKTEST_DEFAULT_DAYS),
|
|
2255
|
-
defaultEndMs: (0, import_time2.getTimestamp)()
|
|
2285
|
+
defaultEndMs: (0, import_time2.getTimestamp)(),
|
|
2286
|
+
closedIntervalMs
|
|
2256
2287
|
});
|
|
2257
2288
|
const preloadStart = (0, import_time2.getBacktestPreloadStart)(
|
|
2258
2289
|
window2.start,
|
|
@@ -2321,6 +2352,7 @@ var import_progress = __toESM(require("progress"));
|
|
|
2321
2352
|
var import_chalk4 = __toESM(require("chalk"));
|
|
2322
2353
|
var import_connectors4 = require("@tradejs/connectors");
|
|
2323
2354
|
var import_connectors5 = require("@tradejs/node/connectors");
|
|
2355
|
+
var import_strategies = require("@tradejs/node/strategies");
|
|
2324
2356
|
var import_timescale = require("@tradejs/infra/timescale");
|
|
2325
2357
|
|
|
2326
2358
|
// src/lib/binanceMarketData.ts
|
|
@@ -2330,14 +2362,6 @@ var MARKET_FEATURE_INTERVAL_MS = {
|
|
|
2330
2362
|
"15m": 9e5,
|
|
2331
2363
|
"1h": 36e5
|
|
2332
2364
|
};
|
|
2333
|
-
var STABLE_QUOTE_SYMBOLS = /* @__PURE__ */ new Set([
|
|
2334
|
-
"USDCUSDT",
|
|
2335
|
-
"FDUSDUSDT",
|
|
2336
|
-
"TUSDUSDT",
|
|
2337
|
-
"BUSDUSDT",
|
|
2338
|
-
"USDPUSDT",
|
|
2339
|
-
"DAIUSDT"
|
|
2340
|
-
]);
|
|
2341
2365
|
var safeDivide = (num, den) => Number.isFinite(num) && Number.isFinite(den) && den !== 0 ? num / den : null;
|
|
2342
2366
|
var mean = (values) => values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null;
|
|
2343
2367
|
var standardDeviation = (values) => {
|
|
@@ -2381,9 +2405,6 @@ var classifyBtcAltRegime = ({
|
|
|
2381
2405
|
if (btcVsAltReturn24h < -5e-3) return "alt_lead";
|
|
2382
2406
|
return "neutral";
|
|
2383
2407
|
};
|
|
2384
|
-
var selectBreadthUniverseFromTickers = (tickers, limit) => tickers.filter(
|
|
2385
|
-
(ticker) => ticker.symbol.endsWith("USDT") && ticker.symbol !== "BTCUSDT" && !STABLE_QUOTE_SYMBOLS.has(ticker.symbol) && Number.isFinite(ticker.turnover24h) && ticker.turnover24h > 0
|
|
2386
|
-
).sort((a, b) => b.turnover24h - a.turnover24h).slice(0, Math.max(0, limit)).map((ticker) => ticker.symbol);
|
|
2387
2408
|
var aggregateAggTradesToRows = ({
|
|
2388
2409
|
symbol,
|
|
2389
2410
|
interval,
|
|
@@ -2865,10 +2886,6 @@ var backfillBinanceMarketContext = async (params, enabled) => {
|
|
|
2865
2886
|
process.env.BINANCE_MARKET_CONTEXT_TRADE_FLOW_SYMBOL_LIMIT,
|
|
2866
2887
|
2
|
|
2867
2888
|
);
|
|
2868
|
-
const breadthLimit = asInt(
|
|
2869
|
-
process.env.BINANCE_MARKET_CONTEXT_BREADTH_LIMIT,
|
|
2870
|
-
30
|
|
2871
|
-
);
|
|
2872
2889
|
const breadthChunkDays = asFloat(
|
|
2873
2890
|
process.env.BINANCE_MARKET_CONTEXT_BREADTH_CHUNK_DAYS,
|
|
2874
2891
|
30
|
|
@@ -2991,88 +3008,113 @@ var backfillBinanceMarketContext = async (params, enabled) => {
|
|
|
2991
3008
|
}
|
|
2992
3009
|
}
|
|
2993
3010
|
if (includeBreadth) {
|
|
2994
|
-
const
|
|
2995
|
-
const breadthSymbols =
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
);
|
|
2999
|
-
const universe = `binance_top${breadthSymbols.length}_usdt`;
|
|
3000
|
-
const coverage = await (0, import_timescale.getMarketBreadthCoverage)({
|
|
3001
|
-
universe,
|
|
3002
|
-
interval,
|
|
3003
|
-
startMs: breadthStartMs,
|
|
3004
|
-
endMs
|
|
3005
|
-
});
|
|
3006
|
-
const hasBtcAltMetrics = (coverage?.btcAltMetricsRows ?? 0) >= Math.floor(Math.max(1, coverage?.rows ?? 0) * 0.9);
|
|
3007
|
-
if (!hasCoverage({
|
|
3008
|
-
coverage,
|
|
3011
|
+
const breadthUniverses = (0, import_strategies.getBinanceBreadthUniverses)();
|
|
3012
|
+
const breadthSymbols = breadthUniverses.at(-1)?.symbols ?? [];
|
|
3013
|
+
const connectorInterval = marketIntervalToConnectorInterval(interval);
|
|
3014
|
+
const chunks = buildBreadthBackfillChunks({
|
|
3009
3015
|
startMs: breadthStartMs,
|
|
3010
3016
|
endMs,
|
|
3011
|
-
intervalMs
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
chunks,
|
|
3022
|
-
coverage,
|
|
3023
|
-
intervalMs
|
|
3024
|
-
});
|
|
3025
|
-
const bar = new import_progress.default(
|
|
3026
|
-
"breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
|
|
3027
|
-
{
|
|
3028
|
-
total: Math.max(1, missingChunks.length * breadthSymbols.length),
|
|
3029
|
-
width: 24
|
|
3030
|
-
}
|
|
3031
|
-
);
|
|
3032
|
-
let candlesRead = 0;
|
|
3033
|
-
let skippedBreadthChunks = chunks.length - missingChunks.length;
|
|
3034
|
-
if (!missingChunks.length) {
|
|
3035
|
-
bar.tick(1, {
|
|
3036
|
-
candles: 0,
|
|
3037
|
-
skip: skippedBreadthChunks,
|
|
3038
|
-
chunk: "cached",
|
|
3039
|
-
symbol: universe
|
|
3017
|
+
intervalMs,
|
|
3018
|
+
chunkDays: breadthChunkDays
|
|
3019
|
+
});
|
|
3020
|
+
const plans = await Promise.all(
|
|
3021
|
+
breadthUniverses.map(async (definition) => {
|
|
3022
|
+
const coverage = await (0, import_timescale.getMarketBreadthCoverage)({
|
|
3023
|
+
universe: definition.universe,
|
|
3024
|
+
interval,
|
|
3025
|
+
startMs: breadthStartMs,
|
|
3026
|
+
endMs
|
|
3040
3027
|
});
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
candlesBySymbol[symbol] = candles;
|
|
3057
|
-
candlesRead += candles.length;
|
|
3058
|
-
bar.tick(1, {
|
|
3059
|
-
candles: candlesRead,
|
|
3060
|
-
skip: skippedBreadthChunks,
|
|
3061
|
-
chunk: `${originalChunkIndex + 1}/${chunks.length}`,
|
|
3062
|
-
symbol
|
|
3063
|
-
});
|
|
3028
|
+
const hasBtcAltMetrics = (coverage?.btcAltMetricsRows ?? 0) >= Math.floor(Math.max(1, coverage?.rows ?? 0) * 0.9);
|
|
3029
|
+
const missingChunks2 = !hasCoverage({
|
|
3030
|
+
coverage,
|
|
3031
|
+
startMs: breadthStartMs,
|
|
3032
|
+
endMs,
|
|
3033
|
+
intervalMs
|
|
3034
|
+
}) || !hasBtcAltMetrics ? !hasBtcAltMetrics ? chunks : filterMissingBreadthBackfillChunks({
|
|
3035
|
+
chunks,
|
|
3036
|
+
coverage,
|
|
3037
|
+
intervalMs
|
|
3038
|
+
}) : [];
|
|
3039
|
+
if (!missingChunks2.length) {
|
|
3040
|
+
console.log(
|
|
3041
|
+
import_chalk4.default.gray(`breadth cached: universe=${definition.universe}`)
|
|
3042
|
+
);
|
|
3064
3043
|
}
|
|
3065
|
-
|
|
3066
|
-
|
|
3044
|
+
return {
|
|
3045
|
+
definition,
|
|
3046
|
+
missingChunkKeys: new Set(
|
|
3047
|
+
missingChunks2.map((chunk) => `${chunk.startMs}:${chunk.endMs}`)
|
|
3048
|
+
)
|
|
3049
|
+
};
|
|
3050
|
+
})
|
|
3051
|
+
);
|
|
3052
|
+
const missingChunks = chunks.filter((chunk) => {
|
|
3053
|
+
const key = `${chunk.startMs}:${chunk.endMs}`;
|
|
3054
|
+
return plans.some((plan) => plan.missingChunkKeys.has(key));
|
|
3055
|
+
});
|
|
3056
|
+
const bar = new import_progress.default(
|
|
3057
|
+
"breadth :current/:total [:bar][:percent] :etas(s) candles=:candles skip=:skip chunk=:chunk :symbol",
|
|
3058
|
+
{
|
|
3059
|
+
total: Math.max(1, missingChunks.length * breadthSymbols.length),
|
|
3060
|
+
width: 24
|
|
3061
|
+
}
|
|
3062
|
+
);
|
|
3063
|
+
let candlesRead = 0;
|
|
3064
|
+
const skippedBreadthChunks = chunks.length - missingChunks.length;
|
|
3065
|
+
if (!missingChunks.length) {
|
|
3066
|
+
bar.tick(1, {
|
|
3067
|
+
candles: 0,
|
|
3068
|
+
skip: skippedBreadthChunks,
|
|
3069
|
+
chunk: "cached",
|
|
3070
|
+
symbol: "top5/top10/top30/top50/top100"
|
|
3071
|
+
});
|
|
3072
|
+
}
|
|
3073
|
+
for (let chunkIndex = 0; chunkIndex < missingChunks.length; chunkIndex += 1) {
|
|
3074
|
+
const chunk = missingChunks[chunkIndex];
|
|
3075
|
+
const originalChunkIndex = chunks.findIndex(
|
|
3076
|
+
(item) => item.startMs === chunk.startMs && item.endMs === chunk.endMs
|
|
3077
|
+
);
|
|
3078
|
+
const candlesBySymbol = {};
|
|
3079
|
+
for (const symbol of breadthSymbols) {
|
|
3080
|
+
const candles = await connector.kline({
|
|
3081
|
+
symbol,
|
|
3067
3082
|
interval: connectorInterval,
|
|
3068
3083
|
start: chunk.fetchStartMs,
|
|
3069
3084
|
end: chunk.endMs,
|
|
3070
3085
|
silent: true
|
|
3071
3086
|
});
|
|
3087
|
+
candlesBySymbol[symbol] = candles;
|
|
3088
|
+
candlesRead += candles.length;
|
|
3089
|
+
bar.tick(1, {
|
|
3090
|
+
candles: candlesRead,
|
|
3091
|
+
skip: skippedBreadthChunks,
|
|
3092
|
+
chunk: `${originalChunkIndex + 1}/${chunks.length}`,
|
|
3093
|
+
symbol
|
|
3094
|
+
});
|
|
3095
|
+
}
|
|
3096
|
+
const btcCandles = await connector.kline({
|
|
3097
|
+
symbol: "BTCUSDT",
|
|
3098
|
+
interval: connectorInterval,
|
|
3099
|
+
start: chunk.fetchStartMs,
|
|
3100
|
+
end: chunk.endMs,
|
|
3101
|
+
silent: true
|
|
3102
|
+
});
|
|
3103
|
+
const chunkKey = `${chunk.startMs}:${chunk.endMs}`;
|
|
3104
|
+
for (const plan of plans) {
|
|
3105
|
+
if (!plan.missingChunkKeys.has(chunkKey)) {
|
|
3106
|
+
continue;
|
|
3107
|
+
}
|
|
3108
|
+
const symbolSet = new Set(plan.definition.symbols);
|
|
3109
|
+
const universeCandles = Object.fromEntries(
|
|
3110
|
+
Object.entries(candlesBySymbol).filter(
|
|
3111
|
+
([symbol]) => symbolSet.has(symbol)
|
|
3112
|
+
)
|
|
3113
|
+
);
|
|
3072
3114
|
const rows = buildMarketBreadthRows({
|
|
3073
|
-
universe,
|
|
3115
|
+
universe: plan.definition.universe,
|
|
3074
3116
|
interval,
|
|
3075
|
-
candlesBySymbol,
|
|
3117
|
+
candlesBySymbol: universeCandles,
|
|
3076
3118
|
btcCandles
|
|
3077
3119
|
}).filter((row) => {
|
|
3078
3120
|
const ts = row.ts.getTime();
|
|
@@ -3081,8 +3123,6 @@ var backfillBinanceMarketContext = async (params, enabled) => {
|
|
|
3081
3123
|
await (0, import_timescale.upsertMarketBreadthRows)(rows);
|
|
3082
3124
|
breadthRows += rows.length;
|
|
3083
3125
|
}
|
|
3084
|
-
} else {
|
|
3085
|
-
console.log(import_chalk4.default.gray(`breadth cached: universe=${universe}`));
|
|
3086
3126
|
}
|
|
3087
3127
|
}
|
|
3088
3128
|
console.log(
|
|
@@ -4307,27 +4347,27 @@ var createCoinMarketCapFetchStats = () => ({
|
|
|
4307
4347
|
totalCredits: 0,
|
|
4308
4348
|
byPath: /* @__PURE__ */ new Map()
|
|
4309
4349
|
});
|
|
4310
|
-
var getCoinMarketCapPathLabel = (
|
|
4311
|
-
if (
|
|
4312
|
-
if (
|
|
4313
|
-
if (
|
|
4314
|
-
if (
|
|
4315
|
-
if (
|
|
4316
|
-
if (
|
|
4317
|
-
return
|
|
4318
|
-
};
|
|
4319
|
-
var recordCoinMarketCapFetchStats = (stats,
|
|
4350
|
+
var getCoinMarketCapPathLabel = (path3) => {
|
|
4351
|
+
if (path3.includes("global-metrics")) return "global";
|
|
4352
|
+
if (path3.includes("cryptocurrency/quotes")) return "reference";
|
|
4353
|
+
if (path3.includes("exchange/quotes")) return "exchangeLiquidity";
|
|
4354
|
+
if (path3.includes("fear-and-greed")) return "fearGreed";
|
|
4355
|
+
if (path3.includes("cmc100")) return "cmc100";
|
|
4356
|
+
if (path3.includes("cmc20")) return "cmc20";
|
|
4357
|
+
return path3;
|
|
4358
|
+
};
|
|
4359
|
+
var recordCoinMarketCapFetchStats = (stats, path3, creditCount) => {
|
|
4320
4360
|
if (!stats) return;
|
|
4321
|
-
const existing = stats.byPath.get(
|
|
4361
|
+
const existing = stats.byPath.get(path3) ?? { requests: 0, credits: 0 };
|
|
4322
4362
|
existing.requests += 1;
|
|
4323
4363
|
existing.credits += creditCount ?? 0;
|
|
4324
4364
|
stats.requests += 1;
|
|
4325
4365
|
stats.totalCredits += creditCount ?? 0;
|
|
4326
|
-
stats.byPath.set(
|
|
4366
|
+
stats.byPath.set(path3, existing);
|
|
4327
4367
|
};
|
|
4328
4368
|
var formatCoinMarketCapFetchStats = (stats) => {
|
|
4329
4369
|
const byPath = [...stats.byPath.entries()].map(
|
|
4330
|
-
([
|
|
4370
|
+
([path3, item]) => `${getCoinMarketCapPathLabel(path3)}=${item.requests}req/${item.credits}cr`
|
|
4331
4371
|
).join(" ");
|
|
4332
4372
|
return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
|
|
4333
4373
|
};
|
|
@@ -5243,8 +5283,12 @@ var prepareMarketContextForRun = async (params) => {
|
|
|
5243
5283
|
};
|
|
5244
5284
|
|
|
5245
5285
|
// src/lib/backtest/runState.ts
|
|
5286
|
+
var import_types = require("@tradejs/types");
|
|
5246
5287
|
var successTests = 0;
|
|
5247
5288
|
var errorTests = 0;
|
|
5289
|
+
var warningCounts = {
|
|
5290
|
+
[import_types.BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY]: 0
|
|
5291
|
+
};
|
|
5248
5292
|
var errorMessages = [];
|
|
5249
5293
|
var topResults = [];
|
|
5250
5294
|
var bestTickerResults = /* @__PURE__ */ new Map();
|
|
@@ -5289,6 +5333,9 @@ var getRuntimeCompareContext = () => ({
|
|
|
5289
5333
|
var resetRunState = () => {
|
|
5290
5334
|
successTests = 0;
|
|
5291
5335
|
errorTests = 0;
|
|
5336
|
+
for (const code of Object.values(import_types.BACKTEST_WARNING_CODES)) {
|
|
5337
|
+
warningCounts[code] = 0;
|
|
5338
|
+
}
|
|
5292
5339
|
errorMessages.length = 0;
|
|
5293
5340
|
topResults = [];
|
|
5294
5341
|
bestTickerResults.clear();
|
|
@@ -5433,13 +5480,16 @@ var REPLAY_RUNTIME_COMPARISON_HEADERS = [
|
|
|
5433
5480
|
import_chalk8.default.yellow("RT TRADES"),
|
|
5434
5481
|
import_chalk8.default.yellow("RT PNL"),
|
|
5435
5482
|
import_chalk8.default.green("MATCHED"),
|
|
5483
|
+
import_chalk8.default.red("ORDER FAILED"),
|
|
5436
5484
|
import_chalk8.default.yellow("RT ONLY"),
|
|
5437
|
-
import_chalk8.default.magenta("BT ONLY")
|
|
5485
|
+
import_chalk8.default.magenta("BT ONLY"),
|
|
5486
|
+
import_chalk8.default.red("UNMATCHED")
|
|
5438
5487
|
];
|
|
5439
5488
|
var REPLAY_RESULTS_CONFIG = "replay";
|
|
5440
5489
|
var REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS = 1;
|
|
5441
5490
|
var REPLAY_RUNTIME_COMPARE_TOLERANCE_MS = REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS * 15 * 60 * 1e3;
|
|
5442
5491
|
var formatReplayRuntimeCompareTolerance = () => `${REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS} ${REPLAY_RUNTIME_COMPARE_TOLERANCE_BARS === 1 ? "bar" : "bars"}`;
|
|
5492
|
+
var getReplayRuntimeUnmatchedCount = (row) => row.orderFailed + row.runtimeOnly + row.backtestOnly;
|
|
5443
5493
|
var buildReplayStrategyConfig = ({
|
|
5444
5494
|
strategyConfig,
|
|
5445
5495
|
interval
|
|
@@ -5504,12 +5554,13 @@ var compactReplaySignal = (signal) => {
|
|
|
5504
5554
|
var import_chalk9 = __toESM(require("chalk"));
|
|
5505
5555
|
var import_progress3 = __toESM(require("progress"));
|
|
5506
5556
|
var import_backtest = require("@tradejs/core/backtest");
|
|
5507
|
-
var
|
|
5557
|
+
var import_strategies2 = require("@tradejs/core/strategies");
|
|
5508
5558
|
var import_time3 = require("@tradejs/core/time");
|
|
5509
5559
|
var import_logger = require("@tradejs/infra/logger");
|
|
5560
|
+
var import_redis3 = require("@tradejs/infra/redis");
|
|
5510
5561
|
var import_connectors6 = require("@tradejs/node/connectors");
|
|
5511
5562
|
var import_cli4 = require("@tradejs/node/cli");
|
|
5512
|
-
var
|
|
5563
|
+
var import_strategies3 = require("@tradejs/node/strategies");
|
|
5513
5564
|
|
|
5514
5565
|
// src/lib/replay/portfolioReplayConnector.ts
|
|
5515
5566
|
var import_constants3 = require("@tradejs/core/constants");
|
|
@@ -5959,21 +6010,231 @@ var invokeAfterSignalsHooks = async (hooks, params) => {
|
|
|
5959
6010
|
}
|
|
5960
6011
|
};
|
|
5961
6012
|
|
|
6013
|
+
// src/lib/runtimeLineage.ts
|
|
6014
|
+
var import_node_child_process = require("child_process");
|
|
6015
|
+
var import_node_crypto = require("crypto");
|
|
6016
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
6017
|
+
var import_node_path = __toESM(require("path"));
|
|
6018
|
+
var RUNTIME_CONTEXT_ENV_KEYS = [
|
|
6019
|
+
"AI_MODE",
|
|
6020
|
+
"MIN_AI_QUALITY",
|
|
6021
|
+
"INTERVAL",
|
|
6022
|
+
"DERIVATIVES_CONTEXT_ENABLED",
|
|
6023
|
+
"DERIVATIVES_CONTEXT_TARGET_ENABLED",
|
|
6024
|
+
"DERIVATIVES_CONTEXT_LOOKBACK_HOURS",
|
|
6025
|
+
"DERIVATIVES_CONTEXT_EXTRA_REFERENCE_SYMBOLS",
|
|
6026
|
+
"DERIVATIVES_CONTEXT_EXCHANGE_PRIORITY",
|
|
6027
|
+
"COINMARKETCAP_CONTEXT_ENABLED",
|
|
6028
|
+
"COINMARKETCAP_CONTEXT_MAX_AGE_MS",
|
|
6029
|
+
"COINMARKETCAP_CONTEXT_EXCHANGE_LIQUIDITY_ENABLED",
|
|
6030
|
+
"COINMARKETCAP_CONTEXT_EXCHANGE_SLUGS",
|
|
6031
|
+
"COINMARKETCAP_CONTEXT_FEAR_GREED_ENABLED",
|
|
6032
|
+
"COINMARKETCAP_CONTEXT_HISTORICAL_ACCESS_MONTHS",
|
|
6033
|
+
"COINMARKETCAP_CONTEXT_BACKFILL_ENABLED",
|
|
6034
|
+
"COINMARKETCAP_CONTEXT_BACKFILL_MAX_DAYS",
|
|
6035
|
+
"COINMARKETCAP_CONTEXT_BACKFILL_WARMUP_DAYS"
|
|
6036
|
+
];
|
|
6037
|
+
var RUNTIME_CONTEXT_DATA_MODEL = {
|
|
6038
|
+
derivativesSourceIntervals: "15m",
|
|
6039
|
+
derivativesDerivedIntervals: "1h",
|
|
6040
|
+
derivativesHourlyFallback: "stored-1h",
|
|
6041
|
+
derivativesDataModelVersion: 2,
|
|
6042
|
+
coinMarketCapAvailabilityModel: "closed-interval-all-daily",
|
|
6043
|
+
coinMarketCapDataModelVersion: 3,
|
|
6044
|
+
binanceBreadthUniverseModel: "fixed-versioned-top5-top10-top30-top50-top100",
|
|
6045
|
+
binanceBreadthDataModelVersion: 2
|
|
6046
|
+
};
|
|
6047
|
+
var normalizeForStableJson = (value) => {
|
|
6048
|
+
if (Array.isArray(value)) {
|
|
6049
|
+
return value.map(normalizeForStableJson);
|
|
6050
|
+
}
|
|
6051
|
+
if (value && typeof value === "object") {
|
|
6052
|
+
return Object.fromEntries(
|
|
6053
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeForStableJson(entry)])
|
|
6054
|
+
);
|
|
6055
|
+
}
|
|
6056
|
+
return value;
|
|
6057
|
+
};
|
|
6058
|
+
var fingerprintRuntimeValue = (value) => (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
|
|
6059
|
+
var gitLineageCache = /* @__PURE__ */ new Map();
|
|
6060
|
+
var gateFingerprintCache = /* @__PURE__ */ new Map();
|
|
6061
|
+
var binanceBreadthFingerprintCache = /* @__PURE__ */ new Map();
|
|
6062
|
+
var resolveGitLineage = (projectRoot) => {
|
|
6063
|
+
const cached = gitLineageCache.get(projectRoot);
|
|
6064
|
+
if (cached) {
|
|
6065
|
+
return cached;
|
|
6066
|
+
}
|
|
6067
|
+
const envSha = String(process.env.TRADEJS_GIT_SHA ?? "").trim();
|
|
6068
|
+
if (envSha && envSha !== "unknown") {
|
|
6069
|
+
const result = {
|
|
6070
|
+
gitSha: envSha,
|
|
6071
|
+
gitDirty: false
|
|
6072
|
+
};
|
|
6073
|
+
gitLineageCache.set(projectRoot, result);
|
|
6074
|
+
return result;
|
|
6075
|
+
}
|
|
6076
|
+
try {
|
|
6077
|
+
const gitSha = (0, import_node_child_process.execFileSync)("git", ["rev-parse", "HEAD"], {
|
|
6078
|
+
cwd: projectRoot,
|
|
6079
|
+
encoding: "utf8",
|
|
6080
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
6081
|
+
}).trim();
|
|
6082
|
+
const status = (0, import_node_child_process.execFileSync)(
|
|
6083
|
+
"git",
|
|
6084
|
+
["status", "--porcelain", "--untracked-files=normal"],
|
|
6085
|
+
{
|
|
6086
|
+
cwd: projectRoot,
|
|
6087
|
+
encoding: "utf8",
|
|
6088
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
6089
|
+
}
|
|
6090
|
+
).trim();
|
|
6091
|
+
const result = {
|
|
6092
|
+
gitSha: gitSha || null,
|
|
6093
|
+
gitDirty: status.length > 0
|
|
6094
|
+
};
|
|
6095
|
+
gitLineageCache.set(projectRoot, result);
|
|
6096
|
+
return result;
|
|
6097
|
+
} catch {
|
|
6098
|
+
const result = {
|
|
6099
|
+
gitSha: null,
|
|
6100
|
+
gitDirty: null
|
|
6101
|
+
};
|
|
6102
|
+
gitLineageCache.set(projectRoot, result);
|
|
6103
|
+
return result;
|
|
6104
|
+
}
|
|
6105
|
+
};
|
|
6106
|
+
var readOptionalFile = async (filePath) => {
|
|
6107
|
+
try {
|
|
6108
|
+
return await import_promises.default.readFile(filePath);
|
|
6109
|
+
} catch {
|
|
6110
|
+
return null;
|
|
6111
|
+
}
|
|
6112
|
+
};
|
|
6113
|
+
var resolveBinanceBreadthFingerprint = async (projectRoot) => {
|
|
6114
|
+
if (binanceBreadthFingerprintCache.has(projectRoot)) {
|
|
6115
|
+
return binanceBreadthFingerprintCache.get(projectRoot) ?? null;
|
|
6116
|
+
}
|
|
6117
|
+
const content = await readOptionalFile(
|
|
6118
|
+
import_node_path.default.join(
|
|
6119
|
+
projectRoot,
|
|
6120
|
+
"packages/node/src/config/binanceBreadthUniverses.json"
|
|
6121
|
+
)
|
|
6122
|
+
);
|
|
6123
|
+
let result = null;
|
|
6124
|
+
if (content) {
|
|
6125
|
+
try {
|
|
6126
|
+
const parsed = JSON.parse(content.toString("utf8"));
|
|
6127
|
+
result = typeof parsed.fingerprint === "string" && parsed.fingerprint.trim() ? parsed.fingerprint.trim() : null;
|
|
6128
|
+
} catch {
|
|
6129
|
+
result = null;
|
|
6130
|
+
}
|
|
6131
|
+
}
|
|
6132
|
+
binanceBreadthFingerprintCache.set(projectRoot, result);
|
|
6133
|
+
return result;
|
|
6134
|
+
};
|
|
6135
|
+
var resolveGateFingerprint = async ({
|
|
6136
|
+
projectRoot,
|
|
6137
|
+
strategyName,
|
|
6138
|
+
gitSha
|
|
6139
|
+
}) => {
|
|
6140
|
+
const cacheKey = `${projectRoot}:${strategyName}:${gitSha ?? "unknown"}`;
|
|
6141
|
+
const cached = gateFingerprintCache.get(cacheKey);
|
|
6142
|
+
if (cached) {
|
|
6143
|
+
return cached;
|
|
6144
|
+
}
|
|
6145
|
+
const relativeCandidates = [
|
|
6146
|
+
`packages/strategies/src/${strategyName}/adapters/ai.ts`,
|
|
6147
|
+
`packages/strategies/src/${strategyName}/guardrails.ts`,
|
|
6148
|
+
`packages/strategies/src/${strategyName}/pockets.ts`,
|
|
6149
|
+
`packages/strategies/src/${strategyName}/config.ts`,
|
|
6150
|
+
"packages/node/src/ai.ts"
|
|
6151
|
+
];
|
|
6152
|
+
const optionalEntries = await Promise.all(
|
|
6153
|
+
relativeCandidates.map(async (relativePath) => {
|
|
6154
|
+
const content = await readOptionalFile(
|
|
6155
|
+
import_node_path.default.join(projectRoot, relativePath)
|
|
6156
|
+
);
|
|
6157
|
+
return content == null ? null : { relativePath, content };
|
|
6158
|
+
})
|
|
6159
|
+
);
|
|
6160
|
+
const sourceEntries = [];
|
|
6161
|
+
for (const entry of optionalEntries) {
|
|
6162
|
+
if (entry != null) {
|
|
6163
|
+
sourceEntries.push(entry);
|
|
6164
|
+
}
|
|
6165
|
+
}
|
|
6166
|
+
const hash = (0, import_node_crypto.createHash)("sha256");
|
|
6167
|
+
hash.update(strategyName);
|
|
6168
|
+
if (sourceEntries.length) {
|
|
6169
|
+
for (const entry of sourceEntries) {
|
|
6170
|
+
hash.update(entry.relativePath);
|
|
6171
|
+
hash.update(entry.content);
|
|
6172
|
+
}
|
|
6173
|
+
} else {
|
|
6174
|
+
hash.update(gitSha ?? "unknown-git-sha");
|
|
6175
|
+
}
|
|
6176
|
+
const fingerprint = hash.digest("hex").slice(0, 16);
|
|
6177
|
+
gateFingerprintCache.set(cacheKey, fingerprint);
|
|
6178
|
+
return fingerprint;
|
|
6179
|
+
};
|
|
6180
|
+
var buildRuntimeLineage = async ({
|
|
6181
|
+
projectRoot,
|
|
6182
|
+
strategyName,
|
|
6183
|
+
config,
|
|
6184
|
+
runContext,
|
|
6185
|
+
env = process.env
|
|
6186
|
+
}) => {
|
|
6187
|
+
const git = resolveGitLineage(projectRoot);
|
|
6188
|
+
const context = {
|
|
6189
|
+
...Object.fromEntries(
|
|
6190
|
+
RUNTIME_CONTEXT_ENV_KEYS.map((key) => [key, env[key] ?? null])
|
|
6191
|
+
),
|
|
6192
|
+
...RUNTIME_CONTEXT_DATA_MODEL,
|
|
6193
|
+
binanceBreadthUniverseFingerprint: await resolveBinanceBreadthFingerprint(projectRoot),
|
|
6194
|
+
...runContext
|
|
6195
|
+
};
|
|
6196
|
+
return {
|
|
6197
|
+
schemaVersion: 1,
|
|
6198
|
+
...git,
|
|
6199
|
+
gateFingerprint: await resolveGateFingerprint({
|
|
6200
|
+
projectRoot,
|
|
6201
|
+
strategyName,
|
|
6202
|
+
gitSha: git.gitSha
|
|
6203
|
+
}),
|
|
6204
|
+
configFingerprint: fingerprintRuntimeValue(config),
|
|
6205
|
+
contextFingerprint: fingerprintRuntimeValue(context)
|
|
6206
|
+
};
|
|
6207
|
+
};
|
|
6208
|
+
var runtimeLineageKey = (lineage) => [
|
|
6209
|
+
lineage.schemaVersion,
|
|
6210
|
+
lineage.gitSha ?? "unknown",
|
|
6211
|
+
lineage.gitDirty == null ? "unknown" : lineage.gitDirty ? "dirty" : "clean",
|
|
6212
|
+
lineage.gateFingerprint,
|
|
6213
|
+
lineage.configFingerprint,
|
|
6214
|
+
lineage.contextFingerprint
|
|
6215
|
+
].join(":");
|
|
6216
|
+
var runtimeLineagesMatch = (left, right) => left != null && right != null && runtimeLineageKey(left) === runtimeLineageKey(right);
|
|
6217
|
+
|
|
5962
6218
|
// src/lib/replay/historicalSignalsReplay.ts
|
|
5963
6219
|
var loadRuntimeStrategies = async (runtimeStrategies) => {
|
|
5964
6220
|
const strategies = await Promise.all(
|
|
5965
6221
|
runtimeStrategies.map(async ({ strategyName, strategyConfig }) => {
|
|
5966
|
-
const strategyCreator = await (0,
|
|
6222
|
+
const strategyCreator = await (0, import_strategies3.getStrategyCreator)(
|
|
5967
6223
|
strategyName,
|
|
5968
6224
|
replayProjectRoot
|
|
5969
6225
|
);
|
|
5970
6226
|
if (!strategyCreator) {
|
|
5971
6227
|
throw new Error(`Unknown strategy: ${strategyName}`);
|
|
5972
6228
|
}
|
|
6229
|
+
const strategyResults = await (0, import_redis3.getData)(
|
|
6230
|
+
import_redis3.redisKeys.strategyResults(replayUserName, strategyName),
|
|
6231
|
+
{}
|
|
6232
|
+
);
|
|
5973
6233
|
return {
|
|
5974
6234
|
strategyName,
|
|
5975
6235
|
strategyCreator,
|
|
5976
|
-
strategyConfig
|
|
6236
|
+
strategyConfig,
|
|
6237
|
+
strategyResults
|
|
5977
6238
|
};
|
|
5978
6239
|
})
|
|
5979
6240
|
);
|
|
@@ -6074,6 +6335,7 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6074
6335
|
}) => {
|
|
6075
6336
|
const startedAt = Date.now();
|
|
6076
6337
|
const signals = [];
|
|
6338
|
+
const runtimeLineages = [];
|
|
6077
6339
|
const projectConfig = await (0, import_cli4.loadTradejsConfig)(replayProjectRoot);
|
|
6078
6340
|
const projectHooks = projectConfig.hooks;
|
|
6079
6341
|
const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
|
|
@@ -6161,26 +6423,54 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6161
6423
|
sharedReplayKeyPrefixes.push(sharedIndicatorsReplayKey);
|
|
6162
6424
|
const strategiesForSymbol = await Promise.all(
|
|
6163
6425
|
loadedStrategies.map(
|
|
6164
|
-
async ({
|
|
6426
|
+
async ({
|
|
6165
6427
|
strategyName,
|
|
6428
|
+
strategyCreator,
|
|
6166
6429
|
strategyConfig,
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6430
|
+
strategyResults
|
|
6431
|
+
}) => {
|
|
6432
|
+
const runtimeLineage = await buildRuntimeLineage({
|
|
6433
|
+
projectRoot: replayProjectRoot,
|
|
6434
|
+
strategyName,
|
|
6435
|
+
config: {
|
|
6436
|
+
configId: "config",
|
|
6171
6437
|
strategyConfig,
|
|
6172
|
-
|
|
6173
|
-
}
|
|
6438
|
+
symbolResultConfig: strategyResults?.[symbol]?.config ?? null
|
|
6439
|
+
},
|
|
6440
|
+
runContext: {
|
|
6441
|
+
connectorName: connectorName.toLowerCase(),
|
|
6442
|
+
interval: String(interval),
|
|
6443
|
+
universe: preparedRun.universe ?? null
|
|
6444
|
+
}
|
|
6445
|
+
});
|
|
6446
|
+
runtimeLineages.push({
|
|
6447
|
+
strategy: strategyName,
|
|
6174
6448
|
symbol,
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6449
|
+
lineage: runtimeLineage
|
|
6450
|
+
});
|
|
6451
|
+
return {
|
|
6452
|
+
strategyName,
|
|
6453
|
+
strategyConfig,
|
|
6454
|
+
runtimeLineage,
|
|
6455
|
+
run: await strategyCreator({
|
|
6456
|
+
userName: replayUserName,
|
|
6457
|
+
connectorName,
|
|
6458
|
+
runtimeConfigId: "config",
|
|
6459
|
+
config: buildReplayStrategyConfig({
|
|
6460
|
+
strategyConfig,
|
|
6461
|
+
interval
|
|
6462
|
+
}),
|
|
6463
|
+
symbol,
|
|
6464
|
+
data: preparedData.prevData,
|
|
6465
|
+
btcData: preparedData.btcPrevData,
|
|
6466
|
+
ethData: preparedData.ethPrevData,
|
|
6467
|
+
btcBinanceData: preparedData.btcBinancePrevData,
|
|
6468
|
+
btcCoinbaseData: preparedData.btcCoinbasePrevData,
|
|
6469
|
+
connector: replayConnector,
|
|
6470
|
+
sharedIndicatorsReplayKey
|
|
6471
|
+
})
|
|
6472
|
+
};
|
|
6473
|
+
}
|
|
6184
6474
|
)
|
|
6185
6475
|
);
|
|
6186
6476
|
const symbolRuntime = {
|
|
@@ -6274,7 +6564,8 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6274
6564
|
ethCandle?.timestamp === timestamp ? ethCandle : void 0
|
|
6275
6565
|
);
|
|
6276
6566
|
if (result && typeof result !== "string") {
|
|
6277
|
-
|
|
6567
|
+
result.runtimeLineage = strategyRuntime.runtimeLineage;
|
|
6568
|
+
await (0, import_strategies3.enrichSignalWithBinanceMarketContext)({
|
|
6278
6569
|
signal: result,
|
|
6279
6570
|
env: "PARITY"
|
|
6280
6571
|
});
|
|
@@ -6298,8 +6589,8 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6298
6589
|
}
|
|
6299
6590
|
} finally {
|
|
6300
6591
|
for (const keyPrefix of sharedReplayKeyPrefixes) {
|
|
6301
|
-
(0,
|
|
6302
|
-
(0,
|
|
6592
|
+
(0, import_strategies2.releaseStrategyIndicatorsReplayCache)(keyPrefix);
|
|
6593
|
+
(0, import_strategies2.releaseStrategyReplayCache)(keyPrefix);
|
|
6303
6594
|
}
|
|
6304
6595
|
}
|
|
6305
6596
|
const artifacts = replayConnector.getReplayArtifacts();
|
|
@@ -6333,7 +6624,8 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6333
6624
|
orderLog: artifacts.orderLog,
|
|
6334
6625
|
positionLog: artifacts.positionLog,
|
|
6335
6626
|
cycleCount: orderedTimestamps.length,
|
|
6336
|
-
abortedCycles
|
|
6627
|
+
abortedCycles,
|
|
6628
|
+
runtimeLineages
|
|
6337
6629
|
};
|
|
6338
6630
|
};
|
|
6339
6631
|
|
|
@@ -6817,7 +7109,7 @@ var summarizeTradeParityByStrategy = ({
|
|
|
6817
7109
|
};
|
|
6818
7110
|
|
|
6819
7111
|
// src/lib/runtimeSignalsLoader.ts
|
|
6820
|
-
var
|
|
7112
|
+
var import_redis4 = require("@tradejs/infra/redis");
|
|
6821
7113
|
|
|
6822
7114
|
// src/lib/runtimeSignalsStorage.ts
|
|
6823
7115
|
var import_constants4 = require("@tradejs/core/constants");
|
|
@@ -6867,14 +7159,14 @@ var loadRuntimeSignals = async (userName, {
|
|
|
6867
7159
|
startTime,
|
|
6868
7160
|
endTime
|
|
6869
7161
|
} = {}) => {
|
|
6870
|
-
const bucketPrefix =
|
|
7162
|
+
const bucketPrefix = import_redis4.redisKeys.runtimeSignalBuckets(userName);
|
|
6871
7163
|
const bucketKeys = filterBucketKeysByWindow(
|
|
6872
|
-
await (0,
|
|
7164
|
+
await (0, import_redis4.getKeys)(bucketPrefix),
|
|
6873
7165
|
bucketPrefix,
|
|
6874
7166
|
startTime,
|
|
6875
7167
|
endTime
|
|
6876
7168
|
);
|
|
6877
|
-
const refs = (await Promise.all(bucketKeys.map((key) => (0,
|
|
7169
|
+
const refs = (await Promise.all(bucketKeys.map((key) => (0, import_redis4.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalBucketRef);
|
|
6878
7170
|
const seen = /* @__PURE__ */ new Set();
|
|
6879
7171
|
const payloads = await Promise.all(
|
|
6880
7172
|
refs.map(async (ref) => {
|
|
@@ -6883,8 +7175,8 @@ var loadRuntimeSignals = async (userName, {
|
|
|
6883
7175
|
return null;
|
|
6884
7176
|
}
|
|
6885
7177
|
seen.add(dedupeKey);
|
|
6886
|
-
const signal = await (0,
|
|
6887
|
-
|
|
7178
|
+
const signal = await (0, import_redis4.getData)(
|
|
7179
|
+
import_redis4.redisKeys.storeSignal(ref.symbol, ref.signalId),
|
|
6888
7180
|
null
|
|
6889
7181
|
);
|
|
6890
7182
|
return isSignalRecord(signal) ? signal : null;
|
|
@@ -6896,25 +7188,42 @@ var loadRuntimeSignalEvaluations = async (userName, {
|
|
|
6896
7188
|
startTime,
|
|
6897
7189
|
endTime
|
|
6898
7190
|
} = {}) => {
|
|
6899
|
-
const bucketPrefix =
|
|
7191
|
+
const bucketPrefix = import_redis4.redisKeys.runtimeSignalEvaluationBuckets(userName);
|
|
6900
7192
|
const bucketKeys = filterBucketKeysByWindow(
|
|
6901
|
-
await (0,
|
|
7193
|
+
await (0, import_redis4.getKeys)(bucketPrefix),
|
|
6902
7194
|
bucketPrefix,
|
|
6903
7195
|
startTime,
|
|
6904
7196
|
endTime
|
|
6905
7197
|
);
|
|
6906
|
-
const evaluations = (await Promise.all(bucketKeys.map((key) => (0,
|
|
7198
|
+
const evaluations = (await Promise.all(bucketKeys.map((key) => (0, import_redis4.getHashJsonValues)(key)))).flat().filter(isRuntimeSignalEvaluationRecord);
|
|
6907
7199
|
const deduped = /* @__PURE__ */ new Map();
|
|
6908
7200
|
for (const evaluation of evaluations) {
|
|
6909
7201
|
deduped.set(evaluation.evaluationId, evaluation);
|
|
6910
7202
|
}
|
|
6911
7203
|
return sortByTimestamp([...deduped.values()]);
|
|
6912
7204
|
};
|
|
7205
|
+
var loadRuntimeLineageScopes = async (userName, {
|
|
7206
|
+
startTime,
|
|
7207
|
+
endTime
|
|
7208
|
+
}) => {
|
|
7209
|
+
const records = (await Promise.all(
|
|
7210
|
+
(0, import_time4.getRuntimeStorageDayKeys)(startTime, endTime).map(
|
|
7211
|
+
(dayKey) => (0, import_redis4.getHashJsonValues)(
|
|
7212
|
+
import_redis4.redisKeys.runtimeLineageScopeBucket(userName, dayKey)
|
|
7213
|
+
)
|
|
7214
|
+
)
|
|
7215
|
+
)).flat().filter(
|
|
7216
|
+
(record) => record != null && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.firstTimestamp === "number" && typeof record.lastTimestamp === "number" && record.lineage != null
|
|
7217
|
+
);
|
|
7218
|
+
return records.sort(
|
|
7219
|
+
(left, right) => left.firstTimestamp - right.firstTimestamp || left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol)
|
|
7220
|
+
);
|
|
7221
|
+
};
|
|
6913
7222
|
|
|
6914
7223
|
// src/lib/runtimeTradeSync.ts
|
|
6915
7224
|
var import_constants5 = require("@tradejs/core/constants");
|
|
6916
7225
|
var import_time5 = require("@tradejs/core/time");
|
|
6917
|
-
var
|
|
7226
|
+
var import_redis5 = require("@tradejs/infra/redis");
|
|
6918
7227
|
var CLOSED_PNL_LIMIT = 100;
|
|
6919
7228
|
var CLOSED_PNL_RECONCILIATION_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
|
|
6920
7229
|
var EXCHANGE_HISTORY_MAX_RANGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
@@ -7008,6 +7317,18 @@ var consumeClosedPnlMatch = (buckets, trade) => {
|
|
|
7008
7317
|
const [row] = rows.splice(matchIndex, 1);
|
|
7009
7318
|
return row ?? null;
|
|
7010
7319
|
};
|
|
7320
|
+
var isRuntimeTradeInConnectorScope = (trade, connector) => {
|
|
7321
|
+
if (trade.deploymentId && trade.deploymentId !== connector.deploymentId) {
|
|
7322
|
+
return false;
|
|
7323
|
+
}
|
|
7324
|
+
if (trade.accountId && trade.accountId !== connector.accountId) {
|
|
7325
|
+
return false;
|
|
7326
|
+
}
|
|
7327
|
+
if (trade.universe && connector.universe && trade.universe !== connector.universe) {
|
|
7328
|
+
return false;
|
|
7329
|
+
}
|
|
7330
|
+
return true;
|
|
7331
|
+
};
|
|
7011
7332
|
var syncRuntimeTrades = async ({
|
|
7012
7333
|
userName,
|
|
7013
7334
|
connector,
|
|
@@ -7031,16 +7352,23 @@ var syncRuntimeTrades = async ({
|
|
|
7031
7352
|
const openPositionsBySymbol = new Map(
|
|
7032
7353
|
openPositions.map((position) => [position.symbol, position])
|
|
7033
7354
|
);
|
|
7034
|
-
const
|
|
7035
|
-
const
|
|
7355
|
+
const activeOrderIdByKey = /* @__PURE__ */ new Map();
|
|
7356
|
+
const activeTradeKeys = [
|
|
7357
|
+
...new Set(
|
|
7358
|
+
trades.filter((trade) => isRuntimeTradeInConnectorScope(trade, connector)).map(
|
|
7359
|
+
(trade) => import_redis5.redisKeys.runtimeActiveTrade(
|
|
7360
|
+
userName,
|
|
7361
|
+
trade.symbol,
|
|
7362
|
+
trade.deploymentId ?? trade.accountId
|
|
7363
|
+
)
|
|
7364
|
+
)
|
|
7365
|
+
)
|
|
7366
|
+
];
|
|
7036
7367
|
await Promise.all(
|
|
7037
|
-
|
|
7038
|
-
const activeRef = await (0,
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
);
|
|
7042
|
-
activeOrderIdBySymbol.set(
|
|
7043
|
-
symbol,
|
|
7368
|
+
activeTradeKeys.map(async (key) => {
|
|
7369
|
+
const activeRef = await (0, import_redis5.getData)(key, null);
|
|
7370
|
+
activeOrderIdByKey.set(
|
|
7371
|
+
key,
|
|
7044
7372
|
typeof activeRef?.orderId === "string" ? activeRef.orderId : null
|
|
7045
7373
|
);
|
|
7046
7374
|
})
|
|
@@ -7059,12 +7387,21 @@ var syncRuntimeTrades = async ({
|
|
|
7059
7387
|
}
|
|
7060
7388
|
const syncedTrades = [];
|
|
7061
7389
|
for (const trade of trades) {
|
|
7390
|
+
if (!isRuntimeTradeInConnectorScope(trade, connector)) {
|
|
7391
|
+
syncedTrades.push(trade);
|
|
7392
|
+
continue;
|
|
7393
|
+
}
|
|
7062
7394
|
if (trade.status !== "active" && !isRuntimeTradeSyncFallbackClose(trade)) {
|
|
7063
7395
|
syncedTrades.push(trade);
|
|
7064
7396
|
continue;
|
|
7065
7397
|
}
|
|
7398
|
+
const activeTradeKey = import_redis5.redisKeys.runtimeActiveTrade(
|
|
7399
|
+
userName,
|
|
7400
|
+
trade.symbol,
|
|
7401
|
+
trade.deploymentId ?? trade.accountId
|
|
7402
|
+
);
|
|
7066
7403
|
const openPosition = openPositionsBySymbol.get(trade.symbol);
|
|
7067
|
-
const activeOrderId =
|
|
7404
|
+
const activeOrderId = activeOrderIdByKey.get(activeTradeKey);
|
|
7068
7405
|
const isCurrentActiveTrade = activeOrderId === trade.orderId;
|
|
7069
7406
|
if (trade.status === "active" && !openPositionsReliable) {
|
|
7070
7407
|
syncedTrades.push({
|
|
@@ -7082,15 +7419,15 @@ var syncRuntimeTrades = async ({
|
|
|
7082
7419
|
currentPnl: openPosition.unrealizedPnl,
|
|
7083
7420
|
lastSyncedAt: endTime
|
|
7084
7421
|
};
|
|
7085
|
-
await (0,
|
|
7086
|
-
|
|
7422
|
+
await (0, import_redis5.setData)(
|
|
7423
|
+
import_redis5.redisKeys.runtimeTrade(userName, trade.orderId),
|
|
7087
7424
|
nextTrade2,
|
|
7088
7425
|
{
|
|
7089
7426
|
expire: 0
|
|
7090
7427
|
}
|
|
7091
7428
|
);
|
|
7092
|
-
await (0,
|
|
7093
|
-
|
|
7429
|
+
await (0, import_redis5.setHashJsonField)(
|
|
7430
|
+
import_redis5.redisKeys.runtimeTradeBucket(
|
|
7094
7431
|
userName,
|
|
7095
7432
|
(0, import_time5.getRuntimeStorageDayKey)(trade.entryTimestamp)
|
|
7096
7433
|
),
|
|
@@ -7103,6 +7440,7 @@ var syncRuntimeTrades = async ({
|
|
|
7103
7440
|
}
|
|
7104
7441
|
const matchedClosedPnl = consumeClosedPnlMatch(closedPnlBuckets, trade);
|
|
7105
7442
|
if (!matchedClosedPnl) {
|
|
7443
|
+
syncedTrades.push(trade);
|
|
7106
7444
|
continue;
|
|
7107
7445
|
}
|
|
7108
7446
|
const nextTrade = {
|
|
@@ -7123,11 +7461,11 @@ var syncRuntimeTrades = async ({
|
|
|
7123
7461
|
lastSyncedAt: endTime
|
|
7124
7462
|
};
|
|
7125
7463
|
await Promise.all([
|
|
7126
|
-
(0,
|
|
7464
|
+
(0, import_redis5.setData)(import_redis5.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
|
|
7127
7465
|
expire: import_constants5.TTL_1M
|
|
7128
7466
|
}),
|
|
7129
|
-
(0,
|
|
7130
|
-
|
|
7467
|
+
(0, import_redis5.setHashJsonField)(
|
|
7468
|
+
import_redis5.redisKeys.runtimeTradeBucket(
|
|
7131
7469
|
userName,
|
|
7132
7470
|
(0, import_time5.getRuntimeStorageDayKey)(trade.entryTimestamp)
|
|
7133
7471
|
),
|
|
@@ -7135,7 +7473,7 @@ var syncRuntimeTrades = async ({
|
|
|
7135
7473
|
nextTrade,
|
|
7136
7474
|
{ expire: import_constants5.TTL_1M }
|
|
7137
7475
|
),
|
|
7138
|
-
...isCurrentActiveTrade ? [(0,
|
|
7476
|
+
...isCurrentActiveTrade ? [(0, import_redis5.delKey)(activeTradeKey)] : []
|
|
7139
7477
|
]);
|
|
7140
7478
|
syncedTrades.push(nextTrade);
|
|
7141
7479
|
}
|
|
@@ -7745,6 +8083,7 @@ var buildReplayRuntimeComparisonDetails = ({
|
|
|
7745
8083
|
),
|
|
7746
8084
|
limit
|
|
7747
8085
|
),
|
|
8086
|
+
orderFailed: [],
|
|
7748
8087
|
runtimeOnly: cappedRuntimeOnlyDetails,
|
|
7749
8088
|
backtestOnly: cappedBacktestOnlyDetails,
|
|
7750
8089
|
nearestCandidates: cappedNearestCandidates,
|
|
@@ -7764,6 +8103,7 @@ var buildReplayRuntimeComparisonDetails = ({
|
|
|
7764
8103
|
};
|
|
7765
8104
|
var buildReplayExchangeComparisonDetails = ({
|
|
7766
8105
|
matched,
|
|
8106
|
+
orderFailed = [],
|
|
7767
8107
|
exchangeOnly,
|
|
7768
8108
|
backtestOnly,
|
|
7769
8109
|
exchangeEntries,
|
|
@@ -7814,7 +8154,7 @@ var buildReplayExchangeComparisonDetails = ({
|
|
|
7814
8154
|
limit
|
|
7815
8155
|
);
|
|
7816
8156
|
return {
|
|
7817
|
-
capped: matched.length > limit || exchangeOnlyDetails.length > limit || backtestOnlyDetails.length > limit || nearestCandidates.length > limit,
|
|
8157
|
+
capped: matched.length > limit || orderFailed.length > limit || exchangeOnlyDetails.length > limit || backtestOnlyDetails.length > limit || nearestCandidates.length > limit,
|
|
7818
8158
|
limit,
|
|
7819
8159
|
matched: capReplayDetails(
|
|
7820
8160
|
matched.map(
|
|
@@ -7830,6 +8170,27 @@ var buildReplayExchangeComparisonDetails = ({
|
|
|
7830
8170
|
),
|
|
7831
8171
|
limit
|
|
7832
8172
|
),
|
|
8173
|
+
orderFailed: capReplayDetails(
|
|
8174
|
+
orderFailed.map((item) => {
|
|
8175
|
+
const detail = buildMatchedReplayDetail({
|
|
8176
|
+
runtime: toExchangeDetail(item.exchange),
|
|
8177
|
+
backtest: toBacktestParityDetail(
|
|
8178
|
+
item.backtest,
|
|
8179
|
+
backtestTimestampOffsetMs
|
|
8180
|
+
),
|
|
8181
|
+
timestampDiffMs: item.timestampDiffMs,
|
|
8182
|
+
priceDeltaPct: item.priceDeltaPct
|
|
8183
|
+
});
|
|
8184
|
+
return {
|
|
8185
|
+
runtime: detail.runtime,
|
|
8186
|
+
backtest: detail.backtest,
|
|
8187
|
+
timestampDiffMs: detail.timestampDiffMs,
|
|
8188
|
+
priceDeltaPct: detail.priceDeltaPct,
|
|
8189
|
+
reason: item.reason
|
|
8190
|
+
};
|
|
8191
|
+
}),
|
|
8192
|
+
limit
|
|
8193
|
+
),
|
|
7833
8194
|
runtimeOnly: cappedExchangeOnlyDetails,
|
|
7834
8195
|
backtestOnly: cappedBacktestOnlyDetails,
|
|
7835
8196
|
nearestCandidates: cappedNearestCandidates,
|
|
@@ -7865,11 +8226,147 @@ var buildReplaySignalEvaluations = (signals) => signals.map((signal) => ({
|
|
|
7865
8226
|
reason: signal.orderSkipReason || signal.orderStatus || "SIGNAL",
|
|
7866
8227
|
signalId: signal.signalId,
|
|
7867
8228
|
direction: signal.direction,
|
|
8229
|
+
runtimeLineage: signal.runtimeLineage,
|
|
7868
8230
|
orderStatus: signal.orderStatus,
|
|
7869
8231
|
orderSkipReason: signal.orderSkipReason,
|
|
7870
8232
|
...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
|
|
7871
8233
|
...signal.ml ? { ml: signal.ml } : {}
|
|
7872
8234
|
}));
|
|
8235
|
+
var lineageScopeKey = (strategy, symbol) => `${strategy}::${symbol}`;
|
|
8236
|
+
var buildExpectedLineageByScope = (replayLineages) => new Map(
|
|
8237
|
+
replayLineages.map((record) => [
|
|
8238
|
+
lineageScopeKey(record.strategy, record.symbol),
|
|
8239
|
+
record.lineage
|
|
8240
|
+
])
|
|
8241
|
+
);
|
|
8242
|
+
var hasExpectedLineage = ({
|
|
8243
|
+
strategy,
|
|
8244
|
+
symbol,
|
|
8245
|
+
lineage,
|
|
8246
|
+
expectedByScope
|
|
8247
|
+
}) => runtimeLineagesMatch(
|
|
8248
|
+
lineage,
|
|
8249
|
+
expectedByScope.get(lineageScopeKey(strategy, symbol))
|
|
8250
|
+
);
|
|
8251
|
+
var filterReplayComparisonByLineage = ({
|
|
8252
|
+
replayLineages,
|
|
8253
|
+
runtimeTrades,
|
|
8254
|
+
runtimeSignals,
|
|
8255
|
+
runtimeSignalEvaluations,
|
|
8256
|
+
runtimeLineageScopes,
|
|
8257
|
+
backtestEntries
|
|
8258
|
+
}) => {
|
|
8259
|
+
const expectedByScope = buildExpectedLineageByScope(replayLineages);
|
|
8260
|
+
const comparableRuntimeSignals = runtimeSignals.filter(
|
|
8261
|
+
(signal) => hasExpectedLineage({
|
|
8262
|
+
strategy: signal.strategy,
|
|
8263
|
+
symbol: signal.symbol,
|
|
8264
|
+
lineage: signal.runtimeLineage,
|
|
8265
|
+
expectedByScope
|
|
8266
|
+
})
|
|
8267
|
+
);
|
|
8268
|
+
const comparableRuntimeEvaluations = runtimeSignalEvaluations.filter(
|
|
8269
|
+
(evaluation) => hasExpectedLineage({
|
|
8270
|
+
strategy: evaluation.strategy,
|
|
8271
|
+
symbol: evaluation.symbol,
|
|
8272
|
+
lineage: evaluation.runtimeLineage,
|
|
8273
|
+
expectedByScope
|
|
8274
|
+
})
|
|
8275
|
+
);
|
|
8276
|
+
const comparableRuntimeLineageScopes = runtimeLineageScopes.filter(
|
|
8277
|
+
(scope) => hasExpectedLineage({
|
|
8278
|
+
strategy: scope.strategy,
|
|
8279
|
+
symbol: scope.symbol,
|
|
8280
|
+
lineage: scope.lineage,
|
|
8281
|
+
expectedByScope
|
|
8282
|
+
})
|
|
8283
|
+
);
|
|
8284
|
+
const deploymentWindows = /* @__PURE__ */ new Map();
|
|
8285
|
+
for (const scope of comparableRuntimeLineageScopes) {
|
|
8286
|
+
const key = lineageScopeKey(scope.strategy, scope.symbol);
|
|
8287
|
+
const existing = deploymentWindows.get(key);
|
|
8288
|
+
deploymentWindows.set(key, {
|
|
8289
|
+
firstTimestamp: existing == null ? scope.firstTimestamp : Math.min(existing.firstTimestamp, scope.firstTimestamp),
|
|
8290
|
+
lastTimestamp: existing == null ? scope.lastTimestamp : Math.max(existing.lastTimestamp, scope.lastTimestamp)
|
|
8291
|
+
});
|
|
8292
|
+
}
|
|
8293
|
+
const lineageBySignalId = /* @__PURE__ */ new Map();
|
|
8294
|
+
for (const signal of runtimeSignals) {
|
|
8295
|
+
if (signal.runtimeLineage) {
|
|
8296
|
+
lineageBySignalId.set(signal.signalId, signal.runtimeLineage);
|
|
8297
|
+
}
|
|
8298
|
+
}
|
|
8299
|
+
for (const evaluation of runtimeSignalEvaluations) {
|
|
8300
|
+
if (evaluation.signalId && evaluation.runtimeLineage) {
|
|
8301
|
+
lineageBySignalId.set(evaluation.signalId, evaluation.runtimeLineage);
|
|
8302
|
+
}
|
|
8303
|
+
}
|
|
8304
|
+
const comparableRuntimeTrades = runtimeTrades.filter((trade) => {
|
|
8305
|
+
const lineage = trade.signalId ? lineageBySignalId.get(trade.signalId) : void 0;
|
|
8306
|
+
if (lineage) {
|
|
8307
|
+
return hasExpectedLineage({
|
|
8308
|
+
strategy: trade.strategy,
|
|
8309
|
+
symbol: trade.symbol,
|
|
8310
|
+
lineage,
|
|
8311
|
+
expectedByScope
|
|
8312
|
+
});
|
|
8313
|
+
}
|
|
8314
|
+
const window2 = deploymentWindows.get(
|
|
8315
|
+
lineageScopeKey(trade.strategy, trade.symbol)
|
|
8316
|
+
);
|
|
8317
|
+
const signalTimestamp = trade.signalTimestamp ?? trade.entryTimestamp;
|
|
8318
|
+
return window2 != null && signalTimestamp >= window2.firstTimestamp && signalTimestamp <= window2.lastTimestamp;
|
|
8319
|
+
});
|
|
8320
|
+
for (const artifact of [
|
|
8321
|
+
...comparableRuntimeSignals,
|
|
8322
|
+
...comparableRuntimeEvaluations
|
|
8323
|
+
]) {
|
|
8324
|
+
const key = lineageScopeKey(artifact.strategy, artifact.symbol);
|
|
8325
|
+
const existing = deploymentWindows.get(key);
|
|
8326
|
+
deploymentWindows.set(key, {
|
|
8327
|
+
firstTimestamp: existing == null ? artifact.timestamp : Math.min(existing.firstTimestamp, artifact.timestamp),
|
|
8328
|
+
lastTimestamp: existing == null ? artifact.timestamp : Math.max(existing.lastTimestamp, artifact.timestamp)
|
|
8329
|
+
});
|
|
8330
|
+
}
|
|
8331
|
+
const comparableBacktestEntries = backtestEntries.filter((entry) => {
|
|
8332
|
+
const window2 = deploymentWindows.get(
|
|
8333
|
+
lineageScopeKey(entry.strategy, entry.symbol)
|
|
8334
|
+
);
|
|
8335
|
+
const signalTimestamp = entry.signalTimestamp ?? entry.timestamp;
|
|
8336
|
+
return window2 != null && signalTimestamp >= window2.firstTimestamp && signalTimestamp <= window2.lastTimestamp;
|
|
8337
|
+
});
|
|
8338
|
+
const comparableScopeKeys = new Set(deploymentWindows.keys());
|
|
8339
|
+
return {
|
|
8340
|
+
runtimeTrades: comparableRuntimeTrades,
|
|
8341
|
+
runtimeSignals: comparableRuntimeSignals,
|
|
8342
|
+
runtimeSignalEvaluations: comparableRuntimeEvaluations,
|
|
8343
|
+
backtestEntries: comparableBacktestEntries,
|
|
8344
|
+
lineage: {
|
|
8345
|
+
enforced: true,
|
|
8346
|
+
replayScopes: expectedByScope.size,
|
|
8347
|
+
comparableScopes: comparableScopeKeys.size,
|
|
8348
|
+
excludedRuntimeTrades: runtimeTrades.length - comparableRuntimeTrades.length,
|
|
8349
|
+
excludedRuntimeSignals: runtimeSignals.length - comparableRuntimeSignals.length,
|
|
8350
|
+
excludedRuntimeEvaluations: runtimeSignalEvaluations.length - comparableRuntimeEvaluations.length,
|
|
8351
|
+
excludedRuntimeLineageScopes: runtimeLineageScopes.length - comparableRuntimeLineageScopes.length,
|
|
8352
|
+
excludedExchangeEntries: 0,
|
|
8353
|
+
excludedBacktestEntries: backtestEntries.length - comparableBacktestEntries.length,
|
|
8354
|
+
reason: comparableScopeKeys.size > 0 ? null : "no_runtime_artifacts_with_matching_lineage",
|
|
8355
|
+
replay: [...expectedByScope.entries()].map(([scope, lineage]) => {
|
|
8356
|
+
const separator = scope.indexOf("::");
|
|
8357
|
+
return {
|
|
8358
|
+
strategy: scope.slice(0, separator),
|
|
8359
|
+
symbol: scope.slice(separator + 2),
|
|
8360
|
+
lineage
|
|
8361
|
+
};
|
|
8362
|
+
}).sort(
|
|
8363
|
+
(left, right) => left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || runtimeLineageKey(left.lineage).localeCompare(
|
|
8364
|
+
runtimeLineageKey(right.lineage)
|
|
8365
|
+
)
|
|
8366
|
+
)
|
|
8367
|
+
}
|
|
8368
|
+
};
|
|
8369
|
+
};
|
|
7873
8370
|
var formatDrilldownSummary = (summary) => Object.entries(summary).filter(([, count]) => count > 0).map(([classification, count]) => `${classification}=${count}`).join(", ");
|
|
7874
8371
|
var toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
7875
8372
|
var sumOptionalNumbers = (values) => {
|
|
@@ -7893,6 +8390,14 @@ var firstFiniteNumber = (values) => {
|
|
|
7893
8390
|
return null;
|
|
7894
8391
|
};
|
|
7895
8392
|
var firstString = (values) => values.find((value) => typeof value === "string" && value.trim());
|
|
8393
|
+
var summarizeBacktestPnlByStrategy = (entries) => {
|
|
8394
|
+
const totals = /* @__PURE__ */ new Map();
|
|
8395
|
+
for (const entry of entries) {
|
|
8396
|
+
const pnl = typeof entry.expectedPnl === "number" && Number.isFinite(entry.expectedPnl) ? entry.expectedPnl : 0;
|
|
8397
|
+
totals.set(entry.strategy, (totals.get(entry.strategy) ?? 0) + pnl);
|
|
8398
|
+
}
|
|
8399
|
+
return totals;
|
|
8400
|
+
};
|
|
7896
8401
|
var getExchangeExecutionMergeKey = (entry) => {
|
|
7897
8402
|
const orderId = firstString([entry.orderId, entry.orderLinkId]);
|
|
7898
8403
|
return orderId ? `${entry.symbol}::${entry.direction}::${orderId}` : null;
|
|
@@ -8159,16 +8664,89 @@ var compareExchangeEntriesToBacktest = ({
|
|
|
8159
8664
|
backtestOnly
|
|
8160
8665
|
};
|
|
8161
8666
|
};
|
|
8667
|
+
var findExchangeRuntimeOutcome = ({
|
|
8668
|
+
exchangeEntry,
|
|
8669
|
+
strategyName,
|
|
8670
|
+
runtimeSignals,
|
|
8671
|
+
runtimeSignalEvaluations,
|
|
8672
|
+
toleranceMs,
|
|
8673
|
+
signalTimestampOffsetMs
|
|
8674
|
+
}) => {
|
|
8675
|
+
const candidates = [];
|
|
8676
|
+
for (const signal of runtimeSignals) {
|
|
8677
|
+
if (signal.strategy === strategyName && signal.symbol === exchangeEntry.symbol && signal.direction === exchangeEntry.direction) {
|
|
8678
|
+
candidates.push({
|
|
8679
|
+
timestamp: signal.timestamp,
|
|
8680
|
+
orderStatus: signal.orderStatus,
|
|
8681
|
+
reason: signal.orderFailureReason || signal.orderSkipReason || signal.orderStatus
|
|
8682
|
+
});
|
|
8683
|
+
}
|
|
8684
|
+
}
|
|
8685
|
+
for (const evaluation of runtimeSignalEvaluations) {
|
|
8686
|
+
if (evaluation.strategy === strategyName && evaluation.symbol === exchangeEntry.symbol && (evaluation.direction == null || evaluation.direction === exchangeEntry.direction)) {
|
|
8687
|
+
candidates.push({
|
|
8688
|
+
timestamp: evaluation.timestamp,
|
|
8689
|
+
orderStatus: evaluation.orderStatus,
|
|
8690
|
+
reason: evaluation.orderSkipReason || evaluation.reason || evaluation.orderStatus
|
|
8691
|
+
});
|
|
8692
|
+
}
|
|
8693
|
+
}
|
|
8694
|
+
return candidates.map((candidate) => ({
|
|
8695
|
+
...candidate,
|
|
8696
|
+
timestampDiffMs: Math.abs(
|
|
8697
|
+
candidate.timestamp + signalTimestampOffsetMs - exchangeEntry.entryTimestamp
|
|
8698
|
+
)
|
|
8699
|
+
})).filter((candidate) => candidate.timestampDiffMs <= toleranceMs).sort(
|
|
8700
|
+
(left, right) => left.timestampDiffMs - right.timestampDiffMs || Number(right.orderStatus === "failed") - Number(left.orderStatus === "failed")
|
|
8701
|
+
)[0] ?? null;
|
|
8702
|
+
};
|
|
8703
|
+
var splitExchangeMatchesByRuntimeOrderStatus = ({
|
|
8704
|
+
matched,
|
|
8705
|
+
strategyNameByOrderLinkKey,
|
|
8706
|
+
runtimeSignals,
|
|
8707
|
+
runtimeSignalEvaluations,
|
|
8708
|
+
toleranceMs,
|
|
8709
|
+
signalTimestampOffsetMs
|
|
8710
|
+
}) => {
|
|
8711
|
+
const orderFailed = [];
|
|
8712
|
+
const completed = matched.filter((item) => {
|
|
8713
|
+
const strategyName = resolveReplayStrategyNameFromExchangeEntry({
|
|
8714
|
+
exchangeEntry: item.exchange,
|
|
8715
|
+
strategyNameByOrderLinkKey
|
|
8716
|
+
});
|
|
8717
|
+
if (!strategyName) {
|
|
8718
|
+
return true;
|
|
8719
|
+
}
|
|
8720
|
+
const outcome = findExchangeRuntimeOutcome({
|
|
8721
|
+
exchangeEntry: item.exchange,
|
|
8722
|
+
strategyName,
|
|
8723
|
+
runtimeSignals,
|
|
8724
|
+
runtimeSignalEvaluations,
|
|
8725
|
+
toleranceMs,
|
|
8726
|
+
signalTimestampOffsetMs
|
|
8727
|
+
});
|
|
8728
|
+
if (outcome?.orderStatus !== "failed") {
|
|
8729
|
+
return true;
|
|
8730
|
+
}
|
|
8731
|
+
orderFailed.push({
|
|
8732
|
+
...item,
|
|
8733
|
+
reason: outcome.reason || "orderStatus=failed"
|
|
8734
|
+
});
|
|
8735
|
+
return false;
|
|
8736
|
+
});
|
|
8737
|
+
return { completed, orderFailed };
|
|
8738
|
+
};
|
|
8162
8739
|
var saveAndPrintReplayExchangeComparison = async ({
|
|
8163
8740
|
liveStrategySummaries,
|
|
8164
8741
|
backtestEntries,
|
|
8165
8742
|
replaySignals,
|
|
8166
8743
|
replaySignalEvaluations,
|
|
8167
8744
|
runtimeSignals,
|
|
8168
|
-
runtimeSignalEvaluations
|
|
8745
|
+
runtimeSignalEvaluations,
|
|
8746
|
+
lineage
|
|
8169
8747
|
}) => {
|
|
8170
8748
|
const { connector, connectorName, window: window2 } = getRuntimeCompareContext();
|
|
8171
|
-
const
|
|
8749
|
+
const rawExchangeEntries = await loadExchangeEntriesForComparison({
|
|
8172
8750
|
connector,
|
|
8173
8751
|
startTime: window2.start,
|
|
8174
8752
|
endTime: window2.end
|
|
@@ -8176,11 +8754,36 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8176
8754
|
const strategyNameByOrderLinkKey = buildStrategyNameByOrderLinkKey(
|
|
8177
8755
|
liveStrategySummaries.map((summary) => summary.strategyName)
|
|
8178
8756
|
);
|
|
8757
|
+
const exchangeOutcomeByEntry = /* @__PURE__ */ new Map();
|
|
8758
|
+
const exchangeEntries = rawExchangeEntries.filter((entry) => {
|
|
8759
|
+
const strategyName = resolveReplayStrategyNameFromExchangeEntry({
|
|
8760
|
+
exchangeEntry: entry,
|
|
8761
|
+
strategyNameByOrderLinkKey
|
|
8762
|
+
});
|
|
8763
|
+
if (!strategyName) {
|
|
8764
|
+
return false;
|
|
8765
|
+
}
|
|
8766
|
+
const outcome = findExchangeRuntimeOutcome({
|
|
8767
|
+
exchangeEntry: entry,
|
|
8768
|
+
strategyName,
|
|
8769
|
+
runtimeSignals,
|
|
8770
|
+
runtimeSignalEvaluations,
|
|
8771
|
+
toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
|
|
8772
|
+
signalTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
|
|
8773
|
+
});
|
|
8774
|
+
exchangeOutcomeByEntry.set(entry, outcome);
|
|
8775
|
+
return outcome != null;
|
|
8776
|
+
});
|
|
8777
|
+
const comparisonLineage = {
|
|
8778
|
+
...lineage,
|
|
8779
|
+
excludedExchangeEntries: lineage.excludedExchangeEntries + rawExchangeEntries.length - exchangeEntries.length
|
|
8780
|
+
};
|
|
8781
|
+
const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(backtestEntries);
|
|
8179
8782
|
if (!exchangeEntries.length) {
|
|
8180
8783
|
console.log("");
|
|
8181
8784
|
console.log(
|
|
8182
8785
|
import_chalk11.default.yellow(
|
|
8183
|
-
`SIGNALS REPLAY VS EXCHANGE: no exchange entry executions found for ${connectorName} in ${(0, import_time6.formatUnix)(
|
|
8786
|
+
`SIGNALS REPLAY VS EXCHANGE: no lineage-linked exchange entry executions found for ${connectorName} in ${(0, import_time6.formatUnix)(
|
|
8184
8787
|
window2.start
|
|
8185
8788
|
)} -> ${(0, import_time6.formatUnix)(window2.end)}`
|
|
8186
8789
|
)
|
|
@@ -8207,18 +8810,21 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8207
8810
|
runtimeEntriesCount: 0,
|
|
8208
8811
|
backtestEntriesCount: backtestEntries.length,
|
|
8209
8812
|
matchedCount: 0,
|
|
8813
|
+
orderFailedCount: 0,
|
|
8210
8814
|
runtimeOnlyCount: 0,
|
|
8211
8815
|
backtestOnlyCount: backtestEntries.length,
|
|
8212
8816
|
details: details2,
|
|
8817
|
+
lineage: comparisonLineage,
|
|
8213
8818
|
rows: liveStrategySummaries.map((summary) => ({
|
|
8214
8819
|
strategyName: summary.strategyName,
|
|
8215
8820
|
backtestEntries: backtestEntries.filter(
|
|
8216
8821
|
(entry) => entry.strategy === summary.strategyName
|
|
8217
8822
|
).length,
|
|
8218
|
-
backtestNetProfit: summary.
|
|
8823
|
+
backtestNetProfit: backtestPnlByStrategy.get(summary.strategyName) ?? 0,
|
|
8219
8824
|
runtimeTrades: 0,
|
|
8220
8825
|
runtimePnl: 0,
|
|
8221
8826
|
matched: 0,
|
|
8827
|
+
orderFailed: 0,
|
|
8222
8828
|
runtimeOnly: 0,
|
|
8223
8829
|
backtestOnly: backtestEntries.filter(
|
|
8224
8830
|
(entry) => entry.strategy === summary.strategyName
|
|
@@ -8232,6 +8838,14 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8232
8838
|
toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
|
|
8233
8839
|
backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
|
|
8234
8840
|
});
|
|
8841
|
+
const { completed: matched, orderFailed } = splitExchangeMatchesByRuntimeOrderStatus({
|
|
8842
|
+
matched: comparison.matched,
|
|
8843
|
+
strategyNameByOrderLinkKey,
|
|
8844
|
+
runtimeSignals,
|
|
8845
|
+
runtimeSignalEvaluations,
|
|
8846
|
+
toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
|
|
8847
|
+
signalTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
|
|
8848
|
+
});
|
|
8235
8849
|
const liveSummaryByStrategy = new Map(
|
|
8236
8850
|
liveStrategySummaries.map((summary) => [summary.strategyName, summary])
|
|
8237
8851
|
);
|
|
@@ -8244,10 +8858,11 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8244
8858
|
const next = {
|
|
8245
8859
|
strategyName,
|
|
8246
8860
|
backtestEntries: 0,
|
|
8247
|
-
backtestNetProfit:
|
|
8861
|
+
backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
|
|
8248
8862
|
runtimeTrades: 0,
|
|
8249
8863
|
runtimePnl: 0,
|
|
8250
8864
|
matched: 0,
|
|
8865
|
+
orderFailed: 0,
|
|
8251
8866
|
runtimeOnly: 0,
|
|
8252
8867
|
backtestOnly: 0
|
|
8253
8868
|
};
|
|
@@ -8260,7 +8875,7 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8260
8875
|
for (const entry of backtestEntries) {
|
|
8261
8876
|
ensureRow(entry.strategy).backtestEntries += 1;
|
|
8262
8877
|
}
|
|
8263
|
-
for (const item of
|
|
8878
|
+
for (const item of matched) {
|
|
8264
8879
|
const row = ensureRow(item.backtest.strategy);
|
|
8265
8880
|
row.runtimeTrades += 1;
|
|
8266
8881
|
row.matched += 1;
|
|
@@ -8268,6 +8883,11 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8268
8883
|
row.runtimePnl += item.exchange.closedPnl;
|
|
8269
8884
|
}
|
|
8270
8885
|
}
|
|
8886
|
+
for (const item of orderFailed) {
|
|
8887
|
+
const row = ensureRow(item.backtest.strategy);
|
|
8888
|
+
row.runtimeTrades += 1;
|
|
8889
|
+
row.orderFailed += 1;
|
|
8890
|
+
}
|
|
8271
8891
|
for (const entry of comparison.backtestOnly) {
|
|
8272
8892
|
ensureRow(entry.strategy).backtestOnly += 1;
|
|
8273
8893
|
}
|
|
@@ -8286,7 +8906,8 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8286
8906
|
}
|
|
8287
8907
|
}
|
|
8288
8908
|
const details = buildReplayExchangeComparisonDetails({
|
|
8289
|
-
matched
|
|
8909
|
+
matched,
|
|
8910
|
+
orderFailed,
|
|
8290
8911
|
exchangeOnly: comparison.exchangeOnly,
|
|
8291
8912
|
backtestOnly: comparison.backtestOnly,
|
|
8292
8913
|
exchangeEntries,
|
|
@@ -8313,8 +8934,10 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8313
8934
|
import_chalk11.default.yellow(String(row.runtimeTrades)),
|
|
8314
8935
|
rtPnlColor(`${row.runtimePnl.toFixed(2)}$`),
|
|
8315
8936
|
import_chalk11.default.green(String(row.matched)),
|
|
8937
|
+
import_chalk11.default.red(String(row.orderFailed)),
|
|
8316
8938
|
import_chalk11.default.yellow(String(row.runtimeOnly)),
|
|
8317
|
-
import_chalk11.default.magenta(String(row.backtestOnly))
|
|
8939
|
+
import_chalk11.default.magenta(String(row.backtestOnly)),
|
|
8940
|
+
import_chalk11.default.red(String(getReplayRuntimeUnmatchedCount(row)))
|
|
8318
8941
|
];
|
|
8319
8942
|
});
|
|
8320
8943
|
console.log("");
|
|
@@ -8342,17 +8965,20 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8342
8965
|
windowTradesCount: exchangeEntries.length,
|
|
8343
8966
|
runtimeEntriesCount: exchangeEntries.length,
|
|
8344
8967
|
backtestEntriesCount: backtestEntries.length,
|
|
8345
|
-
matchedCount:
|
|
8968
|
+
matchedCount: matched.length,
|
|
8969
|
+
orderFailedCount: orderFailed.length,
|
|
8346
8970
|
runtimeOnlyCount: comparison.exchangeOnly.length,
|
|
8347
8971
|
backtestOnlyCount: comparison.backtestOnly.length,
|
|
8348
8972
|
rows,
|
|
8349
|
-
details
|
|
8973
|
+
details,
|
|
8974
|
+
lineage: comparisonLineage
|
|
8350
8975
|
};
|
|
8351
8976
|
};
|
|
8352
8977
|
var saveAndPrintReplayRuntimeComparison = async ({
|
|
8353
8978
|
liveStrategySummaries,
|
|
8354
8979
|
backtestEntries,
|
|
8355
|
-
replaySignals
|
|
8980
|
+
replaySignals,
|
|
8981
|
+
replayLineages
|
|
8356
8982
|
}) => {
|
|
8357
8983
|
const { connector, connectorName, window: window2 } = getRuntimeCompareContext();
|
|
8358
8984
|
if (!connector || !window2) {
|
|
@@ -8362,8 +8988,16 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8362
8988
|
liveStrategySummaries.map((summary) => summary.strategyName)
|
|
8363
8989
|
);
|
|
8364
8990
|
const replaySignalEvaluations = buildReplaySignalEvaluations(replaySignals);
|
|
8365
|
-
const [
|
|
8366
|
-
|
|
8991
|
+
const [
|
|
8992
|
+
rawRuntimeTrades,
|
|
8993
|
+
runtimeSignals,
|
|
8994
|
+
runtimeSignalEvaluations,
|
|
8995
|
+
runtimeLineageScopes
|
|
8996
|
+
] = await Promise.all([
|
|
8997
|
+
loadRuntimeTrades(replayUserName, {
|
|
8998
|
+
startTime: window2.start,
|
|
8999
|
+
endTime: window2.end
|
|
9000
|
+
}),
|
|
8367
9001
|
loadRuntimeSignals(replayUserName, {
|
|
8368
9002
|
startTime: window2.start,
|
|
8369
9003
|
endTime: window2.end
|
|
@@ -8371,6 +9005,10 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8371
9005
|
loadRuntimeSignalEvaluations(replayUserName, {
|
|
8372
9006
|
startTime: window2.start,
|
|
8373
9007
|
endTime: window2.end
|
|
9008
|
+
}),
|
|
9009
|
+
loadRuntimeLineageScopes(replayUserName, {
|
|
9010
|
+
startTime: window2.start,
|
|
9011
|
+
endTime: window2.end
|
|
8374
9012
|
})
|
|
8375
9013
|
]);
|
|
8376
9014
|
const syncedRuntimeTrades = await syncRuntimeTrades({
|
|
@@ -8412,7 +9050,16 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8412
9050
|
}
|
|
8413
9051
|
}
|
|
8414
9052
|
});
|
|
8415
|
-
const
|
|
9053
|
+
const comparable = filterReplayComparisonByLineage({
|
|
9054
|
+
replayLineages,
|
|
9055
|
+
runtimeTrades: syncedRuntimeTrades,
|
|
9056
|
+
runtimeSignals,
|
|
9057
|
+
runtimeSignalEvaluations,
|
|
9058
|
+
runtimeLineageScopes,
|
|
9059
|
+
backtestEntries
|
|
9060
|
+
});
|
|
9061
|
+
const comparableBacktestEntries = comparable.backtestEntries;
|
|
9062
|
+
const windowRuntimeTrades = comparable.runtimeTrades.filter(
|
|
8416
9063
|
(trade) => trade.entryTimestamp >= window2.start && trade.entryTimestamp < window2.end && relevantStrategies.has(trade.strategy)
|
|
8417
9064
|
);
|
|
8418
9065
|
if (!windowRuntimeTrades.length) {
|
|
@@ -8421,17 +9068,18 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8421
9068
|
import_chalk11.default.yellow(
|
|
8422
9069
|
`SIGNALS REPLAY VS RUNTIME: no local runtime trades found for ${connectorName} in ${(0, import_time6.formatUnix)(
|
|
8423
9070
|
window2.start
|
|
8424
|
-
)} -> ${(0, import_time6.formatUnix)(window2.end)}
|
|
9071
|
+
)} -> ${(0, import_time6.formatUnix)(window2.end)} with matching lineage; checking lineage-linked exchange executions`
|
|
8425
9072
|
)
|
|
8426
9073
|
);
|
|
8427
9074
|
console.log("");
|
|
8428
9075
|
return saveAndPrintReplayExchangeComparison({
|
|
8429
9076
|
liveStrategySummaries,
|
|
8430
|
-
backtestEntries,
|
|
9077
|
+
backtestEntries: comparableBacktestEntries,
|
|
8431
9078
|
replaySignals,
|
|
8432
9079
|
replaySignalEvaluations,
|
|
8433
|
-
runtimeSignals,
|
|
8434
|
-
runtimeSignalEvaluations
|
|
9080
|
+
runtimeSignals: comparable.runtimeSignals,
|
|
9081
|
+
runtimeSignalEvaluations: comparable.runtimeSignalEvaluations,
|
|
9082
|
+
lineage: comparable.lineage
|
|
8435
9083
|
});
|
|
8436
9084
|
}
|
|
8437
9085
|
const runtimeSummaries = summarizeRuntimeTradesByStrategy(windowRuntimeTrades);
|
|
@@ -8442,7 +9090,7 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8442
9090
|
const runtimeDedupe = dedupeRuntimeParityEntries(rawRuntimeEntries);
|
|
8443
9091
|
const comparison = compareTradeParityEntries({
|
|
8444
9092
|
runtimeEntries: runtimeDedupe.entries,
|
|
8445
|
-
backtestEntries,
|
|
9093
|
+
backtestEntries: comparableBacktestEntries,
|
|
8446
9094
|
toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
|
|
8447
9095
|
backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
|
|
8448
9096
|
});
|
|
@@ -8451,18 +9099,18 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8451
9099
|
runtimeOnly: comparison.runtimeOnly,
|
|
8452
9100
|
backtestOnly: comparison.backtestOnly,
|
|
8453
9101
|
runtimeEntries: runtimeDedupe.entries,
|
|
8454
|
-
backtestEntries,
|
|
9102
|
+
backtestEntries: comparableBacktestEntries,
|
|
8455
9103
|
toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
|
|
8456
9104
|
backtestTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs(),
|
|
8457
|
-
runtimeSignals,
|
|
8458
|
-
runtimeSignalEvaluations,
|
|
9105
|
+
runtimeSignals: comparable.runtimeSignals,
|
|
9106
|
+
runtimeSignalEvaluations: comparable.runtimeSignalEvaluations,
|
|
8459
9107
|
replaySignals,
|
|
8460
9108
|
replaySignalEvaluations
|
|
8461
9109
|
});
|
|
8462
9110
|
const parityRows = summarizeTradeParityByStrategy({
|
|
8463
9111
|
runtimeEntries: runtimeDedupe.entries,
|
|
8464
9112
|
runtimeDuplicateEntries: runtimeDedupe.duplicateEntries,
|
|
8465
|
-
backtestEntries,
|
|
9113
|
+
backtestEntries: comparableBacktestEntries,
|
|
8466
9114
|
matchedEntries: comparison.matched,
|
|
8467
9115
|
runtimeOnlyEntries: comparison.runtimeOnly,
|
|
8468
9116
|
backtestOnlyEntries: comparison.backtestOnly
|
|
@@ -8471,22 +9119,25 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8471
9119
|
const liveSummaryByStrategy = new Map(
|
|
8472
9120
|
liveStrategySummaries.map((summary) => [summary.strategyName, summary])
|
|
8473
9121
|
);
|
|
9122
|
+
const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(
|
|
9123
|
+
comparableBacktestEntries
|
|
9124
|
+
);
|
|
8474
9125
|
const strategyNames = /* @__PURE__ */ new Set([
|
|
8475
9126
|
...liveSummaryByStrategy.keys(),
|
|
8476
9127
|
...runtimeSummaryByStrategy.keys(),
|
|
8477
9128
|
...parityByStrategy.keys()
|
|
8478
9129
|
]);
|
|
8479
9130
|
const rows = [...strategyNames].sort((left, right) => left.localeCompare(right)).map((strategyName) => {
|
|
8480
|
-
const liveSummary = liveSummaryByStrategy.get(strategyName);
|
|
8481
9131
|
const runtimeSummary = runtimeSummaryByStrategy.get(strategyName);
|
|
8482
9132
|
const parity = parityByStrategy.get(strategyName);
|
|
8483
9133
|
return {
|
|
8484
9134
|
strategyName,
|
|
8485
9135
|
backtestEntries: parity?.backtest ?? 0,
|
|
8486
|
-
backtestNetProfit:
|
|
9136
|
+
backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
|
|
8487
9137
|
runtimeTrades: runtimeSummary?.trades ?? 0,
|
|
8488
9138
|
runtimePnl: runtimeSummary?.totalPnl ?? 0,
|
|
8489
9139
|
matched: parity?.matched ?? 0,
|
|
9140
|
+
orderFailed: 0,
|
|
8490
9141
|
runtimeOnly: parity?.runtimeOnly ?? 0,
|
|
8491
9142
|
backtestOnly: parity?.backtestOnly ?? 0
|
|
8492
9143
|
};
|
|
@@ -8501,8 +9152,10 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8501
9152
|
import_chalk11.default.yellow(String(row.runtimeTrades)),
|
|
8502
9153
|
rtPnlColor(`${row.runtimePnl.toFixed(2)}$`),
|
|
8503
9154
|
import_chalk11.default.green(String(row.matched)),
|
|
9155
|
+
import_chalk11.default.red(String(row.orderFailed)),
|
|
8504
9156
|
import_chalk11.default.yellow(String(row.runtimeOnly)),
|
|
8505
|
-
import_chalk11.default.magenta(String(row.backtestOnly))
|
|
9157
|
+
import_chalk11.default.magenta(String(row.backtestOnly)),
|
|
9158
|
+
import_chalk11.default.red(String(getReplayRuntimeUnmatchedCount(row)))
|
|
8506
9159
|
];
|
|
8507
9160
|
});
|
|
8508
9161
|
console.log("");
|
|
@@ -8529,18 +9182,20 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8529
9182
|
syncedTradesCount: syncedRuntimeTrades.length,
|
|
8530
9183
|
windowTradesCount: windowRuntimeTrades.length,
|
|
8531
9184
|
runtimeEntriesCount: runtimeDedupe.entries.length,
|
|
8532
|
-
backtestEntriesCount:
|
|
9185
|
+
backtestEntriesCount: comparableBacktestEntries.length,
|
|
8533
9186
|
matchedCount: comparison.matched.length,
|
|
9187
|
+
orderFailedCount: 0,
|
|
8534
9188
|
runtimeOnlyCount: comparison.runtimeOnly.length,
|
|
8535
9189
|
backtestOnlyCount: comparison.backtestOnly.length,
|
|
8536
9190
|
rows,
|
|
8537
|
-
details
|
|
9191
|
+
details,
|
|
9192
|
+
lineage: comparable.lineage
|
|
8538
9193
|
};
|
|
8539
9194
|
};
|
|
8540
9195
|
|
|
8541
9196
|
// src/lib/replay/outputReport.ts
|
|
8542
|
-
var
|
|
8543
|
-
var
|
|
9197
|
+
var import_promises2 = __toESM(require("fs/promises"));
|
|
9198
|
+
var import_node_path2 = __toESM(require("path"));
|
|
8544
9199
|
var import_data = require("@tradejs/core/data");
|
|
8545
9200
|
var formatMsk = (timestamp) => typeof timestamp === "number" && Number.isFinite(timestamp) ? new Intl.DateTimeFormat("ru-RU", {
|
|
8546
9201
|
timeZone: "Europe/Moscow",
|
|
@@ -8575,6 +9230,7 @@ var buildPerTradeAnalysis = (runtimeComparison) => {
|
|
|
8575
9230
|
if (!details) {
|
|
8576
9231
|
return {
|
|
8577
9232
|
matched: [],
|
|
9233
|
+
orderFailed: [],
|
|
8578
9234
|
runtimeOnly: [],
|
|
8579
9235
|
backtestOnly: []
|
|
8580
9236
|
};
|
|
@@ -8603,6 +9259,17 @@ var buildPerTradeAnalysis = (runtimeComparison) => {
|
|
|
8603
9259
|
slippage: item.slippage
|
|
8604
9260
|
}
|
|
8605
9261
|
})),
|
|
9262
|
+
orderFailed: details.orderFailed.map((item) => ({
|
|
9263
|
+
status: "orderFailed",
|
|
9264
|
+
strategy: entryStrategy(item.backtest),
|
|
9265
|
+
symbol: item.backtest.symbol,
|
|
9266
|
+
direction: item.backtest.direction,
|
|
9267
|
+
runtime: item.runtime,
|
|
9268
|
+
backtest: item.backtest,
|
|
9269
|
+
reason: item.reason,
|
|
9270
|
+
timestampDiffMs: item.timestampDiffMs,
|
|
9271
|
+
entryPriceDeltaPct: item.priceDeltaPct
|
|
9272
|
+
})),
|
|
8606
9273
|
runtimeOnly: details.runtimeOnly.map((entry) => ({
|
|
8607
9274
|
status: "runtimeOnly",
|
|
8608
9275
|
strategy: entryStrategy(entry),
|
|
@@ -8686,6 +9353,26 @@ var renderRuntimeOnlyRows = (perTrade) => perTrade.length ? createMarkdownTable(
|
|
|
8686
9353
|
];
|
|
8687
9354
|
})
|
|
8688
9355
|
) : "_No runtime-only trades._";
|
|
9356
|
+
var renderOrderFailedRows = (perTrade) => perTrade.length ? createMarkdownTable(
|
|
9357
|
+
[
|
|
9358
|
+
"Strategy",
|
|
9359
|
+
"Symbol",
|
|
9360
|
+
"Dir",
|
|
9361
|
+
"BT time",
|
|
9362
|
+
"RT time",
|
|
9363
|
+
"Entry delta",
|
|
9364
|
+
"Reason"
|
|
9365
|
+
],
|
|
9366
|
+
perTrade.map((item) => [
|
|
9367
|
+
item.strategy,
|
|
9368
|
+
item.symbol,
|
|
9369
|
+
item.direction,
|
|
9370
|
+
formatMsk(item.backtest.timestamp),
|
|
9371
|
+
formatMsk(item.runtime.timestamp),
|
|
9372
|
+
formatPct(item.entryPriceDeltaPct),
|
|
9373
|
+
item.reason
|
|
9374
|
+
])
|
|
9375
|
+
) : "_No failed runtime orders._";
|
|
8689
9376
|
var renderBacktestOnlyRows = (perTrade) => perTrade.length ? createMarkdownTable(
|
|
8690
9377
|
[
|
|
8691
9378
|
"Strategy",
|
|
@@ -8723,8 +9410,10 @@ var renderStrategyRows = (runtimeComparison) => runtimeComparison?.rows.length ?
|
|
|
8723
9410
|
"RT trades",
|
|
8724
9411
|
"RT PnL",
|
|
8725
9412
|
"Matched",
|
|
9413
|
+
"Order failed",
|
|
8726
9414
|
"RT only",
|
|
8727
|
-
"BT only"
|
|
9415
|
+
"BT only",
|
|
9416
|
+
"Unmatched"
|
|
8728
9417
|
],
|
|
8729
9418
|
runtimeComparison.rows.map((row) => [
|
|
8730
9419
|
row.strategyName,
|
|
@@ -8733,10 +9422,69 @@ var renderStrategyRows = (runtimeComparison) => runtimeComparison?.rows.length ?
|
|
|
8733
9422
|
row.runtimeTrades,
|
|
8734
9423
|
formatMoney(row.runtimePnl),
|
|
8735
9424
|
row.matched,
|
|
9425
|
+
row.orderFailed,
|
|
8736
9426
|
row.runtimeOnly,
|
|
8737
|
-
row.backtestOnly
|
|
9427
|
+
row.backtestOnly,
|
|
9428
|
+
getReplayRuntimeUnmatchedCount(row)
|
|
8738
9429
|
])
|
|
8739
9430
|
) : "_No runtime comparison rows._";
|
|
9431
|
+
var renderLineageRows = (runtimeComparison) => {
|
|
9432
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
9433
|
+
for (const record of runtimeComparison?.lineage.replay ?? []) {
|
|
9434
|
+
const existing = grouped.get(record.strategy) ?? {
|
|
9435
|
+
strategy: record.strategy,
|
|
9436
|
+
scopes: 0,
|
|
9437
|
+
gitShas: /* @__PURE__ */ new Set(),
|
|
9438
|
+
dirty: /* @__PURE__ */ new Set(),
|
|
9439
|
+
gates: /* @__PURE__ */ new Set(),
|
|
9440
|
+
configs: /* @__PURE__ */ new Set(),
|
|
9441
|
+
contexts: /* @__PURE__ */ new Set()
|
|
9442
|
+
};
|
|
9443
|
+
existing.scopes += 1;
|
|
9444
|
+
existing.gitShas.add(record.lineage.gitSha ?? "unknown");
|
|
9445
|
+
existing.dirty.add(
|
|
9446
|
+
record.lineage.gitDirty == null ? "unknown" : String(record.lineage.gitDirty)
|
|
9447
|
+
);
|
|
9448
|
+
existing.gates.add(record.lineage.gateFingerprint);
|
|
9449
|
+
existing.configs.add(record.lineage.configFingerprint);
|
|
9450
|
+
existing.contexts.add(record.lineage.contextFingerprint);
|
|
9451
|
+
grouped.set(record.strategy, existing);
|
|
9452
|
+
}
|
|
9453
|
+
const formatSet = (values) => {
|
|
9454
|
+
const sorted = [...values].sort();
|
|
9455
|
+
return sorted.length <= 3 ? sorted.join(", ") : `${sorted.slice(0, 3).join(", ")} \u2026 (+${sorted.length - 3})`;
|
|
9456
|
+
};
|
|
9457
|
+
return grouped.size ? createMarkdownTable(
|
|
9458
|
+
[
|
|
9459
|
+
"Strategy",
|
|
9460
|
+
"Scopes",
|
|
9461
|
+
"Git SHA",
|
|
9462
|
+
"Dirty",
|
|
9463
|
+
"Gate",
|
|
9464
|
+
"Config variants",
|
|
9465
|
+
"Context"
|
|
9466
|
+
],
|
|
9467
|
+
[...grouped.values()].sort((left, right) => left.strategy.localeCompare(right.strategy)).map(
|
|
9468
|
+
({
|
|
9469
|
+
strategy,
|
|
9470
|
+
scopes,
|
|
9471
|
+
gitShas,
|
|
9472
|
+
dirty,
|
|
9473
|
+
gates,
|
|
9474
|
+
configs,
|
|
9475
|
+
contexts
|
|
9476
|
+
}) => [
|
|
9477
|
+
strategy,
|
|
9478
|
+
scopes,
|
|
9479
|
+
formatSet(gitShas),
|
|
9480
|
+
formatSet(dirty),
|
|
9481
|
+
formatSet(gates),
|
|
9482
|
+
`${configs.size}: ${formatSet(configs)}`,
|
|
9483
|
+
formatSet(contexts)
|
|
9484
|
+
]
|
|
9485
|
+
)
|
|
9486
|
+
) : "_No replay lineage records._";
|
|
9487
|
+
};
|
|
8740
9488
|
var writeReplayOutputReport = async ({
|
|
8741
9489
|
projectRoot,
|
|
8742
9490
|
timestamp,
|
|
@@ -8751,8 +9499,8 @@ var writeReplayOutputReport = async ({
|
|
|
8751
9499
|
strategySnapshot,
|
|
8752
9500
|
runtimeComparison
|
|
8753
9501
|
}) => {
|
|
8754
|
-
const outputDir =
|
|
8755
|
-
await
|
|
9502
|
+
const outputDir = import_node_path2.default.resolve(projectRoot, "data/replay/output");
|
|
9503
|
+
await import_promises2.default.mkdir(outputDir, { recursive: true });
|
|
8756
9504
|
const perTradeAnalysis = buildPerTradeAnalysis(runtimeComparison);
|
|
8757
9505
|
const report = {
|
|
8758
9506
|
reportType: "replay-output",
|
|
@@ -8778,15 +9526,17 @@ var writeReplayOutputReport = async ({
|
|
|
8778
9526
|
cycleCount: replayResult.cycleCount,
|
|
8779
9527
|
abortedCycles: replayResult.abortedCycles,
|
|
8780
9528
|
matched: runtimeComparison?.matchedCount ?? 0,
|
|
9529
|
+
orderFailed: runtimeComparison?.orderFailedCount ?? 0,
|
|
8781
9530
|
runtimeOnly: runtimeComparison?.runtimeOnlyCount ?? 0,
|
|
8782
9531
|
backtestOnly: runtimeComparison?.backtestOnlyCount ?? 0
|
|
8783
9532
|
},
|
|
8784
9533
|
resultsByStrategies: strategySnapshot.summaries,
|
|
9534
|
+
replayLineage: replayResult.runtimeLineages,
|
|
8785
9535
|
runtimeComparison,
|
|
8786
9536
|
perTradeAnalysis
|
|
8787
9537
|
};
|
|
8788
|
-
const markdownPath =
|
|
8789
|
-
const jsonPath =
|
|
9538
|
+
const markdownPath = import_node_path2.default.join(outputDir, `${timestamp}-replay-report.md`);
|
|
9539
|
+
const jsonPath = import_node_path2.default.join(outputDir, `${timestamp}-replay-report.json`);
|
|
8790
9540
|
const lines = [
|
|
8791
9541
|
"# Replay Report",
|
|
8792
9542
|
"",
|
|
@@ -8806,13 +9556,26 @@ var writeReplayOutputReport = async ({
|
|
|
8806
9556
|
["Window", `${formatMsk(window2.start)} - ${formatMsk(window2.end)} MSK`],
|
|
8807
9557
|
["Runtime compare mode", runtimeComparison?.mode ?? "none"],
|
|
8808
9558
|
["Matched", runtimeComparison?.matchedCount ?? 0],
|
|
9559
|
+
["Order failed", runtimeComparison?.orderFailedCount ?? 0],
|
|
8809
9560
|
["Runtime-only", runtimeComparison?.runtimeOnlyCount ?? 0],
|
|
8810
9561
|
["Backtest-only", runtimeComparison?.backtestOnlyCount ?? 0],
|
|
9562
|
+
[
|
|
9563
|
+
"Comparable lineage scopes",
|
|
9564
|
+
runtimeComparison ? `${runtimeComparison.lineage.comparableScopes}/${runtimeComparison.lineage.replayScopes}` : "0/0"
|
|
9565
|
+
],
|
|
9566
|
+
[
|
|
9567
|
+
"Lineage exclusions",
|
|
9568
|
+
runtimeComparison ? `runtime=${runtimeComparison.lineage.excludedRuntimeTrades}, exchange=${runtimeComparison.lineage.excludedExchangeEntries}, backtest=${runtimeComparison.lineage.excludedBacktestEntries}` : ""
|
|
9569
|
+
],
|
|
8811
9570
|
["Command", `\`${process.argv.join(" ")}\``],
|
|
8812
9571
|
["JSON report", jsonPath]
|
|
8813
9572
|
]
|
|
8814
9573
|
),
|
|
8815
9574
|
"",
|
|
9575
|
+
"## Replay Lineage",
|
|
9576
|
+
"",
|
|
9577
|
+
renderLineageRows(runtimeComparison),
|
|
9578
|
+
"",
|
|
8816
9579
|
"## Strategy Comparison",
|
|
8817
9580
|
"",
|
|
8818
9581
|
renderStrategyRows(runtimeComparison),
|
|
@@ -8821,6 +9584,10 @@ var writeReplayOutputReport = async ({
|
|
|
8821
9584
|
"",
|
|
8822
9585
|
renderMatchedRows(perTradeAnalysis.matched),
|
|
8823
9586
|
"",
|
|
9587
|
+
"## Failed Runtime Orders",
|
|
9588
|
+
"",
|
|
9589
|
+
renderOrderFailedRows(perTradeAnalysis.orderFailed),
|
|
9590
|
+
"",
|
|
8824
9591
|
"## Runtime-Only Trades",
|
|
8825
9592
|
"",
|
|
8826
9593
|
renderRuntimeOnlyRows(perTradeAnalysis.runtimeOnly),
|
|
@@ -8835,8 +9602,8 @@ var writeReplayOutputReport = async ({
|
|
|
8835
9602
|
""
|
|
8836
9603
|
];
|
|
8837
9604
|
await Promise.all([
|
|
8838
|
-
|
|
8839
|
-
|
|
9605
|
+
import_promises2.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
|
|
9606
|
+
import_promises2.default.writeFile(jsonPath, `${(0, import_data.toJson)(report, true)}
|
|
8840
9607
|
`, "utf8")
|
|
8841
9608
|
]);
|
|
8842
9609
|
return {
|
|
@@ -8875,7 +9642,8 @@ var finishReplay = async ({
|
|
|
8875
9642
|
const replayRuntimeComparison = await saveAndPrintReplayRuntimeComparison({
|
|
8876
9643
|
liveStrategySummaries: replayStrategySnapshot.summaries,
|
|
8877
9644
|
backtestEntries: replayStrategySnapshot.backtestEntries,
|
|
8878
|
-
replaySignals: replayResult.signals
|
|
9645
|
+
replaySignals: replayResult.signals,
|
|
9646
|
+
replayLineages: replayResult.runtimeLineages
|
|
8879
9647
|
});
|
|
8880
9648
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
8881
9649
|
const durationSeconds = Number(
|
|
@@ -8891,7 +9659,7 @@ var finishReplay = async ({
|
|
|
8891
9659
|
replayResult.orderLog[replayResult.orderLog.length - 1]?.timestamp ?? finishedAt.getTime()
|
|
8892
9660
|
)}`
|
|
8893
9661
|
}) : null;
|
|
8894
|
-
const replayKey =
|
|
9662
|
+
const replayKey = import_redis6.redisKeys.backtestResults(
|
|
8895
9663
|
replayUserName,
|
|
8896
9664
|
REPLAY_RESULTS_CONFIG,
|
|
8897
9665
|
timestamp
|
|
@@ -8912,7 +9680,7 @@ var finishReplay = async ({
|
|
|
8912
9680
|
});
|
|
8913
9681
|
console.log(import_chalk12.default.green(`Replay report: ${outputReport.markdownPath}`));
|
|
8914
9682
|
console.log(import_chalk12.default.green(`Replay report JSON: ${outputReport.jsonPath}`));
|
|
8915
|
-
await (0,
|
|
9683
|
+
await (0, import_redis6.setData)(
|
|
8916
9684
|
replayKey,
|
|
8917
9685
|
{
|
|
8918
9686
|
config: REPLAY_RESULTS_CONFIG,
|
|
@@ -8933,6 +9701,7 @@ var finishReplay = async ({
|
|
|
8933
9701
|
cycleCount: replayResult.cycleCount,
|
|
8934
9702
|
abortedCycles: replayResult.abortedCycles,
|
|
8935
9703
|
signalsCount: replayResult.signals.length,
|
|
9704
|
+
replayLineage: replayResult.runtimeLineages,
|
|
8936
9705
|
strategyCharts: replayChartSnapshot,
|
|
8937
9706
|
outputReport
|
|
8938
9707
|
},
|
|
@@ -8943,8 +9712,8 @@ var finishReplay = async ({
|
|
|
8943
9712
|
if (replayChartSnapshot) {
|
|
8944
9713
|
await Promise.all(
|
|
8945
9714
|
replayChartSnapshot.strategies.map(
|
|
8946
|
-
(card) => (0,
|
|
8947
|
-
|
|
9715
|
+
(card) => (0, import_redis6.setData)(
|
|
9716
|
+
import_redis6.redisKeys.strategyChartCard(replayUserName, "replay", card.cardId),
|
|
8948
9717
|
card,
|
|
8949
9718
|
{
|
|
8950
9719
|
expire: import_constants6.TTL_1M
|
|
@@ -8969,7 +9738,8 @@ var replayBacktest = async () => {
|
|
|
8969
9738
|
endTime: replayFlags.endTime,
|
|
8970
9739
|
cacheOnly: replayFlags.cacheOnly,
|
|
8971
9740
|
interval: replayInterval,
|
|
8972
|
-
projectRoot: replayProjectRoot
|
|
9741
|
+
projectRoot: replayProjectRoot,
|
|
9742
|
+
closedIntervalMs: (0, import_data2.intervalToMs)(replayInterval)
|
|
8973
9743
|
});
|
|
8974
9744
|
if (!preparedRun || isReplayUpdateOnlyRun) {
|
|
8975
9745
|
return;
|