@tradejs/cli 2.0.15 → 2.0.16
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 +2916 -978
- package/dist/lib/aiPocketSearch.js +493 -24
- package/dist/lib/aiTrainResearch.js +19 -2
- package/dist/lib/marketContextPrepare.js +85 -8
- package/dist/lib/researchAgent.js +3 -30
- package/dist/lib/runtimeDebugEvidence.js +76 -1
- package/dist/lib/runtimeEvidenceArtifacts.js +255 -0
- package/dist/lib/runtimeEvidenceSync.js +308 -0
- package/dist/lib/runtimeRedis.js +25 -0
- package/dist/lib/runtimeScorecard.js +442 -0
- package/dist/lib/runtimeTradeSync.js +9 -0
- package/dist/scripts/agentRun.js +16 -31
- package/dist/scripts/aiPocketSearch.js +717 -29
- package/dist/scripts/aiTrain.js +32 -2
- package/dist/scripts/backtest.js +83 -8
- package/dist/scripts/replay.js +92 -8
- package/dist/scripts/replayRunner.js +92 -8
- package/dist/scripts/replayRuntimeEvidence.js +68 -161
- package/dist/scripts/runtimeEvidence.js +316 -161
- package/dist/scripts/runtimeEvidenceSync.js +335 -0
- package/dist/scripts/runtimeParity.js +83 -8
- package/dist/scripts/runtimeScorecard.js +731 -0
- package/dist/scripts/signals.js +234 -142
- package/dist/scripts/signalsDaemon.js +234 -142
- package/dist/scripts/signalsSummary.js +9 -0
- package/dist/workers/testerWorker.js +2 -0
- package/package.json +11 -9
|
@@ -0,0 +1,442 @@
|
|
|
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/runtimeScorecard.ts
|
|
21
|
+
var runtimeScorecard_exports = {};
|
|
22
|
+
__export(runtimeScorecard_exports, {
|
|
23
|
+
buildRuntimeScorecard: () => buildRuntimeScorecard,
|
|
24
|
+
formatRuntimeScorecardMarkdown: () => formatRuntimeScorecardMarkdown
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(runtimeScorecard_exports);
|
|
27
|
+
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
28
|
+
var asArray = (value) => Array.isArray(value) ? value : [];
|
|
29
|
+
var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
30
|
+
var finiteString = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
|
|
31
|
+
var round = (value, decimals = 6) => Number(value.toFixed(decimals));
|
|
32
|
+
var unwrapRows = (value, key) => asArray(value).map((item) => asRecord(asRecord(item)?.[key]) ?? asRecord(item)).filter((item) => item != null);
|
|
33
|
+
var runtimePayload = (artifact) => {
|
|
34
|
+
const root = asRecord(artifact) ?? {};
|
|
35
|
+
return asRecord(root.runtime) ?? root;
|
|
36
|
+
};
|
|
37
|
+
var runtimeWindow = (artifact) => {
|
|
38
|
+
const root = asRecord(artifact) ?? {};
|
|
39
|
+
return asRecord(root.window) ?? asRecord(runtimePayload(artifact).window) ?? {};
|
|
40
|
+
};
|
|
41
|
+
var extractRuntimeRows = (artifact) => {
|
|
42
|
+
const runtime = runtimePayload(artifact);
|
|
43
|
+
return {
|
|
44
|
+
evaluations: unwrapRows(runtime.evaluations, "evaluation"),
|
|
45
|
+
signals: unwrapRows(runtime.signals, "signal"),
|
|
46
|
+
trades: unwrapRows(runtime.trades, "trade"),
|
|
47
|
+
statsBuckets: asArray(runtime.evaluationStatsBuckets).map(asRecord).filter((item) => item != null)
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
var countBy = (items, getter) => {
|
|
51
|
+
const counts = {};
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
const key = getter(item);
|
|
54
|
+
if (!key) continue;
|
|
55
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
56
|
+
}
|
|
57
|
+
return Object.fromEntries(
|
|
58
|
+
Object.entries(counts).sort(([left], [right]) => left.localeCompare(right))
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
var buildDistributions = ({
|
|
62
|
+
evaluations,
|
|
63
|
+
signals
|
|
64
|
+
}) => {
|
|
65
|
+
const candidates = evaluations.filter(
|
|
66
|
+
(evaluation) => evaluation.status === "signal"
|
|
67
|
+
);
|
|
68
|
+
return {
|
|
69
|
+
candidatesByStrategy: countBy(
|
|
70
|
+
candidates,
|
|
71
|
+
(evaluation) => finiteString(evaluation.strategy)
|
|
72
|
+
),
|
|
73
|
+
orderStatus: countBy(signals, (signal) => finiteString(signal.orderStatus)),
|
|
74
|
+
skipReasons: countBy(
|
|
75
|
+
signals,
|
|
76
|
+
(signal) => finiteString(signal.orderSkipReason)
|
|
77
|
+
),
|
|
78
|
+
aiQuality: countBy(candidates, (evaluation) => {
|
|
79
|
+
const quality = finiteNumber(asRecord(evaluation.aiAnalysis)?.quality);
|
|
80
|
+
return quality == null ? null : String(quality);
|
|
81
|
+
})
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
var buildDistributionDelta = (current, previous) => {
|
|
85
|
+
const currentTotal = Object.values(current).reduce(
|
|
86
|
+
(total, value) => total + value,
|
|
87
|
+
0
|
|
88
|
+
);
|
|
89
|
+
const previousTotal = Object.values(previous).reduce(
|
|
90
|
+
(total, value) => total + value,
|
|
91
|
+
0
|
|
92
|
+
);
|
|
93
|
+
const keys = [
|
|
94
|
+
.../* @__PURE__ */ new Set([...Object.keys(current), ...Object.keys(previous)])
|
|
95
|
+
].sort((left, right) => left.localeCompare(right));
|
|
96
|
+
return Object.fromEntries(
|
|
97
|
+
keys.map((key) => {
|
|
98
|
+
const currentCount = current[key] ?? 0;
|
|
99
|
+
const previousCount = previous[key] ?? 0;
|
|
100
|
+
const currentShare = currentTotal > 0 ? currentCount / currentTotal : 0;
|
|
101
|
+
const previousShare = previousTotal > 0 ? previousCount / previousTotal : 0;
|
|
102
|
+
return [
|
|
103
|
+
key,
|
|
104
|
+
{
|
|
105
|
+
currentCount,
|
|
106
|
+
previousCount,
|
|
107
|
+
currentShare: round(currentShare),
|
|
108
|
+
previousShare: round(previousShare),
|
|
109
|
+
shareDelta: round(currentShare - previousShare)
|
|
110
|
+
}
|
|
111
|
+
];
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
var sum = (values) => round(values.reduce((total, value) => total + (value ?? 0), 0));
|
|
116
|
+
var statsCount = (buckets, field) => sum(buckets.map((bucket) => finiteNumber(asRecord(bucket.stats)?.[field])));
|
|
117
|
+
var tradeTimestamp = (trade) => finiteNumber(trade.exitTimestamp) ?? finiteNumber(trade.lastSyncedAt) ?? finiteNumber(trade.entryTimestamp) ?? 0;
|
|
118
|
+
var dedupeTrades = (artifacts) => {
|
|
119
|
+
const tradesByOrderId = /* @__PURE__ */ new Map();
|
|
120
|
+
for (const artifact of artifacts) {
|
|
121
|
+
for (const trade of extractRuntimeRows(artifact).trades) {
|
|
122
|
+
const orderId = finiteString(trade.orderId);
|
|
123
|
+
if (!orderId) continue;
|
|
124
|
+
const existing = tradesByOrderId.get(orderId);
|
|
125
|
+
if (!existing || tradeTimestamp(trade) >= tradeTimestamp(existing)) {
|
|
126
|
+
tradesByOrderId.set(orderId, trade);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [...tradesByOrderId.values()];
|
|
131
|
+
};
|
|
132
|
+
var maxDrawdown = (trades) => {
|
|
133
|
+
let cumulative = 0;
|
|
134
|
+
let peak = 0;
|
|
135
|
+
let drawdown = 0;
|
|
136
|
+
for (const trade of trades.sort(
|
|
137
|
+
(left, right) => tradeTimestamp(left) - tradeTimestamp(right)
|
|
138
|
+
)) {
|
|
139
|
+
cumulative += finiteNumber(trade.closedPnl) ?? 0;
|
|
140
|
+
peak = Math.max(peak, cumulative);
|
|
141
|
+
drawdown = Math.max(drawdown, peak - cumulative);
|
|
142
|
+
}
|
|
143
|
+
return round(drawdown);
|
|
144
|
+
};
|
|
145
|
+
var buildRollingPerformance = ({
|
|
146
|
+
trades,
|
|
147
|
+
anchorTime,
|
|
148
|
+
days
|
|
149
|
+
}) => {
|
|
150
|
+
const startTime = anchorTime - days * 24 * 60 * 60 * 1e3;
|
|
151
|
+
const closed = trades.filter((trade) => {
|
|
152
|
+
const exitTimestamp = finiteNumber(trade.exitTimestamp);
|
|
153
|
+
return trade.status === "closed" && exitTimestamp != null && exitTimestamp >= startTime && exitTimestamp < anchorTime;
|
|
154
|
+
});
|
|
155
|
+
const realizedPnl = sum(closed.map((trade) => finiteNumber(trade.closedPnl)));
|
|
156
|
+
return {
|
|
157
|
+
days,
|
|
158
|
+
closedTrades: closed.length,
|
|
159
|
+
realizedPnl,
|
|
160
|
+
expectancy: closed.length > 0 ? round(realizedPnl / closed.length) : null,
|
|
161
|
+
maxDrawdown: maxDrawdown(closed)
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
var getReplayComparison = (artifact) => {
|
|
165
|
+
const root = asRecord(artifact) ?? {};
|
|
166
|
+
return asRecord(asRecord(root.replay)?.runtimeComparison) ?? null;
|
|
167
|
+
};
|
|
168
|
+
var getCalibrationSummary = (artifact) => {
|
|
169
|
+
const root = asRecord(artifact) ?? {};
|
|
170
|
+
return asRecord(asRecord(root.summary)?.all) ?? null;
|
|
171
|
+
};
|
|
172
|
+
var buildRuntimeScorecard = ({
|
|
173
|
+
runtimeArtifact,
|
|
174
|
+
replayEvidenceArtifact,
|
|
175
|
+
calibrationArtifact,
|
|
176
|
+
historyRuntimeArtifacts = [],
|
|
177
|
+
thresholds = {
|
|
178
|
+
minimumParityRatio: 0.95,
|
|
179
|
+
maximumSlippageResidualBps: 3,
|
|
180
|
+
minimumClosedTrades: 20,
|
|
181
|
+
minimumExpectancy: 0
|
|
182
|
+
},
|
|
183
|
+
generatedAt = Date.now()
|
|
184
|
+
}) => {
|
|
185
|
+
const rows = extractRuntimeRows(runtimeArtifact);
|
|
186
|
+
const window = runtimeWindow(runtimeArtifact);
|
|
187
|
+
const startTime = finiteNumber(window.startTime) ?? generatedAt - 864e5;
|
|
188
|
+
const endTime = finiteNumber(window.endTime) ?? generatedAt;
|
|
189
|
+
const evaluationsFromStats = statsCount(rows.statsBuckets, "evaluated");
|
|
190
|
+
const candidatesFromStats = statsCount(rows.statsBuckets, "signals");
|
|
191
|
+
const signalEvaluations = rows.evaluations.filter(
|
|
192
|
+
(evaluation) => evaluation.status === "signal"
|
|
193
|
+
);
|
|
194
|
+
const gateDecisions = signalEvaluations.map((evaluation) => ({
|
|
195
|
+
evaluation,
|
|
196
|
+
decision: finiteString(asRecord(evaluation.aiAnalysis)?.gateDecision)
|
|
197
|
+
})).filter(
|
|
198
|
+
({ decision }) => decision === "approved" || decision === "rejected"
|
|
199
|
+
);
|
|
200
|
+
const allocatorDecisions = signalEvaluations.map((evaluation) => asRecord(evaluation.allocatorDecision)).filter((item) => item != null);
|
|
201
|
+
const riskDecisions = signalEvaluations.map((evaluation) => asRecord(evaluation.riskDecision)).filter((item) => item != null);
|
|
202
|
+
const orderAttempts = rows.signals.filter(
|
|
203
|
+
(signal) => signal.orderStatus === "completed" || signal.orderStatus === "failed"
|
|
204
|
+
);
|
|
205
|
+
const currentClosedTrades = rows.trades.filter((trade) => {
|
|
206
|
+
const exitTimestamp = finiteNumber(trade.exitTimestamp);
|
|
207
|
+
return trade.status === "closed" && exitTimestamp != null && exitTimestamp >= startTime && exitTimestamp < endTime;
|
|
208
|
+
});
|
|
209
|
+
const replayComparison = getReplayComparison(replayEvidenceArtifact);
|
|
210
|
+
const replayCounts = asRecord(replayComparison?.counts);
|
|
211
|
+
const matched = finiteNumber(replayCounts?.matched) ?? 0;
|
|
212
|
+
const backtestOnly = finiteNumber(replayCounts?.backtestOnly) ?? 0;
|
|
213
|
+
const runtimeOnly = finiteNumber(replayCounts?.runtimeOnly) ?? 0;
|
|
214
|
+
const comparisonTotal = matched + backtestOnly + runtimeOnly;
|
|
215
|
+
const parityRatio = comparisonTotal > 0 ? matched / comparisonTotal : null;
|
|
216
|
+
const lineage = asRecord(replayComparison?.lineage);
|
|
217
|
+
const lineageReason = finiteString(lineage?.reason);
|
|
218
|
+
const calibration = getCalibrationSummary(calibrationArtifact);
|
|
219
|
+
const actualSlippageBps = finiteNumber(
|
|
220
|
+
asRecord(calibration?.signalToFillAdverseBps)?.avg
|
|
221
|
+
);
|
|
222
|
+
const residualVsModelBps = finiteNumber(
|
|
223
|
+
asRecord(calibration?.residualVsCurrentModelBps)?.avg
|
|
224
|
+
);
|
|
225
|
+
const historyTrades = dedupeTrades([
|
|
226
|
+
...historyRuntimeArtifacts,
|
|
227
|
+
runtimeArtifact
|
|
228
|
+
]);
|
|
229
|
+
const currentDistributions = buildDistributions(rows);
|
|
230
|
+
const previousArtifact = historyRuntimeArtifacts.map((artifact) => ({ artifact, window: runtimeWindow(artifact) })).filter(({ window: candidateWindow }) => {
|
|
231
|
+
const candidateEndTime = finiteNumber(candidateWindow.endTime);
|
|
232
|
+
return candidateEndTime != null && candidateEndTime <= startTime;
|
|
233
|
+
}).sort(
|
|
234
|
+
(left, right) => (finiteNumber(right.window.endTime) ?? 0) - (finiteNumber(left.window.endTime) ?? 0)
|
|
235
|
+
)[0];
|
|
236
|
+
const previousDistributions = previousArtifact ? buildDistributions(extractRuntimeRows(previousArtifact.artifact)) : null;
|
|
237
|
+
const rolling = [7, 30, 90].map(
|
|
238
|
+
(days) => buildRollingPerformance({
|
|
239
|
+
trades: [...historyTrades],
|
|
240
|
+
anchorTime: endTime,
|
|
241
|
+
days
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
const sevenDay = rolling[0];
|
|
245
|
+
const reactions = [];
|
|
246
|
+
if (lineageReason) {
|
|
247
|
+
reactions.push({
|
|
248
|
+
code: "LINEAGE_NOT_COMPARABLE",
|
|
249
|
+
severity: "blocking",
|
|
250
|
+
message: lineageReason
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (parityRatio != null && parityRatio < thresholds.minimumParityRatio) {
|
|
254
|
+
reactions.push({
|
|
255
|
+
code: "PARITY_REGRESSION",
|
|
256
|
+
severity: "blocking",
|
|
257
|
+
message: `Parity ${round(parityRatio * 100, 2)}% is below ${round(thresholds.minimumParityRatio * 100, 2)}%.`
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (residualVsModelBps != null && residualVsModelBps > thresholds.maximumSlippageResidualBps) {
|
|
261
|
+
reactions.push({
|
|
262
|
+
code: "SLIPPAGE_DRIFT",
|
|
263
|
+
severity: "warning",
|
|
264
|
+
message: `Execution residual ${round(residualVsModelBps, 2)} bps exceeds ${thresholds.maximumSlippageResidualBps} bps.`
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (sevenDay.closedTrades >= thresholds.minimumClosedTrades && sevenDay.expectancy != null && sevenDay.expectancy < thresholds.minimumExpectancy) {
|
|
268
|
+
reactions.push({
|
|
269
|
+
code: "EXPECTANCY_DEGRADATION",
|
|
270
|
+
severity: "blocking",
|
|
271
|
+
message: `7d expectancy ${sevenDay.expectancy} is below ${thresholds.minimumExpectancy}.`
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (sevenDay.closedTrades < thresholds.minimumClosedTrades) {
|
|
275
|
+
reactions.push({
|
|
276
|
+
code: "INSUFFICIENT_RECENT_SAMPLE",
|
|
277
|
+
severity: "info",
|
|
278
|
+
message: `Only ${sevenDay.closedTrades} closed trades in 7d; ${thresholds.minimumClosedTrades} required for expectancy reaction.`
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
const promotionStatus = reactions.some(
|
|
282
|
+
(reaction) => reaction.severity === "blocking"
|
|
283
|
+
) ? "PROMOTION_BLOCKED" : reactions.some((reaction) => reaction.severity === "warning") ? "REVIEW_REQUIRED" : "OBSERVE";
|
|
284
|
+
return {
|
|
285
|
+
reportType: "runtime-scorecard",
|
|
286
|
+
schemaVersion: 1,
|
|
287
|
+
generatedAt,
|
|
288
|
+
window: { startTime, endTime },
|
|
289
|
+
promotionStatus,
|
|
290
|
+
thresholds,
|
|
291
|
+
funnel: {
|
|
292
|
+
evaluations: evaluationsFromStats > 0 ? evaluationsFromStats : rows.evaluations.length,
|
|
293
|
+
coreCandidates: candidatesFromStats > 0 ? candidatesFromStats : signalEvaluations.length,
|
|
294
|
+
gate: {
|
|
295
|
+
available: gateDecisions.length > 0,
|
|
296
|
+
approved: gateDecisions.filter(
|
|
297
|
+
({ decision }) => decision === "approved"
|
|
298
|
+
).length,
|
|
299
|
+
rejected: gateDecisions.filter(
|
|
300
|
+
({ decision }) => decision === "rejected"
|
|
301
|
+
).length,
|
|
302
|
+
coverage: signalEvaluations.length ? round(gateDecisions.length / signalEvaluations.length) : null
|
|
303
|
+
},
|
|
304
|
+
allocator: {
|
|
305
|
+
available: allocatorDecisions.length > 0,
|
|
306
|
+
approved: allocatorDecisions.filter(
|
|
307
|
+
(decision) => decision.decision === "approved"
|
|
308
|
+
).length,
|
|
309
|
+
rejected: allocatorDecisions.filter(
|
|
310
|
+
(decision) => decision.decision === "rejected"
|
|
311
|
+
).length
|
|
312
|
+
},
|
|
313
|
+
risk: {
|
|
314
|
+
available: riskDecisions.length > 0,
|
|
315
|
+
approved: riskDecisions.filter(
|
|
316
|
+
(decision) => decision.decision === "approved"
|
|
317
|
+
).length,
|
|
318
|
+
rejected: riskDecisions.filter(
|
|
319
|
+
(decision) => decision.decision === "rejected"
|
|
320
|
+
).length
|
|
321
|
+
},
|
|
322
|
+
orderAttempts: orderAttempts.length,
|
|
323
|
+
orderFailures: orderAttempts.filter(
|
|
324
|
+
(signal) => signal.orderStatus === "failed"
|
|
325
|
+
).length,
|
|
326
|
+
balanceRejects: rows.signals.filter(
|
|
327
|
+
(signal) => /BALANCE|MARGIN/i.test(
|
|
328
|
+
`${finiteString(signal.orderSkipReason) ?? ""} ${finiteString(signal.orderFailureReason) ?? ""}`
|
|
329
|
+
)
|
|
330
|
+
).length,
|
|
331
|
+
fills: rows.trades.length,
|
|
332
|
+
closedTrades: currentClosedTrades.length
|
|
333
|
+
},
|
|
334
|
+
realized: {
|
|
335
|
+
pnl: sum(
|
|
336
|
+
currentClosedTrades.map((trade) => finiteNumber(trade.closedPnl))
|
|
337
|
+
),
|
|
338
|
+
fees: sum(
|
|
339
|
+
currentClosedTrades.map((trade) => finiteNumber(trade.totalFee))
|
|
340
|
+
),
|
|
341
|
+
funding: sum(
|
|
342
|
+
currentClosedTrades.map((trade) => finiteNumber(trade.fundingFee))
|
|
343
|
+
)
|
|
344
|
+
},
|
|
345
|
+
parity: {
|
|
346
|
+
available: comparisonTotal > 0,
|
|
347
|
+
matched,
|
|
348
|
+
backtestOnly,
|
|
349
|
+
runtimeOnly,
|
|
350
|
+
ratio: parityRatio == null ? null : round(parityRatio),
|
|
351
|
+
lineageReason
|
|
352
|
+
},
|
|
353
|
+
execution: {
|
|
354
|
+
available: calibration != null,
|
|
355
|
+
actualSignalToFillSlippageBps: actualSlippageBps,
|
|
356
|
+
residualVsCurrentModelBps: residualVsModelBps
|
|
357
|
+
},
|
|
358
|
+
rolling,
|
|
359
|
+
distributions: currentDistributions,
|
|
360
|
+
distributionChanges: {
|
|
361
|
+
available: previousDistributions != null,
|
|
362
|
+
baselineWindow: previousArtifact ? {
|
|
363
|
+
startTime: finiteNumber(previousArtifact.window.startTime),
|
|
364
|
+
endTime: finiteNumber(previousArtifact.window.endTime)
|
|
365
|
+
} : null,
|
|
366
|
+
metrics: previousDistributions ? Object.fromEntries(
|
|
367
|
+
Object.entries(currentDistributions).map(([name, current]) => [
|
|
368
|
+
name,
|
|
369
|
+
buildDistributionDelta(
|
|
370
|
+
current,
|
|
371
|
+
previousDistributions[name]
|
|
372
|
+
)
|
|
373
|
+
])
|
|
374
|
+
) : {}
|
|
375
|
+
},
|
|
376
|
+
reactions
|
|
377
|
+
};
|
|
378
|
+
};
|
|
379
|
+
var formatRuntimeScorecardMarkdown = (scorecard) => {
|
|
380
|
+
const rollingRows = scorecard.rolling.map(
|
|
381
|
+
(row) => `| ${row.days}d | ${row.closedTrades} | ${row.realizedPnl} | ${row.expectancy ?? "n/a"} | ${row.maxDrawdown} |`
|
|
382
|
+
).join("\n");
|
|
383
|
+
const reactions = scorecard.reactions.length ? scorecard.reactions.map(
|
|
384
|
+
(reaction) => `- ${reaction.severity.toUpperCase()} \`${reaction.code}\`: ${reaction.message}`
|
|
385
|
+
).join("\n") : "- No automatic reactions.";
|
|
386
|
+
const distributionChanges = Object.entries(
|
|
387
|
+
scorecard.distributionChanges.metrics
|
|
388
|
+
).flatMap(
|
|
389
|
+
([metric, values]) => Object.entries(values).map(([value, change]) => ({
|
|
390
|
+
metric,
|
|
391
|
+
value,
|
|
392
|
+
shareDelta: change.shareDelta
|
|
393
|
+
}))
|
|
394
|
+
).sort(
|
|
395
|
+
(left, right) => Math.abs(right.shareDelta) - Math.abs(left.shareDelta)
|
|
396
|
+
).slice(0, 10).map(
|
|
397
|
+
(change) => `- ${change.metric}.${change.value}: ${change.shareDelta >= 0 ? "+" : ""}${round(change.shareDelta * 100, 2)} pp`
|
|
398
|
+
).join("\n");
|
|
399
|
+
return `# Runtime scorecard
|
|
400
|
+
|
|
401
|
+
Status: **${scorecard.promotionStatus}**
|
|
402
|
+
|
|
403
|
+
Window: ${new Date(scorecard.window.startTime).toISOString()} \u2014 ${new Date(scorecard.window.endTime).toISOString()}
|
|
404
|
+
|
|
405
|
+
## Funnel
|
|
406
|
+
|
|
407
|
+
- Evaluations: ${scorecard.funnel.evaluations}
|
|
408
|
+
- Core candidates: ${scorecard.funnel.coreCandidates}
|
|
409
|
+
- Gate approvals/rejects: ${scorecard.funnel.gate.approved}/${scorecard.funnel.gate.rejected}
|
|
410
|
+
- Allocator approvals/rejects: ${scorecard.funnel.allocator.available ? `${scorecard.funnel.allocator.approved}/${scorecard.funnel.allocator.rejected}` : "unavailable"}
|
|
411
|
+
- Risk approvals/rejects: ${scorecard.funnel.risk.available ? `${scorecard.funnel.risk.approved}/${scorecard.funnel.risk.rejected}` : "unavailable"}
|
|
412
|
+
- Order attempts/failures: ${scorecard.funnel.orderAttempts}/${scorecard.funnel.orderFailures}
|
|
413
|
+
- Fills: ${scorecard.funnel.fills}
|
|
414
|
+
- Closed trades: ${scorecard.funnel.closedTrades}
|
|
415
|
+
|
|
416
|
+
## Replay and execution
|
|
417
|
+
|
|
418
|
+
- Parity: ${scorecard.parity.ratio == null ? "n/a" : `${round(scorecard.parity.ratio * 100, 2)}%`}
|
|
419
|
+
- Runtime/replay mismatches: ${scorecard.parity.backtestOnly + scorecard.parity.runtimeOnly}
|
|
420
|
+
- Actual signal-to-fill slippage: ${scorecard.execution.actualSignalToFillSlippageBps ?? "n/a"} bps
|
|
421
|
+
- Residual vs current model: ${scorecard.execution.residualVsCurrentModelBps ?? "n/a"} bps
|
|
422
|
+
|
|
423
|
+
## Rolling performance
|
|
424
|
+
|
|
425
|
+
| Window | Closed | Realized PnL | Expectancy | Max drawdown |
|
|
426
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
427
|
+
${rollingRows}
|
|
428
|
+
|
|
429
|
+
## Distribution changes
|
|
430
|
+
|
|
431
|
+
${scorecard.distributionChanges.available ? distributionChanges || "- No distribution changes." : "- Previous comparable artifact is unavailable."}
|
|
432
|
+
|
|
433
|
+
## Reactions
|
|
434
|
+
|
|
435
|
+
${reactions}
|
|
436
|
+
`;
|
|
437
|
+
};
|
|
438
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
439
|
+
0 && (module.exports = {
|
|
440
|
+
buildRuntimeScorecard,
|
|
441
|
+
formatRuntimeScorecardMarkdown
|
|
442
|
+
});
|
|
@@ -281,6 +281,15 @@ var syncRuntimeTrades = async ({
|
|
|
281
281
|
nextTrade,
|
|
282
282
|
{ expire: import_constants.TTL_1M }
|
|
283
283
|
),
|
|
284
|
+
(0, import_redis.setHashJsonField)(
|
|
285
|
+
import_redis.redisKeys.runtimeClosedTradeBucket(
|
|
286
|
+
userName,
|
|
287
|
+
(0, import_time.getRuntimeStorageDayKey)(nextTrade.exitTimestamp)
|
|
288
|
+
),
|
|
289
|
+
trade.orderId,
|
|
290
|
+
nextTrade,
|
|
291
|
+
{ expire: import_constants.TTL_1M }
|
|
292
|
+
),
|
|
284
293
|
...isCurrentActiveTrade ? [(0, import_redis.delKey)(activeTradeKey)] : []
|
|
285
294
|
]);
|
|
286
295
|
syncedTrades.push(nextTrade);
|
package/dist/scripts/agentRun.js
CHANGED
|
@@ -37,7 +37,7 @@ var import_args = __toESM(require("args"));
|
|
|
37
37
|
var import_chalk = __toESM(require("chalk"));
|
|
38
38
|
var import_promises = __toESM(require("fs/promises"));
|
|
39
39
|
var import_node_os = __toESM(require("os"));
|
|
40
|
-
var
|
|
40
|
+
var import_node_path = __toESM(require("path"));
|
|
41
41
|
var import_node_child_process = require("child_process");
|
|
42
42
|
var import_constants = require("@tradejs/core/constants");
|
|
43
43
|
var import_redis = require("@tradejs/infra/redis");
|
|
@@ -113,17 +113,9 @@ ${chunk}`, {
|
|
|
113
113
|
};
|
|
114
114
|
|
|
115
115
|
// src/lib/researchAgent.ts
|
|
116
|
-
var import_node_path = __toESM(require("path"));
|
|
117
116
|
var RESEARCH_AGENT_MODEL = "openai/gpt-5.4";
|
|
118
117
|
var RESEARCH_AGENT_REASONING_EFFORT = "medium";
|
|
119
118
|
var RESEARCH_AGENT_BRANCH_PREFIX = "codex/research";
|
|
120
|
-
var toUpperSnakeCase = (value) => value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
121
|
-
var RESEARCH_NOTE_FILE_OVERRIDES = {
|
|
122
|
-
TrendLine: "AI_TRENDLINE_REPLAY_NOTES.md",
|
|
123
|
-
ReverseTrendLine: "AI_REVERSE_TRENDLINE_REPLAY_NOTES.md",
|
|
124
|
-
AdaptiveMomentumRibbon: "AI_ADAPTIVE_MOMENTUM_RIBBON_REPLAY_NOTES.md",
|
|
125
|
-
VolumeDivergence: "AI_VOLUME_DIVERGENCE_REPLAY_NOTES.md"
|
|
126
|
-
};
|
|
127
119
|
var buildResearchAgentBranchName = (strategy, runId) => {
|
|
128
120
|
const normalizedStrategy = strategy.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
129
121
|
const normalizedRunId = runId.trim().replace(/[^a-zA-Z0-9-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
@@ -131,13 +123,8 @@ var buildResearchAgentBranchName = (strategy, runId) => {
|
|
|
131
123
|
};
|
|
132
124
|
var buildResearchAgentCommitMessage = (strategy, runId) => `Research agent: ${strategy} follow-up for ${runId}`;
|
|
133
125
|
var buildResearchAgentPrTitle = (strategy, runId) => `Research agent: ${strategy} follow-up for ${runId}`;
|
|
134
|
-
var getResearchAgentNotePath = (strategy) => import_node_path.default.join(
|
|
135
|
-
"notes",
|
|
136
|
-
RESEARCH_NOTE_FILE_OVERRIDES[strategy] || `AI_${toUpperSnakeCase(strategy)}_REPLAY_NOTES.md`
|
|
137
|
-
);
|
|
138
126
|
var getResearchAgentAllowedPathPrefixes = (strategy) => [
|
|
139
|
-
`packages/strategies/src/${strategy}
|
|
140
|
-
getResearchAgentNotePath(strategy)
|
|
127
|
+
`packages/strategies/src/${strategy}/`
|
|
141
128
|
];
|
|
142
129
|
var parseGithubRepositoryFromRemote = (value) => {
|
|
143
130
|
const trimmed = value.trim();
|
|
@@ -263,7 +250,7 @@ Output ONLY one of:
|
|
|
263
250
|
|
|
264
251
|
Rules:
|
|
265
252
|
- Keep the patch small and surgical.
|
|
266
|
-
- Prefer strategy-scoped tests
|
|
253
|
+
- Prefer strategy-scoped tests over broad refactors.
|
|
267
254
|
- If you change strategy logic, also add or update tests in the same strategy directory.
|
|
268
255
|
- Do not touch package boundaries or unrelated files.
|
|
269
256
|
- Do not add markdown fences, commentary, or explanations.
|
|
@@ -332,7 +319,7 @@ var listFilesRecursive = async (targetPath) => {
|
|
|
332
319
|
const entries = await import_promises.default.readdir(targetPath, { withFileTypes: true });
|
|
333
320
|
const nested = await Promise.all(
|
|
334
321
|
entries.map(async (entry) => {
|
|
335
|
-
const absolutePath =
|
|
322
|
+
const absolutePath = import_node_path.default.join(targetPath, entry.name);
|
|
336
323
|
if (entry.isDirectory()) {
|
|
337
324
|
return listFilesRecursive(absolutePath);
|
|
338
325
|
}
|
|
@@ -345,27 +332,21 @@ var listFilesRecursive = async (targetPath) => {
|
|
|
345
332
|
}
|
|
346
333
|
};
|
|
347
334
|
var collectStrategyContextFiles = async (worktreePath, strategy) => {
|
|
348
|
-
const strategyRoot =
|
|
335
|
+
const strategyRoot = import_node_path.default.join(
|
|
349
336
|
worktreePath,
|
|
350
337
|
"packages",
|
|
351
338
|
"strategies",
|
|
352
339
|
"src",
|
|
353
340
|
strategy
|
|
354
341
|
);
|
|
355
|
-
const notePath = import_node_path2.default.join(worktreePath, getResearchAgentNotePath(strategy));
|
|
356
342
|
const strategyFiles = await listFilesRecursive(strategyRoot);
|
|
357
343
|
const files = strategyFiles.filter(
|
|
358
344
|
(filePath) => /\.(ts|tsx|md)$/.test(filePath)
|
|
359
345
|
);
|
|
360
|
-
try {
|
|
361
|
-
await import_promises.default.access(notePath);
|
|
362
|
-
files.push(notePath);
|
|
363
|
-
} catch {
|
|
364
|
-
}
|
|
365
346
|
return [...new Set(files)].sort((left, right) => left.localeCompare(right));
|
|
366
347
|
};
|
|
367
348
|
var renderFileContext = async (worktreePath, filePath) => {
|
|
368
|
-
const relativePath =
|
|
349
|
+
const relativePath = import_node_path.default.relative(worktreePath, filePath).replace(/\\/g, "/");
|
|
369
350
|
const content = await import_promises.default.readFile(filePath, "utf8");
|
|
370
351
|
return [
|
|
371
352
|
`=== FILE: ${relativePath} ===`,
|
|
@@ -390,9 +371,12 @@ var buildPrompt = async (worktreePath, run, allowedPaths) => {
|
|
|
390
371
|
{
|
|
391
372
|
status: run.status,
|
|
392
373
|
finishedAt: run.finishedAt,
|
|
374
|
+
backtestResultKey: run.artifacts.backtestResultKey,
|
|
393
375
|
backtestResult: run.artifacts.backtestResult,
|
|
376
|
+
aiExportFile: run.artifacts.aiExportFile,
|
|
394
377
|
aiTrainLocal: run.artifacts.aiTrainLocal,
|
|
395
|
-
strategyConfig: run.artifacts.strategyConfig
|
|
378
|
+
strategyConfig: run.artifacts.strategyConfig,
|
|
379
|
+
backtestConfig: run.artifacts.backtestConfig
|
|
396
380
|
},
|
|
397
381
|
null,
|
|
398
382
|
2
|
|
@@ -401,8 +385,9 @@ var buildPrompt = async (worktreePath, run, allowedPaths) => {
|
|
|
401
385
|
"Task:",
|
|
402
386
|
"- Inspect the research results and current strategy files.",
|
|
403
387
|
"- Produce the smallest safe follow-up patch.",
|
|
404
|
-
"- Prefer
|
|
388
|
+
"- Prefer strategy-scoped tests or NO_CHANGES if the data does not justify a production logic change.",
|
|
405
389
|
"- If you change strategy logic, include matching regression tests.",
|
|
390
|
+
"- Do not create or modify notes/: local research notes are permanently Git-ignored and are not allowed in agent commits.",
|
|
406
391
|
"",
|
|
407
392
|
contexts.join("\n\n")
|
|
408
393
|
].join("\n");
|
|
@@ -475,8 +460,8 @@ var requestAgentDiff = async (userName, messages) => {
|
|
|
475
460
|
return normalized;
|
|
476
461
|
};
|
|
477
462
|
var applyDiff = async (worktreePath, diffText) => {
|
|
478
|
-
const tempDir = await import_promises.default.mkdtemp(
|
|
479
|
-
const patchPath =
|
|
463
|
+
const tempDir = await import_promises.default.mkdtemp(import_node_path.default.join(import_node_os.default.tmpdir(), "tradejs-agent-"));
|
|
464
|
+
const patchPath = import_node_path.default.join(tempDir, "agent.patch");
|
|
480
465
|
try {
|
|
481
466
|
await import_promises.default.writeFile(patchPath, diffText, "utf8");
|
|
482
467
|
await runCommandOrThrow(
|
|
@@ -701,8 +686,8 @@ var createGithubPullRequest = async (params) => {
|
|
|
701
686
|
};
|
|
702
687
|
var createWorktree = async (branchName) => {
|
|
703
688
|
const worktreeToken = branchName.replace(/[/:]+/g, "--");
|
|
704
|
-
const worktreeRoot =
|
|
705
|
-
const worktreePath =
|
|
689
|
+
const worktreeRoot = import_node_path.default.join(projectRoot, ".agent-worktrees");
|
|
690
|
+
const worktreePath = import_node_path.default.join(worktreeRoot, worktreeToken);
|
|
706
691
|
await import_promises.default.mkdir(worktreeRoot, { recursive: true });
|
|
707
692
|
await runCommand("git", ["worktree", "remove", "--force", worktreePath], {
|
|
708
693
|
cwd: projectRoot
|