@tradejs/cli 1.0.5 → 1.0.8
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/README.md +4 -1
- package/dist/cli.js +13791 -7652
- package/dist/lib/aiTrainMetrics.js +173 -0
- package/dist/lib/candlesProviderMigration.js +266 -0
- package/dist/lib/cliArgs.js +40 -0
- package/dist/lib/derivativesContextBackfill.js +487 -0
- package/dist/lib/researchAgent.js +195 -0
- package/dist/lib/runtimeParity.js +234 -0
- package/dist/lib/telegramReports.js +91 -0
- package/dist/lib/timeWindow.js +101 -0
- package/dist/scripts/agentRun.js +895 -0
- package/dist/scripts/aiExport.js +192 -0
- package/dist/scripts/aiExportSelect.js +135 -0
- package/dist/scripts/aiTrain.js +2321 -0
- package/dist/scripts/backtest.js +998 -111
- package/dist/scripts/candlesMigrateProvider.js +5302 -0
- package/dist/scripts/continuity.js +1 -1
- package/dist/scripts/derivativesIngest.js +5 -0
- package/dist/scripts/derivativesIngestCoinalyzeAll.js +21 -12
- package/dist/scripts/migration.js +1 -1
- package/dist/scripts/mlExport.js +112 -13
- package/dist/scripts/mlExportSelect.js +43 -9
- package/dist/scripts/mlTrainLatestSelect.js +4 -3
- package/dist/scripts/researchAuto.js +845 -0
- package/dist/scripts/resolveExportStrategy.js +133 -0
- package/dist/scripts/results.js +46 -22
- package/dist/scripts/runtimeParity.js +1651 -0
- package/dist/scripts/selectStrategy.js +4 -3
- package/dist/scripts/signals.js +799 -62
- package/dist/scripts/signalsSummary.js +655 -0
- package/dist/scripts/spawnSiblingScript.js +71 -0
- package/dist/scripts/user-add.js +0 -9
- package/dist/workers/testerWorker.js +13 -2
- package/package.json +18 -12
|
@@ -0,0 +1,1651 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/scripts/runtimeParity.ts
|
|
31
|
+
var runtimeParity_exports = {};
|
|
32
|
+
__export(runtimeParity_exports, {
|
|
33
|
+
buildRuntimeParityMessage: () => buildRuntimeParityMessage,
|
|
34
|
+
runtimeParity: () => runtimeParity
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(runtimeParity_exports);
|
|
37
|
+
var import_config = require("dotenv/config");
|
|
38
|
+
var import_node_crypto = require("crypto");
|
|
39
|
+
var import_args = __toESM(require("args"));
|
|
40
|
+
var import_chalk = __toESM(require("chalk"));
|
|
41
|
+
var import_connectors = require("@tradejs/connectors");
|
|
42
|
+
var import_constants = require("@tradejs/core/constants");
|
|
43
|
+
var import_time = require("@tradejs/core/time");
|
|
44
|
+
var import_logger = require("@tradejs/infra/logger");
|
|
45
|
+
var import_redis = require("@tradejs/infra/redis");
|
|
46
|
+
var import_cli2 = require("@tradejs/node/cli");
|
|
47
|
+
var import_backtest = require("@tradejs/node/backtest");
|
|
48
|
+
var import_connectors2 = require("@tradejs/node/connectors");
|
|
49
|
+
|
|
50
|
+
// src/lib/runtimeParity.ts
|
|
51
|
+
var toFiniteNumberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
52
|
+
var toGroupKey = (entry) => `${entry.strategy}::${entry.symbol}::${entry.direction}`;
|
|
53
|
+
var toRuntimeDuplicateKey = (entry) => `${toGroupKey(entry)}::${entry.timestamp}`;
|
|
54
|
+
var sortTradeParityEntries = (left, right) => left.timestamp - right.timestamp || left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || left.direction.localeCompare(right.direction) || left.id.localeCompare(right.id);
|
|
55
|
+
var buildPriceDeltaPct = (leftPrice, rightPrice) => {
|
|
56
|
+
if (leftPrice == null || rightPrice == null || !Number.isFinite(leftPrice) || !Number.isFinite(rightPrice) || leftPrice === 0) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return Math.abs((rightPrice - leftPrice) / leftPrice * 100);
|
|
60
|
+
};
|
|
61
|
+
var extractBacktestEntryParityEntries = (orderLog) => {
|
|
62
|
+
if (!Array.isArray(orderLog)) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
return orderLog.filter(
|
|
66
|
+
(item) => (item.type === "OPEN_LONG" || item.type === "OPEN_SHORT") && item.signal && typeof item.signal.strategy === "string" && typeof item.signal.symbol === "string" && (item.signal.direction === "LONG" || item.signal.direction === "SHORT")
|
|
67
|
+
).map((item, index) => {
|
|
68
|
+
const signal = item.signal;
|
|
69
|
+
const timestamp = typeof signal.timestamp === "number" && Number.isFinite(signal.timestamp) ? signal.timestamp : item.timestamp;
|
|
70
|
+
return {
|
|
71
|
+
id: typeof signal.signalId === "string" && signal.signalId.trim() ? signal.signalId : `backtest-${index}-${timestamp}`,
|
|
72
|
+
source: "backtest",
|
|
73
|
+
strategy: signal.strategy,
|
|
74
|
+
symbol: signal.symbol,
|
|
75
|
+
direction: signal.direction,
|
|
76
|
+
timestamp,
|
|
77
|
+
price: toFiniteNumberOrNull(item.price),
|
|
78
|
+
signalId: typeof signal.signalId === "string" ? signal.signalId : void 0
|
|
79
|
+
};
|
|
80
|
+
}).sort((left, right) => left.timestamp - right.timestamp);
|
|
81
|
+
};
|
|
82
|
+
var extractRuntimeParityEntries = (trades) => trades.filter(
|
|
83
|
+
(trade) => typeof trade.strategy === "string" && typeof trade.symbol === "string" && (trade.direction === "LONG" || trade.direction === "SHORT") && typeof trade.entryTimestamp === "number" && Number.isFinite(trade.entryTimestamp)
|
|
84
|
+
).map((trade) => ({
|
|
85
|
+
id: trade.orderId,
|
|
86
|
+
source: "runtime",
|
|
87
|
+
strategy: trade.strategy,
|
|
88
|
+
symbol: trade.symbol,
|
|
89
|
+
direction: trade.direction,
|
|
90
|
+
timestamp: trade.entryTimestamp,
|
|
91
|
+
price: toFiniteNumberOrNull(trade.entryPrice),
|
|
92
|
+
orderId: trade.orderId,
|
|
93
|
+
signalId: typeof trade.signalId === "string" ? trade.signalId : void 0
|
|
94
|
+
})).sort((left, right) => left.timestamp - right.timestamp);
|
|
95
|
+
var findRuntimeDuplicateGroups = (entries) => {
|
|
96
|
+
const groups = /* @__PURE__ */ new Map();
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
if (entry.source !== "runtime") {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const key = toRuntimeDuplicateKey(entry);
|
|
102
|
+
const bucket = groups.get(key) ?? [];
|
|
103
|
+
bucket.push(entry);
|
|
104
|
+
groups.set(key, bucket);
|
|
105
|
+
}
|
|
106
|
+
return [...groups.entries()].filter(([, group]) => group.length > 1).map(([key, group]) => {
|
|
107
|
+
const sortedGroup = [...group].sort(sortTradeParityEntries);
|
|
108
|
+
const first = sortedGroup[0];
|
|
109
|
+
return {
|
|
110
|
+
key,
|
|
111
|
+
strategy: first.strategy,
|
|
112
|
+
symbol: first.symbol,
|
|
113
|
+
direction: first.direction,
|
|
114
|
+
timestamp: first.timestamp,
|
|
115
|
+
entries: sortedGroup
|
|
116
|
+
};
|
|
117
|
+
}).sort(
|
|
118
|
+
(left, right) => left.timestamp - right.timestamp || left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || left.direction.localeCompare(right.direction)
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
var dedupeRuntimeParityEntries = (entries) => {
|
|
122
|
+
const duplicateGroups = findRuntimeDuplicateGroups(entries);
|
|
123
|
+
const duplicateEntriesSet = /* @__PURE__ */ new Set();
|
|
124
|
+
for (const group of duplicateGroups) {
|
|
125
|
+
for (const duplicate of group.entries.slice(1)) {
|
|
126
|
+
duplicateEntriesSet.add(duplicate);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const dedupedEntries = entries.filter((entry) => !duplicateEntriesSet.has(entry)).sort(sortTradeParityEntries);
|
|
130
|
+
const duplicateEntries = duplicateGroups.flatMap((group) => group.entries.slice(1)).sort(sortTradeParityEntries);
|
|
131
|
+
return {
|
|
132
|
+
entries: dedupedEntries,
|
|
133
|
+
duplicateGroups,
|
|
134
|
+
duplicateEntries
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
var compareTradeParityEntries = ({
|
|
138
|
+
runtimeEntries,
|
|
139
|
+
backtestEntries,
|
|
140
|
+
toleranceMs
|
|
141
|
+
}) => {
|
|
142
|
+
const matched = [];
|
|
143
|
+
const runtimeOnly = [];
|
|
144
|
+
const backtestOnly = [];
|
|
145
|
+
const groupedRuntime = /* @__PURE__ */ new Map();
|
|
146
|
+
const groupedBacktest = /* @__PURE__ */ new Map();
|
|
147
|
+
for (const entry of runtimeEntries) {
|
|
148
|
+
const key = toGroupKey(entry);
|
|
149
|
+
const bucket = groupedRuntime.get(key) ?? [];
|
|
150
|
+
bucket.push(entry);
|
|
151
|
+
groupedRuntime.set(key, bucket);
|
|
152
|
+
}
|
|
153
|
+
for (const entry of backtestEntries) {
|
|
154
|
+
const key = toGroupKey(entry);
|
|
155
|
+
const bucket = groupedBacktest.get(key) ?? [];
|
|
156
|
+
bucket.push(entry);
|
|
157
|
+
groupedBacktest.set(key, bucket);
|
|
158
|
+
}
|
|
159
|
+
const groupKeys = /* @__PURE__ */ new Set([
|
|
160
|
+
...groupedRuntime.keys(),
|
|
161
|
+
...groupedBacktest.keys()
|
|
162
|
+
]);
|
|
163
|
+
for (const key of groupKeys) {
|
|
164
|
+
const runtimeGroup = [...groupedRuntime.get(key) ?? []].sort(
|
|
165
|
+
(left, right) => left.timestamp - right.timestamp
|
|
166
|
+
);
|
|
167
|
+
const backtestGroup = [...groupedBacktest.get(key) ?? []].sort(
|
|
168
|
+
(left, right) => left.timestamp - right.timestamp
|
|
169
|
+
);
|
|
170
|
+
const unmatchedBacktest = backtestGroup.map((entry) => ({
|
|
171
|
+
entry,
|
|
172
|
+
used: false
|
|
173
|
+
}));
|
|
174
|
+
for (const runtimeEntry of runtimeGroup) {
|
|
175
|
+
let bestIndex = -1;
|
|
176
|
+
let bestDiff = Number.POSITIVE_INFINITY;
|
|
177
|
+
for (let index = 0; index < unmatchedBacktest.length; index += 1) {
|
|
178
|
+
const candidate = unmatchedBacktest[index];
|
|
179
|
+
if (candidate.used) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const diff = Math.abs(
|
|
183
|
+
candidate.entry.timestamp - runtimeEntry.timestamp
|
|
184
|
+
);
|
|
185
|
+
if (diff > toleranceMs || diff >= bestDiff) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
bestIndex = index;
|
|
189
|
+
bestDiff = diff;
|
|
190
|
+
}
|
|
191
|
+
if (bestIndex < 0) {
|
|
192
|
+
runtimeOnly.push(runtimeEntry);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
unmatchedBacktest[bestIndex].used = true;
|
|
196
|
+
const backtestEntry = unmatchedBacktest[bestIndex].entry;
|
|
197
|
+
matched.push({
|
|
198
|
+
runtime: runtimeEntry,
|
|
199
|
+
backtest: backtestEntry,
|
|
200
|
+
timestampDiffMs: bestDiff,
|
|
201
|
+
priceDeltaPct: buildPriceDeltaPct(
|
|
202
|
+
runtimeEntry.price,
|
|
203
|
+
backtestEntry.price
|
|
204
|
+
)
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
for (const candidate of unmatchedBacktest) {
|
|
208
|
+
if (!candidate.used) {
|
|
209
|
+
backtestOnly.push(candidate.entry);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
matched.sort(
|
|
214
|
+
(left, right) => left.runtime.timestamp - right.runtime.timestamp
|
|
215
|
+
);
|
|
216
|
+
runtimeOnly.sort((left, right) => left.timestamp - right.timestamp);
|
|
217
|
+
backtestOnly.sort((left, right) => left.timestamp - right.timestamp);
|
|
218
|
+
return {
|
|
219
|
+
matched,
|
|
220
|
+
runtimeOnly,
|
|
221
|
+
backtestOnly
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
var summarizeMatchedParity = (matched) => {
|
|
225
|
+
if (!matched.length) {
|
|
226
|
+
return {
|
|
227
|
+
avgPriceDeltaPct: null,
|
|
228
|
+
maxPriceDeltaPct: null,
|
|
229
|
+
avgTimestampDiffMs: null,
|
|
230
|
+
maxTimestampDiffMs: null
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const priceDeltas = matched.map((item) => item.priceDeltaPct).filter(
|
|
234
|
+
(value) => value != null && Number.isFinite(value)
|
|
235
|
+
);
|
|
236
|
+
const timestampDiffs = matched.map((item) => item.timestampDiffMs).filter((value) => Number.isFinite(value));
|
|
237
|
+
const average = (values) => values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : null;
|
|
238
|
+
const maximum = (values) => values.length ? Math.max(...values) : null;
|
|
239
|
+
return {
|
|
240
|
+
avgPriceDeltaPct: average(priceDeltas),
|
|
241
|
+
maxPriceDeltaPct: maximum(priceDeltas),
|
|
242
|
+
avgTimestampDiffMs: average(timestampDiffs),
|
|
243
|
+
maxTimestampDiffMs: maximum(timestampDiffs)
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/lib/cliArgs.ts
|
|
248
|
+
var normalizeCliArgv = (argv, legacyFlagMap) => argv.map((arg) => {
|
|
249
|
+
const [flag, ...rest] = String(arg).split("=");
|
|
250
|
+
const normalizedFlag = legacyFlagMap[flag];
|
|
251
|
+
if (!normalizedFlag) {
|
|
252
|
+
return arg;
|
|
253
|
+
}
|
|
254
|
+
if (!rest.length) {
|
|
255
|
+
return normalizedFlag;
|
|
256
|
+
}
|
|
257
|
+
return `${normalizedFlag}=${rest.join("=")}`;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// src/lib/telegramReports.ts
|
|
261
|
+
var import_cli = require("@tradejs/node/cli");
|
|
262
|
+
var TELEGRAM_REPORT_CHUNK_LIMIT = 3900;
|
|
263
|
+
var buildPartHeader = (index, total) => `\u{1F4E9} <b>Part ${index}/${total}</b>`;
|
|
264
|
+
var splitLongLine = (line, limit) => {
|
|
265
|
+
const chunks = [];
|
|
266
|
+
for (let index = 0; index < line.length; index += limit) {
|
|
267
|
+
chunks.push(line.slice(index, index + limit));
|
|
268
|
+
}
|
|
269
|
+
return chunks;
|
|
270
|
+
};
|
|
271
|
+
var splitTelegramReport = (message, limit = TELEGRAM_REPORT_CHUNK_LIMIT) => {
|
|
272
|
+
const normalizedLimit = Math.max(100, Math.floor(limit));
|
|
273
|
+
if (message.length <= normalizedLimit) {
|
|
274
|
+
return [message];
|
|
275
|
+
}
|
|
276
|
+
const chunks = [];
|
|
277
|
+
let current = "";
|
|
278
|
+
const flush = () => {
|
|
279
|
+
if (!current) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
chunks.push(current);
|
|
283
|
+
current = "";
|
|
284
|
+
};
|
|
285
|
+
for (const line of message.split("\n")) {
|
|
286
|
+
const next = current ? `${current}
|
|
287
|
+
${line}` : line;
|
|
288
|
+
if (next.length <= normalizedLimit) {
|
|
289
|
+
current = next;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
flush();
|
|
293
|
+
if (line.length <= normalizedLimit) {
|
|
294
|
+
current = line;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
chunks.push(...splitLongLine(line, normalizedLimit));
|
|
298
|
+
}
|
|
299
|
+
flush();
|
|
300
|
+
return chunks;
|
|
301
|
+
};
|
|
302
|
+
var sendTelegramReport = async (message, options = {}) => {
|
|
303
|
+
const chunks = splitTelegramReport(message);
|
|
304
|
+
if (chunks.length === 1) {
|
|
305
|
+
await (0, import_cli.sendTextToTG)(chunks[0], options);
|
|
306
|
+
return chunks.length;
|
|
307
|
+
}
|
|
308
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
309
|
+
const header = buildPartHeader(index + 1, chunks.length);
|
|
310
|
+
await (0, import_cli.sendTextToTG)(`${header}
|
|
311
|
+
|
|
312
|
+
${chunk}`, {
|
|
313
|
+
userName: options.userName,
|
|
314
|
+
markup: index === chunks.length - 1 ? options.markup : void 0
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return chunks.length;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// src/lib/timeWindow.ts
|
|
321
|
+
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
322
|
+
var toEpochMs = (value) => {
|
|
323
|
+
if (value == null || value === "") {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
const parsed = Number(value);
|
|
327
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return parsed < 1e12 ? Math.trunc(parsed * 1e3) : Math.trunc(parsed);
|
|
331
|
+
};
|
|
332
|
+
var toPositiveNumber = (value) => {
|
|
333
|
+
if (value == null || value === "") {
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
const parsed = Number(value);
|
|
337
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return parsed;
|
|
341
|
+
};
|
|
342
|
+
var resolveTimeWindow = ({
|
|
343
|
+
days,
|
|
344
|
+
startTime,
|
|
345
|
+
endTime,
|
|
346
|
+
nowMs = Date.now(),
|
|
347
|
+
defaultStartMs,
|
|
348
|
+
defaultEndMs
|
|
349
|
+
}) => {
|
|
350
|
+
const explicitStart = toEpochMs(startTime);
|
|
351
|
+
const explicitEnd = toEpochMs(endTime);
|
|
352
|
+
if (explicitStart != null || explicitEnd != null) {
|
|
353
|
+
const resolvedEnd2 = explicitEnd ?? defaultEndMs ?? nowMs;
|
|
354
|
+
const resolvedStart = explicitStart ?? defaultStartMs;
|
|
355
|
+
if (resolvedStart >= resolvedEnd2) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
`Invalid time window: startTime (${resolvedStart}) must be less than endTime (${resolvedEnd2})`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
start: resolvedStart,
|
|
362
|
+
end: resolvedEnd2,
|
|
363
|
+
source: "explicit"
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const resolvedDays = toPositiveNumber(days);
|
|
367
|
+
if (resolvedDays != null) {
|
|
368
|
+
const resolvedEnd2 = defaultEndMs ?? nowMs;
|
|
369
|
+
const resolvedStart = resolvedEnd2 - resolvedDays * ONE_DAY_MS;
|
|
370
|
+
if (resolvedStart >= resolvedEnd2) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
`Invalid time window: computed start (${resolvedStart}) must be less than end (${resolvedEnd2})`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
start: Math.trunc(resolvedStart),
|
|
377
|
+
end: Math.trunc(resolvedEnd2),
|
|
378
|
+
source: "days"
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const resolvedEnd = defaultEndMs ?? nowMs;
|
|
382
|
+
if (defaultStartMs >= resolvedEnd) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`Invalid default time window: start (${defaultStartMs}) must be less than end (${resolvedEnd})`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
start: Math.trunc(defaultStartMs),
|
|
389
|
+
end: Math.trunc(resolvedEnd),
|
|
390
|
+
source: "default"
|
|
391
|
+
};
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// src/scripts/runtimeParity.ts
|
|
395
|
+
import_args.default.option(["u", "user"], "Use user config", "root");
|
|
396
|
+
import_args.default.option(
|
|
397
|
+
["o", "connector"],
|
|
398
|
+
"Connector provider or name for parity replay (e.g. bybit, binance, coinbase, custom)",
|
|
399
|
+
"bybit"
|
|
400
|
+
);
|
|
401
|
+
import_args.default.option(["d", "days"], "Replay window in days", 3);
|
|
402
|
+
import_args.default.option(
|
|
403
|
+
["b", "startTime"],
|
|
404
|
+
"Explicit replay start timestamp (ms or seconds)"
|
|
405
|
+
);
|
|
406
|
+
import_args.default.option(["e", "endTime"], "Explicit replay end timestamp (ms or seconds)");
|
|
407
|
+
import_args.default.option(["s", "strategy"], "Only compare one strategy");
|
|
408
|
+
import_args.default.option(
|
|
409
|
+
["t", "tickers"],
|
|
410
|
+
"Replay comma-separated symbols for all configured strategies"
|
|
411
|
+
);
|
|
412
|
+
import_args.default.option(
|
|
413
|
+
["C", "cacheOnly"],
|
|
414
|
+
"Do not refresh market history before replay",
|
|
415
|
+
false
|
|
416
|
+
);
|
|
417
|
+
import_args.default.option(
|
|
418
|
+
["a", "toleranceBars"],
|
|
419
|
+
"Allowed entry timestamp drift in bars when matching runtime vs backtest",
|
|
420
|
+
1
|
|
421
|
+
);
|
|
422
|
+
import_args.default.option(
|
|
423
|
+
"fullUniverse",
|
|
424
|
+
"Replay every configured strategy across the full connector universe. By default parity replays only runtime-traded and strategy-results symbols unless --tickers is provided.",
|
|
425
|
+
false
|
|
426
|
+
);
|
|
427
|
+
import_args.default.option(
|
|
428
|
+
"runtimeGates",
|
|
429
|
+
"Replay with runtime AI/ML gates enabled when configured. This may call external AI providers and ML inference.",
|
|
430
|
+
false
|
|
431
|
+
);
|
|
432
|
+
import_args.default.option(["N", "notify"], "Send parity summary to Telegram", false);
|
|
433
|
+
import_args.default.option(["D", "details"], "Print unmatched entry details (capped)", false);
|
|
434
|
+
process.argv = normalizeCliArgv(process.argv, {
|
|
435
|
+
"-C": "--cacheOnly",
|
|
436
|
+
"-D": "--details",
|
|
437
|
+
"-E": "--endTime",
|
|
438
|
+
"-N": "--notify",
|
|
439
|
+
"-S": "--startTime",
|
|
440
|
+
"-T": "--toleranceBars"
|
|
441
|
+
});
|
|
442
|
+
var flags = import_args.default.parse(process.argv);
|
|
443
|
+
var interval = "15";
|
|
444
|
+
var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
|
|
445
|
+
var DEFAULT_LOOKBACK_DAYS = 3;
|
|
446
|
+
var DETAIL_LIMIT = 10;
|
|
447
|
+
var TELEGRAM_DETAIL_LIMIT = 5;
|
|
448
|
+
var SUMMARY_TIMEZONE = "Europe/Moscow";
|
|
449
|
+
var SUMMARY_TIMEZONE_LABEL = "MSK";
|
|
450
|
+
var REPLAY_ENV = flags.runtimeGates ? "PARITY" : "BACKTEST";
|
|
451
|
+
var formatDuration = (startedAt) => {
|
|
452
|
+
const seconds = Math.max(0, (Date.now() - startedAt) / 1e3);
|
|
453
|
+
if (seconds < 60) {
|
|
454
|
+
return `${seconds.toFixed(1)}s`;
|
|
455
|
+
}
|
|
456
|
+
const minutes = Math.floor(seconds / 60);
|
|
457
|
+
const restSeconds = Math.round(seconds % 60);
|
|
458
|
+
return `${minutes}m ${restSeconds}s`;
|
|
459
|
+
};
|
|
460
|
+
var BACKTEST_ONLY_CLASSIFICATIONS = [
|
|
461
|
+
"gated_out",
|
|
462
|
+
"order_failed",
|
|
463
|
+
"core_skipped",
|
|
464
|
+
"not_evaluated",
|
|
465
|
+
"true_mismatch"
|
|
466
|
+
];
|
|
467
|
+
var RUNTIME_ONLY_CLASSIFICATIONS = [
|
|
468
|
+
"gated_out",
|
|
469
|
+
"order_failed",
|
|
470
|
+
"core_skipped",
|
|
471
|
+
"backtest_drift",
|
|
472
|
+
"not_evaluated",
|
|
473
|
+
"true_mismatch"
|
|
474
|
+
];
|
|
475
|
+
var parseSymbolsFromCLI = (symbols = "") => symbols.split(",").map((symbol) => symbol.trim().toUpperCase()).filter(Boolean).map((symbol) => symbol.endsWith("USDT") ? symbol : `${symbol}USDT`);
|
|
476
|
+
var toTargetKey = (target) => `${target.strategy}::${target.symbol}`;
|
|
477
|
+
var escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
478
|
+
var formatMskDateTime = (timestamp) => new Intl.DateTimeFormat("ru-RU", {
|
|
479
|
+
timeZone: SUMMARY_TIMEZONE,
|
|
480
|
+
year: "numeric",
|
|
481
|
+
month: "2-digit",
|
|
482
|
+
day: "2-digit",
|
|
483
|
+
hour: "2-digit",
|
|
484
|
+
minute: "2-digit",
|
|
485
|
+
hour12: false
|
|
486
|
+
}).format(new Date(timestamp));
|
|
487
|
+
var normalizeSignalOrderStatus = (value) => {
|
|
488
|
+
if (value === "completed" || value === "failed" || value === "skipped" || value === "canceled") {
|
|
489
|
+
return value;
|
|
490
|
+
}
|
|
491
|
+
return "unknown";
|
|
492
|
+
};
|
|
493
|
+
var isRuntimeTradeRecord = (value) => {
|
|
494
|
+
if (!value || typeof value !== "object") {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
const record = value;
|
|
498
|
+
return typeof record.orderId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.entryTimestamp === "number" && typeof record.entryPrice === "number" && typeof record.qty === "number" && (record.direction === "LONG" || record.direction === "SHORT");
|
|
499
|
+
};
|
|
500
|
+
var isSignalRecord = (value) => {
|
|
501
|
+
if (!value || typeof value !== "object") {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
const record = value;
|
|
505
|
+
return typeof record.signalId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.timestamp === "number" && (record.direction === "LONG" || record.direction === "SHORT");
|
|
506
|
+
};
|
|
507
|
+
var isRuntimeSignalEvaluationRecord = (value) => {
|
|
508
|
+
if (!value || typeof value !== "object") {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
const record = value;
|
|
512
|
+
return typeof record.evaluationId === "string" && typeof record.strategy === "string" && typeof record.symbol === "string" && typeof record.timestamp === "number" && (record.status === "signal" || record.status === "skip" || record.status === "error");
|
|
513
|
+
};
|
|
514
|
+
var resolveParityConnectorName = async (value) => {
|
|
515
|
+
const connectorName = await (0, import_connectors2.resolveConnectorName)(value, projectRoot);
|
|
516
|
+
if (connectorName) {
|
|
517
|
+
return connectorName;
|
|
518
|
+
}
|
|
519
|
+
import_logger.logger.warn(
|
|
520
|
+
'Unknown connector "%s". Fallback to %s.',
|
|
521
|
+
String(value || "").trim() || String(value),
|
|
522
|
+
import_connectors2.DEFAULT_CONNECTOR_NAME
|
|
523
|
+
);
|
|
524
|
+
return import_connectors2.DEFAULT_CONNECTOR_NAME;
|
|
525
|
+
};
|
|
526
|
+
var loadRuntimeTrades = async (userName) => {
|
|
527
|
+
const keys = await (0, import_redis.getKeys)(import_redis.redisKeys.runtimeTrades(userName));
|
|
528
|
+
const trades = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
|
|
529
|
+
return trades.filter(isRuntimeTradeRecord).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
|
|
530
|
+
};
|
|
531
|
+
var loadRuntimeSignals = async (userName) => {
|
|
532
|
+
const keys = await (0, import_redis.getKeys)(import_redis.redisKeys.runtimeSignals(userName));
|
|
533
|
+
const signals = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
|
|
534
|
+
return signals.filter(isSignalRecord).sort((left, right) => left.timestamp - right.timestamp);
|
|
535
|
+
};
|
|
536
|
+
var loadRuntimeSignalEvaluations = async (userName) => {
|
|
537
|
+
const keys = await (0, import_redis.getKeys)(import_redis.redisKeys.runtimeSignalEvaluations(userName));
|
|
538
|
+
const evaluations = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
|
|
539
|
+
return evaluations.filter(isRuntimeSignalEvaluationRecord).sort((left, right) => left.timestamp - right.timestamp);
|
|
540
|
+
};
|
|
541
|
+
var resolveStrategyNameByConfigKey = (userName, key) => {
|
|
542
|
+
const parts = key.split(":");
|
|
543
|
+
if (parts.length !== 5) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
const [users, keyUserName, strategiesKey, strategyName, configKey] = parts;
|
|
547
|
+
if (users !== "users" || keyUserName !== userName || strategiesKey !== "strategies" || configKey !== "config" || !strategyName) {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
return strategyName;
|
|
551
|
+
};
|
|
552
|
+
var loadConfiguredStrategyNames = async (userName) => {
|
|
553
|
+
const keys = await (0, import_redis.getKeys)(`${import_redis.redisKeys.strategies(userName)}:`);
|
|
554
|
+
return keys.filter((key) => key.endsWith(":config")).map((key) => resolveStrategyNameByConfigKey(userName, key)).filter((value) => Boolean(value)).sort((left, right) => left.localeCompare(right));
|
|
555
|
+
};
|
|
556
|
+
var loadStrategyResultSymbols = async ({
|
|
557
|
+
userName,
|
|
558
|
+
strategy
|
|
559
|
+
}) => {
|
|
560
|
+
const results = await (0, import_redis.getData)(
|
|
561
|
+
import_redis.redisKeys.strategyResults(userName, strategy),
|
|
562
|
+
{}
|
|
563
|
+
);
|
|
564
|
+
return Object.keys(results ?? {}).sort(
|
|
565
|
+
(left, right) => left.localeCompare(right)
|
|
566
|
+
);
|
|
567
|
+
};
|
|
568
|
+
var addReplayTarget = (targets, target, source) => {
|
|
569
|
+
const key = toTargetKey(target);
|
|
570
|
+
const existing = targets.get(key);
|
|
571
|
+
if (existing) {
|
|
572
|
+
if (!existing.sources.includes(source)) {
|
|
573
|
+
existing.sources.push(source);
|
|
574
|
+
}
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
targets.set(key, {
|
|
578
|
+
strategy: target.strategy,
|
|
579
|
+
symbol: target.symbol,
|
|
580
|
+
sources: [source]
|
|
581
|
+
});
|
|
582
|
+
};
|
|
583
|
+
var countReplayTargetSources = (targets) => ({
|
|
584
|
+
runtime: targets.filter((target) => target.sources.includes("runtime")).length,
|
|
585
|
+
connectorUniverse: targets.filter(
|
|
586
|
+
(target) => target.sources.includes("connectorUniverse")
|
|
587
|
+
).length,
|
|
588
|
+
explicitTickers: targets.filter(
|
|
589
|
+
(target) => target.sources.includes("explicitTickers")
|
|
590
|
+
).length,
|
|
591
|
+
strategyResults: targets.filter(
|
|
592
|
+
(target) => target.sources.includes("strategyResults")
|
|
593
|
+
).length
|
|
594
|
+
});
|
|
595
|
+
var buildReplayTargets = async ({
|
|
596
|
+
userName,
|
|
597
|
+
runtimeTrades,
|
|
598
|
+
connectorSymbols,
|
|
599
|
+
strategyFilter,
|
|
600
|
+
explicitSymbols,
|
|
601
|
+
includeConnectorUniverse
|
|
602
|
+
}) => {
|
|
603
|
+
const targets = /* @__PURE__ */ new Map();
|
|
604
|
+
let configuredStrategies = (await loadConfiguredStrategyNames(userName)).filter((strategy) => !strategyFilter || strategy === strategyFilter);
|
|
605
|
+
if (strategyFilter && !configuredStrategies.some((strategy) => strategy === strategyFilter)) {
|
|
606
|
+
configuredStrategies = [strategyFilter];
|
|
607
|
+
}
|
|
608
|
+
const explicitSymbolSet = explicitSymbols.length ? new Set(explicitSymbols) : null;
|
|
609
|
+
for (const trade of runtimeTrades) {
|
|
610
|
+
if (strategyFilter && trade.strategy !== strategyFilter) {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (explicitSymbolSet && !explicitSymbolSet.has(trade.symbol)) {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
addReplayTarget(
|
|
617
|
+
targets,
|
|
618
|
+
{ strategy: trade.strategy, symbol: trade.symbol },
|
|
619
|
+
"runtime"
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
if (explicitSymbols.length || includeConnectorUniverse) {
|
|
623
|
+
const universeSymbols = explicitSymbols.length ? explicitSymbols : connectorSymbols;
|
|
624
|
+
const universeSource = explicitSymbols.length ? "explicitTickers" : "connectorUniverse";
|
|
625
|
+
for (const strategy of configuredStrategies) {
|
|
626
|
+
for (const symbol of universeSymbols) {
|
|
627
|
+
addReplayTarget(targets, { strategy, symbol }, universeSource);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const strategySymbolsEntries = await Promise.all(
|
|
632
|
+
configuredStrategies.map(
|
|
633
|
+
async (strategy) => [
|
|
634
|
+
strategy,
|
|
635
|
+
await loadStrategyResultSymbols({ userName, strategy })
|
|
636
|
+
]
|
|
637
|
+
)
|
|
638
|
+
);
|
|
639
|
+
for (const [strategy, symbols] of strategySymbolsEntries) {
|
|
640
|
+
for (const symbol of symbols) {
|
|
641
|
+
if (explicitSymbolSet && !explicitSymbolSet.has(symbol)) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
addReplayTarget(targets, { strategy, symbol }, "strategyResults");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return [...targets.values()].sort(
|
|
648
|
+
(left, right) => left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol)
|
|
649
|
+
);
|
|
650
|
+
};
|
|
651
|
+
var createConnector = async ({
|
|
652
|
+
userName,
|
|
653
|
+
connectorName
|
|
654
|
+
}) => {
|
|
655
|
+
const connectorFactory = await (0, import_connectors2.getConnectorCreatorByName)(
|
|
656
|
+
connectorName,
|
|
657
|
+
projectRoot
|
|
658
|
+
);
|
|
659
|
+
if (!connectorFactory) {
|
|
660
|
+
throw new Error(`Connector "${connectorName}" is not registered`);
|
|
661
|
+
}
|
|
662
|
+
return await connectorFactory({ userName });
|
|
663
|
+
};
|
|
664
|
+
var buildReplayAiAnalyses = ({
|
|
665
|
+
runtimeTrades,
|
|
666
|
+
runtimeSignalEvaluations,
|
|
667
|
+
strategy,
|
|
668
|
+
symbol,
|
|
669
|
+
toleranceMs
|
|
670
|
+
}) => {
|
|
671
|
+
const snapshots = [
|
|
672
|
+
...runtimeTrades.filter(
|
|
673
|
+
(trade) => trade.strategy === strategy && trade.symbol === symbol && trade.aiAnalysis && typeof trade.entryTimestamp === "number" && Number.isFinite(trade.entryTimestamp)
|
|
674
|
+
).map((trade) => ({
|
|
675
|
+
strategy: trade.strategy,
|
|
676
|
+
symbol: trade.symbol,
|
|
677
|
+
direction: trade.direction,
|
|
678
|
+
timestamp: trade.entryTimestamp,
|
|
679
|
+
toleranceMs,
|
|
680
|
+
analysis: trade.aiAnalysis
|
|
681
|
+
})),
|
|
682
|
+
...runtimeSignalEvaluations.filter(
|
|
683
|
+
(evaluation) => evaluation.strategy === strategy && evaluation.symbol === symbol && evaluation.aiAnalysis && evaluation.direction && typeof evaluation.timestamp === "number" && Number.isFinite(evaluation.timestamp)
|
|
684
|
+
).map((evaluation) => ({
|
|
685
|
+
strategy: evaluation.strategy,
|
|
686
|
+
symbol: evaluation.symbol,
|
|
687
|
+
direction: evaluation.direction,
|
|
688
|
+
timestamp: evaluation.timestamp,
|
|
689
|
+
toleranceMs,
|
|
690
|
+
analysis: evaluation.aiAnalysis
|
|
691
|
+
}))
|
|
692
|
+
];
|
|
693
|
+
const seen = /* @__PURE__ */ new Set();
|
|
694
|
+
return snapshots.filter((snapshot) => {
|
|
695
|
+
const key = `${snapshot.strategy}:${snapshot.symbol}:${snapshot.direction}:${snapshot.timestamp}`;
|
|
696
|
+
if (seen.has(key)) {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
seen.add(key);
|
|
700
|
+
return true;
|
|
701
|
+
});
|
|
702
|
+
};
|
|
703
|
+
var buildReplayConfig = async ({
|
|
704
|
+
userName,
|
|
705
|
+
strategy,
|
|
706
|
+
symbol,
|
|
707
|
+
runtimeTrades,
|
|
708
|
+
runtimeSignalEvaluations,
|
|
709
|
+
toleranceMs
|
|
710
|
+
}) => {
|
|
711
|
+
const [userConfig, strategyResults] = await Promise.all([
|
|
712
|
+
(0, import_redis.getData)(import_redis.redisKeys.strategyConfig(userName, strategy), {}),
|
|
713
|
+
(0, import_redis.getData)(import_redis.redisKeys.strategyResults(userName, strategy), {})
|
|
714
|
+
]);
|
|
715
|
+
const typedResults = strategyResults ?? {};
|
|
716
|
+
const symbolResult = typedResults?.[symbol];
|
|
717
|
+
const symbolConfig = symbolResult && symbolResult.config && typeof symbolResult.config === "object" ? symbolResult.config : {};
|
|
718
|
+
const aiReplayAnalyses = buildReplayAiAnalyses({
|
|
719
|
+
runtimeTrades,
|
|
720
|
+
runtimeSignalEvaluations,
|
|
721
|
+
strategy,
|
|
722
|
+
symbol,
|
|
723
|
+
toleranceMs
|
|
724
|
+
});
|
|
725
|
+
return {
|
|
726
|
+
...userConfig,
|
|
727
|
+
...symbolConfig,
|
|
728
|
+
ENV: REPLAY_ENV,
|
|
729
|
+
MAKE_ORDERS: true,
|
|
730
|
+
INTERVAL: interval,
|
|
731
|
+
RECORD_RUNTIME_TRADES: false,
|
|
732
|
+
...aiReplayAnalyses.length ? { AI_REPLAY_ANALYSES: aiReplayAnalyses } : {}
|
|
733
|
+
};
|
|
734
|
+
};
|
|
735
|
+
var warmReplayHistory = async ({
|
|
736
|
+
userName,
|
|
737
|
+
connectorName,
|
|
738
|
+
targets,
|
|
739
|
+
preloadStart,
|
|
740
|
+
preloadEnd
|
|
741
|
+
}) => {
|
|
742
|
+
const connector = await createConnector({ userName, connectorName });
|
|
743
|
+
const symbols = [...new Set(targets.map((target) => target.symbol))];
|
|
744
|
+
const warmStartedAt = Date.now();
|
|
745
|
+
console.log(
|
|
746
|
+
import_chalk.default.gray(
|
|
747
|
+
`Warm history: ${symbols.length} symbol(s) on ${connectorName}, preload ${(0, import_time.formatUnix)(preloadStart)} -> ${(0, import_time.formatUnix)(preloadEnd)}`
|
|
748
|
+
)
|
|
749
|
+
);
|
|
750
|
+
await (0, import_cli2.update)(connector, interval, symbols, void 0, {
|
|
751
|
+
connectorLabel: connectorName,
|
|
752
|
+
preloadStart,
|
|
753
|
+
preloadEnd
|
|
754
|
+
});
|
|
755
|
+
const [binanceFactory, coinbaseFactory] = await Promise.all([
|
|
756
|
+
(0, import_connectors2.getConnectorCreatorByName)(import_connectors.ConnectorNames.Binance, projectRoot),
|
|
757
|
+
(0, import_connectors2.getConnectorCreatorByName)(import_connectors.ConnectorNames.Coinbase, projectRoot)
|
|
758
|
+
]);
|
|
759
|
+
if (binanceFactory) {
|
|
760
|
+
const binanceConnector = await binanceFactory({
|
|
761
|
+
userName
|
|
762
|
+
});
|
|
763
|
+
await (0, import_cli2.update)(binanceConnector, interval, ["BTCUSDT"], void 0, {
|
|
764
|
+
connectorLabel: import_connectors.ConnectorNames.Binance,
|
|
765
|
+
preloadStart,
|
|
766
|
+
preloadEnd
|
|
767
|
+
});
|
|
768
|
+
} else {
|
|
769
|
+
import_logger.logger.warn(
|
|
770
|
+
"Binance connector is unavailable. BTC reference replay may drift."
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
if (coinbaseFactory) {
|
|
774
|
+
const coinbaseConnector = await coinbaseFactory({
|
|
775
|
+
userName
|
|
776
|
+
});
|
|
777
|
+
await (0, import_cli2.update)(coinbaseConnector, interval, ["BTCUSDT"], void 0, {
|
|
778
|
+
connectorLabel: import_connectors.ConnectorNames.Coinbase,
|
|
779
|
+
preloadStart,
|
|
780
|
+
preloadEnd
|
|
781
|
+
});
|
|
782
|
+
} else {
|
|
783
|
+
import_logger.logger.warn(
|
|
784
|
+
"Coinbase connector is unavailable. BTC reference replay may drift."
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
console.log(
|
|
788
|
+
import_chalk.default.gray(`Warm history: done in ${formatDuration(warmStartedAt)}`)
|
|
789
|
+
);
|
|
790
|
+
};
|
|
791
|
+
var formatPercent = (value) => value == null || !Number.isFinite(value) ? "n/a" : `${value.toFixed(2)}%`;
|
|
792
|
+
var formatMinutes = (value) => value == null || !Number.isFinite(value) ? "n/a" : `${(value / 6e4).toFixed(2)}m`;
|
|
793
|
+
var formatEntryLabel = (entry) => `${entry.strategy} ${entry.symbol} ${entry.direction} ${(0, import_time.formatUnix)(entry.timestamp)}`;
|
|
794
|
+
var printRuntimeDuplicateDetails = (groups) => {
|
|
795
|
+
if (!groups.length) {
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
console.log("");
|
|
799
|
+
console.log(import_chalk.default.yellow("Runtime duplicates"));
|
|
800
|
+
for (const group of groups.slice(0, DETAIL_LIMIT)) {
|
|
801
|
+
const ids = group.entries.map((entry) => entry.orderId ?? entry.id);
|
|
802
|
+
console.log(
|
|
803
|
+
`- ${formatEntryLabel(group.entries[0])} count=${group.entries.length}, duplicateEntries=${group.entries.length - 1}, ids=${ids.join(",")}`
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
if (groups.length > DETAIL_LIMIT) {
|
|
807
|
+
console.log(`- ... ${groups.length - DETAIL_LIMIT} more`);
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
var findNearestRuntimeSignal = ({
|
|
811
|
+
entry,
|
|
812
|
+
runtimeSignals,
|
|
813
|
+
toleranceMs
|
|
814
|
+
}) => {
|
|
815
|
+
let bestSignal = null;
|
|
816
|
+
let bestDiff = Number.POSITIVE_INFINITY;
|
|
817
|
+
for (const signal of runtimeSignals) {
|
|
818
|
+
if (signal.strategy !== entry.strategy || signal.symbol !== entry.symbol || signal.direction !== entry.direction) {
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const diff = Math.abs(signal.timestamp - entry.timestamp);
|
|
822
|
+
if (diff > toleranceMs || diff >= bestDiff) {
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
bestSignal = signal;
|
|
826
|
+
bestDiff = diff;
|
|
827
|
+
}
|
|
828
|
+
return bestSignal ? {
|
|
829
|
+
signal: bestSignal,
|
|
830
|
+
timestampDiffMs: bestDiff
|
|
831
|
+
} : null;
|
|
832
|
+
};
|
|
833
|
+
var findNearestRuntimeSignalEvaluation = ({
|
|
834
|
+
entry,
|
|
835
|
+
runtimeSignalEvaluations,
|
|
836
|
+
toleranceMs
|
|
837
|
+
}) => {
|
|
838
|
+
let bestEvaluation = null;
|
|
839
|
+
let bestDiff = Number.POSITIVE_INFINITY;
|
|
840
|
+
for (const evaluation of runtimeSignalEvaluations) {
|
|
841
|
+
if (evaluation.strategy !== entry.strategy || evaluation.symbol !== entry.symbol) {
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
const diff = Math.abs(evaluation.timestamp - entry.timestamp);
|
|
845
|
+
if (diff > toleranceMs || diff >= bestDiff) {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
bestEvaluation = evaluation;
|
|
849
|
+
bestDiff = diff;
|
|
850
|
+
}
|
|
851
|
+
return bestEvaluation ? {
|
|
852
|
+
evaluation: bestEvaluation,
|
|
853
|
+
timestampDiffMs: bestDiff
|
|
854
|
+
} : null;
|
|
855
|
+
};
|
|
856
|
+
var findNearestBacktestEntryForRuntimeOnly = ({
|
|
857
|
+
entry,
|
|
858
|
+
backtestEntries
|
|
859
|
+
}) => {
|
|
860
|
+
let bestEntry = null;
|
|
861
|
+
let bestDiff = Number.POSITIVE_INFINITY;
|
|
862
|
+
for (const backtestEntry of backtestEntries) {
|
|
863
|
+
if (backtestEntry.strategy !== entry.strategy || backtestEntry.symbol !== entry.symbol || backtestEntry.direction !== entry.direction) {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
const diff = Math.abs(backtestEntry.timestamp - entry.timestamp);
|
|
867
|
+
if (diff >= bestDiff) {
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
bestEntry = backtestEntry;
|
|
871
|
+
bestDiff = diff;
|
|
872
|
+
}
|
|
873
|
+
return bestEntry ? {
|
|
874
|
+
entry: bestEntry,
|
|
875
|
+
timestampDiffMs: bestDiff
|
|
876
|
+
} : null;
|
|
877
|
+
};
|
|
878
|
+
var buildSignalClassificationReason = ({
|
|
879
|
+
signal,
|
|
880
|
+
orderStatus,
|
|
881
|
+
classification
|
|
882
|
+
}) => {
|
|
883
|
+
const skipReason = typeof signal.orderSkipReason === "string" ? signal.orderSkipReason.trim() : "";
|
|
884
|
+
if (skipReason) {
|
|
885
|
+
return skipReason;
|
|
886
|
+
}
|
|
887
|
+
if (classification === "gated_out" && signal.ml?.passed === false) {
|
|
888
|
+
return `ml_probability=${signal.ml.probability.toFixed(4)} threshold=${signal.ml.threshold.toFixed(4)}`;
|
|
889
|
+
}
|
|
890
|
+
if (orderStatus === "completed") {
|
|
891
|
+
return "completed_signal_without_runtime_trade";
|
|
892
|
+
}
|
|
893
|
+
if (orderStatus !== "unknown") {
|
|
894
|
+
return `orderStatus=${orderStatus}`;
|
|
895
|
+
}
|
|
896
|
+
return "runtime_signal_without_completed_trade";
|
|
897
|
+
};
|
|
898
|
+
var buildEvaluationClassification = ({
|
|
899
|
+
entry,
|
|
900
|
+
evaluation
|
|
901
|
+
}) => {
|
|
902
|
+
if (evaluation.status === "skip") {
|
|
903
|
+
return {
|
|
904
|
+
classification: "core_skipped",
|
|
905
|
+
reason: evaluation.reason || "NO_SIGNAL"
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
if (evaluation.status === "error") {
|
|
909
|
+
return {
|
|
910
|
+
classification: "true_mismatch",
|
|
911
|
+
reason: evaluation.reason || "runtime_evaluation_error"
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
if (evaluation.direction && evaluation.direction !== entry.direction) {
|
|
915
|
+
return {
|
|
916
|
+
classification: "core_skipped",
|
|
917
|
+
reason: `runtime_signal_direction=${evaluation.direction}`
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
const orderStatus = normalizeSignalOrderStatus(evaluation.orderStatus);
|
|
921
|
+
if (orderStatus === "failed") {
|
|
922
|
+
return {
|
|
923
|
+
classification: "order_failed",
|
|
924
|
+
reason: evaluation.reason || "orderStatus=failed"
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
if (orderStatus === "skipped" || orderStatus === "canceled" || typeof evaluation.orderSkipReason === "string" && evaluation.orderSkipReason.trim()) {
|
|
928
|
+
return {
|
|
929
|
+
classification: "gated_out",
|
|
930
|
+
reason: evaluation.orderSkipReason || evaluation.reason || `orderStatus=${orderStatus}`
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
if (orderStatus === "completed") {
|
|
934
|
+
return {
|
|
935
|
+
classification: "true_mismatch",
|
|
936
|
+
reason: "completed_signal_without_runtime_trade"
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
return {
|
|
940
|
+
classification: "true_mismatch",
|
|
941
|
+
reason: evaluation.reason || "runtime_signal_without_completed_trade"
|
|
942
|
+
};
|
|
943
|
+
};
|
|
944
|
+
var buildRuntimeOnlyEvaluationClassification = ({
|
|
945
|
+
entry,
|
|
946
|
+
evaluation
|
|
947
|
+
}) => {
|
|
948
|
+
if (evaluation.status === "skip") {
|
|
949
|
+
return {
|
|
950
|
+
classification: "core_skipped",
|
|
951
|
+
reason: evaluation.reason || "NO_SIGNAL"
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
if (evaluation.status === "error") {
|
|
955
|
+
return {
|
|
956
|
+
classification: "true_mismatch",
|
|
957
|
+
reason: evaluation.reason || "replay_evaluation_error"
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
if (evaluation.direction && evaluation.direction !== entry.direction) {
|
|
961
|
+
return {
|
|
962
|
+
classification: "true_mismatch",
|
|
963
|
+
reason: `replay_signal_direction=${evaluation.direction}`
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
const orderStatus = normalizeSignalOrderStatus(evaluation.orderStatus);
|
|
967
|
+
if (orderStatus === "failed") {
|
|
968
|
+
return {
|
|
969
|
+
classification: "order_failed",
|
|
970
|
+
reason: evaluation.reason || "orderStatus=failed"
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
if (orderStatus === "skipped" || orderStatus === "canceled" || typeof evaluation.orderSkipReason === "string" && evaluation.orderSkipReason.trim()) {
|
|
974
|
+
return {
|
|
975
|
+
classification: "gated_out",
|
|
976
|
+
reason: evaluation.orderSkipReason || evaluation.reason || `orderStatus=${orderStatus}`
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
if (orderStatus === "completed") {
|
|
980
|
+
return {
|
|
981
|
+
classification: "true_mismatch",
|
|
982
|
+
reason: "completed_replay_signal_without_backtest_trade"
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
classification: "true_mismatch",
|
|
987
|
+
reason: evaluation.reason || "runtime_trade_without_replay_trade"
|
|
988
|
+
};
|
|
989
|
+
};
|
|
990
|
+
var classifyBacktestOnlyEntries = ({
|
|
991
|
+
entries,
|
|
992
|
+
runtimeSignals,
|
|
993
|
+
runtimeSignalEvaluations,
|
|
994
|
+
toleranceMs
|
|
995
|
+
}) => entries.map((entry) => {
|
|
996
|
+
const nearestSignal = findNearestRuntimeSignal({
|
|
997
|
+
entry,
|
|
998
|
+
runtimeSignals,
|
|
999
|
+
toleranceMs
|
|
1000
|
+
});
|
|
1001
|
+
if (!nearestSignal) {
|
|
1002
|
+
const nearestEvaluation = findNearestRuntimeSignalEvaluation({
|
|
1003
|
+
entry,
|
|
1004
|
+
runtimeSignalEvaluations,
|
|
1005
|
+
toleranceMs
|
|
1006
|
+
});
|
|
1007
|
+
if (nearestEvaluation) {
|
|
1008
|
+
const evaluationClassification = buildEvaluationClassification({
|
|
1009
|
+
entry,
|
|
1010
|
+
evaluation: nearestEvaluation.evaluation
|
|
1011
|
+
});
|
|
1012
|
+
return {
|
|
1013
|
+
entry,
|
|
1014
|
+
...evaluationClassification,
|
|
1015
|
+
evaluation: nearestEvaluation.evaluation,
|
|
1016
|
+
evaluationTimestampDiffMs: nearestEvaluation.timestampDiffMs
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
entry,
|
|
1021
|
+
classification: "not_evaluated",
|
|
1022
|
+
reason: "no_runtime_evaluation"
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
const orderStatus = normalizeSignalOrderStatus(
|
|
1026
|
+
nearestSignal.signal.orderStatus
|
|
1027
|
+
);
|
|
1028
|
+
let classification;
|
|
1029
|
+
if (orderStatus === "failed") {
|
|
1030
|
+
classification = "order_failed";
|
|
1031
|
+
} else if (orderStatus === "skipped" || orderStatus === "canceled" || nearestSignal.signal.ml?.passed === false || typeof nearestSignal.signal.orderSkipReason === "string" && nearestSignal.signal.orderSkipReason.trim()) {
|
|
1032
|
+
classification = "gated_out";
|
|
1033
|
+
} else {
|
|
1034
|
+
classification = "true_mismatch";
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
entry,
|
|
1038
|
+
classification,
|
|
1039
|
+
reason: buildSignalClassificationReason({
|
|
1040
|
+
signal: nearestSignal.signal,
|
|
1041
|
+
orderStatus,
|
|
1042
|
+
classification
|
|
1043
|
+
}),
|
|
1044
|
+
signal: nearestSignal.signal,
|
|
1045
|
+
signalTimestampDiffMs: nearestSignal.timestampDiffMs
|
|
1046
|
+
};
|
|
1047
|
+
});
|
|
1048
|
+
var classifyRuntimeOnlyEntries = ({
|
|
1049
|
+
entries,
|
|
1050
|
+
replaySignalEvaluations,
|
|
1051
|
+
backtestEntries,
|
|
1052
|
+
toleranceMs
|
|
1053
|
+
}) => entries.map((entry) => {
|
|
1054
|
+
const nearestBacktestEntry = findNearestBacktestEntryForRuntimeOnly({
|
|
1055
|
+
entry,
|
|
1056
|
+
backtestEntries
|
|
1057
|
+
});
|
|
1058
|
+
if (nearestBacktestEntry && nearestBacktestEntry.timestampDiffMs > toleranceMs) {
|
|
1059
|
+
return {
|
|
1060
|
+
entry,
|
|
1061
|
+
classification: "backtest_drift",
|
|
1062
|
+
reason: `nearest_backtest_entry_drift=${formatMinutes(nearestBacktestEntry.timestampDiffMs)}`,
|
|
1063
|
+
nearestBacktestEntry: nearestBacktestEntry.entry,
|
|
1064
|
+
nearestBacktestEntryTimestampDiffMs: nearestBacktestEntry.timestampDiffMs
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
const nearestEvaluation = findNearestRuntimeSignalEvaluation({
|
|
1068
|
+
entry,
|
|
1069
|
+
runtimeSignalEvaluations: replaySignalEvaluations,
|
|
1070
|
+
toleranceMs
|
|
1071
|
+
});
|
|
1072
|
+
if (!nearestEvaluation) {
|
|
1073
|
+
return {
|
|
1074
|
+
entry,
|
|
1075
|
+
classification: "not_evaluated",
|
|
1076
|
+
reason: "no_replay_evaluation"
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
return {
|
|
1080
|
+
entry,
|
|
1081
|
+
...buildRuntimeOnlyEvaluationClassification({
|
|
1082
|
+
entry,
|
|
1083
|
+
evaluation: nearestEvaluation.evaluation
|
|
1084
|
+
}),
|
|
1085
|
+
evaluation: nearestEvaluation.evaluation,
|
|
1086
|
+
evaluationTimestampDiffMs: nearestEvaluation.timestampDiffMs
|
|
1087
|
+
};
|
|
1088
|
+
});
|
|
1089
|
+
var summarizeBacktestOnlyClassifications = (classifiedEntries) => {
|
|
1090
|
+
const counts = new Map(
|
|
1091
|
+
BACKTEST_ONLY_CLASSIFICATIONS.map((classification) => [classification, 0])
|
|
1092
|
+
);
|
|
1093
|
+
for (const item of classifiedEntries) {
|
|
1094
|
+
counts.set(item.classification, (counts.get(item.classification) ?? 0) + 1);
|
|
1095
|
+
}
|
|
1096
|
+
return BACKTEST_ONLY_CLASSIFICATIONS.map(
|
|
1097
|
+
(classification) => `${classification}=${counts.get(classification) ?? 0}`
|
|
1098
|
+
).join(", ");
|
|
1099
|
+
};
|
|
1100
|
+
var summarizeRuntimeOnlyClassifications = (classifiedEntries) => {
|
|
1101
|
+
const counts = new Map(
|
|
1102
|
+
RUNTIME_ONLY_CLASSIFICATIONS.map((classification) => [classification, 0])
|
|
1103
|
+
);
|
|
1104
|
+
for (const item of classifiedEntries) {
|
|
1105
|
+
counts.set(item.classification, (counts.get(item.classification) ?? 0) + 1);
|
|
1106
|
+
}
|
|
1107
|
+
return RUNTIME_ONLY_CLASSIFICATIONS.map(
|
|
1108
|
+
(classification) => `${classification}=${counts.get(classification) ?? 0}`
|
|
1109
|
+
).join(", ");
|
|
1110
|
+
};
|
|
1111
|
+
var printClassifiedBacktestOnlyDetails = (classifiedEntries) => {
|
|
1112
|
+
if (!classifiedEntries.length) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
console.log("");
|
|
1116
|
+
console.log(import_chalk.default.yellow("Backtest only"));
|
|
1117
|
+
for (const item of classifiedEntries.slice(0, DETAIL_LIMIT)) {
|
|
1118
|
+
const evidenceSuffix = item.signal ? ` signalId=${item.signal.signalId} signalDrift=${formatMinutes(item.signalTimestampDiffMs ?? null)}` : item.evaluation ? ` evaluationId=${item.evaluation.evaluationId} evaluationDrift=${formatMinutes(item.evaluationTimestampDiffMs ?? null)}` : "";
|
|
1119
|
+
const price = item.entry.price == null ? "n/a" : item.entry.price.toFixed(6);
|
|
1120
|
+
console.log(
|
|
1121
|
+
`- [${item.classification}] ${formatEntryLabel(item.entry)} price=${price} id=${item.entry.id} reason=${item.reason}${evidenceSuffix}`
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
if (classifiedEntries.length > DETAIL_LIMIT) {
|
|
1125
|
+
console.log(`- ... ${classifiedEntries.length - DETAIL_LIMIT} more`);
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
var printClassifiedRuntimeOnlyDetails = (classifiedEntries) => {
|
|
1129
|
+
if (!classifiedEntries.length) {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
console.log("");
|
|
1133
|
+
console.log(import_chalk.default.yellow("Runtime only"));
|
|
1134
|
+
for (const item of classifiedEntries.slice(0, DETAIL_LIMIT)) {
|
|
1135
|
+
const evidenceSuffix = item.nearestBacktestEntry ? ` nearestBacktestId=${item.nearestBacktestEntry.id} backtestDrift=${formatMinutes(item.nearestBacktestEntryTimestampDiffMs ?? null)}` : item.evaluation ? ` replayEvaluationId=${item.evaluation.evaluationId} replayDrift=${formatMinutes(item.evaluationTimestampDiffMs ?? null)} status=${item.evaluation.status}` : "";
|
|
1136
|
+
const price = item.entry.price == null ? "n/a" : item.entry.price.toFixed(6);
|
|
1137
|
+
console.log(
|
|
1138
|
+
`- [${item.classification}] ${formatEntryLabel(item.entry)} price=${price} id=${item.entry.id} reason=${item.reason}${evidenceSuffix}`
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
if (classifiedEntries.length > DETAIL_LIMIT) {
|
|
1142
|
+
console.log(`- ... ${classifiedEntries.length - DETAIL_LIMIT} more`);
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
var summarizeByStrategy = ({
|
|
1146
|
+
targets,
|
|
1147
|
+
successfulTargetKeys,
|
|
1148
|
+
replayErrors,
|
|
1149
|
+
runtimeEntries,
|
|
1150
|
+
runtimeDuplicateEntries,
|
|
1151
|
+
backtestEntries,
|
|
1152
|
+
matchedEntries,
|
|
1153
|
+
runtimeOnlyEntries,
|
|
1154
|
+
backtestOnlyEntries
|
|
1155
|
+
}) => {
|
|
1156
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1157
|
+
const ensureRow = (strategy) => {
|
|
1158
|
+
const row = rows.get(strategy) ?? {
|
|
1159
|
+
runtime: 0,
|
|
1160
|
+
runtimeDuplicates: 0,
|
|
1161
|
+
backtest: 0,
|
|
1162
|
+
matched: 0,
|
|
1163
|
+
runtimeOnly: 0,
|
|
1164
|
+
backtestOnly: 0,
|
|
1165
|
+
targets: 0,
|
|
1166
|
+
compared: 0,
|
|
1167
|
+
errors: 0
|
|
1168
|
+
};
|
|
1169
|
+
rows.set(strategy, row);
|
|
1170
|
+
return row;
|
|
1171
|
+
};
|
|
1172
|
+
for (const target of targets) {
|
|
1173
|
+
const row = ensureRow(target.strategy);
|
|
1174
|
+
row.targets += 1;
|
|
1175
|
+
if (successfulTargetKeys.has(toTargetKey(target))) {
|
|
1176
|
+
row.compared += 1;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
for (const error of replayErrors) {
|
|
1180
|
+
ensureRow(error.strategy).errors += 1;
|
|
1181
|
+
}
|
|
1182
|
+
for (const entry of runtimeEntries) {
|
|
1183
|
+
ensureRow(entry.strategy).runtime += 1;
|
|
1184
|
+
}
|
|
1185
|
+
for (const entry of runtimeDuplicateEntries) {
|
|
1186
|
+
ensureRow(entry.strategy).runtimeDuplicates += 1;
|
|
1187
|
+
}
|
|
1188
|
+
for (const entry of backtestEntries) {
|
|
1189
|
+
ensureRow(entry.strategy).backtest += 1;
|
|
1190
|
+
}
|
|
1191
|
+
for (const entry of matchedEntries) {
|
|
1192
|
+
ensureRow(entry.runtime.strategy).matched += 1;
|
|
1193
|
+
}
|
|
1194
|
+
for (const entry of runtimeOnlyEntries) {
|
|
1195
|
+
ensureRow(entry.strategy).runtimeOnly += 1;
|
|
1196
|
+
}
|
|
1197
|
+
for (const entry of backtestOnlyEntries) {
|
|
1198
|
+
ensureRow(entry.strategy).backtestOnly += 1;
|
|
1199
|
+
}
|
|
1200
|
+
return [...rows.entries()].sort(
|
|
1201
|
+
([left], [right]) => left.localeCompare(right)
|
|
1202
|
+
);
|
|
1203
|
+
};
|
|
1204
|
+
var buildRuntimeParityMessage = ({
|
|
1205
|
+
window,
|
|
1206
|
+
connectorName,
|
|
1207
|
+
replayEnv,
|
|
1208
|
+
runtimeGatesEnabled,
|
|
1209
|
+
toleranceBars,
|
|
1210
|
+
toleranceMs,
|
|
1211
|
+
replayTargetsCount,
|
|
1212
|
+
comparedTargetsCount,
|
|
1213
|
+
replayErrors,
|
|
1214
|
+
sourceCounts,
|
|
1215
|
+
rawRuntimeEntriesCount,
|
|
1216
|
+
runtimeEntriesCount,
|
|
1217
|
+
runtimeDuplicateEntriesCount,
|
|
1218
|
+
backtestEntriesCount,
|
|
1219
|
+
matchedCount,
|
|
1220
|
+
runtimeOnlyCount,
|
|
1221
|
+
backtestOnlyCount,
|
|
1222
|
+
matchedSummary,
|
|
1223
|
+
classifiedRuntimeOnly,
|
|
1224
|
+
classifiedBacktestOnly,
|
|
1225
|
+
runtimeSignalEvaluationsCount,
|
|
1226
|
+
strategyRows,
|
|
1227
|
+
runtimeGateWarningCounts
|
|
1228
|
+
}) => {
|
|
1229
|
+
const lines = [];
|
|
1230
|
+
lines.push("\u{1F9EA} <b>TradeJS runtime parity</b>");
|
|
1231
|
+
lines.push("");
|
|
1232
|
+
lines.push("\u{1F552} <b>Window</b>");
|
|
1233
|
+
lines.push(
|
|
1234
|
+
`<b>${escapeHtml(formatMskDateTime(window.start))} - ${escapeHtml(formatMskDateTime(window.end))} ${SUMMARY_TIMEZONE_LABEL}</b>`
|
|
1235
|
+
);
|
|
1236
|
+
lines.push("");
|
|
1237
|
+
lines.push(`\u{1F50C} Connector: <b>${escapeHtml(connectorName)}</b>`);
|
|
1238
|
+
lines.push(`\u{1F9EC} Replay env: <b>${escapeHtml(replayEnv)}</b>`);
|
|
1239
|
+
lines.push(
|
|
1240
|
+
`\u{1F6A6} Runtime gates: <b>${runtimeGatesEnabled ? "\u2705 enabled" : "\u26D4 disabled"}</b>`
|
|
1241
|
+
);
|
|
1242
|
+
lines.push(
|
|
1243
|
+
`\u{1F3AF} Tolerance: <b>${toleranceBars} bar(s) / ${(toleranceMs / 6e4).toFixed(0)}m</b>`
|
|
1244
|
+
);
|
|
1245
|
+
lines.push("");
|
|
1246
|
+
lines.push("\u{1F4CC} <b>Overview</b>");
|
|
1247
|
+
lines.push(
|
|
1248
|
+
`\u2022 Targets: <b>${replayTargetsCount}</b> / compared <b>${comparedTargetsCount}</b> / errors <b>${replayErrors.length}</b>`
|
|
1249
|
+
);
|
|
1250
|
+
lines.push(
|
|
1251
|
+
`\u2022 Sources: runtime=<b>${sourceCounts.runtime}</b>, universe=<b>${sourceCounts.connectorUniverse}</b>, explicit=<b>${sourceCounts.explicitTickers}</b>, results=<b>${sourceCounts.strategyResults}</b>`
|
|
1252
|
+
);
|
|
1253
|
+
lines.push("");
|
|
1254
|
+
lines.push("\u{1F4C8} <b>Entries</b>");
|
|
1255
|
+
lines.push(
|
|
1256
|
+
`\u2022 Runtime: <b>${rawRuntimeEntriesCount}</b> (deduped <b>${runtimeEntriesCount}</b>, dup <b>${runtimeDuplicateEntriesCount}</b>)`
|
|
1257
|
+
);
|
|
1258
|
+
lines.push(`\u2022 Backtest: <b>${backtestEntriesCount}</b>`);
|
|
1259
|
+
lines.push(`\u2022 Matched: <b>${matchedCount}</b>`);
|
|
1260
|
+
lines.push(
|
|
1261
|
+
`\u2022 Runtime only: <b>${runtimeOnlyCount}</b> / Backtest only: <b>${backtestOnlyCount}</b>`
|
|
1262
|
+
);
|
|
1263
|
+
lines.push(
|
|
1264
|
+
`\u2022 Deltas: price <b>${escapeHtml(formatPercent(matchedSummary.avgPriceDeltaPct))} / ${escapeHtml(formatPercent(matchedSummary.maxPriceDeltaPct))}</b>, drift <b>${escapeHtml(formatMinutes(matchedSummary.avgTimestampDiffMs))} / ${escapeHtml(formatMinutes(matchedSummary.maxTimestampDiffMs))}</b>`
|
|
1265
|
+
);
|
|
1266
|
+
if (classifiedRuntimeOnly.length) {
|
|
1267
|
+
lines.push(
|
|
1268
|
+
`\u2022 Runtime-only classes: <code>${escapeHtml(summarizeRuntimeOnlyClassifications(classifiedRuntimeOnly))}</code>`
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
if (classifiedBacktestOnly.length) {
|
|
1272
|
+
lines.push(
|
|
1273
|
+
`\u2022 Backtest-only classes: <code>${escapeHtml(summarizeBacktestOnlyClassifications(classifiedBacktestOnly))}</code>`
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
if (runtimeSignalEvaluationsCount) {
|
|
1277
|
+
lines.push(
|
|
1278
|
+
`\u2022 Runtime evaluations: <b>${runtimeSignalEvaluationsCount}</b>`
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
if (strategyRows.length) {
|
|
1282
|
+
lines.push("");
|
|
1283
|
+
lines.push("\u{1F4CA} <b>By strategy</b>");
|
|
1284
|
+
for (const [strategy, row] of strategyRows) {
|
|
1285
|
+
lines.push("");
|
|
1286
|
+
lines.push(`<b>${escapeHtml(strategy)}</b>`);
|
|
1287
|
+
lines.push(
|
|
1288
|
+
`\u2022 targets=<b>${row.targets}</b>, compared=<b>${row.compared}</b>, errors=<b>${row.errors}</b>`
|
|
1289
|
+
);
|
|
1290
|
+
lines.push(
|
|
1291
|
+
`\u2022 runtime=<b>${row.runtime}</b> (dup <b>${row.runtimeDuplicates}</b>), backtest=<b>${row.backtest}</b>, matched=<b>${row.matched}</b>`
|
|
1292
|
+
);
|
|
1293
|
+
lines.push(
|
|
1294
|
+
`\u2022 runtimeOnly=<b>${row.runtimeOnly}</b>, backtestOnly=<b>${row.backtestOnly}</b>`
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (runtimeGateWarningCounts.size && !runtimeGatesEnabled) {
|
|
1299
|
+
lines.push("");
|
|
1300
|
+
lines.push("\u26A0\uFE0F <b>Warnings</b>");
|
|
1301
|
+
for (const [strategy, count] of [
|
|
1302
|
+
...runtimeGateWarningCounts.entries()
|
|
1303
|
+
].sort(([left], [right]) => left.localeCompare(right))) {
|
|
1304
|
+
lines.push(
|
|
1305
|
+
`\u2022 <b>${escapeHtml(strategy)}</b>: AI/ML runtime gates configured on <b>${count}</b> target(s); BACKTEST replay covers core execution only.`
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (replayErrors.length) {
|
|
1310
|
+
lines.push("");
|
|
1311
|
+
lines.push("\u274C <b>Replay errors</b>");
|
|
1312
|
+
for (const error of replayErrors.slice(0, TELEGRAM_DETAIL_LIMIT)) {
|
|
1313
|
+
lines.push(
|
|
1314
|
+
`\u2022 <b>${escapeHtml(error.strategy)}</b> ${escapeHtml(error.symbol)}: <code>${escapeHtml(error.message)}</code>`
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
if (replayErrors.length > TELEGRAM_DETAIL_LIMIT) {
|
|
1318
|
+
lines.push(
|
|
1319
|
+
`... <b>${replayErrors.length - TELEGRAM_DETAIL_LIMIT}</b> more`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
return lines.join("\n");
|
|
1324
|
+
};
|
|
1325
|
+
var buildRuntimeParityNoTargetsMessage = ({
|
|
1326
|
+
window,
|
|
1327
|
+
connectorName,
|
|
1328
|
+
replayEnv,
|
|
1329
|
+
runtimeGatesEnabled,
|
|
1330
|
+
userName
|
|
1331
|
+
}) => [
|
|
1332
|
+
"\u{1F9EA} <b>TradeJS runtime parity</b>",
|
|
1333
|
+
"",
|
|
1334
|
+
"\u{1F552} <b>Window</b>",
|
|
1335
|
+
`<b>${escapeHtml(formatMskDateTime(window.start))} - ${escapeHtml(formatMskDateTime(window.end))} ${SUMMARY_TIMEZONE_LABEL}</b>`,
|
|
1336
|
+
"",
|
|
1337
|
+
`\u{1F50C} Connector: <b>${escapeHtml(connectorName)}</b>`,
|
|
1338
|
+
`\u{1F9EC} Replay env: <b>${escapeHtml(replayEnv)}</b>`,
|
|
1339
|
+
`\u{1F6A6} Runtime gates: <b>${runtimeGatesEnabled ? "\u2705 enabled" : "\u26D4 disabled"}</b>`,
|
|
1340
|
+
"",
|
|
1341
|
+
`\u26A0\uFE0F No replay targets found for user <b>${escapeHtml(userName)}</b>.`
|
|
1342
|
+
].join("\n");
|
|
1343
|
+
var runtimeParity = async () => {
|
|
1344
|
+
const window = resolveTimeWindow({
|
|
1345
|
+
days: flags.days ?? DEFAULT_LOOKBACK_DAYS,
|
|
1346
|
+
startTime: flags.startTime,
|
|
1347
|
+
endTime: flags.endTime,
|
|
1348
|
+
defaultStartMs: Date.now() - DEFAULT_LOOKBACK_DAYS * 24 * 60 * 60 * 1e3,
|
|
1349
|
+
defaultEndMs: Date.now()
|
|
1350
|
+
});
|
|
1351
|
+
const toleranceBars = Math.max(
|
|
1352
|
+
0,
|
|
1353
|
+
Number.parseInt(String(flags.toleranceBars ?? 1), 10) || 0
|
|
1354
|
+
);
|
|
1355
|
+
const toleranceMs = toleranceBars * 15 * 60 * 1e3;
|
|
1356
|
+
const connectorName = await resolveParityConnectorName(flags.connector);
|
|
1357
|
+
const requestedSymbols = parseSymbolsFromCLI(String(flags.tickers || ""));
|
|
1358
|
+
const requestedSymbolSet = requestedSymbols.length ? new Set(requestedSymbols) : null;
|
|
1359
|
+
let replayErrors = [];
|
|
1360
|
+
try {
|
|
1361
|
+
const parityConnector = await createConnector({
|
|
1362
|
+
userName: flags.user,
|
|
1363
|
+
connectorName
|
|
1364
|
+
});
|
|
1365
|
+
const [allRuntimeTrades, allRuntimeSignals, allRuntimeSignalEvaluations] = await Promise.all([
|
|
1366
|
+
loadRuntimeTrades(flags.user),
|
|
1367
|
+
loadRuntimeSignals(flags.user),
|
|
1368
|
+
loadRuntimeSignalEvaluations(flags.user)
|
|
1369
|
+
]);
|
|
1370
|
+
const runtimeTrades = allRuntimeTrades.filter(
|
|
1371
|
+
(trade) => trade.entryTimestamp >= window.start && trade.entryTimestamp <= window.end && (!flags.strategy || trade.strategy === flags.strategy)
|
|
1372
|
+
);
|
|
1373
|
+
const runtimeSignals = allRuntimeSignals.filter(
|
|
1374
|
+
(signal) => signal.timestamp >= window.start && signal.timestamp <= window.end && (!flags.strategy || signal.strategy === flags.strategy) && (!requestedSymbolSet || requestedSymbolSet.has(signal.symbol))
|
|
1375
|
+
);
|
|
1376
|
+
const runtimeSignalEvaluations = allRuntimeSignalEvaluations.filter(
|
|
1377
|
+
(evaluation) => evaluation.timestamp >= window.start && evaluation.timestamp <= window.end && (!flags.strategy || evaluation.strategy === flags.strategy) && (!requestedSymbolSet || requestedSymbolSet.has(evaluation.symbol))
|
|
1378
|
+
);
|
|
1379
|
+
const connectorSymbols = requestedSymbols.length ? requestedSymbols : await (0, import_cli2.getTickers)(parityConnector);
|
|
1380
|
+
const replayTargets = await buildReplayTargets({
|
|
1381
|
+
userName: flags.user,
|
|
1382
|
+
runtimeTrades,
|
|
1383
|
+
connectorSymbols,
|
|
1384
|
+
strategyFilter: String(flags.strategy || "").trim() || void 0,
|
|
1385
|
+
explicitSymbols: requestedSymbols,
|
|
1386
|
+
includeConnectorUniverse: Boolean(flags.fullUniverse)
|
|
1387
|
+
});
|
|
1388
|
+
if (!replayTargets.length) {
|
|
1389
|
+
console.log(
|
|
1390
|
+
import_chalk.default.yellow(
|
|
1391
|
+
`No replay targets found for ${flags.user} in ${(0, import_time.formatUnix)(window.start)} -> ${(0, import_time.formatUnix)(window.end)}.`
|
|
1392
|
+
)
|
|
1393
|
+
);
|
|
1394
|
+
if (flags.notify) {
|
|
1395
|
+
await sendTelegramReport(
|
|
1396
|
+
buildRuntimeParityNoTargetsMessage({
|
|
1397
|
+
window,
|
|
1398
|
+
connectorName,
|
|
1399
|
+
replayEnv: REPLAY_ENV,
|
|
1400
|
+
runtimeGatesEnabled: Boolean(flags.runtimeGates),
|
|
1401
|
+
userName: String(flags.user)
|
|
1402
|
+
}),
|
|
1403
|
+
{ userName: flags.user }
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
const sourceCounts = countReplayTargetSources(replayTargets);
|
|
1409
|
+
const preloadStart = (0, import_time.getBacktestPreloadStart)(
|
|
1410
|
+
window.start,
|
|
1411
|
+
import_constants.BACKTEST_PRELOAD_DAYS
|
|
1412
|
+
);
|
|
1413
|
+
const replayStartedAt = Date.now();
|
|
1414
|
+
const replaySymbolsCount = new Set(
|
|
1415
|
+
replayTargets.map((target) => target.symbol)
|
|
1416
|
+
).size;
|
|
1417
|
+
console.log(
|
|
1418
|
+
import_chalk.default.gray(
|
|
1419
|
+
`Replay queue: ${replayTargets.length} target(s), ${replaySymbolsCount} symbol(s), mode=${flags.fullUniverse ? "full-universe" : requestedSymbols.length ? "explicit-tickers" : "runtime+results"}`
|
|
1420
|
+
)
|
|
1421
|
+
);
|
|
1422
|
+
if (!flags.cacheOnly) {
|
|
1423
|
+
await warmReplayHistory({
|
|
1424
|
+
userName: flags.user,
|
|
1425
|
+
connectorName,
|
|
1426
|
+
targets: replayTargets,
|
|
1427
|
+
preloadStart,
|
|
1428
|
+
preloadEnd: window.end
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
const backtestEntries = [];
|
|
1432
|
+
const replaySignalEvaluations = [];
|
|
1433
|
+
const successfulTargetKeys = /* @__PURE__ */ new Set();
|
|
1434
|
+
const runtimeGateWarningCounts = /* @__PURE__ */ new Map();
|
|
1435
|
+
for (const [targetIndex, target] of replayTargets.entries()) {
|
|
1436
|
+
const progressPosition = targetIndex + 1;
|
|
1437
|
+
try {
|
|
1438
|
+
const replayConfig = await buildReplayConfig({
|
|
1439
|
+
userName: flags.user,
|
|
1440
|
+
strategy: target.strategy,
|
|
1441
|
+
symbol: target.symbol,
|
|
1442
|
+
runtimeTrades,
|
|
1443
|
+
runtimeSignalEvaluations,
|
|
1444
|
+
toleranceMs
|
|
1445
|
+
});
|
|
1446
|
+
if (replayConfig.AI_ENABLED === true || replayConfig.ML_ENABLED === true) {
|
|
1447
|
+
runtimeGateWarningCounts.set(
|
|
1448
|
+
target.strategy,
|
|
1449
|
+
(runtimeGateWarningCounts.get(target.strategy) ?? 0) + 1
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
const result = await (0, import_backtest.testing)({
|
|
1453
|
+
userName: flags.user,
|
|
1454
|
+
symbol: target.symbol,
|
|
1455
|
+
options: {
|
|
1456
|
+
start: window.start,
|
|
1457
|
+
end: window.end
|
|
1458
|
+
},
|
|
1459
|
+
name: `${target.symbol}_${target.strategy}_${(0, import_node_crypto.randomUUID)().slice(0, 8)}`,
|
|
1460
|
+
testId: (0, import_node_crypto.randomUUID)().slice(0, 8),
|
|
1461
|
+
testSuiteId: (0, import_node_crypto.randomUUID)().slice(0, 8),
|
|
1462
|
+
strategyName: target.strategy,
|
|
1463
|
+
strategyConfig: replayConfig,
|
|
1464
|
+
connectorName,
|
|
1465
|
+
timeoutMs: 12e4
|
|
1466
|
+
});
|
|
1467
|
+
backtestEntries.push(
|
|
1468
|
+
...extractBacktestEntryParityEntries(result?.inlineOrderLog)
|
|
1469
|
+
);
|
|
1470
|
+
replaySignalEvaluations.push(
|
|
1471
|
+
...result?.inlineReplaySignalEvaluations ?? []
|
|
1472
|
+
);
|
|
1473
|
+
successfulTargetKeys.add(toTargetKey(target));
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
replayErrors.push({
|
|
1476
|
+
...target,
|
|
1477
|
+
message: error?.message || String(error)
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
if (progressPosition === 1 || progressPosition === replayTargets.length || progressPosition % 25 === 0) {
|
|
1481
|
+
console.log(
|
|
1482
|
+
import_chalk.default.gray(
|
|
1483
|
+
`Replay progress: ${progressPosition}/${replayTargets.length}, ok=${successfulTargetKeys.size}, errors=${replayErrors.length}, current=${target.strategy} ${target.symbol}, elapsed=${formatDuration(replayStartedAt)}`
|
|
1484
|
+
)
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
const comparableRuntimeTrades = runtimeTrades.filter(
|
|
1489
|
+
(trade) => successfulTargetKeys.has(toTargetKey(trade))
|
|
1490
|
+
);
|
|
1491
|
+
const rawRuntimeEntries = extractRuntimeParityEntries(
|
|
1492
|
+
comparableRuntimeTrades
|
|
1493
|
+
);
|
|
1494
|
+
const runtimeDedupe = dedupeRuntimeParityEntries(rawRuntimeEntries);
|
|
1495
|
+
const runtimeEntries = runtimeDedupe.entries;
|
|
1496
|
+
const comparison = compareTradeParityEntries({
|
|
1497
|
+
runtimeEntries,
|
|
1498
|
+
backtestEntries,
|
|
1499
|
+
toleranceMs
|
|
1500
|
+
});
|
|
1501
|
+
const classifiedBacktestOnly = classifyBacktestOnlyEntries({
|
|
1502
|
+
entries: comparison.backtestOnly,
|
|
1503
|
+
runtimeSignals,
|
|
1504
|
+
runtimeSignalEvaluations,
|
|
1505
|
+
toleranceMs
|
|
1506
|
+
});
|
|
1507
|
+
const classifiedRuntimeOnly = classifyRuntimeOnlyEntries({
|
|
1508
|
+
entries: comparison.runtimeOnly,
|
|
1509
|
+
replaySignalEvaluations,
|
|
1510
|
+
backtestEntries,
|
|
1511
|
+
toleranceMs
|
|
1512
|
+
});
|
|
1513
|
+
const summary = summarizeMatchedParity(comparison.matched);
|
|
1514
|
+
console.log(import_chalk.default.cyan("TradeJS runtime parity"));
|
|
1515
|
+
console.log(
|
|
1516
|
+
`Window: ${(0, import_time.formatUnix)(window.start)} -> ${(0, import_time.formatUnix)(window.end)} (${window.source})`
|
|
1517
|
+
);
|
|
1518
|
+
console.log(`Connector: ${connectorName}`);
|
|
1519
|
+
console.log(
|
|
1520
|
+
`Replay env: ${REPLAY_ENV}${flags.runtimeGates ? " (runtime AI/ML gates enabled)" : " (core/backtest gates only)"}`
|
|
1521
|
+
);
|
|
1522
|
+
console.log(
|
|
1523
|
+
`Tolerance: ${toleranceBars} bar(s) / ${(toleranceMs / 6e4).toFixed(0)}m`
|
|
1524
|
+
);
|
|
1525
|
+
console.log(
|
|
1526
|
+
`Targets: ${replayTargets.length}, compared: ${successfulTargetKeys.size}, replayErrors: ${replayErrors.length}`
|
|
1527
|
+
);
|
|
1528
|
+
console.log(
|
|
1529
|
+
`Target sources: runtime=${sourceCounts.runtime}, connectorUniverse=${sourceCounts.connectorUniverse}, explicitTickers=${sourceCounts.explicitTickers}, strategyResults=${sourceCounts.strategyResults}`
|
|
1530
|
+
);
|
|
1531
|
+
console.log("");
|
|
1532
|
+
console.log(
|
|
1533
|
+
`Runtime entries: ${rawRuntimeEntries.length} (deduped: ${runtimeEntries.length}, duplicates: ${runtimeDedupe.duplicateEntries.length}), backtest entries: ${backtestEntries.length}, matched: ${comparison.matched.length}, runtimeOnly: ${comparison.runtimeOnly.length}, backtestOnly: ${comparison.backtestOnly.length}`
|
|
1534
|
+
);
|
|
1535
|
+
console.log(
|
|
1536
|
+
`Matched price delta avg/max: ${formatPercent(summary.avgPriceDeltaPct)} / ${formatPercent(summary.maxPriceDeltaPct)}`
|
|
1537
|
+
);
|
|
1538
|
+
console.log(
|
|
1539
|
+
`Matched timestamp drift avg/max: ${formatMinutes(summary.avgTimestampDiffMs)} / ${formatMinutes(summary.maxTimestampDiffMs)}`
|
|
1540
|
+
);
|
|
1541
|
+
if (runtimeDedupe.duplicateGroups.length) {
|
|
1542
|
+
console.log(
|
|
1543
|
+
`Runtime duplicate groups: ${runtimeDedupe.duplicateGroups.length}, duplicate entries: ${runtimeDedupe.duplicateEntries.length}`
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
if (classifiedRuntimeOnly.length) {
|
|
1547
|
+
console.log(
|
|
1548
|
+
`Runtime only classifications: ${summarizeRuntimeOnlyClassifications(classifiedRuntimeOnly)}`
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
if (classifiedBacktestOnly.length) {
|
|
1552
|
+
console.log(
|
|
1553
|
+
`Backtest only classifications: ${summarizeBacktestOnlyClassifications(classifiedBacktestOnly)}`
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
if (runtimeSignalEvaluations.length) {
|
|
1557
|
+
console.log(`Runtime evaluations: ${runtimeSignalEvaluations.length}`);
|
|
1558
|
+
}
|
|
1559
|
+
const strategyRows = summarizeByStrategy({
|
|
1560
|
+
targets: replayTargets,
|
|
1561
|
+
successfulTargetKeys,
|
|
1562
|
+
replayErrors,
|
|
1563
|
+
runtimeEntries,
|
|
1564
|
+
runtimeDuplicateEntries: runtimeDedupe.duplicateEntries,
|
|
1565
|
+
backtestEntries,
|
|
1566
|
+
matchedEntries: comparison.matched,
|
|
1567
|
+
runtimeOnlyEntries: comparison.runtimeOnly,
|
|
1568
|
+
backtestOnlyEntries: comparison.backtestOnly
|
|
1569
|
+
});
|
|
1570
|
+
if (strategyRows.length) {
|
|
1571
|
+
console.log("");
|
|
1572
|
+
console.log(import_chalk.default.cyan("By strategy"));
|
|
1573
|
+
for (const [strategy, row] of strategyRows) {
|
|
1574
|
+
console.log(
|
|
1575
|
+
`- ${strategy}: targets=${row.targets}, compared=${row.compared}, errors=${row.errors}, runtime=${row.runtime}, runtimeDuplicates=${row.runtimeDuplicates}, backtest=${row.backtest}, matched=${row.matched}, runtimeOnly=${row.runtimeOnly}, backtestOnly=${row.backtestOnly}`
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
if (runtimeGateWarningCounts.size && !flags.runtimeGates) {
|
|
1580
|
+
console.log("");
|
|
1581
|
+
console.log(import_chalk.default.yellow("Warnings"));
|
|
1582
|
+
for (const [strategy, count] of [
|
|
1583
|
+
...runtimeGateWarningCounts.entries()
|
|
1584
|
+
].sort(([left], [right]) => left.localeCompare(right))) {
|
|
1585
|
+
console.log(
|
|
1586
|
+
`- ${strategy} uses AI/ML runtime gates on ${count} replay target(s); BACKTEST replay covers core execution, not live gating.`
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
if (replayErrors.length) {
|
|
1591
|
+
console.log("");
|
|
1592
|
+
console.log(import_chalk.default.red("Replay errors"));
|
|
1593
|
+
for (const error of replayErrors.slice(0, DETAIL_LIMIT)) {
|
|
1594
|
+
console.log(`- ${error.strategy} ${error.symbol}: ${error.message}`);
|
|
1595
|
+
}
|
|
1596
|
+
if (replayErrors.length > DETAIL_LIMIT) {
|
|
1597
|
+
console.log(`- ... ${replayErrors.length - DETAIL_LIMIT} more`);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if (flags.details) {
|
|
1601
|
+
printRuntimeDuplicateDetails(runtimeDedupe.duplicateGroups);
|
|
1602
|
+
printClassifiedRuntimeOnlyDetails(classifiedRuntimeOnly);
|
|
1603
|
+
printClassifiedBacktestOnlyDetails(classifiedBacktestOnly);
|
|
1604
|
+
}
|
|
1605
|
+
if (flags.notify) {
|
|
1606
|
+
await sendTelegramReport(
|
|
1607
|
+
buildRuntimeParityMessage({
|
|
1608
|
+
window,
|
|
1609
|
+
connectorName,
|
|
1610
|
+
replayEnv: REPLAY_ENV,
|
|
1611
|
+
runtimeGatesEnabled: Boolean(flags.runtimeGates),
|
|
1612
|
+
toleranceBars,
|
|
1613
|
+
toleranceMs,
|
|
1614
|
+
replayTargetsCount: replayTargets.length,
|
|
1615
|
+
comparedTargetsCount: successfulTargetKeys.size,
|
|
1616
|
+
replayErrors,
|
|
1617
|
+
sourceCounts,
|
|
1618
|
+
rawRuntimeEntriesCount: rawRuntimeEntries.length,
|
|
1619
|
+
runtimeEntriesCount: runtimeEntries.length,
|
|
1620
|
+
runtimeDuplicateEntriesCount: runtimeDedupe.duplicateEntries.length,
|
|
1621
|
+
backtestEntriesCount: backtestEntries.length,
|
|
1622
|
+
matchedCount: comparison.matched.length,
|
|
1623
|
+
runtimeOnlyCount: comparison.runtimeOnly.length,
|
|
1624
|
+
backtestOnlyCount: comparison.backtestOnly.length,
|
|
1625
|
+
matchedSummary: summary,
|
|
1626
|
+
classifiedRuntimeOnly,
|
|
1627
|
+
classifiedBacktestOnly,
|
|
1628
|
+
runtimeSignalEvaluationsCount: runtimeSignalEvaluations.length,
|
|
1629
|
+
strategyRows,
|
|
1630
|
+
runtimeGateWarningCounts
|
|
1631
|
+
}),
|
|
1632
|
+
{ userName: flags.user }
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
} finally {
|
|
1636
|
+
(0, import_backtest.resetTestingKlineCache)(projectRoot);
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
if (process.env.NODE_ENV !== "test") {
|
|
1640
|
+
runtimeParity().then(() => {
|
|
1641
|
+
process.exit(0);
|
|
1642
|
+
}).catch((error) => {
|
|
1643
|
+
console.error(error);
|
|
1644
|
+
process.exit(1);
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1648
|
+
0 && (module.exports = {
|
|
1649
|
+
buildRuntimeParityMessage,
|
|
1650
|
+
runtimeParity
|
|
1651
|
+
});
|