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