@tradejs/cli 3.1.29-beta.282 → 3.1.29-beta.283
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2716 -2375
- package/dist/lib/mcpReplayProcess.js +1 -1
- package/dist/lib/runtimeFeedbackProcess.js +1700 -0
- package/dist/scripts/mcpWorker.js +43 -9
- package/dist/scripts/replay.js +173 -47
- package/dist/scripts/replayRunner.js +173 -47
- package/dist/scripts/replayRuntimeEvidence.js +109 -6
- package/dist/scripts/runtimeFeedbackProcess.js +1736 -0
- package/package.json +7 -7
|
@@ -0,0 +1,1700 @@
|
|
|
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 __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/lib/runtimeFeedbackProcess.ts
|
|
32
|
+
var runtimeFeedbackProcess_exports = {};
|
|
33
|
+
__export(runtimeFeedbackProcess_exports, {
|
|
34
|
+
processRuntimeFeedbackPair: () => processRuntimeFeedbackPair
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(runtimeFeedbackProcess_exports);
|
|
37
|
+
var import_promises2 = __toESM(require("fs/promises"));
|
|
38
|
+
var import_node_path2 = __toESM(require("path"));
|
|
39
|
+
var import_node_crypto2 = require("crypto");
|
|
40
|
+
var import_diagnostics = require("@tradejs/node/diagnostics");
|
|
41
|
+
|
|
42
|
+
// src/lib/executionCalibration.ts
|
|
43
|
+
var import_constants = require("@tradejs/core/constants");
|
|
44
|
+
var import_data = require("@tradejs/core/data");
|
|
45
|
+
var import_trade = require("@tradejs/core/trade");
|
|
46
|
+
var roundMetric = (value, decimals = 6) => typeof value === "number" && Number.isFinite(value) ? Number(value.toFixed(decimals)) : null;
|
|
47
|
+
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
48
|
+
var asArray = (value) => Array.isArray(value) ? value : [];
|
|
49
|
+
var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
50
|
+
var finiteString = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
|
|
51
|
+
var finiteDirection = (value) => value === "LONG" || value === "SHORT" ? value : null;
|
|
52
|
+
var metricValues = (samples, key) => samples.map((sample) => sample[key]).filter(
|
|
53
|
+
(value) => typeof value === "number" && Number.isFinite(value)
|
|
54
|
+
);
|
|
55
|
+
var quantile = (values, probability) => {
|
|
56
|
+
if (!values.length) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
60
|
+
const index = (sorted.length - 1) * probability;
|
|
61
|
+
const lowerIndex = Math.floor(index);
|
|
62
|
+
const upperIndex = Math.ceil(index);
|
|
63
|
+
const lower = sorted[lowerIndex];
|
|
64
|
+
const upper = sorted[upperIndex];
|
|
65
|
+
if (lower == null || upper == null) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (lowerIndex === upperIndex) {
|
|
69
|
+
return lower;
|
|
70
|
+
}
|
|
71
|
+
return lower + (upper - lower) * (index - lowerIndex);
|
|
72
|
+
};
|
|
73
|
+
var summarizeNumbers = (values) => {
|
|
74
|
+
const finite = values.filter((value) => Number.isFinite(value));
|
|
75
|
+
if (!finite.length) {
|
|
76
|
+
return {
|
|
77
|
+
count: 0,
|
|
78
|
+
min: null,
|
|
79
|
+
max: null,
|
|
80
|
+
avg: null,
|
|
81
|
+
p50: null,
|
|
82
|
+
p75: null,
|
|
83
|
+
p90: null,
|
|
84
|
+
p95: null
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const total = finite.reduce((sum2, value) => sum2 + value, 0);
|
|
88
|
+
return {
|
|
89
|
+
count: finite.length,
|
|
90
|
+
min: roundMetric(Math.min(...finite)),
|
|
91
|
+
max: roundMetric(Math.max(...finite)),
|
|
92
|
+
avg: roundMetric(total / finite.length),
|
|
93
|
+
p50: roundMetric(quantile(finite, 0.5)),
|
|
94
|
+
p75: roundMetric(quantile(finite, 0.75)),
|
|
95
|
+
p90: roundMetric(quantile(finite, 0.9)),
|
|
96
|
+
p95: roundMetric(quantile(finite, 0.95))
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
var getIntervalMs = (interval) => {
|
|
100
|
+
if (!interval) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return (0, import_data.intervalToMs)(interval);
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var getSignalKey = ({
|
|
110
|
+
strategy,
|
|
111
|
+
symbol,
|
|
112
|
+
timestamp
|
|
113
|
+
}) => strategy && symbol && timestamp != null ? `${strategy}:${symbol}:${timestamp}` : null;
|
|
114
|
+
var addSignalToMap = (signalsById, signalsByShape, signal) => {
|
|
115
|
+
if (!signal) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const signalId = finiteString(signal.signalId);
|
|
119
|
+
if (signalId) {
|
|
120
|
+
signalsById.set(signalId, signal);
|
|
121
|
+
}
|
|
122
|
+
const shapeKey = getSignalKey({
|
|
123
|
+
strategy: finiteString(signal.strategy),
|
|
124
|
+
symbol: finiteString(signal.symbol),
|
|
125
|
+
timestamp: finiteNumber(signal.timestamp)
|
|
126
|
+
});
|
|
127
|
+
if (shapeKey) {
|
|
128
|
+
signalsByShape.set(shapeKey, signal);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
var getRuntimePayload = ({
|
|
132
|
+
runtimeArtifact,
|
|
133
|
+
replayEvidenceArtifact
|
|
134
|
+
}) => {
|
|
135
|
+
const explicitRuntime = asRecord(runtimeArtifact);
|
|
136
|
+
if (explicitRuntime) {
|
|
137
|
+
return asRecord(explicitRuntime.runtime) ?? explicitRuntime;
|
|
138
|
+
}
|
|
139
|
+
const replayEvidence = asRecord(replayEvidenceArtifact);
|
|
140
|
+
return asRecord(replayEvidence?.runtime);
|
|
141
|
+
};
|
|
142
|
+
var extractRuntimeTradeRows = (runtimePayload2) => {
|
|
143
|
+
if (!runtimePayload2) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
const signalsById = /* @__PURE__ */ new Map();
|
|
147
|
+
const signalsByShape = /* @__PURE__ */ new Map();
|
|
148
|
+
for (const signalRow of asArray(runtimePayload2.signals)) {
|
|
149
|
+
const record = asRecord(signalRow);
|
|
150
|
+
addSignalToMap(
|
|
151
|
+
signalsById,
|
|
152
|
+
signalsByShape,
|
|
153
|
+
asRecord(record?.signal) ?? record
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return asArray(runtimePayload2.trades).map((row) => {
|
|
157
|
+
const record = asRecord(row);
|
|
158
|
+
if (!record) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const redisValues = asRecord(record.redisValues);
|
|
162
|
+
const trade = asRecord(record.trade) ?? asRecord(redisValues?.trade) ?? record;
|
|
163
|
+
const signalId = finiteString(trade.signalId);
|
|
164
|
+
const shapeKey = getSignalKey({
|
|
165
|
+
strategy: finiteString(trade.strategy),
|
|
166
|
+
symbol: finiteString(trade.symbol),
|
|
167
|
+
timestamp: finiteNumber(trade.signalTimestamp ?? trade.entryTimestamp)
|
|
168
|
+
});
|
|
169
|
+
const signal = asRecord(redisValues?.signal) ?? (signalId ? signalsById.get(signalId) ?? null : null) ?? (shapeKey ? signalsByShape.get(shapeKey) ?? null : null);
|
|
170
|
+
return {
|
|
171
|
+
trade,
|
|
172
|
+
signal,
|
|
173
|
+
runtimeLineage: asRecord(trade.runtimeLineage) ?? null
|
|
174
|
+
};
|
|
175
|
+
}).filter((row) => row != null);
|
|
176
|
+
};
|
|
177
|
+
var normalizeReplayMatch = (value) => {
|
|
178
|
+
const item = asRecord(value);
|
|
179
|
+
if (!item) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const runtime = asRecord(item.runtime);
|
|
183
|
+
const backtest = asRecord(item.backtest);
|
|
184
|
+
return {
|
|
185
|
+
orderId: finiteString(
|
|
186
|
+
item.orderId ?? runtime?.orderId ?? backtest?.orderId
|
|
187
|
+
),
|
|
188
|
+
orderLinkId: finiteString(item.orderLinkId ?? runtime?.orderLinkId),
|
|
189
|
+
signalId: finiteString(item.signalId ?? backtest?.signalId),
|
|
190
|
+
strategy: finiteString(
|
|
191
|
+
item.strategy ?? backtest?.strategy ?? runtime?.inferredStrategy
|
|
192
|
+
),
|
|
193
|
+
symbol: finiteString(item.symbol ?? backtest?.symbol ?? runtime?.symbol),
|
|
194
|
+
direction: finiteDirection(
|
|
195
|
+
item.direction ?? backtest?.direction ?? runtime?.direction
|
|
196
|
+
),
|
|
197
|
+
backtestPrice: finiteNumber(item.backtestPrice ?? backtest?.price),
|
|
198
|
+
runtimePrice: finiteNumber(item.runtimePrice ?? runtime?.price)
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
var extractReplayMatches = (replayEvidenceArtifact) => {
|
|
202
|
+
const artifact = asRecord(replayEvidenceArtifact);
|
|
203
|
+
if (!artifact) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
const replay = asRecord(artifact.replay) ?? artifact;
|
|
207
|
+
const runtimeComparison = asRecord(replay.runtimeComparison);
|
|
208
|
+
const details = asRecord(runtimeComparison?.details);
|
|
209
|
+
const matched = asArray(runtimeComparison?.matched).length ? asArray(runtimeComparison?.matched) : asArray(details?.matched);
|
|
210
|
+
return matched.map(normalizeReplayMatch).filter((match) => match != null);
|
|
211
|
+
};
|
|
212
|
+
var buildReplayMatchMaps = (matches) => {
|
|
213
|
+
const maps = {
|
|
214
|
+
byOrderId: /* @__PURE__ */ new Map(),
|
|
215
|
+
bySignalId: /* @__PURE__ */ new Map()
|
|
216
|
+
};
|
|
217
|
+
for (const match of matches) {
|
|
218
|
+
for (const orderId of [match.orderLinkId, match.orderId]) {
|
|
219
|
+
if (orderId) {
|
|
220
|
+
maps.byOrderId.set(orderId, match);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (match.signalId) {
|
|
224
|
+
maps.bySignalId.set(match.signalId, match);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return maps;
|
|
228
|
+
};
|
|
229
|
+
var getSignalCandles = (signal, interval) => {
|
|
230
|
+
const indicators = asRecord(signal?.indicators);
|
|
231
|
+
if (!indicators) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
const intervalKey = (() => {
|
|
235
|
+
switch (interval) {
|
|
236
|
+
case "15":
|
|
237
|
+
return "candles15m";
|
|
238
|
+
case "60":
|
|
239
|
+
return "candles1h";
|
|
240
|
+
case "240":
|
|
241
|
+
return "candles4h";
|
|
242
|
+
case "D":
|
|
243
|
+
return "candles1d";
|
|
244
|
+
default:
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
})();
|
|
248
|
+
const keys = [
|
|
249
|
+
intervalKey,
|
|
250
|
+
"candles15m",
|
|
251
|
+
"candles1h",
|
|
252
|
+
"candles4h",
|
|
253
|
+
"candles1d",
|
|
254
|
+
"candles"
|
|
255
|
+
].filter((key) => Boolean(key));
|
|
256
|
+
for (const key of keys) {
|
|
257
|
+
const value = indicators[key];
|
|
258
|
+
if (Array.isArray(value) && value.length > 1) {
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const candle = indicators.candle;
|
|
263
|
+
const prevCandle = indicators.prevCandle;
|
|
264
|
+
return prevCandle && candle ? [prevCandle, candle] : null;
|
|
265
|
+
};
|
|
266
|
+
var adverseEntryBps = ({
|
|
267
|
+
direction,
|
|
268
|
+
expectedPrice,
|
|
269
|
+
actualPrice
|
|
270
|
+
}) => {
|
|
271
|
+
if (!direction || expectedPrice == null || actualPrice == null || expectedPrice <= 0 || actualPrice <= 0) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const value = direction === "LONG" ? (actualPrice / expectedPrice - 1) * 1e4 : (expectedPrice / actualPrice - 1) * 1e4;
|
|
275
|
+
return roundMetric(value);
|
|
276
|
+
};
|
|
277
|
+
var feeBps = ({
|
|
278
|
+
fee,
|
|
279
|
+
fillAvgPrice,
|
|
280
|
+
qty
|
|
281
|
+
}) => {
|
|
282
|
+
const notional = fillAvgPrice != null && qty != null && fillAvgPrice > 0 && qty > 0 ? fillAvgPrice * qty : null;
|
|
283
|
+
return fee != null && notional != null && notional > 0 ? roundMetric(fee / notional * 1e4) : null;
|
|
284
|
+
};
|
|
285
|
+
var latencyDelta = (end, start) => end != null && start != null ? roundMetric(end - start, 3) : null;
|
|
286
|
+
var resolveTelemetryQuality = ({
|
|
287
|
+
signalClosePrice,
|
|
288
|
+
arrivalMid,
|
|
289
|
+
orderSubmitTime,
|
|
290
|
+
orderAckTime,
|
|
291
|
+
fillAvgPrice,
|
|
292
|
+
fillTime
|
|
293
|
+
}) => {
|
|
294
|
+
if (signalClosePrice != null && arrivalMid != null && orderSubmitTime != null && orderAckTime != null && fillAvgPrice != null && fillTime != null) {
|
|
295
|
+
return "full";
|
|
296
|
+
}
|
|
297
|
+
if (fillAvgPrice != null && (arrivalMid != null || orderSubmitTime != null)) {
|
|
298
|
+
return "partial";
|
|
299
|
+
}
|
|
300
|
+
if (fillAvgPrice != null) {
|
|
301
|
+
return "price_only";
|
|
302
|
+
}
|
|
303
|
+
return "none";
|
|
304
|
+
};
|
|
305
|
+
var finiteTelemetryQuality = (value) => {
|
|
306
|
+
const raw = finiteString(value);
|
|
307
|
+
return raw === "full" || raw === "partial" || raw === "price_only" || raw === "none" ? raw : null;
|
|
308
|
+
};
|
|
309
|
+
var countBy = (samples, keyGetter) => {
|
|
310
|
+
const counts = /* @__PURE__ */ new Map();
|
|
311
|
+
for (const sample of samples) {
|
|
312
|
+
const key = keyGetter(sample) ?? "unknown";
|
|
313
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
314
|
+
}
|
|
315
|
+
return Object.fromEntries(
|
|
316
|
+
[...counts.entries()].sort(([left], [right]) => left.localeCompare(right))
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
var toLatencyBucket = (sample) => {
|
|
320
|
+
const latencyMs = sample.signalCloseToSubmitMs ?? sample.signalToSubmitMs;
|
|
321
|
+
if (latencyMs == null || !Number.isFinite(latencyMs)) return "unknown";
|
|
322
|
+
if (latencyMs <= 1e3) return "0-1s";
|
|
323
|
+
if (latencyMs <= 5e3) return "1-5s";
|
|
324
|
+
if (latencyMs <= 3e4) return "5-30s";
|
|
325
|
+
if (latencyMs <= 12e4) return "30-120s";
|
|
326
|
+
return "120s+";
|
|
327
|
+
};
|
|
328
|
+
var toSpreadBucket = (sample) => {
|
|
329
|
+
const spreadBps = sample.spreadBps;
|
|
330
|
+
if (spreadBps == null || !Number.isFinite(spreadBps)) return "unknown";
|
|
331
|
+
if (spreadBps <= 5) return "0-5bps";
|
|
332
|
+
if (spreadBps <= 20) return "5-20bps";
|
|
333
|
+
if (spreadBps <= 50) return "20-50bps";
|
|
334
|
+
return "50bps+";
|
|
335
|
+
};
|
|
336
|
+
var buildSample = ({
|
|
337
|
+
row,
|
|
338
|
+
match
|
|
339
|
+
}) => {
|
|
340
|
+
const { trade, signal } = row;
|
|
341
|
+
const strategy = finiteString(trade.strategy ?? signal?.strategy);
|
|
342
|
+
const symbol = finiteString(trade.symbol ?? signal?.symbol);
|
|
343
|
+
const interval = finiteString(trade.interval ?? signal?.interval);
|
|
344
|
+
const direction = finiteDirection(trade.direction ?? signal?.direction);
|
|
345
|
+
const orderId = finiteString(trade.orderId);
|
|
346
|
+
const signalId = finiteString(trade.signalId ?? signal?.signalId);
|
|
347
|
+
const qty = finiteNumber(trade.qty);
|
|
348
|
+
const signalTimestamp = finiteNumber(
|
|
349
|
+
trade.signalTimestamp ?? signal?.timestamp
|
|
350
|
+
);
|
|
351
|
+
const intervalMs = getIntervalMs(interval);
|
|
352
|
+
const signalCloseTimestamp = signalTimestamp != null && intervalMs != null ? signalTimestamp + intervalMs : signalTimestamp;
|
|
353
|
+
const signalClosePrice = finiteNumber(trade.signalClosePrice);
|
|
354
|
+
const arrivalSnapshotTime = finiteNumber(trade.arrivalSnapshotTime);
|
|
355
|
+
const arrivalSource = finiteString(trade.arrivalSource);
|
|
356
|
+
const arrivalMid = finiteNumber(trade.arrivalMid);
|
|
357
|
+
const bid = finiteNumber(trade.bid);
|
|
358
|
+
const ask = finiteNumber(trade.ask);
|
|
359
|
+
const spreadBps = finiteNumber(trade.spreadBps);
|
|
360
|
+
const orderSubmitTime = finiteNumber(trade.orderSubmitTime);
|
|
361
|
+
const orderAckTime = finiteNumber(trade.orderAckTime);
|
|
362
|
+
const fillAvgPrice = finiteNumber(trade.fillAvgPrice);
|
|
363
|
+
const fillSource = finiteString(trade.fillSource);
|
|
364
|
+
const fillTime = finiteNumber(trade.fillTime);
|
|
365
|
+
const telemetryQuality = finiteTelemetryQuality(trade.telemetryQuality) ?? resolveTelemetryQuality({
|
|
366
|
+
signalClosePrice,
|
|
367
|
+
arrivalMid,
|
|
368
|
+
orderSubmitTime,
|
|
369
|
+
orderAckTime,
|
|
370
|
+
fillAvgPrice,
|
|
371
|
+
fillTime
|
|
372
|
+
});
|
|
373
|
+
const fee = finiteNumber(trade.fee ?? trade.openFee);
|
|
374
|
+
const arrivalSnapshotAgeMs = latencyDelta(
|
|
375
|
+
orderSubmitTime,
|
|
376
|
+
arrivalSnapshotTime
|
|
377
|
+
);
|
|
378
|
+
const signalToSubmitMs = latencyDelta(orderSubmitTime, signalTimestamp);
|
|
379
|
+
const signalCloseToSubmitMs = latencyDelta(
|
|
380
|
+
orderSubmitTime,
|
|
381
|
+
signalCloseTimestamp
|
|
382
|
+
);
|
|
383
|
+
const submitToAckMs = latencyDelta(orderAckTime, orderSubmitTime);
|
|
384
|
+
const submitToFillMs = latencyDelta(fillTime, orderSubmitTime);
|
|
385
|
+
const orderAckToFillMs = latencyDelta(fillTime, orderAckTime);
|
|
386
|
+
const signalToFillMs = latencyDelta(fillTime, signalTimestamp);
|
|
387
|
+
const observedDelayMs = signalCloseToSubmitMs != null && signalCloseToSubmitMs >= 0 ? signalCloseToSubmitMs : signalToSubmitMs != null && signalToSubmitMs >= 0 ? signalToSubmitMs : null;
|
|
388
|
+
const candles = getSignalCandles(signal, interval);
|
|
389
|
+
const currentDelayRiskBps = roundMetric(
|
|
390
|
+
(0, import_trade.calculateDelayRiskBps)({
|
|
391
|
+
candles,
|
|
392
|
+
intervalMs
|
|
393
|
+
})
|
|
394
|
+
);
|
|
395
|
+
const rawConfiguredDelayRiskBps = roundMetric(
|
|
396
|
+
(0, import_trade.calculateDelayRiskBps)({
|
|
397
|
+
candles,
|
|
398
|
+
intervalMs,
|
|
399
|
+
multiplier: 1,
|
|
400
|
+
maxBps: Number.POSITIVE_INFINITY
|
|
401
|
+
})
|
|
402
|
+
);
|
|
403
|
+
const rawObservedDelayRiskBps = roundMetric(
|
|
404
|
+
(0, import_trade.calculateDelayRiskBps)({
|
|
405
|
+
candles,
|
|
406
|
+
intervalMs,
|
|
407
|
+
expectedDelayMs: observedDelayMs,
|
|
408
|
+
multiplier: 1,
|
|
409
|
+
maxBps: Number.POSITIVE_INFINITY
|
|
410
|
+
})
|
|
411
|
+
);
|
|
412
|
+
const signalToArrivalAdverseBps = adverseEntryBps({
|
|
413
|
+
direction,
|
|
414
|
+
expectedPrice: signalClosePrice,
|
|
415
|
+
actualPrice: arrivalMid
|
|
416
|
+
});
|
|
417
|
+
const arrivalToFillAdverseBps = adverseEntryBps({
|
|
418
|
+
direction,
|
|
419
|
+
expectedPrice: arrivalMid,
|
|
420
|
+
actualPrice: fillAvgPrice
|
|
421
|
+
});
|
|
422
|
+
const signalToFillAdverseBps = adverseEntryBps({
|
|
423
|
+
direction,
|
|
424
|
+
expectedPrice: signalClosePrice,
|
|
425
|
+
actualPrice: fillAvgPrice
|
|
426
|
+
});
|
|
427
|
+
const currentModelEntrySlippageBps = signalToFillAdverseBps != null ? roundMetric(
|
|
428
|
+
import_constants.BACKTEST_BASE_SLIPPAGE_BPS + (spreadBps ?? 0) * import_constants.BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER + import_constants.BACKTEST_MARKET_IMPACT_BPS + (currentDelayRiskBps ?? 0)
|
|
429
|
+
) : null;
|
|
430
|
+
const residualVsCurrentModelBps = signalToFillAdverseBps != null && currentModelEntrySlippageBps != null ? roundMetric(signalToFillAdverseBps - currentModelEntrySlippageBps) : null;
|
|
431
|
+
return {
|
|
432
|
+
strategy,
|
|
433
|
+
symbol,
|
|
434
|
+
interval,
|
|
435
|
+
direction,
|
|
436
|
+
orderId,
|
|
437
|
+
signalId,
|
|
438
|
+
qty,
|
|
439
|
+
signalTimestamp,
|
|
440
|
+
signalCloseTimestamp,
|
|
441
|
+
signalClosePrice,
|
|
442
|
+
arrivalSnapshotTime,
|
|
443
|
+
arrivalSource,
|
|
444
|
+
arrivalMid,
|
|
445
|
+
bid,
|
|
446
|
+
ask,
|
|
447
|
+
spreadBps,
|
|
448
|
+
orderSubmitTime,
|
|
449
|
+
orderAckTime,
|
|
450
|
+
fillAvgPrice,
|
|
451
|
+
fillSource,
|
|
452
|
+
fillTime,
|
|
453
|
+
telemetryQuality,
|
|
454
|
+
fee,
|
|
455
|
+
feeBps: feeBps({ fee, fillAvgPrice, qty }),
|
|
456
|
+
arrivalSnapshotAgeMs,
|
|
457
|
+
signalToSubmitMs,
|
|
458
|
+
signalCloseToSubmitMs,
|
|
459
|
+
submitToAckMs,
|
|
460
|
+
submitToFillMs,
|
|
461
|
+
orderAckToFillMs,
|
|
462
|
+
signalToFillMs,
|
|
463
|
+
signalToArrivalAdverseBps,
|
|
464
|
+
arrivalToFillAdverseBps,
|
|
465
|
+
signalToFillAdverseBps,
|
|
466
|
+
currentDelayRiskBps,
|
|
467
|
+
rawConfiguredDelayRiskBps,
|
|
468
|
+
rawObservedDelayRiskBps,
|
|
469
|
+
currentModelEntrySlippageBps,
|
|
470
|
+
residualVsCurrentModelBps,
|
|
471
|
+
replayEntryResidualBps: adverseEntryBps({
|
|
472
|
+
direction: direction ?? match?.direction ?? null,
|
|
473
|
+
expectedPrice: match?.backtestPrice ?? null,
|
|
474
|
+
actualPrice: match?.runtimePrice ?? null
|
|
475
|
+
}),
|
|
476
|
+
runtimeLineage: row.runtimeLineage
|
|
477
|
+
};
|
|
478
|
+
};
|
|
479
|
+
var buildReplayOnlySample = (match) => ({
|
|
480
|
+
strategy: match.strategy,
|
|
481
|
+
symbol: match.symbol,
|
|
482
|
+
interval: null,
|
|
483
|
+
direction: match.direction,
|
|
484
|
+
orderId: match.orderLinkId ?? match.orderId,
|
|
485
|
+
signalId: match.signalId,
|
|
486
|
+
qty: null,
|
|
487
|
+
signalTimestamp: null,
|
|
488
|
+
signalCloseTimestamp: null,
|
|
489
|
+
signalClosePrice: null,
|
|
490
|
+
arrivalSnapshotTime: null,
|
|
491
|
+
arrivalSource: null,
|
|
492
|
+
arrivalMid: null,
|
|
493
|
+
bid: null,
|
|
494
|
+
ask: null,
|
|
495
|
+
spreadBps: null,
|
|
496
|
+
orderSubmitTime: null,
|
|
497
|
+
orderAckTime: null,
|
|
498
|
+
fillAvgPrice: match.runtimePrice,
|
|
499
|
+
fillSource: "replay_runtime_price",
|
|
500
|
+
fillTime: null,
|
|
501
|
+
telemetryQuality: match.runtimePrice != null ? "price_only" : "none",
|
|
502
|
+
fee: null,
|
|
503
|
+
feeBps: null,
|
|
504
|
+
arrivalSnapshotAgeMs: null,
|
|
505
|
+
signalToSubmitMs: null,
|
|
506
|
+
signalCloseToSubmitMs: null,
|
|
507
|
+
submitToAckMs: null,
|
|
508
|
+
submitToFillMs: null,
|
|
509
|
+
orderAckToFillMs: null,
|
|
510
|
+
signalToFillMs: null,
|
|
511
|
+
signalToArrivalAdverseBps: null,
|
|
512
|
+
arrivalToFillAdverseBps: null,
|
|
513
|
+
signalToFillAdverseBps: null,
|
|
514
|
+
currentDelayRiskBps: null,
|
|
515
|
+
rawConfiguredDelayRiskBps: null,
|
|
516
|
+
rawObservedDelayRiskBps: null,
|
|
517
|
+
currentModelEntrySlippageBps: null,
|
|
518
|
+
residualVsCurrentModelBps: null,
|
|
519
|
+
replayEntryResidualBps: adverseEntryBps({
|
|
520
|
+
direction: match.direction,
|
|
521
|
+
expectedPrice: match.backtestPrice,
|
|
522
|
+
actualPrice: match.runtimePrice
|
|
523
|
+
}),
|
|
524
|
+
runtimeLineage: null
|
|
525
|
+
});
|
|
526
|
+
var hasTelemetry = (sample) => sample.signalClosePrice != null || sample.arrivalMid != null || sample.orderSubmitTime != null || sample.fillAvgPrice != null || sample.fillTime != null || sample.fee != null;
|
|
527
|
+
var hasFullTelemetry = (sample) => sample.signalClosePrice != null && sample.arrivalMid != null && sample.orderSubmitTime != null && sample.orderAckTime != null && sample.fillAvgPrice != null && sample.fillTime != null;
|
|
528
|
+
var buildGroupSummary = (samples) => ({
|
|
529
|
+
trades: samples.length,
|
|
530
|
+
fullTelemetryTrades: samples.filter(hasFullTelemetry).length,
|
|
531
|
+
replayMatchedTrades: metricValues(samples, "replayEntryResidualBps").length,
|
|
532
|
+
telemetryQuality: countBy(samples, (sample) => sample.telemetryQuality),
|
|
533
|
+
fillSource: countBy(samples, (sample) => sample.fillSource),
|
|
534
|
+
arrivalSource: countBy(samples, (sample) => sample.arrivalSource),
|
|
535
|
+
signalToArrivalAdverseBps: summarizeNumbers(
|
|
536
|
+
metricValues(samples, "signalToArrivalAdverseBps")
|
|
537
|
+
),
|
|
538
|
+
arrivalToFillAdverseBps: summarizeNumbers(
|
|
539
|
+
metricValues(samples, "arrivalToFillAdverseBps")
|
|
540
|
+
),
|
|
541
|
+
signalToFillAdverseBps: summarizeNumbers(
|
|
542
|
+
metricValues(samples, "signalToFillAdverseBps")
|
|
543
|
+
),
|
|
544
|
+
residualVsCurrentModelBps: summarizeNumbers(
|
|
545
|
+
metricValues(samples, "residualVsCurrentModelBps")
|
|
546
|
+
),
|
|
547
|
+
replayEntryResidualBps: summarizeNumbers(
|
|
548
|
+
metricValues(samples, "replayEntryResidualBps")
|
|
549
|
+
),
|
|
550
|
+
spreadBps: summarizeNumbers(metricValues(samples, "spreadBps")),
|
|
551
|
+
feeBps: summarizeNumbers(metricValues(samples, "feeBps")),
|
|
552
|
+
arrivalSnapshotAgeMs: summarizeNumbers(
|
|
553
|
+
metricValues(samples, "arrivalSnapshotAgeMs")
|
|
554
|
+
),
|
|
555
|
+
signalToSubmitMs: summarizeNumbers(metricValues(samples, "signalToSubmitMs")),
|
|
556
|
+
signalCloseToSubmitMs: summarizeNumbers(
|
|
557
|
+
metricValues(samples, "signalCloseToSubmitMs")
|
|
558
|
+
),
|
|
559
|
+
submitToAckMs: summarizeNumbers(metricValues(samples, "submitToAckMs")),
|
|
560
|
+
submitToFillMs: summarizeNumbers(metricValues(samples, "submitToFillMs")),
|
|
561
|
+
orderAckToFillMs: summarizeNumbers(metricValues(samples, "orderAckToFillMs")),
|
|
562
|
+
currentDelayRiskBps: summarizeNumbers(
|
|
563
|
+
metricValues(samples, "currentDelayRiskBps")
|
|
564
|
+
),
|
|
565
|
+
rawConfiguredDelayRiskBps: summarizeNumbers(
|
|
566
|
+
metricValues(samples, "rawConfiguredDelayRiskBps")
|
|
567
|
+
),
|
|
568
|
+
rawObservedDelayRiskBps: summarizeNumbers(
|
|
569
|
+
metricValues(samples, "rawObservedDelayRiskBps")
|
|
570
|
+
)
|
|
571
|
+
});
|
|
572
|
+
var groupSamples = (samples, keyGetter) => {
|
|
573
|
+
const groups = /* @__PURE__ */ new Map();
|
|
574
|
+
for (const sample of samples) {
|
|
575
|
+
const key = keyGetter(sample);
|
|
576
|
+
if (!key) {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
groups.set(key, [...groups.get(key) ?? [], sample]);
|
|
580
|
+
}
|
|
581
|
+
return Object.fromEntries(
|
|
582
|
+
[...groups.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([key, group]) => [key, buildGroupSummary(group)])
|
|
583
|
+
);
|
|
584
|
+
};
|
|
585
|
+
var positiveMetricValues = (samples, key) => metricValues(samples, key).filter((value) => value > 0);
|
|
586
|
+
var buildRecommendation = (samples) => {
|
|
587
|
+
const fullTelemetrySamples = samples.filter(hasFullTelemetry);
|
|
588
|
+
const notes = [];
|
|
589
|
+
if (!fullTelemetrySamples.length) {
|
|
590
|
+
return {
|
|
591
|
+
confidence: "none",
|
|
592
|
+
baseSlippageBps: null,
|
|
593
|
+
spreadMultiplier: null,
|
|
594
|
+
delayRiskMultiplier: null,
|
|
595
|
+
delayRiskMaxBps: null,
|
|
596
|
+
expectedDelayMs: null,
|
|
597
|
+
notes: [
|
|
598
|
+
"No full live execution telemetry was found. Use a newer runtime debug artifact collected after telemetry persistence was enabled.",
|
|
599
|
+
"Replay residuals can still diagnose model drift, but they are not enough to calibrate signal-to-arrival and arrival-to-fill components separately."
|
|
600
|
+
]
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
if (fullTelemetrySamples.length < 20) {
|
|
604
|
+
notes.push(
|
|
605
|
+
"Sample is small; treat recommendations as diagnostic until at least 20-50 live fills are available."
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
const spreadRatios = fullTelemetrySamples.map((sample) => {
|
|
609
|
+
if (sample.arrivalToFillAdverseBps == null || sample.spreadBps == null || sample.spreadBps <= 0) {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
return Math.max(0, sample.arrivalToFillAdverseBps) / sample.spreadBps;
|
|
613
|
+
}).filter(
|
|
614
|
+
(value) => value != null && Number.isFinite(value)
|
|
615
|
+
);
|
|
616
|
+
const spreadMultiplier = roundMetric(quantile(spreadRatios, 0.75), 4);
|
|
617
|
+
const baseResiduals = fullTelemetrySamples.map((sample) => {
|
|
618
|
+
if (sample.arrivalToFillAdverseBps == null) {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
return Math.max(
|
|
622
|
+
0,
|
|
623
|
+
sample.arrivalToFillAdverseBps - (sample.spreadBps ?? 0) * (spreadMultiplier ?? import_constants.BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER)
|
|
624
|
+
);
|
|
625
|
+
}).filter(
|
|
626
|
+
(value) => value != null && Number.isFinite(value)
|
|
627
|
+
);
|
|
628
|
+
const closeDelayMs = positiveMetricValues(
|
|
629
|
+
fullTelemetrySamples,
|
|
630
|
+
"signalCloseToSubmitMs"
|
|
631
|
+
);
|
|
632
|
+
const fallbackDelayMs = positiveMetricValues(
|
|
633
|
+
fullTelemetrySamples,
|
|
634
|
+
"signalToSubmitMs"
|
|
635
|
+
);
|
|
636
|
+
if (!spreadRatios.length) {
|
|
637
|
+
notes.push(
|
|
638
|
+
"Spread multiplier could not be calibrated because spreadBps or arrival-to-fill telemetry is missing."
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
notes.push(
|
|
642
|
+
"Delay risk bps is disabled; signal-to-arrival latency is modeled by next-bar-open backtest fills."
|
|
643
|
+
);
|
|
644
|
+
return {
|
|
645
|
+
confidence: fullTelemetrySamples.length >= 50 ? "high" : fullTelemetrySamples.length >= 20 ? "medium" : "low",
|
|
646
|
+
baseSlippageBps: roundMetric(quantile(baseResiduals, 0.75), 2),
|
|
647
|
+
spreadMultiplier,
|
|
648
|
+
delayRiskMultiplier: null,
|
|
649
|
+
delayRiskMaxBps: null,
|
|
650
|
+
expectedDelayMs: roundMetric(
|
|
651
|
+
quantile(closeDelayMs.length ? closeDelayMs : fallbackDelayMs, 0.5),
|
|
652
|
+
0
|
|
653
|
+
),
|
|
654
|
+
notes
|
|
655
|
+
};
|
|
656
|
+
};
|
|
657
|
+
var buildExecutionCalibrationReport = ({
|
|
658
|
+
runtimeArtifact,
|
|
659
|
+
replayEvidenceArtifact,
|
|
660
|
+
sourcePaths = {},
|
|
661
|
+
nowMs = Date.now()
|
|
662
|
+
}) => {
|
|
663
|
+
const runtimePayload2 = getRuntimePayload({
|
|
664
|
+
runtimeArtifact,
|
|
665
|
+
replayEvidenceArtifact
|
|
666
|
+
});
|
|
667
|
+
const runtimeRows = extractRuntimeTradeRows(runtimePayload2);
|
|
668
|
+
const replayMatches = extractReplayMatches(replayEvidenceArtifact);
|
|
669
|
+
const replayMaps = buildReplayMatchMaps(replayMatches);
|
|
670
|
+
const usedReplayMatches = /* @__PURE__ */ new Set();
|
|
671
|
+
const samples = runtimeRows.map((row) => {
|
|
672
|
+
const orderId = finiteString(row.trade.orderId);
|
|
673
|
+
const signalId = finiteString(row.trade.signalId);
|
|
674
|
+
const match = (orderId ? replayMaps.byOrderId.get(orderId) ?? null : null) ?? (signalId ? replayMaps.bySignalId.get(signalId) ?? null : null);
|
|
675
|
+
if (match) {
|
|
676
|
+
usedReplayMatches.add(match);
|
|
677
|
+
}
|
|
678
|
+
return buildSample({ row, match });
|
|
679
|
+
});
|
|
680
|
+
const replayOnlySamples = replayMatches.filter((match) => !usedReplayMatches.has(match)).map(buildReplayOnlySample);
|
|
681
|
+
return {
|
|
682
|
+
reportType: "execution-calibration",
|
|
683
|
+
generatedAt: nowMs,
|
|
684
|
+
sources: sourcePaths,
|
|
685
|
+
currentModel: {
|
|
686
|
+
baseSlippageBps: import_constants.BACKTEST_BASE_SLIPPAGE_BPS,
|
|
687
|
+
spreadMultiplier: import_constants.BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER,
|
|
688
|
+
marketImpactBps: import_constants.BACKTEST_MARKET_IMPACT_BPS
|
|
689
|
+
},
|
|
690
|
+
counts: {
|
|
691
|
+
runtimeTrades: runtimeRows.length,
|
|
692
|
+
telemetryTrades: samples.filter(hasTelemetry).length,
|
|
693
|
+
fullTelemetryTrades: samples.filter(hasFullTelemetry).length,
|
|
694
|
+
replayMatched: replayMatches.length,
|
|
695
|
+
replayMatchedRuntimeTrades: usedReplayMatches.size,
|
|
696
|
+
replayOnlyMatches: replayOnlySamples.length
|
|
697
|
+
},
|
|
698
|
+
summary: {
|
|
699
|
+
all: buildGroupSummary([...samples, ...replayOnlySamples]),
|
|
700
|
+
byStrategy: groupSamples(
|
|
701
|
+
[...samples, ...replayOnlySamples],
|
|
702
|
+
(sample) => sample.strategy
|
|
703
|
+
),
|
|
704
|
+
bySymbol: groupSamples(samples, (sample) => sample.symbol),
|
|
705
|
+
byInterval: groupSamples(samples, (sample) => sample.interval),
|
|
706
|
+
byTelemetryQuality: groupSamples(
|
|
707
|
+
[...samples, ...replayOnlySamples],
|
|
708
|
+
(sample) => sample.telemetryQuality
|
|
709
|
+
),
|
|
710
|
+
byFillSource: groupSamples(
|
|
711
|
+
[...samples, ...replayOnlySamples],
|
|
712
|
+
(sample) => sample.fillSource
|
|
713
|
+
),
|
|
714
|
+
byLatencyBucket: groupSamples(samples, toLatencyBucket),
|
|
715
|
+
bySpreadBucket: groupSamples(samples, toSpreadBucket)
|
|
716
|
+
},
|
|
717
|
+
recommendation: buildRecommendation(samples),
|
|
718
|
+
samples,
|
|
719
|
+
replayOnlySamples
|
|
720
|
+
};
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
// src/lib/runtimeLineage.ts
|
|
724
|
+
var import_node_crypto = require("crypto");
|
|
725
|
+
var normalizeForStableJson = (value) => {
|
|
726
|
+
if (Array.isArray(value)) {
|
|
727
|
+
return value.map(normalizeForStableJson);
|
|
728
|
+
}
|
|
729
|
+
if (value && typeof value === "object") {
|
|
730
|
+
return Object.fromEntries(
|
|
731
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalizeForStableJson(entry)])
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
return value;
|
|
735
|
+
};
|
|
736
|
+
var fingerprintRuntimeValue = (value) => (0, import_node_crypto.createHash)("sha256").update(JSON.stringify(normalizeForStableJson(value))).digest("hex").slice(0, 16);
|
|
737
|
+
var runtimeLineageKey = (lineage) => [
|
|
738
|
+
"v3",
|
|
739
|
+
lineage.deploymentCompositionId,
|
|
740
|
+
lineage.strategyRevision,
|
|
741
|
+
lineage.strategyPackageVersion,
|
|
742
|
+
`deps:${fingerprintRuntimeValue(lineage.strategyDependencyVersions).slice(0, 16)}`,
|
|
743
|
+
lineage.runtimePackageVersion
|
|
744
|
+
].join(":");
|
|
745
|
+
|
|
746
|
+
// src/lib/runtimeEvidenceDeployment.ts
|
|
747
|
+
var runtimeEvidenceDeployment_exports = {};
|
|
748
|
+
__reExport(runtimeEvidenceDeployment_exports, require("@tradejs/node/diagnostics"));
|
|
749
|
+
|
|
750
|
+
// src/lib/runtimeScorecard.ts
|
|
751
|
+
var asRecord2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
752
|
+
var asArray2 = (value) => Array.isArray(value) ? value : [];
|
|
753
|
+
var finiteNumber2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
754
|
+
var finiteString2 = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
|
|
755
|
+
var packageVersionMap = (value) => {
|
|
756
|
+
const record = asRecord2(value);
|
|
757
|
+
if (!record || Object.keys(record).length === 0) return null;
|
|
758
|
+
const entries = Object.entries(record);
|
|
759
|
+
if (entries.some(
|
|
760
|
+
([name, version]) => !name.startsWith("@tradejs/") || finiteString2(version) == null
|
|
761
|
+
)) {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
return Object.fromEntries(
|
|
765
|
+
entries.map(([name, version]) => [name, String(version).trim()]).sort(([left], [right]) => left.localeCompare(right))
|
|
766
|
+
);
|
|
767
|
+
};
|
|
768
|
+
var round = (value, decimals = 6) => Number(value.toFixed(decimals));
|
|
769
|
+
var unwrapRows = (value, key) => asArray2(value).map((item) => asRecord2(asRecord2(item)?.[key]) ?? asRecord2(item)).filter((item) => item != null);
|
|
770
|
+
var runtimePayload = (artifact) => {
|
|
771
|
+
const root = asRecord2(artifact) ?? {};
|
|
772
|
+
return asRecord2(root.runtime) ?? root;
|
|
773
|
+
};
|
|
774
|
+
var runtimeWindow = (artifact) => {
|
|
775
|
+
const root = asRecord2(artifact) ?? {};
|
|
776
|
+
return asRecord2(root.window) ?? asRecord2(runtimePayload(artifact).window) ?? {};
|
|
777
|
+
};
|
|
778
|
+
var extractRuntimeRows = (artifact) => {
|
|
779
|
+
const runtime = runtimePayload(artifact);
|
|
780
|
+
return {
|
|
781
|
+
evaluations: unwrapRows(runtime.evaluations, "evaluation"),
|
|
782
|
+
signals: unwrapRows(runtime.signals, "signal"),
|
|
783
|
+
trades: unwrapRows(runtime.trades, "trade"),
|
|
784
|
+
lineageScopes: unwrapRows(runtime.lineageScopes, "lineageScope")
|
|
785
|
+
};
|
|
786
|
+
};
|
|
787
|
+
var buildEmbeddedLineageScope = ({
|
|
788
|
+
artifact,
|
|
789
|
+
rows,
|
|
790
|
+
strategy
|
|
791
|
+
}) => {
|
|
792
|
+
const deployment = asRecord2(asRecord2(artifact)?.deployment);
|
|
793
|
+
if (finiteNumber2(deployment?.schemaVersion) !== 2) return null;
|
|
794
|
+
const deploymentId = finiteString2(deployment?.id);
|
|
795
|
+
const accountId = finiteString2(deployment?.accountId);
|
|
796
|
+
const deploymentCompositionId = finiteString2(
|
|
797
|
+
deployment?.deploymentCompositionId
|
|
798
|
+
);
|
|
799
|
+
if (!deploymentId || !accountId || !deploymentCompositionId) return null;
|
|
800
|
+
const snapshots = asArray2(deployment?.strategies).map(asRecord2).filter((item) => item != null).filter((item) => item.enabled === true).filter(
|
|
801
|
+
(item) => !strategy || finiteString2(item.strategyName) === strategy
|
|
802
|
+
);
|
|
803
|
+
const targetSeeds = snapshots.map((snapshot) => {
|
|
804
|
+
const strategyName = finiteString2(snapshot.strategyName);
|
|
805
|
+
const strategyRevision = finiteString2(snapshot.strategyRevision);
|
|
806
|
+
const strategyPackageVersion = finiteString2(
|
|
807
|
+
snapshot.strategyPackageVersion
|
|
808
|
+
);
|
|
809
|
+
const strategyDependencyVersions = packageVersionMap(
|
|
810
|
+
snapshot.strategyDependencyVersions
|
|
811
|
+
);
|
|
812
|
+
const runtimePackageVersion = finiteString2(
|
|
813
|
+
snapshot.runtimePackageVersion
|
|
814
|
+
);
|
|
815
|
+
return strategyName && strategyRevision && strategyPackageVersion && strategyDependencyVersions && runtimePackageVersion ? {
|
|
816
|
+
strategyName,
|
|
817
|
+
strategyRevision,
|
|
818
|
+
deploymentCompositionId,
|
|
819
|
+
strategyPackageVersion,
|
|
820
|
+
strategyDependencyVersions,
|
|
821
|
+
runtimePackageVersion
|
|
822
|
+
} : null;
|
|
823
|
+
}).filter((item) => item != null);
|
|
824
|
+
if (targetSeeds.length !== snapshots.length || targetSeeds.length === 0) {
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
const lineageRows = [
|
|
828
|
+
...rows.evaluations,
|
|
829
|
+
...rows.signals,
|
|
830
|
+
...rows.trades,
|
|
831
|
+
...rows.lineageScopes.map((scope) => ({
|
|
832
|
+
strategy: scope.strategy,
|
|
833
|
+
deploymentId: scope.deploymentId,
|
|
834
|
+
accountId: scope.accountId,
|
|
835
|
+
runtimeLineage: scope.lineage
|
|
836
|
+
}))
|
|
837
|
+
];
|
|
838
|
+
const identityMatches = (lineage, target) => finiteNumber2(lineage?.schemaVersion) === 3 && finiteString2(lineage?.strategyRevision) === target.strategyRevision && finiteString2(lineage?.deploymentCompositionId) === target.deploymentCompositionId && finiteString2(lineage?.strategyPackageVersion) === target.strategyPackageVersion && JSON.stringify(packageVersionMap(lineage?.strategyDependencyVersions)) === JSON.stringify(target.strategyDependencyVersions) && finiteString2(lineage?.runtimePackageVersion) === target.runtimePackageVersion;
|
|
839
|
+
const targets = /* @__PURE__ */ new Map();
|
|
840
|
+
for (const seed of targetSeeds) {
|
|
841
|
+
const matchingRows = lineageRows.filter(
|
|
842
|
+
(row) => finiteString2(row.strategy) === seed.strategyName && finiteString2(row.deploymentId) === deploymentId && finiteString2(row.accountId) === accountId && identityMatches(asRecord2(row.runtimeLineage), seed)
|
|
843
|
+
);
|
|
844
|
+
const riskValues = new Set(
|
|
845
|
+
matchingRows.map((row) => finiteNumber2(asRecord2(row.runtimeLineage)?.maxLossValue)).filter((value) => value != null)
|
|
846
|
+
);
|
|
847
|
+
targets.set(seed.strategyName, {
|
|
848
|
+
...seed,
|
|
849
|
+
maxLossValue: riskValues.size === 1 ? [...riskValues][0] : null,
|
|
850
|
+
rowsWithLineage: matchingRows.length,
|
|
851
|
+
riskConflict: riskValues.size > 1
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
const belongs = (row) => {
|
|
855
|
+
const strategyName = finiteString2(row.strategy);
|
|
856
|
+
const target = strategyName ? targets.get(strategyName) : null;
|
|
857
|
+
const lineage = asRecord2(row.runtimeLineage);
|
|
858
|
+
return Boolean(
|
|
859
|
+
target && target.maxLossValue != null && identityMatches(lineage, target) && finiteNumber2(lineage?.maxLossValue) === target.maxLossValue
|
|
860
|
+
);
|
|
861
|
+
};
|
|
862
|
+
const targetValues = [...targets.values()];
|
|
863
|
+
const coverageComplete = targetValues.every(
|
|
864
|
+
(target) => target.rowsWithLineage > 0 && target.maxLossValue != null
|
|
865
|
+
);
|
|
866
|
+
const commonRiskValues = new Set(
|
|
867
|
+
targetValues.map((target) => target.maxLossValue).filter((value) => value != null)
|
|
868
|
+
);
|
|
869
|
+
const singleTarget = targetValues.length === 1 ? targetValues[0] : null;
|
|
870
|
+
const singleLineage = singleTarget?.maxLossValue != null ? {
|
|
871
|
+
schemaVersion: 3,
|
|
872
|
+
strategyRevision: singleTarget.strategyRevision,
|
|
873
|
+
deploymentCompositionId: singleTarget.deploymentCompositionId,
|
|
874
|
+
strategyPackageVersion: singleTarget.strategyPackageVersion,
|
|
875
|
+
strategyDependencyVersions: singleTarget.strategyDependencyVersions,
|
|
876
|
+
runtimePackageVersion: singleTarget.runtimePackageVersion,
|
|
877
|
+
maxLossValue: singleTarget.maxLossValue
|
|
878
|
+
} : null;
|
|
879
|
+
return {
|
|
880
|
+
belongs,
|
|
881
|
+
summary: {
|
|
882
|
+
complete: coverageComplete,
|
|
883
|
+
identityComplete: true,
|
|
884
|
+
coverageComplete,
|
|
885
|
+
conflicts: targetValues.some((target) => target.riskConflict),
|
|
886
|
+
rows: lineageRows.length,
|
|
887
|
+
rowsWithLineage: targetValues.reduce(
|
|
888
|
+
(total, target) => total + target.rowsWithLineage,
|
|
889
|
+
0
|
|
890
|
+
),
|
|
891
|
+
schemaVersion: 3,
|
|
892
|
+
lineageKey: singleLineage ? runtimeLineageKey(singleLineage) : null,
|
|
893
|
+
strategyRevision: singleTarget?.strategyRevision ?? null,
|
|
894
|
+
deploymentCompositionId,
|
|
895
|
+
strategyPackageVersion: singleTarget?.strategyPackageVersion ?? null,
|
|
896
|
+
strategyDependencyVersions: singleTarget?.strategyDependencyVersions ?? null,
|
|
897
|
+
runtimePackageVersion: singleTarget?.runtimePackageVersion ?? null,
|
|
898
|
+
maxLossValue: commonRiskValues.size === 1 ? [...commonRiskValues][0] : null,
|
|
899
|
+
strategyScopes: Object.fromEntries(
|
|
900
|
+
targetValues.map((target) => [
|
|
901
|
+
target.strategyName,
|
|
902
|
+
{
|
|
903
|
+
strategyRevision: target.strategyRevision,
|
|
904
|
+
rowsWithLineage: target.rowsWithLineage,
|
|
905
|
+
maxLossValue: target.maxLossValue,
|
|
906
|
+
complete: target.rowsWithLineage > 0 && target.maxLossValue != null
|
|
907
|
+
}
|
|
908
|
+
])
|
|
909
|
+
)
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
};
|
|
913
|
+
var runtimeDeploymentBinding = (artifact) => {
|
|
914
|
+
const deployment = asRecord2(asRecord2(artifact)?.deployment);
|
|
915
|
+
return {
|
|
916
|
+
deploymentId: finiteString2(deployment?.id),
|
|
917
|
+
accountId: finiteString2(deployment?.accountId)
|
|
918
|
+
};
|
|
919
|
+
};
|
|
920
|
+
var belongsToRuntimeDeployment = (row, binding) => binding.deploymentId == null && binding.accountId == null ? true : finiteString2(row.deploymentId) === binding.deploymentId && finiteString2(row.accountId) === binding.accountId;
|
|
921
|
+
var belongsToStrategy = (row, strategy) => strategy == null || finiteString2(row.strategy) === strategy;
|
|
922
|
+
var filterArtifactByStrategy = (artifact, strategy) => {
|
|
923
|
+
if (!strategy) return artifact;
|
|
924
|
+
const root = asRecord2(artifact) ?? {};
|
|
925
|
+
const runtime = runtimePayload(artifact);
|
|
926
|
+
return {
|
|
927
|
+
...root,
|
|
928
|
+
runtime: {
|
|
929
|
+
...runtime,
|
|
930
|
+
evaluations: asArray2(runtime.evaluations).filter(
|
|
931
|
+
(item) => belongsToStrategy(
|
|
932
|
+
asRecord2(asRecord2(item)?.evaluation) ?? asRecord2(item) ?? {},
|
|
933
|
+
strategy
|
|
934
|
+
)
|
|
935
|
+
),
|
|
936
|
+
signals: asArray2(runtime.signals).filter(
|
|
937
|
+
(item) => belongsToStrategy(
|
|
938
|
+
asRecord2(asRecord2(item)?.signal) ?? asRecord2(item) ?? {},
|
|
939
|
+
strategy
|
|
940
|
+
)
|
|
941
|
+
),
|
|
942
|
+
trades: asArray2(runtime.trades).filter(
|
|
943
|
+
(item) => belongsToStrategy(
|
|
944
|
+
asRecord2(asRecord2(item)?.trade) ?? asRecord2(item) ?? {},
|
|
945
|
+
strategy
|
|
946
|
+
)
|
|
947
|
+
),
|
|
948
|
+
lineageScopes: asArray2(runtime.lineageScopes).filter(
|
|
949
|
+
(item) => belongsToStrategy(asRecord2(item) ?? {}, strategy)
|
|
950
|
+
)
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
};
|
|
954
|
+
var countBy2 = (items, getter) => {
|
|
955
|
+
const counts = {};
|
|
956
|
+
for (const item of items) {
|
|
957
|
+
const key = getter(item);
|
|
958
|
+
if (!key) continue;
|
|
959
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
960
|
+
}
|
|
961
|
+
return Object.fromEntries(
|
|
962
|
+
Object.entries(counts).sort(([left], [right]) => left.localeCompare(right))
|
|
963
|
+
);
|
|
964
|
+
};
|
|
965
|
+
var resolveGateDecision = (evaluation) => {
|
|
966
|
+
const analysis = asRecord2(evaluation.aiAnalysis);
|
|
967
|
+
const explicit = finiteString2(analysis?.gateDecision);
|
|
968
|
+
if (explicit === "approved" || explicit === "rejected") {
|
|
969
|
+
return { decision: explicit, source: "gate_decision" };
|
|
970
|
+
}
|
|
971
|
+
if (typeof analysis?.approved === "boolean") {
|
|
972
|
+
return {
|
|
973
|
+
decision: analysis.approved ? "approved" : "rejected",
|
|
974
|
+
source: "legacy_approved_fallback"
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
return null;
|
|
978
|
+
};
|
|
979
|
+
var buildDistributions = ({
|
|
980
|
+
evaluations,
|
|
981
|
+
signals
|
|
982
|
+
}) => {
|
|
983
|
+
const candidates = evaluations.filter(
|
|
984
|
+
(evaluation) => evaluation.status === "signal"
|
|
985
|
+
);
|
|
986
|
+
return {
|
|
987
|
+
candidatesByStrategy: countBy2(
|
|
988
|
+
candidates,
|
|
989
|
+
(evaluation) => finiteString2(evaluation.strategy)
|
|
990
|
+
),
|
|
991
|
+
orderStatus: countBy2(signals, (signal) => finiteString2(signal.orderStatus)),
|
|
992
|
+
skipReasons: countBy2(
|
|
993
|
+
signals,
|
|
994
|
+
(signal) => finiteString2(signal.orderSkipReason)
|
|
995
|
+
),
|
|
996
|
+
aiQuality: countBy2(candidates, (evaluation) => {
|
|
997
|
+
const quality = finiteNumber2(asRecord2(evaluation.aiAnalysis)?.quality);
|
|
998
|
+
return quality == null ? null : String(quality);
|
|
999
|
+
})
|
|
1000
|
+
};
|
|
1001
|
+
};
|
|
1002
|
+
var buildDistributionDelta = (current, previous) => {
|
|
1003
|
+
const currentTotal = Object.values(current).reduce(
|
|
1004
|
+
(total, value) => total + value,
|
|
1005
|
+
0
|
|
1006
|
+
);
|
|
1007
|
+
const previousTotal = Object.values(previous).reduce(
|
|
1008
|
+
(total, value) => total + value,
|
|
1009
|
+
0
|
|
1010
|
+
);
|
|
1011
|
+
const keys = [
|
|
1012
|
+
.../* @__PURE__ */ new Set([...Object.keys(current), ...Object.keys(previous)])
|
|
1013
|
+
].sort((left, right) => left.localeCompare(right));
|
|
1014
|
+
return Object.fromEntries(
|
|
1015
|
+
keys.map((key) => {
|
|
1016
|
+
const currentCount = current[key] ?? 0;
|
|
1017
|
+
const previousCount = previous[key] ?? 0;
|
|
1018
|
+
const currentShare = currentTotal > 0 ? currentCount / currentTotal : 0;
|
|
1019
|
+
const previousShare = previousTotal > 0 ? previousCount / previousTotal : 0;
|
|
1020
|
+
return [
|
|
1021
|
+
key,
|
|
1022
|
+
{
|
|
1023
|
+
currentCount,
|
|
1024
|
+
previousCount,
|
|
1025
|
+
currentShare: round(currentShare),
|
|
1026
|
+
previousShare: round(previousShare),
|
|
1027
|
+
shareDelta: round(currentShare - previousShare)
|
|
1028
|
+
}
|
|
1029
|
+
];
|
|
1030
|
+
})
|
|
1031
|
+
);
|
|
1032
|
+
};
|
|
1033
|
+
var sum = (values) => round(values.reduce((total, value) => total + (value ?? 0), 0));
|
|
1034
|
+
var tradeTimestamp = (trade) => finiteNumber2(trade.exitTimestamp) ?? finiteNumber2(trade.lastSyncedAt) ?? finiteNumber2(trade.entryTimestamp) ?? 0;
|
|
1035
|
+
var dedupeTrades = (artifacts) => {
|
|
1036
|
+
const tradesByOrderId = /* @__PURE__ */ new Map();
|
|
1037
|
+
for (const artifact of artifacts) {
|
|
1038
|
+
for (const trade of extractRuntimeRows(artifact).trades) {
|
|
1039
|
+
const orderId = finiteString2(trade.orderId);
|
|
1040
|
+
if (!orderId) continue;
|
|
1041
|
+
const existing = tradesByOrderId.get(orderId);
|
|
1042
|
+
if (!existing || tradeTimestamp(trade) >= tradeTimestamp(existing)) {
|
|
1043
|
+
tradesByOrderId.set(orderId, trade);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return [...tradesByOrderId.values()];
|
|
1048
|
+
};
|
|
1049
|
+
var maxDrawdown = (trades) => {
|
|
1050
|
+
let cumulative = 0;
|
|
1051
|
+
let peak = 0;
|
|
1052
|
+
let drawdown = 0;
|
|
1053
|
+
for (const trade of trades.sort(
|
|
1054
|
+
(left, right) => tradeTimestamp(left) - tradeTimestamp(right)
|
|
1055
|
+
)) {
|
|
1056
|
+
cumulative += finiteNumber2(trade.closedPnl) ?? 0;
|
|
1057
|
+
peak = Math.max(peak, cumulative);
|
|
1058
|
+
drawdown = Math.max(drawdown, peak - cumulative);
|
|
1059
|
+
}
|
|
1060
|
+
return round(drawdown);
|
|
1061
|
+
};
|
|
1062
|
+
var buildRollingPerformance = ({
|
|
1063
|
+
trades,
|
|
1064
|
+
anchorTime,
|
|
1065
|
+
days
|
|
1066
|
+
}) => {
|
|
1067
|
+
const startTime = anchorTime - days * 24 * 60 * 60 * 1e3;
|
|
1068
|
+
const closed = trades.filter((trade) => {
|
|
1069
|
+
const exitTimestamp = finiteNumber2(trade.exitTimestamp);
|
|
1070
|
+
return trade.status === "closed" && exitTimestamp != null && exitTimestamp >= startTime && exitTimestamp < anchorTime;
|
|
1071
|
+
});
|
|
1072
|
+
const realizedPnl = sum(closed.map((trade) => finiteNumber2(trade.closedPnl)));
|
|
1073
|
+
return {
|
|
1074
|
+
days,
|
|
1075
|
+
closedTrades: closed.length,
|
|
1076
|
+
realizedPnl,
|
|
1077
|
+
expectancy: closed.length > 0 ? round(realizedPnl / closed.length) : null,
|
|
1078
|
+
maxDrawdown: maxDrawdown(closed)
|
|
1079
|
+
};
|
|
1080
|
+
};
|
|
1081
|
+
var getReplayComparison = (artifact) => {
|
|
1082
|
+
const root = asRecord2(artifact) ?? {};
|
|
1083
|
+
return asRecord2(asRecord2(root.replay)?.runtimeComparison) ?? null;
|
|
1084
|
+
};
|
|
1085
|
+
var replayCountsForStrategy = (replayComparison, strategy) => {
|
|
1086
|
+
if (strategy) {
|
|
1087
|
+
const byStrategy = asRecord2(replayComparison?.byStrategy);
|
|
1088
|
+
const strategyCounts = asRecord2(byStrategy?.[strategy]);
|
|
1089
|
+
if (strategyCounts) {
|
|
1090
|
+
return {
|
|
1091
|
+
matched: finiteNumber2(strategyCounts.matched) ?? 0,
|
|
1092
|
+
backtestOnly: finiteNumber2(strategyCounts.backtestOnly) ?? 0,
|
|
1093
|
+
runtimeOnly: finiteNumber2(strategyCounts.runtimeOnly) ?? 0
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const counts = asRecord2(replayComparison?.counts);
|
|
1098
|
+
return {
|
|
1099
|
+
matched: finiteNumber2(counts?.matched) ?? 0,
|
|
1100
|
+
backtestOnly: finiteNumber2(counts?.backtestOnly) ?? 0,
|
|
1101
|
+
runtimeOnly: finiteNumber2(counts?.runtimeOnly) ?? 0
|
|
1102
|
+
};
|
|
1103
|
+
};
|
|
1104
|
+
var getProspectiveSummary = (artifact) => {
|
|
1105
|
+
const root = asRecord2(artifact);
|
|
1106
|
+
if (!root || root.reportType !== "strategy-prospective-evidence") return null;
|
|
1107
|
+
return {
|
|
1108
|
+
rawCoreExpectancy: finiteNumber2(root.rawCoreExpectancy),
|
|
1109
|
+
aiGateExpectancy: finiteNumber2(root.aiGateExpectancy),
|
|
1110
|
+
regimeCoverage: finiteNumber2(root.regimeCoverage)
|
|
1111
|
+
};
|
|
1112
|
+
};
|
|
1113
|
+
var buildRuntimeScorecard = ({
|
|
1114
|
+
runtimeArtifact,
|
|
1115
|
+
replayEvidenceArtifact,
|
|
1116
|
+
calibrationArtifact,
|
|
1117
|
+
prospectiveEvidenceArtifact,
|
|
1118
|
+
historyRuntimeArtifacts = [],
|
|
1119
|
+
thresholds = {
|
|
1120
|
+
minimumParityRatio: 0.95,
|
|
1121
|
+
maximumSlippageResidualBps: 3,
|
|
1122
|
+
minimumClosedTrades: 20,
|
|
1123
|
+
minimumExpectancy: 0
|
|
1124
|
+
},
|
|
1125
|
+
generatedAt = Date.now(),
|
|
1126
|
+
strategy = null,
|
|
1127
|
+
llmComparatorPolicy = "ai_approved_only"
|
|
1128
|
+
}) => {
|
|
1129
|
+
(0, runtimeEvidenceDeployment_exports.assertCurrentRuntimeEvidenceArtifact)(runtimeArtifact);
|
|
1130
|
+
for (const artifact of historyRuntimeArtifacts) {
|
|
1131
|
+
(0, runtimeEvidenceDeployment_exports.assertCurrentRuntimeEvidenceArtifact)(artifact);
|
|
1132
|
+
}
|
|
1133
|
+
const scopedRuntimeArtifact = filterArtifactByStrategy(
|
|
1134
|
+
runtimeArtifact,
|
|
1135
|
+
strategy
|
|
1136
|
+
);
|
|
1137
|
+
const scopedHistoryArtifacts = historyRuntimeArtifacts.map(
|
|
1138
|
+
(artifact) => filterArtifactByStrategy(artifact, strategy)
|
|
1139
|
+
);
|
|
1140
|
+
const deploymentBinding = runtimeDeploymentBinding(runtimeArtifact);
|
|
1141
|
+
const rows = extractRuntimeRows(scopedRuntimeArtifact);
|
|
1142
|
+
const embeddedLineageScope = buildEmbeddedLineageScope({
|
|
1143
|
+
artifact: runtimeArtifact,
|
|
1144
|
+
rows,
|
|
1145
|
+
strategy
|
|
1146
|
+
});
|
|
1147
|
+
if (!embeddedLineageScope) {
|
|
1148
|
+
throw new Error(
|
|
1149
|
+
"Runtime scorecard requires the current embedded deployment lineage"
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
const runtimeLineage = embeddedLineageScope.summary;
|
|
1153
|
+
const window = runtimeWindow(runtimeArtifact);
|
|
1154
|
+
const startTime = finiteNumber2(window.startTime) ?? generatedAt - 864e5;
|
|
1155
|
+
const endTime = finiteNumber2(window.endTime) ?? generatedAt;
|
|
1156
|
+
const signalEvaluations = rows.evaluations.filter(
|
|
1157
|
+
(evaluation) => evaluation.status === "signal"
|
|
1158
|
+
);
|
|
1159
|
+
const gateDecisions = signalEvaluations.map(resolveGateDecision).filter(
|
|
1160
|
+
(decision) => Boolean(decision)
|
|
1161
|
+
);
|
|
1162
|
+
const llmEligibleEvaluations = llmComparatorPolicy === "disabled" ? [] : llmComparatorPolicy === "ai_approved_only" ? signalEvaluations.filter(
|
|
1163
|
+
(evaluation) => resolveGateDecision(evaluation)?.decision === "approved"
|
|
1164
|
+
) : signalEvaluations;
|
|
1165
|
+
const gateComparisons = llmEligibleEvaluations.map((evaluation) => {
|
|
1166
|
+
const analysis = asRecord2(evaluation.aiAnalysis);
|
|
1167
|
+
const gateDecision = resolveGateDecision(evaluation)?.decision ?? null;
|
|
1168
|
+
const llmDecision = finiteString2(analysis?.llmDecision);
|
|
1169
|
+
if (gateDecision !== "approved" && gateDecision !== "rejected" || llmDecision !== "approved" && llmDecision !== "rejected") {
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
return { gateDecision, llmDecision };
|
|
1173
|
+
}).filter(
|
|
1174
|
+
(comparison) => Boolean(comparison)
|
|
1175
|
+
);
|
|
1176
|
+
const allocatorDecisions = signalEvaluations.map((evaluation) => asRecord2(evaluation.allocatorDecision)).filter((item) => item != null);
|
|
1177
|
+
const riskDecisions = signalEvaluations.map((evaluation) => asRecord2(evaluation.riskDecision)).filter((item) => item != null);
|
|
1178
|
+
const orderAttempts = rows.signals.filter(
|
|
1179
|
+
(signal) => signal.orderStatus === "completed" || signal.orderStatus === "failed"
|
|
1180
|
+
);
|
|
1181
|
+
const currentClosedTrades = rows.trades.filter((trade) => {
|
|
1182
|
+
const exitTimestamp = finiteNumber2(trade.exitTimestamp);
|
|
1183
|
+
return trade.status === "closed" && exitTimestamp != null && exitTimestamp >= startTime && exitTimestamp < endTime;
|
|
1184
|
+
});
|
|
1185
|
+
const replayComparison = getReplayComparison(replayEvidenceArtifact);
|
|
1186
|
+
const replayCounts = replayCountsForStrategy(replayComparison, strategy);
|
|
1187
|
+
const matched = replayCounts.matched;
|
|
1188
|
+
const backtestOnly = replayCounts.backtestOnly;
|
|
1189
|
+
const runtimeOnly = replayCounts.runtimeOnly;
|
|
1190
|
+
const comparisonTotal = matched + backtestOnly + runtimeOnly;
|
|
1191
|
+
const parityRatio = comparisonTotal > 0 ? matched / comparisonTotal : null;
|
|
1192
|
+
const lineage = asRecord2(replayComparison?.lineage);
|
|
1193
|
+
const lineageReason = finiteString2(lineage?.reason);
|
|
1194
|
+
const calibrationRoot = asRecord2(calibrationArtifact) ?? {};
|
|
1195
|
+
const calibrationSamples = asArray2(calibrationRoot.samples).map(asRecord2).filter((item) => item != null).filter((item) => !strategy || finiteString2(item.strategy) === strategy).filter((item) => embeddedLineageScope.belongs(item));
|
|
1196
|
+
const averageSampleMetric = (field) => {
|
|
1197
|
+
const values = calibrationSamples.map((sample) => finiteNumber2(sample[field])).filter((value) => value != null);
|
|
1198
|
+
return values.length ? round(sum(values) / values.length) : null;
|
|
1199
|
+
};
|
|
1200
|
+
const actualSlippageBps = averageSampleMetric("signalToFillAdverseBps");
|
|
1201
|
+
const residualVsModelBps = averageSampleMetric("residualVsCurrentModelBps");
|
|
1202
|
+
const signalToArrivalAdverseBps = averageSampleMetric(
|
|
1203
|
+
"signalToArrivalAdverseBps"
|
|
1204
|
+
);
|
|
1205
|
+
const arrivalToFillAdverseBps = averageSampleMetric(
|
|
1206
|
+
"arrivalToFillAdverseBps"
|
|
1207
|
+
);
|
|
1208
|
+
const averageSignalCloseToSubmitMs = averageSampleMetric(
|
|
1209
|
+
"signalCloseToSubmitMs"
|
|
1210
|
+
);
|
|
1211
|
+
const averageSubmitToFillMs = averageSampleMetric("submitToFillMs");
|
|
1212
|
+
const fullTelemetryTrades = calibrationSamples.filter(
|
|
1213
|
+
(sample) => finiteString2(sample.telemetryQuality) === "full"
|
|
1214
|
+
).length;
|
|
1215
|
+
const arrivalQuoteTrades = calibrationSamples.filter(
|
|
1216
|
+
(sample) => finiteNumber2(sample.arrivalMid) != null
|
|
1217
|
+
).length;
|
|
1218
|
+
const arrivalQuoteCoverage = calibrationSamples.length ? round(arrivalQuoteTrades / calibrationSamples.length) : null;
|
|
1219
|
+
const hasTelemetryQuality = calibrationSamples.some(
|
|
1220
|
+
(sample) => finiteString2(sample.telemetryQuality) != null
|
|
1221
|
+
);
|
|
1222
|
+
const maximumSignalCloseToSubmitMs = thresholds.maximumSignalCloseToSubmitMs ?? 3e4;
|
|
1223
|
+
const prospective = getProspectiveSummary(prospectiveEvidenceArtifact);
|
|
1224
|
+
const historyTrades = dedupeTrades([
|
|
1225
|
+
...scopedHistoryArtifacts,
|
|
1226
|
+
scopedRuntimeArtifact
|
|
1227
|
+
]);
|
|
1228
|
+
const comparableHistoryTrades = historyTrades.filter(
|
|
1229
|
+
(trade) => belongsToRuntimeDeployment(trade, deploymentBinding) && embeddedLineageScope.belongs(trade)
|
|
1230
|
+
);
|
|
1231
|
+
const comparableCurrentClosedTrades = currentClosedTrades.filter(
|
|
1232
|
+
(trade) => belongsToRuntimeDeployment(trade, deploymentBinding) && embeddedLineageScope.belongs(trade)
|
|
1233
|
+
);
|
|
1234
|
+
const comparableFills = rows.trades.filter(
|
|
1235
|
+
(trade) => belongsToRuntimeDeployment(trade, deploymentBinding) && embeddedLineageScope.belongs(trade)
|
|
1236
|
+
);
|
|
1237
|
+
const currentDistributions = buildDistributions(rows);
|
|
1238
|
+
const previousArtifact = scopedHistoryArtifacts.map((artifact) => ({ artifact, window: runtimeWindow(artifact) })).filter(({ window: candidateWindow }) => {
|
|
1239
|
+
const candidateEndTime = finiteNumber2(candidateWindow.endTime);
|
|
1240
|
+
return candidateEndTime != null && candidateEndTime <= startTime;
|
|
1241
|
+
}).sort(
|
|
1242
|
+
(left, right) => (finiteNumber2(right.window.endTime) ?? 0) - (finiteNumber2(left.window.endTime) ?? 0)
|
|
1243
|
+
)[0];
|
|
1244
|
+
const previousDistributions = previousArtifact ? buildDistributions(extractRuntimeRows(previousArtifact.artifact)) : null;
|
|
1245
|
+
const rolling = [7, 30, 90].map(
|
|
1246
|
+
(days) => buildRollingPerformance({
|
|
1247
|
+
trades: [...comparableHistoryTrades],
|
|
1248
|
+
anchorTime: endTime,
|
|
1249
|
+
days
|
|
1250
|
+
})
|
|
1251
|
+
);
|
|
1252
|
+
const sevenDay = rolling[0];
|
|
1253
|
+
const reactions = [];
|
|
1254
|
+
if (lineageReason) {
|
|
1255
|
+
reactions.push({
|
|
1256
|
+
code: "LINEAGE_NOT_COMPARABLE",
|
|
1257
|
+
severity: "blocking",
|
|
1258
|
+
message: lineageReason
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
if (!runtimeLineage.complete) {
|
|
1262
|
+
reactions.push({
|
|
1263
|
+
code: "RUNTIME_LINEAGE_INCOMPLETE",
|
|
1264
|
+
severity: "blocking",
|
|
1265
|
+
message: "Runtime rows do not share one complete composition lineage."
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
if (parityRatio != null && parityRatio < thresholds.minimumParityRatio) {
|
|
1269
|
+
reactions.push({
|
|
1270
|
+
code: "PARITY_REGRESSION",
|
|
1271
|
+
severity: "blocking",
|
|
1272
|
+
message: `Parity ${round(parityRatio * 100, 2)}% is below ${round(thresholds.minimumParityRatio * 100, 2)}%.`
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
if (residualVsModelBps != null && residualVsModelBps > thresholds.maximumSlippageResidualBps) {
|
|
1276
|
+
reactions.push({
|
|
1277
|
+
code: "SLIPPAGE_DRIFT",
|
|
1278
|
+
severity: "warning",
|
|
1279
|
+
message: `Signal-close-to-fill residual ${round(residualVsModelBps, 2)} bps exceeds ${thresholds.maximumSlippageResidualBps} bps.`
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
if (averageSignalCloseToSubmitMs != null && averageSignalCloseToSubmitMs > maximumSignalCloseToSubmitMs) {
|
|
1283
|
+
reactions.push({
|
|
1284
|
+
code: "PRE_SUBMIT_LATENCY",
|
|
1285
|
+
severity: "warning",
|
|
1286
|
+
message: `Average signal-close-to-submit latency ${round(averageSignalCloseToSubmitMs, 3)} ms exceeds ${maximumSignalCloseToSubmitMs} ms.`
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
if (hasTelemetryQuality && calibrationSamples.length > 0 && fullTelemetryTrades < calibrationSamples.length) {
|
|
1290
|
+
reactions.push({
|
|
1291
|
+
code: "EXECUTION_TELEMETRY_INCOMPLETE",
|
|
1292
|
+
severity: "info",
|
|
1293
|
+
message: `Full arrival and fill telemetry is available for ${fullTelemetryTrades}/${calibrationSamples.length} trades.`
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
if (riskDecisions.some((decision) => decision.status === "rejected")) {
|
|
1297
|
+
reactions.push({
|
|
1298
|
+
code: "RISK_PLAN_EXCEEDS_DECLARED_LIMIT",
|
|
1299
|
+
severity: "warning",
|
|
1300
|
+
message: "At least one observed order plan exceeds its declared MAX_LOSS_VALUE; telemetry is not yet enforced."
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
if (sevenDay.closedTrades >= thresholds.minimumClosedTrades && sevenDay.expectancy != null && sevenDay.expectancy < thresholds.minimumExpectancy) {
|
|
1304
|
+
reactions.push({
|
|
1305
|
+
code: "EXPECTANCY_DEGRADATION",
|
|
1306
|
+
severity: "blocking",
|
|
1307
|
+
message: `7d expectancy ${sevenDay.expectancy} is below ${thresholds.minimumExpectancy}.`
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
if (sevenDay.closedTrades < thresholds.minimumClosedTrades) {
|
|
1311
|
+
reactions.push({
|
|
1312
|
+
code: "INSUFFICIENT_RECENT_SAMPLE",
|
|
1313
|
+
severity: "info",
|
|
1314
|
+
message: `Only ${sevenDay.closedTrades} closed trades in 7d; ${thresholds.minimumClosedTrades} required for expectancy reaction.`
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
const promotionStatus = reactions.some(
|
|
1318
|
+
(reaction) => reaction.severity === "blocking"
|
|
1319
|
+
) ? "PROMOTION_BLOCKED" : reactions.some((reaction) => reaction.severity === "warning") ? "REVIEW_REQUIRED" : "OBSERVE";
|
|
1320
|
+
return {
|
|
1321
|
+
reportType: "runtime-scorecard",
|
|
1322
|
+
schemaVersion: 1,
|
|
1323
|
+
generatedAt,
|
|
1324
|
+
window: { startTime, endTime },
|
|
1325
|
+
strategy,
|
|
1326
|
+
deployment: deploymentBinding,
|
|
1327
|
+
lineage: runtimeLineage,
|
|
1328
|
+
promotionStatus,
|
|
1329
|
+
thresholds: {
|
|
1330
|
+
...thresholds,
|
|
1331
|
+
maximumSignalCloseToSubmitMs
|
|
1332
|
+
},
|
|
1333
|
+
funnel: {
|
|
1334
|
+
evaluations: rows.evaluations.length,
|
|
1335
|
+
coreCandidates: signalEvaluations.length,
|
|
1336
|
+
gate: {
|
|
1337
|
+
available: gateDecisions.length > 0,
|
|
1338
|
+
approved: gateDecisions.filter(
|
|
1339
|
+
({ decision }) => decision === "approved"
|
|
1340
|
+
).length,
|
|
1341
|
+
rejected: gateDecisions.filter(
|
|
1342
|
+
({ decision }) => decision === "rejected"
|
|
1343
|
+
).length,
|
|
1344
|
+
coverage: signalEvaluations.length ? round(gateDecisions.length / signalEvaluations.length) : null,
|
|
1345
|
+
legacyApprovedFallback: gateDecisions.filter(
|
|
1346
|
+
({ source }) => source === "legacy_approved_fallback"
|
|
1347
|
+
).length
|
|
1348
|
+
},
|
|
1349
|
+
allocator: {
|
|
1350
|
+
available: allocatorDecisions.length > 0,
|
|
1351
|
+
approved: allocatorDecisions.filter(
|
|
1352
|
+
(decision) => decision.status === "approved"
|
|
1353
|
+
).length,
|
|
1354
|
+
rejected: allocatorDecisions.filter(
|
|
1355
|
+
(decision) => decision.status === "rejected"
|
|
1356
|
+
).length,
|
|
1357
|
+
notApplicable: allocatorDecisions.filter(
|
|
1358
|
+
(decision) => decision.status === "not_applicable"
|
|
1359
|
+
).length,
|
|
1360
|
+
unavailable: allocatorDecisions.filter(
|
|
1361
|
+
(decision) => decision.status === "unavailable"
|
|
1362
|
+
).length
|
|
1363
|
+
},
|
|
1364
|
+
risk: {
|
|
1365
|
+
available: riskDecisions.length > 0,
|
|
1366
|
+
approved: riskDecisions.filter(
|
|
1367
|
+
(decision) => decision.status === "approved"
|
|
1368
|
+
).length,
|
|
1369
|
+
rejected: riskDecisions.filter(
|
|
1370
|
+
(decision) => decision.status === "rejected"
|
|
1371
|
+
).length,
|
|
1372
|
+
unavailable: riskDecisions.filter(
|
|
1373
|
+
(decision) => decision.status === "unavailable"
|
|
1374
|
+
).length,
|
|
1375
|
+
enforced: riskDecisions.some((decision) => decision.enforced === true)
|
|
1376
|
+
},
|
|
1377
|
+
orderAttempts: orderAttempts.length,
|
|
1378
|
+
orderFailures: orderAttempts.filter(
|
|
1379
|
+
(signal) => signal.orderStatus === "failed"
|
|
1380
|
+
).length,
|
|
1381
|
+
balanceRejects: rows.signals.filter(
|
|
1382
|
+
(signal) => /BALANCE|MARGIN/i.test(
|
|
1383
|
+
`${finiteString2(signal.orderSkipReason) ?? ""} ${finiteString2(signal.orderFailureReason) ?? ""}`
|
|
1384
|
+
)
|
|
1385
|
+
).length,
|
|
1386
|
+
fills: comparableFills.length,
|
|
1387
|
+
nonComparableFills: rows.trades.length - comparableFills.length,
|
|
1388
|
+
closedTrades: currentClosedTrades.length,
|
|
1389
|
+
comparableClosedTrades: comparableCurrentClosedTrades.length,
|
|
1390
|
+
nonComparableClosedTrades: currentClosedTrades.length - comparableCurrentClosedTrades.length
|
|
1391
|
+
},
|
|
1392
|
+
realized: {
|
|
1393
|
+
pnl: sum(
|
|
1394
|
+
comparableCurrentClosedTrades.map(
|
|
1395
|
+
(trade) => finiteNumber2(trade.closedPnl)
|
|
1396
|
+
)
|
|
1397
|
+
),
|
|
1398
|
+
fees: sum(
|
|
1399
|
+
comparableCurrentClosedTrades.map(
|
|
1400
|
+
(trade) => finiteNumber2(trade.totalFee)
|
|
1401
|
+
)
|
|
1402
|
+
),
|
|
1403
|
+
funding: sum(
|
|
1404
|
+
comparableCurrentClosedTrades.map(
|
|
1405
|
+
(trade) => finiteNumber2(trade.fundingFee)
|
|
1406
|
+
)
|
|
1407
|
+
)
|
|
1408
|
+
},
|
|
1409
|
+
gateComparison: {
|
|
1410
|
+
policy: llmComparatorPolicy,
|
|
1411
|
+
eligible: llmEligibleEvaluations.length,
|
|
1412
|
+
compared: gateComparisons.length,
|
|
1413
|
+
coverage: llmEligibleEvaluations.length ? round(gateComparisons.length / llmEligibleEvaluations.length) : null,
|
|
1414
|
+
agreements: gateComparisons.filter(
|
|
1415
|
+
(comparison) => comparison.gateDecision === comparison.llmDecision
|
|
1416
|
+
).length,
|
|
1417
|
+
disagreements: gateComparisons.filter(
|
|
1418
|
+
(comparison) => comparison.gateDecision !== comparison.llmDecision
|
|
1419
|
+
).length,
|
|
1420
|
+
gateApprovedLlmRejected: gateComparisons.filter(
|
|
1421
|
+
(comparison) => comparison.gateDecision === "approved" && comparison.llmDecision === "rejected"
|
|
1422
|
+
).length,
|
|
1423
|
+
gateRejectedLlmApproved: gateComparisons.filter(
|
|
1424
|
+
(comparison) => comparison.gateDecision === "rejected" && comparison.llmDecision === "approved"
|
|
1425
|
+
).length
|
|
1426
|
+
},
|
|
1427
|
+
parity: {
|
|
1428
|
+
available: comparisonTotal > 0,
|
|
1429
|
+
matched,
|
|
1430
|
+
backtestOnly,
|
|
1431
|
+
runtimeOnly,
|
|
1432
|
+
ratio: parityRatio == null ? null : round(parityRatio),
|
|
1433
|
+
lineageReason
|
|
1434
|
+
},
|
|
1435
|
+
execution: {
|
|
1436
|
+
available: actualSlippageBps != null || residualVsModelBps != null,
|
|
1437
|
+
metricScope: arrivalToFillAdverseBps == null ? "signal_close_to_fill" : "signal_and_exchange_components",
|
|
1438
|
+
samples: calibrationSamples.length,
|
|
1439
|
+
fullTelemetryTrades,
|
|
1440
|
+
arrivalQuoteCoverage,
|
|
1441
|
+
actualSignalToFillSlippageBps: actualSlippageBps,
|
|
1442
|
+
residualVsCurrentModelBps: residualVsModelBps,
|
|
1443
|
+
signalToArrivalAdverseBps,
|
|
1444
|
+
arrivalToFillAdverseBps,
|
|
1445
|
+
averageSignalCloseToSubmitMs,
|
|
1446
|
+
averageSubmitToFillMs
|
|
1447
|
+
},
|
|
1448
|
+
prospective,
|
|
1449
|
+
rolling,
|
|
1450
|
+
distributions: currentDistributions,
|
|
1451
|
+
distributionChanges: {
|
|
1452
|
+
available: previousDistributions != null,
|
|
1453
|
+
baselineWindow: previousArtifact ? {
|
|
1454
|
+
startTime: finiteNumber2(previousArtifact.window.startTime),
|
|
1455
|
+
endTime: finiteNumber2(previousArtifact.window.endTime)
|
|
1456
|
+
} : null,
|
|
1457
|
+
metrics: previousDistributions ? Object.fromEntries(
|
|
1458
|
+
Object.entries(currentDistributions).map(([name, current]) => [
|
|
1459
|
+
name,
|
|
1460
|
+
buildDistributionDelta(
|
|
1461
|
+
current,
|
|
1462
|
+
previousDistributions[name]
|
|
1463
|
+
)
|
|
1464
|
+
])
|
|
1465
|
+
) : {}
|
|
1466
|
+
},
|
|
1467
|
+
reactions
|
|
1468
|
+
};
|
|
1469
|
+
};
|
|
1470
|
+
var formatRuntimeScorecardMarkdown = (scorecard) => {
|
|
1471
|
+
const rollingRows = scorecard.rolling.map(
|
|
1472
|
+
(row) => `| ${row.days}d | ${row.closedTrades} | ${row.realizedPnl} | ${row.expectancy ?? "n/a"} | ${row.maxDrawdown} |`
|
|
1473
|
+
).join("\n");
|
|
1474
|
+
const reactions = scorecard.reactions.length ? scorecard.reactions.map(
|
|
1475
|
+
(reaction) => `- ${reaction.severity.toUpperCase()} \`${reaction.code}\`: ${reaction.message}`
|
|
1476
|
+
).join("\n") : "- No automatic reactions.";
|
|
1477
|
+
const distributionChanges = Object.entries(
|
|
1478
|
+
scorecard.distributionChanges.metrics
|
|
1479
|
+
).flatMap(
|
|
1480
|
+
([metric, values]) => Object.entries(values).map(([value, change]) => ({
|
|
1481
|
+
metric,
|
|
1482
|
+
value,
|
|
1483
|
+
shareDelta: change.shareDelta
|
|
1484
|
+
}))
|
|
1485
|
+
).sort(
|
|
1486
|
+
(left, right) => Math.abs(right.shareDelta) - Math.abs(left.shareDelta)
|
|
1487
|
+
).slice(0, 10).map(
|
|
1488
|
+
(change) => `- ${change.metric}.${change.value}: ${change.shareDelta >= 0 ? "+" : ""}${round(change.shareDelta * 100, 2)} pp`
|
|
1489
|
+
).join("\n");
|
|
1490
|
+
return `# Runtime scorecard
|
|
1491
|
+
|
|
1492
|
+
Status: **${scorecard.promotionStatus}**
|
|
1493
|
+
|
|
1494
|
+
Window: ${new Date(scorecard.window.startTime).toISOString()} \u2014 ${new Date(scorecard.window.endTime).toISOString()}
|
|
1495
|
+
|
|
1496
|
+
## Funnel
|
|
1497
|
+
|
|
1498
|
+
- Evaluations: ${scorecard.funnel.evaluations}
|
|
1499
|
+
- Core candidates: ${scorecard.funnel.coreCandidates}
|
|
1500
|
+
- Gate approvals/rejects: ${scorecard.funnel.gate.approved}/${scorecard.funnel.gate.rejected} (legacy fallback: ${scorecard.funnel.gate.legacyApprovedFallback})
|
|
1501
|
+
- AI / LLM disagreement: ${scorecard.gateComparison.disagreements}/${scorecard.gateComparison.compared}
|
|
1502
|
+
- Runtime admission approved/rejected/not-applicable: ${scorecard.funnel.allocator.available ? `${scorecard.funnel.allocator.approved}/${scorecard.funnel.allocator.rejected}/${scorecard.funnel.allocator.notApplicable}` : "unavailable"}
|
|
1503
|
+
- Risk assessment within/exceeds/unavailable: ${scorecard.funnel.risk.available ? `${scorecard.funnel.risk.approved}/${scorecard.funnel.risk.rejected}/${scorecard.funnel.risk.unavailable}` : "unavailable"} (enforced: ${scorecard.funnel.risk.enforced})
|
|
1504
|
+
- Order attempts/failures: ${scorecard.funnel.orderAttempts}/${scorecard.funnel.orderFailures}
|
|
1505
|
+
- Fills: ${scorecard.funnel.fills}
|
|
1506
|
+
- Closed trades: ${scorecard.funnel.closedTrades}
|
|
1507
|
+
|
|
1508
|
+
## Replay and execution
|
|
1509
|
+
|
|
1510
|
+
- Parity: ${scorecard.parity.ratio == null ? "n/a" : `${round(scorecard.parity.ratio * 100, 2)}%`}
|
|
1511
|
+
- Runtime/replay mismatches: ${scorecard.parity.backtestOnly + scorecard.parity.runtimeOnly}
|
|
1512
|
+
- Execution metric scope: ${scorecard.execution.metricScope}
|
|
1513
|
+
- Telemetry samples/full/arrival coverage: ${scorecard.execution.samples}/${scorecard.execution.fullTelemetryTrades}/${scorecard.execution.arrivalQuoteCoverage == null ? "n/a" : `${round(scorecard.execution.arrivalQuoteCoverage * 100, 2)}%`}
|
|
1514
|
+
- Signal-close-to-fill adverse move: ${scorecard.execution.actualSignalToFillSlippageBps ?? "n/a"} bps
|
|
1515
|
+
- Signal-to-arrival adverse move: ${scorecard.execution.signalToArrivalAdverseBps ?? "n/a"} bps
|
|
1516
|
+
- Arrival-to-fill exchange slippage: ${scorecard.execution.arrivalToFillAdverseBps ?? "n/a"} bps
|
|
1517
|
+
- Residual vs current model: ${scorecard.execution.residualVsCurrentModelBps ?? "n/a"} bps
|
|
1518
|
+
- Average signal-close-to-submit: ${scorecard.execution.averageSignalCloseToSubmitMs ?? "n/a"} ms
|
|
1519
|
+
- Average submit-to-fill: ${scorecard.execution.averageSubmitToFillMs ?? "n/a"} ms
|
|
1520
|
+
|
|
1521
|
+
## Rolling performance
|
|
1522
|
+
|
|
1523
|
+
| Window | Closed | Realized PnL | Expectancy | Max drawdown |
|
|
1524
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
1525
|
+
${rollingRows}
|
|
1526
|
+
|
|
1527
|
+
## Distribution changes
|
|
1528
|
+
|
|
1529
|
+
${scorecard.distributionChanges.available ? distributionChanges || "- No distribution changes." : "- Previous comparable artifact is unavailable."}
|
|
1530
|
+
|
|
1531
|
+
## Reactions
|
|
1532
|
+
|
|
1533
|
+
${reactions}
|
|
1534
|
+
`;
|
|
1535
|
+
};
|
|
1536
|
+
|
|
1537
|
+
// src/lib/runtimeEvidenceSync.ts
|
|
1538
|
+
var import_node_child_process = require("child_process");
|
|
1539
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
1540
|
+
var import_node_path = __toESM(require("path"));
|
|
1541
|
+
var import_node_util = require("util");
|
|
1542
|
+
|
|
1543
|
+
// src/lib/runtimeEvidenceArtifacts.ts
|
|
1544
|
+
var runtimeEvidenceArtifacts_exports = {};
|
|
1545
|
+
__reExport(runtimeEvidenceArtifacts_exports, require("@tradejs/node/diagnostics"));
|
|
1546
|
+
|
|
1547
|
+
// src/lib/runtimeEvidenceSync.ts
|
|
1548
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
1549
|
+
var safeSegment = (value, fallback) => {
|
|
1550
|
+
const normalized = value.trim().replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
1551
|
+
return normalized && normalized !== "." && normalized !== ".." ? normalized : fallback;
|
|
1552
|
+
};
|
|
1553
|
+
var receiptPath = (receiptsRoot, artifactId) => import_node_path.default.join(receiptsRoot, `${safeSegment(artifactId, "artifact")}.json`);
|
|
1554
|
+
var markRuntimeEvidenceBundleProcessed = async ({
|
|
1555
|
+
evidenceRoot,
|
|
1556
|
+
deploymentId,
|
|
1557
|
+
bundleDir,
|
|
1558
|
+
scorecardPath,
|
|
1559
|
+
runtimeFeedback
|
|
1560
|
+
}) => {
|
|
1561
|
+
const safeDeploymentId = safeSegment(deploymentId, "production");
|
|
1562
|
+
const verified = await (0, runtimeEvidenceArtifacts_exports.verifyRuntimeEvidenceBundle)(bundleDir);
|
|
1563
|
+
if (verified.manifest.deploymentId !== safeDeploymentId) {
|
|
1564
|
+
throw new Error(
|
|
1565
|
+
`Artifact deployment mismatch: expected ${safeDeploymentId}, received ${verified.manifest.deploymentId}`
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
const receiptsRoot = import_node_path.default.join(evidenceRoot, "receipts", safeDeploymentId);
|
|
1569
|
+
const finalPath = receiptPath(receiptsRoot, verified.manifest.artifactId);
|
|
1570
|
+
const temporaryPath = `${finalPath}.tmp-${process.pid}`;
|
|
1571
|
+
const receipt = {
|
|
1572
|
+
schemaVersion: runtimeFeedback ? 2 : 1,
|
|
1573
|
+
artifactId: verified.manifest.artifactId,
|
|
1574
|
+
payloadSha256: verified.manifest.payload.sha256,
|
|
1575
|
+
processedAt: Date.now(),
|
|
1576
|
+
scorecardPath: scorecardPath ?? null,
|
|
1577
|
+
...runtimeFeedback ? {
|
|
1578
|
+
runtimeFeedbackArtifactId: runtimeFeedback.artifactId,
|
|
1579
|
+
replayEvidenceSha256: runtimeFeedback.replayEvidenceSha256
|
|
1580
|
+
} : {}
|
|
1581
|
+
};
|
|
1582
|
+
await import_promises.default.mkdir(receiptsRoot, { recursive: true });
|
|
1583
|
+
await import_promises.default.writeFile(
|
|
1584
|
+
temporaryPath,
|
|
1585
|
+
`${JSON.stringify(receipt, null, 2)}
|
|
1586
|
+
`,
|
|
1587
|
+
"utf8"
|
|
1588
|
+
);
|
|
1589
|
+
await import_promises.default.rename(temporaryPath, finalPath);
|
|
1590
|
+
return finalPath;
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1593
|
+
// src/lib/runtimeFeedbackProcess.ts
|
|
1594
|
+
var processRuntimeFeedbackPair = async ({
|
|
1595
|
+
runtimeDir,
|
|
1596
|
+
replayDir,
|
|
1597
|
+
evidenceRoot,
|
|
1598
|
+
outputRoot,
|
|
1599
|
+
historyDir
|
|
1600
|
+
}) => {
|
|
1601
|
+
const { runtimeEvidence, feedback } = await (0, import_diagnostics.verifyRuntimeFeedbackReplaySource)(
|
|
1602
|
+
{ bundleDir: replayDir, runtimeEvidenceBundleDir: runtimeDir }
|
|
1603
|
+
);
|
|
1604
|
+
const { artifactId, deploymentId } = runtimeEvidence.manifest;
|
|
1605
|
+
const snapshot = (0, import_diagnostics.parseRuntimeEvidenceDeploymentSnapshot)(
|
|
1606
|
+
runtimeEvidence.artifact.deployment
|
|
1607
|
+
);
|
|
1608
|
+
if (!snapshot)
|
|
1609
|
+
throw new Error("Runtime evidence requires a deployment snapshot");
|
|
1610
|
+
const strategies = (0, import_diagnostics.activeRuntimeEvidenceStrategies)(snapshot);
|
|
1611
|
+
for (const segment of [
|
|
1612
|
+
artifactId,
|
|
1613
|
+
deploymentId,
|
|
1614
|
+
...strategies.map((s) => s.strategyName)
|
|
1615
|
+
]) {
|
|
1616
|
+
if (!/^[A-Za-z0-9._:-]+$/.test(segment) || segment === "." || segment === "..")
|
|
1617
|
+
throw new Error("Invalid feedback output path identity");
|
|
1618
|
+
}
|
|
1619
|
+
const receiptPath2 = import_node_path2.default.join(
|
|
1620
|
+
evidenceRoot,
|
|
1621
|
+
"receipts",
|
|
1622
|
+
deploymentId,
|
|
1623
|
+
`${artifactId}.json`
|
|
1624
|
+
);
|
|
1625
|
+
await import_promises2.default.mkdir(import_node_path2.default.dirname(receiptPath2), { recursive: true });
|
|
1626
|
+
const lock = receiptPath2 + ".processing";
|
|
1627
|
+
await import_promises2.default.mkdir(lock);
|
|
1628
|
+
try {
|
|
1629
|
+
const previous = await import_promises2.default.readFile(receiptPath2, "utf8").catch((error) => {
|
|
1630
|
+
if (error.code !== "ENOENT") throw error;
|
|
1631
|
+
return null;
|
|
1632
|
+
});
|
|
1633
|
+
if (previous) {
|
|
1634
|
+
const receipt = JSON.parse(previous);
|
|
1635
|
+
if (!receipt || typeof receipt !== "object" || !("schemaVersion" in receipt) || receipt.schemaVersion !== 2 || !("payloadSha256" in receipt) || !("runtimeFeedbackArtifactId" in receipt) || !("replayEvidenceSha256" in receipt) || !("scorecardPath" in receipt) || receipt.payloadSha256 !== runtimeEvidence.manifest.payload.sha256 || receipt.runtimeFeedbackArtifactId !== feedback.manifest.artifactId || receipt.replayEvidenceSha256 !== feedback.manifest.payloads.replayEvidence.sha256 || typeof receipt.scorecardPath !== "string")
|
|
1636
|
+
throw new Error("Processing receipt conflicts with the exact pair");
|
|
1637
|
+
await import_promises2.default.access(receipt.scorecardPath);
|
|
1638
|
+
return {
|
|
1639
|
+
processed: false,
|
|
1640
|
+
receiptPath: receiptPath2,
|
|
1641
|
+
scorecardPath: receipt.scorecardPath
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
const history = [];
|
|
1645
|
+
if (historyDir)
|
|
1646
|
+
for (const directory of await (0, import_diagnostics.discoverRuntimeEvidenceBundles)(historyDir))
|
|
1647
|
+
history.push((await (0, import_diagnostics.verifyRuntimeEvidenceBundle)(directory)).artifact);
|
|
1648
|
+
const runId = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.]/g, "") + "-" + (0, import_node_crypto2.randomUUID)();
|
|
1649
|
+
const runDir = import_node_path2.default.join(outputRoot, artifactId, runId);
|
|
1650
|
+
await import_promises2.default.mkdir(runDir, { recursive: true });
|
|
1651
|
+
const calibration = buildExecutionCalibrationReport({
|
|
1652
|
+
runtimeArtifact: runtimeEvidence.artifact,
|
|
1653
|
+
replayEvidenceArtifact: feedback.replayEvidence
|
|
1654
|
+
});
|
|
1655
|
+
await import_promises2.default.writeFile(
|
|
1656
|
+
import_node_path2.default.join(runDir, "execution-calibration.json"),
|
|
1657
|
+
JSON.stringify(calibration, null, 2)
|
|
1658
|
+
);
|
|
1659
|
+
const inputs = {
|
|
1660
|
+
runtimeArtifact: runtimeEvidence.artifact,
|
|
1661
|
+
replayEvidenceArtifact: feedback.replayEvidence,
|
|
1662
|
+
calibrationArtifact: calibration,
|
|
1663
|
+
historyRuntimeArtifacts: history
|
|
1664
|
+
};
|
|
1665
|
+
for (const strategy of [...strategies.map((s) => s.strategyName), null]) {
|
|
1666
|
+
const scorecard = buildRuntimeScorecard({ ...inputs, strategy });
|
|
1667
|
+
const base = import_node_path2.default.join(
|
|
1668
|
+
runDir,
|
|
1669
|
+
strategy ? `runtime-scorecard-${strategy}` : "runtime-scorecard"
|
|
1670
|
+
);
|
|
1671
|
+
await import_promises2.default.writeFile(base + ".json", JSON.stringify(scorecard, null, 2));
|
|
1672
|
+
await import_promises2.default.writeFile(
|
|
1673
|
+
base + ".md",
|
|
1674
|
+
formatRuntimeScorecardMarkdown(scorecard)
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
await (0, import_diagnostics.verifyRuntimeFeedbackReplaySource)({
|
|
1678
|
+
bundleDir: replayDir,
|
|
1679
|
+
runtimeEvidenceBundleDir: runtimeDir
|
|
1680
|
+
});
|
|
1681
|
+
const scorecardPath = import_node_path2.default.join(runDir, "runtime-scorecard.json");
|
|
1682
|
+
await markRuntimeEvidenceBundleProcessed({
|
|
1683
|
+
evidenceRoot,
|
|
1684
|
+
deploymentId,
|
|
1685
|
+
bundleDir: runtimeDir,
|
|
1686
|
+
scorecardPath,
|
|
1687
|
+
runtimeFeedback: {
|
|
1688
|
+
artifactId: feedback.manifest.artifactId,
|
|
1689
|
+
replayEvidenceSha256: feedback.manifest.payloads.replayEvidence.sha256
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
return { processed: true, receiptPath: receiptPath2, scorecardPath };
|
|
1693
|
+
} finally {
|
|
1694
|
+
await import_promises2.default.rmdir(lock);
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1698
|
+
0 && (module.exports = {
|
|
1699
|
+
processRuntimeFeedbackPair
|
|
1700
|
+
});
|