@tradejs/cli 3.1.19 → 3.1.20-beta.236
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 +1782 -1580
- package/dist/scripts/replay.js +952 -849
- package/dist/scripts/replayRunner.js +952 -849
- package/dist/scripts/runtimeParity.js +630 -628
- package/package.json +7 -7
package/dist/scripts/replay.js
CHANGED
|
@@ -35,7 +35,7 @@ __export(replay_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(replay_exports);
|
|
36
36
|
|
|
37
37
|
// src/scripts/replayRunner.ts
|
|
38
|
-
var
|
|
38
|
+
var import_chalk13 = __toESM(require("chalk"));
|
|
39
39
|
var import_constants6 = require("@tradejs/core/constants");
|
|
40
40
|
var import_data2 = require("@tradejs/core/data");
|
|
41
41
|
var import_time7 = require("@tradejs/core/time");
|
|
@@ -4948,7 +4948,6 @@ var compactReplaySignal = (signal) => {
|
|
|
4948
4948
|
// src/lib/replay/historicalSignalsReplay.ts
|
|
4949
4949
|
var import_chalk9 = __toESM(require("chalk"));
|
|
4950
4950
|
var import_progress4 = __toESM(require("progress"));
|
|
4951
|
-
var import_backtest = require("@tradejs/core/backtest");
|
|
4952
4951
|
var import_strategies3 = require("@tradejs/core/strategies");
|
|
4953
4952
|
var import_time3 = require("@tradejs/core/time");
|
|
4954
4953
|
var import_logger = require("@tradejs/infra/logger");
|
|
@@ -5361,25 +5360,6 @@ var createPortfolioReplayConnector = (connector) => {
|
|
|
5361
5360
|
};
|
|
5362
5361
|
};
|
|
5363
5362
|
|
|
5364
|
-
// src/lib/marketData/windows.ts
|
|
5365
|
-
var import_indicators2 = require("@tradejs/core/indicators");
|
|
5366
|
-
var splitCandlesForReplayWindow = (candles, start, preloadStart) => {
|
|
5367
|
-
const prevData = [];
|
|
5368
|
-
const replayData = [];
|
|
5369
|
-
for (const candle of candles) {
|
|
5370
|
-
if (candle.timestamp < preloadStart) {
|
|
5371
|
-
continue;
|
|
5372
|
-
}
|
|
5373
|
-
if (candle.timestamp < start) {
|
|
5374
|
-
prevData.push(candle);
|
|
5375
|
-
continue;
|
|
5376
|
-
}
|
|
5377
|
-
replayData.push(candle);
|
|
5378
|
-
}
|
|
5379
|
-
return { prevData, replayData };
|
|
5380
|
-
};
|
|
5381
|
-
var alignSymbolWithBtcReference = (symbolCandles, btcCandles) => (0, import_indicators2.alignSortedCandlesByTimestamp)(symbolCandles, btcCandles);
|
|
5382
|
-
|
|
5383
5363
|
// src/lib/signals/hooks.ts
|
|
5384
5364
|
var normalizeHookList = (value) => {
|
|
5385
5365
|
if (Array.isArray(value)) {
|
|
@@ -5406,70 +5386,26 @@ var invokeAfterSignalsHooks = async (hooks, params) => {
|
|
|
5406
5386
|
}
|
|
5407
5387
|
};
|
|
5408
5388
|
|
|
5409
|
-
// src/lib/
|
|
5410
|
-
var
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
} = runtimeStrategy;
|
|
5424
|
-
const strategyCreator = await (0, import_strategies4.getStrategyCreator)(
|
|
5425
|
-
strategyName,
|
|
5426
|
-
replayProjectRoot
|
|
5427
|
-
);
|
|
5428
|
-
if (!strategyCreator) {
|
|
5429
|
-
throw new Error(`Unknown strategy: ${strategyName}`);
|
|
5430
|
-
}
|
|
5431
|
-
const installed = await (0, import_runtimeStrategies3.getRuntimeStrategyPackageMetadata)({
|
|
5432
|
-
strategyName,
|
|
5433
|
-
projectRoot: replayProjectRoot
|
|
5434
|
-
});
|
|
5435
|
-
if (installed.strategyPackage !== strategyPackage || installed.strategyPackageVersion !== strategyPackageVersion || JSON.stringify(installed.strategyDependencyVersions) !== JSON.stringify(strategyDependencyVersions) || installed.runtimePackageVersion !== runtimePackageVersion) {
|
|
5436
|
-
throw new Error(
|
|
5437
|
-
`Runtime evidence package mismatch for ${strategyName}: expected=${strategyPackage}@${strategyPackageVersion}/@tradejs/node@${runtimePackageVersion}, installed=${installed.strategyPackage ?? "missing"}@${installed.strategyPackageVersion ?? "missing"}/@tradejs/node@${installed.runtimePackageVersion ?? "missing"}`
|
|
5438
|
-
);
|
|
5439
|
-
}
|
|
5440
|
-
const strategyResults = await (0, import_redis3.getData)(
|
|
5441
|
-
import_redis3.redisKeys.strategyResults(replayUserName, strategyName),
|
|
5442
|
-
{}
|
|
5443
|
-
);
|
|
5444
|
-
return {
|
|
5445
|
-
strategyName,
|
|
5446
|
-
strategyRevision,
|
|
5447
|
-
deploymentCompositionId,
|
|
5448
|
-
strategyPackage,
|
|
5449
|
-
strategyPackageVersion,
|
|
5450
|
-
strategyDependencyVersions,
|
|
5451
|
-
runtimePackageVersion,
|
|
5452
|
-
strategyCreator,
|
|
5453
|
-
strategyConfig,
|
|
5454
|
-
strategyResults,
|
|
5455
|
-
...selection ? { selection } : {}
|
|
5456
|
-
};
|
|
5457
|
-
})
|
|
5458
|
-
);
|
|
5459
|
-
return strategies;
|
|
5460
|
-
};
|
|
5461
|
-
var loadReferenceConnector = async (connectorName) => {
|
|
5462
|
-
const connectorFactory = await (0, import_connectors6.getConnectorCreatorByName)(
|
|
5463
|
-
connectorName,
|
|
5464
|
-
replayProjectRoot
|
|
5465
|
-
);
|
|
5466
|
-
if (!connectorFactory) {
|
|
5467
|
-
throw new Error(`Connector "${connectorName}" is not registered`);
|
|
5389
|
+
// src/lib/marketData/windows.ts
|
|
5390
|
+
var import_indicators2 = require("@tradejs/core/indicators");
|
|
5391
|
+
var splitCandlesForReplayWindow = (candles, start, preloadStart) => {
|
|
5392
|
+
const prevData = [];
|
|
5393
|
+
const replayData = [];
|
|
5394
|
+
for (const candle of candles) {
|
|
5395
|
+
if (candle.timestamp < preloadStart) {
|
|
5396
|
+
continue;
|
|
5397
|
+
}
|
|
5398
|
+
if (candle.timestamp < start) {
|
|
5399
|
+
prevData.push(candle);
|
|
5400
|
+
continue;
|
|
5401
|
+
}
|
|
5402
|
+
replayData.push(candle);
|
|
5468
5403
|
}
|
|
5469
|
-
return
|
|
5470
|
-
userName: replayUserName
|
|
5471
|
-
});
|
|
5404
|
+
return { prevData, replayData };
|
|
5472
5405
|
};
|
|
5406
|
+
var alignSymbolWithBtcReference = (symbolCandles, btcCandles) => (0, import_indicators2.alignSortedCandlesByTimestamp)(symbolCandles, btcCandles);
|
|
5407
|
+
|
|
5408
|
+
// src/lib/replay/historicalSignalsReplayPreparation.ts
|
|
5473
5409
|
var buildPreparedData = ({
|
|
5474
5410
|
data,
|
|
5475
5411
|
btcData,
|
|
@@ -5518,25 +5454,6 @@ var buildPreparedData = ({
|
|
|
5518
5454
|
btcCoinbasePrevData
|
|
5519
5455
|
};
|
|
5520
5456
|
};
|
|
5521
|
-
var buildAfterSignalsContext = ({
|
|
5522
|
-
connector,
|
|
5523
|
-
connectorName,
|
|
5524
|
-
tickers,
|
|
5525
|
-
runtimeStrategies,
|
|
5526
|
-
interval
|
|
5527
|
-
}) => ({
|
|
5528
|
-
connector,
|
|
5529
|
-
connectorName,
|
|
5530
|
-
userName: replayUserName,
|
|
5531
|
-
interval,
|
|
5532
|
-
tickers: [...tickers],
|
|
5533
|
-
runtimeStrategies: runtimeStrategies.map(
|
|
5534
|
-
({ strategyName, strategyConfig }) => ({
|
|
5535
|
-
strategyName,
|
|
5536
|
-
strategyConfig
|
|
5537
|
-
})
|
|
5538
|
-
)
|
|
5539
|
-
});
|
|
5540
5457
|
var strategyIncludesSymbol = (strategy, symbol) => {
|
|
5541
5458
|
const tickers = strategy.selection?.tickers;
|
|
5542
5459
|
if (!tickers) return true;
|
|
@@ -5546,73 +5463,28 @@ var strategyIncludesSymbol = (strategy, symbol) => {
|
|
|
5546
5463
|
);
|
|
5547
5464
|
};
|
|
5548
5465
|
var buildSymbolSharedReplayKey = ({
|
|
5466
|
+
userName,
|
|
5549
5467
|
connectorName,
|
|
5550
5468
|
interval,
|
|
5551
5469
|
symbol,
|
|
5552
5470
|
start,
|
|
5553
5471
|
end
|
|
5554
|
-
}) => ["replay",
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
const projectHooks = projectConfig.hooks;
|
|
5567
|
-
const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
|
|
5568
|
-
const replayConnector = createPortfolioReplayConnector(
|
|
5569
|
-
preparedRun.marketConnector
|
|
5570
|
-
);
|
|
5571
|
-
const connectorName = String(
|
|
5572
|
-
preparedRun.marketConnector?.name || import_connectors6.DEFAULT_CONNECTOR_NAME
|
|
5573
|
-
).trim() || import_connectors6.DEFAULT_CONNECTOR_NAME;
|
|
5574
|
-
const binanceConnector = connectorName.toLowerCase() === import_connectors6.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Binance") : preparedRun.marketConnector;
|
|
5575
|
-
const coinbaseConnector = connectorName.toLowerCase() === import_connectors6.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Coinbase") : preparedRun.marketConnector;
|
|
5576
|
-
const [btcBinanceData, btcCoinbaseData] = await Promise.all([
|
|
5577
|
-
binanceConnector.kline({
|
|
5578
|
-
symbol: "BTCUSDT",
|
|
5579
|
-
start: preparedRun.preloadStart,
|
|
5580
|
-
end: preparedRun.window.end,
|
|
5581
|
-
cacheOnly: true,
|
|
5582
|
-
interval
|
|
5583
|
-
}),
|
|
5584
|
-
coinbaseConnector.kline({
|
|
5585
|
-
symbol: "BTCUSDT",
|
|
5586
|
-
start: preparedRun.preloadStart,
|
|
5587
|
-
end: preparedRun.window.end,
|
|
5588
|
-
cacheOnly: true,
|
|
5589
|
-
interval
|
|
5590
|
-
})
|
|
5591
|
-
]);
|
|
5592
|
-
const btcMarketData = await preparedRun.marketConnector.kline({
|
|
5593
|
-
symbol: "BTCUSDT",
|
|
5594
|
-
start: preparedRun.preloadStart,
|
|
5595
|
-
end: preparedRun.window.end,
|
|
5596
|
-
cacheOnly: true,
|
|
5597
|
-
interval
|
|
5598
|
-
});
|
|
5599
|
-
const ethMarketData = await preparedRun.marketConnector.kline({
|
|
5600
|
-
symbol: "ETHUSDT",
|
|
5601
|
-
start: preparedRun.preloadStart,
|
|
5602
|
-
end: preparedRun.window.end,
|
|
5603
|
-
cacheOnly: true,
|
|
5604
|
-
interval
|
|
5605
|
-
});
|
|
5472
|
+
}) => ["replay", userName, connectorName, symbol, interval, start, end].join(":");
|
|
5473
|
+
var prepareHistoricalReplay = async (context, adapters) => {
|
|
5474
|
+
const {
|
|
5475
|
+
userName,
|
|
5476
|
+
projectRoot,
|
|
5477
|
+
preparedRun,
|
|
5478
|
+
interval,
|
|
5479
|
+
connectorName,
|
|
5480
|
+
replayConnector,
|
|
5481
|
+
strategies,
|
|
5482
|
+
references
|
|
5483
|
+
} = context;
|
|
5606
5484
|
const cycleSymbolsByTimestamp = /* @__PURE__ */ new Map();
|
|
5607
5485
|
const sharedReplayKeyPrefixes = [];
|
|
5486
|
+
const runtimeLineages = [];
|
|
5608
5487
|
let skippedSymbols = 0;
|
|
5609
|
-
const prepareBar = new import_progress4.default(
|
|
5610
|
-
"prepare :current/:total [:bar][:percent] skipped=:skipped :etas(s) :symbol",
|
|
5611
|
-
{
|
|
5612
|
-
total: preparedRun.tickers.length,
|
|
5613
|
-
width: 30
|
|
5614
|
-
}
|
|
5615
|
-
);
|
|
5616
5488
|
for (const symbol of preparedRun.tickers) {
|
|
5617
5489
|
const data = await preparedRun.marketConnector.kline({
|
|
5618
5490
|
symbol,
|
|
@@ -5623,22 +5495,23 @@ var runHistoricalSignalsReplay = async ({
|
|
|
5623
5495
|
});
|
|
5624
5496
|
const preparedData = buildPreparedData({
|
|
5625
5497
|
data,
|
|
5626
|
-
btcData: btcMarketData,
|
|
5627
|
-
ethData: ethMarketData,
|
|
5628
|
-
btcBinanceData,
|
|
5629
|
-
btcCoinbaseData,
|
|
5498
|
+
btcData: references.btcMarketData,
|
|
5499
|
+
ethData: references.ethMarketData,
|
|
5500
|
+
btcBinanceData: references.btcBinanceData,
|
|
5501
|
+
btcCoinbaseData: references.btcCoinbaseData,
|
|
5630
5502
|
start: preparedRun.window.start,
|
|
5631
5503
|
preloadStart: preparedRun.preloadStart
|
|
5632
5504
|
});
|
|
5633
5505
|
if (!preparedData.replayData.length || !preparedData.btcReplayData.length) {
|
|
5634
5506
|
skippedSymbols += 1;
|
|
5635
|
-
|
|
5636
|
-
skipped:
|
|
5637
|
-
symbol:
|
|
5507
|
+
adapters.progress.tick({
|
|
5508
|
+
skipped: adapters.display.skipped(skippedSymbols),
|
|
5509
|
+
symbol: adapters.display.symbol(symbol)
|
|
5638
5510
|
});
|
|
5639
5511
|
continue;
|
|
5640
5512
|
}
|
|
5641
5513
|
const sharedIndicatorsReplayKey = buildSymbolSharedReplayKey({
|
|
5514
|
+
userName,
|
|
5642
5515
|
connectorName,
|
|
5643
5516
|
interval,
|
|
5644
5517
|
symbol,
|
|
@@ -5647,66 +5520,61 @@ var runHistoricalSignalsReplay = async ({
|
|
|
5647
5520
|
});
|
|
5648
5521
|
sharedReplayKeyPrefixes.push(sharedIndicatorsReplayKey);
|
|
5649
5522
|
const strategiesForSymbol = await Promise.all(
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
}
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5523
|
+
strategies.filter((strategy) => strategyIncludesSymbol(strategy, symbol)).map(async (strategy) => {
|
|
5524
|
+
const runtimeLineage = await adapters.buildLineage({
|
|
5525
|
+
projectRoot,
|
|
5526
|
+
strategyName: strategy.strategyName,
|
|
5527
|
+
strategyRevision: strategy.strategyRevision,
|
|
5528
|
+
deploymentCompositionId: strategy.deploymentCompositionId,
|
|
5529
|
+
strategyPackageVersion: strategy.strategyPackageVersion,
|
|
5530
|
+
strategyDependencyVersions: strategy.strategyDependencyVersions,
|
|
5531
|
+
runtimePackageVersion: strategy.runtimePackageVersion,
|
|
5532
|
+
config: {
|
|
5533
|
+
configId: strategy.strategyRevision,
|
|
5534
|
+
strategyConfig: strategy.strategyConfig,
|
|
5535
|
+
symbolResultConfig: strategy.strategyResults?.[symbol]?.config ?? null
|
|
5536
|
+
},
|
|
5537
|
+
runContext: {
|
|
5538
|
+
connectorName: connectorName.toLowerCase(),
|
|
5539
|
+
interval: String(interval),
|
|
5540
|
+
universe: preparedRun.universe ?? null
|
|
5541
|
+
}
|
|
5542
|
+
});
|
|
5543
|
+
runtimeLineages.push({
|
|
5544
|
+
strategy: strategy.strategyName,
|
|
5545
|
+
symbol,
|
|
5546
|
+
...preparedRun.deploymentId ? { deploymentId: preparedRun.deploymentId } : {},
|
|
5547
|
+
...preparedRun.accountId ? { accountId: preparedRun.accountId } : {},
|
|
5548
|
+
lineage: runtimeLineage
|
|
5549
|
+
});
|
|
5550
|
+
return {
|
|
5551
|
+
strategyName: strategy.strategyName,
|
|
5552
|
+
strategyConfig: strategy.strategyConfig,
|
|
5553
|
+
runtimeLineage,
|
|
5554
|
+
run: await strategy.strategyCreator({
|
|
5555
|
+
userName,
|
|
5556
|
+
connectorName,
|
|
5557
|
+
runtimeConfigId: strategy.strategyRevision,
|
|
5558
|
+
strategyRevision: strategy.strategyRevision,
|
|
5684
5559
|
runtimeLineage,
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
btcCoinbaseData: preparedData.btcCoinbasePrevData,
|
|
5704
|
-
connector: replayConnector,
|
|
5705
|
-
sharedIndicatorsReplayKey
|
|
5706
|
-
})
|
|
5707
|
-
};
|
|
5708
|
-
}
|
|
5709
|
-
)
|
|
5560
|
+
universe: preparedRun.universe,
|
|
5561
|
+
accountId: preparedRun.accountId,
|
|
5562
|
+
deploymentId: preparedRun.deploymentId,
|
|
5563
|
+
config: buildReplayStrategyConfig({
|
|
5564
|
+
strategyConfig: strategy.strategyConfig,
|
|
5565
|
+
interval
|
|
5566
|
+
}),
|
|
5567
|
+
symbol,
|
|
5568
|
+
data: preparedData.prevData,
|
|
5569
|
+
btcData: preparedData.btcPrevData,
|
|
5570
|
+
ethData: preparedData.ethPrevData,
|
|
5571
|
+
btcBinanceData: preparedData.btcBinancePrevData,
|
|
5572
|
+
btcCoinbaseData: preparedData.btcCoinbasePrevData,
|
|
5573
|
+
connector: replayConnector,
|
|
5574
|
+
sharedIndicatorsReplayKey
|
|
5575
|
+
})
|
|
5576
|
+
};
|
|
5577
|
+
})
|
|
5710
5578
|
);
|
|
5711
5579
|
const symbolRuntime = {
|
|
5712
5580
|
symbol,
|
|
@@ -5716,9 +5584,9 @@ var runHistoricalSignalsReplay = async ({
|
|
|
5716
5584
|
currentIndex: 0,
|
|
5717
5585
|
strategies: strategiesForSymbol
|
|
5718
5586
|
};
|
|
5719
|
-
|
|
5720
|
-
skipped:
|
|
5721
|
-
symbol:
|
|
5587
|
+
adapters.progress.tick({
|
|
5588
|
+
skipped: adapters.display.skipped(skippedSymbols),
|
|
5589
|
+
symbol: adapters.display.symbol(symbol)
|
|
5722
5590
|
});
|
|
5723
5591
|
for (const candle of preparedData.replayData) {
|
|
5724
5592
|
const bucket = cycleSymbolsByTimestamp.get(candle.timestamp) ?? [];
|
|
@@ -5726,60 +5594,66 @@ var runHistoricalSignalsReplay = async ({
|
|
|
5726
5594
|
cycleSymbolsByTimestamp.set(candle.timestamp, bucket);
|
|
5727
5595
|
}
|
|
5728
5596
|
}
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5597
|
+
return {
|
|
5598
|
+
cycleSymbolsByTimestamp,
|
|
5599
|
+
orderedTimestamps: [...cycleSymbolsByTimestamp.keys()].sort(
|
|
5600
|
+
(left, right) => left - right
|
|
5601
|
+
),
|
|
5602
|
+
sharedReplayKeyPrefixes,
|
|
5603
|
+
runtimeLineages
|
|
5604
|
+
};
|
|
5605
|
+
};
|
|
5606
|
+
|
|
5607
|
+
// src/lib/replay/historicalSignalsReplayExecution.ts
|
|
5608
|
+
var advanceCycleMarket = async (plan, connector, timestamp) => {
|
|
5609
|
+
const cycleSymbols = plan.cycleSymbolsByTimestamp.get(timestamp) ?? [];
|
|
5610
|
+
for (const symbolRuntime of cycleSymbols) {
|
|
5611
|
+
const candle = symbolRuntime.replayData[symbolRuntime.currentIndex];
|
|
5612
|
+
if (candle?.timestamp === timestamp) {
|
|
5613
|
+
await connector.advanceMarket({
|
|
5614
|
+
symbol: symbolRuntime.symbol,
|
|
5615
|
+
candle
|
|
5616
|
+
});
|
|
5745
5617
|
}
|
|
5746
|
-
|
|
5618
|
+
}
|
|
5619
|
+
return cycleSymbols;
|
|
5620
|
+
};
|
|
5621
|
+
var advanceAbortedCycle = (cycleSymbols, timestamp) => {
|
|
5622
|
+
for (const symbolRuntime of cycleSymbols ?? []) {
|
|
5623
|
+
const candle = symbolRuntime.replayData[symbolRuntime.currentIndex];
|
|
5624
|
+
if (candle?.timestamp === timestamp) {
|
|
5625
|
+
symbolRuntime.currentIndex += 1;
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
};
|
|
5629
|
+
var executeHistoricalReplay = async (context, adapters) => {
|
|
5630
|
+
const signals = [];
|
|
5631
|
+
let abortedCycles = 0;
|
|
5747
5632
|
try {
|
|
5748
|
-
for (const timestamp of orderedTimestamps) {
|
|
5749
|
-
const cycleStartedAt =
|
|
5750
|
-
const cycleSymbols =
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
candle
|
|
5759
|
-
});
|
|
5760
|
-
}
|
|
5761
|
-
const beforeSignalsResult = await invokeBeforeSignalsHooks(
|
|
5762
|
-
projectHooks,
|
|
5763
|
-
afterSignalsContextBase
|
|
5633
|
+
for (const timestamp of context.plan.orderedTimestamps) {
|
|
5634
|
+
const cycleStartedAt = adapters.clock.now();
|
|
5635
|
+
const cycleSymbols = await advanceCycleMarket(
|
|
5636
|
+
context.plan,
|
|
5637
|
+
context.connector,
|
|
5638
|
+
timestamp
|
|
5639
|
+
);
|
|
5640
|
+
const beforeSignalsResult = await adapters.invokeBeforeSignals(
|
|
5641
|
+
context.hooks,
|
|
5642
|
+
context.hookContext
|
|
5764
5643
|
);
|
|
5765
5644
|
if (beforeSignalsResult?.abort === true) {
|
|
5766
5645
|
abortedCycles += 1;
|
|
5767
|
-
await
|
|
5768
|
-
...
|
|
5646
|
+
await adapters.invokeAfterSignals(context.hooks, {
|
|
5647
|
+
...context.hookContext,
|
|
5769
5648
|
signals: [],
|
|
5770
5649
|
status: "completed",
|
|
5771
|
-
durationMs:
|
|
5650
|
+
durationMs: adapters.clock.now() - cycleStartedAt
|
|
5772
5651
|
});
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
}
|
|
5779
|
-
cycleBar.tick(1, {
|
|
5780
|
-
signals: import_chalk9.default.cyan(signals.length),
|
|
5781
|
-
aborted: import_chalk9.default.yellow(abortedCycles),
|
|
5782
|
-
ts: import_chalk9.default.gray((0, import_time3.formatUnix)(timestamp))
|
|
5652
|
+
advanceAbortedCycle(cycleSymbols, timestamp);
|
|
5653
|
+
adapters.progress.tick({
|
|
5654
|
+
signals: adapters.display.signals(signals.length),
|
|
5655
|
+
aborted: adapters.display.aborted(abortedCycles),
|
|
5656
|
+
ts: adapters.display.timestamp(timestamp)
|
|
5783
5657
|
});
|
|
5784
5658
|
continue;
|
|
5785
5659
|
}
|
|
@@ -5799,73 +5673,286 @@ var runHistoricalSignalsReplay = async ({
|
|
|
5799
5673
|
);
|
|
5800
5674
|
if (result && typeof result !== "string") {
|
|
5801
5675
|
result.runtimeLineage = strategyRuntime.runtimeLineage;
|
|
5802
|
-
await (
|
|
5803
|
-
signal: result,
|
|
5804
|
-
env: "PARITY"
|
|
5805
|
-
});
|
|
5676
|
+
await adapters.enrichSignal(result);
|
|
5806
5677
|
cycleSignals.push(result);
|
|
5807
5678
|
signals.push(result);
|
|
5808
5679
|
}
|
|
5809
5680
|
}
|
|
5810
5681
|
symbolRuntime.currentIndex += 1;
|
|
5811
5682
|
}
|
|
5812
|
-
await
|
|
5813
|
-
...
|
|
5683
|
+
await adapters.invokeAfterSignals(context.hooks, {
|
|
5684
|
+
...context.hookContext,
|
|
5814
5685
|
signals: cycleSignals,
|
|
5815
5686
|
status: "completed",
|
|
5816
|
-
durationMs:
|
|
5687
|
+
durationMs: adapters.clock.now() - cycleStartedAt
|
|
5817
5688
|
});
|
|
5818
|
-
|
|
5819
|
-
signals:
|
|
5820
|
-
aborted:
|
|
5821
|
-
ts:
|
|
5689
|
+
adapters.progress.tick({
|
|
5690
|
+
signals: adapters.display.signals(signals.length),
|
|
5691
|
+
aborted: adapters.display.aborted(abortedCycles),
|
|
5692
|
+
ts: adapters.display.timestamp(timestamp)
|
|
5822
5693
|
});
|
|
5823
5694
|
}
|
|
5824
5695
|
} finally {
|
|
5825
|
-
for (const keyPrefix of sharedReplayKeyPrefixes) {
|
|
5826
|
-
|
|
5827
|
-
|
|
5696
|
+
for (const keyPrefix of context.plan.sharedReplayKeyPrefixes) {
|
|
5697
|
+
adapters.releaseIndicatorsCache(keyPrefix);
|
|
5698
|
+
adapters.releaseReplayCache(keyPrefix);
|
|
5828
5699
|
}
|
|
5829
5700
|
}
|
|
5830
|
-
|
|
5831
|
-
const strategies = loadedStrategies.map(
|
|
5832
|
-
({ strategyName, strategyConfig }) => {
|
|
5833
|
-
const orderLog = artifacts.orderLogByStrategy.get(strategyName) ?? [];
|
|
5834
|
-
const positionLog = artifacts.positionLogByStrategy.get(strategyName) ?? [];
|
|
5835
|
-
return {
|
|
5836
|
-
strategyName,
|
|
5837
|
-
strategyConfig,
|
|
5838
|
-
orderLog,
|
|
5839
|
-
positionLog,
|
|
5840
|
-
stat: positionLog.length ? (0, import_backtest.calculateStatsFull)(positionLog) : {
|
|
5841
|
-
orders: 0,
|
|
5842
|
-
wins: 0,
|
|
5843
|
-
losses: 0,
|
|
5844
|
-
netProfit: 0,
|
|
5845
|
-
amount: 0
|
|
5846
|
-
}
|
|
5847
|
-
};
|
|
5848
|
-
}
|
|
5849
|
-
);
|
|
5850
|
-
import_logger.logger.info(
|
|
5851
|
-
import_chalk9.default.gray(
|
|
5852
|
-
`signals replay historical cycles: ${orderedTimestamps.length} (aborted=${abortedCycles}) done in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s`
|
|
5853
|
-
)
|
|
5854
|
-
);
|
|
5855
|
-
return {
|
|
5856
|
-
strategies,
|
|
5857
|
-
signals,
|
|
5858
|
-
orderLog: artifacts.orderLog,
|
|
5859
|
-
positionLog: artifacts.positionLog,
|
|
5860
|
-
cycleCount: orderedTimestamps.length,
|
|
5861
|
-
abortedCycles,
|
|
5862
|
-
runtimeLineages
|
|
5863
|
-
};
|
|
5701
|
+
return { signals, abortedCycles };
|
|
5864
5702
|
};
|
|
5865
5703
|
|
|
5866
|
-
// src/lib/replay/
|
|
5867
|
-
var
|
|
5868
|
-
var
|
|
5704
|
+
// src/lib/replay/historicalSignalsReplayResults.ts
|
|
5705
|
+
var import_backtest = require("@tradejs/core/backtest");
|
|
5706
|
+
var emptyStat = () => ({
|
|
5707
|
+
orders: 0,
|
|
5708
|
+
wins: 0,
|
|
5709
|
+
losses: 0,
|
|
5710
|
+
netProfit: 0,
|
|
5711
|
+
amount: 0
|
|
5712
|
+
});
|
|
5713
|
+
var collectHistoricalReplayResult = (context) => ({
|
|
5714
|
+
strategies: context.strategies.map(({ strategyName, strategyConfig }) => {
|
|
5715
|
+
const orderLog = context.artifacts.orderLogByStrategy.get(strategyName) ?? [];
|
|
5716
|
+
const positionLog = context.artifacts.positionLogByStrategy.get(strategyName) ?? [];
|
|
5717
|
+
return {
|
|
5718
|
+
strategyName,
|
|
5719
|
+
strategyConfig,
|
|
5720
|
+
orderLog,
|
|
5721
|
+
positionLog,
|
|
5722
|
+
stat: positionLog.length ? (0, import_backtest.calculateStatsFull)(positionLog) : emptyStat()
|
|
5723
|
+
};
|
|
5724
|
+
}),
|
|
5725
|
+
signals: context.signals,
|
|
5726
|
+
orderLog: context.artifacts.orderLog,
|
|
5727
|
+
positionLog: context.artifacts.positionLog,
|
|
5728
|
+
cycleCount: context.cycleCount,
|
|
5729
|
+
abortedCycles: context.abortedCycles,
|
|
5730
|
+
runtimeLineages: context.runtimeLineages
|
|
5731
|
+
});
|
|
5732
|
+
|
|
5733
|
+
// src/lib/replay/historicalSignalsReplay.ts
|
|
5734
|
+
var loadRuntimeStrategies = async (runtimeStrategies) => {
|
|
5735
|
+
const strategies = await Promise.all(
|
|
5736
|
+
runtimeStrategies.map(async (runtimeStrategy) => {
|
|
5737
|
+
const {
|
|
5738
|
+
strategyName,
|
|
5739
|
+
strategyRevision,
|
|
5740
|
+
deploymentCompositionId,
|
|
5741
|
+
strategyPackage,
|
|
5742
|
+
strategyPackageVersion,
|
|
5743
|
+
strategyDependencyVersions,
|
|
5744
|
+
runtimePackageVersion,
|
|
5745
|
+
strategyConfig,
|
|
5746
|
+
selection
|
|
5747
|
+
} = runtimeStrategy;
|
|
5748
|
+
const strategyCreator = await (0, import_strategies4.getStrategyCreator)(
|
|
5749
|
+
strategyName,
|
|
5750
|
+
replayProjectRoot
|
|
5751
|
+
);
|
|
5752
|
+
if (!strategyCreator) {
|
|
5753
|
+
throw new Error(`Unknown strategy: ${strategyName}`);
|
|
5754
|
+
}
|
|
5755
|
+
const installed = await (0, import_runtimeStrategies3.getRuntimeStrategyPackageMetadata)({
|
|
5756
|
+
strategyName,
|
|
5757
|
+
projectRoot: replayProjectRoot
|
|
5758
|
+
});
|
|
5759
|
+
if (installed.strategyPackage !== strategyPackage || installed.strategyPackageVersion !== strategyPackageVersion || JSON.stringify(installed.strategyDependencyVersions) !== JSON.stringify(strategyDependencyVersions) || installed.runtimePackageVersion !== runtimePackageVersion) {
|
|
5760
|
+
throw new Error(
|
|
5761
|
+
`Runtime evidence package mismatch for ${strategyName}: expected=${strategyPackage}@${strategyPackageVersion}/@tradejs/node@${runtimePackageVersion}, installed=${installed.strategyPackage ?? "missing"}@${installed.strategyPackageVersion ?? "missing"}/@tradejs/node@${installed.runtimePackageVersion ?? "missing"}`
|
|
5762
|
+
);
|
|
5763
|
+
}
|
|
5764
|
+
const strategyResults = await (0, import_redis3.getData)(
|
|
5765
|
+
import_redis3.redisKeys.strategyResults(replayUserName, strategyName),
|
|
5766
|
+
{}
|
|
5767
|
+
);
|
|
5768
|
+
return {
|
|
5769
|
+
strategyName,
|
|
5770
|
+
strategyRevision,
|
|
5771
|
+
deploymentCompositionId,
|
|
5772
|
+
strategyPackage,
|
|
5773
|
+
strategyPackageVersion,
|
|
5774
|
+
strategyDependencyVersions,
|
|
5775
|
+
runtimePackageVersion,
|
|
5776
|
+
strategyCreator,
|
|
5777
|
+
strategyConfig,
|
|
5778
|
+
strategyResults,
|
|
5779
|
+
...selection ? { selection } : {}
|
|
5780
|
+
};
|
|
5781
|
+
})
|
|
5782
|
+
);
|
|
5783
|
+
return strategies;
|
|
5784
|
+
};
|
|
5785
|
+
var loadReferenceConnector = async (connectorName) => {
|
|
5786
|
+
const connectorFactory = await (0, import_connectors6.getConnectorCreatorByName)(
|
|
5787
|
+
connectorName,
|
|
5788
|
+
replayProjectRoot
|
|
5789
|
+
);
|
|
5790
|
+
if (!connectorFactory) {
|
|
5791
|
+
throw new Error(`Connector "${connectorName}" is not registered`);
|
|
5792
|
+
}
|
|
5793
|
+
return await connectorFactory({
|
|
5794
|
+
userName: replayUserName
|
|
5795
|
+
});
|
|
5796
|
+
};
|
|
5797
|
+
var buildAfterSignalsContext = ({
|
|
5798
|
+
connector,
|
|
5799
|
+
connectorName,
|
|
5800
|
+
tickers,
|
|
5801
|
+
runtimeStrategies,
|
|
5802
|
+
interval
|
|
5803
|
+
}) => ({
|
|
5804
|
+
connector,
|
|
5805
|
+
connectorName,
|
|
5806
|
+
userName: replayUserName,
|
|
5807
|
+
interval,
|
|
5808
|
+
tickers: [...tickers],
|
|
5809
|
+
runtimeStrategies: runtimeStrategies.map(
|
|
5810
|
+
({ strategyName, strategyConfig }) => ({
|
|
5811
|
+
strategyName,
|
|
5812
|
+
strategyConfig
|
|
5813
|
+
})
|
|
5814
|
+
)
|
|
5815
|
+
});
|
|
5816
|
+
var getConnectorName = (connector) => {
|
|
5817
|
+
const name = "name" in connector ? connector.name : void 0;
|
|
5818
|
+
return String(name || import_connectors6.DEFAULT_CONNECTOR_NAME).trim() || import_connectors6.DEFAULT_CONNECTOR_NAME;
|
|
5819
|
+
};
|
|
5820
|
+
var runHistoricalSignalsReplay = async ({
|
|
5821
|
+
preparedRun,
|
|
5822
|
+
interval,
|
|
5823
|
+
runtimeStrategies
|
|
5824
|
+
}) => {
|
|
5825
|
+
const startedAt = Date.now();
|
|
5826
|
+
const projectConfig = await (0, import_cli4.loadTradejsConfig)(replayProjectRoot);
|
|
5827
|
+
const projectHooks = projectConfig.hooks;
|
|
5828
|
+
const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
|
|
5829
|
+
const replayConnector = createPortfolioReplayConnector(
|
|
5830
|
+
preparedRun.marketConnector
|
|
5831
|
+
);
|
|
5832
|
+
const connectorName = getConnectorName(preparedRun.marketConnector);
|
|
5833
|
+
const binanceConnector = connectorName.toLowerCase() === import_connectors6.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Binance") : preparedRun.marketConnector;
|
|
5834
|
+
const coinbaseConnector = connectorName.toLowerCase() === import_connectors6.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Coinbase") : preparedRun.marketConnector;
|
|
5835
|
+
const [btcBinanceData, btcCoinbaseData] = await Promise.all([
|
|
5836
|
+
binanceConnector.kline({
|
|
5837
|
+
symbol: "BTCUSDT",
|
|
5838
|
+
start: preparedRun.preloadStart,
|
|
5839
|
+
end: preparedRun.window.end,
|
|
5840
|
+
cacheOnly: true,
|
|
5841
|
+
interval
|
|
5842
|
+
}),
|
|
5843
|
+
coinbaseConnector.kline({
|
|
5844
|
+
symbol: "BTCUSDT",
|
|
5845
|
+
start: preparedRun.preloadStart,
|
|
5846
|
+
end: preparedRun.window.end,
|
|
5847
|
+
cacheOnly: true,
|
|
5848
|
+
interval
|
|
5849
|
+
})
|
|
5850
|
+
]);
|
|
5851
|
+
const btcMarketData = await preparedRun.marketConnector.kline({
|
|
5852
|
+
symbol: "BTCUSDT",
|
|
5853
|
+
start: preparedRun.preloadStart,
|
|
5854
|
+
end: preparedRun.window.end,
|
|
5855
|
+
cacheOnly: true,
|
|
5856
|
+
interval
|
|
5857
|
+
});
|
|
5858
|
+
const ethMarketData = await preparedRun.marketConnector.kline({
|
|
5859
|
+
symbol: "ETHUSDT",
|
|
5860
|
+
start: preparedRun.preloadStart,
|
|
5861
|
+
end: preparedRun.window.end,
|
|
5862
|
+
cacheOnly: true,
|
|
5863
|
+
interval
|
|
5864
|
+
});
|
|
5865
|
+
const prepareBar = new import_progress4.default(
|
|
5866
|
+
"prepare :current/:total [:bar][:percent] skipped=:skipped :etas(s) :symbol",
|
|
5867
|
+
{
|
|
5868
|
+
total: preparedRun.tickers.length,
|
|
5869
|
+
width: 30
|
|
5870
|
+
}
|
|
5871
|
+
);
|
|
5872
|
+
const plan = await prepareHistoricalReplay(
|
|
5873
|
+
{
|
|
5874
|
+
userName: replayUserName,
|
|
5875
|
+
projectRoot: replayProjectRoot,
|
|
5876
|
+
preparedRun,
|
|
5877
|
+
interval,
|
|
5878
|
+
connectorName,
|
|
5879
|
+
replayConnector,
|
|
5880
|
+
strategies: loadedStrategies,
|
|
5881
|
+
references: {
|
|
5882
|
+
btcMarketData,
|
|
5883
|
+
ethMarketData,
|
|
5884
|
+
btcBinanceData,
|
|
5885
|
+
btcCoinbaseData
|
|
5886
|
+
}
|
|
5887
|
+
},
|
|
5888
|
+
{
|
|
5889
|
+
progress: {
|
|
5890
|
+
tick: (tokens) => prepareBar.tick(1, tokens)
|
|
5891
|
+
},
|
|
5892
|
+
display: {
|
|
5893
|
+
skipped: (value) => import_chalk9.default.yellow(value),
|
|
5894
|
+
symbol: (value) => import_chalk9.default.gray(value)
|
|
5895
|
+
},
|
|
5896
|
+
buildLineage: buildRuntimeLineage
|
|
5897
|
+
}
|
|
5898
|
+
);
|
|
5899
|
+
const hookContext = buildAfterSignalsContext({
|
|
5900
|
+
connector: replayConnector,
|
|
5901
|
+
connectorName: preparedRun.connectorName,
|
|
5902
|
+
tickers: preparedRun.tickers,
|
|
5903
|
+
runtimeStrategies: loadedStrategies,
|
|
5904
|
+
interval
|
|
5905
|
+
});
|
|
5906
|
+
const cycleBar = new import_progress4.default(
|
|
5907
|
+
"cycles :current/:total [:bar][:percent] sig=:signals abort=:aborted :etas(s) :ts",
|
|
5908
|
+
{
|
|
5909
|
+
total: plan.orderedTimestamps.length,
|
|
5910
|
+
width: 30
|
|
5911
|
+
}
|
|
5912
|
+
);
|
|
5913
|
+
const execution = await executeHistoricalReplay(
|
|
5914
|
+
{
|
|
5915
|
+
plan,
|
|
5916
|
+
connector: replayConnector,
|
|
5917
|
+
hooks: projectHooks,
|
|
5918
|
+
hookContext
|
|
5919
|
+
},
|
|
5920
|
+
{
|
|
5921
|
+
clock: { now: Date.now },
|
|
5922
|
+
progress: {
|
|
5923
|
+
tick: (tokens) => cycleBar.tick(1, tokens)
|
|
5924
|
+
},
|
|
5925
|
+
display: {
|
|
5926
|
+
signals: (value) => import_chalk9.default.cyan(value),
|
|
5927
|
+
aborted: (value) => import_chalk9.default.yellow(value),
|
|
5928
|
+
timestamp: (value) => import_chalk9.default.gray((0, import_time3.formatUnix)(value))
|
|
5929
|
+
},
|
|
5930
|
+
invokeBeforeSignals: invokeBeforeSignalsHooks,
|
|
5931
|
+
invokeAfterSignals: invokeAfterSignalsHooks,
|
|
5932
|
+
enrichSignal: (signal) => (0, import_strategies4.enrichSignalWithBinanceMarketContext)({ signal, env: "PARITY" }),
|
|
5933
|
+
releaseIndicatorsCache: import_strategies3.releaseStrategyIndicatorsReplayCache,
|
|
5934
|
+
releaseReplayCache: import_strategies3.releaseStrategyReplayCache
|
|
5935
|
+
}
|
|
5936
|
+
);
|
|
5937
|
+
const artifacts = replayConnector.getReplayArtifacts();
|
|
5938
|
+
import_logger.logger.info(
|
|
5939
|
+
import_chalk9.default.gray(
|
|
5940
|
+
`signals replay historical cycles: ${plan.orderedTimestamps.length} (aborted=${execution.abortedCycles}) done in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s`
|
|
5941
|
+
)
|
|
5942
|
+
);
|
|
5943
|
+
return collectHistoricalReplayResult({
|
|
5944
|
+
strategies: loadedStrategies,
|
|
5945
|
+
artifacts,
|
|
5946
|
+
signals: execution.signals,
|
|
5947
|
+
cycleCount: plan.orderedTimestamps.length,
|
|
5948
|
+
abortedCycles: execution.abortedCycles,
|
|
5949
|
+
runtimeLineages: plan.runtimeLineages
|
|
5950
|
+
});
|
|
5951
|
+
};
|
|
5952
|
+
|
|
5953
|
+
// src/lib/replay/chartSnapshot.ts
|
|
5954
|
+
var formatPercent = (value) => typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(1)}%` : "n/a";
|
|
5955
|
+
var formatSigned = (value) => typeof value === "number" && Number.isFinite(value) ? `${value >= 0 ? "+" : ""}${value.toFixed(2)}` : "n/a";
|
|
5869
5956
|
var toSimpleOrderLog = (orderLog) => orderLog.map((entry) => [entry.timestamp, entry.amount]);
|
|
5870
5957
|
var toReplayChartOrders = (orderLog) => orderLog.map((entry, index) => {
|
|
5871
5958
|
const isOpenOrder = entry.type.startsWith("OPEN");
|
|
@@ -6267,8 +6354,7 @@ var saveAndPrintReplayResultsByStrategy = async ({
|
|
|
6267
6354
|
};
|
|
6268
6355
|
|
|
6269
6356
|
// src/lib/replay/runtimeComparison.ts
|
|
6270
|
-
var
|
|
6271
|
-
var import_time6 = require("@tradejs/core/time");
|
|
6357
|
+
var import_chalk12 = __toESM(require("chalk"));
|
|
6272
6358
|
|
|
6273
6359
|
// src/lib/paritySummary.ts
|
|
6274
6360
|
var summarizeRuntimeTradesByStrategy = (trades) => {
|
|
@@ -7452,31 +7538,7 @@ var buildReplayExchangeComparisonDetails = ({
|
|
|
7452
7538
|
};
|
|
7453
7539
|
};
|
|
7454
7540
|
|
|
7455
|
-
// src/lib/replay/
|
|
7456
|
-
var getReplayEntryTimestampCompareOffsetMs = () => {
|
|
7457
|
-
const intervalMinutes = Number(replayInterval);
|
|
7458
|
-
return Number.isFinite(intervalMinutes) && intervalMinutes > 0 ? intervalMinutes * 60 * 1e3 : 15 * 60 * 1e3;
|
|
7459
|
-
};
|
|
7460
|
-
var buildReplaySignalEvaluations = (signals) => signals.map((signal) => ({
|
|
7461
|
-
evaluationId: `${signal.strategy}:${signal.symbol}:${signal.timestamp}`,
|
|
7462
|
-
userName: replayUserName,
|
|
7463
|
-
strategy: signal.strategy,
|
|
7464
|
-
symbol: signal.symbol,
|
|
7465
|
-
interval: signal.interval,
|
|
7466
|
-
timestamp: signal.timestamp,
|
|
7467
|
-
evaluatedAt: signal.timestamp,
|
|
7468
|
-
status: "signal",
|
|
7469
|
-
reason: signal.orderSkipReason || signal.orderStatus || "SIGNAL",
|
|
7470
|
-
signalId: signal.signalId,
|
|
7471
|
-
direction: signal.direction,
|
|
7472
|
-
accountId: signal.accountId,
|
|
7473
|
-
deploymentId: signal.deploymentId,
|
|
7474
|
-
runtimeLineage: signal.runtimeLineage,
|
|
7475
|
-
orderStatus: signal.orderStatus,
|
|
7476
|
-
orderSkipReason: signal.orderSkipReason,
|
|
7477
|
-
...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
|
|
7478
|
-
...signal.ml ? { ml: signal.ml } : {}
|
|
7479
|
-
}));
|
|
7541
|
+
// src/lib/replay/runtimeComparisonCalculations.ts
|
|
7480
7542
|
var lineageScopeKey = ({
|
|
7481
7543
|
strategy,
|
|
7482
7544
|
symbol,
|
|
@@ -7496,12 +7558,7 @@ var hasExpectedLineage = ({
|
|
|
7496
7558
|
}) => runtimeLineagesComparable(
|
|
7497
7559
|
lineage,
|
|
7498
7560
|
expectedByScope.get(
|
|
7499
|
-
lineageScopeKey({
|
|
7500
|
-
strategy,
|
|
7501
|
-
symbol,
|
|
7502
|
-
deploymentId,
|
|
7503
|
-
accountId
|
|
7504
|
-
})
|
|
7561
|
+
lineageScopeKey({ strategy, symbol, deploymentId, accountId })
|
|
7505
7562
|
)
|
|
7506
7563
|
);
|
|
7507
7564
|
var filterReplayComparisonByLineage = ({
|
|
@@ -7623,13 +7680,7 @@ var filterReplayComparisonByLineage = ({
|
|
|
7623
7680
|
reason: comparableScopeKeys.size > 0 ? null : expectedByScope.size === 0 ? "replay_produced_no_lineage_scopes" : "no_runtime_artifacts_with_matching_lineage",
|
|
7624
7681
|
replay: [...expectedByScope.entries()].map(([scope, lineage]) => {
|
|
7625
7682
|
const [deploymentId, accountId, strategy, symbol] = scope.split("::");
|
|
7626
|
-
return {
|
|
7627
|
-
deploymentId,
|
|
7628
|
-
accountId,
|
|
7629
|
-
strategy,
|
|
7630
|
-
symbol,
|
|
7631
|
-
lineage
|
|
7632
|
-
};
|
|
7683
|
+
return { deploymentId, accountId, strategy, symbol, lineage };
|
|
7633
7684
|
}).sort(
|
|
7634
7685
|
(left, right) => left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || runtimeLineageKey(left.lineage).localeCompare(
|
|
7635
7686
|
runtimeLineageKey(right.lineage)
|
|
@@ -7638,29 +7689,174 @@ var filterReplayComparisonByLineage = ({
|
|
|
7638
7689
|
}
|
|
7639
7690
|
};
|
|
7640
7691
|
};
|
|
7641
|
-
var
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
let sum = 0;
|
|
7645
|
-
let hasValue = false;
|
|
7646
|
-
for (const value of values) {
|
|
7647
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
7648
|
-
continue;
|
|
7649
|
-
}
|
|
7650
|
-
sum += value;
|
|
7651
|
-
hasValue = true;
|
|
7692
|
+
var buildPriceDeltaPct3 = (leftPrice, rightPrice) => {
|
|
7693
|
+
if (leftPrice == null || rightPrice == null || !Number.isFinite(leftPrice) || !Number.isFinite(rightPrice) || leftPrice === 0) {
|
|
7694
|
+
return null;
|
|
7652
7695
|
}
|
|
7653
|
-
return
|
|
7696
|
+
return Math.abs((rightPrice - leftPrice) / leftPrice * 100);
|
|
7654
7697
|
};
|
|
7655
|
-
var
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7698
|
+
var compareExchangeEntriesToBacktest = ({
|
|
7699
|
+
exchangeEntries,
|
|
7700
|
+
backtestEntries,
|
|
7701
|
+
toleranceMs,
|
|
7702
|
+
backtestTimestampOffsetMs = 0
|
|
7703
|
+
}) => {
|
|
7704
|
+
const groupedExchange = /* @__PURE__ */ new Map();
|
|
7705
|
+
const groupedBacktest = /* @__PURE__ */ new Map();
|
|
7706
|
+
for (const entry of exchangeEntries) {
|
|
7707
|
+
const key = `${entry.symbol}::${entry.direction}`;
|
|
7708
|
+
const bucket = groupedExchange.get(key) ?? [];
|
|
7709
|
+
bucket.push(entry);
|
|
7710
|
+
groupedExchange.set(key, bucket);
|
|
7660
7711
|
}
|
|
7661
|
-
|
|
7712
|
+
for (const entry of backtestEntries) {
|
|
7713
|
+
const key = `${entry.symbol}::${entry.direction}`;
|
|
7714
|
+
const bucket = groupedBacktest.get(key) ?? [];
|
|
7715
|
+
bucket.push(entry);
|
|
7716
|
+
groupedBacktest.set(key, bucket);
|
|
7717
|
+
}
|
|
7718
|
+
const matched = [];
|
|
7719
|
+
const exchangeOnly = [];
|
|
7720
|
+
const backtestOnly = [];
|
|
7721
|
+
const matchedBacktestEntries = /* @__PURE__ */ new Set();
|
|
7722
|
+
const groupKeys = /* @__PURE__ */ new Set([
|
|
7723
|
+
...groupedExchange.keys(),
|
|
7724
|
+
...groupedBacktest.keys()
|
|
7725
|
+
]);
|
|
7726
|
+
for (const key of groupKeys) {
|
|
7727
|
+
const exchangeGroup = [...groupedExchange.get(key) ?? []].sort(
|
|
7728
|
+
(left, right) => left.entryTimestamp - right.entryTimestamp
|
|
7729
|
+
);
|
|
7730
|
+
const unmatchedBacktest = [...groupedBacktest.get(key) ?? []].sort((left, right) => left.timestamp - right.timestamp).map((entry) => ({ entry, used: false }));
|
|
7731
|
+
for (const exchangeEntry of exchangeGroup) {
|
|
7732
|
+
let bestIndex = -1;
|
|
7733
|
+
let bestDiff = Number.POSITIVE_INFINITY;
|
|
7734
|
+
for (let index = 0; index < unmatchedBacktest.length; index += 1) {
|
|
7735
|
+
const candidate = unmatchedBacktest[index];
|
|
7736
|
+
if (candidate.used) continue;
|
|
7737
|
+
const diff = Math.abs(
|
|
7738
|
+
getBacktestParityComparisonTimestamp(
|
|
7739
|
+
candidate.entry,
|
|
7740
|
+
backtestTimestampOffsetMs
|
|
7741
|
+
) - exchangeEntry.entryTimestamp
|
|
7742
|
+
);
|
|
7743
|
+
if (diff > toleranceMs || diff >= bestDiff) continue;
|
|
7744
|
+
bestIndex = index;
|
|
7745
|
+
bestDiff = diff;
|
|
7746
|
+
}
|
|
7747
|
+
if (bestIndex < 0) {
|
|
7748
|
+
exchangeOnly.push(exchangeEntry);
|
|
7749
|
+
continue;
|
|
7750
|
+
}
|
|
7751
|
+
unmatchedBacktest[bestIndex].used = true;
|
|
7752
|
+
const backtestEntry = unmatchedBacktest[bestIndex].entry;
|
|
7753
|
+
matchedBacktestEntries.add(backtestEntry);
|
|
7754
|
+
matched.push({
|
|
7755
|
+
exchange: exchangeEntry,
|
|
7756
|
+
backtest: backtestEntry,
|
|
7757
|
+
timestampDiffMs: bestDiff,
|
|
7758
|
+
priceDeltaPct: buildPriceDeltaPct3(
|
|
7759
|
+
exchangeEntry.entryPrice,
|
|
7760
|
+
backtestEntry.price
|
|
7761
|
+
)
|
|
7762
|
+
});
|
|
7763
|
+
}
|
|
7764
|
+
}
|
|
7765
|
+
for (const entry of backtestEntries) {
|
|
7766
|
+
if (!matchedBacktestEntries.has(entry)) backtestOnly.push(entry);
|
|
7767
|
+
}
|
|
7768
|
+
matched.sort(
|
|
7769
|
+
(left, right) => left.exchange.entryTimestamp - right.exchange.entryTimestamp || left.backtest.strategy.localeCompare(right.backtest.strategy)
|
|
7770
|
+
);
|
|
7771
|
+
exchangeOnly.sort(
|
|
7772
|
+
(left, right) => left.entryTimestamp - right.entryTimestamp
|
|
7773
|
+
);
|
|
7774
|
+
backtestOnly.sort((left, right) => left.timestamp - right.timestamp);
|
|
7775
|
+
return { matched, exchangeOnly, backtestOnly };
|
|
7776
|
+
};
|
|
7777
|
+
var findExchangeRuntimeOutcome = ({
|
|
7778
|
+
exchangeEntry,
|
|
7779
|
+
strategyName,
|
|
7780
|
+
runtimeSignals,
|
|
7781
|
+
runtimeSignalEvaluations,
|
|
7782
|
+
toleranceMs,
|
|
7783
|
+
signalTimestampOffsetMs
|
|
7784
|
+
}) => {
|
|
7785
|
+
const candidates = [];
|
|
7786
|
+
for (const signal of runtimeSignals) {
|
|
7787
|
+
if (signal.strategy === strategyName && signal.symbol === exchangeEntry.symbol && signal.direction === exchangeEntry.direction) {
|
|
7788
|
+
candidates.push({
|
|
7789
|
+
timestamp: signal.timestamp,
|
|
7790
|
+
orderStatus: signal.orderStatus,
|
|
7791
|
+
reason: signal.orderFailureReason || signal.orderSkipReason || signal.orderStatus
|
|
7792
|
+
});
|
|
7793
|
+
}
|
|
7794
|
+
}
|
|
7795
|
+
for (const evaluation of runtimeSignalEvaluations) {
|
|
7796
|
+
if (evaluation.strategy === strategyName && evaluation.symbol === exchangeEntry.symbol && (evaluation.direction == null || evaluation.direction === exchangeEntry.direction)) {
|
|
7797
|
+
candidates.push({
|
|
7798
|
+
timestamp: evaluation.timestamp,
|
|
7799
|
+
orderStatus: evaluation.orderStatus,
|
|
7800
|
+
reason: evaluation.orderSkipReason || evaluation.reason || evaluation.orderStatus
|
|
7801
|
+
});
|
|
7802
|
+
}
|
|
7803
|
+
}
|
|
7804
|
+
return candidates.map((candidate) => ({
|
|
7805
|
+
...candidate,
|
|
7806
|
+
timestampDiffMs: Math.abs(
|
|
7807
|
+
candidate.timestamp + signalTimestampOffsetMs - exchangeEntry.entryTimestamp
|
|
7808
|
+
)
|
|
7809
|
+
})).filter((candidate) => candidate.timestampDiffMs <= toleranceMs).sort(
|
|
7810
|
+
(left, right) => left.timestampDiffMs - right.timestampDiffMs || Number(right.orderStatus === "failed") - Number(left.orderStatus === "failed")
|
|
7811
|
+
)[0] ?? null;
|
|
7812
|
+
};
|
|
7813
|
+
var hasLineageLinkedRuntimeOutcome = ({
|
|
7814
|
+
exchangeEntry,
|
|
7815
|
+
strategyName,
|
|
7816
|
+
runtimeSignals,
|
|
7817
|
+
runtimeSignalEvaluations,
|
|
7818
|
+
toleranceMs,
|
|
7819
|
+
signalTimestampOffsetMs
|
|
7820
|
+
}) => findExchangeRuntimeOutcome({
|
|
7821
|
+
exchangeEntry,
|
|
7822
|
+
strategyName,
|
|
7823
|
+
runtimeSignals,
|
|
7824
|
+
runtimeSignalEvaluations,
|
|
7825
|
+
toleranceMs,
|
|
7826
|
+
signalTimestampOffsetMs
|
|
7827
|
+
}) != null;
|
|
7828
|
+
var splitExchangeMatchesByRuntimeOrderStatus = ({
|
|
7829
|
+
matched,
|
|
7830
|
+
strategyNameByOrderLinkKey,
|
|
7831
|
+
runtimeSignals,
|
|
7832
|
+
runtimeSignalEvaluations,
|
|
7833
|
+
toleranceMs,
|
|
7834
|
+
signalTimestampOffsetMs
|
|
7835
|
+
}) => {
|
|
7836
|
+
const orderFailed = [];
|
|
7837
|
+
const completed = matched.filter((item) => {
|
|
7838
|
+
const strategyName = resolveReplayStrategyNameFromExchangeEntry({
|
|
7839
|
+
exchangeEntry: item.exchange,
|
|
7840
|
+
strategyNameByOrderLinkKey
|
|
7841
|
+
});
|
|
7842
|
+
if (!strategyName) return true;
|
|
7843
|
+
const outcome = findExchangeRuntimeOutcome({
|
|
7844
|
+
exchangeEntry: item.exchange,
|
|
7845
|
+
strategyName,
|
|
7846
|
+
runtimeSignals,
|
|
7847
|
+
runtimeSignalEvaluations,
|
|
7848
|
+
toleranceMs,
|
|
7849
|
+
signalTimestampOffsetMs
|
|
7850
|
+
});
|
|
7851
|
+
if (outcome?.orderStatus !== "failed") return true;
|
|
7852
|
+
orderFailed.push({
|
|
7853
|
+
...item,
|
|
7854
|
+
reason: outcome.reason || "orderStatus=failed"
|
|
7855
|
+
});
|
|
7856
|
+
return false;
|
|
7857
|
+
});
|
|
7858
|
+
return { completed, orderFailed };
|
|
7662
7859
|
};
|
|
7663
|
-
var firstString = (values) => values.find((value) => typeof value === "string" && value.trim());
|
|
7664
7860
|
var summarizeBacktestPnlByStrategy = (entries) => {
|
|
7665
7861
|
const totals = /* @__PURE__ */ new Map();
|
|
7666
7862
|
for (const entry of entries) {
|
|
@@ -7669,6 +7865,231 @@ var summarizeBacktestPnlByStrategy = (entries) => {
|
|
|
7669
7865
|
}
|
|
7670
7866
|
return totals;
|
|
7671
7867
|
};
|
|
7868
|
+
var buildExchangeComparisonRows = ({
|
|
7869
|
+
liveStrategySummaries,
|
|
7870
|
+
backtestEntries,
|
|
7871
|
+
matched,
|
|
7872
|
+
orderFailed,
|
|
7873
|
+
exchangeOnly,
|
|
7874
|
+
strategyNameByOrderLinkKey
|
|
7875
|
+
}) => {
|
|
7876
|
+
const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(backtestEntries);
|
|
7877
|
+
const rowByStrategy = /* @__PURE__ */ new Map();
|
|
7878
|
+
const ensureRow = (strategyName) => {
|
|
7879
|
+
const existing = rowByStrategy.get(strategyName);
|
|
7880
|
+
if (existing) return existing;
|
|
7881
|
+
const next = {
|
|
7882
|
+
strategyName,
|
|
7883
|
+
backtestEntries: 0,
|
|
7884
|
+
backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
|
|
7885
|
+
runtimeTrades: 0,
|
|
7886
|
+
runtimePnl: 0,
|
|
7887
|
+
matched: 0,
|
|
7888
|
+
orderFailed: 0,
|
|
7889
|
+
runtimeOnly: 0,
|
|
7890
|
+
backtestOnly: 0
|
|
7891
|
+
};
|
|
7892
|
+
rowByStrategy.set(strategyName, next);
|
|
7893
|
+
return next;
|
|
7894
|
+
};
|
|
7895
|
+
for (const summary of liveStrategySummaries) ensureRow(summary.strategyName);
|
|
7896
|
+
for (const entry of backtestEntries) {
|
|
7897
|
+
ensureRow(entry.strategy).backtestEntries += 1;
|
|
7898
|
+
}
|
|
7899
|
+
for (const item of matched) {
|
|
7900
|
+
const row = ensureRow(item.backtest.strategy);
|
|
7901
|
+
row.runtimeTrades += 1;
|
|
7902
|
+
row.matched += 1;
|
|
7903
|
+
if (typeof item.exchange.closedPnl === "number" && Number.isFinite(item.exchange.closedPnl)) {
|
|
7904
|
+
row.runtimePnl += item.exchange.closedPnl;
|
|
7905
|
+
}
|
|
7906
|
+
}
|
|
7907
|
+
for (const item of orderFailed) {
|
|
7908
|
+
const row = ensureRow(item.backtest.strategy);
|
|
7909
|
+
row.runtimeTrades += 1;
|
|
7910
|
+
row.orderFailed += 1;
|
|
7911
|
+
}
|
|
7912
|
+
const matchedBacktestEntries = /* @__PURE__ */ new Set([
|
|
7913
|
+
...matched.map(({ backtest }) => backtest),
|
|
7914
|
+
...orderFailed.map(({ backtest }) => backtest)
|
|
7915
|
+
]);
|
|
7916
|
+
for (const entry of backtestEntries) {
|
|
7917
|
+
if (!matchedBacktestEntries.has(entry)) {
|
|
7918
|
+
ensureRow(entry.strategy).backtestOnly += 1;
|
|
7919
|
+
}
|
|
7920
|
+
}
|
|
7921
|
+
for (const entry of exchangeOnly) {
|
|
7922
|
+
const strategyName = resolveReplayStrategyNameFromExchangeEntry({
|
|
7923
|
+
exchangeEntry: entry,
|
|
7924
|
+
strategyNameByOrderLinkKey
|
|
7925
|
+
}) ?? "[exchange-unmatched]";
|
|
7926
|
+
const row = ensureRow(strategyName);
|
|
7927
|
+
row.runtimeTrades += 1;
|
|
7928
|
+
row.runtimeOnly += 1;
|
|
7929
|
+
if (typeof entry.closedPnl === "number" && Number.isFinite(entry.closedPnl)) {
|
|
7930
|
+
row.runtimePnl += entry.closedPnl;
|
|
7931
|
+
}
|
|
7932
|
+
}
|
|
7933
|
+
return [...rowByStrategy.values()].map((row) => ({
|
|
7934
|
+
...row,
|
|
7935
|
+
runtimePnl: Number(row.runtimePnl.toFixed(2))
|
|
7936
|
+
})).sort((left, right) => left.strategyName.localeCompare(right.strategyName));
|
|
7937
|
+
};
|
|
7938
|
+
var buildRuntimeComparisonRows = ({
|
|
7939
|
+
liveStrategySummaries,
|
|
7940
|
+
runtimeSummaries,
|
|
7941
|
+
parityRows,
|
|
7942
|
+
backtestEntries
|
|
7943
|
+
}) => {
|
|
7944
|
+
const runtimeSummaryByStrategy = new Map(
|
|
7945
|
+
runtimeSummaries.map((summary) => [summary.strategyName, summary])
|
|
7946
|
+
);
|
|
7947
|
+
const parityByStrategy = new Map(parityRows);
|
|
7948
|
+
const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(backtestEntries);
|
|
7949
|
+
const strategyNames = /* @__PURE__ */ new Set([
|
|
7950
|
+
...liveStrategySummaries.map(({ strategyName }) => strategyName),
|
|
7951
|
+
...runtimeSummaryByStrategy.keys(),
|
|
7952
|
+
...parityByStrategy.keys()
|
|
7953
|
+
]);
|
|
7954
|
+
return [...strategyNames].sort((left, right) => left.localeCompare(right)).map((strategyName) => {
|
|
7955
|
+
const runtimeSummary = runtimeSummaryByStrategy.get(strategyName);
|
|
7956
|
+
const parity = parityByStrategy.get(strategyName);
|
|
7957
|
+
return {
|
|
7958
|
+
strategyName,
|
|
7959
|
+
backtestEntries: parity?.backtest ?? 0,
|
|
7960
|
+
backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
|
|
7961
|
+
runtimeTrades: runtimeSummary?.trades ?? 0,
|
|
7962
|
+
runtimePnl: runtimeSummary?.totalPnl ?? 0,
|
|
7963
|
+
matched: parity?.matched ?? 0,
|
|
7964
|
+
orderFailed: 0,
|
|
7965
|
+
runtimeOnly: parity?.runtimeOnly ?? 0,
|
|
7966
|
+
backtestOnly: parity?.backtestOnly ?? 0
|
|
7967
|
+
};
|
|
7968
|
+
});
|
|
7969
|
+
};
|
|
7970
|
+
|
|
7971
|
+
// src/lib/replay/runtimeComparisonReporting.ts
|
|
7972
|
+
var import_chalk11 = __toESM(require("chalk"));
|
|
7973
|
+
var import_time6 = require("@tradejs/core/time");
|
|
7974
|
+
var formatDrilldownSummary = (summary) => Object.entries(summary).filter(([, count]) => count > 0).map(([classification, count]) => `${classification}=${count}`).join(", ");
|
|
7975
|
+
var colorizeRows = (rows) => rows.map((row) => {
|
|
7976
|
+
const btPnlColor = row.backtestNetProfit > 0 ? import_chalk11.default.green : row.backtestNetProfit < 0 ? import_chalk11.default.red : import_chalk11.default.gray;
|
|
7977
|
+
const rtPnlColor = row.runtimePnl > 0 ? import_chalk11.default.green : row.runtimePnl < 0 ? import_chalk11.default.red : import_chalk11.default.gray;
|
|
7978
|
+
return [
|
|
7979
|
+
import_chalk11.default.blue(row.strategyName),
|
|
7980
|
+
import_chalk11.default.cyan(String(row.backtestEntries)),
|
|
7981
|
+
btPnlColor(`${row.backtestNetProfit.toFixed(2)}$`),
|
|
7982
|
+
import_chalk11.default.yellow(String(row.runtimeTrades)),
|
|
7983
|
+
rtPnlColor(`${row.runtimePnl.toFixed(2)}$`),
|
|
7984
|
+
import_chalk11.default.green(String(row.matched)),
|
|
7985
|
+
import_chalk11.default.red(String(row.orderFailed)),
|
|
7986
|
+
import_chalk11.default.yellow(String(row.runtimeOnly)),
|
|
7987
|
+
import_chalk11.default.magenta(String(row.backtestOnly)),
|
|
7988
|
+
import_chalk11.default.red(String(getReplayRuntimeUnmatchedCount(row)))
|
|
7989
|
+
];
|
|
7990
|
+
});
|
|
7991
|
+
var buildReplayComparisonReport = (context) => {
|
|
7992
|
+
const source = context.mode === "exchange" ? "EXCHANGE" : "RUNTIME";
|
|
7993
|
+
const inference = context.mode === "exchange" ? ", inferredStrategy=orderLinkId | nearest backtest entry" : "";
|
|
7994
|
+
const runtimeOnlyLabel = context.mode === "exchange" ? "exchangeOnly" : "runtimeOnly";
|
|
7995
|
+
const runtimeOnlyDrilldownSummary = formatDrilldownSummary(
|
|
7996
|
+
context.details?.mismatchDrilldown?.summary.runtimeOnly ?? {}
|
|
7997
|
+
);
|
|
7998
|
+
const backtestOnlyDrilldownSummary = formatDrilldownSummary(
|
|
7999
|
+
context.details?.mismatchDrilldown?.summary.backtestOnly ?? {}
|
|
8000
|
+
);
|
|
8001
|
+
const lines = [
|
|
8002
|
+
"",
|
|
8003
|
+
`SIGNALS REPLAY VS ${source} BY STRATEGY (connector=${context.connectorName}${inference}, tolerance=${formatReplayRuntimeCompareTolerance()})`,
|
|
8004
|
+
createTable(REPLAY_RUNTIME_COMPARISON_HEADERS, colorizeRows(context.rows))
|
|
8005
|
+
];
|
|
8006
|
+
if (runtimeOnlyDrilldownSummary || backtestOnlyDrilldownSummary) {
|
|
8007
|
+
lines.push(
|
|
8008
|
+
import_chalk11.default.gray(
|
|
8009
|
+
`Mismatch drilldown: ${runtimeOnlyLabel}=[${runtimeOnlyDrilldownSummary || "none"}], backtestOnly=[${backtestOnlyDrilldownSummary || "none"}]`
|
|
8010
|
+
)
|
|
8011
|
+
);
|
|
8012
|
+
}
|
|
8013
|
+
lines.push("");
|
|
8014
|
+
return lines;
|
|
8015
|
+
};
|
|
8016
|
+
var buildNoExchangeEntriesReport = ({
|
|
8017
|
+
connectorName,
|
|
8018
|
+
window
|
|
8019
|
+
}) => [
|
|
8020
|
+
"",
|
|
8021
|
+
import_chalk11.default.yellow(
|
|
8022
|
+
`SIGNALS REPLAY VS EXCHANGE: no lineage-linked exchange entry executions found for ${connectorName} in ${(0, import_time6.formatUnix)(
|
|
8023
|
+
window.start
|
|
8024
|
+
)} -> ${(0, import_time6.formatUnix)(window.end)}`
|
|
8025
|
+
),
|
|
8026
|
+
""
|
|
8027
|
+
];
|
|
8028
|
+
var buildNoRuntimeTradesReport = ({
|
|
8029
|
+
connectorName,
|
|
8030
|
+
window
|
|
8031
|
+
}) => [
|
|
8032
|
+
"",
|
|
8033
|
+
import_chalk11.default.yellow(
|
|
8034
|
+
`SIGNALS REPLAY VS RUNTIME: no local runtime trades found for ${connectorName} in ${(0, import_time6.formatUnix)(
|
|
8035
|
+
window.start
|
|
8036
|
+
)} -> ${(0, import_time6.formatUnix)(window.end)} with matching lineage; checking lineage-linked exchange executions`
|
|
8037
|
+
),
|
|
8038
|
+
""
|
|
8039
|
+
];
|
|
8040
|
+
var writeReplayComparisonReport = (lines) => {
|
|
8041
|
+
for (const line of lines) {
|
|
8042
|
+
console.log(line);
|
|
8043
|
+
}
|
|
8044
|
+
};
|
|
8045
|
+
|
|
8046
|
+
// src/lib/replay/runtimeComparison.ts
|
|
8047
|
+
var getReplayEntryTimestampCompareOffsetMs = () => {
|
|
8048
|
+
const intervalMinutes = Number(replayInterval);
|
|
8049
|
+
return Number.isFinite(intervalMinutes) && intervalMinutes > 0 ? intervalMinutes * 60 * 1e3 : 15 * 60 * 1e3;
|
|
8050
|
+
};
|
|
8051
|
+
var buildReplaySignalEvaluations = (signals) => signals.map((signal) => ({
|
|
8052
|
+
evaluationId: `${signal.strategy}:${signal.symbol}:${signal.timestamp}`,
|
|
8053
|
+
userName: replayUserName,
|
|
8054
|
+
strategy: signal.strategy,
|
|
8055
|
+
symbol: signal.symbol,
|
|
8056
|
+
interval: signal.interval,
|
|
8057
|
+
timestamp: signal.timestamp,
|
|
8058
|
+
evaluatedAt: signal.timestamp,
|
|
8059
|
+
status: "signal",
|
|
8060
|
+
reason: signal.orderSkipReason || signal.orderStatus || "SIGNAL",
|
|
8061
|
+
signalId: signal.signalId,
|
|
8062
|
+
direction: signal.direction,
|
|
8063
|
+
accountId: signal.accountId,
|
|
8064
|
+
deploymentId: signal.deploymentId,
|
|
8065
|
+
runtimeLineage: signal.runtimeLineage,
|
|
8066
|
+
orderStatus: signal.orderStatus,
|
|
8067
|
+
orderSkipReason: signal.orderSkipReason,
|
|
8068
|
+
...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
|
|
8069
|
+
...signal.ml ? { ml: signal.ml } : {}
|
|
8070
|
+
}));
|
|
8071
|
+
var toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
8072
|
+
var sumOptionalNumbers = (values) => {
|
|
8073
|
+
let sum = 0;
|
|
8074
|
+
let hasValue = false;
|
|
8075
|
+
for (const value of values) {
|
|
8076
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
8077
|
+
continue;
|
|
8078
|
+
}
|
|
8079
|
+
sum += value;
|
|
8080
|
+
hasValue = true;
|
|
8081
|
+
}
|
|
8082
|
+
return hasValue ? Number(sum.toFixed(12)) : null;
|
|
8083
|
+
};
|
|
8084
|
+
var firstFiniteNumber = (values) => {
|
|
8085
|
+
for (const value of values) {
|
|
8086
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
8087
|
+
return value;
|
|
8088
|
+
}
|
|
8089
|
+
}
|
|
8090
|
+
return null;
|
|
8091
|
+
};
|
|
8092
|
+
var firstString = (values) => values.find((value) => typeof value === "string" && value.trim());
|
|
7672
8093
|
var getExchangeExecutionMergeKey = (entry) => {
|
|
7673
8094
|
const orderId = firstString([entry.orderId, entry.orderLinkId]);
|
|
7674
8095
|
return orderId ? `${entry.symbol}::${entry.direction}::${orderId}` : null;
|
|
@@ -7736,276 +8157,103 @@ var mergeExchangeEntryExecutions = (rows) => {
|
|
|
7736
8157
|
);
|
|
7737
8158
|
}
|
|
7738
8159
|
return mergedRows.sort(
|
|
7739
|
-
(left, right) => left.entryTimestamp - right.entryTimestamp
|
|
7740
|
-
);
|
|
7741
|
-
};
|
|
7742
|
-
var loadExchangeEntryRows = async ({
|
|
7743
|
-
connector,
|
|
7744
|
-
startTime,
|
|
7745
|
-
endTime
|
|
7746
|
-
}) => {
|
|
7747
|
-
if (typeof connector.getEntryExecutions !== "function") {
|
|
7748
|
-
console.log(
|
|
7749
|
-
import_chalk11.default.yellow(
|
|
7750
|
-
"runtime compare: connector does not support entry execution history"
|
|
7751
|
-
)
|
|
7752
|
-
);
|
|
7753
|
-
return [];
|
|
7754
|
-
}
|
|
7755
|
-
const rows = [];
|
|
7756
|
-
const chunks = splitExchangeHistoryTimeRange({ startTime, endTime });
|
|
7757
|
-
for (const chunk of chunks) {
|
|
7758
|
-
try {
|
|
7759
|
-
const chunkRows = await connector.getEntryExecutions({
|
|
7760
|
-
startTime: chunk.startTime,
|
|
7761
|
-
endTime: chunk.endTime,
|
|
7762
|
-
limit: 100
|
|
7763
|
-
});
|
|
7764
|
-
if (chunkRows.length >= 100) {
|
|
7765
|
-
console.log(
|
|
7766
|
-
import_chalk11.default.yellow(
|
|
7767
|
-
"runtime compare: exchange entry executions returned 100 rows (connector cap); older entry trades in this chunk may be truncated"
|
|
7768
|
-
)
|
|
7769
|
-
);
|
|
7770
|
-
}
|
|
7771
|
-
rows.push(...chunkRows);
|
|
7772
|
-
} catch (error) {
|
|
7773
|
-
console.log(
|
|
7774
|
-
import_chalk11.default.yellow(
|
|
7775
|
-
`runtime compare: getEntryExecutions failed: ${formatRuntimeTradeSyncError(error)}`
|
|
7776
|
-
)
|
|
7777
|
-
);
|
|
7778
|
-
}
|
|
7779
|
-
}
|
|
7780
|
-
return mergeExchangeEntryExecutions(rows);
|
|
7781
|
-
};
|
|
7782
|
-
var buildPriceDeltaPct3 = (leftPrice, rightPrice) => {
|
|
7783
|
-
if (leftPrice == null || rightPrice == null || !Number.isFinite(leftPrice) || !Number.isFinite(rightPrice) || leftPrice === 0) {
|
|
7784
|
-
return null;
|
|
7785
|
-
}
|
|
7786
|
-
return Math.abs((rightPrice - leftPrice) / leftPrice * 100);
|
|
7787
|
-
};
|
|
7788
|
-
var loadExchangeEntriesForComparison = async ({
|
|
7789
|
-
connector,
|
|
7790
|
-
startTime,
|
|
7791
|
-
endTime
|
|
7792
|
-
}) => {
|
|
7793
|
-
const entryRows = await loadExchangeEntryRows({
|
|
7794
|
-
connector,
|
|
7795
|
-
startTime,
|
|
7796
|
-
endTime
|
|
7797
|
-
});
|
|
7798
|
-
const closedPnlRows = await loadClosedPnlRows({
|
|
7799
|
-
connector,
|
|
7800
|
-
startTime,
|
|
7801
|
-
endTime,
|
|
7802
|
-
callbacks: {
|
|
7803
|
-
onUnsupported: () => {
|
|
7804
|
-
console.log(
|
|
7805
|
-
import_chalk11.default.yellow(
|
|
7806
|
-
"runtime compare: connector does not support getClosedPnl, using runtime trade records as-is"
|
|
7807
|
-
)
|
|
7808
|
-
);
|
|
7809
|
-
},
|
|
7810
|
-
onCapped: () => {
|
|
7811
|
-
console.log(
|
|
7812
|
-
import_chalk11.default.yellow(
|
|
7813
|
-
"runtime compare: exchange closed pnl returned 100 rows (connector cap); older closed trades in the window may be truncated"
|
|
7814
|
-
)
|
|
7815
|
-
);
|
|
7816
|
-
},
|
|
7817
|
-
onError: (error) => {
|
|
7818
|
-
console.log(
|
|
7819
|
-
import_chalk11.default.yellow(
|
|
7820
|
-
`runtime compare: getClosedPnl failed: ${formatRuntimeTradeSyncError(error)}`
|
|
7821
|
-
)
|
|
7822
|
-
);
|
|
7823
|
-
}
|
|
7824
|
-
}
|
|
7825
|
-
});
|
|
7826
|
-
const closedPnlByOrderId = new Map(
|
|
7827
|
-
closedPnlRows.filter((row) => typeof row.orderId === "string" && row.orderId.trim()).map((row) => [row.orderId, row])
|
|
7828
|
-
);
|
|
7829
|
-
return entryRows.map((entry) => {
|
|
7830
|
-
const closedPnl = typeof entry.orderId === "string" ? closedPnlByOrderId.get(entry.orderId) : null;
|
|
7831
|
-
return {
|
|
7832
|
-
...entry,
|
|
7833
|
-
exitPrice: closedPnl?.exitPrice ?? entry.exitPrice ?? null,
|
|
7834
|
-
exitTimestamp: closedPnl?.closedAt ?? entry.exitTimestamp ?? null,
|
|
7835
|
-
closedPnl: closedPnl?.closedPnl ?? entry.closedPnl ?? null,
|
|
7836
|
-
openFee: closedPnl?.openFee ?? entry.openFee ?? null,
|
|
7837
|
-
closeFee: closedPnl?.closeFee ?? entry.closeFee ?? null,
|
|
7838
|
-
fundingFee: closedPnl?.fundingFee ?? entry.fundingFee ?? null,
|
|
7839
|
-
totalFee: closedPnl?.totalFee ?? entry.totalFee ?? null
|
|
7840
|
-
};
|
|
7841
|
-
});
|
|
7842
|
-
};
|
|
7843
|
-
var compareExchangeEntriesToBacktest = ({
|
|
7844
|
-
exchangeEntries,
|
|
7845
|
-
backtestEntries,
|
|
7846
|
-
toleranceMs,
|
|
7847
|
-
backtestTimestampOffsetMs = 0
|
|
7848
|
-
}) => {
|
|
7849
|
-
const groupedExchange = /* @__PURE__ */ new Map();
|
|
7850
|
-
const groupedBacktest = /* @__PURE__ */ new Map();
|
|
7851
|
-
for (const entry of exchangeEntries) {
|
|
7852
|
-
const key = `${entry.symbol}::${entry.direction}`;
|
|
7853
|
-
const bucket = groupedExchange.get(key) ?? [];
|
|
7854
|
-
bucket.push(entry);
|
|
7855
|
-
groupedExchange.set(key, bucket);
|
|
7856
|
-
}
|
|
7857
|
-
for (const entry of backtestEntries) {
|
|
7858
|
-
const key = `${entry.symbol}::${entry.direction}`;
|
|
7859
|
-
const bucket = groupedBacktest.get(key) ?? [];
|
|
7860
|
-
bucket.push(entry);
|
|
7861
|
-
groupedBacktest.set(key, bucket);
|
|
7862
|
-
}
|
|
7863
|
-
const matched = [];
|
|
7864
|
-
const exchangeOnly = [];
|
|
7865
|
-
const backtestOnly = [];
|
|
7866
|
-
const matchedBacktestEntries = /* @__PURE__ */ new Set();
|
|
7867
|
-
const groupKeys = /* @__PURE__ */ new Set([
|
|
7868
|
-
...groupedExchange.keys(),
|
|
7869
|
-
...groupedBacktest.keys()
|
|
7870
|
-
]);
|
|
7871
|
-
for (const key of groupKeys) {
|
|
7872
|
-
const exchangeGroup = [...groupedExchange.get(key) ?? []].sort(
|
|
7873
|
-
(left, right) => left.entryTimestamp - right.entryTimestamp
|
|
7874
|
-
);
|
|
7875
|
-
const availableBacktest = [...groupedBacktest.get(key) ?? []].sort(
|
|
7876
|
-
(left, right) => left.timestamp - right.timestamp
|
|
7877
|
-
);
|
|
7878
|
-
const unmatchedBacktest = availableBacktest.map((entry) => ({
|
|
7879
|
-
entry,
|
|
7880
|
-
used: false
|
|
7881
|
-
}));
|
|
7882
|
-
for (const exchangeEntry of exchangeGroup) {
|
|
7883
|
-
let bestIndex = -1;
|
|
7884
|
-
let bestDiff = Number.POSITIVE_INFINITY;
|
|
7885
|
-
for (let index = 0; index < unmatchedBacktest.length; index += 1) {
|
|
7886
|
-
const candidate = unmatchedBacktest[index];
|
|
7887
|
-
if (candidate.used) {
|
|
7888
|
-
continue;
|
|
7889
|
-
}
|
|
7890
|
-
const diff = Math.abs(
|
|
7891
|
-
getBacktestParityComparisonTimestamp(
|
|
7892
|
-
candidate.entry,
|
|
7893
|
-
backtestTimestampOffsetMs
|
|
7894
|
-
) - exchangeEntry.entryTimestamp
|
|
7895
|
-
);
|
|
7896
|
-
if (diff > toleranceMs || diff >= bestDiff) {
|
|
7897
|
-
continue;
|
|
7898
|
-
}
|
|
7899
|
-
bestIndex = index;
|
|
7900
|
-
bestDiff = diff;
|
|
7901
|
-
}
|
|
7902
|
-
if (bestIndex < 0) {
|
|
7903
|
-
exchangeOnly.push(exchangeEntry);
|
|
7904
|
-
continue;
|
|
7905
|
-
}
|
|
7906
|
-
unmatchedBacktest[bestIndex].used = true;
|
|
7907
|
-
const backtestEntry = unmatchedBacktest[bestIndex].entry;
|
|
7908
|
-
matchedBacktestEntries.add(backtestEntry);
|
|
7909
|
-
matched.push({
|
|
7910
|
-
exchange: exchangeEntry,
|
|
7911
|
-
backtest: backtestEntry,
|
|
7912
|
-
timestampDiffMs: bestDiff,
|
|
7913
|
-
priceDeltaPct: buildPriceDeltaPct3(
|
|
7914
|
-
exchangeEntry.entryPrice,
|
|
7915
|
-
backtestEntry.price
|
|
7916
|
-
)
|
|
7917
|
-
});
|
|
7918
|
-
}
|
|
7919
|
-
}
|
|
7920
|
-
for (const entry of backtestEntries) {
|
|
7921
|
-
if (!matchedBacktestEntries.has(entry)) {
|
|
7922
|
-
backtestOnly.push(entry);
|
|
7923
|
-
}
|
|
7924
|
-
}
|
|
7925
|
-
matched.sort(
|
|
7926
|
-
(left, right) => left.exchange.entryTimestamp - right.exchange.entryTimestamp || left.backtest.strategy.localeCompare(right.backtest.strategy)
|
|
7927
|
-
);
|
|
7928
|
-
exchangeOnly.sort(
|
|
7929
|
-
(left, right) => left.entryTimestamp - right.entryTimestamp
|
|
7930
|
-
);
|
|
7931
|
-
backtestOnly.sort((left, right) => left.timestamp - right.timestamp);
|
|
7932
|
-
return {
|
|
7933
|
-
matched,
|
|
7934
|
-
exchangeOnly,
|
|
7935
|
-
backtestOnly
|
|
7936
|
-
};
|
|
7937
|
-
};
|
|
7938
|
-
var findExchangeRuntimeOutcome = ({
|
|
7939
|
-
exchangeEntry,
|
|
7940
|
-
strategyName,
|
|
7941
|
-
runtimeSignals,
|
|
7942
|
-
runtimeSignalEvaluations,
|
|
7943
|
-
toleranceMs,
|
|
7944
|
-
signalTimestampOffsetMs
|
|
8160
|
+
(left, right) => left.entryTimestamp - right.entryTimestamp
|
|
8161
|
+
);
|
|
8162
|
+
};
|
|
8163
|
+
var loadExchangeEntryRows = async ({
|
|
8164
|
+
connector,
|
|
8165
|
+
startTime,
|
|
8166
|
+
endTime
|
|
7945
8167
|
}) => {
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
});
|
|
7954
|
-
}
|
|
8168
|
+
if (typeof connector.getEntryExecutions !== "function") {
|
|
8169
|
+
console.log(
|
|
8170
|
+
import_chalk12.default.yellow(
|
|
8171
|
+
"runtime compare: connector does not support entry execution history"
|
|
8172
|
+
)
|
|
8173
|
+
);
|
|
8174
|
+
return [];
|
|
7955
8175
|
}
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
8176
|
+
const rows = [];
|
|
8177
|
+
const chunks = splitExchangeHistoryTimeRange({ startTime, endTime });
|
|
8178
|
+
for (const chunk of chunks) {
|
|
8179
|
+
try {
|
|
8180
|
+
const chunkRows = await connector.getEntryExecutions({
|
|
8181
|
+
startTime: chunk.startTime,
|
|
8182
|
+
endTime: chunk.endTime,
|
|
8183
|
+
limit: 100
|
|
7962
8184
|
});
|
|
8185
|
+
if (chunkRows.length >= 100) {
|
|
8186
|
+
console.log(
|
|
8187
|
+
import_chalk12.default.yellow(
|
|
8188
|
+
"runtime compare: exchange entry executions returned 100 rows (connector cap); older entry trades in this chunk may be truncated"
|
|
8189
|
+
)
|
|
8190
|
+
);
|
|
8191
|
+
}
|
|
8192
|
+
rows.push(...chunkRows);
|
|
8193
|
+
} catch (error) {
|
|
8194
|
+
console.log(
|
|
8195
|
+
import_chalk12.default.yellow(
|
|
8196
|
+
`runtime compare: getEntryExecutions failed: ${formatRuntimeTradeSyncError(error)}`
|
|
8197
|
+
)
|
|
8198
|
+
);
|
|
7963
8199
|
}
|
|
7964
8200
|
}
|
|
7965
|
-
return
|
|
7966
|
-
...candidate,
|
|
7967
|
-
timestampDiffMs: Math.abs(
|
|
7968
|
-
candidate.timestamp + signalTimestampOffsetMs - exchangeEntry.entryTimestamp
|
|
7969
|
-
)
|
|
7970
|
-
})).filter((candidate) => candidate.timestampDiffMs <= toleranceMs).sort(
|
|
7971
|
-
(left, right) => left.timestampDiffMs - right.timestampDiffMs || Number(right.orderStatus === "failed") - Number(left.orderStatus === "failed")
|
|
7972
|
-
)[0] ?? null;
|
|
8201
|
+
return mergeExchangeEntryExecutions(rows);
|
|
7973
8202
|
};
|
|
7974
|
-
var
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
runtimeSignalEvaluations,
|
|
7979
|
-
toleranceMs,
|
|
7980
|
-
signalTimestampOffsetMs
|
|
8203
|
+
var loadExchangeEntriesForComparison = async ({
|
|
8204
|
+
connector,
|
|
8205
|
+
startTime,
|
|
8206
|
+
endTime
|
|
7981
8207
|
}) => {
|
|
7982
|
-
const
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8208
|
+
const entryRows = await loadExchangeEntryRows({
|
|
8209
|
+
connector,
|
|
8210
|
+
startTime,
|
|
8211
|
+
endTime
|
|
8212
|
+
});
|
|
8213
|
+
const closedPnlRows = await loadClosedPnlRows({
|
|
8214
|
+
connector,
|
|
8215
|
+
startTime,
|
|
8216
|
+
endTime,
|
|
8217
|
+
callbacks: {
|
|
8218
|
+
onUnsupported: () => {
|
|
8219
|
+
console.log(
|
|
8220
|
+
import_chalk12.default.yellow(
|
|
8221
|
+
"runtime compare: connector does not support getClosedPnl, using runtime trade records as-is"
|
|
8222
|
+
)
|
|
8223
|
+
);
|
|
8224
|
+
},
|
|
8225
|
+
onCapped: () => {
|
|
8226
|
+
console.log(
|
|
8227
|
+
import_chalk12.default.yellow(
|
|
8228
|
+
"runtime compare: exchange closed pnl returned 100 rows (connector cap); older closed trades in the window may be truncated"
|
|
8229
|
+
)
|
|
8230
|
+
);
|
|
8231
|
+
},
|
|
8232
|
+
onError: (error) => {
|
|
8233
|
+
console.log(
|
|
8234
|
+
import_chalk12.default.yellow(
|
|
8235
|
+
`runtime compare: getClosedPnl failed: ${formatRuntimeTradeSyncError(error)}`
|
|
8236
|
+
)
|
|
8237
|
+
);
|
|
8238
|
+
}
|
|
8001
8239
|
}
|
|
8002
|
-
orderFailed.push({
|
|
8003
|
-
...item,
|
|
8004
|
-
reason: outcome.reason || "orderStatus=failed"
|
|
8005
|
-
});
|
|
8006
|
-
return false;
|
|
8007
8240
|
});
|
|
8008
|
-
|
|
8241
|
+
const closedPnlByOrderId = new Map(
|
|
8242
|
+
closedPnlRows.filter((row) => typeof row.orderId === "string" && row.orderId.trim()).map((row) => [row.orderId, row])
|
|
8243
|
+
);
|
|
8244
|
+
return entryRows.map((entry) => {
|
|
8245
|
+
const closedPnl = typeof entry.orderId === "string" ? closedPnlByOrderId.get(entry.orderId) : null;
|
|
8246
|
+
return {
|
|
8247
|
+
...entry,
|
|
8248
|
+
exitPrice: closedPnl?.exitPrice ?? entry.exitPrice ?? null,
|
|
8249
|
+
exitTimestamp: closedPnl?.closedAt ?? entry.exitTimestamp ?? null,
|
|
8250
|
+
closedPnl: closedPnl?.closedPnl ?? entry.closedPnl ?? null,
|
|
8251
|
+
openFee: closedPnl?.openFee ?? entry.openFee ?? null,
|
|
8252
|
+
closeFee: closedPnl?.closeFee ?? entry.closeFee ?? null,
|
|
8253
|
+
fundingFee: closedPnl?.fundingFee ?? entry.fundingFee ?? null,
|
|
8254
|
+
totalFee: closedPnl?.totalFee ?? entry.totalFee ?? null
|
|
8255
|
+
};
|
|
8256
|
+
});
|
|
8009
8257
|
};
|
|
8010
8258
|
var saveAndPrintReplayExchangeComparison = async ({
|
|
8011
8259
|
liveStrategySummaries,
|
|
@@ -8025,7 +8273,6 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8025
8273
|
const strategyNameByOrderLinkKey = buildStrategyNameByOrderLinkKey(
|
|
8026
8274
|
liveStrategySummaries.map((summary) => summary.strategyName)
|
|
8027
8275
|
);
|
|
8028
|
-
const exchangeOutcomeByEntry = /* @__PURE__ */ new Map();
|
|
8029
8276
|
const exchangeEntries = rawExchangeEntries.filter((entry) => {
|
|
8030
8277
|
const strategyName = resolveReplayStrategyNameFromExchangeEntry({
|
|
8031
8278
|
exchangeEntry: entry,
|
|
@@ -8034,7 +8281,7 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8034
8281
|
if (!strategyName) {
|
|
8035
8282
|
return false;
|
|
8036
8283
|
}
|
|
8037
|
-
|
|
8284
|
+
return hasLineageLinkedRuntimeOutcome({
|
|
8038
8285
|
exchangeEntry: entry,
|
|
8039
8286
|
strategyName,
|
|
8040
8287
|
runtimeSignals,
|
|
@@ -8042,24 +8289,18 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8042
8289
|
toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
|
|
8043
8290
|
signalTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
|
|
8044
8291
|
});
|
|
8045
|
-
exchangeOutcomeByEntry.set(entry, outcome);
|
|
8046
|
-
return outcome != null;
|
|
8047
8292
|
});
|
|
8048
8293
|
const comparisonLineage = {
|
|
8049
8294
|
...lineage,
|
|
8050
8295
|
excludedExchangeEntries: lineage.excludedExchangeEntries + rawExchangeEntries.length - exchangeEntries.length
|
|
8051
8296
|
};
|
|
8052
|
-
const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(backtestEntries);
|
|
8053
8297
|
if (!exchangeEntries.length) {
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
)} -> ${(0, import_time6.formatUnix)(window.end)}`
|
|
8060
|
-
)
|
|
8298
|
+
writeReplayComparisonReport(
|
|
8299
|
+
buildNoExchangeEntriesReport({
|
|
8300
|
+
connectorName,
|
|
8301
|
+
window
|
|
8302
|
+
})
|
|
8061
8303
|
);
|
|
8062
|
-
console.log("");
|
|
8063
8304
|
const details2 = buildReplayExchangeComparisonDetails({
|
|
8064
8305
|
matched: [],
|
|
8065
8306
|
exchangeOnly: [],
|
|
@@ -8074,6 +8315,16 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8074
8315
|
replaySignals,
|
|
8075
8316
|
replaySignalEvaluations
|
|
8076
8317
|
});
|
|
8318
|
+
const emptyRowsByStrategy = new Map(
|
|
8319
|
+
buildExchangeComparisonRows({
|
|
8320
|
+
liveStrategySummaries,
|
|
8321
|
+
backtestEntries,
|
|
8322
|
+
matched: [],
|
|
8323
|
+
orderFailed: [],
|
|
8324
|
+
exchangeOnly: [],
|
|
8325
|
+
strategyNameByOrderLinkKey
|
|
8326
|
+
}).map((row) => [row.strategyName, row])
|
|
8327
|
+
);
|
|
8077
8328
|
return {
|
|
8078
8329
|
mode: "exchange",
|
|
8079
8330
|
syncedTradesCount: 0,
|
|
@@ -8086,21 +8337,9 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8086
8337
|
backtestOnlyCount: backtestEntries.length,
|
|
8087
8338
|
details: details2,
|
|
8088
8339
|
lineage: comparisonLineage,
|
|
8089
|
-
rows: liveStrategySummaries.map(
|
|
8090
|
-
strategyName
|
|
8091
|
-
|
|
8092
|
-
(entry) => entry.strategy === summary.strategyName
|
|
8093
|
-
).length,
|
|
8094
|
-
backtestNetProfit: backtestPnlByStrategy.get(summary.strategyName) ?? 0,
|
|
8095
|
-
runtimeTrades: 0,
|
|
8096
|
-
runtimePnl: 0,
|
|
8097
|
-
matched: 0,
|
|
8098
|
-
orderFailed: 0,
|
|
8099
|
-
runtimeOnly: 0,
|
|
8100
|
-
backtestOnly: backtestEntries.filter(
|
|
8101
|
-
(entry) => entry.strategy === summary.strategyName
|
|
8102
|
-
).length
|
|
8103
|
-
}))
|
|
8340
|
+
rows: liveStrategySummaries.map(
|
|
8341
|
+
({ strategyName }) => emptyRowsByStrategy.get(strategyName)
|
|
8342
|
+
)
|
|
8104
8343
|
};
|
|
8105
8344
|
}
|
|
8106
8345
|
const comparison = compareExchangeEntriesToBacktest({
|
|
@@ -8117,62 +8356,6 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8117
8356
|
toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
|
|
8118
8357
|
signalTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
|
|
8119
8358
|
});
|
|
8120
|
-
const rowByStrategy = /* @__PURE__ */ new Map();
|
|
8121
|
-
const ensureRow = (strategyName) => {
|
|
8122
|
-
const existing = rowByStrategy.get(strategyName);
|
|
8123
|
-
if (existing) {
|
|
8124
|
-
return existing;
|
|
8125
|
-
}
|
|
8126
|
-
const next = {
|
|
8127
|
-
strategyName,
|
|
8128
|
-
backtestEntries: 0,
|
|
8129
|
-
backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
|
|
8130
|
-
runtimeTrades: 0,
|
|
8131
|
-
runtimePnl: 0,
|
|
8132
|
-
matched: 0,
|
|
8133
|
-
orderFailed: 0,
|
|
8134
|
-
runtimeOnly: 0,
|
|
8135
|
-
backtestOnly: 0
|
|
8136
|
-
};
|
|
8137
|
-
rowByStrategy.set(strategyName, next);
|
|
8138
|
-
return next;
|
|
8139
|
-
};
|
|
8140
|
-
for (const summary of liveStrategySummaries) {
|
|
8141
|
-
ensureRow(summary.strategyName);
|
|
8142
|
-
}
|
|
8143
|
-
for (const entry of backtestEntries) {
|
|
8144
|
-
ensureRow(entry.strategy).backtestEntries += 1;
|
|
8145
|
-
}
|
|
8146
|
-
for (const item of matched) {
|
|
8147
|
-
const row = ensureRow(item.backtest.strategy);
|
|
8148
|
-
row.runtimeTrades += 1;
|
|
8149
|
-
row.matched += 1;
|
|
8150
|
-
if (typeof item.exchange.closedPnl === "number" && Number.isFinite(item.exchange.closedPnl)) {
|
|
8151
|
-
row.runtimePnl += item.exchange.closedPnl;
|
|
8152
|
-
}
|
|
8153
|
-
}
|
|
8154
|
-
for (const item of orderFailed) {
|
|
8155
|
-
const row = ensureRow(item.backtest.strategy);
|
|
8156
|
-
row.runtimeTrades += 1;
|
|
8157
|
-
row.orderFailed += 1;
|
|
8158
|
-
}
|
|
8159
|
-
for (const entry of comparison.backtestOnly) {
|
|
8160
|
-
ensureRow(entry.strategy).backtestOnly += 1;
|
|
8161
|
-
}
|
|
8162
|
-
if (comparison.exchangeOnly.length) {
|
|
8163
|
-
for (const entry of comparison.exchangeOnly) {
|
|
8164
|
-
const strategyName = resolveReplayStrategyNameFromExchangeEntry({
|
|
8165
|
-
exchangeEntry: entry,
|
|
8166
|
-
strategyNameByOrderLinkKey
|
|
8167
|
-
}) ?? "[exchange-unmatched]";
|
|
8168
|
-
const row = ensureRow(strategyName);
|
|
8169
|
-
row.runtimeTrades += 1;
|
|
8170
|
-
row.runtimeOnly += 1;
|
|
8171
|
-
if (typeof entry.closedPnl === "number" && Number.isFinite(entry.closedPnl)) {
|
|
8172
|
-
row.runtimePnl += entry.closedPnl;
|
|
8173
|
-
}
|
|
8174
|
-
}
|
|
8175
|
-
}
|
|
8176
8359
|
const details = buildReplayExchangeComparisonDetails({
|
|
8177
8360
|
matched,
|
|
8178
8361
|
orderFailed,
|
|
@@ -8188,45 +8371,22 @@ var saveAndPrintReplayExchangeComparison = async ({
|
|
|
8188
8371
|
replaySignals,
|
|
8189
8372
|
replaySignalEvaluations
|
|
8190
8373
|
});
|
|
8191
|
-
const rows =
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
return [
|
|
8199
|
-
import_chalk11.default.blue(row.strategyName),
|
|
8200
|
-
import_chalk11.default.cyan(String(row.backtestEntries)),
|
|
8201
|
-
btPnlColor(`${row.backtestNetProfit.toFixed(2)}$`),
|
|
8202
|
-
import_chalk11.default.yellow(String(row.runtimeTrades)),
|
|
8203
|
-
rtPnlColor(`${row.runtimePnl.toFixed(2)}$`),
|
|
8204
|
-
import_chalk11.default.green(String(row.matched)),
|
|
8205
|
-
import_chalk11.default.red(String(row.orderFailed)),
|
|
8206
|
-
import_chalk11.default.yellow(String(row.runtimeOnly)),
|
|
8207
|
-
import_chalk11.default.magenta(String(row.backtestOnly)),
|
|
8208
|
-
import_chalk11.default.red(String(getReplayRuntimeUnmatchedCount(row)))
|
|
8209
|
-
];
|
|
8374
|
+
const rows = buildExchangeComparisonRows({
|
|
8375
|
+
liveStrategySummaries,
|
|
8376
|
+
backtestEntries,
|
|
8377
|
+
matched,
|
|
8378
|
+
orderFailed,
|
|
8379
|
+
exchangeOnly: comparison.exchangeOnly,
|
|
8380
|
+
strategyNameByOrderLinkKey
|
|
8210
8381
|
});
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
|
|
8216
|
-
|
|
8217
|
-
|
|
8218
|
-
);
|
|
8219
|
-
const backtestOnlyDrilldownSummary = formatDrilldownSummary(
|
|
8220
|
-
details.mismatchDrilldown?.summary.backtestOnly ?? {}
|
|
8382
|
+
writeReplayComparisonReport(
|
|
8383
|
+
buildReplayComparisonReport({
|
|
8384
|
+
mode: "exchange",
|
|
8385
|
+
connectorName,
|
|
8386
|
+
rows,
|
|
8387
|
+
details
|
|
8388
|
+
})
|
|
8221
8389
|
);
|
|
8222
|
-
if (runtimeOnlyDrilldownSummary || backtestOnlyDrilldownSummary) {
|
|
8223
|
-
console.log(
|
|
8224
|
-
import_chalk11.default.gray(
|
|
8225
|
-
`Mismatch drilldown: exchangeOnly=[${runtimeOnlyDrilldownSummary || "none"}], backtestOnly=[${backtestOnlyDrilldownSummary || "none"}]`
|
|
8226
|
-
)
|
|
8227
|
-
);
|
|
8228
|
-
}
|
|
8229
|
-
console.log("");
|
|
8230
8390
|
return {
|
|
8231
8391
|
mode: "exchange",
|
|
8232
8392
|
syncedTradesCount: exchangeEntries.length,
|
|
@@ -8293,7 +8453,7 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8293
8453
|
]);
|
|
8294
8454
|
if (runtimeEvidence) {
|
|
8295
8455
|
console.log(
|
|
8296
|
-
|
|
8456
|
+
import_chalk12.default.gray(
|
|
8297
8457
|
`runtime compare: using immutable evidence ${runtimeEvidence.path}`
|
|
8298
8458
|
)
|
|
8299
8459
|
);
|
|
@@ -8307,7 +8467,7 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8307
8467
|
openPositionCallbacks: {
|
|
8308
8468
|
onError: (error) => {
|
|
8309
8469
|
console.log(
|
|
8310
|
-
|
|
8470
|
+
import_chalk12.default.yellow(
|
|
8311
8471
|
`runtime compare: getOpenPositionPnl failed: ${formatRuntimeTradeSyncError(error)}; continuing without open-position mark prices`
|
|
8312
8472
|
)
|
|
8313
8473
|
);
|
|
@@ -8316,21 +8476,21 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8316
8476
|
closedPnlCallbacks: {
|
|
8317
8477
|
onUnsupported: () => {
|
|
8318
8478
|
console.log(
|
|
8319
|
-
|
|
8479
|
+
import_chalk12.default.yellow(
|
|
8320
8480
|
"runtime compare: connector does not support getClosedPnl, using runtime trade records as-is"
|
|
8321
8481
|
)
|
|
8322
8482
|
);
|
|
8323
8483
|
},
|
|
8324
8484
|
onCapped: () => {
|
|
8325
8485
|
console.log(
|
|
8326
|
-
|
|
8486
|
+
import_chalk12.default.yellow(
|
|
8327
8487
|
"runtime compare: exchange closed pnl returned 100 rows (connector cap); older closed trades in the window may be truncated"
|
|
8328
8488
|
)
|
|
8329
8489
|
);
|
|
8330
8490
|
},
|
|
8331
8491
|
onError: (error) => {
|
|
8332
8492
|
console.log(
|
|
8333
|
-
|
|
8493
|
+
import_chalk12.default.yellow(
|
|
8334
8494
|
`runtime compare: getClosedPnl failed: ${formatRuntimeTradeSyncError(error)}`
|
|
8335
8495
|
)
|
|
8336
8496
|
);
|
|
@@ -8350,15 +8510,9 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8350
8510
|
(trade) => trade.entryTimestamp >= window.start && trade.entryTimestamp < window.end && relevantStrategies.has(trade.strategy)
|
|
8351
8511
|
);
|
|
8352
8512
|
if (!windowRuntimeTrades.length && !runtimeEvidence) {
|
|
8353
|
-
|
|
8354
|
-
|
|
8355
|
-
import_chalk11.default.yellow(
|
|
8356
|
-
`SIGNALS REPLAY VS RUNTIME: no local runtime trades found for ${connectorName} in ${(0, import_time6.formatUnix)(
|
|
8357
|
-
window.start
|
|
8358
|
-
)} -> ${(0, import_time6.formatUnix)(window.end)} with matching lineage; checking lineage-linked exchange executions`
|
|
8359
|
-
)
|
|
8513
|
+
writeReplayComparisonReport(
|
|
8514
|
+
buildNoRuntimeTradesReport({ connectorName, window })
|
|
8360
8515
|
);
|
|
8361
|
-
console.log("");
|
|
8362
8516
|
return saveAndPrintReplayExchangeComparison({
|
|
8363
8517
|
liveStrategySummaries,
|
|
8364
8518
|
backtestEntries: comparableBacktestEntries,
|
|
@@ -8370,9 +8524,6 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8370
8524
|
});
|
|
8371
8525
|
}
|
|
8372
8526
|
const runtimeSummaries = summarizeRuntimeTradesByStrategy(windowRuntimeTrades);
|
|
8373
|
-
const runtimeSummaryByStrategy = new Map(
|
|
8374
|
-
runtimeSummaries.map((summary) => [summary.strategyName, summary])
|
|
8375
|
-
);
|
|
8376
8527
|
const rawRuntimeEntries = extractRuntimeParityEntries(windowRuntimeTrades);
|
|
8377
8528
|
const runtimeDedupe = dedupeRuntimeParityEntries(rawRuntimeEntries);
|
|
8378
8529
|
const comparison = compareTradeParityEntries({
|
|
@@ -8402,68 +8553,20 @@ var saveAndPrintReplayRuntimeComparison = async ({
|
|
|
8402
8553
|
runtimeOnlyEntries: comparison.runtimeOnly,
|
|
8403
8554
|
backtestOnlyEntries: comparison.backtestOnly
|
|
8404
8555
|
});
|
|
8405
|
-
const
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
comparableBacktestEntries
|
|
8411
|
-
);
|
|
8412
|
-
const strategyNames = /* @__PURE__ */ new Set([
|
|
8413
|
-
...liveSummaryByStrategy.keys(),
|
|
8414
|
-
...runtimeSummaryByStrategy.keys(),
|
|
8415
|
-
...parityByStrategy.keys()
|
|
8416
|
-
]);
|
|
8417
|
-
const rows = [...strategyNames].sort((left, right) => left.localeCompare(right)).map((strategyName) => {
|
|
8418
|
-
const runtimeSummary = runtimeSummaryByStrategy.get(strategyName);
|
|
8419
|
-
const parity = parityByStrategy.get(strategyName);
|
|
8420
|
-
return {
|
|
8421
|
-
strategyName,
|
|
8422
|
-
backtestEntries: parity?.backtest ?? 0,
|
|
8423
|
-
backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
|
|
8424
|
-
runtimeTrades: runtimeSummary?.trades ?? 0,
|
|
8425
|
-
runtimePnl: runtimeSummary?.totalPnl ?? 0,
|
|
8426
|
-
matched: parity?.matched ?? 0,
|
|
8427
|
-
orderFailed: 0,
|
|
8428
|
-
runtimeOnly: parity?.runtimeOnly ?? 0,
|
|
8429
|
-
backtestOnly: parity?.backtestOnly ?? 0
|
|
8430
|
-
};
|
|
8431
|
-
});
|
|
8432
|
-
const colorizedRows = rows.map((row) => {
|
|
8433
|
-
const btPnlColor = row.backtestNetProfit > 0 ? import_chalk11.default.green : row.backtestNetProfit < 0 ? import_chalk11.default.red : import_chalk11.default.gray;
|
|
8434
|
-
const rtPnlColor = row.runtimePnl > 0 ? import_chalk11.default.green : row.runtimePnl < 0 ? import_chalk11.default.red : import_chalk11.default.gray;
|
|
8435
|
-
return [
|
|
8436
|
-
import_chalk11.default.blue(row.strategyName),
|
|
8437
|
-
import_chalk11.default.cyan(String(row.backtestEntries)),
|
|
8438
|
-
btPnlColor(`${row.backtestNetProfit.toFixed(2)}$`),
|
|
8439
|
-
import_chalk11.default.yellow(String(row.runtimeTrades)),
|
|
8440
|
-
rtPnlColor(`${row.runtimePnl.toFixed(2)}$`),
|
|
8441
|
-
import_chalk11.default.green(String(row.matched)),
|
|
8442
|
-
import_chalk11.default.red(String(row.orderFailed)),
|
|
8443
|
-
import_chalk11.default.yellow(String(row.runtimeOnly)),
|
|
8444
|
-
import_chalk11.default.magenta(String(row.backtestOnly)),
|
|
8445
|
-
import_chalk11.default.red(String(getReplayRuntimeUnmatchedCount(row)))
|
|
8446
|
-
];
|
|
8556
|
+
const rows = buildRuntimeComparisonRows({
|
|
8557
|
+
liveStrategySummaries,
|
|
8558
|
+
runtimeSummaries,
|
|
8559
|
+
parityRows,
|
|
8560
|
+
backtestEntries: comparableBacktestEntries
|
|
8447
8561
|
});
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
);
|
|
8456
|
-
const backtestOnlyDrilldownSummary = formatDrilldownSummary(
|
|
8457
|
-
details.mismatchDrilldown?.summary.backtestOnly ?? {}
|
|
8562
|
+
writeReplayComparisonReport(
|
|
8563
|
+
buildReplayComparisonReport({
|
|
8564
|
+
mode: "runtime",
|
|
8565
|
+
connectorName,
|
|
8566
|
+
rows,
|
|
8567
|
+
details
|
|
8568
|
+
})
|
|
8458
8569
|
);
|
|
8459
|
-
if (runtimeOnlyDrilldownSummary || backtestOnlyDrilldownSummary) {
|
|
8460
|
-
console.log(
|
|
8461
|
-
import_chalk11.default.gray(
|
|
8462
|
-
`Mismatch drilldown: runtimeOnly=[${runtimeOnlyDrilldownSummary || "none"}], backtestOnly=[${backtestOnlyDrilldownSummary || "none"}]`
|
|
8463
|
-
)
|
|
8464
|
-
);
|
|
8465
|
-
}
|
|
8466
|
-
console.log("");
|
|
8467
8570
|
return {
|
|
8468
8571
|
mode: "runtime",
|
|
8469
8572
|
syncedTradesCount: syncedRuntimeTrades.length,
|
|
@@ -8914,7 +9017,7 @@ var prepareReplayBinanceMarketContext = async (preparedRun) => {
|
|
|
8914
9017
|
aiEnabled: preparedRun.aiEnabled,
|
|
8915
9018
|
mlEnabled: preparedRun.mlEnabled,
|
|
8916
9019
|
strategyNames: preparedRun.strategyNames,
|
|
8917
|
-
log: (message) => console.log(
|
|
9020
|
+
log: (message) => console.log(import_chalk13.default.gray(message))
|
|
8918
9021
|
});
|
|
8919
9022
|
};
|
|
8920
9023
|
var finishReplay = async ({
|
|
@@ -8967,8 +9070,8 @@ var finishReplay = async ({
|
|
|
8967
9070
|
strategySnapshot: replayStrategySnapshot,
|
|
8968
9071
|
runtimeComparison: replayRuntimeComparison
|
|
8969
9072
|
});
|
|
8970
|
-
console.log(
|
|
8971
|
-
console.log(
|
|
9073
|
+
console.log(import_chalk13.default.green(`Replay report: ${outputReport.markdownPath}`));
|
|
9074
|
+
console.log(import_chalk13.default.green(`Replay report JSON: ${outputReport.jsonPath}`));
|
|
8972
9075
|
await (0, import_redis6.setData)(
|
|
8973
9076
|
replayKey,
|
|
8974
9077
|
{
|
|
@@ -9121,9 +9224,9 @@ var replayBacktest = async () => {
|
|
|
9121
9224
|
),
|
|
9122
9225
|
strategyNames: replayStrategies.map(({ strategyName }) => strategyName)
|
|
9123
9226
|
});
|
|
9124
|
-
console.log(
|
|
9227
|
+
console.log(import_chalk13.default.yellow(`tickers: ${preparedRun.tickers.length}`));
|
|
9125
9228
|
console.log(
|
|
9126
|
-
|
|
9229
|
+
import_chalk13.default.gray(
|
|
9127
9230
|
`mode: replay (${replayStrategies.map(({ strategyName }) => strategyName).join(", ")})`
|
|
9128
9231
|
)
|
|
9129
9232
|
);
|