@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
|
@@ -103,7 +103,7 @@ var buildMcpReplayProcess = async ({
|
|
|
103
103
|
const heartbeat = JSON.parse(
|
|
104
104
|
await import_promises.default.readFile(import_node_path.default.join(root, "heartbeat.json"), "utf8")
|
|
105
105
|
);
|
|
106
|
-
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)
|
|
107
107
|
throw new Error("Host replay runner heartbeat is stale");
|
|
108
108
|
const bytes = await import_promises.default.readFile(evidencePath);
|
|
109
109
|
const request = JSON.stringify({
|
|
@@ -136,14 +136,17 @@ var main = async (options = {}) => {
|
|
|
136
136
|
let leaseLost = false;
|
|
137
137
|
let manifestHash = "";
|
|
138
138
|
let replayReady = false;
|
|
139
|
+
let replayReadiness = "unknown";
|
|
139
140
|
const updateReplayReady = async () => {
|
|
140
141
|
try {
|
|
141
142
|
const root = process.env.MCP_REPLAY_REQUEST_ROOT || `${process.env.PROJECT_CWD || process.cwd()}/data/mcp-replay-requests`;
|
|
142
143
|
const heartbeat2 = JSON.parse(
|
|
143
144
|
await (0, import_promises2.readFile)(`${root}/heartbeat.json`, "utf8")
|
|
144
145
|
);
|
|
145
|
-
|
|
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;
|
|
146
148
|
} catch {
|
|
149
|
+
replayReadiness = "unknown";
|
|
147
150
|
replayReady = false;
|
|
148
151
|
}
|
|
149
152
|
};
|
|
@@ -160,7 +163,8 @@ var main = async (options = {}) => {
|
|
|
160
163
|
const heartbeatValue = () => ({
|
|
161
164
|
observedAt: Date.now(),
|
|
162
165
|
packageManifestSha256: manifestHash,
|
|
163
|
-
replayAvailable: replayReady
|
|
166
|
+
replayAvailable: replayReady,
|
|
167
|
+
replayReadiness
|
|
164
168
|
});
|
|
165
169
|
const stop = () => {
|
|
166
170
|
stopping = true;
|
|
@@ -221,7 +225,9 @@ var main = async (options = {}) => {
|
|
|
221
225
|
cursor = page.cursor;
|
|
222
226
|
for (const key of page.ids) {
|
|
223
227
|
const job = await import_redis.mcpStorage.get(key);
|
|
224
|
-
if (job
|
|
228
|
+
if (job && ["queued", "waiting_resources", "waiting_executor"].includes(
|
|
229
|
+
job.status
|
|
230
|
+
) && (!queued || job.createdAt < queued.createdAt))
|
|
225
231
|
queued = job;
|
|
226
232
|
}
|
|
227
233
|
} while (cursor !== "0" && !stopping);
|
|
@@ -232,13 +238,13 @@ var main = async (options = {}) => {
|
|
|
232
238
|
const jobKey = (0, import_mcp.mcpJobStorageId)(queued.userName, queued.id);
|
|
233
239
|
const started = {
|
|
234
240
|
...queued,
|
|
235
|
-
status: "running",
|
|
236
|
-
startedAt: Date.now(),
|
|
241
|
+
status: queued.kind === "runtime-feedback-replay" ? "waiting_executor" : "running",
|
|
242
|
+
...queued.kind === "runtime-feedback-replay" ? {} : { startedAt: Date.now() },
|
|
237
243
|
updatedAt: Date.now()
|
|
238
244
|
};
|
|
239
245
|
if (!await import_redis.mcpStorage.compare(jobKey, queued, started, import_mcp.MCP_JOB_TTL))
|
|
240
246
|
continue;
|
|
241
|
-
if (started.packageManifestSha256 !== manifestHash) {
|
|
247
|
+
if (started.kind !== "runtime-feedback-replay" && started.packageManifestSha256 !== manifestHash) {
|
|
242
248
|
await import_redis.mcpStorage.compare(
|
|
243
249
|
jobKey,
|
|
244
250
|
started,
|
|
@@ -326,7 +332,8 @@ var main = async (options = {}) => {
|
|
|
326
332
|
});
|
|
327
333
|
const currentChild = child;
|
|
328
334
|
let timedOut = false;
|
|
329
|
-
|
|
335
|
+
let replayStartedAt;
|
|
336
|
+
const timeout = started.kind === "runtime-feedback-replay" ? void 0 : setTimeout(() => {
|
|
330
337
|
timedOut = true;
|
|
331
338
|
signalChild("SIGTERM");
|
|
332
339
|
}, JOB_TIMEOUT_MS);
|
|
@@ -346,11 +353,38 @@ var main = async (options = {}) => {
|
|
|
346
353
|
killAt ??= Date.now();
|
|
347
354
|
signalChild(Date.now() - killAt > 8e3 ? "SIGKILL" : "SIGTERM");
|
|
348
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
|
+
}
|
|
349
375
|
if (latest && !leaseLost)
|
|
350
376
|
await import_redis.mcpStorage.compare(
|
|
351
377
|
jobKey,
|
|
352
378
|
latest,
|
|
353
|
-
{
|
|
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
|
+
},
|
|
354
388
|
import_mcp.MCP_JOB_TTL
|
|
355
389
|
);
|
|
356
390
|
} catch {
|
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,
|