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