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