@tradejs/cli 1.0.9 → 1.0.11
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 +13 -4
- package/dist/cli.js +29531 -13520
- package/dist/lib/aiPocketSearch.js +1316 -0
- package/dist/lib/aiTrainCharts.js +711 -0
- package/dist/lib/aiTrainDataset.js +40 -0
- package/dist/lib/aiTrainEvaluationDump.js +76 -0
- package/dist/lib/aiTrainMetrics.js +148 -8
- package/dist/lib/aiTrainOptions.js +130 -0
- package/dist/lib/aiTrainQuarantine.js +199 -0
- package/dist/lib/aiTrainResearch.js +467 -0
- package/dist/lib/binanceMarketContextBackfill.js +841 -0
- package/dist/lib/binanceMarketData.js +392 -0
- package/dist/lib/coinMarketCapContextBackfill.js +1126 -0
- package/dist/lib/continuity.js +60 -0
- package/dist/lib/derivativesContextBackfill.js +490 -77
- package/dist/lib/executionCalibration.js +705 -0
- package/dist/lib/marketContextPrepare.js +4441 -0
- package/dist/lib/paritySummary.js +101 -0
- package/dist/lib/runBot.js +1 -0
- package/dist/lib/runEnvironment.js +2259 -0
- package/dist/lib/runFormatting.js +1657 -0
- package/dist/lib/runtimeDebugEvidence.js +526 -0
- package/dist/lib/runtimeModeConfig.js +53 -0
- package/dist/lib/runtimeParity.js +88 -11
- package/dist/lib/runtimeParityDetails.js +751 -0
- package/dist/lib/runtimeRedis.js +157 -0
- package/dist/lib/runtimeSignalsLoader.js +40 -6
- package/dist/lib/runtimeSignalsStorage.js +45 -22
- package/dist/lib/runtimeStrategyBacktest.js +134 -0
- package/dist/lib/runtimeTradeSync.js +263 -0
- package/dist/lib/telegramReports.js +9 -0
- package/dist/lib/tickerUniverseCache.js +116 -0
- package/dist/lib/timeWindow.js +3 -3
- package/dist/scripts/agentRun.js +14 -8
- package/dist/scripts/aiExport.js +1721 -7
- package/dist/scripts/aiExportSelect.js +16 -4
- package/dist/scripts/aiPocketSearch.js +4190 -0
- package/dist/scripts/aiTrain.js +1866 -87
- package/dist/scripts/backtest.js +4534 -803
- package/dist/scripts/binanceMarketIngest.js +565 -0
- package/dist/scripts/bot.js +16 -2
- package/dist/scripts/candlesMigrateProvider.js +32 -22
- package/dist/scripts/cleanDir.js +15 -2
- package/dist/scripts/cleanRedis.js +15 -2
- package/dist/scripts/cleanTests.js +38 -2
- package/dist/scripts/cleanupMarketContext.js +69 -0
- package/dist/scripts/continuity.js +148 -67
- package/dist/scripts/derivativesIngest.js +14 -4
- package/dist/scripts/derivativesIngestCoinalyzeAll.js +14 -4
- package/dist/scripts/doctor.js +14 -4
- package/dist/scripts/executionCalibration.js +792 -0
- package/dist/scripts/infraCommon.js +18 -5
- package/dist/scripts/infraDown.js +14 -4
- package/dist/scripts/infraInit.js +32 -9
- package/dist/scripts/infraUp.js +24 -6
- package/dist/scripts/marketWs.js +222 -0
- package/dist/scripts/migration.js +15 -2
- package/dist/scripts/mlExport.js +1698 -10
- package/dist/scripts/mlExportSelect.js +16 -4
- package/dist/scripts/mlInspect.js +13 -3
- package/dist/scripts/mlTrainLatestSelect.js +14 -4
- package/dist/scripts/replay.js +8867 -0
- package/dist/scripts/replayRunner.js +8870 -0
- package/dist/scripts/replayRuntimeEvidence.js +868 -0
- package/dist/scripts/researchAuto.js +286 -59
- package/dist/scripts/results.js +3 -3
- package/dist/scripts/runtimeEvidence.js +605 -0
- package/dist/scripts/runtimeParity.js +6400 -1043
- package/dist/scripts/serverHealth.js +588 -0
- package/dist/scripts/signals.js +5628 -511
- package/dist/scripts/signalsDaemon.js +6205 -0
- package/dist/scripts/signalsSummary.js +833 -248
- package/dist/scripts/test-ml.js +25 -3
- package/dist/scripts/test.js +26 -1
- package/dist/scripts/user-add.js +15 -2
- package/dist/workers/testerWorker.js +130 -21
- package/package.json +18 -12
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/lib/executionCalibration.ts
|
|
21
|
+
var executionCalibration_exports = {};
|
|
22
|
+
__export(executionCalibration_exports, {
|
|
23
|
+
buildExecutionCalibrationReport: () => buildExecutionCalibrationReport
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(executionCalibration_exports);
|
|
26
|
+
var import_constants = require("@tradejs/core/constants");
|
|
27
|
+
var import_data = require("@tradejs/core/data");
|
|
28
|
+
var import_trade = require("@tradejs/core/trade");
|
|
29
|
+
var roundMetric = (value, decimals = 6) => typeof value === "number" && Number.isFinite(value) ? Number(value.toFixed(decimals)) : null;
|
|
30
|
+
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
31
|
+
var asArray = (value) => Array.isArray(value) ? value : [];
|
|
32
|
+
var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
33
|
+
var finiteString = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
|
|
34
|
+
var finiteDirection = (value) => value === "LONG" || value === "SHORT" ? value : null;
|
|
35
|
+
var metricValues = (samples, key) => samples.map((sample) => sample[key]).filter(
|
|
36
|
+
(value) => typeof value === "number" && Number.isFinite(value)
|
|
37
|
+
);
|
|
38
|
+
var quantile = (values, probability) => {
|
|
39
|
+
if (!values.length) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
43
|
+
const index = (sorted.length - 1) * probability;
|
|
44
|
+
const lowerIndex = Math.floor(index);
|
|
45
|
+
const upperIndex = Math.ceil(index);
|
|
46
|
+
const lower = sorted[lowerIndex];
|
|
47
|
+
const upper = sorted[upperIndex];
|
|
48
|
+
if (lower == null || upper == null) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (lowerIndex === upperIndex) {
|
|
52
|
+
return lower;
|
|
53
|
+
}
|
|
54
|
+
return lower + (upper - lower) * (index - lowerIndex);
|
|
55
|
+
};
|
|
56
|
+
var summarizeNumbers = (values) => {
|
|
57
|
+
const finite = values.filter((value) => Number.isFinite(value));
|
|
58
|
+
if (!finite.length) {
|
|
59
|
+
return {
|
|
60
|
+
count: 0,
|
|
61
|
+
min: null,
|
|
62
|
+
max: null,
|
|
63
|
+
avg: null,
|
|
64
|
+
p50: null,
|
|
65
|
+
p75: null,
|
|
66
|
+
p90: null,
|
|
67
|
+
p95: null
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const total = finite.reduce((sum, value) => sum + value, 0);
|
|
71
|
+
return {
|
|
72
|
+
count: finite.length,
|
|
73
|
+
min: roundMetric(Math.min(...finite)),
|
|
74
|
+
max: roundMetric(Math.max(...finite)),
|
|
75
|
+
avg: roundMetric(total / finite.length),
|
|
76
|
+
p50: roundMetric(quantile(finite, 0.5)),
|
|
77
|
+
p75: roundMetric(quantile(finite, 0.75)),
|
|
78
|
+
p90: roundMetric(quantile(finite, 0.9)),
|
|
79
|
+
p95: roundMetric(quantile(finite, 0.95))
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
var getIntervalMs = (interval) => {
|
|
83
|
+
if (!interval) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return (0, import_data.intervalToMs)(interval);
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
var getSignalKey = ({
|
|
93
|
+
strategy,
|
|
94
|
+
symbol,
|
|
95
|
+
timestamp
|
|
96
|
+
}) => strategy && symbol && timestamp != null ? `${strategy}:${symbol}:${timestamp}` : null;
|
|
97
|
+
var addSignalToMap = (signalsById, signalsByShape, signal) => {
|
|
98
|
+
if (!signal) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const signalId = finiteString(signal.signalId);
|
|
102
|
+
if (signalId) {
|
|
103
|
+
signalsById.set(signalId, signal);
|
|
104
|
+
}
|
|
105
|
+
const shapeKey = getSignalKey({
|
|
106
|
+
strategy: finiteString(signal.strategy),
|
|
107
|
+
symbol: finiteString(signal.symbol),
|
|
108
|
+
timestamp: finiteNumber(signal.timestamp)
|
|
109
|
+
});
|
|
110
|
+
if (shapeKey) {
|
|
111
|
+
signalsByShape.set(shapeKey, signal);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
var getRuntimePayload = ({
|
|
115
|
+
runtimeArtifact,
|
|
116
|
+
replayEvidenceArtifact
|
|
117
|
+
}) => {
|
|
118
|
+
const explicitRuntime = asRecord(runtimeArtifact);
|
|
119
|
+
if (explicitRuntime) {
|
|
120
|
+
return asRecord(explicitRuntime.runtime) ?? explicitRuntime;
|
|
121
|
+
}
|
|
122
|
+
const replayEvidence = asRecord(replayEvidenceArtifact);
|
|
123
|
+
return asRecord(replayEvidence?.runtime);
|
|
124
|
+
};
|
|
125
|
+
var extractRuntimeTradeRows = (runtimePayload) => {
|
|
126
|
+
if (!runtimePayload) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
const signalsById = /* @__PURE__ */ new Map();
|
|
130
|
+
const signalsByShape = /* @__PURE__ */ new Map();
|
|
131
|
+
for (const signalRow of asArray(runtimePayload.signals)) {
|
|
132
|
+
const record = asRecord(signalRow);
|
|
133
|
+
addSignalToMap(
|
|
134
|
+
signalsById,
|
|
135
|
+
signalsByShape,
|
|
136
|
+
asRecord(record?.signal) ?? record
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return asArray(runtimePayload.trades).map((row) => {
|
|
140
|
+
const record = asRecord(row);
|
|
141
|
+
if (!record) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const redisValues = asRecord(record.redisValues);
|
|
145
|
+
const trade = asRecord(record.trade) ?? asRecord(redisValues?.trade) ?? record;
|
|
146
|
+
const signalId = finiteString(trade.signalId);
|
|
147
|
+
const shapeKey = getSignalKey({
|
|
148
|
+
strategy: finiteString(trade.strategy),
|
|
149
|
+
symbol: finiteString(trade.symbol),
|
|
150
|
+
timestamp: finiteNumber(trade.signalTimestamp ?? trade.entryTimestamp)
|
|
151
|
+
});
|
|
152
|
+
const signal = asRecord(redisValues?.signal) ?? (signalId ? signalsById.get(signalId) ?? null : null) ?? (shapeKey ? signalsByShape.get(shapeKey) ?? null : null);
|
|
153
|
+
return {
|
|
154
|
+
trade,
|
|
155
|
+
signal
|
|
156
|
+
};
|
|
157
|
+
}).filter((row) => row != null);
|
|
158
|
+
};
|
|
159
|
+
var normalizeReplayMatch = (value) => {
|
|
160
|
+
const item = asRecord(value);
|
|
161
|
+
if (!item) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const runtime = asRecord(item.runtime);
|
|
165
|
+
const backtest = asRecord(item.backtest);
|
|
166
|
+
return {
|
|
167
|
+
orderId: finiteString(
|
|
168
|
+
item.orderId ?? runtime?.orderId ?? backtest?.orderId
|
|
169
|
+
),
|
|
170
|
+
orderLinkId: finiteString(item.orderLinkId ?? runtime?.orderLinkId),
|
|
171
|
+
signalId: finiteString(item.signalId ?? backtest?.signalId),
|
|
172
|
+
strategy: finiteString(
|
|
173
|
+
item.strategy ?? backtest?.strategy ?? runtime?.inferredStrategy
|
|
174
|
+
),
|
|
175
|
+
symbol: finiteString(item.symbol ?? backtest?.symbol ?? runtime?.symbol),
|
|
176
|
+
direction: finiteDirection(
|
|
177
|
+
item.direction ?? backtest?.direction ?? runtime?.direction
|
|
178
|
+
),
|
|
179
|
+
backtestPrice: finiteNumber(item.backtestPrice ?? backtest?.price),
|
|
180
|
+
runtimePrice: finiteNumber(item.runtimePrice ?? runtime?.price)
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
var extractReplayMatches = (replayEvidenceArtifact) => {
|
|
184
|
+
const artifact = asRecord(replayEvidenceArtifact);
|
|
185
|
+
if (!artifact) {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
const replay = asRecord(artifact.replay) ?? artifact;
|
|
189
|
+
const runtimeComparison = asRecord(replay.runtimeComparison);
|
|
190
|
+
const details = asRecord(runtimeComparison?.details);
|
|
191
|
+
const matched = asArray(runtimeComparison?.matched).length ? asArray(runtimeComparison?.matched) : asArray(details?.matched);
|
|
192
|
+
return matched.map(normalizeReplayMatch).filter((match) => match != null);
|
|
193
|
+
};
|
|
194
|
+
var buildReplayMatchMaps = (matches) => {
|
|
195
|
+
const maps = {
|
|
196
|
+
byOrderId: /* @__PURE__ */ new Map(),
|
|
197
|
+
bySignalId: /* @__PURE__ */ new Map()
|
|
198
|
+
};
|
|
199
|
+
for (const match of matches) {
|
|
200
|
+
for (const orderId of [match.orderLinkId, match.orderId]) {
|
|
201
|
+
if (orderId) {
|
|
202
|
+
maps.byOrderId.set(orderId, match);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (match.signalId) {
|
|
206
|
+
maps.bySignalId.set(match.signalId, match);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return maps;
|
|
210
|
+
};
|
|
211
|
+
var getSignalCandles = (signal, interval) => {
|
|
212
|
+
const indicators = asRecord(signal?.indicators);
|
|
213
|
+
if (!indicators) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const intervalKey = (() => {
|
|
217
|
+
switch (interval) {
|
|
218
|
+
case "15":
|
|
219
|
+
return "candles15m";
|
|
220
|
+
case "60":
|
|
221
|
+
return "candles1h";
|
|
222
|
+
case "240":
|
|
223
|
+
return "candles4h";
|
|
224
|
+
case "D":
|
|
225
|
+
return "candles1d";
|
|
226
|
+
default:
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
})();
|
|
230
|
+
const keys = [
|
|
231
|
+
intervalKey,
|
|
232
|
+
"candles15m",
|
|
233
|
+
"candles1h",
|
|
234
|
+
"candles4h",
|
|
235
|
+
"candles1d",
|
|
236
|
+
"candles"
|
|
237
|
+
].filter((key) => Boolean(key));
|
|
238
|
+
for (const key of keys) {
|
|
239
|
+
const value = indicators[key];
|
|
240
|
+
if (Array.isArray(value) && value.length > 1) {
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const candle = indicators.candle;
|
|
245
|
+
const prevCandle = indicators.prevCandle;
|
|
246
|
+
return prevCandle && candle ? [prevCandle, candle] : null;
|
|
247
|
+
};
|
|
248
|
+
var adverseEntryBps = ({
|
|
249
|
+
direction,
|
|
250
|
+
expectedPrice,
|
|
251
|
+
actualPrice
|
|
252
|
+
}) => {
|
|
253
|
+
if (!direction || expectedPrice == null || actualPrice == null || expectedPrice <= 0 || actualPrice <= 0) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const value = direction === "LONG" ? (actualPrice / expectedPrice - 1) * 1e4 : (expectedPrice / actualPrice - 1) * 1e4;
|
|
257
|
+
return roundMetric(value);
|
|
258
|
+
};
|
|
259
|
+
var feeBps = ({
|
|
260
|
+
fee,
|
|
261
|
+
fillAvgPrice,
|
|
262
|
+
qty
|
|
263
|
+
}) => {
|
|
264
|
+
const notional = fillAvgPrice != null && qty != null && fillAvgPrice > 0 && qty > 0 ? fillAvgPrice * qty : null;
|
|
265
|
+
return fee != null && notional != null && notional > 0 ? roundMetric(fee / notional * 1e4) : null;
|
|
266
|
+
};
|
|
267
|
+
var latencyDelta = (end, start) => end != null && start != null ? roundMetric(end - start, 3) : null;
|
|
268
|
+
var resolveTelemetryQuality = ({
|
|
269
|
+
signalClosePrice,
|
|
270
|
+
arrivalMid,
|
|
271
|
+
orderSubmitTime,
|
|
272
|
+
orderAckTime,
|
|
273
|
+
fillAvgPrice,
|
|
274
|
+
fillTime
|
|
275
|
+
}) => {
|
|
276
|
+
if (signalClosePrice != null && arrivalMid != null && orderSubmitTime != null && orderAckTime != null && fillAvgPrice != null && fillTime != null) {
|
|
277
|
+
return "full";
|
|
278
|
+
}
|
|
279
|
+
if (fillAvgPrice != null && (arrivalMid != null || orderSubmitTime != null)) {
|
|
280
|
+
return "partial";
|
|
281
|
+
}
|
|
282
|
+
if (fillAvgPrice != null) {
|
|
283
|
+
return "price_only";
|
|
284
|
+
}
|
|
285
|
+
return "none";
|
|
286
|
+
};
|
|
287
|
+
var finiteTelemetryQuality = (value) => {
|
|
288
|
+
const raw = finiteString(value);
|
|
289
|
+
return raw === "full" || raw === "partial" || raw === "price_only" || raw === "none" ? raw : null;
|
|
290
|
+
};
|
|
291
|
+
var countBy = (samples, keyGetter) => {
|
|
292
|
+
const counts = /* @__PURE__ */ new Map();
|
|
293
|
+
for (const sample of samples) {
|
|
294
|
+
const key = keyGetter(sample) ?? "unknown";
|
|
295
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
296
|
+
}
|
|
297
|
+
return Object.fromEntries(
|
|
298
|
+
[...counts.entries()].sort(([left], [right]) => left.localeCompare(right))
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
var toLatencyBucket = (sample) => {
|
|
302
|
+
const latencyMs = sample.signalCloseToSubmitMs ?? sample.signalToSubmitMs;
|
|
303
|
+
if (latencyMs == null || !Number.isFinite(latencyMs)) return "unknown";
|
|
304
|
+
if (latencyMs <= 1e3) return "0-1s";
|
|
305
|
+
if (latencyMs <= 5e3) return "1-5s";
|
|
306
|
+
if (latencyMs <= 3e4) return "5-30s";
|
|
307
|
+
if (latencyMs <= 12e4) return "30-120s";
|
|
308
|
+
return "120s+";
|
|
309
|
+
};
|
|
310
|
+
var toSpreadBucket = (sample) => {
|
|
311
|
+
const spreadBps = sample.spreadBps;
|
|
312
|
+
if (spreadBps == null || !Number.isFinite(spreadBps)) return "unknown";
|
|
313
|
+
if (spreadBps <= 5) return "0-5bps";
|
|
314
|
+
if (spreadBps <= 20) return "5-20bps";
|
|
315
|
+
if (spreadBps <= 50) return "20-50bps";
|
|
316
|
+
return "50bps+";
|
|
317
|
+
};
|
|
318
|
+
var buildSample = ({
|
|
319
|
+
row,
|
|
320
|
+
match
|
|
321
|
+
}) => {
|
|
322
|
+
const { trade, signal } = row;
|
|
323
|
+
const strategy = finiteString(trade.strategy ?? signal?.strategy);
|
|
324
|
+
const symbol = finiteString(trade.symbol ?? signal?.symbol);
|
|
325
|
+
const interval = finiteString(trade.interval ?? signal?.interval);
|
|
326
|
+
const direction = finiteDirection(trade.direction ?? signal?.direction);
|
|
327
|
+
const orderId = finiteString(trade.orderId);
|
|
328
|
+
const signalId = finiteString(trade.signalId ?? signal?.signalId);
|
|
329
|
+
const qty = finiteNumber(trade.qty);
|
|
330
|
+
const signalTimestamp = finiteNumber(
|
|
331
|
+
trade.signalTimestamp ?? signal?.timestamp
|
|
332
|
+
);
|
|
333
|
+
const intervalMs = getIntervalMs(interval);
|
|
334
|
+
const signalCloseTimestamp = signalTimestamp != null && intervalMs != null ? signalTimestamp + intervalMs : signalTimestamp;
|
|
335
|
+
const signalClosePrice = finiteNumber(trade.signalClosePrice);
|
|
336
|
+
const arrivalSnapshotTime = finiteNumber(trade.arrivalSnapshotTime);
|
|
337
|
+
const arrivalSource = finiteString(trade.arrivalSource);
|
|
338
|
+
const arrivalMid = finiteNumber(trade.arrivalMid);
|
|
339
|
+
const bid = finiteNumber(trade.bid);
|
|
340
|
+
const ask = finiteNumber(trade.ask);
|
|
341
|
+
const spreadBps = finiteNumber(trade.spreadBps);
|
|
342
|
+
const orderSubmitTime = finiteNumber(trade.orderSubmitTime);
|
|
343
|
+
const orderAckTime = finiteNumber(trade.orderAckTime);
|
|
344
|
+
const fillAvgPrice = finiteNumber(trade.fillAvgPrice);
|
|
345
|
+
const fillSource = finiteString(trade.fillSource);
|
|
346
|
+
const fillTime = finiteNumber(trade.fillTime);
|
|
347
|
+
const telemetryQuality = finiteTelemetryQuality(trade.telemetryQuality) ?? resolveTelemetryQuality({
|
|
348
|
+
signalClosePrice,
|
|
349
|
+
arrivalMid,
|
|
350
|
+
orderSubmitTime,
|
|
351
|
+
orderAckTime,
|
|
352
|
+
fillAvgPrice,
|
|
353
|
+
fillTime
|
|
354
|
+
});
|
|
355
|
+
const fee = finiteNumber(trade.fee ?? trade.openFee);
|
|
356
|
+
const arrivalSnapshotAgeMs = latencyDelta(
|
|
357
|
+
orderSubmitTime,
|
|
358
|
+
arrivalSnapshotTime
|
|
359
|
+
);
|
|
360
|
+
const signalToSubmitMs = latencyDelta(orderSubmitTime, signalTimestamp);
|
|
361
|
+
const signalCloseToSubmitMs = latencyDelta(
|
|
362
|
+
orderSubmitTime,
|
|
363
|
+
signalCloseTimestamp
|
|
364
|
+
);
|
|
365
|
+
const submitToAckMs = latencyDelta(orderAckTime, orderSubmitTime);
|
|
366
|
+
const submitToFillMs = latencyDelta(fillTime, orderSubmitTime);
|
|
367
|
+
const orderAckToFillMs = latencyDelta(fillTime, orderAckTime);
|
|
368
|
+
const signalToFillMs = latencyDelta(fillTime, signalTimestamp);
|
|
369
|
+
const observedDelayMs = signalCloseToSubmitMs != null && signalCloseToSubmitMs >= 0 ? signalCloseToSubmitMs : signalToSubmitMs != null && signalToSubmitMs >= 0 ? signalToSubmitMs : null;
|
|
370
|
+
const candles = getSignalCandles(signal, interval);
|
|
371
|
+
const currentDelayRiskBps = roundMetric(
|
|
372
|
+
(0, import_trade.calculateDelayRiskBps)({
|
|
373
|
+
candles,
|
|
374
|
+
intervalMs
|
|
375
|
+
})
|
|
376
|
+
);
|
|
377
|
+
const rawConfiguredDelayRiskBps = roundMetric(
|
|
378
|
+
(0, import_trade.calculateDelayRiskBps)({
|
|
379
|
+
candles,
|
|
380
|
+
intervalMs,
|
|
381
|
+
multiplier: 1,
|
|
382
|
+
maxBps: Number.POSITIVE_INFINITY
|
|
383
|
+
})
|
|
384
|
+
);
|
|
385
|
+
const rawObservedDelayRiskBps = roundMetric(
|
|
386
|
+
(0, import_trade.calculateDelayRiskBps)({
|
|
387
|
+
candles,
|
|
388
|
+
intervalMs,
|
|
389
|
+
expectedDelayMs: observedDelayMs,
|
|
390
|
+
multiplier: 1,
|
|
391
|
+
maxBps: Number.POSITIVE_INFINITY
|
|
392
|
+
})
|
|
393
|
+
);
|
|
394
|
+
const signalToArrivalAdverseBps = adverseEntryBps({
|
|
395
|
+
direction,
|
|
396
|
+
expectedPrice: signalClosePrice,
|
|
397
|
+
actualPrice: arrivalMid
|
|
398
|
+
});
|
|
399
|
+
const arrivalToFillAdverseBps = adverseEntryBps({
|
|
400
|
+
direction,
|
|
401
|
+
expectedPrice: arrivalMid,
|
|
402
|
+
actualPrice: fillAvgPrice
|
|
403
|
+
});
|
|
404
|
+
const signalToFillAdverseBps = adverseEntryBps({
|
|
405
|
+
direction,
|
|
406
|
+
expectedPrice: signalClosePrice,
|
|
407
|
+
actualPrice: fillAvgPrice
|
|
408
|
+
});
|
|
409
|
+
const currentModelEntrySlippageBps = signalToFillAdverseBps != null ? roundMetric(
|
|
410
|
+
import_constants.BACKTEST_BASE_SLIPPAGE_BPS + (spreadBps ?? 0) * import_constants.BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER + import_constants.BACKTEST_MARKET_IMPACT_BPS + (currentDelayRiskBps ?? 0)
|
|
411
|
+
) : null;
|
|
412
|
+
const residualVsCurrentModelBps = signalToFillAdverseBps != null && currentModelEntrySlippageBps != null ? roundMetric(signalToFillAdverseBps - currentModelEntrySlippageBps) : null;
|
|
413
|
+
return {
|
|
414
|
+
strategy,
|
|
415
|
+
symbol,
|
|
416
|
+
interval,
|
|
417
|
+
direction,
|
|
418
|
+
orderId,
|
|
419
|
+
signalId,
|
|
420
|
+
qty,
|
|
421
|
+
signalTimestamp,
|
|
422
|
+
signalCloseTimestamp,
|
|
423
|
+
signalClosePrice,
|
|
424
|
+
arrivalSnapshotTime,
|
|
425
|
+
arrivalSource,
|
|
426
|
+
arrivalMid,
|
|
427
|
+
bid,
|
|
428
|
+
ask,
|
|
429
|
+
spreadBps,
|
|
430
|
+
orderSubmitTime,
|
|
431
|
+
orderAckTime,
|
|
432
|
+
fillAvgPrice,
|
|
433
|
+
fillSource,
|
|
434
|
+
fillTime,
|
|
435
|
+
telemetryQuality,
|
|
436
|
+
fee,
|
|
437
|
+
feeBps: feeBps({ fee, fillAvgPrice, qty }),
|
|
438
|
+
arrivalSnapshotAgeMs,
|
|
439
|
+
signalToSubmitMs,
|
|
440
|
+
signalCloseToSubmitMs,
|
|
441
|
+
submitToAckMs,
|
|
442
|
+
submitToFillMs,
|
|
443
|
+
orderAckToFillMs,
|
|
444
|
+
signalToFillMs,
|
|
445
|
+
signalToArrivalAdverseBps,
|
|
446
|
+
arrivalToFillAdverseBps,
|
|
447
|
+
signalToFillAdverseBps,
|
|
448
|
+
currentDelayRiskBps,
|
|
449
|
+
rawConfiguredDelayRiskBps,
|
|
450
|
+
rawObservedDelayRiskBps,
|
|
451
|
+
currentModelEntrySlippageBps,
|
|
452
|
+
residualVsCurrentModelBps,
|
|
453
|
+
replayEntryResidualBps: adverseEntryBps({
|
|
454
|
+
direction: direction ?? match?.direction ?? null,
|
|
455
|
+
expectedPrice: match?.backtestPrice ?? null,
|
|
456
|
+
actualPrice: match?.runtimePrice ?? null
|
|
457
|
+
})
|
|
458
|
+
};
|
|
459
|
+
};
|
|
460
|
+
var buildReplayOnlySample = (match) => ({
|
|
461
|
+
strategy: match.strategy,
|
|
462
|
+
symbol: match.symbol,
|
|
463
|
+
interval: null,
|
|
464
|
+
direction: match.direction,
|
|
465
|
+
orderId: match.orderLinkId ?? match.orderId,
|
|
466
|
+
signalId: match.signalId,
|
|
467
|
+
qty: null,
|
|
468
|
+
signalTimestamp: null,
|
|
469
|
+
signalCloseTimestamp: null,
|
|
470
|
+
signalClosePrice: null,
|
|
471
|
+
arrivalSnapshotTime: null,
|
|
472
|
+
arrivalSource: null,
|
|
473
|
+
arrivalMid: null,
|
|
474
|
+
bid: null,
|
|
475
|
+
ask: null,
|
|
476
|
+
spreadBps: null,
|
|
477
|
+
orderSubmitTime: null,
|
|
478
|
+
orderAckTime: null,
|
|
479
|
+
fillAvgPrice: match.runtimePrice,
|
|
480
|
+
fillSource: "replay_runtime_price",
|
|
481
|
+
fillTime: null,
|
|
482
|
+
telemetryQuality: match.runtimePrice != null ? "price_only" : "none",
|
|
483
|
+
fee: null,
|
|
484
|
+
feeBps: null,
|
|
485
|
+
arrivalSnapshotAgeMs: null,
|
|
486
|
+
signalToSubmitMs: null,
|
|
487
|
+
signalCloseToSubmitMs: null,
|
|
488
|
+
submitToAckMs: null,
|
|
489
|
+
submitToFillMs: null,
|
|
490
|
+
orderAckToFillMs: null,
|
|
491
|
+
signalToFillMs: null,
|
|
492
|
+
signalToArrivalAdverseBps: null,
|
|
493
|
+
arrivalToFillAdverseBps: null,
|
|
494
|
+
signalToFillAdverseBps: null,
|
|
495
|
+
currentDelayRiskBps: null,
|
|
496
|
+
rawConfiguredDelayRiskBps: null,
|
|
497
|
+
rawObservedDelayRiskBps: null,
|
|
498
|
+
currentModelEntrySlippageBps: null,
|
|
499
|
+
residualVsCurrentModelBps: null,
|
|
500
|
+
replayEntryResidualBps: adverseEntryBps({
|
|
501
|
+
direction: match.direction,
|
|
502
|
+
expectedPrice: match.backtestPrice,
|
|
503
|
+
actualPrice: match.runtimePrice
|
|
504
|
+
})
|
|
505
|
+
});
|
|
506
|
+
var hasTelemetry = (sample) => sample.signalClosePrice != null || sample.arrivalMid != null || sample.orderSubmitTime != null || sample.fillAvgPrice != null || sample.fillTime != null || sample.fee != null;
|
|
507
|
+
var hasFullTelemetry = (sample) => sample.signalClosePrice != null && sample.arrivalMid != null && sample.orderSubmitTime != null && sample.orderAckTime != null && sample.fillAvgPrice != null && sample.fillTime != null;
|
|
508
|
+
var buildGroupSummary = (samples) => ({
|
|
509
|
+
trades: samples.length,
|
|
510
|
+
fullTelemetryTrades: samples.filter(hasFullTelemetry).length,
|
|
511
|
+
replayMatchedTrades: metricValues(samples, "replayEntryResidualBps").length,
|
|
512
|
+
telemetryQuality: countBy(samples, (sample) => sample.telemetryQuality),
|
|
513
|
+
fillSource: countBy(samples, (sample) => sample.fillSource),
|
|
514
|
+
arrivalSource: countBy(samples, (sample) => sample.arrivalSource),
|
|
515
|
+
signalToArrivalAdverseBps: summarizeNumbers(
|
|
516
|
+
metricValues(samples, "signalToArrivalAdverseBps")
|
|
517
|
+
),
|
|
518
|
+
arrivalToFillAdverseBps: summarizeNumbers(
|
|
519
|
+
metricValues(samples, "arrivalToFillAdverseBps")
|
|
520
|
+
),
|
|
521
|
+
signalToFillAdverseBps: summarizeNumbers(
|
|
522
|
+
metricValues(samples, "signalToFillAdverseBps")
|
|
523
|
+
),
|
|
524
|
+
residualVsCurrentModelBps: summarizeNumbers(
|
|
525
|
+
metricValues(samples, "residualVsCurrentModelBps")
|
|
526
|
+
),
|
|
527
|
+
replayEntryResidualBps: summarizeNumbers(
|
|
528
|
+
metricValues(samples, "replayEntryResidualBps")
|
|
529
|
+
),
|
|
530
|
+
spreadBps: summarizeNumbers(metricValues(samples, "spreadBps")),
|
|
531
|
+
feeBps: summarizeNumbers(metricValues(samples, "feeBps")),
|
|
532
|
+
arrivalSnapshotAgeMs: summarizeNumbers(
|
|
533
|
+
metricValues(samples, "arrivalSnapshotAgeMs")
|
|
534
|
+
),
|
|
535
|
+
signalToSubmitMs: summarizeNumbers(metricValues(samples, "signalToSubmitMs")),
|
|
536
|
+
signalCloseToSubmitMs: summarizeNumbers(
|
|
537
|
+
metricValues(samples, "signalCloseToSubmitMs")
|
|
538
|
+
),
|
|
539
|
+
submitToAckMs: summarizeNumbers(metricValues(samples, "submitToAckMs")),
|
|
540
|
+
submitToFillMs: summarizeNumbers(metricValues(samples, "submitToFillMs")),
|
|
541
|
+
orderAckToFillMs: summarizeNumbers(metricValues(samples, "orderAckToFillMs")),
|
|
542
|
+
currentDelayRiskBps: summarizeNumbers(
|
|
543
|
+
metricValues(samples, "currentDelayRiskBps")
|
|
544
|
+
),
|
|
545
|
+
rawConfiguredDelayRiskBps: summarizeNumbers(
|
|
546
|
+
metricValues(samples, "rawConfiguredDelayRiskBps")
|
|
547
|
+
),
|
|
548
|
+
rawObservedDelayRiskBps: summarizeNumbers(
|
|
549
|
+
metricValues(samples, "rawObservedDelayRiskBps")
|
|
550
|
+
)
|
|
551
|
+
});
|
|
552
|
+
var groupSamples = (samples, keyGetter) => {
|
|
553
|
+
const groups = /* @__PURE__ */ new Map();
|
|
554
|
+
for (const sample of samples) {
|
|
555
|
+
const key = keyGetter(sample);
|
|
556
|
+
if (!key) {
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
groups.set(key, [...groups.get(key) ?? [], sample]);
|
|
560
|
+
}
|
|
561
|
+
return Object.fromEntries(
|
|
562
|
+
[...groups.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([key, group]) => [key, buildGroupSummary(group)])
|
|
563
|
+
);
|
|
564
|
+
};
|
|
565
|
+
var positiveMetricValues = (samples, key) => metricValues(samples, key).filter((value) => value > 0);
|
|
566
|
+
var buildRecommendation = (samples) => {
|
|
567
|
+
const fullTelemetrySamples = samples.filter(hasFullTelemetry);
|
|
568
|
+
const notes = [];
|
|
569
|
+
if (!fullTelemetrySamples.length) {
|
|
570
|
+
return {
|
|
571
|
+
confidence: "none",
|
|
572
|
+
baseSlippageBps: null,
|
|
573
|
+
spreadMultiplier: null,
|
|
574
|
+
delayRiskMultiplier: null,
|
|
575
|
+
delayRiskMaxBps: null,
|
|
576
|
+
expectedDelayMs: null,
|
|
577
|
+
notes: [
|
|
578
|
+
"No full live execution telemetry was found. Use a newer runtime debug artifact collected after telemetry persistence was enabled.",
|
|
579
|
+
"Replay residuals can still diagnose model drift, but they are not enough to calibrate signal-to-arrival and arrival-to-fill components separately."
|
|
580
|
+
]
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (fullTelemetrySamples.length < 20) {
|
|
584
|
+
notes.push(
|
|
585
|
+
"Sample is small; treat recommendations as diagnostic until at least 20-50 live fills are available."
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
const spreadRatios = fullTelemetrySamples.map((sample) => {
|
|
589
|
+
if (sample.arrivalToFillAdverseBps == null || sample.spreadBps == null || sample.spreadBps <= 0) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
return Math.max(0, sample.arrivalToFillAdverseBps) / sample.spreadBps;
|
|
593
|
+
}).filter(
|
|
594
|
+
(value) => value != null && Number.isFinite(value)
|
|
595
|
+
);
|
|
596
|
+
const spreadMultiplier = roundMetric(quantile(spreadRatios, 0.75), 4);
|
|
597
|
+
const baseResiduals = fullTelemetrySamples.map((sample) => {
|
|
598
|
+
if (sample.arrivalToFillAdverseBps == null) {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
return Math.max(
|
|
602
|
+
0,
|
|
603
|
+
sample.arrivalToFillAdverseBps - (sample.spreadBps ?? 0) * (spreadMultiplier ?? import_constants.BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER)
|
|
604
|
+
);
|
|
605
|
+
}).filter(
|
|
606
|
+
(value) => value != null && Number.isFinite(value)
|
|
607
|
+
);
|
|
608
|
+
const closeDelayMs = positiveMetricValues(
|
|
609
|
+
fullTelemetrySamples,
|
|
610
|
+
"signalCloseToSubmitMs"
|
|
611
|
+
);
|
|
612
|
+
const fallbackDelayMs = positiveMetricValues(
|
|
613
|
+
fullTelemetrySamples,
|
|
614
|
+
"signalToSubmitMs"
|
|
615
|
+
);
|
|
616
|
+
if (!spreadRatios.length) {
|
|
617
|
+
notes.push(
|
|
618
|
+
"Spread multiplier could not be calibrated because spreadBps or arrival-to-fill telemetry is missing."
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
notes.push(
|
|
622
|
+
"Delay risk bps is disabled; signal-to-arrival latency is modeled by next-bar-open backtest fills."
|
|
623
|
+
);
|
|
624
|
+
return {
|
|
625
|
+
confidence: fullTelemetrySamples.length >= 50 ? "high" : fullTelemetrySamples.length >= 20 ? "medium" : "low",
|
|
626
|
+
baseSlippageBps: roundMetric(quantile(baseResiduals, 0.75), 2),
|
|
627
|
+
spreadMultiplier,
|
|
628
|
+
delayRiskMultiplier: null,
|
|
629
|
+
delayRiskMaxBps: null,
|
|
630
|
+
expectedDelayMs: roundMetric(
|
|
631
|
+
quantile(closeDelayMs.length ? closeDelayMs : fallbackDelayMs, 0.5),
|
|
632
|
+
0
|
|
633
|
+
),
|
|
634
|
+
notes
|
|
635
|
+
};
|
|
636
|
+
};
|
|
637
|
+
var buildExecutionCalibrationReport = ({
|
|
638
|
+
runtimeArtifact,
|
|
639
|
+
replayEvidenceArtifact,
|
|
640
|
+
sourcePaths = {},
|
|
641
|
+
nowMs = Date.now()
|
|
642
|
+
}) => {
|
|
643
|
+
const runtimePayload = getRuntimePayload({
|
|
644
|
+
runtimeArtifact,
|
|
645
|
+
replayEvidenceArtifact
|
|
646
|
+
});
|
|
647
|
+
const runtimeRows = extractRuntimeTradeRows(runtimePayload);
|
|
648
|
+
const replayMatches = extractReplayMatches(replayEvidenceArtifact);
|
|
649
|
+
const replayMaps = buildReplayMatchMaps(replayMatches);
|
|
650
|
+
const usedReplayMatches = /* @__PURE__ */ new Set();
|
|
651
|
+
const samples = runtimeRows.map((row) => {
|
|
652
|
+
const orderId = finiteString(row.trade.orderId);
|
|
653
|
+
const signalId = finiteString(row.trade.signalId);
|
|
654
|
+
const match = (orderId ? replayMaps.byOrderId.get(orderId) ?? null : null) ?? (signalId ? replayMaps.bySignalId.get(signalId) ?? null : null);
|
|
655
|
+
if (match) {
|
|
656
|
+
usedReplayMatches.add(match);
|
|
657
|
+
}
|
|
658
|
+
return buildSample({ row, match });
|
|
659
|
+
});
|
|
660
|
+
const replayOnlySamples = replayMatches.filter((match) => !usedReplayMatches.has(match)).map(buildReplayOnlySample);
|
|
661
|
+
return {
|
|
662
|
+
reportType: "execution-calibration",
|
|
663
|
+
generatedAt: nowMs,
|
|
664
|
+
sources: sourcePaths,
|
|
665
|
+
currentModel: {
|
|
666
|
+
baseSlippageBps: import_constants.BACKTEST_BASE_SLIPPAGE_BPS,
|
|
667
|
+
spreadMultiplier: import_constants.BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER,
|
|
668
|
+
marketImpactBps: import_constants.BACKTEST_MARKET_IMPACT_BPS
|
|
669
|
+
},
|
|
670
|
+
counts: {
|
|
671
|
+
runtimeTrades: runtimeRows.length,
|
|
672
|
+
telemetryTrades: samples.filter(hasTelemetry).length,
|
|
673
|
+
fullTelemetryTrades: samples.filter(hasFullTelemetry).length,
|
|
674
|
+
replayMatched: replayMatches.length,
|
|
675
|
+
replayMatchedRuntimeTrades: usedReplayMatches.size,
|
|
676
|
+
replayOnlyMatches: replayOnlySamples.length
|
|
677
|
+
},
|
|
678
|
+
summary: {
|
|
679
|
+
all: buildGroupSummary([...samples, ...replayOnlySamples]),
|
|
680
|
+
byStrategy: groupSamples(
|
|
681
|
+
[...samples, ...replayOnlySamples],
|
|
682
|
+
(sample) => sample.strategy
|
|
683
|
+
),
|
|
684
|
+
bySymbol: groupSamples(samples, (sample) => sample.symbol),
|
|
685
|
+
byInterval: groupSamples(samples, (sample) => sample.interval),
|
|
686
|
+
byTelemetryQuality: groupSamples(
|
|
687
|
+
[...samples, ...replayOnlySamples],
|
|
688
|
+
(sample) => sample.telemetryQuality
|
|
689
|
+
),
|
|
690
|
+
byFillSource: groupSamples(
|
|
691
|
+
[...samples, ...replayOnlySamples],
|
|
692
|
+
(sample) => sample.fillSource
|
|
693
|
+
),
|
|
694
|
+
byLatencyBucket: groupSamples(samples, toLatencyBucket),
|
|
695
|
+
bySpreadBucket: groupSamples(samples, toSpreadBucket)
|
|
696
|
+
},
|
|
697
|
+
recommendation: buildRecommendation(samples),
|
|
698
|
+
samples,
|
|
699
|
+
replayOnlySamples
|
|
700
|
+
};
|
|
701
|
+
};
|
|
702
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
703
|
+
0 && (module.exports = {
|
|
704
|
+
buildExecutionCalibrationReport
|
|
705
|
+
});
|