@tradejs/cli 3.1.27-beta.250 → 3.1.28-beta.251
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 +24040 -23180
- package/dist/lib/aiExport.js +1 -0
- package/dist/lib/coreResearch.js +42 -3
- package/dist/scripts/aiExport.js +1 -0
- package/dist/scripts/backtest.js +35 -6
- package/dist/scripts/coreResearch.js +42 -3
- package/dist/scripts/mlExport.js +1 -0
- package/dist/scripts/replay.js +808 -110
- package/dist/scripts/replayRunner.js +808 -110
- package/dist/scripts/signals.js +103 -27
- package/dist/scripts/signalsDaemon.js +103 -27
- package/package.json +7 -7
package/dist/scripts/replay.js
CHANGED
|
@@ -36,10 +36,13 @@ module.exports = __toCommonJS(replay_exports);
|
|
|
36
36
|
|
|
37
37
|
// src/scripts/replayRunner.ts
|
|
38
38
|
var import_chalk13 = __toESM(require("chalk"));
|
|
39
|
-
var
|
|
39
|
+
var import_constants7 = require("@tradejs/core/constants");
|
|
40
40
|
var import_data4 = require("@tradejs/core/data");
|
|
41
41
|
var import_time7 = require("@tradejs/core/time");
|
|
42
42
|
var import_redis6 = require("@tradejs/infra/redis");
|
|
43
|
+
var import_candles = require("@tradejs/infra/timescale/candles");
|
|
44
|
+
var import_runtimeStrategies4 = require("@tradejs/node/runtimeStrategies");
|
|
45
|
+
var import_strategies5 = require("@tradejs/node/strategies");
|
|
43
46
|
|
|
44
47
|
// src/lib/runFormatting.ts
|
|
45
48
|
var import_date_fns = require("date-fns");
|
|
@@ -887,8 +890,8 @@ var buildMarketBreadthRows = ({
|
|
|
887
890
|
let altTurnover24h = 0;
|
|
888
891
|
let previousAltTurnover24h = 0;
|
|
889
892
|
for (const indexed of indexedSymbols) {
|
|
890
|
-
const { candles, byTimestamp, turnoverPrefix, closePrefix } = indexed;
|
|
891
|
-
const index =
|
|
893
|
+
const { candles, byTimestamp: byTimestamp2, turnoverPrefix, closePrefix } = indexed;
|
|
894
|
+
const index = byTimestamp2.get(timestamp);
|
|
892
895
|
if (index == null) continue;
|
|
893
896
|
const candle = candles[index];
|
|
894
897
|
const previous = candles[index - 1];
|
|
@@ -2704,27 +2707,27 @@ var createCoinMarketCapFetchStats = () => ({
|
|
|
2704
2707
|
totalCredits: 0,
|
|
2705
2708
|
byPath: /* @__PURE__ */ new Map()
|
|
2706
2709
|
});
|
|
2707
|
-
var getCoinMarketCapPathLabel = (
|
|
2708
|
-
if (
|
|
2709
|
-
if (
|
|
2710
|
-
if (
|
|
2711
|
-
if (
|
|
2712
|
-
if (
|
|
2713
|
-
if (
|
|
2714
|
-
return
|
|
2715
|
-
};
|
|
2716
|
-
var recordCoinMarketCapFetchStats = (stats,
|
|
2710
|
+
var getCoinMarketCapPathLabel = (path5) => {
|
|
2711
|
+
if (path5.includes("global-metrics")) return "global";
|
|
2712
|
+
if (path5.includes("cryptocurrency/quotes")) return "reference";
|
|
2713
|
+
if (path5.includes("exchange/quotes")) return "exchangeLiquidity";
|
|
2714
|
+
if (path5.includes("fear-and-greed")) return "fearGreed";
|
|
2715
|
+
if (path5.includes("cmc100")) return "cmc100";
|
|
2716
|
+
if (path5.includes("cmc20")) return "cmc20";
|
|
2717
|
+
return path5;
|
|
2718
|
+
};
|
|
2719
|
+
var recordCoinMarketCapFetchStats = (stats, path5, creditCount) => {
|
|
2717
2720
|
if (!stats) return;
|
|
2718
|
-
const existing = stats.byPath.get(
|
|
2721
|
+
const existing = stats.byPath.get(path5) ?? { requests: 0, credits: 0 };
|
|
2719
2722
|
existing.requests += 1;
|
|
2720
2723
|
existing.credits += creditCount ?? 0;
|
|
2721
2724
|
stats.requests += 1;
|
|
2722
2725
|
stats.totalCredits += creditCount ?? 0;
|
|
2723
|
-
stats.byPath.set(
|
|
2726
|
+
stats.byPath.set(path5, existing);
|
|
2724
2727
|
};
|
|
2725
2728
|
var formatCoinMarketCapFetchStats = (stats) => {
|
|
2726
2729
|
const byPath = [...stats.byPath.entries()].map(
|
|
2727
|
-
([
|
|
2730
|
+
([path5, item]) => `${getCoinMarketCapPathLabel(path5)}=${item.requests}req/${item.credits}cr`
|
|
2728
2731
|
).join(" ");
|
|
2729
2732
|
return `apiRequests=${stats.requests} apiCredits=${stats.totalCredits}${byPath ? ` ${byPath}` : ""}`;
|
|
2730
2733
|
};
|
|
@@ -4320,6 +4323,29 @@ import_args.default.option(
|
|
|
4320
4323
|
"Save compact replay chart data for the strategies UI",
|
|
4321
4324
|
false
|
|
4322
4325
|
);
|
|
4326
|
+
import_args.default.option(
|
|
4327
|
+
"allData",
|
|
4328
|
+
"Use the complete cached candle range for the selected provider, interval, and symbols",
|
|
4329
|
+
false
|
|
4330
|
+
);
|
|
4331
|
+
import_args.default.option(
|
|
4332
|
+
"maxLossValue",
|
|
4333
|
+
"Override MAX_LOSS_VALUE for every selected runtime strategy without changing its declaration"
|
|
4334
|
+
);
|
|
4335
|
+
import_args.default.option(
|
|
4336
|
+
"portfolioReport",
|
|
4337
|
+
"Write aggregate portfolio metrics and SVG charts; skip runtime/exchange comparison",
|
|
4338
|
+
false
|
|
4339
|
+
);
|
|
4340
|
+
import_args.default.option(
|
|
4341
|
+
["B", "portfolioBatchSize"],
|
|
4342
|
+
"Maximum number of symbols held in memory by one portfolio replay batch",
|
|
4343
|
+
4
|
|
4344
|
+
);
|
|
4345
|
+
import_args.default.option(
|
|
4346
|
+
["X", "portfolioOutputSuffix"],
|
|
4347
|
+
"Optional safe suffix for parallel portfolio report shards"
|
|
4348
|
+
);
|
|
4323
4349
|
import_args.default.option(["L", "showTickersList"], "Just show only ticker list", false);
|
|
4324
4350
|
import_args.default.option(["U", "user"], "Use user config", "root");
|
|
4325
4351
|
import_args.default.option(
|
|
@@ -4951,6 +4977,8 @@ var compactReplaySignal = (signal) => {
|
|
|
4951
4977
|
// src/lib/replay/historicalSignalsReplay.ts
|
|
4952
4978
|
var import_chalk9 = __toESM(require("chalk"));
|
|
4953
4979
|
var import_progress4 = __toESM(require("progress"));
|
|
4980
|
+
var import_node_path2 = __toESM(require("path"));
|
|
4981
|
+
var import_node_module = require("module");
|
|
4954
4982
|
var import_strategies3 = require("@tradejs/core/strategies");
|
|
4955
4983
|
var import_time3 = require("@tradejs/core/time");
|
|
4956
4984
|
var import_logger = require("@tradejs/infra/logger");
|
|
@@ -5034,7 +5062,8 @@ var createPortfolioReplayConnector = (connector) => {
|
|
|
5034
5062
|
close: {
|
|
5035
5063
|
timestamp,
|
|
5036
5064
|
amount: (0, import_math.round)(amount)
|
|
5037
|
-
}
|
|
5065
|
+
},
|
|
5066
|
+
netProfit: (0, import_math.round)(positionState.currentProfit)
|
|
5038
5067
|
};
|
|
5039
5068
|
positionLog.push(nextPositionLog);
|
|
5040
5069
|
appendStrategyPositionLog(
|
|
@@ -5484,7 +5513,8 @@ var prepareHistoricalReplay = async (context, adapters) => {
|
|
|
5484
5513
|
strategies,
|
|
5485
5514
|
references
|
|
5486
5515
|
} = context;
|
|
5487
|
-
const
|
|
5516
|
+
const symbolRuntimes = [];
|
|
5517
|
+
const orderedTimestamps = /* @__PURE__ */ new Set();
|
|
5488
5518
|
const sharedReplayKeyPrefixes = [];
|
|
5489
5519
|
const runtimeLineages = [];
|
|
5490
5520
|
let skippedSymbols = 0;
|
|
@@ -5591,19 +5621,18 @@ var prepareHistoricalReplay = async (context, adapters) => {
|
|
|
5591
5621
|
currentIndex: 0,
|
|
5592
5622
|
strategies: strategiesForSymbol
|
|
5593
5623
|
};
|
|
5624
|
+
symbolRuntimes.push(symbolRuntime);
|
|
5594
5625
|
adapters.progress.tick({
|
|
5595
5626
|
skipped: adapters.display.skipped(skippedSymbols),
|
|
5596
5627
|
symbol: adapters.display.symbol(symbol)
|
|
5597
5628
|
});
|
|
5598
5629
|
for (const candle of preparedData.replayData) {
|
|
5599
|
-
|
|
5600
|
-
bucket.push(symbolRuntime);
|
|
5601
|
-
cycleSymbolsByTimestamp.set(candle.timestamp, bucket);
|
|
5630
|
+
orderedTimestamps.add(candle.timestamp);
|
|
5602
5631
|
}
|
|
5603
5632
|
}
|
|
5604
5633
|
return {
|
|
5605
|
-
|
|
5606
|
-
orderedTimestamps: [...
|
|
5634
|
+
symbolRuntimes,
|
|
5635
|
+
orderedTimestamps: [...orderedTimestamps].sort(
|
|
5607
5636
|
(left, right) => left - right
|
|
5608
5637
|
),
|
|
5609
5638
|
sharedReplayKeyPrefixes,
|
|
@@ -5636,7 +5665,13 @@ var appendReplaySignalEvaluationRun = ({
|
|
|
5636
5665
|
}
|
|
5637
5666
|
];
|
|
5638
5667
|
};
|
|
5639
|
-
var createHistoricalReplaySkipEvidence = (runtimeLineages) => {
|
|
5668
|
+
var createHistoricalReplaySkipEvidence = (runtimeLineages, enabled = true) => {
|
|
5669
|
+
if (!enabled) {
|
|
5670
|
+
return {
|
|
5671
|
+
record: () => void 0,
|
|
5672
|
+
values: () => []
|
|
5673
|
+
};
|
|
5674
|
+
}
|
|
5640
5675
|
const lineageByStrategySymbol = new Map(
|
|
5641
5676
|
runtimeLineages.map((record2) => [
|
|
5642
5677
|
replayScopeKey(record2.strategy, record2.symbol),
|
|
@@ -5685,7 +5720,9 @@ var createHistoricalReplaySkipEvidence = (runtimeLineages) => {
|
|
|
5685
5720
|
|
|
5686
5721
|
// src/lib/replay/historicalSignalsReplayExecution.ts
|
|
5687
5722
|
var advanceCycleMarket = async (plan, connector, timestamp) => {
|
|
5688
|
-
const cycleSymbols = plan.
|
|
5723
|
+
const cycleSymbols = plan.symbolRuntimes.filter(
|
|
5724
|
+
(symbolRuntime) => symbolRuntime.replayData[symbolRuntime.currentIndex]?.timestamp === timestamp
|
|
5725
|
+
);
|
|
5689
5726
|
for (const symbolRuntime of cycleSymbols) {
|
|
5690
5727
|
const candle = symbolRuntime.replayData[symbolRuntime.currentIndex];
|
|
5691
5728
|
if (candle?.timestamp === timestamp) {
|
|
@@ -5709,8 +5746,17 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5709
5746
|
const signals = [];
|
|
5710
5747
|
let abortedCycles = 0;
|
|
5711
5748
|
const skipEvidence = createHistoricalReplaySkipEvidence(
|
|
5712
|
-
context.plan.runtimeLineages
|
|
5749
|
+
context.plan.runtimeLineages,
|
|
5750
|
+
context.collectSkipEvidence
|
|
5713
5751
|
);
|
|
5752
|
+
const reportProgress = (timestamp) => {
|
|
5753
|
+
if (adapters.progress.enabled === false) return;
|
|
5754
|
+
adapters.progress.tick({
|
|
5755
|
+
signals: adapters.display.signals(signals.length),
|
|
5756
|
+
aborted: adapters.display.aborted(abortedCycles),
|
|
5757
|
+
ts: adapters.display.timestamp(timestamp)
|
|
5758
|
+
});
|
|
5759
|
+
};
|
|
5714
5760
|
try {
|
|
5715
5761
|
for (const timestamp of context.plan.orderedTimestamps) {
|
|
5716
5762
|
const cycleStartedAt = adapters.clock.now();
|
|
@@ -5732,11 +5778,7 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5732
5778
|
durationMs: adapters.clock.now() - cycleStartedAt
|
|
5733
5779
|
});
|
|
5734
5780
|
advanceAbortedCycle(cycleSymbols, timestamp);
|
|
5735
|
-
|
|
5736
|
-
signals: adapters.display.signals(signals.length),
|
|
5737
|
-
aborted: adapters.display.aborted(abortedCycles),
|
|
5738
|
-
ts: adapters.display.timestamp(timestamp)
|
|
5739
|
-
});
|
|
5781
|
+
reportProgress(timestamp);
|
|
5740
5782
|
continue;
|
|
5741
5783
|
}
|
|
5742
5784
|
const cycleSignals = [];
|
|
@@ -5765,7 +5807,7 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5765
5807
|
result.runtimeLineage = strategyRuntime.runtimeLineage;
|
|
5766
5808
|
await adapters.enrichSignal(result);
|
|
5767
5809
|
cycleSignals.push(result);
|
|
5768
|
-
signals.push(result);
|
|
5810
|
+
if (context.collectSignals !== false) signals.push(result);
|
|
5769
5811
|
}
|
|
5770
5812
|
}
|
|
5771
5813
|
symbolRuntime.currentIndex += 1;
|
|
@@ -5776,11 +5818,7 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5776
5818
|
status: "completed",
|
|
5777
5819
|
durationMs: adapters.clock.now() - cycleStartedAt
|
|
5778
5820
|
});
|
|
5779
|
-
|
|
5780
|
-
signals: adapters.display.signals(signals.length),
|
|
5781
|
-
aborted: adapters.display.aborted(abortedCycles),
|
|
5782
|
-
ts: adapters.display.timestamp(timestamp)
|
|
5783
|
-
});
|
|
5821
|
+
reportProgress(timestamp);
|
|
5784
5822
|
}
|
|
5785
5823
|
} finally {
|
|
5786
5824
|
for (const keyPrefix of context.plan.sharedReplayKeyPrefixes) {
|
|
@@ -5797,6 +5835,7 @@ var executeHistoricalReplay = async (context, adapters) => {
|
|
|
5797
5835
|
|
|
5798
5836
|
// src/lib/replay/historicalSignalsReplayResults.ts
|
|
5799
5837
|
var import_backtest = require("@tradejs/core/backtest");
|
|
5838
|
+
var import_constants4 = require("@tradejs/core/constants");
|
|
5800
5839
|
var emptyStat = () => ({
|
|
5801
5840
|
orders: 0,
|
|
5802
5841
|
wins: 0,
|
|
@@ -5804,6 +5843,25 @@ var emptyStat = () => ({
|
|
|
5804
5843
|
netProfit: 0,
|
|
5805
5844
|
amount: 0
|
|
5806
5845
|
});
|
|
5846
|
+
var positionLogForStats = (positionLog) => {
|
|
5847
|
+
let amount = import_constants4.INITIAL_BACKTEST_AMOUNT;
|
|
5848
|
+
return [...positionLog].sort(
|
|
5849
|
+
(left, right) => left.close.timestamp - right.close.timestamp || left.open.timestamp - right.open.timestamp
|
|
5850
|
+
).map((position) => {
|
|
5851
|
+
const pnl = Number.isFinite(position.netProfit) ? Number(position.netProfit) : position.close.amount - position.open.amount;
|
|
5852
|
+
const next = {
|
|
5853
|
+
...position,
|
|
5854
|
+
open: { ...position.open, amount },
|
|
5855
|
+
close: { ...position.close, amount: amount + pnl },
|
|
5856
|
+
netProfit: pnl
|
|
5857
|
+
};
|
|
5858
|
+
amount += pnl;
|
|
5859
|
+
return next;
|
|
5860
|
+
});
|
|
5861
|
+
};
|
|
5862
|
+
var calculateReplayStat = (positionLog) => positionLog.length ? (0, import_backtest.calculateStatsFull)(positionLogForStats(positionLog)) : emptyStat();
|
|
5863
|
+
var byTimestamp = (left, right) => left.timestamp - right.timestamp;
|
|
5864
|
+
var byPositionClose = (left, right) => left.close.timestamp - right.close.timestamp;
|
|
5807
5865
|
var collectHistoricalReplayResult = (context) => ({
|
|
5808
5866
|
strategies: context.strategies.map(({ strategyName, strategyConfig }) => {
|
|
5809
5867
|
const orderLog = context.artifacts.orderLogByStrategy.get(strategyName) ?? [];
|
|
@@ -5813,7 +5871,7 @@ var collectHistoricalReplayResult = (context) => ({
|
|
|
5813
5871
|
strategyConfig,
|
|
5814
5872
|
orderLog,
|
|
5815
5873
|
positionLog,
|
|
5816
|
-
stat:
|
|
5874
|
+
stat: calculateReplayStat(positionLog)
|
|
5817
5875
|
};
|
|
5818
5876
|
}),
|
|
5819
5877
|
signals: context.signals,
|
|
@@ -5824,8 +5882,78 @@ var collectHistoricalReplayResult = (context) => ({
|
|
|
5824
5882
|
runtimeLineages: context.runtimeLineages,
|
|
5825
5883
|
replayLineageScopes: context.replayLineageScopes
|
|
5826
5884
|
});
|
|
5885
|
+
var compactHistoricalReplayResultForPortfolio = (result) => ({
|
|
5886
|
+
...result,
|
|
5887
|
+
strategies: result.strategies.map((strategy) => ({
|
|
5888
|
+
...strategy,
|
|
5889
|
+
orderLog: []
|
|
5890
|
+
})),
|
|
5891
|
+
signals: [],
|
|
5892
|
+
orderLog: [],
|
|
5893
|
+
replayLineageScopes: []
|
|
5894
|
+
});
|
|
5895
|
+
var mergeHistoricalReplayResults = (parts) => {
|
|
5896
|
+
if (!parts.length) {
|
|
5897
|
+
throw new Error("Cannot merge an empty historical replay result list");
|
|
5898
|
+
}
|
|
5899
|
+
const strategyNames = parts[0].strategies.map(
|
|
5900
|
+
(strategy) => strategy.strategyName
|
|
5901
|
+
);
|
|
5902
|
+
const strategies = strategyNames.map((strategyName) => {
|
|
5903
|
+
const entries = parts.map((part) => {
|
|
5904
|
+
const entry = part.strategies.find(
|
|
5905
|
+
(strategy) => strategy.strategyName === strategyName
|
|
5906
|
+
);
|
|
5907
|
+
if (!entry) {
|
|
5908
|
+
throw new Error(`Replay batch is missing strategy ${strategyName}`);
|
|
5909
|
+
}
|
|
5910
|
+
return entry;
|
|
5911
|
+
});
|
|
5912
|
+
const positionLog = entries.flatMap((entry) => entry.positionLog).sort(byPositionClose);
|
|
5913
|
+
return {
|
|
5914
|
+
strategyName,
|
|
5915
|
+
strategyConfig: entries[0].strategyConfig,
|
|
5916
|
+
orderLog: entries.flatMap((entry) => entry.orderLog).sort(byTimestamp),
|
|
5917
|
+
positionLog,
|
|
5918
|
+
stat: calculateReplayStat(positionLog)
|
|
5919
|
+
};
|
|
5920
|
+
});
|
|
5921
|
+
return {
|
|
5922
|
+
strategies,
|
|
5923
|
+
signals: parts.flatMap((part) => part.signals).sort(byTimestamp),
|
|
5924
|
+
orderLog: parts.flatMap((part) => part.orderLog).sort(byTimestamp),
|
|
5925
|
+
positionLog: parts.flatMap((part) => part.positionLog).sort(byPositionClose),
|
|
5926
|
+
cycleCount: parts.reduce((sum, part) => sum + part.cycleCount, 0),
|
|
5927
|
+
abortedCycles: parts.reduce((sum, part) => sum + part.abortedCycles, 0),
|
|
5928
|
+
runtimeLineages: parts.flatMap((part) => part.runtimeLineages),
|
|
5929
|
+
replayLineageScopes: parts.flatMap((part) => part.replayLineageScopes)
|
|
5930
|
+
};
|
|
5931
|
+
};
|
|
5827
5932
|
|
|
5828
5933
|
// src/lib/replay/historicalSignalsReplay.ts
|
|
5934
|
+
var projectReplayCacheReleasers;
|
|
5935
|
+
var getProjectReplayCacheReleasers = () => {
|
|
5936
|
+
if (projectReplayCacheReleasers !== void 0) {
|
|
5937
|
+
return projectReplayCacheReleasers;
|
|
5938
|
+
}
|
|
5939
|
+
try {
|
|
5940
|
+
const requireFromProject = (0, import_node_module.createRequire)(
|
|
5941
|
+
import_node_path2.default.join(replayProjectRoot, "package.json")
|
|
5942
|
+
);
|
|
5943
|
+
projectReplayCacheReleasers = requireFromProject(
|
|
5944
|
+
"@tradejs/core/strategies"
|
|
5945
|
+
);
|
|
5946
|
+
} catch {
|
|
5947
|
+
projectReplayCacheReleasers = null;
|
|
5948
|
+
}
|
|
5949
|
+
return projectReplayCacheReleasers;
|
|
5950
|
+
};
|
|
5951
|
+
var releaseReplayCacheCopies = (keyPrefix, localRelease, projectRelease) => {
|
|
5952
|
+
localRelease(keyPrefix);
|
|
5953
|
+
if (projectRelease && projectRelease !== localRelease) {
|
|
5954
|
+
projectRelease(keyPrefix);
|
|
5955
|
+
}
|
|
5956
|
+
};
|
|
5829
5957
|
var loadRuntimeStrategies = async (runtimeStrategies) => {
|
|
5830
5958
|
const strategies = await Promise.all(
|
|
5831
5959
|
runtimeStrategies.map(async (runtimeStrategy) => {
|
|
@@ -5840,16 +5968,19 @@ var loadRuntimeStrategies = async (runtimeStrategies) => {
|
|
|
5840
5968
|
strategyConfig,
|
|
5841
5969
|
selection
|
|
5842
5970
|
} = runtimeStrategy;
|
|
5971
|
+
const strategyModule = strategyPackage.startsWith("@tradejs/strategy-") ? strategyPackage : void 0;
|
|
5843
5972
|
const strategyCreator = await (0, import_strategies4.getStrategyCreator)(
|
|
5844
5973
|
strategyName,
|
|
5845
|
-
replayProjectRoot
|
|
5974
|
+
replayProjectRoot,
|
|
5975
|
+
strategyModule
|
|
5846
5976
|
);
|
|
5847
5977
|
if (!strategyCreator) {
|
|
5848
5978
|
throw new Error(`Unknown strategy: ${strategyName}`);
|
|
5849
5979
|
}
|
|
5850
5980
|
const installed = await (0, import_runtimeStrategies3.getRuntimeStrategyPackageMetadata)({
|
|
5851
5981
|
strategyName,
|
|
5852
|
-
projectRoot: replayProjectRoot
|
|
5982
|
+
projectRoot: replayProjectRoot,
|
|
5983
|
+
strategyModule
|
|
5853
5984
|
});
|
|
5854
5985
|
if (installed.strategyPackage !== strategyPackage || installed.strategyPackageVersion !== strategyPackageVersion || JSON.stringify(installed.strategyDependencyVersions) !== JSON.stringify(strategyDependencyVersions) || installed.runtimePackageVersion !== runtimePackageVersion) {
|
|
5855
5986
|
throw new Error(
|
|
@@ -5912,10 +6043,41 @@ var getConnectorName = (connector) => {
|
|
|
5912
6043
|
const name = "name" in connector ? connector.name : void 0;
|
|
5913
6044
|
return String(name || import_connectors6.DEFAULT_CONNECTOR_NAME).trim() || import_connectors6.DEFAULT_CONNECTOR_NAME;
|
|
5914
6045
|
};
|
|
6046
|
+
var loadHistoricalReplayReferences = async ({
|
|
6047
|
+
preparedRun,
|
|
6048
|
+
interval
|
|
6049
|
+
}) => {
|
|
6050
|
+
const connectorName = getConnectorName(preparedRun.marketConnector);
|
|
6051
|
+
const binanceConnector = connectorName.toLowerCase() === import_connectors6.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Binance") : preparedRun.marketConnector;
|
|
6052
|
+
const coinbaseConnector = connectorName.toLowerCase() === import_connectors6.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Coinbase") : preparedRun.marketConnector;
|
|
6053
|
+
const request = {
|
|
6054
|
+
symbol: "BTCUSDT",
|
|
6055
|
+
start: preparedRun.preloadStart,
|
|
6056
|
+
end: preparedRun.window.end,
|
|
6057
|
+
cacheOnly: true,
|
|
6058
|
+
interval
|
|
6059
|
+
};
|
|
6060
|
+
const [btcBinanceData, btcCoinbaseData, btcMarketData, ethMarketData] = await Promise.all([
|
|
6061
|
+
binanceConnector.kline(request),
|
|
6062
|
+
coinbaseConnector.kline(request),
|
|
6063
|
+
preparedRun.marketConnector.kline(request),
|
|
6064
|
+
preparedRun.marketConnector.kline({ ...request, symbol: "ETHUSDT" })
|
|
6065
|
+
]);
|
|
6066
|
+
return {
|
|
6067
|
+
btcMarketData,
|
|
6068
|
+
ethMarketData,
|
|
6069
|
+
btcBinanceData,
|
|
6070
|
+
btcCoinbaseData
|
|
6071
|
+
};
|
|
6072
|
+
};
|
|
5915
6073
|
var runHistoricalSignalsReplay = async ({
|
|
5916
6074
|
preparedRun,
|
|
5917
6075
|
interval,
|
|
5918
|
-
runtimeStrategies
|
|
6076
|
+
runtimeStrategies,
|
|
6077
|
+
references,
|
|
6078
|
+
showProgress = true,
|
|
6079
|
+
collectSkipEvidence = true,
|
|
6080
|
+
collectSignals = true
|
|
5919
6081
|
}) => {
|
|
5920
6082
|
const startedAt = Date.now();
|
|
5921
6083
|
const projectConfig = await (0, import_cli4.loadTradejsConfig)(replayProjectRoot);
|
|
@@ -5925,38 +6087,7 @@ var runHistoricalSignalsReplay = async ({
|
|
|
5925
6087
|
preparedRun.marketConnector
|
|
5926
6088
|
);
|
|
5927
6089
|
const connectorName = getConnectorName(preparedRun.marketConnector);
|
|
5928
|
-
const
|
|
5929
|
-
const coinbaseConnector = connectorName.toLowerCase() === import_connectors6.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Coinbase") : preparedRun.marketConnector;
|
|
5930
|
-
const [btcBinanceData, btcCoinbaseData] = await Promise.all([
|
|
5931
|
-
binanceConnector.kline({
|
|
5932
|
-
symbol: "BTCUSDT",
|
|
5933
|
-
start: preparedRun.preloadStart,
|
|
5934
|
-
end: preparedRun.window.end,
|
|
5935
|
-
cacheOnly: true,
|
|
5936
|
-
interval
|
|
5937
|
-
}),
|
|
5938
|
-
coinbaseConnector.kline({
|
|
5939
|
-
symbol: "BTCUSDT",
|
|
5940
|
-
start: preparedRun.preloadStart,
|
|
5941
|
-
end: preparedRun.window.end,
|
|
5942
|
-
cacheOnly: true,
|
|
5943
|
-
interval
|
|
5944
|
-
})
|
|
5945
|
-
]);
|
|
5946
|
-
const btcMarketData = await preparedRun.marketConnector.kline({
|
|
5947
|
-
symbol: "BTCUSDT",
|
|
5948
|
-
start: preparedRun.preloadStart,
|
|
5949
|
-
end: preparedRun.window.end,
|
|
5950
|
-
cacheOnly: true,
|
|
5951
|
-
interval
|
|
5952
|
-
});
|
|
5953
|
-
const ethMarketData = await preparedRun.marketConnector.kline({
|
|
5954
|
-
symbol: "ETHUSDT",
|
|
5955
|
-
start: preparedRun.preloadStart,
|
|
5956
|
-
end: preparedRun.window.end,
|
|
5957
|
-
cacheOnly: true,
|
|
5958
|
-
interval
|
|
5959
|
-
});
|
|
6090
|
+
const referenceData = references ?? await loadHistoricalReplayReferences({ preparedRun, interval });
|
|
5960
6091
|
const prepareBar = new import_progress4.default(
|
|
5961
6092
|
"prepare :current/:total [:bar][:percent] skipped=:skipped :etas(s) :symbol",
|
|
5962
6093
|
{
|
|
@@ -5974,10 +6105,10 @@ var runHistoricalSignalsReplay = async ({
|
|
|
5974
6105
|
replayConnector,
|
|
5975
6106
|
strategies: loadedStrategies,
|
|
5976
6107
|
references: {
|
|
5977
|
-
btcMarketData,
|
|
5978
|
-
ethMarketData,
|
|
5979
|
-
btcBinanceData,
|
|
5980
|
-
btcCoinbaseData
|
|
6108
|
+
btcMarketData: referenceData.btcMarketData,
|
|
6109
|
+
ethMarketData: referenceData.ethMarketData,
|
|
6110
|
+
btcBinanceData: referenceData.btcBinanceData,
|
|
6111
|
+
btcCoinbaseData: referenceData.btcCoinbaseData
|
|
5981
6112
|
}
|
|
5982
6113
|
},
|
|
5983
6114
|
{
|
|
@@ -6009,12 +6140,15 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6009
6140
|
{
|
|
6010
6141
|
plan,
|
|
6011
6142
|
connector: replayConnector,
|
|
6143
|
+
collectSkipEvidence,
|
|
6144
|
+
collectSignals,
|
|
6012
6145
|
hooks: projectHooks,
|
|
6013
6146
|
hookContext
|
|
6014
6147
|
},
|
|
6015
6148
|
{
|
|
6016
6149
|
clock: { now: Date.now },
|
|
6017
6150
|
progress: {
|
|
6151
|
+
enabled: showProgress,
|
|
6018
6152
|
tick: (tokens) => cycleBar.tick(1, tokens)
|
|
6019
6153
|
},
|
|
6020
6154
|
display: {
|
|
@@ -6025,8 +6159,16 @@ var runHistoricalSignalsReplay = async ({
|
|
|
6025
6159
|
invokeBeforeSignals: invokeBeforeSignalsHooks,
|
|
6026
6160
|
invokeAfterSignals: invokeAfterSignalsHooks,
|
|
6027
6161
|
enrichSignal: (signal) => (0, import_strategies4.enrichSignalWithBinanceMarketContext)({ signal, env: "PARITY" }),
|
|
6028
|
-
releaseIndicatorsCache:
|
|
6029
|
-
|
|
6162
|
+
releaseIndicatorsCache: (keyPrefix) => releaseReplayCacheCopies(
|
|
6163
|
+
keyPrefix,
|
|
6164
|
+
import_strategies3.releaseStrategyIndicatorsReplayCache,
|
|
6165
|
+
getProjectReplayCacheReleasers()?.releaseStrategyIndicatorsReplayCache
|
|
6166
|
+
),
|
|
6167
|
+
releaseReplayCache: (keyPrefix) => releaseReplayCacheCopies(
|
|
6168
|
+
keyPrefix,
|
|
6169
|
+
import_strategies3.releaseStrategyReplayCache,
|
|
6170
|
+
getProjectReplayCacheReleasers()?.releaseStrategyReplayCache
|
|
6171
|
+
)
|
|
6030
6172
|
}
|
|
6031
6173
|
);
|
|
6032
6174
|
const artifacts = replayConnector.getReplayArtifacts();
|
|
@@ -6397,11 +6539,16 @@ var saveAndPrintReplayResultsByStrategy = async ({
|
|
|
6397
6539
|
const backtestEntries = replayResult.strategies.flatMap(
|
|
6398
6540
|
({ orderLog }) => extractBacktestEntryParityEntries(orderLog)
|
|
6399
6541
|
);
|
|
6400
|
-
const summaries = replayResult.strategies.map(({ strategyName, strategyConfig,
|
|
6401
|
-
const
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
const
|
|
6542
|
+
const summaries = replayResult.strategies.map(({ strategyName, strategyConfig, positionLog }) => {
|
|
6543
|
+
const realizedPnl = positionLog.map(
|
|
6544
|
+
(position) => Number.isFinite(position.netProfit) ? Number(position.netProfit) : position.close.amount - position.open.amount
|
|
6545
|
+
);
|
|
6546
|
+
const orders = realizedPnl.length;
|
|
6547
|
+
const wins = realizedPnl.filter((pnl) => pnl > 0).length;
|
|
6548
|
+
const losses = realizedPnl.filter((pnl) => pnl <= 0).length;
|
|
6549
|
+
const netProfit = Number(
|
|
6550
|
+
realizedPnl.reduce((sum, pnl) => sum + pnl, 0).toFixed(2)
|
|
6551
|
+
);
|
|
6405
6552
|
const winRate = orders > 0 ? Number((wins / orders * 100).toFixed(2)) : 0;
|
|
6406
6553
|
const avgTradeProfit = orders > 0 ? Number((netProfit / orders).toFixed(2)) : 0;
|
|
6407
6554
|
const tradedSymbols = new Set(
|
|
@@ -6528,7 +6675,7 @@ var summarizeTradeParityByStrategy = ({
|
|
|
6528
6675
|
var import_redis4 = require("@tradejs/infra/redis");
|
|
6529
6676
|
|
|
6530
6677
|
// src/lib/runtimeSignalsStorage.ts
|
|
6531
|
-
var
|
|
6678
|
+
var import_constants5 = require("@tradejs/core/constants");
|
|
6532
6679
|
var import_data2 = require("@tradejs/core/data");
|
|
6533
6680
|
var import_time4 = require("@tradejs/core/time");
|
|
6534
6681
|
var isRuntimeSignalBucketRef = (value) => {
|
|
@@ -6638,7 +6785,7 @@ var loadRuntimeLineageScopes = async (userName, {
|
|
|
6638
6785
|
};
|
|
6639
6786
|
|
|
6640
6787
|
// src/lib/runtimeTradeSync.ts
|
|
6641
|
-
var
|
|
6788
|
+
var import_constants6 = require("@tradejs/core/constants");
|
|
6642
6789
|
var import_time5 = require("@tradejs/core/time");
|
|
6643
6790
|
var import_redis5 = require("@tradejs/infra/redis");
|
|
6644
6791
|
var CLOSED_PNL_LIMIT = 100;
|
|
@@ -6879,7 +7026,7 @@ var syncRuntimeTrades = async ({
|
|
|
6879
7026
|
};
|
|
6880
7027
|
await Promise.all([
|
|
6881
7028
|
(0, import_redis5.setData)(import_redis5.redisKeys.runtimeTrade(userName, trade.orderId), nextTrade, {
|
|
6882
|
-
expire:
|
|
7029
|
+
expire: import_constants6.TTL_1M
|
|
6883
7030
|
}),
|
|
6884
7031
|
(0, import_redis5.setHashJsonField)(
|
|
6885
7032
|
import_redis5.redisKeys.runtimeTradeBucket(
|
|
@@ -6888,7 +7035,7 @@ var syncRuntimeTrades = async ({
|
|
|
6888
7035
|
),
|
|
6889
7036
|
trade.orderId,
|
|
6890
7037
|
nextTrade,
|
|
6891
|
-
{ expire:
|
|
7038
|
+
{ expire: import_constants6.TTL_1M }
|
|
6892
7039
|
),
|
|
6893
7040
|
(0, import_redis5.setHashJsonField)(
|
|
6894
7041
|
import_redis5.redisKeys.runtimeClosedTradeBucket(
|
|
@@ -6897,7 +7044,7 @@ var syncRuntimeTrades = async ({
|
|
|
6897
7044
|
),
|
|
6898
7045
|
trade.orderId,
|
|
6899
7046
|
nextTrade,
|
|
6900
|
-
{ expire:
|
|
7047
|
+
{ expire: import_constants6.TTL_1M }
|
|
6901
7048
|
),
|
|
6902
7049
|
...isCurrentActiveTrade ? [(0, import_redis5.delKey)(activeTradeKey)] : []
|
|
6903
7050
|
]);
|
|
@@ -8838,7 +8985,7 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8838
8985
|
|
|
8839
8986
|
// src/lib/replay/outputReport.ts
|
|
8840
8987
|
var import_promises2 = __toESM(require("fs/promises"));
|
|
8841
|
-
var
|
|
8988
|
+
var import_node_path3 = __toESM(require("path"));
|
|
8842
8989
|
var import_data3 = require("@tradejs/core/data");
|
|
8843
8990
|
var formatMsk = (timestamp) => typeof timestamp === "number" && Number.isFinite(timestamp) ? new Intl.DateTimeFormat("ru-RU", {
|
|
8844
8991
|
timeZone: "Europe/Moscow",
|
|
@@ -9142,7 +9289,7 @@ var writeReplayOutputReport = async ({
|
|
|
9142
9289
|
strategySnapshot,
|
|
9143
9290
|
runtimeComparison
|
|
9144
9291
|
}) => {
|
|
9145
|
-
const outputDir =
|
|
9292
|
+
const outputDir = import_node_path3.default.resolve(projectRoot, "data/replay/output");
|
|
9146
9293
|
await import_promises2.default.mkdir(outputDir, { recursive: true });
|
|
9147
9294
|
const perTradeAnalysis = buildPerTradeAnalysis(runtimeComparison);
|
|
9148
9295
|
const report = {
|
|
@@ -9178,8 +9325,8 @@ var writeReplayOutputReport = async ({
|
|
|
9178
9325
|
runtimeComparison,
|
|
9179
9326
|
perTradeAnalysis
|
|
9180
9327
|
};
|
|
9181
|
-
const markdownPath =
|
|
9182
|
-
const jsonPath =
|
|
9328
|
+
const markdownPath = import_node_path3.default.join(outputDir, `${timestamp}-replay-report.md`);
|
|
9329
|
+
const jsonPath = import_node_path3.default.join(outputDir, `${timestamp}-replay-report.json`);
|
|
9183
9330
|
const lines = [
|
|
9184
9331
|
"# Replay Report",
|
|
9185
9332
|
"",
|
|
@@ -9255,6 +9402,330 @@ var writeReplayOutputReport = async ({
|
|
|
9255
9402
|
};
|
|
9256
9403
|
};
|
|
9257
9404
|
|
|
9405
|
+
// src/lib/replay/portfolioReport.ts
|
|
9406
|
+
var import_promises3 = __toESM(require("fs/promises"));
|
|
9407
|
+
var import_node_path4 = __toESM(require("path"));
|
|
9408
|
+
var import_node_crypto2 = require("crypto");
|
|
9409
|
+
var DAY_MS4 = 864e5;
|
|
9410
|
+
var WINDOWS = [365, 180, 90, 30, 7];
|
|
9411
|
+
var COLORS = [
|
|
9412
|
+
"#2563eb",
|
|
9413
|
+
"#dc2626",
|
|
9414
|
+
"#059669",
|
|
9415
|
+
"#7c3aed",
|
|
9416
|
+
"#ea580c",
|
|
9417
|
+
"#0891b2",
|
|
9418
|
+
"#c026d3",
|
|
9419
|
+
"#4d7c0f",
|
|
9420
|
+
"#be123c",
|
|
9421
|
+
"#4338ca",
|
|
9422
|
+
"#0f766e"
|
|
9423
|
+
];
|
|
9424
|
+
var finite = (value, fallback = 0) => {
|
|
9425
|
+
const number = Number(value);
|
|
9426
|
+
return Number.isFinite(number) ? number : fallback;
|
|
9427
|
+
};
|
|
9428
|
+
var round2 = (value, digits = 8) => Number(value.toFixed(digits));
|
|
9429
|
+
var tradesFromPositionLog = (strategyName, positionLog) => positionLog.map((position) => ({
|
|
9430
|
+
strategyName,
|
|
9431
|
+
direction: position.direction,
|
|
9432
|
+
openedAt: position.open.timestamp,
|
|
9433
|
+
exitedAt: position.close.timestamp,
|
|
9434
|
+
pnl: finite(
|
|
9435
|
+
position.netProfit,
|
|
9436
|
+
finite(position.close.amount) - finite(position.open.amount)
|
|
9437
|
+
)
|
|
9438
|
+
}));
|
|
9439
|
+
var curveForTrades = (trades, start, end) => {
|
|
9440
|
+
let value = 0;
|
|
9441
|
+
const realized = [...trades].sort(
|
|
9442
|
+
(left, right) => left.exitedAt - right.exitedAt || left.strategyName.localeCompare(right.strategyName)
|
|
9443
|
+
).map((trade) => ({
|
|
9444
|
+
timestamp: trade.exitedAt,
|
|
9445
|
+
value: value = round2(value + trade.pnl)
|
|
9446
|
+
}));
|
|
9447
|
+
return [
|
|
9448
|
+
{ timestamp: start, value: 0 },
|
|
9449
|
+
...realized,
|
|
9450
|
+
{ timestamp: end, value }
|
|
9451
|
+
];
|
|
9452
|
+
};
|
|
9453
|
+
var drawdownCurve = (equity) => {
|
|
9454
|
+
let peak = 0;
|
|
9455
|
+
return equity.map((point) => {
|
|
9456
|
+
peak = Math.max(peak, point.value);
|
|
9457
|
+
return { timestamp: point.timestamp, value: round2(point.value - peak) };
|
|
9458
|
+
});
|
|
9459
|
+
};
|
|
9460
|
+
var maxDrawdown = (trades) => {
|
|
9461
|
+
let equity = 0;
|
|
9462
|
+
let peak = 0;
|
|
9463
|
+
let worst = 0;
|
|
9464
|
+
for (const trade of [...trades].sort((a, b) => a.exitedAt - b.exitedAt)) {
|
|
9465
|
+
equity += trade.pnl;
|
|
9466
|
+
peak = Math.max(peak, equity);
|
|
9467
|
+
worst = Math.max(worst, peak - equity);
|
|
9468
|
+
}
|
|
9469
|
+
return worst;
|
|
9470
|
+
};
|
|
9471
|
+
var calculatePortfolioMetricRow = ({
|
|
9472
|
+
trades,
|
|
9473
|
+
window,
|
|
9474
|
+
scope,
|
|
9475
|
+
cohort,
|
|
9476
|
+
start,
|
|
9477
|
+
end
|
|
9478
|
+
}) => {
|
|
9479
|
+
const selected = trades.filter(
|
|
9480
|
+
(trade) => trade.exitedAt >= start && trade.exitedAt < end && (cohort === "ALL" || trade.direction === cohort)
|
|
9481
|
+
);
|
|
9482
|
+
const grossProfit = selected.filter((trade) => trade.pnl > 0).reduce((sum, trade) => sum + trade.pnl, 0);
|
|
9483
|
+
const grossLoss = Math.abs(
|
|
9484
|
+
selected.filter((trade) => trade.pnl < 0).reduce((sum, trade) => sum + trade.pnl, 0)
|
|
9485
|
+
);
|
|
9486
|
+
const pnl = selected.reduce((sum, trade) => sum + trade.pnl, 0);
|
|
9487
|
+
const wins = selected.filter((trade) => trade.pnl > 0).length;
|
|
9488
|
+
const losses = selected.filter((trade) => trade.pnl <= 0).length;
|
|
9489
|
+
const days = Math.max(1, (end - start) / DAY_MS4);
|
|
9490
|
+
return {
|
|
9491
|
+
window,
|
|
9492
|
+
scope,
|
|
9493
|
+
cohort,
|
|
9494
|
+
n: selected.length,
|
|
9495
|
+
wins,
|
|
9496
|
+
losses,
|
|
9497
|
+
pnl: round2(pnl),
|
|
9498
|
+
pnlPerTrade: selected.length ? round2(pnl / selected.length) : null,
|
|
9499
|
+
profitFactor: grossLoss > 0 ? round2(grossProfit / grossLoss) : null,
|
|
9500
|
+
winRate: selected.length ? round2(wins / selected.length * 100) : null,
|
|
9501
|
+
realizedMaxDrawdown: round2(maxDrawdown(selected)),
|
|
9502
|
+
cadencePerDay: round2(selected.length / days)
|
|
9503
|
+
};
|
|
9504
|
+
};
|
|
9505
|
+
var windowBounds = (start, end) => [
|
|
9506
|
+
{ label: "full", start, end },
|
|
9507
|
+
...WINDOWS.map((days) => ({
|
|
9508
|
+
label: `${days}d`,
|
|
9509
|
+
start: Math.max(start, end - days * DAY_MS4),
|
|
9510
|
+
end
|
|
9511
|
+
}))
|
|
9512
|
+
];
|
|
9513
|
+
var escapeHtml = (value) => String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
9514
|
+
var display = (value, digits = 2) => value == null || !Number.isFinite(value) ? "n/a" : value.toFixed(digits);
|
|
9515
|
+
var displayPercent = (value) => value == null || !Number.isFinite(value) ? "n/a" : `${value.toFixed(1)}%`;
|
|
9516
|
+
var downsample = (points, limit = 2e3) => {
|
|
9517
|
+
if (points.length <= limit) return points;
|
|
9518
|
+
const result = [];
|
|
9519
|
+
const step = (points.length - 1) / (limit - 1);
|
|
9520
|
+
for (let index = 0; index < limit; index += 1) {
|
|
9521
|
+
result.push(points[Math.round(index * step)]);
|
|
9522
|
+
}
|
|
9523
|
+
return result;
|
|
9524
|
+
};
|
|
9525
|
+
var renderSvg = ({
|
|
9526
|
+
title,
|
|
9527
|
+
series,
|
|
9528
|
+
start,
|
|
9529
|
+
end
|
|
9530
|
+
}) => {
|
|
9531
|
+
const width = 1280;
|
|
9532
|
+
const legendColumns = 3;
|
|
9533
|
+
const legendRows = Math.max(1, Math.ceil(series.length / legendColumns));
|
|
9534
|
+
const height = 640 + legendRows * 20;
|
|
9535
|
+
const left = 78;
|
|
9536
|
+
const right = 24;
|
|
9537
|
+
const top = 58;
|
|
9538
|
+
const bottom = 90 + legendRows * 20;
|
|
9539
|
+
const allValues = series.flatMap(
|
|
9540
|
+
({ points }) => points.map(({ value }) => value)
|
|
9541
|
+
);
|
|
9542
|
+
const min = Math.min(0, ...allValues);
|
|
9543
|
+
const max = Math.max(0, ...allValues);
|
|
9544
|
+
const span = Math.max(1, max - min);
|
|
9545
|
+
const timeSpan = Math.max(1, end - start);
|
|
9546
|
+
const x = (timestamp) => left + (timestamp - start) / timeSpan * (width - left - right);
|
|
9547
|
+
const y = (value) => top + (max - value) / span * (height - top - bottom);
|
|
9548
|
+
const grid = Array.from({ length: 6 }, (_, index) => {
|
|
9549
|
+
const value = max - span * index / 5;
|
|
9550
|
+
const py = y(value);
|
|
9551
|
+
return `<line x1="${left}" y1="${py}" x2="${width - right}" y2="${py}" stroke="#e5e7eb"/><text x="${left - 10}" y="${py + 4}" text-anchor="end" font-size="12" fill="#64748b">${value.toFixed(1)}</text>`;
|
|
9552
|
+
}).join("");
|
|
9553
|
+
const paths = series.map(({ name, color, points, strokeWidth = 2 }) => {
|
|
9554
|
+
const sampled = downsample(points);
|
|
9555
|
+
const d = sampled.map(
|
|
9556
|
+
(point, index) => `${index ? "L" : "M"}${x(point.timestamp).toFixed(2)},${y(point.value).toFixed(2)}`
|
|
9557
|
+
).join(" ");
|
|
9558
|
+
return `<path d="${d}" fill="none" stroke="${color}" stroke-width="${strokeWidth}"/><title>${escapeHtml(name)}</title>`;
|
|
9559
|
+
}).join("");
|
|
9560
|
+
const legend = series.map(
|
|
9561
|
+
({ name, color }, index) => `<g transform="translate(${left + index % legendColumns * 390},${height - 20 - (legendRows - 1 - Math.floor(index / legendColumns)) * 20})"><line x1="0" y1="-4" x2="20" y2="-4" stroke="${color}" stroke-width="3"/><text x="26" y="0" font-size="12" fill="#334155">${escapeHtml(name)}</text></g>`
|
|
9562
|
+
).join("");
|
|
9563
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="white"/><text x="${left}" y="32" font-size="22" font-family="sans-serif" fill="#0f172a">${escapeHtml(title)}</text><g font-family="sans-serif">${grid}<line x1="${left}" y1="${y(0)}" x2="${width - right}" y2="${y(0)}" stroke="#94a3b8"/>${paths}<text x="${left}" y="${height - bottom + 26}" font-size="12" fill="#64748b">${new Date(start).toISOString().slice(0, 10)}</text><text x="${width - right}" y="${height - bottom + 26}" text-anchor="end" font-size="12" fill="#64748b">${new Date(end).toISOString().slice(0, 10)}</text>${legend}</g></svg>`;
|
|
9564
|
+
};
|
|
9565
|
+
var metricTable = (rows) => {
|
|
9566
|
+
const body = rows.map(
|
|
9567
|
+
(row) => `<tr><td>${escapeHtml(row.window)}</td><td>${escapeHtml(row.scope)}</td><td>${row.cohort}</td><td>${row.n}</td><td>${display(row.pnl)}</td><td>${display(row.pnlPerTrade)}</td><td>${display(row.profitFactor)}</td><td>${displayPercent(row.winRate)}</td><td>${display(row.realizedMaxDrawdown)}</td><td>${display(row.cadencePerDay, 3)}</td></tr>`
|
|
9568
|
+
).join("");
|
|
9569
|
+
return `<table><thead><tr><th>Window</th><th>Scope</th><th>Cohort</th><th>N</th><th>PnL</th><th>PnL/trade</th><th>PF</th><th>WR</th><th>Realized MaxDD</th><th>Cadence/day</th></tr></thead><tbody>${body}</tbody></table>`;
|
|
9570
|
+
};
|
|
9571
|
+
var markdownTable = (rows) => [
|
|
9572
|
+
"| Window | Scope | Cohort | N | PnL | PnL/trade | PF | WR | Realized MaxDD | Cadence/day |",
|
|
9573
|
+
"| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
|
9574
|
+
...rows.map(
|
|
9575
|
+
(row) => `| ${row.window} | ${row.scope} | ${row.cohort} | ${row.n} | ${display(row.pnl)} | ${display(row.pnlPerTrade)} | ${display(row.profitFactor)} | ${displayPercent(row.winRate)} | ${display(row.realizedMaxDrawdown)} | ${display(row.cadencePerDay, 3)} |`
|
|
9576
|
+
)
|
|
9577
|
+
].join("\n");
|
|
9578
|
+
var writePortfolioReport = async ({
|
|
9579
|
+
projectRoot,
|
|
9580
|
+
timestamp,
|
|
9581
|
+
replayResult,
|
|
9582
|
+
window,
|
|
9583
|
+
lineage,
|
|
9584
|
+
command
|
|
9585
|
+
}) => {
|
|
9586
|
+
const outputDir = import_node_path4.default.resolve(projectRoot, "data/replay/output");
|
|
9587
|
+
await import_promises3.default.mkdir(outputDir, { recursive: true });
|
|
9588
|
+
const trades = replayResult.strategies.flatMap(
|
|
9589
|
+
(strategy) => tradesFromPositionLog(strategy.strategyName, strategy.positionLog)
|
|
9590
|
+
);
|
|
9591
|
+
const bounds = windowBounds(window.start, window.end);
|
|
9592
|
+
const portfolioRows = bounds.flatMap(
|
|
9593
|
+
(bound) => ["ALL", "LONG", "SHORT"].map(
|
|
9594
|
+
(cohort) => calculatePortfolioMetricRow({
|
|
9595
|
+
trades,
|
|
9596
|
+
window: bound.label,
|
|
9597
|
+
scope: "PORTFOLIO",
|
|
9598
|
+
cohort,
|
|
9599
|
+
start: bound.start,
|
|
9600
|
+
end: bound.end
|
|
9601
|
+
})
|
|
9602
|
+
)
|
|
9603
|
+
);
|
|
9604
|
+
const strategyRows = bounds.flatMap(
|
|
9605
|
+
(bound) => replayResult.strategies.map(
|
|
9606
|
+
(strategy) => calculatePortfolioMetricRow({
|
|
9607
|
+
trades: trades.filter(
|
|
9608
|
+
(trade) => trade.strategyName === strategy.strategyName
|
|
9609
|
+
),
|
|
9610
|
+
window: bound.label,
|
|
9611
|
+
scope: strategy.strategyName,
|
|
9612
|
+
cohort: "ALL",
|
|
9613
|
+
start: bound.start,
|
|
9614
|
+
end: bound.end
|
|
9615
|
+
})
|
|
9616
|
+
)
|
|
9617
|
+
);
|
|
9618
|
+
const portfolioEquity = curveForTrades(trades, window.start, window.end);
|
|
9619
|
+
const strategySeries = replayResult.strategies.flatMap((strategy, index) => {
|
|
9620
|
+
const strategyTrades = trades.filter(
|
|
9621
|
+
(trade) => trade.strategyName === strategy.strategyName
|
|
9622
|
+
);
|
|
9623
|
+
return strategyTrades.length ? [
|
|
9624
|
+
{
|
|
9625
|
+
name: strategy.strategyName,
|
|
9626
|
+
color: COLORS[index % COLORS.length],
|
|
9627
|
+
points: curveForTrades(strategyTrades, window.start, window.end)
|
|
9628
|
+
}
|
|
9629
|
+
] : [];
|
|
9630
|
+
});
|
|
9631
|
+
const equitySvg = renderSvg({
|
|
9632
|
+
title: "Portfolio and strategy realized PnL overlay",
|
|
9633
|
+
series: [
|
|
9634
|
+
{
|
|
9635
|
+
name: "PORTFOLIO",
|
|
9636
|
+
color: "#111827",
|
|
9637
|
+
points: portfolioEquity,
|
|
9638
|
+
strokeWidth: 3
|
|
9639
|
+
},
|
|
9640
|
+
...strategySeries
|
|
9641
|
+
],
|
|
9642
|
+
start: window.start,
|
|
9643
|
+
end: window.end
|
|
9644
|
+
});
|
|
9645
|
+
const drawdownSvg = renderSvg({
|
|
9646
|
+
title: "Aggregate portfolio realized drawdown",
|
|
9647
|
+
series: [
|
|
9648
|
+
{
|
|
9649
|
+
name: "PORTFOLIO",
|
|
9650
|
+
color: "#111827",
|
|
9651
|
+
points: drawdownCurve(portfolioEquity)
|
|
9652
|
+
}
|
|
9653
|
+
],
|
|
9654
|
+
start: window.start,
|
|
9655
|
+
end: window.end
|
|
9656
|
+
});
|
|
9657
|
+
const prefix = `${timestamp}-portfolio-backtest`;
|
|
9658
|
+
const paths = {
|
|
9659
|
+
markdown: import_node_path4.default.join(outputDir, `${prefix}.md`),
|
|
9660
|
+
json: import_node_path4.default.join(outputDir, `${prefix}.json`),
|
|
9661
|
+
equitySvg: import_node_path4.default.join(outputDir, `${prefix}-equity.svg`),
|
|
9662
|
+
drawdownSvg: import_node_path4.default.join(outputDir, `${prefix}-drawdown.svg`),
|
|
9663
|
+
html: import_node_path4.default.join(outputDir, `${prefix}.html`)
|
|
9664
|
+
};
|
|
9665
|
+
const report = {
|
|
9666
|
+
schema: "tradejs-portfolio-backtest/v1",
|
|
9667
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9668
|
+
command,
|
|
9669
|
+
window: {
|
|
9670
|
+
start: window.start,
|
|
9671
|
+
end: window.end,
|
|
9672
|
+
startIso: new Date(window.start).toISOString(),
|
|
9673
|
+
endIso: new Date(window.end).toISOString()
|
|
9674
|
+
},
|
|
9675
|
+
lineage,
|
|
9676
|
+
counts: {
|
|
9677
|
+
strategies: replayResult.strategies.length,
|
|
9678
|
+
signals: replayResult.signals.length,
|
|
9679
|
+
completedTrades: trades.length
|
|
9680
|
+
},
|
|
9681
|
+
metrics: { portfolio: portfolioRows, strategies: strategyRows },
|
|
9682
|
+
curves: {
|
|
9683
|
+
portfolioEquity,
|
|
9684
|
+
portfolioDrawdown: drawdownCurve(portfolioEquity),
|
|
9685
|
+
strategies: strategySeries.map(({ name, points }) => ({ name, points }))
|
|
9686
|
+
},
|
|
9687
|
+
trades
|
|
9688
|
+
};
|
|
9689
|
+
const checksum = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(report)).digest("hex");
|
|
9690
|
+
const semantics = "Production strategy order, at most one open position per symbol, and fixed-risk symbol batches merged by canonical realized PnL.";
|
|
9691
|
+
const markdown = `# Portfolio Backtest
|
|
9692
|
+
|
|
9693
|
+
- Window: ${report.window.startIso} .. ${report.window.endIso}
|
|
9694
|
+
- Strategies: ${report.counts.strategies}
|
|
9695
|
+
- Signals: ${report.counts.signals}
|
|
9696
|
+
- Completed trades: ${report.counts.completedTrades}
|
|
9697
|
+
- Portfolio semantics: ${semantics}
|
|
9698
|
+
- Report checksum: \`${checksum}\`
|
|
9699
|
+
- Command: \`${command}\`
|
|
9700
|
+
|
|
9701
|
+
})
|
|
9702
|
+
|
|
9703
|
+
})
|
|
9704
|
+
|
|
9705
|
+
## Portfolio metrics
|
|
9706
|
+
|
|
9707
|
+
${markdownTable(portfolioRows)}
|
|
9708
|
+
|
|
9709
|
+
## Strategy metrics
|
|
9710
|
+
|
|
9711
|
+
${markdownTable(strategyRows)}
|
|
9712
|
+
`;
|
|
9713
|
+
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>`;
|
|
9714
|
+
await Promise.all([
|
|
9715
|
+
import_promises3.default.writeFile(paths.markdown, markdown, "utf8"),
|
|
9716
|
+
import_promises3.default.writeFile(
|
|
9717
|
+
paths.json,
|
|
9718
|
+
`${JSON.stringify({ ...report, checksum }, null, 2)}
|
|
9719
|
+
`,
|
|
9720
|
+
"utf8"
|
|
9721
|
+
),
|
|
9722
|
+
import_promises3.default.writeFile(paths.equitySvg, equitySvg, "utf8"),
|
|
9723
|
+
import_promises3.default.writeFile(paths.drawdownSvg, drawdownSvg, "utf8"),
|
|
9724
|
+
import_promises3.default.writeFile(paths.html, html, "utf8")
|
|
9725
|
+
]);
|
|
9726
|
+
return { ...paths, checksum };
|
|
9727
|
+
};
|
|
9728
|
+
|
|
9258
9729
|
// src/scripts/replayRunner.ts
|
|
9259
9730
|
var prepareReplayBinanceMarketContext = async (preparedRun) => {
|
|
9260
9731
|
await prepareMarketContextForRun({
|
|
@@ -9273,17 +9744,47 @@ var prepareReplayBinanceMarketContext = async (preparedRun) => {
|
|
|
9273
9744
|
log: (message) => console.log(import_chalk13.default.gray(message))
|
|
9274
9745
|
});
|
|
9275
9746
|
};
|
|
9747
|
+
var replayMarketContextInterval = () => {
|
|
9748
|
+
if (replayInterval === "1") return "1m";
|
|
9749
|
+
if (replayInterval === "5") return "5m";
|
|
9750
|
+
if (replayInterval === "60") return "1h";
|
|
9751
|
+
return "15m";
|
|
9752
|
+
};
|
|
9753
|
+
var cachedTickerSymbols = async ({
|
|
9754
|
+
connectorName,
|
|
9755
|
+
universe,
|
|
9756
|
+
accountId
|
|
9757
|
+
}) => {
|
|
9758
|
+
const connectorNames = [
|
|
9759
|
+
connectorName,
|
|
9760
|
+
...connectorName.toLowerCase() === "bybit" ? ["ByBit"] : []
|
|
9761
|
+
];
|
|
9762
|
+
const keys = connectorNames.flatMap((name) => [
|
|
9763
|
+
import_redis6.redisKeys.tickerUniverse(replayUserName, name, universe, accountId),
|
|
9764
|
+
import_redis6.redisKeys.tickerUniverse(replayUserName, name, universe, "default"),
|
|
9765
|
+
import_redis6.redisKeys.tickerUniverse(replayUserName, name)
|
|
9766
|
+
]);
|
|
9767
|
+
for (const key of [...new Set(keys)]) {
|
|
9768
|
+
const cached = await (0, import_redis6.getData)(key, null);
|
|
9769
|
+
const symbols = (cached?.tickers ?? []).map(
|
|
9770
|
+
(ticker) => String(ticker.symbol ?? "").trim().toUpperCase()
|
|
9771
|
+
).filter(Boolean);
|
|
9772
|
+
if (symbols.length) return [...new Set(symbols)];
|
|
9773
|
+
}
|
|
9774
|
+
return [];
|
|
9775
|
+
};
|
|
9276
9776
|
var finishReplay = async ({
|
|
9277
9777
|
replayResult,
|
|
9278
9778
|
tickers,
|
|
9279
9779
|
connectorName,
|
|
9280
|
-
window
|
|
9780
|
+
window,
|
|
9781
|
+
portfolioLineage
|
|
9281
9782
|
}) => {
|
|
9282
9783
|
const replayStrategySnapshot = await saveAndPrintReplayResultsByStrategy({
|
|
9283
9784
|
replayResult,
|
|
9284
9785
|
tickers
|
|
9285
9786
|
});
|
|
9286
|
-
const replayRuntimeComparison = await saveAndPrintReplayRuntimeComparison({
|
|
9787
|
+
const replayRuntimeComparison = replayFlags.portfolioReport ? null : await saveAndPrintReplayRuntimeComparison({
|
|
9287
9788
|
liveStrategySummaries: replayStrategySnapshot.summaries,
|
|
9288
9789
|
backtestEntries: replayStrategySnapshot.backtestEntries,
|
|
9289
9790
|
replaySignals: replayResult.signals,
|
|
@@ -9326,6 +9827,18 @@ var finishReplay = async ({
|
|
|
9326
9827
|
});
|
|
9327
9828
|
console.log(import_chalk13.default.green(`Replay report: ${outputReport.markdownPath}`));
|
|
9328
9829
|
console.log(import_chalk13.default.green(`Replay report JSON: ${outputReport.jsonPath}`));
|
|
9830
|
+
const portfolioReport = replayFlags.portfolioReport ? await writePortfolioReport({
|
|
9831
|
+
projectRoot: replayProjectRoot,
|
|
9832
|
+
timestamp: `${timestamp}${replayFlags.portfolioOutputSuffix ? `-${String(replayFlags.portfolioOutputSuffix).replace(/[^a-zA-Z0-9_-]/g, "")}` : ""}`,
|
|
9833
|
+
replayResult,
|
|
9834
|
+
window,
|
|
9835
|
+
lineage: portfolioLineage ?? {},
|
|
9836
|
+
command: process.argv.join(" ")
|
|
9837
|
+
}) : null;
|
|
9838
|
+
if (portfolioReport) {
|
|
9839
|
+
console.log(import_chalk13.default.green(`Portfolio report: ${portfolioReport.html}`));
|
|
9840
|
+
console.log(import_chalk13.default.green(`Portfolio report JSON: ${portfolioReport.json}`));
|
|
9841
|
+
}
|
|
9329
9842
|
await (0, import_redis6.setData)(
|
|
9330
9843
|
replayKey,
|
|
9331
9844
|
{
|
|
@@ -9349,10 +9862,11 @@ var finishReplay = async ({
|
|
|
9349
9862
|
signalsCount: replayResult.signals.length,
|
|
9350
9863
|
replayLineage: replayResult.runtimeLineages,
|
|
9351
9864
|
strategyCharts: replayChartSnapshot,
|
|
9352
|
-
outputReport
|
|
9865
|
+
outputReport,
|
|
9866
|
+
...portfolioReport ? { portfolioReport } : {}
|
|
9353
9867
|
},
|
|
9354
9868
|
{
|
|
9355
|
-
expire:
|
|
9869
|
+
expire: import_constants7.TTL_1M
|
|
9356
9870
|
}
|
|
9357
9871
|
);
|
|
9358
9872
|
if (replayChartSnapshot) {
|
|
@@ -9362,7 +9876,7 @@ var finishReplay = async ({
|
|
|
9362
9876
|
import_redis6.redisKeys.strategyChartCard(replayUserName, "replay", card.cardId),
|
|
9363
9877
|
card,
|
|
9364
9878
|
{
|
|
9365
|
-
expire:
|
|
9879
|
+
expire: import_constants7.TTL_1M
|
|
9366
9880
|
}
|
|
9367
9881
|
)
|
|
9368
9882
|
)
|
|
@@ -9412,7 +9926,57 @@ var replayBacktest = async () => {
|
|
|
9412
9926
|
projectRoot: replayProjectRoot,
|
|
9413
9927
|
deploymentId: replayDeploymentId
|
|
9414
9928
|
});
|
|
9415
|
-
const { deployment
|
|
9929
|
+
const { deployment } = replayComposition;
|
|
9930
|
+
const maxLossValue = (() => {
|
|
9931
|
+
if (replayFlags.maxLossValue == null || replayFlags.maxLossValue === "") {
|
|
9932
|
+
return null;
|
|
9933
|
+
}
|
|
9934
|
+
const value = Number(replayFlags.maxLossValue);
|
|
9935
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
9936
|
+
throw new Error(
|
|
9937
|
+
`Invalid --maxLossValue: ${String(replayFlags.maxLossValue)}`
|
|
9938
|
+
);
|
|
9939
|
+
}
|
|
9940
|
+
return value;
|
|
9941
|
+
})();
|
|
9942
|
+
const sourceDeploymentCompositionId = replayComposition.strategies[0]?.deploymentCompositionId ?? deployment.deploymentCompositionId;
|
|
9943
|
+
let replayStrategies = replayComposition.strategies.map((strategy) => {
|
|
9944
|
+
if (maxLossValue == null) return strategy;
|
|
9945
|
+
const strategyConfig = {
|
|
9946
|
+
...strategy.strategyConfig,
|
|
9947
|
+
MAX_LOSS_VALUE: maxLossValue
|
|
9948
|
+
};
|
|
9949
|
+
return {
|
|
9950
|
+
...strategy,
|
|
9951
|
+
strategyConfig,
|
|
9952
|
+
strategyRevision: (0, import_runtimeStrategies4.computeStrategyRevision)({
|
|
9953
|
+
strategyName: strategy.strategyName,
|
|
9954
|
+
strategyPackage: strategy.strategyPackage,
|
|
9955
|
+
strategyPackageVersion: strategy.strategyPackageVersion,
|
|
9956
|
+
strategyDependencyVersions: strategy.strategyDependencyVersions,
|
|
9957
|
+
runtimePackageVersion: strategy.runtimePackageVersion,
|
|
9958
|
+
strategyConfig
|
|
9959
|
+
})
|
|
9960
|
+
};
|
|
9961
|
+
});
|
|
9962
|
+
const researchDeploymentCompositionId = (0, import_runtimeStrategies4.computeDeploymentCompositionId)({
|
|
9963
|
+
deploymentId: deployment.id,
|
|
9964
|
+
connectorName: deployment.connectorName,
|
|
9965
|
+
provider: deployment.provider,
|
|
9966
|
+
accountId: deployment.accountId,
|
|
9967
|
+
enabled: deployment.enabled,
|
|
9968
|
+
...deployment.assetClasses ? { assetClasses: deployment.assetClasses } : {},
|
|
9969
|
+
strategies: replayStrategies.map((strategy) => ({
|
|
9970
|
+
strategyName: strategy.strategyName,
|
|
9971
|
+
strategyRevision: strategy.strategyRevision,
|
|
9972
|
+
enabled: true,
|
|
9973
|
+
...strategy.selection ? { selection: strategy.selection } : {}
|
|
9974
|
+
}))
|
|
9975
|
+
});
|
|
9976
|
+
replayStrategies = replayStrategies.map((strategy) => ({
|
|
9977
|
+
...strategy,
|
|
9978
|
+
deploymentCompositionId: researchDeploymentCompositionId
|
|
9979
|
+
}));
|
|
9416
9980
|
if (!replayStrategies.length) {
|
|
9417
9981
|
throw new Error(`No enabled strategies in deployment ${deployment.id}`);
|
|
9418
9982
|
}
|
|
@@ -9437,10 +10001,26 @@ var replayBacktest = async () => {
|
|
|
9437
10001
|
);
|
|
9438
10002
|
}
|
|
9439
10003
|
const replaySelection = mergeRuntimeStrategySelections(replayStrategies);
|
|
10004
|
+
const useAllData = Boolean(replayFlags.allData) && replayFlags.days == null && replayFlags.startTime == null && replayFlags.endTime == null;
|
|
10005
|
+
let candleCoverage;
|
|
10006
|
+
let portfolioMarketContextRows;
|
|
10007
|
+
let allDataTickers;
|
|
10008
|
+
if (useAllData && !replayFlags.tickers) {
|
|
10009
|
+
allDataTickers = await cachedTickerSymbols({
|
|
10010
|
+
connectorName: deployment.connectorName,
|
|
10011
|
+
universe: [...strategyUniverses][0],
|
|
10012
|
+
accountId: deployment.accountId
|
|
10013
|
+
});
|
|
10014
|
+
if (!allDataTickers.length) {
|
|
10015
|
+
throw new Error(
|
|
10016
|
+
`No cached ticker universe found for ${deployment.connectorName}. Pass --tickers explicitly or refresh the ticker cache first.`
|
|
10017
|
+
);
|
|
10018
|
+
}
|
|
10019
|
+
}
|
|
9440
10020
|
const preparedRun = await prepareRunEnvironment({
|
|
9441
10021
|
connector: deployment.connectorName,
|
|
9442
10022
|
userName: replayUserName,
|
|
9443
|
-
tickers: replayFlags.tickers ?? replaySelection?.tickers?.join(",") ?? deployment.tickers?.join(","),
|
|
10023
|
+
tickers: replayFlags.tickers ?? allDataTickers?.join(",") ?? replaySelection?.tickers?.join(",") ?? deployment.tickers?.join(","),
|
|
9444
10024
|
exclude: replayFlags.exclude,
|
|
9445
10025
|
tickersLimit: replayFlags.tickersLimit,
|
|
9446
10026
|
showTickersList: replayFlags.showTickersList,
|
|
@@ -9460,6 +10040,45 @@ var replayBacktest = async () => {
|
|
|
9460
10040
|
if (!preparedRun || isReplayUpdateOnlyRun) {
|
|
9461
10041
|
return;
|
|
9462
10042
|
}
|
|
10043
|
+
if (useAllData) {
|
|
10044
|
+
const intervalMs = (0, import_data4.intervalToMs)(replayInterval);
|
|
10045
|
+
const edges = await (0, import_candles.getDataEdgesForSymbols)(
|
|
10046
|
+
deployment.provider,
|
|
10047
|
+
preparedRun.tickers,
|
|
10048
|
+
Number(replayInterval)
|
|
10049
|
+
);
|
|
10050
|
+
const available = [...edges.values()].filter(
|
|
10051
|
+
(edge) => Number.isFinite(edge.min) && Number.isFinite(edge.max)
|
|
10052
|
+
);
|
|
10053
|
+
if (!available.length) {
|
|
10054
|
+
throw new Error(
|
|
10055
|
+
`No cached candles found for provider=${deployment.provider}, interval=${replayInterval}`
|
|
10056
|
+
);
|
|
10057
|
+
}
|
|
10058
|
+
const start = Math.min(...available.map((edge) => edge.min));
|
|
10059
|
+
const lastClosedEnd = Math.floor(Date.now() / intervalMs) * intervalMs - 1;
|
|
10060
|
+
const max = Math.min(
|
|
10061
|
+
Math.max(...available.map((edge) => edge.max)),
|
|
10062
|
+
lastClosedEnd
|
|
10063
|
+
);
|
|
10064
|
+
preparedRun.window = {
|
|
10065
|
+
start,
|
|
10066
|
+
end: max + intervalMs - 1,
|
|
10067
|
+
source: "explicit"
|
|
10068
|
+
};
|
|
10069
|
+
preparedRun.preloadStart = (0, import_time7.getBacktestPreloadStart)(start);
|
|
10070
|
+
candleCoverage = {
|
|
10071
|
+
symbolsRequested: preparedRun.tickers.length,
|
|
10072
|
+
symbolsWithData: available.length,
|
|
10073
|
+
min: start,
|
|
10074
|
+
max
|
|
10075
|
+
};
|
|
10076
|
+
console.log(
|
|
10077
|
+
import_chalk13.default.gray(
|
|
10078
|
+
`cached candle coverage: ${new Date(start).toISOString()} -> ${new Date(max).toISOString()} (${available.length}/${preparedRun.tickers.length} symbols)`
|
|
10079
|
+
)
|
|
10080
|
+
);
|
|
10081
|
+
}
|
|
9463
10082
|
setRuntimeCompareContext({
|
|
9464
10083
|
connector: preparedRun.marketConnector,
|
|
9465
10084
|
connectorName: preparedRun.connectorName,
|
|
@@ -9468,16 +10087,29 @@ var replayBacktest = async () => {
|
|
|
9468
10087
|
end: preparedRun.window.end
|
|
9469
10088
|
}
|
|
9470
10089
|
});
|
|
10090
|
+
const aiEnabled = replayStrategies.some(
|
|
10091
|
+
({ strategyConfig }) => Boolean(strategyConfig.AI_ENABLED)
|
|
10092
|
+
);
|
|
9471
10093
|
await prepareReplayBinanceMarketContext({
|
|
9472
10094
|
...preparedRun,
|
|
9473
|
-
aiEnabled
|
|
9474
|
-
({ strategyConfig }) => Boolean(strategyConfig.AI_ENABLED)
|
|
9475
|
-
),
|
|
10095
|
+
aiEnabled,
|
|
9476
10096
|
mlEnabled: replayStrategies.some(
|
|
9477
10097
|
({ strategyConfig }) => Boolean(strategyConfig.ML_ENABLED)
|
|
9478
10098
|
),
|
|
9479
10099
|
strategyNames: replayStrategies.map(({ strategyName }) => strategyName)
|
|
9480
10100
|
});
|
|
10101
|
+
if (replayFlags.portfolioReport && replayFlags.allData && aiEnabled) {
|
|
10102
|
+
portfolioMarketContextRows = await (0, import_strategies5.preloadBinanceMarketContextForWindow)({
|
|
10103
|
+
startMs: preparedRun.window.start,
|
|
10104
|
+
endMs: preparedRun.window.end,
|
|
10105
|
+
interval: replayMarketContextInterval()
|
|
10106
|
+
});
|
|
10107
|
+
console.log(
|
|
10108
|
+
import_chalk13.default.gray(
|
|
10109
|
+
`preloaded Binance market context: ${portfolioMarketContextRows.tradeFlowRows} trade-flow rows, ${portfolioMarketContextRows.breadthRows} breadth rows`
|
|
10110
|
+
)
|
|
10111
|
+
);
|
|
10112
|
+
}
|
|
9481
10113
|
console.log(import_chalk13.default.yellow(`tickers: ${preparedRun.tickers.length}`));
|
|
9482
10114
|
console.log(
|
|
9483
10115
|
import_chalk13.default.gray(
|
|
@@ -9485,13 +10117,54 @@ var replayBacktest = async () => {
|
|
|
9485
10117
|
)
|
|
9486
10118
|
);
|
|
9487
10119
|
markTestsStarted();
|
|
9488
|
-
const
|
|
10120
|
+
const portfolioBatchSize = Number(replayFlags.portfolioBatchSize ?? 4);
|
|
10121
|
+
if (replayFlags.portfolioReport && (!Number.isSafeInteger(portfolioBatchSize) || portfolioBatchSize <= 0)) {
|
|
10122
|
+
throw new Error(
|
|
10123
|
+
`Invalid --portfolioBatchSize: ${String(replayFlags.portfolioBatchSize)}`
|
|
10124
|
+
);
|
|
10125
|
+
}
|
|
10126
|
+
const runtimeStrategies = replayFlags.tickers && !replayFlags.portfolioReport ? replayStrategies.map(
|
|
10127
|
+
({ selection: _selection, ...strategy }) => strategy
|
|
10128
|
+
) : replayStrategies;
|
|
10129
|
+
const historicalReplayReferences = replayFlags.portfolioReport ? await loadHistoricalReplayReferences({
|
|
9489
10130
|
preparedRun,
|
|
9490
|
-
interval: replayInterval
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
10131
|
+
interval: replayInterval
|
|
10132
|
+
}) : void 0;
|
|
10133
|
+
const tickerBatches = replayFlags.portfolioReport ? Array.from(
|
|
10134
|
+
{ length: Math.ceil(preparedRun.tickers.length / portfolioBatchSize) },
|
|
10135
|
+
(_, index) => preparedRun.tickers.slice(
|
|
10136
|
+
index * portfolioBatchSize,
|
|
10137
|
+
(index + 1) * portfolioBatchSize
|
|
10138
|
+
)
|
|
10139
|
+
) : [preparedRun.tickers];
|
|
10140
|
+
const runReplayBatch = async (batchTickers) => {
|
|
10141
|
+
const result = await runHistoricalSignalsReplay({
|
|
10142
|
+
preparedRun: { ...preparedRun, tickers: batchTickers },
|
|
10143
|
+
interval: replayInterval,
|
|
10144
|
+
runtimeStrategies,
|
|
10145
|
+
references: historicalReplayReferences,
|
|
10146
|
+
showProgress: !replayFlags.portfolioReport,
|
|
10147
|
+
collectSkipEvidence: !replayFlags.portfolioReport,
|
|
10148
|
+
collectSignals: !replayFlags.portfolioReport
|
|
10149
|
+
});
|
|
10150
|
+
return replayFlags.portfolioReport ? compactHistoricalReplayResultForPortfolio(result) : result;
|
|
10151
|
+
};
|
|
10152
|
+
const batchResults = [];
|
|
10153
|
+
for (const [index, batchTickers] of tickerBatches.entries()) {
|
|
10154
|
+
if (tickerBatches.length > 1) {
|
|
10155
|
+
console.log(
|
|
10156
|
+
import_chalk13.default.gray(
|
|
10157
|
+
`portfolio batch ${index + 1}/${tickerBatches.length}: ${batchTickers.length} symbols`
|
|
10158
|
+
)
|
|
10159
|
+
);
|
|
10160
|
+
}
|
|
10161
|
+
batchResults.push(await runReplayBatch(batchTickers));
|
|
10162
|
+
if (replayFlags.portfolioReport) {
|
|
10163
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
10164
|
+
globalThis.gc?.();
|
|
10165
|
+
}
|
|
10166
|
+
}
|
|
10167
|
+
const replayResult = batchResults.length === 1 ? batchResults[0] : mergeHistoricalReplayResults(batchResults);
|
|
9495
10168
|
for (const _strategy of replayResult.strategies) {
|
|
9496
10169
|
incrementSuccessTests();
|
|
9497
10170
|
}
|
|
@@ -9499,7 +10172,32 @@ var replayBacktest = async () => {
|
|
|
9499
10172
|
replayResult,
|
|
9500
10173
|
tickers: preparedRun.tickers,
|
|
9501
10174
|
connectorName: preparedRun.connectorName,
|
|
9502
|
-
window: preparedRun.window
|
|
10175
|
+
window: preparedRun.window,
|
|
10176
|
+
portfolioLineage: replayFlags.portfolioReport ? {
|
|
10177
|
+
sourceDeploymentId: deployment.id,
|
|
10178
|
+
sourceDeploymentCompositionId,
|
|
10179
|
+
researchDeploymentCompositionId,
|
|
10180
|
+
maxLossValue,
|
|
10181
|
+
aiMode: "strategy runtime declaration",
|
|
10182
|
+
cacheOnly: Boolean(replayFlags.cacheOnly),
|
|
10183
|
+
portfolioSemantics: "production strategy order and one concurrent position per symbol; fixed-risk symbol batches are merged by canonical realized PnL",
|
|
10184
|
+
portfolioBatchSize,
|
|
10185
|
+
tickers: preparedRun.tickers,
|
|
10186
|
+
candleCoverage: candleCoverage ?? null,
|
|
10187
|
+
binanceMarketContextRows: portfolioMarketContextRows ?? null,
|
|
10188
|
+
strategies: replayStrategies.map((strategy) => ({
|
|
10189
|
+
strategyName: strategy.strategyName,
|
|
10190
|
+
strategyRevision: strategy.strategyRevision,
|
|
10191
|
+
strategyPackage: strategy.strategyPackage,
|
|
10192
|
+
strategyPackageVersion: strategy.strategyPackageVersion,
|
|
10193
|
+
aiEnabled: Boolean(strategy.strategyConfig.AI_ENABLED),
|
|
10194
|
+
minAiQuality: strategy.strategyConfig.MIN_AI_QUALITY ?? null,
|
|
10195
|
+
aiMode: strategy.strategyConfig.AI_MODE ?? null,
|
|
10196
|
+
maxLossValue: strategy.strategyConfig.MAX_LOSS_VALUE ?? null,
|
|
10197
|
+
selection: strategy.selection ?? null,
|
|
10198
|
+
effectiveConfig: strategy.strategyConfig
|
|
10199
|
+
}))
|
|
10200
|
+
} : void 0
|
|
9503
10201
|
});
|
|
9504
10202
|
};
|
|
9505
10203
|
|