@tradejs/cli 3.1.29-beta.281 → 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 +2784 -2407
- package/dist/lib/mcpReplayProcess.js +15 -2
- package/dist/lib/runtimeDebugEvidence.js +45 -52
- package/dist/lib/runtimeFeedbackProcess.js +1700 -0
- package/dist/lib/runtimeRedis.js +45 -0
- package/dist/scripts/mcpWorker.js +65 -21
- package/dist/scripts/replay.js +173 -47
- package/dist/scripts/replayRunner.js +173 -47
- package/dist/scripts/replayRuntimeEvidence.js +154 -58
- package/dist/scripts/runtimeEvidence.js +45 -52
- package/dist/scripts/runtimeFeedbackProcess.js +1736 -0
- package/package.json +7 -7
package/dist/lib/runtimeRedis.js
CHANGED
|
@@ -26,6 +26,7 @@ __export(runtimeRedis_exports, {
|
|
|
26
26
|
loadRuntimeActiveTradeOrderIds: () => loadRuntimeActiveTradeOrderIds,
|
|
27
27
|
loadRuntimeActiveTrades: () => loadRuntimeActiveTrades,
|
|
28
28
|
loadRuntimeClosedTrades: () => loadRuntimeClosedTrades,
|
|
29
|
+
loadRuntimeEvidenceTrades: () => loadRuntimeEvidenceTrades,
|
|
29
30
|
loadRuntimeStrategyConfigs: () => import_runtimeStrategyConfigs.loadRuntimeStrategyConfigs,
|
|
30
31
|
loadRuntimeStrategyNames: () => import_runtimeStrategyConfigs.loadRuntimeStrategyNames,
|
|
31
32
|
loadRuntimeTrades: () => loadRuntimeTrades,
|
|
@@ -104,6 +105,49 @@ var loadRuntimeClosedTrades = async (userName, {
|
|
|
104
105
|
(left, right) => left.exitTimestamp - right.exitTimestamp
|
|
105
106
|
);
|
|
106
107
|
};
|
|
108
|
+
var loadRuntimeEvidenceTrades = async (userName, { startTime, endTime }) => {
|
|
109
|
+
const prefix = import_redis.redisKeys.runtimeTradeBuckets(userName);
|
|
110
|
+
const lastDay = (0, import_time.getRuntimeStorageDayKey)(endTime - 1);
|
|
111
|
+
const keys = (await (0, import_redis.getKeys)(prefix)).filter((key) => {
|
|
112
|
+
const day = key.slice(prefix.length);
|
|
113
|
+
return key.startsWith(prefix) && /^\d{4}-\d{2}-\d{2}$/.test(day) && day <= lastDay;
|
|
114
|
+
});
|
|
115
|
+
const retained = /* @__PURE__ */ new Map();
|
|
116
|
+
for (let offset = 0; offset < keys.length; offset += 16) {
|
|
117
|
+
const buckets = await Promise.all(
|
|
118
|
+
keys.slice(offset, offset + 16).map((key) => (0, import_redis.getHashJsonValues)(key))
|
|
119
|
+
);
|
|
120
|
+
for (const row of buckets.flat())
|
|
121
|
+
if (isRuntimeTradeRecord(row)) retained.set(row.orderId, row);
|
|
122
|
+
}
|
|
123
|
+
for (const row of await loadRuntimeClosedTrades(userName, {
|
|
124
|
+
startTime,
|
|
125
|
+
endTime
|
|
126
|
+
}))
|
|
127
|
+
retained.set(row.orderId, row);
|
|
128
|
+
return [...retained.values()].filter(
|
|
129
|
+
(row) => row.entryTimestamp < endTime && (row.status === "active" || Number.isFinite(row.exitTimestamp) && row.exitTimestamp >= startTime)
|
|
130
|
+
).map((row) => {
|
|
131
|
+
const closesAfterCutoff = row.status === "closed" && row.exitTimestamp >= endTime;
|
|
132
|
+
if (!closesAfterCutoff && !(row.status === "active" && row.lastSyncedAt >= endTime))
|
|
133
|
+
return row;
|
|
134
|
+
return {
|
|
135
|
+
...row,
|
|
136
|
+
status: "active",
|
|
137
|
+
currentPrice: null,
|
|
138
|
+
currentPnl: null,
|
|
139
|
+
closedPnl: null,
|
|
140
|
+
exitPrice: null,
|
|
141
|
+
actualExitPrice: null,
|
|
142
|
+
exitTimestamp: null,
|
|
143
|
+
exitType: null,
|
|
144
|
+
closeFee: null,
|
|
145
|
+
fundingFee: null,
|
|
146
|
+
totalFee: null,
|
|
147
|
+
lastSyncedAt: void 0
|
|
148
|
+
};
|
|
149
|
+
}).sort((a, b) => a.entryTimestamp - b.entryTimestamp);
|
|
150
|
+
};
|
|
107
151
|
var loadRuntimeActiveTradeOrderIds = async (userName) => {
|
|
108
152
|
const keys = await (0, import_redis.getKeys)(import_redis.redisKeys.runtimeActiveTrades(userName));
|
|
109
153
|
const refs = await Promise.all(keys.map((key) => (0, import_redis.getData)(key, null)));
|
|
@@ -144,6 +188,7 @@ var loadStrategyResultSymbols = async ({
|
|
|
144
188
|
loadRuntimeActiveTradeOrderIds,
|
|
145
189
|
loadRuntimeActiveTrades,
|
|
146
190
|
loadRuntimeClosedTrades,
|
|
191
|
+
loadRuntimeEvidenceTrades,
|
|
147
192
|
loadRuntimeStrategyConfigs,
|
|
148
193
|
loadRuntimeStrategyNames,
|
|
149
194
|
loadRuntimeTrades,
|
|
@@ -66,8 +66,21 @@ const timer = setInterval(() => {
|
|
|
66
66
|
try {
|
|
67
67
|
const result = JSON.parse(fs.readFileSync(file + '.result', 'utf8'));
|
|
68
68
|
console.log(result.message);
|
|
69
|
+
if (result.exitCode !== 0) {
|
|
70
|
+
let fd;
|
|
71
|
+
try {
|
|
72
|
+
fd = fs.openSync(file + '.log', fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
73
|
+
const size = fs.fstatSync(fd).size;
|
|
74
|
+
const tail = Buffer.alloc(Math.min(size, 65536));
|
|
75
|
+
fs.readSync(fd, tail, 0, tail.length, Math.max(0, size - tail.length));
|
|
76
|
+
// The worker redacts each line before persisting or returning it through MCP.
|
|
77
|
+
for (const line of tail.toString('utf8').split(/[\\r\\n]+/).filter(Boolean).slice(-150)) console.error('Host replay: ' + line);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('Host replay log unavailable: ' + error.code);
|
|
80
|
+
} finally { if (fd !== undefined) fs.closeSync(fd); }
|
|
81
|
+
}
|
|
69
82
|
clearInterval(timer);
|
|
70
|
-
process.
|
|
83
|
+
process.exitCode = result.exitCode === 0 ? 0 : 1;
|
|
71
84
|
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
72
85
|
}, 1000);
|
|
73
86
|
`;
|
|
@@ -90,7 +103,7 @@ var buildMcpReplayProcess = async ({
|
|
|
90
103
|
const heartbeat = JSON.parse(
|
|
91
104
|
await import_promises.default.readFile(import_node_path.default.join(root, "heartbeat.json"), "utf8")
|
|
92
105
|
);
|
|
93
|
-
if (!Number.isFinite(heartbeat.observedAt) || Date.now() - heartbeat.observedAt < 0 || Date.now() - heartbeat.observedAt > 12e4)
|
|
106
|
+
if (heartbeat.protocolVersion !== 2 || !Number.isFinite(heartbeat.observedAt) || Date.now() - heartbeat.observedAt < 0 || Date.now() - heartbeat.observedAt > 12e4)
|
|
94
107
|
throw new Error("Host replay runner heartbeat is stale");
|
|
95
108
|
const bytes = await import_promises.default.readFile(evidencePath);
|
|
96
109
|
const request = JSON.stringify({
|
|
@@ -123,14 +136,17 @@ var main = async (options = {}) => {
|
|
|
123
136
|
let leaseLost = false;
|
|
124
137
|
let manifestHash = "";
|
|
125
138
|
let replayReady = false;
|
|
139
|
+
let replayReadiness = "unknown";
|
|
126
140
|
const updateReplayReady = async () => {
|
|
127
141
|
try {
|
|
128
142
|
const root = process.env.MCP_REPLAY_REQUEST_ROOT || `${process.env.PROJECT_CWD || process.cwd()}/data/mcp-replay-requests`;
|
|
129
143
|
const heartbeat2 = JSON.parse(
|
|
130
144
|
await (0, import_promises2.readFile)(`${root}/heartbeat.json`, "utf8")
|
|
131
145
|
);
|
|
132
|
-
|
|
146
|
+
replayReadiness = heartbeat2.readiness === "ready" || heartbeat2.readiness === "waiting_resources" ? heartbeat2.readiness : "unknown";
|
|
147
|
+
replayReady = heartbeat2.protocolVersion === 2 && Number.isFinite(heartbeat2.observedAt) && Date.now() - heartbeat2.observedAt >= 0 && Date.now() - heartbeat2.observedAt < 12e4;
|
|
133
148
|
} catch {
|
|
149
|
+
replayReadiness = "unknown";
|
|
134
150
|
replayReady = false;
|
|
135
151
|
}
|
|
136
152
|
};
|
|
@@ -147,7 +163,8 @@ var main = async (options = {}) => {
|
|
|
147
163
|
const heartbeatValue = () => ({
|
|
148
164
|
observedAt: Date.now(),
|
|
149
165
|
packageManifestSha256: manifestHash,
|
|
150
|
-
replayAvailable: replayReady
|
|
166
|
+
replayAvailable: replayReady,
|
|
167
|
+
replayReadiness
|
|
151
168
|
});
|
|
152
169
|
const stop = () => {
|
|
153
170
|
stopping = true;
|
|
@@ -208,7 +225,9 @@ var main = async (options = {}) => {
|
|
|
208
225
|
cursor = page.cursor;
|
|
209
226
|
for (const key of page.ids) {
|
|
210
227
|
const job = await import_redis.mcpStorage.get(key);
|
|
211
|
-
if (job
|
|
228
|
+
if (job && ["queued", "waiting_resources", "waiting_executor"].includes(
|
|
229
|
+
job.status
|
|
230
|
+
) && (!queued || job.createdAt < queued.createdAt))
|
|
212
231
|
queued = job;
|
|
213
232
|
}
|
|
214
233
|
} while (cursor !== "0" && !stopping);
|
|
@@ -219,13 +238,13 @@ var main = async (options = {}) => {
|
|
|
219
238
|
const jobKey = (0, import_mcp.mcpJobStorageId)(queued.userName, queued.id);
|
|
220
239
|
const started = {
|
|
221
240
|
...queued,
|
|
222
|
-
status: "running",
|
|
223
|
-
startedAt: Date.now(),
|
|
241
|
+
status: queued.kind === "runtime-feedback-replay" ? "waiting_executor" : "running",
|
|
242
|
+
...queued.kind === "runtime-feedback-replay" ? {} : { startedAt: Date.now() },
|
|
224
243
|
updatedAt: Date.now()
|
|
225
244
|
};
|
|
226
245
|
if (!await import_redis.mcpStorage.compare(jobKey, queued, started, import_mcp.MCP_JOB_TTL))
|
|
227
246
|
continue;
|
|
228
|
-
if (started.packageManifestSha256 !== manifestHash) {
|
|
247
|
+
if (started.kind !== "runtime-feedback-replay" && started.packageManifestSha256 !== manifestHash) {
|
|
229
248
|
await import_redis.mcpStorage.compare(
|
|
230
249
|
jobKey,
|
|
231
250
|
started,
|
|
@@ -299,7 +318,7 @@ var main = async (options = {}) => {
|
|
|
299
318
|
status: "failed",
|
|
300
319
|
finishedAt: Date.now(),
|
|
301
320
|
updatedAt: Date.now(),
|
|
302
|
-
error: error instanceof Error ? error.message : "Replay preparation failed"
|
|
321
|
+
error: error instanceof Error ? (0, import_mcp.redactMcpJobLog)(error.message) : "Replay preparation failed"
|
|
303
322
|
},
|
|
304
323
|
import_mcp.MCP_JOB_TTL
|
|
305
324
|
);
|
|
@@ -313,21 +332,15 @@ var main = async (options = {}) => {
|
|
|
313
332
|
});
|
|
314
333
|
const currentChild = child;
|
|
315
334
|
let timedOut = false;
|
|
316
|
-
|
|
335
|
+
let replayStartedAt;
|
|
336
|
+
const timeout = started.kind === "runtime-feedback-replay" ? void 0 : setTimeout(() => {
|
|
317
337
|
timedOut = true;
|
|
318
338
|
signalChild("SIGTERM");
|
|
319
339
|
}, JOB_TIMEOUT_MS);
|
|
320
340
|
const logs = [];
|
|
321
341
|
const log = (chunk) => {
|
|
322
342
|
const lines = chunk.toString().replace(/\u001b\[[0-9;]*m/g, "").split(/[\r\n]+/).filter(Boolean);
|
|
323
|
-
logs.push(
|
|
324
|
-
...lines.map(
|
|
325
|
-
(line) => line.slice(0, 1e3).replace(
|
|
326
|
-
/((?:secret|password|api.?key|token|authorization)\s*[:=]\s*)\S+/gi,
|
|
327
|
-
"$1[redacted]"
|
|
328
|
-
)
|
|
329
|
-
)
|
|
330
|
-
);
|
|
343
|
+
logs.push(...lines.map(import_mcp.redactMcpJobLog));
|
|
331
344
|
if (logs.length > 150) logs.splice(0, logs.length - 150);
|
|
332
345
|
};
|
|
333
346
|
currentChild.stdout?.on("data", log);
|
|
@@ -340,11 +353,38 @@ var main = async (options = {}) => {
|
|
|
340
353
|
killAt ??= Date.now();
|
|
341
354
|
signalChild(Date.now() - killAt > 8e3 ? "SIGKILL" : "SIGTERM");
|
|
342
355
|
}
|
|
356
|
+
let replayStatus;
|
|
357
|
+
if (latest?.kind === "runtime-feedback-replay") {
|
|
358
|
+
try {
|
|
359
|
+
const root = process.env.MCP_REPLAY_REQUEST_ROOT || `${process.env.PROJECT_CWD || process.cwd()}/data/mcp-replay-requests`;
|
|
360
|
+
const state = JSON.parse(
|
|
361
|
+
await (0, import_promises2.readFile)(`${root}/${latest.id}.state`, "utf8")
|
|
362
|
+
);
|
|
363
|
+
if (state && typeof state === "object" && "status" in state && (state.status === "waiting_resources" || state.status === "waiting_executor" || state.status === "running"))
|
|
364
|
+
replayStatus = state.status;
|
|
365
|
+
} catch {
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (replayStatus === "running") {
|
|
369
|
+
replayStartedAt ??= latest?.startedAt ?? Date.now();
|
|
370
|
+
if (Date.now() - replayStartedAt > JOB_TIMEOUT_MS) {
|
|
371
|
+
timedOut = true;
|
|
372
|
+
signalChild("SIGTERM");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
343
375
|
if (latest && !leaseLost)
|
|
344
376
|
await import_redis.mcpStorage.compare(
|
|
345
377
|
jobKey,
|
|
346
378
|
latest,
|
|
347
|
-
{
|
|
379
|
+
{
|
|
380
|
+
...latest,
|
|
381
|
+
...replayStatus && !latest.cancelRequested ? {
|
|
382
|
+
status: replayStatus,
|
|
383
|
+
...replayStatus === "running" && !latest.startedAt ? { startedAt: Date.now() } : {}
|
|
384
|
+
} : {},
|
|
385
|
+
logs: [...logs],
|
|
386
|
+
updatedAt: Date.now()
|
|
387
|
+
},
|
|
348
388
|
import_mcp.MCP_JOB_TTL
|
|
349
389
|
);
|
|
350
390
|
} catch {
|
|
@@ -357,7 +397,7 @@ var main = async (options = {}) => {
|
|
|
357
397
|
"error",
|
|
358
398
|
() => resolve({ code: null, error: "Unable to start research process" })
|
|
359
399
|
);
|
|
360
|
-
currentChild.once("
|
|
400
|
+
currentChild.once("close", (code) => resolve({ code }));
|
|
361
401
|
});
|
|
362
402
|
clearTimeout(timeout);
|
|
363
403
|
clearInterval(monitor);
|
|
@@ -374,7 +414,11 @@ var main = async (options = {}) => {
|
|
|
374
414
|
exitCode: outcome.code,
|
|
375
415
|
updatedAt: Date.now(),
|
|
376
416
|
finishedAt: Date.now(),
|
|
377
|
-
...cancelled ? {} : timedOut ? { error: "Job exceeded one hour" } : stopping ? { error: "Worker stopped; this job was not replayed" } : outcome.code !== 0 ? {
|
|
417
|
+
...cancelled ? {} : timedOut ? { error: "Job exceeded one hour" } : stopping ? { error: "Worker stopped; this job was not replayed" } : outcome.code !== 0 ? {
|
|
418
|
+
error: outcome.error || (started.kind === "runtime-feedback-replay" ? logs.find(
|
|
419
|
+
(line) => line.startsWith("Host replay: ")
|
|
420
|
+
) || logs.at(-1) : void 0) || "Research process failed"
|
|
421
|
+
} : {}
|
|
378
422
|
};
|
|
379
423
|
if (await import_redis.mcpStorage.compare(jobKey, latest, next, import_mcp.MCP_JOB_TTL))
|
|
380
424
|
break;
|
package/dist/scripts/replay.js
CHANGED
|
@@ -38,7 +38,7 @@ module.exports = __toCommonJS(replay_exports);
|
|
|
38
38
|
// src/scripts/replayRunner.ts
|
|
39
39
|
var import_chalk13 = __toESM(require("chalk"));
|
|
40
40
|
var import_constants7 = require("@tradejs/core/constants");
|
|
41
|
-
var
|
|
41
|
+
var import_data3 = require("@tradejs/core/data");
|
|
42
42
|
var import_time7 = require("@tradejs/core/time");
|
|
43
43
|
var import_redis6 = require("@tradejs/infra/redis");
|
|
44
44
|
var import_candles = require("@tradejs/infra/timescale/candles");
|
|
@@ -2708,27 +2708,27 @@ var createCoinMarketCapFetchStats = () => ({
|
|
|
2708
2708
|
totalCredits: 0,
|
|
2709
2709
|
byPath: /* @__PURE__ */ new Map()
|
|
2710
2710
|
});
|
|
2711
|
-
var getCoinMarketCapPathLabel = (
|
|
2712
|
-
if (
|
|
2713
|
-
if (
|
|
2714
|
-
if (
|
|
2715
|
-
if (
|
|
2716
|
-
if (
|
|
2717
|
-
if (
|
|
2718
|
-
return
|
|
2719
|
-
};
|
|
2720
|
-
var recordCoinMarketCapFetchStats = (stats,
|
|
2711
|
+
var getCoinMarketCapPathLabel = (path7) => {
|
|
2712
|
+
if (path7.includes("global-metrics")) return "global";
|
|
2713
|
+
if (path7.includes("cryptocurrency/quotes")) return "reference";
|
|
2714
|
+
if (path7.includes("exchange/quotes")) return "exchangeLiquidity";
|
|
2715
|
+
if (path7.includes("fear-and-greed")) return "fearGreed";
|
|
2716
|
+
if (path7.includes("cmc100")) return "cmc100";
|
|
2717
|
+
if (path7.includes("cmc20")) return "cmc20";
|
|
2718
|
+
return path7;
|
|
2719
|
+
};
|
|
2720
|
+
var recordCoinMarketCapFetchStats = (stats, path7, creditCount) => {
|
|
2721
2721
|
if (!stats) return;
|
|
2722
|
-
const existing = stats.byPath.get(
|
|
2722
|
+
const existing = stats.byPath.get(path7) ?? { requests: 0, credits: 0 };
|
|
2723
2723
|
existing.requests += 1;
|
|
2724
2724
|
existing.credits += creditCount ?? 0;
|
|
2725
2725
|
stats.requests += 1;
|
|
2726
2726
|
stats.totalCredits += creditCount ?? 0;
|
|
2727
|
-
stats.byPath.set(
|
|
2727
|
+
stats.byPath.set(path7, existing);
|
|
2728
2728
|
};
|
|
2729
2729
|
var formatCoinMarketCapFetchStats = (stats) => {
|
|
2730
2730
|
const byPath = [...stats.byPath.entries()].map(
|
|
2731
|
-
([
|
|
2731
|
+
([path7, item]) => `${getCoinMarketCapPathLabel(path7)}=${item.requests}req/${item.credits}cr`
|
|
2732
2732
|
).join(" ");
|
|
2733
2733
|
return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
|
|
2734
2734
|
};
|
|
@@ -4748,10 +4748,32 @@ var compactReplaySignal = (signal) => {
|
|
|
4748
4748
|
};
|
|
4749
4749
|
};
|
|
4750
4750
|
|
|
4751
|
+
// src/lib/replay/memoryProfile.ts
|
|
4752
|
+
var import_node_fs = __toESM(require("fs"));
|
|
4753
|
+
var import_node_path2 = __toESM(require("path"));
|
|
4754
|
+
var createReplayMemoryProfile = (file = process.env.TRADEJS_REPLAY_MEMORY_PROFILE) => {
|
|
4755
|
+
if (!file) return void 0;
|
|
4756
|
+
import_node_fs.default.mkdirSync(import_node_path2.default.dirname(file), { recursive: true });
|
|
4757
|
+
return (phase, counts = {}) => {
|
|
4758
|
+
import_node_fs.default.appendFileSync(
|
|
4759
|
+
file,
|
|
4760
|
+
JSON.stringify({
|
|
4761
|
+
schemaVersion: 1,
|
|
4762
|
+
observedAt: Date.now(),
|
|
4763
|
+
pid: process.pid,
|
|
4764
|
+
phase,
|
|
4765
|
+
counts,
|
|
4766
|
+
...process.memoryUsage(),
|
|
4767
|
+
maxRssKiB: process.resourceUsage().maxRSS
|
|
4768
|
+
}) + "\n"
|
|
4769
|
+
);
|
|
4770
|
+
};
|
|
4771
|
+
};
|
|
4772
|
+
|
|
4751
4773
|
// src/lib/replay/historicalSignalsReplay.ts
|
|
4752
4774
|
var import_chalk9 = __toESM(require("chalk"));
|
|
4753
4775
|
var import_progress4 = __toESM(require("progress"));
|
|
4754
|
-
var
|
|
4776
|
+
var import_node_path3 = __toESM(require("path"));
|
|
4755
4777
|
var import_node_module = require("module");
|
|
4756
4778
|
var import_strategies3 = require("@tradejs/core/strategies");
|
|
4757
4779
|
var import_time3 = require("@tradejs/core/time");
|
|
@@ -5300,6 +5322,11 @@ var prepareHistoricalReplay = async (context, adapters) => {
|
|
|
5300
5322
|
cacheOnly: true,
|
|
5301
5323
|
interval
|
|
5302
5324
|
});
|
|
5325
|
+
if (symbolRuntimes.length % 50 === 0)
|
|
5326
|
+
adapters.sampleMemory?.("history:loaded", {
|
|
5327
|
+
symbols: symbolRuntimes.length + 1,
|
|
5328
|
+
candles: data.length
|
|
5329
|
+
});
|
|
5303
5330
|
const preparedData = buildPreparedData({
|
|
5304
5331
|
data,
|
|
5305
5332
|
btcData: references.btcMarketData,
|
|
@@ -5396,6 +5423,11 @@ var prepareHistoricalReplay = async (context, adapters) => {
|
|
|
5396
5423
|
strategies: strategiesForSymbol
|
|
5397
5424
|
};
|
|
5398
5425
|
symbolRuntimes.push(symbolRuntime);
|
|
5426
|
+
if (symbolRuntimes.length % 50 === 0 || symbol === preparedRun.tickers.at(-1))
|
|
5427
|
+
adapters.sampleMemory?.("strategies:initializing", {
|
|
5428
|
+
symbols: symbolRuntimes.length,
|
|
5429
|
+
strategiesPerSymbol: strategiesForSymbol.length
|
|
5430
|
+
});
|
|
5399
5431
|
adapters.progress.tick({
|
|
5400
5432
|
skipped: adapters.display.skipped(skippedSymbols),
|
|
5401
5433
|
symbol: adapters.display.symbol(symbol)
|
|
@@ -5413,6 +5445,10 @@ var prepareHistoricalReplay = async (context, adapters) => {
|
|
|
5413
5445
|
runtimeLineages
|
|
5414
5446
|
};
|
|
5415
5447
|
};
|
|
5448
|
+
var releaseHistoricalReplayPlan = (plan) => {
|
|
5449
|
+
plan.symbolRuntimes.length = 0;
|
|
5450
|
+
plan.sharedReplayKeyPrefixes.length = 0;
|
|
5451
|
+
};
|
|
5416
5452
|
|
|
5417
5453
|
// src/lib/replay/historicalSignalsReplaySkipEvidence.ts
|
|
5418
5454
|
var import_data = require("@tradejs/core/data");
|
|
@@ -5712,7 +5748,7 @@ var getProjectReplayCacheReleasers = () => {
|
|
|
5712
5748
|
}
|
|
5713
5749
|
try {
|
|
5714
5750
|
const requireFromProject = (0, import_node_module.createRequire)(
|
|
5715
|
-
|
|
5751
|
+
import_node_path3.default.join(replayProjectRoot, "package.json")
|
|
5716
5752
|
);
|
|
5717
5753
|
projectReplayCacheReleasers = requireFromProject(
|
|
5718
5754
|
"@tradejs/core/strategies"
|
|
@@ -5853,6 +5889,8 @@ var runHistoricalSignalsReplay = async ({
|
|
|
5853
5889
|
collectSkipEvidence = true,
|
|
5854
5890
|
collectSignals = true
|
|
5855
5891
|
}) => {
|
|
5892
|
+
const memory = createReplayMemoryProfile();
|
|
5893
|
+
memory?.("prepare:start");
|
|
5856
5894
|
const startedAt = Date.now();
|
|
5857
5895
|
const projectConfig = await (0, import_cli4.loadTradejsConfig)(replayProjectRoot);
|
|
5858
5896
|
const projectHooks = projectConfig.hooks;
|
|
@@ -5869,6 +5907,7 @@ var runHistoricalSignalsReplay = async ({
|
|
|
5869
5907
|
width: 30
|
|
5870
5908
|
}
|
|
5871
5909
|
);
|
|
5910
|
+
memory?.("references:loaded");
|
|
5872
5911
|
const plan = await prepareHistoricalReplay(
|
|
5873
5912
|
{
|
|
5874
5913
|
userName: replayUserName,
|
|
@@ -5893,9 +5932,11 @@ var runHistoricalSignalsReplay = async ({
|
|
|
5893
5932
|
skipped: (value) => import_chalk9.default.yellow(value),
|
|
5894
5933
|
symbol: (value) => import_chalk9.default.gray(value)
|
|
5895
5934
|
},
|
|
5896
|
-
buildLineage: buildRuntimeLineage
|
|
5935
|
+
buildLineage: buildRuntimeLineage,
|
|
5936
|
+
sampleMemory: memory
|
|
5897
5937
|
}
|
|
5898
5938
|
);
|
|
5939
|
+
memory?.("strategies:prepared", { symbols: plan.symbolRuntimes.length });
|
|
5899
5940
|
const hookContext = buildAfterSignalsContext({
|
|
5900
5941
|
connector: replayConnector,
|
|
5901
5942
|
connectorName: preparedRun.connectorName,
|
|
@@ -5945,13 +5986,16 @@ var runHistoricalSignalsReplay = async ({
|
|
|
5945
5986
|
)
|
|
5946
5987
|
}
|
|
5947
5988
|
);
|
|
5989
|
+
memory?.("execution:complete", { signals: execution.signals.length });
|
|
5990
|
+
releaseHistoricalReplayPlan(plan);
|
|
5991
|
+
memory?.("execution:released");
|
|
5948
5992
|
const artifacts = replayConnector.getReplayArtifacts();
|
|
5949
5993
|
import_logger.logger.info(
|
|
5950
5994
|
import_chalk9.default.gray(
|
|
5951
5995
|
`signals replay historical cycles: ${plan.orderedTimestamps.length} (aborted=${execution.abortedCycles}) done in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s`
|
|
5952
5996
|
)
|
|
5953
5997
|
);
|
|
5954
|
-
|
|
5998
|
+
const result = collectHistoricalReplayResult({
|
|
5955
5999
|
strategies: loadedStrategies,
|
|
5956
6000
|
artifacts,
|
|
5957
6001
|
signals: execution.signals,
|
|
@@ -5960,6 +6004,8 @@ var runHistoricalSignalsReplay = async ({
|
|
|
5960
6004
|
runtimeLineages: plan.runtimeLineages,
|
|
5961
6005
|
replayLineageScopes: execution.replayLineageScopes
|
|
5962
6006
|
});
|
|
6007
|
+
memory?.("result:collected");
|
|
6008
|
+
return result;
|
|
5963
6009
|
};
|
|
5964
6010
|
|
|
5965
6011
|
// src/lib/replay/chartSnapshot.ts
|
|
@@ -8794,9 +8840,87 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8794
8840
|
};
|
|
8795
8841
|
|
|
8796
8842
|
// src/lib/replay/outputReport.ts
|
|
8843
|
+
var import_promises3 = __toESM(require("fs/promises"));
|
|
8844
|
+
var import_node_path5 = __toESM(require("path"));
|
|
8845
|
+
|
|
8846
|
+
// src/lib/replay/jsonFile.ts
|
|
8847
|
+
var import_node_fs2 = __toESM(require("fs"));
|
|
8797
8848
|
var import_promises2 = __toESM(require("fs/promises"));
|
|
8798
|
-
var
|
|
8799
|
-
var
|
|
8849
|
+
var import_node_path4 = __toESM(require("path"));
|
|
8850
|
+
var import_node_events = require("events");
|
|
8851
|
+
var import_node_crypto2 = require("crypto");
|
|
8852
|
+
function* jsonParts(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
8853
|
+
if (value === null || typeof value !== "object") {
|
|
8854
|
+
yield JSON.stringify(value) ?? "null";
|
|
8855
|
+
return;
|
|
8856
|
+
}
|
|
8857
|
+
if (ancestors.has(value)) throw new TypeError("Circular JSON evidence");
|
|
8858
|
+
ancestors.add(value);
|
|
8859
|
+
try {
|
|
8860
|
+
if (Array.isArray(value)) {
|
|
8861
|
+
yield "[";
|
|
8862
|
+
for (let index = 0; index < value.length; index++) {
|
|
8863
|
+
if (index) yield ",";
|
|
8864
|
+
yield* jsonParts(value[index], ancestors);
|
|
8865
|
+
}
|
|
8866
|
+
yield "]";
|
|
8867
|
+
} else {
|
|
8868
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
8869
|
+
throw new TypeError("JSON evidence requires plain records");
|
|
8870
|
+
if ("toJSON" in value && typeof value.toJSON === "function")
|
|
8871
|
+
throw new TypeError("JSON evidence cannot contain toJSON hooks");
|
|
8872
|
+
yield "{";
|
|
8873
|
+
let first = true;
|
|
8874
|
+
for (const key of Object.keys(value)) {
|
|
8875
|
+
const field = Reflect.get(value, key);
|
|
8876
|
+
if (field === void 0 || typeof field === "function" || typeof field === "symbol")
|
|
8877
|
+
continue;
|
|
8878
|
+
if (!first) yield ",";
|
|
8879
|
+
first = false;
|
|
8880
|
+
yield JSON.stringify(key) + ":";
|
|
8881
|
+
yield* jsonParts(field, ancestors);
|
|
8882
|
+
}
|
|
8883
|
+
yield "}";
|
|
8884
|
+
}
|
|
8885
|
+
} finally {
|
|
8886
|
+
ancestors.delete(value);
|
|
8887
|
+
}
|
|
8888
|
+
}
|
|
8889
|
+
var writeReplayJsonFile = async (file, value) => {
|
|
8890
|
+
await import_promises2.default.mkdir(import_node_path4.default.dirname(file), { recursive: true });
|
|
8891
|
+
const temporary = file + ".tmp-" + (0, import_node_crypto2.randomUUID)();
|
|
8892
|
+
const stream = import_node_fs2.default.createWriteStream(temporary, {
|
|
8893
|
+
flags: "wx",
|
|
8894
|
+
highWaterMark: 65536
|
|
8895
|
+
});
|
|
8896
|
+
let failure;
|
|
8897
|
+
stream.on("error", (error) => {
|
|
8898
|
+
failure = error;
|
|
8899
|
+
});
|
|
8900
|
+
try {
|
|
8901
|
+
let buffer = "";
|
|
8902
|
+
for (const part of jsonParts(value)) {
|
|
8903
|
+
buffer += part;
|
|
8904
|
+
if (buffer.length >= 65536) {
|
|
8905
|
+
if (failure) throw failure;
|
|
8906
|
+
if (!stream.write(buffer)) await (0, import_node_events.once)(stream, "drain");
|
|
8907
|
+
buffer = "";
|
|
8908
|
+
}
|
|
8909
|
+
}
|
|
8910
|
+
if (failure) throw failure;
|
|
8911
|
+
stream.end(buffer + "\n");
|
|
8912
|
+
await (0, import_node_events.once)(stream, "close");
|
|
8913
|
+
if (failure) throw failure;
|
|
8914
|
+
await import_promises2.default.rename(temporary, file);
|
|
8915
|
+
} catch (error) {
|
|
8916
|
+
stream.destroy();
|
|
8917
|
+
if (!stream.closed) await (0, import_node_events.once)(stream, "close").catch(() => void 0);
|
|
8918
|
+
await import_promises2.default.rm(temporary, { force: true });
|
|
8919
|
+
throw error;
|
|
8920
|
+
}
|
|
8921
|
+
};
|
|
8922
|
+
|
|
8923
|
+
// src/lib/replay/outputReport.ts
|
|
8800
8924
|
var formatMsk = (timestamp) => typeof timestamp === "number" && Number.isFinite(timestamp) ? new Intl.DateTimeFormat("ru-RU", {
|
|
8801
8925
|
timeZone: "Europe/Moscow",
|
|
8802
8926
|
dateStyle: "short",
|
|
@@ -9099,8 +9223,8 @@ var writeReplayOutputReport = async ({
|
|
|
9099
9223
|
strategySnapshot,
|
|
9100
9224
|
runtimeComparison
|
|
9101
9225
|
}) => {
|
|
9102
|
-
const outputDir =
|
|
9103
|
-
await
|
|
9226
|
+
const outputDir = import_node_path5.default.resolve(projectRoot, "data/replay/output");
|
|
9227
|
+
await import_promises3.default.mkdir(outputDir, { recursive: true });
|
|
9104
9228
|
const perTradeAnalysis = buildPerTradeAnalysis(runtimeComparison);
|
|
9105
9229
|
const report = {
|
|
9106
9230
|
reportType: "replay-output",
|
|
@@ -9135,8 +9259,8 @@ var writeReplayOutputReport = async ({
|
|
|
9135
9259
|
runtimeComparison,
|
|
9136
9260
|
perTradeAnalysis
|
|
9137
9261
|
};
|
|
9138
|
-
const markdownPath =
|
|
9139
|
-
const jsonPath =
|
|
9262
|
+
const markdownPath = import_node_path5.default.join(outputDir, `${timestamp}-replay-report.md`);
|
|
9263
|
+
const jsonPath = import_node_path5.default.join(outputDir, `${timestamp}-replay-report.json`);
|
|
9140
9264
|
const lines = [
|
|
9141
9265
|
"# Replay Report",
|
|
9142
9266
|
"",
|
|
@@ -9201,11 +9325,13 @@ var writeReplayOutputReport = async ({
|
|
|
9201
9325
|
"Full machine-readable details are stored in the JSON sidecar.",
|
|
9202
9326
|
""
|
|
9203
9327
|
];
|
|
9328
|
+
const memory = createReplayMemoryProfile();
|
|
9329
|
+
memory?.("report:write:start");
|
|
9204
9330
|
await Promise.all([
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
`, "utf8")
|
|
9331
|
+
import_promises3.default.writeFile(markdownPath, lines.join("\n"), "utf8"),
|
|
9332
|
+
writeReplayJsonFile(jsonPath, report)
|
|
9208
9333
|
]);
|
|
9334
|
+
memory?.("report:write:complete");
|
|
9209
9335
|
return {
|
|
9210
9336
|
markdownPath,
|
|
9211
9337
|
jsonPath
|
|
@@ -9213,9 +9339,9 @@ var writeReplayOutputReport = async ({
|
|
|
9213
9339
|
};
|
|
9214
9340
|
|
|
9215
9341
|
// src/lib/replay/portfolioReport.ts
|
|
9216
|
-
var
|
|
9217
|
-
var
|
|
9218
|
-
var
|
|
9342
|
+
var import_promises4 = __toESM(require("fs/promises"));
|
|
9343
|
+
var import_node_path6 = __toESM(require("path"));
|
|
9344
|
+
var import_node_crypto3 = require("crypto");
|
|
9219
9345
|
var DAY_MS4 = 864e5;
|
|
9220
9346
|
var WINDOWS = [365, 180, 90, 30, 7];
|
|
9221
9347
|
var COLORS = [
|
|
@@ -9393,8 +9519,8 @@ var writePortfolioReport = async ({
|
|
|
9393
9519
|
lineage,
|
|
9394
9520
|
command
|
|
9395
9521
|
}) => {
|
|
9396
|
-
const outputDir =
|
|
9397
|
-
await
|
|
9522
|
+
const outputDir = import_node_path6.default.resolve(projectRoot, "data/replay/output");
|
|
9523
|
+
await import_promises4.default.mkdir(outputDir, { recursive: true });
|
|
9398
9524
|
const trades = replayResult.strategies.flatMap(
|
|
9399
9525
|
(strategy) => tradesFromPositionLog(strategy.strategyName, strategy.positionLog)
|
|
9400
9526
|
);
|
|
@@ -9466,11 +9592,11 @@ var writePortfolioReport = async ({
|
|
|
9466
9592
|
});
|
|
9467
9593
|
const prefix = `${timestamp}-portfolio-backtest`;
|
|
9468
9594
|
const paths = {
|
|
9469
|
-
markdown:
|
|
9470
|
-
json:
|
|
9471
|
-
equitySvg:
|
|
9472
|
-
drawdownSvg:
|
|
9473
|
-
html:
|
|
9595
|
+
markdown: import_node_path6.default.join(outputDir, `${prefix}.md`),
|
|
9596
|
+
json: import_node_path6.default.join(outputDir, `${prefix}.json`),
|
|
9597
|
+
equitySvg: import_node_path6.default.join(outputDir, `${prefix}-equity.svg`),
|
|
9598
|
+
drawdownSvg: import_node_path6.default.join(outputDir, `${prefix}-drawdown.svg`),
|
|
9599
|
+
html: import_node_path6.default.join(outputDir, `${prefix}.html`)
|
|
9474
9600
|
};
|
|
9475
9601
|
const report = {
|
|
9476
9602
|
schema: "tradejs-portfolio-backtest/v1",
|
|
@@ -9496,7 +9622,7 @@ var writePortfolioReport = async ({
|
|
|
9496
9622
|
},
|
|
9497
9623
|
trades
|
|
9498
9624
|
};
|
|
9499
|
-
const checksum = (0,
|
|
9625
|
+
const checksum = (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(report)).digest("hex");
|
|
9500
9626
|
const semantics = "Production strategy order, at most one open position per symbol, and fixed-risk symbol batches merged by canonical realized PnL.";
|
|
9501
9627
|
const markdown = `# Portfolio Backtest
|
|
9502
9628
|
|
|
@@ -9508,9 +9634,9 @@ var writePortfolioReport = async ({
|
|
|
9508
9634
|
- Report checksum: \`${checksum}\`
|
|
9509
9635
|
- Command: \`${command}\`
|
|
9510
9636
|
|
|
9511
|
-
})
|
|
9512
9638
|
|
|
9513
|
-
})
|
|
9514
9640
|
|
|
9515
9641
|
## Portfolio metrics
|
|
9516
9642
|
|
|
@@ -9522,16 +9648,16 @@ ${markdownTable(strategyRows)}
|
|
|
9522
9648
|
`;
|
|
9523
9649
|
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Portfolio Backtest</title><style>body{font-family:Inter,system-ui,sans-serif;margin:32px;color:#0f172a}h1,h2{margin-top:32px}.meta{color:#475569}table{border-collapse:collapse;width:100%;font-size:13px}th,td{padding:8px;border:1px solid #e2e8f0;text-align:right}th:nth-child(-n+3),td:nth-child(-n+3){text-align:left}.chart{overflow:auto;border:1px solid #e2e8f0;margin:16px 0}</style></head><body><h1>Portfolio Backtest</h1><p class="meta">${escapeHtml(report.window.startIso)} .. ${escapeHtml(report.window.endIso)} \xB7 ${report.counts.strategies} strategies \xB7 ${trades.length} completed trades \xB7 MAX_LOSS_VALUE=${escapeHtml(String(lineage.maxLossValue))}</p><p class="meta">${escapeHtml(semantics)}</p><div class="chart">${equitySvg}</div><div class="chart">${drawdownSvg}</div><h2>Portfolio metrics</h2>${metricTable(portfolioRows)}<h2>Strategy metrics</h2>${metricTable(strategyRows)}<h2>Lineage</h2><pre>${escapeHtml(JSON.stringify(lineage, null, 2))}</pre></body></html>`;
|
|
9524
9650
|
await Promise.all([
|
|
9525
|
-
|
|
9526
|
-
|
|
9651
|
+
import_promises4.default.writeFile(paths.markdown, markdown, "utf8"),
|
|
9652
|
+
import_promises4.default.writeFile(
|
|
9527
9653
|
paths.json,
|
|
9528
9654
|
`${JSON.stringify({ ...report, checksum }, null, 2)}
|
|
9529
9655
|
`,
|
|
9530
9656
|
"utf8"
|
|
9531
9657
|
),
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9658
|
+
import_promises4.default.writeFile(paths.equitySvg, equitySvg, "utf8"),
|
|
9659
|
+
import_promises4.default.writeFile(paths.drawdownSvg, drawdownSvg, "utf8"),
|
|
9660
|
+
import_promises4.default.writeFile(paths.html, html, "utf8")
|
|
9535
9661
|
]);
|
|
9536
9662
|
return { ...paths, checksum };
|
|
9537
9663
|
};
|
|
@@ -9845,13 +9971,13 @@ var replayBacktest = async () => {
|
|
|
9845
9971
|
deploymentId: deployment.id,
|
|
9846
9972
|
assetClasses: deployment.assetClasses,
|
|
9847
9973
|
deployment,
|
|
9848
|
-
closedIntervalMs: (0,
|
|
9974
|
+
closedIntervalMs: (0, import_data3.intervalToMs)(replayInterval)
|
|
9849
9975
|
});
|
|
9850
9976
|
if (!preparedRun || isReplayUpdateOnlyRun) {
|
|
9851
9977
|
return;
|
|
9852
9978
|
}
|
|
9853
9979
|
if (useAllData) {
|
|
9854
|
-
const intervalMs = (0,
|
|
9980
|
+
const intervalMs = (0, import_data3.intervalToMs)(replayInterval);
|
|
9855
9981
|
const edges = await (0, import_candles.getDataEdgesForSymbols)(
|
|
9856
9982
|
deployment.provider,
|
|
9857
9983
|
preparedRun.tickers,
|