@tradejs/cli 3.1.19-beta.234 → 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 CHANGED
@@ -29649,84 +29649,6 @@ var init_runtimeParity = __esm({
29649
29649
  }
29650
29650
  });
29651
29651
 
29652
- // src/lib/paritySummary.ts
29653
- var summarizeRuntimeTradesByStrategy, summarizeTradeParityByStrategy;
29654
- var init_paritySummary = __esm({
29655
- "src/lib/paritySummary.ts"() {
29656
- "use strict";
29657
- summarizeRuntimeTradesByStrategy = (trades) => {
29658
- const summaryByStrategy = /* @__PURE__ */ new Map();
29659
- for (const trade of trades) {
29660
- const summary = summaryByStrategy.get(trade.strategy) ?? {
29661
- strategyName: trade.strategy,
29662
- trades: 0,
29663
- activeTrades: 0,
29664
- closedTrades: 0,
29665
- totalPnl: 0
29666
- };
29667
- summary.trades += 1;
29668
- if (trade.status === "active") {
29669
- summary.activeTrades += 1;
29670
- } else {
29671
- summary.closedTrades += 1;
29672
- }
29673
- const pnl = trade.status === "active" ? trade.currentPnl : trade.closedPnl ?? trade.currentPnl;
29674
- if (typeof pnl === "number" && Number.isFinite(pnl)) {
29675
- summary.totalPnl += pnl;
29676
- }
29677
- summaryByStrategy.set(trade.strategy, summary);
29678
- }
29679
- return [...summaryByStrategy.values()].map((summary) => ({
29680
- ...summary,
29681
- totalPnl: Number(summary.totalPnl.toFixed(2))
29682
- })).sort((left, right) => left.strategyName.localeCompare(right.strategyName));
29683
- };
29684
- summarizeTradeParityByStrategy = ({
29685
- runtimeEntries,
29686
- runtimeDuplicateEntries,
29687
- backtestEntries,
29688
- matchedEntries,
29689
- runtimeOnlyEntries,
29690
- backtestOnlyEntries
29691
- }) => {
29692
- const rows = /* @__PURE__ */ new Map();
29693
- const ensureRow = (strategyName) => {
29694
- const row = rows.get(strategyName) ?? {
29695
- runtime: 0,
29696
- runtimeDuplicates: 0,
29697
- backtest: 0,
29698
- matched: 0,
29699
- runtimeOnly: 0,
29700
- backtestOnly: 0
29701
- };
29702
- rows.set(strategyName, row);
29703
- return row;
29704
- };
29705
- for (const entry of runtimeEntries) {
29706
- ensureRow(entry.strategy).runtime += 1;
29707
- }
29708
- for (const entry of runtimeDuplicateEntries) {
29709
- ensureRow(entry.strategy).runtimeDuplicates += 1;
29710
- }
29711
- for (const entry of backtestEntries) {
29712
- ensureRow(entry.strategy).backtest += 1;
29713
- }
29714
- for (const entry of matchedEntries) {
29715
- ensureRow(entry.runtime.strategy).matched += 1;
29716
- }
29717
- for (const entry of runtimeOnlyEntries) {
29718
- ensureRow(entry.strategy).runtimeOnly += 1;
29719
- }
29720
- for (const entry of backtestOnlyEntries) {
29721
- ensureRow(entry.strategy).backtestOnly += 1;
29722
- }
29723
- return [...rows.entries()].sort(
29724
- ([left], [right]) => left.localeCompare(right)
29725
- );
29726
- };
29727
- }
29728
- });
29729
-
29730
29652
  // src/lib/runtimeParity/targets.ts
29731
29653
  var parseSymbolsFromCLI2, toTargetKey, buildReplayInputsIndex, addReplayTarget, countReplayTargetSources, buildReplayTargets;
29732
29654
  var init_targets = __esm({
@@ -29848,6 +29770,115 @@ var init_targets = __esm({
29848
29770
  }
29849
29771
  });
29850
29772
 
29773
+ // src/lib/runtimeParity/evidence.ts
29774
+ var matchesSelection, loadRuntimeParityEvidence;
29775
+ var init_evidence = __esm({
29776
+ "src/lib/runtimeParity/evidence.ts"() {
29777
+ "use strict";
29778
+ matchesSelection = (context, artifact) => (!context.strategy || artifact.strategy === context.strategy) && (!context.symbols || context.symbols.has(artifact.symbol));
29779
+ loadRuntimeParityEvidence = async (context, adapters) => {
29780
+ const range = {
29781
+ startTime: context.window.start,
29782
+ endTime: context.window.end
29783
+ };
29784
+ const [allRuntimeTrades, allRuntimeSignals, allRuntimeSignalEvaluations] = await Promise.all([
29785
+ adapters.loadRuntimeTrades(context.userName, range),
29786
+ adapters.loadRuntimeSignals(context.userName, range),
29787
+ adapters.loadRuntimeSignalEvaluations(context.userName, range)
29788
+ ]);
29789
+ return {
29790
+ runtimeTrades: allRuntimeTrades.filter(
29791
+ (trade) => trade.entryTimestamp >= context.window.start && trade.entryTimestamp <= context.window.end && matchesSelection(context, trade)
29792
+ ),
29793
+ runtimeSignals: allRuntimeSignals.filter(
29794
+ (signal) => signal.timestamp >= context.window.start && signal.timestamp <= context.window.end && matchesSelection(context, signal)
29795
+ ),
29796
+ runtimeSignalEvaluations: allRuntimeSignalEvaluations.filter(
29797
+ (evaluation) => evaluation.timestamp >= context.window.start && evaluation.timestamp <= context.window.end && matchesSelection(context, evaluation)
29798
+ )
29799
+ };
29800
+ };
29801
+ }
29802
+ });
29803
+
29804
+ // src/lib/paritySummary.ts
29805
+ var summarizeRuntimeTradesByStrategy, summarizeTradeParityByStrategy;
29806
+ var init_paritySummary = __esm({
29807
+ "src/lib/paritySummary.ts"() {
29808
+ "use strict";
29809
+ summarizeRuntimeTradesByStrategy = (trades) => {
29810
+ const summaryByStrategy = /* @__PURE__ */ new Map();
29811
+ for (const trade of trades) {
29812
+ const summary = summaryByStrategy.get(trade.strategy) ?? {
29813
+ strategyName: trade.strategy,
29814
+ trades: 0,
29815
+ activeTrades: 0,
29816
+ closedTrades: 0,
29817
+ totalPnl: 0
29818
+ };
29819
+ summary.trades += 1;
29820
+ if (trade.status === "active") {
29821
+ summary.activeTrades += 1;
29822
+ } else {
29823
+ summary.closedTrades += 1;
29824
+ }
29825
+ const pnl = trade.status === "active" ? trade.currentPnl : trade.closedPnl ?? trade.currentPnl;
29826
+ if (typeof pnl === "number" && Number.isFinite(pnl)) {
29827
+ summary.totalPnl += pnl;
29828
+ }
29829
+ summaryByStrategy.set(trade.strategy, summary);
29830
+ }
29831
+ return [...summaryByStrategy.values()].map((summary) => ({
29832
+ ...summary,
29833
+ totalPnl: Number(summary.totalPnl.toFixed(2))
29834
+ })).sort((left, right) => left.strategyName.localeCompare(right.strategyName));
29835
+ };
29836
+ summarizeTradeParityByStrategy = ({
29837
+ runtimeEntries,
29838
+ runtimeDuplicateEntries,
29839
+ backtestEntries,
29840
+ matchedEntries,
29841
+ runtimeOnlyEntries,
29842
+ backtestOnlyEntries
29843
+ }) => {
29844
+ const rows = /* @__PURE__ */ new Map();
29845
+ const ensureRow = (strategyName) => {
29846
+ const row = rows.get(strategyName) ?? {
29847
+ runtime: 0,
29848
+ runtimeDuplicates: 0,
29849
+ backtest: 0,
29850
+ matched: 0,
29851
+ runtimeOnly: 0,
29852
+ backtestOnly: 0
29853
+ };
29854
+ rows.set(strategyName, row);
29855
+ return row;
29856
+ };
29857
+ for (const entry of runtimeEntries) {
29858
+ ensureRow(entry.strategy).runtime += 1;
29859
+ }
29860
+ for (const entry of runtimeDuplicateEntries) {
29861
+ ensureRow(entry.strategy).runtimeDuplicates += 1;
29862
+ }
29863
+ for (const entry of backtestEntries) {
29864
+ ensureRow(entry.strategy).backtest += 1;
29865
+ }
29866
+ for (const entry of matchedEntries) {
29867
+ ensureRow(entry.runtime.strategy).matched += 1;
29868
+ }
29869
+ for (const entry of runtimeOnlyEntries) {
29870
+ ensureRow(entry.strategy).runtimeOnly += 1;
29871
+ }
29872
+ for (const entry of backtestOnlyEntries) {
29873
+ ensureRow(entry.strategy).backtestOnly += 1;
29874
+ }
29875
+ return [...rows.entries()].sort(
29876
+ ([left], [right]) => left.localeCompare(right)
29877
+ );
29878
+ };
29879
+ }
29880
+ });
29881
+
29851
29882
  // src/lib/runtimeParity/classification.ts
29852
29883
  var BACKTEST_ONLY_CLASSIFICATIONS, RUNTIME_ONLY_CLASSIFICATIONS, formatMinutes, normalizeSignalOrderStatus, findNearestRuntimeSignal, findNearestRuntimeSignalEvaluation, findNearestBacktestEntryForRuntimeOnly, buildSignalClassificationReason, buildEvaluationClassification, buildRuntimeOnlyEvaluationClassification, classifyBacktestOnlyEntries, classifyRuntimeOnlyEntries, summarizeBacktestOnlyClassifications, summarizeRuntimeOnlyClassifications;
29853
29884
  var init_classification = __esm({
@@ -30183,262 +30214,131 @@ var init_classification = __esm({
30183
30214
  }
30184
30215
  });
30185
30216
 
30186
- // src/scripts/runtimeParity.ts
30187
- var runtimeParity_exports = {};
30188
- __export(runtimeParity_exports, {
30189
- buildRuntimeParityMessage: () => buildRuntimeParityMessage,
30190
- main: () => main40,
30191
- runtimeParity: () => runtimeParity
30192
- });
30193
- var import_config11, import_node_crypto14, import_args30, import_chalk43, import_constants15, import_time7, import_logger6, import_redis17, import_cli11, import_backtest3, import_connectors14, flags28, interval3, projectRoot13, DEFAULT_LOOKBACK_DAYS, DETAIL_LIMIT, TELEGRAM_DETAIL_LIMIT, SUMMARY_TIMEZONE, SUMMARY_TIMEZONE_LABEL, DEFAULT_REPLAY_ENV, escapeHtml5, formatMskDateTime2, resolveParityConnectorName, createConnector, buildReplayAiAnalyses, formatReplayEnvSummary, buildReplayConfig, warmReplayHistory, formatPercent4, formatMinutes2, formatEntryLabel, formatRuntimeEntriesSummary, formatSourceCountsSummary, normalizeSummaryText, pickFirstText, buildMismatchSummaryRows, buildStrategyIssueRows, toSerializableTradeParityEntry, toSerializableSignal, toSerializableRuntimeSignalEvaluation, getRuntimeOnlyLikelyCause, getBacktestOnlyLikelyCause, getRuntimeOnlyRecommendedChecks, getBacktestOnlyRecommendedChecks, buildRuntimeParityMismatchAttachment, printRuntimeDuplicateDetails, printClassifiedBacktestOnlyDetails, printClassifiedRuntimeOnlyDetails, summarizeByStrategy, buildRuntimeParityMessage, buildRuntimeParityNoTargetsMessage, runtimeParity, main40;
30194
- var init_runtimeParity2 = __esm({
30195
- "src/scripts/runtimeParity.ts"() {
30217
+ // src/lib/runtimeParity/analysis.ts
30218
+ var summarizeByStrategy, analyzeRuntimeParity;
30219
+ var init_analysis = __esm({
30220
+ "src/lib/runtimeParity/analysis.ts"() {
30196
30221
  "use strict";
30197
- import_config11 = require("dotenv/config");
30198
- import_node_crypto14 = require("crypto");
30199
- import_args30 = __toESM(require("args"));
30200
- import_chalk43 = __toESM(require("chalk"));
30201
- import_constants15 = require("@tradejs/core/constants");
30202
- import_time7 = require("@tradejs/core/time");
30203
- import_logger6 = require("@tradejs/infra/logger");
30204
- import_redis17 = require("@tradejs/infra/redis");
30205
- import_cli11 = require("@tradejs/node/cli");
30206
- import_backtest3 = require("@tradejs/node/backtest");
30207
- import_connectors14 = require("@tradejs/node/connectors");
30208
30222
  init_runtimeParity();
30209
- init_cliArgs();
30210
30223
  init_paritySummary();
30211
- init_runtimeRedis();
30212
- init_runtimeSignalsLoader();
30213
- init_telegramReports();
30214
- init_timeWindow();
30215
- init_runFormatting();
30216
- init_marketContextPrepare();
30217
- init_historyPrepare();
30218
- init_targets();
30219
30224
  init_classification();
30220
- init_runtimeModeConfig();
30221
- import_args30.default.option(["u", "user"], "Use user config", "root");
30222
- import_args30.default.option(
30223
- ["o", "connector"],
30224
- "Connector provider or name for parity replay (e.g. bybit, binance, coinbase, custom)",
30225
- "bybit"
30226
- );
30227
- import_args30.default.option(["d", "days"], "Replay window in days", 3);
30228
- import_args30.default.option(
30229
- ["b", "startTime"],
30230
- "Explicit replay start timestamp (ms or seconds)"
30231
- );
30232
- import_args30.default.option(["e", "endTime"], "Explicit replay end timestamp (ms or seconds)");
30233
- import_args30.default.option(["s", "strategy"], "Only compare one strategy");
30234
- import_args30.default.option(
30235
- ["t", "tickers"],
30236
- "Replay comma-separated symbols for all configured strategies"
30237
- );
30238
- import_args30.default.option(
30239
- ["C", "cacheOnly"],
30240
- "Do not refresh market history before replay",
30241
- false
30242
- );
30243
- import_args30.default.option(
30244
- ["a", "toleranceBars"],
30245
- "Allowed entry timestamp drift in bars when matching runtime vs backtest",
30246
- 1
30247
- );
30248
- import_args30.default.option(
30249
- "fullUniverse",
30250
- "Replay every configured strategy across the full connector universe. By default parity replays only runtime-traded and strategy-results symbols unless --tickers is provided.",
30251
- false
30252
- );
30253
- import_args30.default.option(
30254
- "runtimeGates",
30255
- "Force runtime AI/ML gates for all replay targets. Runtime-gated configs are replayed with gates by default.",
30256
- false
30257
- );
30258
- import_args30.default.option(["N", "notify"], "Send parity summary to Telegram", false);
30259
- import_args30.default.option(["D", "details"], "Print unmatched entry details (capped)", false);
30260
- process.argv = normalizeCliArgv(process.argv, {
30261
- "-C": "--cacheOnly",
30262
- "-D": "--details",
30263
- "-E": "--endTime",
30264
- "-N": "--notify",
30265
- "-S": "--startTime",
30266
- "-T": "--toleranceBars"
30267
- });
30268
- flags28 = import_args30.default.parse(process.argv);
30269
- interval3 = "15";
30270
- projectRoot13 = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
30271
- DEFAULT_LOOKBACK_DAYS = 3;
30272
- DETAIL_LIMIT = 10;
30273
- TELEGRAM_DETAIL_LIMIT = 5;
30274
- SUMMARY_TIMEZONE = "Europe/Moscow";
30275
- SUMMARY_TIMEZONE_LABEL = "MSK";
30276
- DEFAULT_REPLAY_ENV = flags28.runtimeGates ? "PARITY" : "BACKTEST";
30277
- escapeHtml5 = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
30278
- formatMskDateTime2 = (timestamp) => new Intl.DateTimeFormat("ru-RU", {
30279
- timeZone: SUMMARY_TIMEZONE,
30280
- year: "numeric",
30281
- month: "2-digit",
30282
- day: "2-digit",
30283
- hour: "2-digit",
30284
- minute: "2-digit",
30285
- hour12: false
30286
- }).format(new Date(timestamp));
30287
- resolveParityConnectorName = async (value) => {
30288
- const connectorName = await (0, import_connectors14.resolveConnectorName)(value, projectRoot13);
30289
- if (connectorName) {
30290
- return connectorName;
30291
- }
30292
- import_logger6.logger.warn(
30293
- 'Unknown connector "%s". Fallback to %s.',
30294
- String(value || "").trim() || String(value),
30295
- import_connectors14.DEFAULT_CONNECTOR_NAME
30296
- );
30297
- return import_connectors14.DEFAULT_CONNECTOR_NAME;
30298
- };
30299
- createConnector = async ({
30300
- userName: userName3,
30301
- connectorName
30302
- }) => {
30303
- const connectorFactory = await (0, import_connectors14.getConnectorCreatorByName)(
30304
- connectorName,
30305
- projectRoot13
30306
- );
30307
- if (!connectorFactory) {
30308
- throw new Error(`Connector "${connectorName}" is not registered`);
30309
- }
30310
- return await connectorFactory({ userName: userName3 });
30311
- };
30312
- buildReplayAiAnalyses = ({
30313
- runtimeTrades,
30314
- runtimeSignalEvaluations,
30315
- toleranceMs
30225
+ init_targets();
30226
+ summarizeByStrategy = ({
30227
+ targets,
30228
+ successfulTargetKeys,
30229
+ replayErrors,
30230
+ runtimeEntries,
30231
+ runtimeDuplicateEntries,
30232
+ backtestEntries,
30233
+ matchedEntries,
30234
+ runtimeOnlyEntries,
30235
+ backtestOnlyEntries
30316
30236
  }) => {
30317
- const snapshots = [
30318
- ...runtimeTrades.filter(
30319
- (trade) => trade.aiAnalysis && typeof trade.entryTimestamp === "number" && Number.isFinite(trade.entryTimestamp)
30320
- ).map((trade) => ({
30321
- strategy: trade.strategy,
30322
- symbol: trade.symbol,
30323
- direction: trade.direction,
30324
- timestamp: trade.entryTimestamp,
30325
- toleranceMs,
30326
- analysis: trade.aiAnalysis
30327
- })),
30328
- ...runtimeSignalEvaluations.filter(
30329
- (evaluation) => evaluation.aiAnalysis && evaluation.direction && typeof evaluation.timestamp === "number" && Number.isFinite(evaluation.timestamp)
30330
- ).map((evaluation) => ({
30331
- strategy: evaluation.strategy,
30332
- symbol: evaluation.symbol,
30333
- direction: evaluation.direction,
30334
- timestamp: evaluation.timestamp,
30335
- toleranceMs,
30336
- analysis: evaluation.aiAnalysis
30337
- }))
30338
- ];
30339
- const seen = /* @__PURE__ */ new Set();
30340
- return snapshots.filter((snapshot) => {
30341
- const key = `${snapshot.strategy}:${snapshot.symbol}:${snapshot.direction}:${snapshot.timestamp}`;
30342
- if (seen.has(key)) {
30343
- return false;
30344
- }
30345
- seen.add(key);
30346
- return true;
30237
+ const rows = /* @__PURE__ */ new Map();
30238
+ const baseRows = summarizeTradeParityByStrategy({
30239
+ runtimeEntries,
30240
+ runtimeDuplicateEntries,
30241
+ backtestEntries,
30242
+ matchedEntries,
30243
+ runtimeOnlyEntries,
30244
+ backtestOnlyEntries
30347
30245
  });
30348
- };
30349
- formatReplayEnvSummary = (configs) => {
30350
- let backtest2 = 0;
30351
- let parity = 0;
30352
- for (const config2 of configs) {
30353
- if (config2.ENV === "PARITY") {
30354
- parity += 1;
30355
- } else {
30356
- backtest2 += 1;
30357
- }
30246
+ for (const [strategy, baseRow] of baseRows) {
30247
+ rows.set(strategy, {
30248
+ ...baseRow,
30249
+ targets: 0,
30250
+ compared: 0,
30251
+ errors: 0
30252
+ });
30358
30253
  }
30359
- if (parity > 0 && backtest2 > 0) {
30360
- return `MIXED (PARITY=${parity}, BACKTEST=${backtest2})`;
30254
+ const ensureRow = (strategy) => {
30255
+ const row = rows.get(strategy) ?? {
30256
+ runtime: 0,
30257
+ runtimeDuplicates: 0,
30258
+ backtest: 0,
30259
+ matched: 0,
30260
+ runtimeOnly: 0,
30261
+ backtestOnly: 0,
30262
+ targets: 0,
30263
+ compared: 0,
30264
+ errors: 0
30265
+ };
30266
+ rows.set(strategy, row);
30267
+ return row;
30268
+ };
30269
+ for (const target of targets) {
30270
+ const row = ensureRow(target.strategy);
30271
+ row.targets += 1;
30272
+ if (successfulTargetKeys.has(toTargetKey(target))) {
30273
+ row.compared += 1;
30274
+ }
30361
30275
  }
30362
- if (parity > 0) {
30363
- return "PARITY";
30276
+ for (const error of replayErrors) {
30277
+ ensureRow(error.strategy).errors += 1;
30364
30278
  }
30365
- return "BACKTEST";
30366
- };
30367
- buildReplayConfig = async ({
30368
- userName: userName3,
30369
- strategy,
30370
- symbol,
30371
- replayInputsIndex,
30372
- toleranceMs
30373
- }) => {
30374
- const [userConfig, strategyResults] = await Promise.all([
30375
- (0, import_redis17.getData)(import_redis17.redisKeys.strategyConfig(userName3, strategy), {}),
30376
- (0, import_redis17.getData)(import_redis17.redisKeys.strategyResults(userName3, strategy), {})
30377
- ]);
30378
- const typedResults = strategyResults ?? {};
30379
- const symbolResult = typedResults?.[symbol];
30380
- const symbolConfig = symbolResult && symbolResult.config && typeof symbolResult.config === "object" ? symbolResult.config : {};
30381
- const replayInputs = replayInputsIndex.get(toTargetKey({ strategy, symbol }));
30382
- const aiReplayAnalyses = buildReplayAiAnalyses({
30383
- runtimeTrades: replayInputs?.runtimeTrades ?? [],
30384
- runtimeSignalEvaluations: replayInputs?.runtimeSignalEvaluations ?? [],
30385
- strategy,
30386
- symbol,
30387
- toleranceMs
30388
- });
30389
- const strategyConfig = {
30390
- ...userConfig,
30391
- ...symbolConfig
30392
- };
30393
- return buildRuntimeModeStrategyConfig({
30394
- strategyConfig,
30395
- env: resolveReplayStrategyEnv({
30396
- strategyConfig,
30397
- forceRuntimeGates: Boolean(flags28.runtimeGates)
30398
- }),
30399
- interval: interval3,
30400
- makeOrders: true,
30401
- recordRuntimeTrades: false,
30402
- aiReplayAnalyses
30403
- });
30279
+ return [...rows.entries()].sort(
30280
+ ([left], [right]) => left.localeCompare(right)
30281
+ );
30404
30282
  };
30405
- warmReplayHistory = async ({
30406
- userName: userName3,
30407
- connectorName,
30408
- targets,
30409
- preloadStart,
30410
- preloadEnd
30411
- }) => {
30412
- const connector = await createConnector({ userName: userName3, connectorName });
30413
- const symbols = [...new Set(targets.map((target) => target.symbol))];
30414
- const warmStartedAt = Date.now();
30415
- console.log(
30416
- import_chalk43.default.gray(
30417
- `Warm history: ${symbols.length} symbol(s) on ${connectorName}, preload ${(0, import_time7.formatUnix)(preloadStart)} -> ${(0, import_time7.formatUnix)(preloadEnd)}`
30418
- )
30283
+ analyzeRuntimeParity = (context) => {
30284
+ const comparableRuntimeTrades = context.runtimeTrades.filter(
30285
+ (trade) => context.successfulTargetKeys.has(toTargetKey(trade))
30419
30286
  );
30420
- const btcReferences = await loadBtcReferenceConnectors({
30421
- connectorName,
30422
- marketConnector: connector,
30423
- userName: userName3,
30424
- projectRoot: projectRoot13,
30425
- shouldUseDedicatedReferences: true,
30426
- warn: (message) => import_logger6.logger.warn(message)
30427
- });
30428
- await updateMarketHistoryWithBtcReferences({
30429
- marketConnector: connector,
30430
- connectorName,
30431
- btcReferences,
30432
- interval: interval3,
30433
- symbols,
30434
- preloadStart,
30435
- preloadEnd,
30436
- log: (message) => console.log(import_chalk43.default.gray(message))
30437
- });
30438
- console.log(
30439
- import_chalk43.default.gray(`Warm history: done in ${formatDuration(warmStartedAt)}`)
30287
+ const rawRuntimeEntries = extractRuntimeParityEntries(
30288
+ comparableRuntimeTrades
30440
30289
  );
30290
+ const runtimeDedupe = dedupeRuntimeParityEntries(rawRuntimeEntries);
30291
+ const runtimeEntries = runtimeDedupe.entries;
30292
+ const comparison = compareTradeParityEntries({
30293
+ runtimeEntries,
30294
+ backtestEntries: context.backtestEntries,
30295
+ toleranceMs: context.toleranceMs
30296
+ });
30297
+ const classifiedBacktestOnly = classifyBacktestOnlyEntries({
30298
+ entries: comparison.backtestOnly,
30299
+ runtimeSignals: context.runtimeSignals,
30300
+ runtimeSignalEvaluations: context.runtimeSignalEvaluations,
30301
+ toleranceMs: context.toleranceMs
30302
+ });
30303
+ const classifiedRuntimeOnly = classifyRuntimeOnlyEntries({
30304
+ entries: comparison.runtimeOnly,
30305
+ replaySignalEvaluations: context.replaySignalEvaluations,
30306
+ backtestEntries: context.backtestEntries,
30307
+ toleranceMs: context.toleranceMs
30308
+ });
30309
+ const matchedSummary = summarizeMatchedParity(comparison.matched);
30310
+ const strategyRows = summarizeByStrategy({
30311
+ targets: context.replayTargets,
30312
+ successfulTargetKeys: context.successfulTargetKeys,
30313
+ replayErrors: context.replayErrors,
30314
+ runtimeEntries,
30315
+ runtimeDuplicateEntries: runtimeDedupe.duplicateEntries,
30316
+ backtestEntries: context.backtestEntries,
30317
+ matchedEntries: comparison.matched,
30318
+ runtimeOnlyEntries: comparison.runtimeOnly,
30319
+ backtestOnlyEntries: comparison.backtestOnly
30320
+ });
30321
+ return {
30322
+ comparableRuntimeTrades,
30323
+ rawRuntimeEntries,
30324
+ runtimeDedupe,
30325
+ runtimeEntries,
30326
+ comparison,
30327
+ classifiedBacktestOnly,
30328
+ classifiedRuntimeOnly,
30329
+ matchedSummary,
30330
+ strategyRows
30331
+ };
30441
30332
  };
30333
+ }
30334
+ });
30335
+
30336
+ // src/lib/runtimeParity/reportingShared.ts
30337
+ var import_time7, formatPercent4, formatMinutes2, formatEntryLabel, formatRuntimeEntriesSummary, formatSourceCountsSummary, normalizeSummaryText, pickFirstText, buildMismatchSummaryRows, buildStrategyIssueRows;
30338
+ var init_reportingShared = __esm({
30339
+ "src/lib/runtimeParity/reportingShared.ts"() {
30340
+ "use strict";
30341
+ import_time7 = require("@tradejs/core/time");
30442
30342
  formatPercent4 = (value) => value == null || !Number.isFinite(value) ? "n/a" : `${value.toFixed(2)}%`;
30443
30343
  formatMinutes2 = (value) => value == null || !Number.isFinite(value) ? "n/a" : `${(value / 6e4).toFixed(2)}m`;
30444
30344
  formatEntryLabel = (entry) => `${entry.strategy} ${entry.symbol} ${entry.direction} ${(0, import_time7.formatUnix)(entry.timestamp)}`;
@@ -30458,16 +30358,11 @@ var init_runtimeParity2 = __esm({
30458
30358
  };
30459
30359
  normalizeSummaryText = (value, maxLength = 160) => {
30460
30360
  const normalized = value.replace(/\s+/g, " ").trim();
30461
- if (normalized.length <= maxLength) {
30462
- return normalized;
30463
- }
30464
- return `${normalized.slice(0, maxLength - 3)}...`;
30361
+ return normalized.length <= maxLength ? normalized : `${normalized.slice(0, maxLength - 3)}...`;
30465
30362
  };
30466
30363
  pickFirstText = (...values) => {
30467
30364
  for (const value of values) {
30468
- if (typeof value === "string" && value.trim()) {
30469
- return value.trim();
30470
- }
30365
+ if (typeof value === "string" && value.trim()) return value.trim();
30471
30366
  }
30472
30367
  return null;
30473
30368
  };
@@ -30487,9 +30382,7 @@ var init_runtimeParity2 = __esm({
30487
30382
  item.nearestBacktestEntry?.id
30488
30383
  );
30489
30384
  const refs = [`signalId=${signalId ?? "n/a"}`];
30490
- if (orderId && orderId !== signalId) {
30491
- refs.push(`orderId=${orderId}`);
30492
- }
30385
+ if (orderId && orderId !== signalId) refs.push(`orderId=${orderId}`);
30493
30386
  if (item.evaluation?.evaluationId) {
30494
30387
  refs.push(`evaluationId=${item.evaluation.evaluationId}`);
30495
30388
  }
@@ -30551,20 +30444,23 @@ var init_runtimeParity2 = __esm({
30551
30444
  };
30552
30445
  buildStrategyIssueRows = (strategyRows) => strategyRows.map(([strategy, row]) => {
30553
30446
  const issues = [];
30554
- if (row.runtimeOnly) {
30555
- issues.push(`runtimeOnly=${row.runtimeOnly}`);
30556
- }
30557
- if (row.backtestOnly) {
30558
- issues.push(`backtestOnly=${row.backtestOnly}`);
30559
- }
30447
+ if (row.runtimeOnly) issues.push(`runtimeOnly=${row.runtimeOnly}`);
30448
+ if (row.backtestOnly) issues.push(`backtestOnly=${row.backtestOnly}`);
30560
30449
  if (row.runtimeDuplicates) {
30561
30450
  issues.push(`runtimeDuplicates=${row.runtimeDuplicates}`);
30562
30451
  }
30563
- if (row.errors) {
30564
- issues.push(`errors=${row.errors}`);
30565
- }
30452
+ if (row.errors) issues.push(`errors=${row.errors}`);
30566
30453
  return issues.length ? `- ${strategy}: ${issues.join(", ")}` : null;
30567
30454
  }).filter((line) => Boolean(line));
30455
+ }
30456
+ });
30457
+
30458
+ // src/lib/runtimeParity/reportingJson.ts
30459
+ var toSerializableTradeParityEntry, toSerializableSignal, toSerializableRuntimeSignalEvaluation, getRuntimeOnlyLikelyCause, getBacktestOnlyLikelyCause, getRuntimeOnlyRecommendedChecks, getBacktestOnlyRecommendedChecks, buildRuntimeParityMismatchAttachment;
30460
+ var init_reportingJson = __esm({
30461
+ "src/lib/runtimeParity/reportingJson.ts"() {
30462
+ "use strict";
30463
+ init_reportingShared();
30568
30464
  toSerializableTradeParityEntry = (entry) => entry ? {
30569
30465
  id: entry.id,
30570
30466
  source: entry.source,
@@ -30707,34 +30603,12 @@ var init_runtimeParity2 = __esm({
30707
30603
  ];
30708
30604
  }
30709
30605
  };
30710
- buildRuntimeParityMismatchAttachment = ({
30711
- window,
30712
- connectorName,
30713
- replayEnv,
30714
- toleranceBars,
30715
- toleranceMs,
30716
- replayTargetsCount,
30717
- comparedTargetsCount,
30718
- replayErrors,
30719
- sourceCounts,
30720
- rawRuntimeEntriesCount,
30721
- runtimeEntriesCount,
30722
- runtimeDuplicateEntriesCount,
30723
- backtestEntriesCount,
30724
- matchedCount,
30725
- runtimeOnlyCount,
30726
- backtestOnlyCount,
30727
- matchedSummary,
30728
- classifiedRuntimeOnly,
30729
- classifiedBacktestOnly,
30730
- runtimeSignalEvaluationsCount,
30731
- strategyRows
30732
- }) => {
30733
- if (!classifiedRuntimeOnly.length && !classifiedBacktestOnly.length) {
30606
+ buildRuntimeParityMismatchAttachment = (context) => {
30607
+ if (!context.classifiedRuntimeOnly.length && !context.classifiedBacktestOnly.length) {
30734
30608
  return null;
30735
30609
  }
30736
30610
  const cases = [
30737
- ...classifiedRuntimeOnly.map((item) => ({
30611
+ ...context.classifiedRuntimeOnly.map((item) => ({
30738
30612
  kind: "runtimeOnly",
30739
30613
  strategy: item.entry.strategy,
30740
30614
  symbol: item.entry.symbol,
@@ -30773,7 +30647,7 @@ var init_runtimeParity2 = __esm({
30773
30647
  )
30774
30648
  }
30775
30649
  })),
30776
- ...classifiedBacktestOnly.map((item) => ({
30650
+ ...context.classifiedBacktestOnly.map((item) => ({
30777
30651
  kind: "backtestOnly",
30778
30652
  strategy: item.entry.strategy,
30779
30653
  symbol: item.entry.symbol,
@@ -30819,40 +30693,37 @@ var init_runtimeParity2 = __esm({
30819
30693
  generatedAt: Date.now(),
30820
30694
  codexQuestion: "For each mismatch case, explain why runtime and replay/backtest diverged. Use why.classification first, then confirm with decisionTrace, timing, and artifacts.",
30821
30695
  window: {
30822
- start: window.start,
30823
- end: window.end,
30824
- source: window.source
30825
- },
30826
- connectorName,
30827
- replayEnv,
30828
- tolerance: {
30829
- bars: toleranceBars,
30830
- ms: toleranceMs
30696
+ start: context.window.start,
30697
+ end: context.window.end,
30698
+ source: context.window.source
30831
30699
  },
30700
+ connectorName: context.connectorName,
30701
+ replayEnv: context.replayEnv,
30702
+ tolerance: { bars: context.toleranceBars, ms: context.toleranceMs },
30832
30703
  summary: {
30833
- replayTargets: replayTargetsCount,
30834
- comparedTargets: comparedTargetsCount,
30835
- replayErrors: replayErrors.length,
30836
- sourceCounts,
30837
- runtimeEntriesRaw: rawRuntimeEntriesCount,
30838
- runtimeEntries: runtimeEntriesCount,
30839
- runtimeDuplicateEntries: runtimeDuplicateEntriesCount,
30840
- backtestEntries: backtestEntriesCount,
30841
- matchedEntries: matchedCount,
30842
- runtimeOnlyEntries: runtimeOnlyCount,
30843
- backtestOnlyEntries: backtestOnlyCount,
30844
- runtimeSignalEvaluations: runtimeSignalEvaluationsCount,
30704
+ replayTargets: context.replayTargetsCount,
30705
+ comparedTargets: context.comparedTargetsCount,
30706
+ replayErrors: context.replayErrors.length,
30707
+ sourceCounts: context.sourceCounts,
30708
+ runtimeEntriesRaw: context.rawRuntimeEntriesCount,
30709
+ runtimeEntries: context.runtimeEntriesCount,
30710
+ runtimeDuplicateEntries: context.runtimeDuplicateEntriesCount,
30711
+ backtestEntries: context.backtestEntriesCount,
30712
+ matchedEntries: context.matchedCount,
30713
+ runtimeOnlyEntries: context.runtimeOnlyCount,
30714
+ backtestOnlyEntries: context.backtestOnlyCount,
30715
+ runtimeSignalEvaluations: context.runtimeSignalEvaluationsCount,
30845
30716
  matchedDeltas: {
30846
- priceAvgPct: matchedSummary.avgPriceDeltaPct,
30847
- priceMaxPct: matchedSummary.maxPriceDeltaPct,
30848
- timeAvgMs: matchedSummary.avgTimestampDiffMs,
30849
- timeMaxMs: matchedSummary.maxTimestampDiffMs
30717
+ priceAvgPct: context.matchedSummary.avgPriceDeltaPct,
30718
+ priceMaxPct: context.matchedSummary.maxPriceDeltaPct,
30719
+ timeAvgMs: context.matchedSummary.avgTimestampDiffMs,
30720
+ timeMaxMs: context.matchedSummary.maxTimestampDiffMs
30850
30721
  },
30851
- strategyIssues: buildStrategyIssueRows(strategyRows).map(
30722
+ strategyIssues: buildStrategyIssueRows(context.strategyRows).map(
30852
30723
  (line) => line.slice(2)
30853
30724
  )
30854
30725
  },
30855
- replayErrors: replayErrors.map((error) => ({
30726
+ replayErrors: context.replayErrors.map((error) => ({
30856
30727
  strategy: error.strategy,
30857
30728
  symbol: error.symbol,
30858
30729
  sources: error.sources,
@@ -30860,7 +30731,7 @@ var init_runtimeParity2 = __esm({
30860
30731
  })),
30861
30732
  cases,
30862
30733
  mismatches: {
30863
- runtimeOnly: classifiedRuntimeOnly.map((item) => ({
30734
+ runtimeOnly: context.classifiedRuntimeOnly.map((item) => ({
30864
30735
  classification: item.classification,
30865
30736
  reason: item.reason,
30866
30737
  runtimeEntry: toSerializableTradeParityEntry(item.entry),
@@ -30873,7 +30744,7 @@ var init_runtimeParity2 = __esm({
30873
30744
  ),
30874
30745
  nearestBacktestDriftMs: item.nearestBacktestEntryTimestampDiffMs
30875
30746
  })),
30876
- backtestOnly: classifiedBacktestOnly.map((item) => ({
30747
+ backtestOnly: context.classifiedBacktestOnly.map((item) => ({
30877
30748
  classification: item.classification,
30878
30749
  reason: item.reason,
30879
30750
  backtestEntry: toSerializableTradeParityEntry(item.entry),
@@ -30887,31 +30758,115 @@ var init_runtimeParity2 = __esm({
30887
30758
  }
30888
30759
  };
30889
30760
  return {
30890
- filename: `runtime-parity-mismatches-${connectorName}-${window.start}-${window.end}.json`,
30761
+ filename: `runtime-parity-mismatches-${context.connectorName}-${context.window.start}-${context.window.end}.json`,
30891
30762
  content: JSON.stringify(payload, null, 2),
30892
30763
  caption: "Runtime parity mismatch JSON"
30893
30764
  };
30894
30765
  };
30895
- printRuntimeDuplicateDetails = (groups) => {
30896
- if (!groups.length) {
30897
- return;
30898
- }
30899
- console.log("");
30900
- console.log(import_chalk43.default.yellow("Runtime duplicates"));
30901
- for (const group of groups.slice(0, DETAIL_LIMIT)) {
30902
- const ids = group.entries.map((entry) => entry.orderId ?? entry.id);
30903
- console.log(
30904
- `- ${formatEntryLabel(group.entries[0])} count=${group.entries.length}, duplicateEntries=${group.entries.length - 1}, ids=${ids.join(",")}`
30905
- );
30906
- }
30907
- if (groups.length > DETAIL_LIMIT) {
30766
+ }
30767
+ });
30768
+
30769
+ // src/lib/runtimeParity/reportingTerminal.ts
30770
+ var import_chalk43, import_time8, DETAIL_LIMIT, buildRuntimeParityTerminalReport, writeRuntimeParityTerminalReport, printRuntimeDuplicateDetails, printClassifiedBacktestOnlyDetails, printClassifiedRuntimeOnlyDetails;
30771
+ var init_reportingTerminal = __esm({
30772
+ "src/lib/runtimeParity/reportingTerminal.ts"() {
30773
+ "use strict";
30774
+ import_chalk43 = __toESM(require("chalk"));
30775
+ import_time8 = require("@tradejs/core/time");
30776
+ init_classification();
30777
+ init_reportingShared();
30778
+ DETAIL_LIMIT = 10;
30779
+ buildRuntimeParityTerminalReport = (context) => {
30780
+ const mismatchRows = buildMismatchSummaryRows(context);
30781
+ const strategyIssueRows = buildStrategyIssueRows(context.strategyRows);
30782
+ const lines = [
30783
+ import_chalk43.default.cyan("TradeJS runtime parity"),
30784
+ `Window: ${(0, import_time8.formatUnix)(context.window.start)} -> ${(0, import_time8.formatUnix)(context.window.end)} (${context.window.source})`,
30785
+ `Connector: ${context.connectorName}`,
30786
+ `Replay env: ${context.replayEnv}${context.runtimeGatesEnabled ? " (runtime AI/ML gates enabled)" : " (core/backtest gates only)"}`,
30787
+ `Tolerance: ${context.toleranceBars} bar(s) / ${(context.toleranceMs / 6e4).toFixed(0)}m`,
30788
+ `Summary: targets=${context.replayTargetsCount}, compared=${context.comparedTargetsCount}, errors=${context.replayErrors.length}, runtime=${context.runtimeEntriesCount}, backtest=${context.backtestEntriesCount}, runtimeOnly=${context.runtimeOnlyCount}, backtestOnly=${context.backtestOnlyCount}, evaluations=${context.runtimeSignalEvaluationsCount}`,
30789
+ `Sources: ${formatSourceCountsSummary(context.sourceCounts)}`,
30790
+ `Runtime entries: ${formatRuntimeEntriesSummary(context)}`,
30791
+ `Matched deltas: price avg/max=${formatPercent4(context.matchedSummary.avgPriceDeltaPct)} / ${formatPercent4(context.matchedSummary.maxPriceDeltaPct)}, time avg/max=${formatMinutes2(context.matchedSummary.avgTimestampDiffMs)} / ${formatMinutes2(context.matchedSummary.maxTimestampDiffMs)}`
30792
+ ];
30793
+ if (context.runtimeDuplicateGroupsCount) {
30794
+ lines.push(
30795
+ `Runtime duplicate groups: ${context.runtimeDuplicateGroupsCount}, duplicate entries: ${context.runtimeDuplicateEntriesCount}`
30796
+ );
30797
+ }
30798
+ if (context.classifiedRuntimeOnly.length) {
30799
+ lines.push(
30800
+ `Runtime only classifications: ${summarizeRuntimeOnlyClassifications(context.classifiedRuntimeOnly)}`
30801
+ );
30802
+ }
30803
+ if (context.classifiedBacktestOnly.length) {
30804
+ lines.push(
30805
+ `Backtest only classifications: ${summarizeBacktestOnlyClassifications(context.classifiedBacktestOnly)}`
30806
+ );
30807
+ }
30808
+ if (context.runtimeSignalEvaluationsCount) {
30809
+ lines.push(`Runtime evaluations: ${context.runtimeSignalEvaluationsCount}`);
30810
+ }
30811
+ if (mismatchRows.length) {
30812
+ lines.push("", import_chalk43.default.yellow("Signal mismatches"));
30813
+ for (const item of mismatchRows.slice(0, context.detailLimit)) {
30814
+ lines.push(`- ${item.line}`);
30815
+ }
30816
+ if (mismatchRows.length > context.detailLimit) {
30817
+ lines.push(`- ... ${mismatchRows.length - context.detailLimit} more`);
30818
+ }
30819
+ }
30820
+ lines.push("");
30821
+ if (strategyIssueRows.length) {
30822
+ lines.push(import_chalk43.default.cyan("Strategy issues"), ...strategyIssueRows);
30823
+ } else if (context.strategyRows.length) {
30824
+ lines.push(
30825
+ `Strategies: clean ${context.strategyRows.length}/${context.strategyRows.length}`
30826
+ );
30827
+ }
30828
+ if (context.runtimeGateWarningCounts.size && !context.runtimeGatesRequested) {
30829
+ lines.push("", import_chalk43.default.yellow("Warnings"));
30830
+ for (const [strategy, count] of [
30831
+ ...context.runtimeGateWarningCounts.entries()
30832
+ ].sort(([left], [right]) => left.localeCompare(right))) {
30833
+ lines.push(
30834
+ `- ${strategy} uses AI/ML runtime gates on ${count} replay target(s); BACKTEST replay covers core execution, not live gating.`
30835
+ );
30836
+ }
30837
+ }
30838
+ if (context.replayErrors.length) {
30839
+ lines.push("", import_chalk43.default.red("Replay errors"));
30840
+ for (const error of context.replayErrors.slice(0, context.detailLimit)) {
30841
+ lines.push(`- ${error.strategy} ${error.symbol}: ${error.message}`);
30842
+ }
30843
+ if (context.replayErrors.length > context.detailLimit) {
30844
+ lines.push(
30845
+ `- ... ${context.replayErrors.length - context.detailLimit} more`
30846
+ );
30847
+ }
30848
+ }
30849
+ return lines;
30850
+ };
30851
+ writeRuntimeParityTerminalReport = (lines) => {
30852
+ for (const line of lines) console.log(line);
30853
+ };
30854
+ printRuntimeDuplicateDetails = (groups) => {
30855
+ if (!groups.length) return;
30856
+ console.log("");
30857
+ console.log(import_chalk43.default.yellow("Runtime duplicates"));
30858
+ for (const group of groups.slice(0, DETAIL_LIMIT)) {
30859
+ const ids = group.entries.map((entry) => entry.orderId ?? entry.id);
30860
+ console.log(
30861
+ `- ${formatEntryLabel(group.entries[0])} count=${group.entries.length}, duplicateEntries=${group.entries.length - 1}, ids=${ids.join(",")}`
30862
+ );
30863
+ }
30864
+ if (groups.length > DETAIL_LIMIT) {
30908
30865
  console.log(`- ... ${groups.length - DETAIL_LIMIT} more`);
30909
30866
  }
30910
30867
  };
30911
30868
  printClassifiedBacktestOnlyDetails = (classifiedEntries) => {
30912
- if (!classifiedEntries.length) {
30913
- return;
30914
- }
30869
+ if (!classifiedEntries.length) return;
30915
30870
  console.log("");
30916
30871
  console.log(import_chalk43.default.yellow("Backtest only"));
30917
30872
  for (const item of classifiedEntries.slice(0, DETAIL_LIMIT)) {
@@ -30926,9 +30881,7 @@ var init_runtimeParity2 = __esm({
30926
30881
  }
30927
30882
  };
30928
30883
  printClassifiedRuntimeOnlyDetails = (classifiedEntries) => {
30929
- if (!classifiedEntries.length) {
30930
- return;
30931
- }
30884
+ if (!classifiedEntries.length) return;
30932
30885
  console.log("");
30933
30886
  console.log(import_chalk43.default.yellow("Runtime only"));
30934
30887
  for (const item of classifiedEntries.slice(0, DETAIL_LIMIT)) {
@@ -30942,149 +30895,86 @@ var init_runtimeParity2 = __esm({
30942
30895
  console.log(`- ... ${classifiedEntries.length - DETAIL_LIMIT} more`);
30943
30896
  }
30944
30897
  };
30945
- summarizeByStrategy = ({
30946
- targets,
30947
- successfulTargetKeys,
30948
- replayErrors,
30949
- runtimeEntries,
30950
- runtimeDuplicateEntries,
30951
- backtestEntries,
30952
- matchedEntries,
30953
- runtimeOnlyEntries,
30954
- backtestOnlyEntries
30955
- }) => {
30956
- const rows = /* @__PURE__ */ new Map();
30957
- const baseRows = summarizeTradeParityByStrategy({
30958
- runtimeEntries,
30959
- runtimeDuplicateEntries,
30960
- backtestEntries,
30961
- matchedEntries,
30962
- runtimeOnlyEntries,
30963
- backtestOnlyEntries
30964
- });
30965
- for (const [strategy, baseRow] of baseRows) {
30966
- rows.set(strategy, {
30967
- ...baseRow,
30968
- targets: 0,
30969
- compared: 0,
30970
- errors: 0
30971
- });
30972
- }
30973
- const ensureRow = (strategy) => {
30974
- const row = rows.get(strategy) ?? {
30975
- runtime: 0,
30976
- runtimeDuplicates: 0,
30977
- backtest: 0,
30978
- matched: 0,
30979
- runtimeOnly: 0,
30980
- backtestOnly: 0,
30981
- targets: 0,
30982
- compared: 0,
30983
- errors: 0
30984
- };
30985
- rows.set(strategy, row);
30986
- return row;
30987
- };
30988
- for (const target of targets) {
30989
- const row = ensureRow(target.strategy);
30990
- row.targets += 1;
30991
- if (successfulTargetKeys.has(toTargetKey(target))) {
30992
- row.compared += 1;
30993
- }
30994
- }
30995
- for (const error of replayErrors) {
30996
- ensureRow(error.strategy).errors += 1;
30997
- }
30998
- return [...rows.entries()].sort(
30999
- ([left], [right]) => left.localeCompare(right)
31000
- );
31001
- };
30898
+ }
30899
+ });
30900
+
30901
+ // src/lib/runtimeParity/reportingTelegram.ts
30902
+ var TELEGRAM_DETAIL_LIMIT, SUMMARY_TIMEZONE, SUMMARY_TIMEZONE_LABEL, escapeHtml5, formatMskDateTime2, buildRuntimeParityMessage, buildRuntimeParityNoTargetsMessage;
30903
+ var init_reportingTelegram = __esm({
30904
+ "src/lib/runtimeParity/reportingTelegram.ts"() {
30905
+ "use strict";
30906
+ init_classification();
30907
+ init_reportingShared();
30908
+ TELEGRAM_DETAIL_LIMIT = 5;
30909
+ SUMMARY_TIMEZONE = "Europe/Moscow";
30910
+ SUMMARY_TIMEZONE_LABEL = "MSK";
30911
+ escapeHtml5 = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
30912
+ formatMskDateTime2 = (timestamp) => new Intl.DateTimeFormat("ru-RU", {
30913
+ timeZone: SUMMARY_TIMEZONE,
30914
+ year: "numeric",
30915
+ month: "2-digit",
30916
+ day: "2-digit",
30917
+ hour: "2-digit",
30918
+ minute: "2-digit",
30919
+ hour12: false
30920
+ }).format(new Date(timestamp));
31002
30921
  buildRuntimeParityMessage = ({
31003
- window,
31004
- connectorName,
31005
- replayEnv,
31006
30922
  runtimeGatesEnabled,
31007
- toleranceBars,
31008
- toleranceMs,
31009
- replayTargetsCount,
31010
- comparedTargetsCount,
31011
- replayErrors,
31012
- sourceCounts,
31013
- rawRuntimeEntriesCount,
31014
- runtimeEntriesCount,
31015
- runtimeDuplicateEntriesCount,
31016
- backtestEntriesCount,
31017
- matchedCount: _matchedCount,
31018
- runtimeOnlyCount,
31019
- backtestOnlyCount,
31020
- matchedSummary,
31021
- classifiedRuntimeOnly,
31022
- classifiedBacktestOnly,
31023
- runtimeSignalEvaluationsCount,
31024
- strategyRows,
31025
- runtimeGateWarningCounts
30923
+ runtimeGateWarningCounts,
30924
+ ...context
31026
30925
  }) => {
31027
30926
  const lines = [];
31028
30927
  lines.push("\u{1F9EA} <b>TradeJS runtime parity</b>");
31029
30928
  lines.push("");
31030
30929
  lines.push("\u{1F552} <b>Window</b>");
31031
30930
  lines.push(
31032
- `<b>${escapeHtml5(formatMskDateTime2(window.start))} - ${escapeHtml5(formatMskDateTime2(window.end))} ${SUMMARY_TIMEZONE_LABEL}</b>`
30931
+ `<b>${escapeHtml5(formatMskDateTime2(context.window.start))} - ${escapeHtml5(formatMskDateTime2(context.window.end))} ${SUMMARY_TIMEZONE_LABEL}</b>`
31033
30932
  );
31034
30933
  lines.push("");
31035
- lines.push(`\u{1F50C} Connector: <b>${escapeHtml5(connectorName)}</b>`);
31036
- lines.push(`\u{1F9EC} Replay env: <b>${escapeHtml5(replayEnv)}</b>`);
30934
+ lines.push(`\u{1F50C} Connector: <b>${escapeHtml5(context.connectorName)}</b>`);
30935
+ lines.push(`\u{1F9EC} Replay env: <b>${escapeHtml5(context.replayEnv)}</b>`);
31037
30936
  lines.push(
31038
- `\u{1F3AF} Tolerance: <b>${toleranceBars} bar(s) / ${(toleranceMs / 6e4).toFixed(0)}m</b>`
30937
+ `\u{1F3AF} Tolerance: <b>${context.toleranceBars} bar(s) / ${(context.toleranceMs / 6e4).toFixed(0)}m</b>`
31039
30938
  );
31040
30939
  lines.push("");
31041
30940
  lines.push("\u{1F4CC} <b>Overview</b>");
31042
30941
  lines.push(
31043
- `\u2022 Targets: <b>${replayTargetsCount}</b> / compared <b>${comparedTargetsCount}</b> / errors <b>${replayErrors.length}</b>`
30942
+ `\u2022 Targets: <b>${context.replayTargetsCount}</b> / compared <b>${context.comparedTargetsCount}</b> / errors <b>${context.replayErrors.length}</b>`
31044
30943
  );
31045
30944
  lines.push(
31046
- `\u2022 Sources: <b>${escapeHtml5(formatSourceCountsSummary(sourceCounts))}</b>`
30945
+ `\u2022 Sources: <b>${escapeHtml5(formatSourceCountsSummary(context.sourceCounts))}</b>`
31047
30946
  );
31048
30947
  lines.push("");
31049
30948
  lines.push("\u{1F4C8} <b>Entries</b>");
31050
30949
  lines.push(
31051
- `\u2022 Runtime: <b>${escapeHtml5(
31052
- formatRuntimeEntriesSummary({
31053
- rawRuntimeEntriesCount,
31054
- runtimeEntriesCount,
31055
- runtimeDuplicateEntriesCount
31056
- })
31057
- )}</b>`
30950
+ `\u2022 Runtime: <b>${escapeHtml5(formatRuntimeEntriesSummary(context))}</b>`
31058
30951
  );
31059
- lines.push(`\u2022 Backtest: <b>${backtestEntriesCount}</b>`);
30952
+ lines.push(`\u2022 Backtest: <b>${context.backtestEntriesCount}</b>`);
31060
30953
  lines.push(
31061
- `\u2022 Runtime only: <b>${runtimeOnlyCount}</b> / Backtest only: <b>${backtestOnlyCount}</b>`
30954
+ `\u2022 Runtime only: <b>${context.runtimeOnlyCount}</b> / Backtest only: <b>${context.backtestOnlyCount}</b>`
31062
30955
  );
31063
30956
  lines.push(
31064
- `\u2022 Matched deltas: price avg/max=<b>${escapeHtml5(formatPercent4(matchedSummary.avgPriceDeltaPct))} / ${escapeHtml5(formatPercent4(matchedSummary.maxPriceDeltaPct))}</b>, time avg/max=<b>${escapeHtml5(formatMinutes2(matchedSummary.avgTimestampDiffMs))} / ${escapeHtml5(formatMinutes2(matchedSummary.maxTimestampDiffMs))}</b>`
30957
+ `\u2022 Matched deltas: price avg/max=<b>${escapeHtml5(formatPercent4(context.matchedSummary.avgPriceDeltaPct))} / ${escapeHtml5(formatPercent4(context.matchedSummary.maxPriceDeltaPct))}</b>, time avg/max=<b>${escapeHtml5(formatMinutes2(context.matchedSummary.avgTimestampDiffMs))} / ${escapeHtml5(formatMinutes2(context.matchedSummary.maxTimestampDiffMs))}</b>`
31065
30958
  );
31066
- if (classifiedRuntimeOnly.length) {
30959
+ if (context.classifiedRuntimeOnly.length) {
31067
30960
  lines.push(
31068
- `\u2022 Runtime-only classes: <code>${escapeHtml5(summarizeRuntimeOnlyClassifications(classifiedRuntimeOnly))}</code>`
30961
+ `\u2022 Runtime-only classes: <code>${escapeHtml5(summarizeRuntimeOnlyClassifications(context.classifiedRuntimeOnly))}</code>`
31069
30962
  );
31070
30963
  }
31071
- if (classifiedBacktestOnly.length) {
30964
+ if (context.classifiedBacktestOnly.length) {
31072
30965
  lines.push(
31073
- `\u2022 Backtest-only classes: <code>${escapeHtml5(summarizeBacktestOnlyClassifications(classifiedBacktestOnly))}</code>`
30966
+ `\u2022 Backtest-only classes: <code>${escapeHtml5(summarizeBacktestOnlyClassifications(context.classifiedBacktestOnly))}</code>`
31074
30967
  );
31075
30968
  }
31076
- if (runtimeSignalEvaluationsCount) {
30969
+ if (context.runtimeSignalEvaluationsCount) {
31077
30970
  lines.push(
31078
- `\u2022 Runtime evaluations: <b>${runtimeSignalEvaluationsCount}</b>`
30971
+ `\u2022 Runtime evaluations: <b>${context.runtimeSignalEvaluationsCount}</b>`
31079
30972
  );
31080
30973
  }
31081
- const mismatchRows = buildMismatchSummaryRows({
31082
- classifiedRuntimeOnly,
31083
- classifiedBacktestOnly
31084
- });
30974
+ const mismatchRows = buildMismatchSummaryRows(context);
31085
30975
  if (mismatchRows.length) {
31086
30976
  lines.push("");
31087
- lines.push(`\u{1F50E} <b>Mismatches</b>`);
30977
+ lines.push("\u{1F50E} <b>Mismatches</b>");
31088
30978
  for (const item of mismatchRows.slice(0, TELEGRAM_DETAIL_LIMIT)) {
31089
30979
  lines.push(`\u2022 <code>${escapeHtml5(item.line)}</code>`);
31090
30980
  }
@@ -31094,17 +30984,17 @@ var init_runtimeParity2 = __esm({
31094
30984
  );
31095
30985
  }
31096
30986
  }
31097
- const strategyIssueRows = buildStrategyIssueRows(strategyRows);
30987
+ const strategyIssueRows = buildStrategyIssueRows(context.strategyRows);
31098
30988
  if (strategyIssueRows.length) {
31099
30989
  lines.push("");
31100
30990
  lines.push("\u{1F4CA} <b>Strategy issues</b>");
31101
30991
  for (const line of strategyIssueRows) {
31102
30992
  lines.push(`\u2022 ${escapeHtml5(line.slice(2))}`);
31103
30993
  }
31104
- } else if (strategyRows.length) {
30994
+ } else if (context.strategyRows.length) {
31105
30995
  lines.push("");
31106
30996
  lines.push(
31107
- `\u{1F4CA} <b>Strategies</b>: clean <b>${strategyRows.length}</b> / total <b>${strategyRows.length}</b>`
30997
+ `\u{1F4CA} <b>Strategies</b>: clean <b>${context.strategyRows.length}</b> / total <b>${context.strategyRows.length}</b>`
31108
30998
  );
31109
30999
  }
31110
31000
  if (runtimeGateWarningCounts.size && !runtimeGatesEnabled) {
@@ -31118,17 +31008,17 @@ var init_runtimeParity2 = __esm({
31118
31008
  );
31119
31009
  }
31120
31010
  }
31121
- if (replayErrors.length) {
31011
+ if (context.replayErrors.length) {
31122
31012
  lines.push("");
31123
31013
  lines.push("\u274C <b>Replay errors</b>");
31124
- for (const error of replayErrors.slice(0, TELEGRAM_DETAIL_LIMIT)) {
31014
+ for (const error of context.replayErrors.slice(0, TELEGRAM_DETAIL_LIMIT)) {
31125
31015
  lines.push(
31126
31016
  `\u2022 <b>${escapeHtml5(error.strategy)}</b> ${escapeHtml5(error.symbol)}: <code>${escapeHtml5(error.message)}</code>`
31127
31017
  );
31128
31018
  }
31129
- if (replayErrors.length > TELEGRAM_DETAIL_LIMIT) {
31019
+ if (context.replayErrors.length > TELEGRAM_DETAIL_LIMIT) {
31130
31020
  lines.push(
31131
- `... <b>${replayErrors.length - TELEGRAM_DETAIL_LIMIT}</b> more`
31021
+ `... <b>${context.replayErrors.length - TELEGRAM_DETAIL_LIMIT}</b> more`
31132
31022
  );
31133
31023
  }
31134
31024
  }
@@ -31138,7 +31028,6 @@ var init_runtimeParity2 = __esm({
31138
31028
  window,
31139
31029
  connectorName,
31140
31030
  replayEnv,
31141
- runtimeGatesEnabled: _runtimeGatesEnabled,
31142
31031
  userName: userName3
31143
31032
  }) => [
31144
31033
  "\u{1F9EA} <b>TradeJS runtime parity</b>",
@@ -31151,19 +31040,278 @@ var init_runtimeParity2 = __esm({
31151
31040
  "",
31152
31041
  `\u26A0\uFE0F No replay targets found for user <b>${escapeHtml5(userName3)}</b>.`
31153
31042
  ].join("\n");
31154
- runtimeParity = async () => {
31155
- const window = resolveTimeWindow({
31156
- days: flags28.days ?? DEFAULT_LOOKBACK_DAYS,
31157
- startTime: flags28.startTime,
31158
- endTime: flags28.endTime,
31159
- defaultStartMs: Date.now() - DEFAULT_LOOKBACK_DAYS * 24 * 60 * 60 * 1e3,
31160
- defaultEndMs: Date.now()
31161
- });
31162
- const toleranceBars = Math.max(
31163
- 0,
31164
- Number.parseInt(String(flags28.toleranceBars ?? 1), 10) || 0
31043
+ }
31044
+ });
31045
+
31046
+ // src/lib/runtimeParity/reporting.ts
31047
+ var init_reporting = __esm({
31048
+ "src/lib/runtimeParity/reporting.ts"() {
31049
+ "use strict";
31050
+ init_reportingShared();
31051
+ init_reportingJson();
31052
+ init_reportingTerminal();
31053
+ init_reportingTelegram();
31054
+ }
31055
+ });
31056
+
31057
+ // src/scripts/runtimeParity.ts
31058
+ var runtimeParity_exports = {};
31059
+ __export(runtimeParity_exports, {
31060
+ buildRuntimeParityMessage: () => buildRuntimeParityMessage,
31061
+ main: () => main40,
31062
+ runtimeParity: () => runtimeParity
31063
+ });
31064
+ var import_config11, import_node_crypto14, import_args30, import_chalk44, import_constants15, import_time9, import_logger6, import_redis17, import_cli11, import_backtest3, import_connectors14, flags28, interval3, projectRoot13, DEFAULT_LOOKBACK_DAYS, DETAIL_LIMIT2, DEFAULT_REPLAY_ENV, resolveParityConnectorName, createConnector, buildReplayAiAnalyses, formatReplayEnvSummary, buildReplayConfig, warmReplayHistory, runtimeParity, main40;
31065
+ var init_runtimeParity2 = __esm({
31066
+ "src/scripts/runtimeParity.ts"() {
31067
+ "use strict";
31068
+ import_config11 = require("dotenv/config");
31069
+ import_node_crypto14 = require("crypto");
31070
+ import_args30 = __toESM(require("args"));
31071
+ import_chalk44 = __toESM(require("chalk"));
31072
+ import_constants15 = require("@tradejs/core/constants");
31073
+ import_time9 = require("@tradejs/core/time");
31074
+ import_logger6 = require("@tradejs/infra/logger");
31075
+ import_redis17 = require("@tradejs/infra/redis");
31076
+ import_cli11 = require("@tradejs/node/cli");
31077
+ import_backtest3 = require("@tradejs/node/backtest");
31078
+ import_connectors14 = require("@tradejs/node/connectors");
31079
+ init_runtimeParity();
31080
+ init_cliArgs();
31081
+ init_runtimeRedis();
31082
+ init_runtimeSignalsLoader();
31083
+ init_telegramReports();
31084
+ init_timeWindow();
31085
+ init_runFormatting();
31086
+ init_marketContextPrepare();
31087
+ init_historyPrepare();
31088
+ init_targets();
31089
+ init_evidence();
31090
+ init_analysis();
31091
+ init_reporting();
31092
+ init_reporting();
31093
+ init_runtimeModeConfig();
31094
+ import_args30.default.option(["u", "user"], "Use user config", "root");
31095
+ import_args30.default.option(
31096
+ ["o", "connector"],
31097
+ "Connector provider or name for parity replay (e.g. bybit, binance, coinbase, custom)",
31098
+ "bybit"
31099
+ );
31100
+ import_args30.default.option(["d", "days"], "Replay window in days", 3);
31101
+ import_args30.default.option(
31102
+ ["b", "startTime"],
31103
+ "Explicit replay start timestamp (ms or seconds)"
31104
+ );
31105
+ import_args30.default.option(["e", "endTime"], "Explicit replay end timestamp (ms or seconds)");
31106
+ import_args30.default.option(["s", "strategy"], "Only compare one strategy");
31107
+ import_args30.default.option(
31108
+ ["t", "tickers"],
31109
+ "Replay comma-separated symbols for all configured strategies"
31110
+ );
31111
+ import_args30.default.option(
31112
+ ["C", "cacheOnly"],
31113
+ "Do not refresh market history before replay",
31114
+ false
31115
+ );
31116
+ import_args30.default.option(
31117
+ ["a", "toleranceBars"],
31118
+ "Allowed entry timestamp drift in bars when matching runtime vs backtest",
31119
+ 1
31120
+ );
31121
+ import_args30.default.option(
31122
+ "fullUniverse",
31123
+ "Replay every configured strategy across the full connector universe. By default parity replays only runtime-traded and strategy-results symbols unless --tickers is provided.",
31124
+ false
31125
+ );
31126
+ import_args30.default.option(
31127
+ "runtimeGates",
31128
+ "Force runtime AI/ML gates for all replay targets. Runtime-gated configs are replayed with gates by default.",
31129
+ false
31130
+ );
31131
+ import_args30.default.option(["N", "notify"], "Send parity summary to Telegram", false);
31132
+ import_args30.default.option(["D", "details"], "Print unmatched entry details (capped)", false);
31133
+ process.argv = normalizeCliArgv(process.argv, {
31134
+ "-C": "--cacheOnly",
31135
+ "-D": "--details",
31136
+ "-E": "--endTime",
31137
+ "-N": "--notify",
31138
+ "-S": "--startTime",
31139
+ "-T": "--toleranceBars"
31140
+ });
31141
+ flags28 = import_args30.default.parse(process.argv);
31142
+ interval3 = "15";
31143
+ projectRoot13 = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
31144
+ DEFAULT_LOOKBACK_DAYS = 3;
31145
+ DETAIL_LIMIT2 = 10;
31146
+ DEFAULT_REPLAY_ENV = flags28.runtimeGates ? "PARITY" : "BACKTEST";
31147
+ resolveParityConnectorName = async (value) => {
31148
+ const connectorName = await (0, import_connectors14.resolveConnectorName)(value, projectRoot13);
31149
+ if (connectorName) {
31150
+ return connectorName;
31151
+ }
31152
+ import_logger6.logger.warn(
31153
+ 'Unknown connector "%s". Fallback to %s.',
31154
+ String(value || "").trim() || String(value),
31155
+ import_connectors14.DEFAULT_CONNECTOR_NAME
31165
31156
  );
31166
- const toleranceMs = toleranceBars * 15 * 60 * 1e3;
31157
+ return import_connectors14.DEFAULT_CONNECTOR_NAME;
31158
+ };
31159
+ createConnector = async ({
31160
+ userName: userName3,
31161
+ connectorName
31162
+ }) => {
31163
+ const connectorFactory = await (0, import_connectors14.getConnectorCreatorByName)(
31164
+ connectorName,
31165
+ projectRoot13
31166
+ );
31167
+ if (!connectorFactory) {
31168
+ throw new Error(`Connector "${connectorName}" is not registered`);
31169
+ }
31170
+ return await connectorFactory({ userName: userName3 });
31171
+ };
31172
+ buildReplayAiAnalyses = ({
31173
+ runtimeTrades,
31174
+ runtimeSignalEvaluations,
31175
+ toleranceMs
31176
+ }) => {
31177
+ const snapshots = [
31178
+ ...runtimeTrades.filter(
31179
+ (trade) => trade.aiAnalysis && typeof trade.entryTimestamp === "number" && Number.isFinite(trade.entryTimestamp)
31180
+ ).map((trade) => ({
31181
+ strategy: trade.strategy,
31182
+ symbol: trade.symbol,
31183
+ direction: trade.direction,
31184
+ timestamp: trade.entryTimestamp,
31185
+ toleranceMs,
31186
+ analysis: trade.aiAnalysis
31187
+ })),
31188
+ ...runtimeSignalEvaluations.filter(
31189
+ (evaluation) => evaluation.aiAnalysis && evaluation.direction && typeof evaluation.timestamp === "number" && Number.isFinite(evaluation.timestamp)
31190
+ ).map((evaluation) => ({
31191
+ strategy: evaluation.strategy,
31192
+ symbol: evaluation.symbol,
31193
+ direction: evaluation.direction,
31194
+ timestamp: evaluation.timestamp,
31195
+ toleranceMs,
31196
+ analysis: evaluation.aiAnalysis
31197
+ }))
31198
+ ];
31199
+ const seen = /* @__PURE__ */ new Set();
31200
+ return snapshots.filter((snapshot) => {
31201
+ const key = `${snapshot.strategy}:${snapshot.symbol}:${snapshot.direction}:${snapshot.timestamp}`;
31202
+ if (seen.has(key)) {
31203
+ return false;
31204
+ }
31205
+ seen.add(key);
31206
+ return true;
31207
+ });
31208
+ };
31209
+ formatReplayEnvSummary = (configs) => {
31210
+ let backtest2 = 0;
31211
+ let parity = 0;
31212
+ for (const config2 of configs) {
31213
+ if (config2.ENV === "PARITY") {
31214
+ parity += 1;
31215
+ } else {
31216
+ backtest2 += 1;
31217
+ }
31218
+ }
31219
+ if (parity > 0 && backtest2 > 0) {
31220
+ return `MIXED (PARITY=${parity}, BACKTEST=${backtest2})`;
31221
+ }
31222
+ if (parity > 0) {
31223
+ return "PARITY";
31224
+ }
31225
+ return "BACKTEST";
31226
+ };
31227
+ buildReplayConfig = async ({
31228
+ userName: userName3,
31229
+ strategy,
31230
+ symbol,
31231
+ replayInputsIndex,
31232
+ toleranceMs
31233
+ }) => {
31234
+ const [userConfig, strategyResults] = await Promise.all([
31235
+ (0, import_redis17.getData)(import_redis17.redisKeys.strategyConfig(userName3, strategy), {}),
31236
+ (0, import_redis17.getData)(import_redis17.redisKeys.strategyResults(userName3, strategy), {})
31237
+ ]);
31238
+ const typedResults = strategyResults ?? {};
31239
+ const symbolResult = typedResults?.[symbol];
31240
+ const symbolConfig = symbolResult && symbolResult.config && typeof symbolResult.config === "object" ? symbolResult.config : {};
31241
+ const replayInputs = replayInputsIndex.get(toTargetKey({ strategy, symbol }));
31242
+ const aiReplayAnalyses = buildReplayAiAnalyses({
31243
+ runtimeTrades: replayInputs?.runtimeTrades ?? [],
31244
+ runtimeSignalEvaluations: replayInputs?.runtimeSignalEvaluations ?? [],
31245
+ strategy,
31246
+ symbol,
31247
+ toleranceMs
31248
+ });
31249
+ const strategyConfig = {
31250
+ ...userConfig,
31251
+ ...symbolConfig
31252
+ };
31253
+ return buildRuntimeModeStrategyConfig({
31254
+ strategyConfig,
31255
+ env: resolveReplayStrategyEnv({
31256
+ strategyConfig,
31257
+ forceRuntimeGates: Boolean(flags28.runtimeGates)
31258
+ }),
31259
+ interval: interval3,
31260
+ makeOrders: true,
31261
+ recordRuntimeTrades: false,
31262
+ aiReplayAnalyses
31263
+ });
31264
+ };
31265
+ warmReplayHistory = async ({
31266
+ userName: userName3,
31267
+ connectorName,
31268
+ targets,
31269
+ preloadStart,
31270
+ preloadEnd
31271
+ }) => {
31272
+ const connector = await createConnector({ userName: userName3, connectorName });
31273
+ const symbols = [...new Set(targets.map((target) => target.symbol))];
31274
+ const warmStartedAt = Date.now();
31275
+ console.log(
31276
+ import_chalk44.default.gray(
31277
+ `Warm history: ${symbols.length} symbol(s) on ${connectorName}, preload ${(0, import_time9.formatUnix)(preloadStart)} -> ${(0, import_time9.formatUnix)(preloadEnd)}`
31278
+ )
31279
+ );
31280
+ const btcReferences = await loadBtcReferenceConnectors({
31281
+ connectorName,
31282
+ marketConnector: connector,
31283
+ userName: userName3,
31284
+ projectRoot: projectRoot13,
31285
+ shouldUseDedicatedReferences: true,
31286
+ warn: (message) => import_logger6.logger.warn(message)
31287
+ });
31288
+ await updateMarketHistoryWithBtcReferences({
31289
+ marketConnector: connector,
31290
+ connectorName,
31291
+ btcReferences,
31292
+ interval: interval3,
31293
+ symbols,
31294
+ preloadStart,
31295
+ preloadEnd,
31296
+ log: (message) => console.log(import_chalk44.default.gray(message))
31297
+ });
31298
+ console.log(
31299
+ import_chalk44.default.gray(`Warm history: done in ${formatDuration(warmStartedAt)}`)
31300
+ );
31301
+ };
31302
+ runtimeParity = async () => {
31303
+ const window = resolveTimeWindow({
31304
+ days: flags28.days ?? DEFAULT_LOOKBACK_DAYS,
31305
+ startTime: flags28.startTime,
31306
+ endTime: flags28.endTime,
31307
+ defaultStartMs: Date.now() - DEFAULT_LOOKBACK_DAYS * 24 * 60 * 60 * 1e3,
31308
+ defaultEndMs: Date.now()
31309
+ });
31310
+ const toleranceBars = Math.max(
31311
+ 0,
31312
+ Number.parseInt(String(flags28.toleranceBars ?? 1), 10) || 0
31313
+ );
31314
+ const toleranceMs = toleranceBars * 15 * 60 * 1e3;
31167
31315
  const connectorName = await resolveParityConnectorName(flags28.connector);
31168
31316
  const requestedSymbols = parseSymbolsFromCLI2(String(flags28.tickers || ""));
31169
31317
  const requestedSymbolSet = requestedSymbols.length ? new Set(requestedSymbols) : null;
@@ -31173,28 +31321,18 @@ var init_runtimeParity2 = __esm({
31173
31321
  userName: flags28.user,
31174
31322
  connectorName
31175
31323
  });
31176
- const [allRuntimeTrades, allRuntimeSignals, allRuntimeSignalEvaluations] = await Promise.all([
31177
- loadRuntimeTrades(flags28.user, {
31178
- startTime: window.start,
31179
- endTime: window.end
31180
- }),
31181
- loadRuntimeSignals(flags28.user, {
31182
- startTime: window.start,
31183
- endTime: window.end
31184
- }),
31185
- loadRuntimeSignalEvaluations(flags28.user, {
31186
- startTime: window.start,
31187
- endTime: window.end
31188
- })
31189
- ]);
31190
- const runtimeTrades = allRuntimeTrades.filter(
31191
- (trade) => trade.entryTimestamp >= window.start && trade.entryTimestamp <= window.end && (!flags28.strategy || trade.strategy === flags28.strategy) && (!requestedSymbolSet || requestedSymbolSet.has(trade.symbol))
31192
- );
31193
- const runtimeSignals = allRuntimeSignals.filter(
31194
- (signal) => signal.timestamp >= window.start && signal.timestamp <= window.end && (!flags28.strategy || signal.strategy === flags28.strategy) && (!requestedSymbolSet || requestedSymbolSet.has(signal.symbol))
31195
- );
31196
- const runtimeSignalEvaluations = allRuntimeSignalEvaluations.filter(
31197
- (evaluation) => evaluation.timestamp >= window.start && evaluation.timestamp <= window.end && (!flags28.strategy || evaluation.strategy === flags28.strategy) && (!requestedSymbolSet || requestedSymbolSet.has(evaluation.symbol))
31324
+ const { runtimeTrades, runtimeSignals, runtimeSignalEvaluations } = await loadRuntimeParityEvidence(
31325
+ {
31326
+ userName: flags28.user,
31327
+ window,
31328
+ strategy: flags28.strategy,
31329
+ symbols: requestedSymbolSet
31330
+ },
31331
+ {
31332
+ loadRuntimeTrades,
31333
+ loadRuntimeSignals,
31334
+ loadRuntimeSignalEvaluations
31335
+ }
31198
31336
  );
31199
31337
  const connectorSymbols = requestedSymbols.length ? requestedSymbols : await (0, import_cli11.getTickers)(parityConnector);
31200
31338
  const replayInputsIndex = buildReplayInputsIndex({
@@ -31211,8 +31349,8 @@ var init_runtimeParity2 = __esm({
31211
31349
  });
31212
31350
  if (!replayTargets.length) {
31213
31351
  console.log(
31214
- import_chalk43.default.yellow(
31215
- `No replay targets found for ${flags28.user} in ${(0, import_time7.formatUnix)(window.start)} -> ${(0, import_time7.formatUnix)(window.end)}.`
31352
+ import_chalk44.default.yellow(
31353
+ `No replay targets found for ${flags28.user} in ${(0, import_time9.formatUnix)(window.start)} -> ${(0, import_time9.formatUnix)(window.end)}.`
31216
31354
  )
31217
31355
  );
31218
31356
  if (flags28.notify) {
@@ -31230,7 +31368,7 @@ var init_runtimeParity2 = __esm({
31230
31368
  return;
31231
31369
  }
31232
31370
  const sourceCounts = countReplayTargetSources(replayTargets);
31233
- const preloadStart = (0, import_time7.getBacktestPreloadStart)(
31371
+ const preloadStart = (0, import_time9.getBacktestPreloadStart)(
31234
31372
  window.start,
31235
31373
  import_constants15.BACKTEST_PRELOAD_DAYS
31236
31374
  );
@@ -31239,7 +31377,7 @@ var init_runtimeParity2 = __esm({
31239
31377
  replayTargets.map((target) => target.symbol)
31240
31378
  ).size;
31241
31379
  console.log(
31242
- import_chalk43.default.gray(
31380
+ import_chalk44.default.gray(
31243
31381
  `Replay queue: ${replayTargets.length} target(s), ${replaySymbolsCount} symbol(s), mode=${flags28.fullUniverse ? "full-universe" : requestedSymbols.length ? "explicit-tickers" : "runtime+results"}`
31244
31382
  )
31245
31383
  );
@@ -31274,7 +31412,7 @@ var init_runtimeParity2 = __esm({
31274
31412
  aiEnabled: hasParityAiTargets,
31275
31413
  mlEnabled: hasParityMlTargets,
31276
31414
  strategyNames: replayTargets.map((target) => target.strategy),
31277
- log: (message) => console.log(import_chalk43.default.gray(message))
31415
+ log: (message) => console.log(import_chalk44.default.gray(message))
31278
31416
  });
31279
31417
  if (!flags28.cacheOnly) {
31280
31418
  await warmReplayHistory({
@@ -31342,139 +31480,61 @@ var init_runtimeParity2 = __esm({
31342
31480
  }
31343
31481
  if (progressPosition === 1 || progressPosition === replayTargets.length || progressPosition % 25 === 0) {
31344
31482
  console.log(
31345
- import_chalk43.default.gray(
31483
+ import_chalk44.default.gray(
31346
31484
  `Replay progress: ${progressPosition}/${replayTargets.length}, ok=${successfulTargetKeys.size}, errors=${replayErrors.length}, current=${target.strategy} ${target.symbol}, elapsed=${formatDuration(replayStartedAt)}`
31347
31485
  )
31348
31486
  );
31349
31487
  }
31350
31488
  }
31351
- const comparableRuntimeTrades = runtimeTrades.filter(
31352
- (trade) => successfulTargetKeys.has(toTargetKey(trade))
31353
- );
31354
- const rawRuntimeEntries = extractRuntimeParityEntries(
31355
- comparableRuntimeTrades
31356
- );
31357
- const runtimeDedupe = dedupeRuntimeParityEntries(rawRuntimeEntries);
31358
- const runtimeEntries = runtimeDedupe.entries;
31359
- const comparison = compareTradeParityEntries({
31489
+ const {
31490
+ rawRuntimeEntries,
31491
+ runtimeDedupe,
31360
31492
  runtimeEntries,
31361
- backtestEntries,
31362
- toleranceMs
31363
- });
31364
- const classifiedBacktestOnly = classifyBacktestOnlyEntries({
31365
- entries: comparison.backtestOnly,
31493
+ comparison,
31494
+ classifiedBacktestOnly,
31495
+ classifiedRuntimeOnly,
31496
+ matchedSummary: summary,
31497
+ strategyRows
31498
+ } = analyzeRuntimeParity({
31499
+ runtimeTrades,
31366
31500
  runtimeSignals,
31367
31501
  runtimeSignalEvaluations,
31368
- toleranceMs
31369
- });
31370
- const classifiedRuntimeOnly = classifyRuntimeOnlyEntries({
31371
- entries: comparison.runtimeOnly,
31372
- replaySignalEvaluations,
31373
31502
  backtestEntries,
31503
+ replaySignalEvaluations,
31504
+ replayTargets,
31505
+ successfulTargetKeys,
31506
+ replayErrors,
31374
31507
  toleranceMs
31375
31508
  });
31376
- const summary = summarizeMatchedParity(comparison.matched);
31377
- console.log(import_chalk43.default.cyan("TradeJS runtime parity"));
31378
- console.log(
31379
- `Window: ${(0, import_time7.formatUnix)(window.start)} -> ${(0, import_time7.formatUnix)(window.end)} (${window.source})`
31380
- );
31381
- console.log(`Connector: ${connectorName}`);
31382
- console.log(
31383
- `Replay env: ${replayEnvSummary}${flags28.runtimeGates || replayEnvSummary.includes("PARITY") ? " (runtime AI/ML gates enabled)" : " (core/backtest gates only)"}`
31384
- );
31385
- console.log(
31386
- `Tolerance: ${toleranceBars} bar(s) / ${(toleranceMs / 6e4).toFixed(0)}m`
31387
- );
31388
- console.log(
31389
- `Summary: targets=${replayTargets.length}, compared=${successfulTargetKeys.size}, errors=${replayErrors.length}, runtime=${runtimeEntries.length}, backtest=${backtestEntries.length}, runtimeOnly=${comparison.runtimeOnly.length}, backtestOnly=${comparison.backtestOnly.length}, evaluations=${runtimeSignalEvaluations.length}`
31390
- );
31391
- console.log(`Sources: ${formatSourceCountsSummary(sourceCounts)}`);
31392
- console.log(
31393
- `Runtime entries: ${formatRuntimeEntriesSummary({
31509
+ writeRuntimeParityTerminalReport(
31510
+ buildRuntimeParityTerminalReport({
31511
+ window,
31512
+ connectorName,
31513
+ replayEnv: replayEnvSummary,
31514
+ runtimeGatesEnabled: Boolean(flags28.runtimeGates) || replayEnvSummary.includes("PARITY"),
31515
+ runtimeGatesRequested: Boolean(flags28.runtimeGates),
31516
+ toleranceBars,
31517
+ toleranceMs,
31518
+ replayTargetsCount: replayTargets.length,
31519
+ comparedTargetsCount: successfulTargetKeys.size,
31520
+ replayErrors,
31521
+ sourceCounts,
31394
31522
  rawRuntimeEntriesCount: rawRuntimeEntries.length,
31395
31523
  runtimeEntriesCount: runtimeEntries.length,
31396
- runtimeDuplicateEntriesCount: runtimeDedupe.duplicateEntries.length
31397
- })}`
31398
- );
31399
- console.log(
31400
- `Matched deltas: price avg/max=${formatPercent4(summary.avgPriceDeltaPct)} / ${formatPercent4(summary.maxPriceDeltaPct)}, time avg/max=${formatMinutes2(summary.avgTimestampDiffMs)} / ${formatMinutes2(summary.maxTimestampDiffMs)}`
31524
+ runtimeDuplicateGroupsCount: runtimeDedupe.duplicateGroups.length,
31525
+ runtimeDuplicateEntriesCount: runtimeDedupe.duplicateEntries.length,
31526
+ backtestEntriesCount: backtestEntries.length,
31527
+ runtimeOnlyCount: comparison.runtimeOnly.length,
31528
+ backtestOnlyCount: comparison.backtestOnly.length,
31529
+ matchedSummary: summary,
31530
+ classifiedRuntimeOnly,
31531
+ classifiedBacktestOnly,
31532
+ runtimeSignalEvaluationsCount: runtimeSignalEvaluations.length,
31533
+ strategyRows,
31534
+ runtimeGateWarningCounts,
31535
+ detailLimit: DETAIL_LIMIT2
31536
+ })
31401
31537
  );
31402
- if (runtimeDedupe.duplicateGroups.length) {
31403
- console.log(
31404
- `Runtime duplicate groups: ${runtimeDedupe.duplicateGroups.length}, duplicate entries: ${runtimeDedupe.duplicateEntries.length}`
31405
- );
31406
- }
31407
- if (classifiedRuntimeOnly.length) {
31408
- console.log(
31409
- `Runtime only classifications: ${summarizeRuntimeOnlyClassifications(classifiedRuntimeOnly)}`
31410
- );
31411
- }
31412
- if (classifiedBacktestOnly.length) {
31413
- console.log(
31414
- `Backtest only classifications: ${summarizeBacktestOnlyClassifications(classifiedBacktestOnly)}`
31415
- );
31416
- }
31417
- if (runtimeSignalEvaluations.length) {
31418
- console.log(`Runtime evaluations: ${runtimeSignalEvaluations.length}`);
31419
- }
31420
- const strategyRows = summarizeByStrategy({
31421
- targets: replayTargets,
31422
- successfulTargetKeys,
31423
- replayErrors,
31424
- runtimeEntries,
31425
- runtimeDuplicateEntries: runtimeDedupe.duplicateEntries,
31426
- backtestEntries,
31427
- matchedEntries: comparison.matched,
31428
- runtimeOnlyEntries: comparison.runtimeOnly,
31429
- backtestOnlyEntries: comparison.backtestOnly
31430
- });
31431
- const mismatchRows = buildMismatchSummaryRows({
31432
- classifiedRuntimeOnly,
31433
- classifiedBacktestOnly
31434
- });
31435
- const strategyIssueRows = buildStrategyIssueRows(strategyRows);
31436
- if (mismatchRows.length) {
31437
- console.log("");
31438
- console.log(import_chalk43.default.yellow("Signal mismatches"));
31439
- for (const item of mismatchRows.slice(0, DETAIL_LIMIT)) {
31440
- console.log(`- ${item.line}`);
31441
- }
31442
- if (mismatchRows.length > DETAIL_LIMIT) {
31443
- console.log(`- ... ${mismatchRows.length - DETAIL_LIMIT} more`);
31444
- }
31445
- }
31446
- console.log("");
31447
- if (strategyIssueRows.length) {
31448
- console.log(import_chalk43.default.cyan("Strategy issues"));
31449
- for (const line of strategyIssueRows) {
31450
- console.log(line);
31451
- }
31452
- } else if (strategyRows.length) {
31453
- console.log(
31454
- `Strategies: clean ${strategyRows.length}/${strategyRows.length}`
31455
- );
31456
- }
31457
- if (runtimeGateWarningCounts.size && !flags28.runtimeGates) {
31458
- console.log("");
31459
- console.log(import_chalk43.default.yellow("Warnings"));
31460
- for (const [strategy, count] of [
31461
- ...runtimeGateWarningCounts.entries()
31462
- ].sort(([left], [right]) => left.localeCompare(right))) {
31463
- console.log(
31464
- `- ${strategy} uses AI/ML runtime gates on ${count} replay target(s); BACKTEST replay covers core execution, not live gating.`
31465
- );
31466
- }
31467
- }
31468
- if (replayErrors.length) {
31469
- console.log("");
31470
- console.log(import_chalk43.default.red("Replay errors"));
31471
- for (const error of replayErrors.slice(0, DETAIL_LIMIT)) {
31472
- console.log(`- ${error.strategy} ${error.symbol}: ${error.message}`);
31473
- }
31474
- if (replayErrors.length > DETAIL_LIMIT) {
31475
- console.log(`- ... ${replayErrors.length - DETAIL_LIMIT} more`);
31476
- }
31477
- }
31478
31538
  if (flags28.details) {
31479
31539
  printRuntimeDuplicateDetails(runtimeDedupe.duplicateGroups);
31480
31540
  printClassifiedRuntimeOnlyDetails(classifiedRuntimeOnly);
@@ -31619,11 +31679,11 @@ var init_runtimeStrategies = __esm({
31619
31679
  });
31620
31680
 
31621
31681
  // src/lib/signals/skipStats.ts
31622
- var import_chalk44, import_logger7, createStrategySkipStats, recordStrategyReason, logStrategySkipStats;
31682
+ var import_chalk45, import_logger7, createStrategySkipStats, recordStrategyReason, logStrategySkipStats;
31623
31683
  var init_skipStats = __esm({
31624
31684
  "src/lib/signals/skipStats.ts"() {
31625
31685
  "use strict";
31626
- import_chalk44 = __toESM(require("chalk"));
31686
+ import_chalk45 = __toESM(require("chalk"));
31627
31687
  import_logger7 = require("@tradejs/infra/logger");
31628
31688
  init_runtimeSignalsStorage();
31629
31689
  createStrategySkipStats = (runtimeStrategies) => new Map(
@@ -31649,7 +31709,7 @@ var init_skipStats = __esm({
31649
31709
  );
31650
31710
  };
31651
31711
  logStrategySkipStats = (runtimeStrategies, strategyStats) => {
31652
- import_logger7.logger.info(import_chalk44.default.yellow("skip stats:"));
31712
+ import_logger7.logger.info(import_chalk45.default.yellow("skip stats:"));
31653
31713
  for (const { strategyName } of runtimeStrategies) {
31654
31714
  const stats = strategyStats.get(strategyName);
31655
31715
  if (!stats) {
@@ -32594,18 +32654,18 @@ var init_configuredScopes = __esm({
32594
32654
  });
32595
32655
 
32596
32656
  // src/lib/signals/runner.ts
32597
- var import_progress11, import_chalk45, import_connectors16, import_cli12, import_async5, import_strategies11, import_constants16, import_time8, import_logger10, import_runtimeHeartbeats, import_runtimeStrategies6, SLOW_SIGNALS_WARNING_MS, formatDuration3, resolvePositiveInteger2, resolveNonNegativeInteger, createSignalsRunner;
32657
+ var import_progress11, import_chalk46, import_connectors16, import_cli12, import_async5, import_strategies11, import_constants16, import_time10, import_logger10, import_runtimeHeartbeats, import_runtimeStrategies6, SLOW_SIGNALS_WARNING_MS, formatDuration3, resolvePositiveInteger2, resolveNonNegativeInteger, createSignalsRunner;
32598
32658
  var init_runner = __esm({
32599
32659
  "src/lib/signals/runner.ts"() {
32600
32660
  "use strict";
32601
32661
  import_progress11 = __toESM(require("progress"));
32602
- import_chalk45 = __toESM(require("chalk"));
32662
+ import_chalk46 = __toESM(require("chalk"));
32603
32663
  import_connectors16 = require("@tradejs/node/connectors");
32604
32664
  import_cli12 = require("@tradejs/node/cli");
32605
32665
  import_async5 = require("@tradejs/core/async");
32606
32666
  import_strategies11 = require("@tradejs/core/strategies");
32607
32667
  import_constants16 = require("@tradejs/core/constants");
32608
- import_time8 = require("@tradejs/core/time");
32668
+ import_time10 = require("@tradejs/core/time");
32609
32669
  import_logger10 = require("@tradejs/infra/logger");
32610
32670
  import_runtimeHeartbeats = require("@tradejs/infra/runtimeHeartbeats");
32611
32671
  import_runtimeStrategies6 = require("@tradejs/node/runtimeStrategies");
@@ -32666,7 +32726,7 @@ var init_runner = __esm({
32666
32726
  const timeOperation3 = (label, operation) => timeOperation(
32667
32727
  label,
32668
32728
  operation,
32669
- (message) => import_logger10.logger.info(import_chalk45.default.gray(message))
32729
+ (message) => import_logger10.logger.info(import_chalk46.default.gray(message))
32670
32730
  );
32671
32731
  const formatSlowSignalsWarning = (params) => [
32672
32732
  "<b>Slow yarn signals run</b>",
@@ -32781,7 +32841,7 @@ var init_runner = __esm({
32781
32841
  const selectedTickers = selection?.tickers ? [.../* @__PURE__ */ new Set([...selection.tickers, ...selectedActiveSymbols])] : void 0;
32782
32842
  if (selectedActiveSymbols.length) {
32783
32843
  import_logger10.logger.info(
32784
- import_chalk45.default.gray(
32844
+ import_chalk46.default.gray(
32785
32845
  `strategy selection retained active symbols: ${selectedActiveSymbols.join(", ")}`
32786
32846
  )
32787
32847
  );
@@ -32811,13 +32871,13 @@ var init_runner = __esm({
32811
32871
  instruments.map((instrument) => [instrument.symbol, instrument])
32812
32872
  );
32813
32873
  if (config2.showTickersList) {
32814
- console.log(import_chalk45.default.gray(JSON.stringify(tickers.sort(), null, 2)));
32874
+ console.log(import_chalk46.default.gray(JSON.stringify(tickers.sort(), null, 2)));
32815
32875
  return;
32816
32876
  }
32817
32877
  const projectConfig = await (0, import_cli12.loadTradejsConfig)(projectRoot15);
32818
32878
  projectHooks = projectConfig.hooks;
32819
- const currentTimestamp = (0, import_time8.getTimestamp)();
32820
- const preloadStart = (0, import_time8.getTimestamp)(import_constants16.SIGNALS_CLI_PRELOAD_DAYS);
32879
+ const currentTimestamp = (0, import_time10.getTimestamp)();
32880
+ const preloadStart = (0, import_time10.getTimestamp)(import_constants16.SIGNALS_CLI_PRELOAD_DAYS);
32821
32881
  if (!config2.cacheOnly && session.klineFeed) {
32822
32882
  const expectedSymbols = universe === "crypto" ? [.../* @__PURE__ */ new Set([...tickers, "BTCUSDT", "ETHUSDT"])] : tickers;
32823
32883
  session.klineFeed.setSubscriptions(expectedSymbols);
@@ -32832,7 +32892,7 @@ var init_runner = __esm({
32832
32892
  );
32833
32893
  await session.klineFeed.flush();
32834
32894
  import_logger10.logger.info(
32835
- import_chalk45.default.gray(
32895
+ import_chalk46.default.gray(
32836
32896
  `websocket candles: ready=${expectedSymbols.length - missingSymbols.length}/${expectedSymbols.length} missing=${missingSymbols.length}`
32837
32897
  )
32838
32898
  );
@@ -32843,7 +32903,7 @@ var init_runner = __esm({
32843
32903
  symbols: missingSymbols,
32844
32904
  preloadDays: import_constants16.SIGNALS_CLI_PRELOAD_DAYS,
32845
32905
  universe,
32846
- log: (message) => import_logger10.logger.info(import_chalk45.default.gray(message))
32906
+ log: (message) => import_logger10.logger.info(import_chalk46.default.gray(message))
32847
32907
  });
32848
32908
  await updateBtcReferenceHistory({
32849
32909
  marketConnector,
@@ -32851,7 +32911,7 @@ var init_runner = __esm({
32851
32911
  interval: interval5,
32852
32912
  preloadDays: import_constants16.SIGNALS_CLI_PRELOAD_DAYS,
32853
32913
  universe,
32854
- log: (message) => import_logger10.logger.info(import_chalk45.default.gray(message))
32914
+ log: (message) => import_logger10.logger.info(import_chalk46.default.gray(message))
32855
32915
  });
32856
32916
  } else if (!config2.cacheOnly) {
32857
32917
  await updateMarketHistoryWithBtcReferences({
@@ -32862,7 +32922,7 @@ var init_runner = __esm({
32862
32922
  symbols: tickers,
32863
32923
  preloadDays: import_constants16.SIGNALS_CLI_PRELOAD_DAYS,
32864
32924
  universe,
32865
- log: (message) => import_logger10.logger.info(import_chalk45.default.gray(message))
32925
+ log: (message) => import_logger10.logger.info(import_chalk46.default.gray(message))
32866
32926
  });
32867
32927
  }
32868
32928
  await prepareMarketContextForRun({
@@ -32876,7 +32936,7 @@ var init_runner = __esm({
32876
32936
  endMs: currentTimestamp,
32877
32937
  preloadStartMs: preloadStart,
32878
32938
  cacheOnly: config2.cacheOnly,
32879
- log: (message) => import_logger10.logger.info(import_chalk45.default.gray(message))
32939
+ log: (message) => import_logger10.logger.info(import_chalk46.default.gray(message))
32880
32940
  });
32881
32941
  if (config2.updateOnly) {
32882
32942
  return;
@@ -32971,7 +33031,7 @@ var init_runner = __esm({
32971
33031
  )
32972
33032
  );
32973
33033
  import_logger10.logger.info(
32974
- import_chalk45.default.yellow(
33034
+ import_chalk46.default.yellow(
32975
33035
  `loaded strategies (user=${config2.userName}): ${runtimeStrategies.map(formatConfiguredStrategyIdentity).join(", ")}`
32976
33036
  )
32977
33037
  );
@@ -33007,9 +33067,9 @@ var init_runner = __esm({
33007
33067
  width: 30
33008
33068
  }
33009
33069
  );
33010
- import_logger10.logger.info(import_chalk45.default.yellow(`tickers: ${tickers.length}`));
33070
+ import_logger10.logger.info(import_chalk46.default.yellow(`tickers: ${tickers.length}`));
33011
33071
  const signalsParallel = resolvePositiveInteger2(config2.parallel, 4);
33012
- import_logger10.logger.info(import_chalk45.default.yellow(`signal workers: ${signalsParallel}`));
33072
+ import_logger10.logger.info(import_chalk46.default.yellow(`signal workers: ${signalsParallel}`));
33013
33073
  const evaluateTicker = createSignalsTickerEvaluator({
33014
33074
  userName: config2.userName,
33015
33075
  makeOrders: config2.makeOrders,
@@ -33048,8 +33108,8 @@ var init_runner = __esm({
33048
33108
  signals3.push(...strategySignals);
33049
33109
  }
33050
33110
  bar.tick(1, {
33051
- found: import_chalk45.default.cyan(signals3.length),
33052
- symbol: import_chalk45.default.gray(symbol)
33111
+ found: import_chalk46.default.cyan(signals3.length),
33112
+ symbol: import_chalk46.default.gray(symbol)
33053
33113
  });
33054
33114
  });
33055
33115
  });
@@ -33153,7 +33213,7 @@ var init_runner = __esm({
33153
33213
  }
33154
33214
  }
33155
33215
  import_logger10.logger.info(
33156
- import_chalk45.default.yellow(
33216
+ import_chalk46.default.yellow(
33157
33217
  `signals ${status} in ${formatDuration3(durationMs)} (found=${signals3.length})`
33158
33218
  )
33159
33219
  );
@@ -34148,91 +34208,13 @@ var init_portfolioReplayConnector = __esm({
34148
34208
  }
34149
34209
  });
34150
34210
 
34151
- // src/lib/replay/historicalSignalsReplay.ts
34152
- var import_chalk46, import_progress12, import_backtest4, import_strategies12, import_time9, import_logger11, import_redis19, import_connectors17, import_cli13, import_strategies13, import_runtimeStrategies8, loadRuntimeStrategies, loadReferenceConnector, buildPreparedData, buildAfterSignalsContext, strategyIncludesSymbol, buildSymbolSharedReplayKey, runHistoricalSignalsReplay;
34153
- var init_historicalSignalsReplay = __esm({
34154
- "src/lib/replay/historicalSignalsReplay.ts"() {
34211
+ // src/lib/replay/historicalSignalsReplayPreparation.ts
34212
+ var buildPreparedData, strategyIncludesSymbol, buildSymbolSharedReplayKey, prepareHistoricalReplay;
34213
+ var init_historicalSignalsReplayPreparation = __esm({
34214
+ "src/lib/replay/historicalSignalsReplayPreparation.ts"() {
34155
34215
  "use strict";
34156
- import_chalk46 = __toESM(require("chalk"));
34157
- import_progress12 = __toESM(require("progress"));
34158
- import_backtest4 = require("@tradejs/core/backtest");
34159
- import_strategies12 = require("@tradejs/core/strategies");
34160
- import_time9 = require("@tradejs/core/time");
34161
- import_logger11 = require("@tradejs/infra/logger");
34162
- import_redis19 = require("@tradejs/infra/redis");
34163
- import_connectors17 = require("@tradejs/node/connectors");
34164
- import_cli13 = require("@tradejs/node/cli");
34165
- import_strategies13 = require("@tradejs/node/strategies");
34166
- import_runtimeStrategies8 = require("@tradejs/node/runtimeStrategies");
34167
- init_cliConfig2();
34168
34216
  init_support();
34169
- init_portfolioReplayConnector();
34170
34217
  init_windows();
34171
- init_hooks();
34172
- init_runtimeLineage();
34173
- loadRuntimeStrategies = async (runtimeStrategies) => {
34174
- const strategies = await Promise.all(
34175
- runtimeStrategies.map(async (runtimeStrategy) => {
34176
- const {
34177
- strategyName,
34178
- strategyRevision,
34179
- deploymentCompositionId,
34180
- strategyPackage,
34181
- strategyPackageVersion,
34182
- strategyDependencyVersions,
34183
- runtimePackageVersion,
34184
- strategyConfig,
34185
- selection
34186
- } = runtimeStrategy;
34187
- const strategyCreator = await (0, import_strategies13.getStrategyCreator)(
34188
- strategyName,
34189
- replayProjectRoot
34190
- );
34191
- if (!strategyCreator) {
34192
- throw new Error(`Unknown strategy: ${strategyName}`);
34193
- }
34194
- const installed = await (0, import_runtimeStrategies8.getRuntimeStrategyPackageMetadata)({
34195
- strategyName,
34196
- projectRoot: replayProjectRoot
34197
- });
34198
- if (installed.strategyPackage !== strategyPackage || installed.strategyPackageVersion !== strategyPackageVersion || JSON.stringify(installed.strategyDependencyVersions) !== JSON.stringify(strategyDependencyVersions) || installed.runtimePackageVersion !== runtimePackageVersion) {
34199
- throw new Error(
34200
- `Runtime evidence package mismatch for ${strategyName}: expected=${strategyPackage}@${strategyPackageVersion}/@tradejs/node@${runtimePackageVersion}, installed=${installed.strategyPackage ?? "missing"}@${installed.strategyPackageVersion ?? "missing"}/@tradejs/node@${installed.runtimePackageVersion ?? "missing"}`
34201
- );
34202
- }
34203
- const strategyResults = await (0, import_redis19.getData)(
34204
- import_redis19.redisKeys.strategyResults(replayUserName, strategyName),
34205
- {}
34206
- );
34207
- return {
34208
- strategyName,
34209
- strategyRevision,
34210
- deploymentCompositionId,
34211
- strategyPackage,
34212
- strategyPackageVersion,
34213
- strategyDependencyVersions,
34214
- runtimePackageVersion,
34215
- strategyCreator,
34216
- strategyConfig,
34217
- strategyResults,
34218
- ...selection ? { selection } : {}
34219
- };
34220
- })
34221
- );
34222
- return strategies;
34223
- };
34224
- loadReferenceConnector = async (connectorName) => {
34225
- const connectorFactory = await (0, import_connectors17.getConnectorCreatorByName)(
34226
- connectorName,
34227
- replayProjectRoot
34228
- );
34229
- if (!connectorFactory) {
34230
- throw new Error(`Connector "${connectorName}" is not registered`);
34231
- }
34232
- return await connectorFactory({
34233
- userName: replayUserName
34234
- });
34235
- };
34236
34218
  buildPreparedData = ({
34237
34219
  data,
34238
34220
  btcData,
@@ -34281,25 +34263,6 @@ var init_historicalSignalsReplay = __esm({
34281
34263
  btcCoinbasePrevData
34282
34264
  };
34283
34265
  };
34284
- buildAfterSignalsContext = ({
34285
- connector,
34286
- connectorName,
34287
- tickers,
34288
- runtimeStrategies,
34289
- interval: interval4
34290
- }) => ({
34291
- connector,
34292
- connectorName,
34293
- userName: replayUserName,
34294
- interval: interval4,
34295
- tickers: [...tickers],
34296
- runtimeStrategies: runtimeStrategies.map(
34297
- ({ strategyName, strategyConfig }) => ({
34298
- strategyName,
34299
- strategyConfig
34300
- })
34301
- )
34302
- });
34303
34266
  strategyIncludesSymbol = (strategy, symbol) => {
34304
34267
  const tickers = strategy.selection?.tickers;
34305
34268
  if (!tickers) return true;
@@ -34309,73 +34272,28 @@ var init_historicalSignalsReplay = __esm({
34309
34272
  );
34310
34273
  };
34311
34274
  buildSymbolSharedReplayKey = ({
34275
+ userName: userName3,
34312
34276
  connectorName,
34313
34277
  interval: interval4,
34314
34278
  symbol,
34315
34279
  start,
34316
34280
  end
34317
- }) => ["replay", replayUserName, connectorName, symbol, interval4, start, end].join(
34318
- ":"
34319
- );
34320
- runHistoricalSignalsReplay = async ({
34321
- preparedRun,
34322
- interval: interval4,
34323
- runtimeStrategies
34324
- }) => {
34325
- const startedAt = Date.now();
34326
- const signals2 = [];
34327
- const runtimeLineages = [];
34328
- const projectConfig = await (0, import_cli13.loadTradejsConfig)(replayProjectRoot);
34329
- const projectHooks = projectConfig.hooks;
34330
- const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
34331
- const replayConnector = createPortfolioReplayConnector(
34332
- preparedRun.marketConnector
34333
- );
34334
- const connectorName = String(
34335
- preparedRun.marketConnector?.name || import_connectors17.DEFAULT_CONNECTOR_NAME
34336
- ).trim() || import_connectors17.DEFAULT_CONNECTOR_NAME;
34337
- const binanceConnector = connectorName.toLowerCase() === import_connectors17.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Binance") : preparedRun.marketConnector;
34338
- const coinbaseConnector = connectorName.toLowerCase() === import_connectors17.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Coinbase") : preparedRun.marketConnector;
34339
- const [btcBinanceData, btcCoinbaseData] = await Promise.all([
34340
- binanceConnector.kline({
34341
- symbol: "BTCUSDT",
34342
- start: preparedRun.preloadStart,
34343
- end: preparedRun.window.end,
34344
- cacheOnly: true,
34345
- interval: interval4
34346
- }),
34347
- coinbaseConnector.kline({
34348
- symbol: "BTCUSDT",
34349
- start: preparedRun.preloadStart,
34350
- end: preparedRun.window.end,
34351
- cacheOnly: true,
34352
- interval: interval4
34353
- })
34354
- ]);
34355
- const btcMarketData = await preparedRun.marketConnector.kline({
34356
- symbol: "BTCUSDT",
34357
- start: preparedRun.preloadStart,
34358
- end: preparedRun.window.end,
34359
- cacheOnly: true,
34360
- interval: interval4
34361
- });
34362
- const ethMarketData = await preparedRun.marketConnector.kline({
34363
- symbol: "ETHUSDT",
34364
- start: preparedRun.preloadStart,
34365
- end: preparedRun.window.end,
34366
- cacheOnly: true,
34367
- interval: interval4
34368
- });
34281
+ }) => ["replay", userName3, connectorName, symbol, interval4, start, end].join(":");
34282
+ prepareHistoricalReplay = async (context, adapters) => {
34283
+ const {
34284
+ userName: userName3,
34285
+ projectRoot: projectRoot15,
34286
+ preparedRun,
34287
+ interval: interval4,
34288
+ connectorName,
34289
+ replayConnector,
34290
+ strategies,
34291
+ references
34292
+ } = context;
34369
34293
  const cycleSymbolsByTimestamp = /* @__PURE__ */ new Map();
34370
34294
  const sharedReplayKeyPrefixes = [];
34295
+ const runtimeLineages = [];
34371
34296
  let skippedSymbols = 0;
34372
- const prepareBar = new import_progress12.default(
34373
- "prepare :current/:total [:bar][:percent] skipped=:skipped :etas(s) :symbol",
34374
- {
34375
- total: preparedRun.tickers.length,
34376
- width: 30
34377
- }
34378
- );
34379
34297
  for (const symbol of preparedRun.tickers) {
34380
34298
  const data = await preparedRun.marketConnector.kline({
34381
34299
  symbol,
@@ -34386,22 +34304,23 @@ var init_historicalSignalsReplay = __esm({
34386
34304
  });
34387
34305
  const preparedData = buildPreparedData({
34388
34306
  data,
34389
- btcData: btcMarketData,
34390
- ethData: ethMarketData,
34391
- btcBinanceData,
34392
- btcCoinbaseData,
34307
+ btcData: references.btcMarketData,
34308
+ ethData: references.ethMarketData,
34309
+ btcBinanceData: references.btcBinanceData,
34310
+ btcCoinbaseData: references.btcCoinbaseData,
34393
34311
  start: preparedRun.window.start,
34394
34312
  preloadStart: preparedRun.preloadStart
34395
34313
  });
34396
34314
  if (!preparedData.replayData.length || !preparedData.btcReplayData.length) {
34397
34315
  skippedSymbols += 1;
34398
- prepareBar.tick(1, {
34399
- skipped: import_chalk46.default.yellow(skippedSymbols),
34400
- symbol: import_chalk46.default.gray(symbol)
34316
+ adapters.progress.tick({
34317
+ skipped: adapters.display.skipped(skippedSymbols),
34318
+ symbol: adapters.display.symbol(symbol)
34401
34319
  });
34402
34320
  continue;
34403
34321
  }
34404
34322
  const sharedIndicatorsReplayKey = buildSymbolSharedReplayKey({
34323
+ userName: userName3,
34405
34324
  connectorName,
34406
34325
  interval: interval4,
34407
34326
  symbol,
@@ -34410,66 +34329,61 @@ var init_historicalSignalsReplay = __esm({
34410
34329
  });
34411
34330
  sharedReplayKeyPrefixes.push(sharedIndicatorsReplayKey);
34412
34331
  const strategiesForSymbol = await Promise.all(
34413
- loadedStrategies.filter((strategy) => strategyIncludesSymbol(strategy, symbol)).map(
34414
- async ({
34415
- strategyName,
34416
- strategyRevision,
34417
- deploymentCompositionId,
34418
- strategyPackageVersion,
34419
- strategyDependencyVersions,
34420
- runtimePackageVersion,
34421
- strategyCreator,
34422
- strategyConfig,
34423
- strategyResults
34424
- }) => {
34425
- const runtimeLineage = await buildRuntimeLineage({
34426
- strategyRevision,
34427
- deploymentCompositionId,
34428
- strategyPackageVersion,
34429
- strategyDependencyVersions,
34430
- runtimePackageVersion,
34431
- config: {
34432
- configId: strategyRevision,
34433
- strategyConfig,
34434
- symbolResultConfig: strategyResults?.[symbol]?.config ?? null
34435
- }
34436
- });
34437
- runtimeLineages.push({
34438
- strategy: strategyName,
34439
- symbol,
34440
- ...preparedRun.deploymentId ? { deploymentId: preparedRun.deploymentId } : {},
34441
- ...preparedRun.accountId ? { accountId: preparedRun.accountId } : {},
34442
- lineage: runtimeLineage
34443
- });
34444
- return {
34445
- strategyName,
34446
- strategyConfig,
34332
+ strategies.filter((strategy) => strategyIncludesSymbol(strategy, symbol)).map(async (strategy) => {
34333
+ const runtimeLineage = await adapters.buildLineage({
34334
+ projectRoot: projectRoot15,
34335
+ strategyName: strategy.strategyName,
34336
+ strategyRevision: strategy.strategyRevision,
34337
+ deploymentCompositionId: strategy.deploymentCompositionId,
34338
+ strategyPackageVersion: strategy.strategyPackageVersion,
34339
+ strategyDependencyVersions: strategy.strategyDependencyVersions,
34340
+ runtimePackageVersion: strategy.runtimePackageVersion,
34341
+ config: {
34342
+ configId: strategy.strategyRevision,
34343
+ strategyConfig: strategy.strategyConfig,
34344
+ symbolResultConfig: strategy.strategyResults?.[symbol]?.config ?? null
34345
+ },
34346
+ runContext: {
34347
+ connectorName: connectorName.toLowerCase(),
34348
+ interval: String(interval4),
34349
+ universe: preparedRun.universe ?? null
34350
+ }
34351
+ });
34352
+ runtimeLineages.push({
34353
+ strategy: strategy.strategyName,
34354
+ symbol,
34355
+ ...preparedRun.deploymentId ? { deploymentId: preparedRun.deploymentId } : {},
34356
+ ...preparedRun.accountId ? { accountId: preparedRun.accountId } : {},
34357
+ lineage: runtimeLineage
34358
+ });
34359
+ return {
34360
+ strategyName: strategy.strategyName,
34361
+ strategyConfig: strategy.strategyConfig,
34362
+ runtimeLineage,
34363
+ run: await strategy.strategyCreator({
34364
+ userName: userName3,
34365
+ connectorName,
34366
+ runtimeConfigId: strategy.strategyRevision,
34367
+ strategyRevision: strategy.strategyRevision,
34447
34368
  runtimeLineage,
34448
- run: await strategyCreator({
34449
- userName: replayUserName,
34450
- connectorName,
34451
- runtimeConfigId: strategyRevision,
34452
- strategyRevision,
34453
- runtimeLineage,
34454
- universe: preparedRun.universe,
34455
- accountId: preparedRun.accountId,
34456
- deploymentId: preparedRun.deploymentId,
34457
- config: buildReplayStrategyConfig({
34458
- strategyConfig,
34459
- interval: interval4
34460
- }),
34461
- symbol,
34462
- data: preparedData.prevData,
34463
- btcData: preparedData.btcPrevData,
34464
- ethData: preparedData.ethPrevData,
34465
- btcBinanceData: preparedData.btcBinancePrevData,
34466
- btcCoinbaseData: preparedData.btcCoinbasePrevData,
34467
- connector: replayConnector,
34468
- sharedIndicatorsReplayKey
34469
- })
34470
- };
34471
- }
34472
- )
34369
+ universe: preparedRun.universe,
34370
+ accountId: preparedRun.accountId,
34371
+ deploymentId: preparedRun.deploymentId,
34372
+ config: buildReplayStrategyConfig({
34373
+ strategyConfig: strategy.strategyConfig,
34374
+ interval: interval4
34375
+ }),
34376
+ symbol,
34377
+ data: preparedData.prevData,
34378
+ btcData: preparedData.btcPrevData,
34379
+ ethData: preparedData.ethPrevData,
34380
+ btcBinanceData: preparedData.btcBinancePrevData,
34381
+ btcCoinbaseData: preparedData.btcCoinbasePrevData,
34382
+ connector: replayConnector,
34383
+ sharedIndicatorsReplayKey
34384
+ })
34385
+ };
34386
+ })
34473
34387
  );
34474
34388
  const symbolRuntime = {
34475
34389
  symbol,
@@ -34479,9 +34393,9 @@ var init_historicalSignalsReplay = __esm({
34479
34393
  currentIndex: 0,
34480
34394
  strategies: strategiesForSymbol
34481
34395
  };
34482
- prepareBar.tick(1, {
34483
- skipped: import_chalk46.default.yellow(skippedSymbols),
34484
- symbol: import_chalk46.default.gray(symbol)
34396
+ adapters.progress.tick({
34397
+ skipped: adapters.display.skipped(skippedSymbols),
34398
+ symbol: adapters.display.symbol(symbol)
34485
34399
  });
34486
34400
  for (const candle of preparedData.replayData) {
34487
34401
  const bucket = cycleSymbolsByTimestamp.get(candle.timestamp) ?? [];
@@ -34489,60 +34403,72 @@ var init_historicalSignalsReplay = __esm({
34489
34403
  cycleSymbolsByTimestamp.set(candle.timestamp, bucket);
34490
34404
  }
34491
34405
  }
34492
- const orderedTimestamps = [...cycleSymbolsByTimestamp.keys()].sort(
34493
- (left, right) => left - right
34494
- );
34495
- const afterSignalsContextBase = buildAfterSignalsContext({
34496
- connector: replayConnector,
34497
- connectorName: preparedRun.connectorName,
34498
- tickers: preparedRun.tickers,
34499
- runtimeStrategies: loadedStrategies,
34500
- interval: interval4
34501
- });
34502
- let abortedCycles = 0;
34503
- const cycleBar = new import_progress12.default(
34504
- "cycles :current/:total [:bar][:percent] sig=:signals abort=:aborted :etas(s) :ts",
34505
- {
34506
- total: orderedTimestamps.length,
34507
- width: 30
34406
+ return {
34407
+ cycleSymbolsByTimestamp,
34408
+ orderedTimestamps: [...cycleSymbolsByTimestamp.keys()].sort(
34409
+ (left, right) => left - right
34410
+ ),
34411
+ sharedReplayKeyPrefixes,
34412
+ runtimeLineages
34413
+ };
34414
+ };
34415
+ }
34416
+ });
34417
+
34418
+ // src/lib/replay/historicalSignalsReplayExecution.ts
34419
+ var advanceCycleMarket, advanceAbortedCycle, executeHistoricalReplay;
34420
+ var init_historicalSignalsReplayExecution = __esm({
34421
+ "src/lib/replay/historicalSignalsReplayExecution.ts"() {
34422
+ "use strict";
34423
+ advanceCycleMarket = async (plan, connector, timestamp) => {
34424
+ const cycleSymbols = plan.cycleSymbolsByTimestamp.get(timestamp) ?? [];
34425
+ for (const symbolRuntime of cycleSymbols) {
34426
+ const candle = symbolRuntime.replayData[symbolRuntime.currentIndex];
34427
+ if (candle?.timestamp === timestamp) {
34428
+ await connector.advanceMarket({
34429
+ symbol: symbolRuntime.symbol,
34430
+ candle
34431
+ });
34508
34432
  }
34509
- );
34433
+ }
34434
+ return cycleSymbols;
34435
+ };
34436
+ advanceAbortedCycle = (cycleSymbols, timestamp) => {
34437
+ for (const symbolRuntime of cycleSymbols ?? []) {
34438
+ const candle = symbolRuntime.replayData[symbolRuntime.currentIndex];
34439
+ if (candle?.timestamp === timestamp) {
34440
+ symbolRuntime.currentIndex += 1;
34441
+ }
34442
+ }
34443
+ };
34444
+ executeHistoricalReplay = async (context, adapters) => {
34445
+ const signals2 = [];
34446
+ let abortedCycles = 0;
34510
34447
  try {
34511
- for (const timestamp of orderedTimestamps) {
34512
- const cycleStartedAt = Date.now();
34513
- const cycleSymbols = cycleSymbolsByTimestamp.get(timestamp) ?? [];
34514
- for (const symbolRuntime of cycleSymbols) {
34515
- const candle = symbolRuntime.replayData[symbolRuntime.currentIndex];
34516
- if (!candle || candle.timestamp !== timestamp) {
34517
- continue;
34518
- }
34519
- await replayConnector.advanceMarket({
34520
- symbol: symbolRuntime.symbol,
34521
- candle
34522
- });
34523
- }
34524
- const beforeSignalsResult = await invokeBeforeSignalsHooks(
34525
- projectHooks,
34526
- afterSignalsContextBase
34448
+ for (const timestamp of context.plan.orderedTimestamps) {
34449
+ const cycleStartedAt = adapters.clock.now();
34450
+ const cycleSymbols = await advanceCycleMarket(
34451
+ context.plan,
34452
+ context.connector,
34453
+ timestamp
34454
+ );
34455
+ const beforeSignalsResult = await adapters.invokeBeforeSignals(
34456
+ context.hooks,
34457
+ context.hookContext
34527
34458
  );
34528
34459
  if (beforeSignalsResult?.abort === true) {
34529
34460
  abortedCycles += 1;
34530
- await invokeAfterSignalsHooks(projectHooks, {
34531
- ...afterSignalsContextBase,
34461
+ await adapters.invokeAfterSignals(context.hooks, {
34462
+ ...context.hookContext,
34532
34463
  signals: [],
34533
34464
  status: "completed",
34534
- durationMs: Date.now() - cycleStartedAt
34465
+ durationMs: adapters.clock.now() - cycleStartedAt
34535
34466
  });
34536
- for (const symbolRuntime of cycleSymbols) {
34537
- const candle = symbolRuntime.replayData[symbolRuntime.currentIndex];
34538
- if (candle?.timestamp === timestamp) {
34539
- symbolRuntime.currentIndex += 1;
34540
- }
34541
- }
34542
- cycleBar.tick(1, {
34543
- signals: import_chalk46.default.cyan(signals2.length),
34544
- aborted: import_chalk46.default.yellow(abortedCycles),
34545
- ts: import_chalk46.default.gray((0, import_time9.formatUnix)(timestamp))
34467
+ advanceAbortedCycle(cycleSymbols, timestamp);
34468
+ adapters.progress.tick({
34469
+ signals: adapters.display.signals(signals2.length),
34470
+ aborted: adapters.display.aborted(abortedCycles),
34471
+ ts: adapters.display.timestamp(timestamp)
34546
34472
  });
34547
34473
  continue;
34548
34474
  }
@@ -34562,81 +34488,323 @@ var init_historicalSignalsReplay = __esm({
34562
34488
  );
34563
34489
  if (result && typeof result !== "string") {
34564
34490
  result.runtimeLineage = strategyRuntime.runtimeLineage;
34565
- await (0, import_strategies13.enrichSignalWithBinanceMarketContext)({
34566
- signal: result,
34567
- env: "PARITY"
34568
- });
34491
+ await adapters.enrichSignal(result);
34569
34492
  cycleSignals.push(result);
34570
34493
  signals2.push(result);
34571
34494
  }
34572
34495
  }
34573
34496
  symbolRuntime.currentIndex += 1;
34574
34497
  }
34575
- await invokeAfterSignalsHooks(projectHooks, {
34576
- ...afterSignalsContextBase,
34498
+ await adapters.invokeAfterSignals(context.hooks, {
34499
+ ...context.hookContext,
34577
34500
  signals: cycleSignals,
34578
34501
  status: "completed",
34579
- durationMs: Date.now() - cycleStartedAt
34502
+ durationMs: adapters.clock.now() - cycleStartedAt
34580
34503
  });
34581
- cycleBar.tick(1, {
34582
- signals: import_chalk46.default.cyan(signals2.length),
34583
- aborted: import_chalk46.default.yellow(abortedCycles),
34584
- ts: import_chalk46.default.gray((0, import_time9.formatUnix)(timestamp))
34504
+ adapters.progress.tick({
34505
+ signals: adapters.display.signals(signals2.length),
34506
+ aborted: adapters.display.aborted(abortedCycles),
34507
+ ts: adapters.display.timestamp(timestamp)
34585
34508
  });
34586
34509
  }
34587
34510
  } finally {
34588
- for (const keyPrefix of sharedReplayKeyPrefixes) {
34589
- (0, import_strategies12.releaseStrategyIndicatorsReplayCache)(keyPrefix);
34590
- (0, import_strategies12.releaseStrategyReplayCache)(keyPrefix);
34511
+ for (const keyPrefix of context.plan.sharedReplayKeyPrefixes) {
34512
+ adapters.releaseIndicatorsCache(keyPrefix);
34513
+ adapters.releaseReplayCache(keyPrefix);
34591
34514
  }
34592
34515
  }
34593
- const artifacts = replayConnector.getReplayArtifacts();
34594
- const strategies = loadedStrategies.map(
34595
- ({ strategyName, strategyConfig }) => {
34596
- const orderLog = artifacts.orderLogByStrategy.get(strategyName) ?? [];
34597
- const positionLog = artifacts.positionLogByStrategy.get(strategyName) ?? [];
34598
- return {
34599
- strategyName,
34600
- strategyConfig,
34601
- orderLog,
34602
- positionLog,
34603
- stat: positionLog.length ? (0, import_backtest4.calculateStatsFull)(positionLog) : {
34604
- orders: 0,
34605
- wins: 0,
34606
- losses: 0,
34607
- netProfit: 0,
34608
- amount: 0
34609
- }
34610
- };
34611
- }
34612
- );
34613
- import_logger11.logger.info(
34614
- import_chalk46.default.gray(
34615
- `signals replay historical cycles: ${orderedTimestamps.length} (aborted=${abortedCycles}) done in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s`
34616
- )
34617
- );
34618
- return {
34619
- strategies,
34620
- signals: signals2,
34621
- orderLog: artifacts.orderLog,
34622
- positionLog: artifacts.positionLog,
34623
- cycleCount: orderedTimestamps.length,
34624
- abortedCycles,
34625
- runtimeLineages
34626
- };
34516
+ return { signals: signals2, abortedCycles };
34627
34517
  };
34628
34518
  }
34629
34519
  });
34630
34520
 
34631
- // src/lib/replay/chartSnapshot.ts
34632
- var formatPercent5, formatSigned2, toSimpleOrderLog, toReplayChartOrders, buildReplayChartSnapshot;
34633
- var init_chartSnapshot = __esm({
34634
- "src/lib/replay/chartSnapshot.ts"() {
34521
+ // src/lib/replay/historicalSignalsReplayResults.ts
34522
+ var import_backtest4, emptyStat, collectHistoricalReplayResult;
34523
+ var init_historicalSignalsReplayResults = __esm({
34524
+ "src/lib/replay/historicalSignalsReplayResults.ts"() {
34635
34525
  "use strict";
34636
- formatPercent5 = (value) => typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(1)}%` : "n/a";
34637
- formatSigned2 = (value) => typeof value === "number" && Number.isFinite(value) ? `${value >= 0 ? "+" : ""}${value.toFixed(2)}` : "n/a";
34638
- toSimpleOrderLog = (orderLog) => orderLog.map((entry) => [entry.timestamp, entry.amount]);
34639
- toReplayChartOrders = (orderLog) => orderLog.map((entry, index) => {
34526
+ import_backtest4 = require("@tradejs/core/backtest");
34527
+ emptyStat = () => ({
34528
+ orders: 0,
34529
+ wins: 0,
34530
+ losses: 0,
34531
+ netProfit: 0,
34532
+ amount: 0
34533
+ });
34534
+ collectHistoricalReplayResult = (context) => ({
34535
+ strategies: context.strategies.map(({ strategyName, strategyConfig }) => {
34536
+ const orderLog = context.artifacts.orderLogByStrategy.get(strategyName) ?? [];
34537
+ const positionLog = context.artifacts.positionLogByStrategy.get(strategyName) ?? [];
34538
+ return {
34539
+ strategyName,
34540
+ strategyConfig,
34541
+ orderLog,
34542
+ positionLog,
34543
+ stat: positionLog.length ? (0, import_backtest4.calculateStatsFull)(positionLog) : emptyStat()
34544
+ };
34545
+ }),
34546
+ signals: context.signals,
34547
+ orderLog: context.artifacts.orderLog,
34548
+ positionLog: context.artifacts.positionLog,
34549
+ cycleCount: context.cycleCount,
34550
+ abortedCycles: context.abortedCycles,
34551
+ runtimeLineages: context.runtimeLineages
34552
+ });
34553
+ }
34554
+ });
34555
+
34556
+ // src/lib/replay/historicalSignalsReplay.ts
34557
+ var import_chalk47, import_progress12, import_strategies12, import_time11, import_logger11, import_redis19, import_connectors17, import_cli13, import_strategies13, import_runtimeStrategies8, loadRuntimeStrategies, loadReferenceConnector, buildAfterSignalsContext, getConnectorName, runHistoricalSignalsReplay;
34558
+ var init_historicalSignalsReplay = __esm({
34559
+ "src/lib/replay/historicalSignalsReplay.ts"() {
34560
+ "use strict";
34561
+ import_chalk47 = __toESM(require("chalk"));
34562
+ import_progress12 = __toESM(require("progress"));
34563
+ import_strategies12 = require("@tradejs/core/strategies");
34564
+ import_time11 = require("@tradejs/core/time");
34565
+ import_logger11 = require("@tradejs/infra/logger");
34566
+ import_redis19 = require("@tradejs/infra/redis");
34567
+ import_connectors17 = require("@tradejs/node/connectors");
34568
+ import_cli13 = require("@tradejs/node/cli");
34569
+ import_strategies13 = require("@tradejs/node/strategies");
34570
+ import_runtimeStrategies8 = require("@tradejs/node/runtimeStrategies");
34571
+ init_cliConfig2();
34572
+ init_portfolioReplayConnector();
34573
+ init_hooks();
34574
+ init_runtimeLineage();
34575
+ init_historicalSignalsReplayPreparation();
34576
+ init_historicalSignalsReplayExecution();
34577
+ init_historicalSignalsReplayResults();
34578
+ loadRuntimeStrategies = async (runtimeStrategies) => {
34579
+ const strategies = await Promise.all(
34580
+ runtimeStrategies.map(async (runtimeStrategy) => {
34581
+ const {
34582
+ strategyName,
34583
+ strategyRevision,
34584
+ deploymentCompositionId,
34585
+ strategyPackage,
34586
+ strategyPackageVersion,
34587
+ strategyDependencyVersions,
34588
+ runtimePackageVersion,
34589
+ strategyConfig,
34590
+ selection
34591
+ } = runtimeStrategy;
34592
+ const strategyCreator = await (0, import_strategies13.getStrategyCreator)(
34593
+ strategyName,
34594
+ replayProjectRoot
34595
+ );
34596
+ if (!strategyCreator) {
34597
+ throw new Error(`Unknown strategy: ${strategyName}`);
34598
+ }
34599
+ const installed = await (0, import_runtimeStrategies8.getRuntimeStrategyPackageMetadata)({
34600
+ strategyName,
34601
+ projectRoot: replayProjectRoot
34602
+ });
34603
+ if (installed.strategyPackage !== strategyPackage || installed.strategyPackageVersion !== strategyPackageVersion || JSON.stringify(installed.strategyDependencyVersions) !== JSON.stringify(strategyDependencyVersions) || installed.runtimePackageVersion !== runtimePackageVersion) {
34604
+ throw new Error(
34605
+ `Runtime evidence package mismatch for ${strategyName}: expected=${strategyPackage}@${strategyPackageVersion}/@tradejs/node@${runtimePackageVersion}, installed=${installed.strategyPackage ?? "missing"}@${installed.strategyPackageVersion ?? "missing"}/@tradejs/node@${installed.runtimePackageVersion ?? "missing"}`
34606
+ );
34607
+ }
34608
+ const strategyResults = await (0, import_redis19.getData)(
34609
+ import_redis19.redisKeys.strategyResults(replayUserName, strategyName),
34610
+ {}
34611
+ );
34612
+ return {
34613
+ strategyName,
34614
+ strategyRevision,
34615
+ deploymentCompositionId,
34616
+ strategyPackage,
34617
+ strategyPackageVersion,
34618
+ strategyDependencyVersions,
34619
+ runtimePackageVersion,
34620
+ strategyCreator,
34621
+ strategyConfig,
34622
+ strategyResults,
34623
+ ...selection ? { selection } : {}
34624
+ };
34625
+ })
34626
+ );
34627
+ return strategies;
34628
+ };
34629
+ loadReferenceConnector = async (connectorName) => {
34630
+ const connectorFactory = await (0, import_connectors17.getConnectorCreatorByName)(
34631
+ connectorName,
34632
+ replayProjectRoot
34633
+ );
34634
+ if (!connectorFactory) {
34635
+ throw new Error(`Connector "${connectorName}" is not registered`);
34636
+ }
34637
+ return await connectorFactory({
34638
+ userName: replayUserName
34639
+ });
34640
+ };
34641
+ buildAfterSignalsContext = ({
34642
+ connector,
34643
+ connectorName,
34644
+ tickers,
34645
+ runtimeStrategies,
34646
+ interval: interval4
34647
+ }) => ({
34648
+ connector,
34649
+ connectorName,
34650
+ userName: replayUserName,
34651
+ interval: interval4,
34652
+ tickers: [...tickers],
34653
+ runtimeStrategies: runtimeStrategies.map(
34654
+ ({ strategyName, strategyConfig }) => ({
34655
+ strategyName,
34656
+ strategyConfig
34657
+ })
34658
+ )
34659
+ });
34660
+ getConnectorName = (connector) => {
34661
+ const name = "name" in connector ? connector.name : void 0;
34662
+ return String(name || import_connectors17.DEFAULT_CONNECTOR_NAME).trim() || import_connectors17.DEFAULT_CONNECTOR_NAME;
34663
+ };
34664
+ runHistoricalSignalsReplay = async ({
34665
+ preparedRun,
34666
+ interval: interval4,
34667
+ runtimeStrategies
34668
+ }) => {
34669
+ const startedAt = Date.now();
34670
+ const projectConfig = await (0, import_cli13.loadTradejsConfig)(replayProjectRoot);
34671
+ const projectHooks = projectConfig.hooks;
34672
+ const loadedStrategies = await loadRuntimeStrategies(runtimeStrategies);
34673
+ const replayConnector = createPortfolioReplayConnector(
34674
+ preparedRun.marketConnector
34675
+ );
34676
+ const connectorName = getConnectorName(preparedRun.marketConnector);
34677
+ const binanceConnector = connectorName.toLowerCase() === import_connectors17.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Binance") : preparedRun.marketConnector;
34678
+ const coinbaseConnector = connectorName.toLowerCase() === import_connectors17.DEFAULT_CONNECTOR_NAME.toLowerCase() ? await loadReferenceConnector("Coinbase") : preparedRun.marketConnector;
34679
+ const [btcBinanceData, btcCoinbaseData] = await Promise.all([
34680
+ binanceConnector.kline({
34681
+ symbol: "BTCUSDT",
34682
+ start: preparedRun.preloadStart,
34683
+ end: preparedRun.window.end,
34684
+ cacheOnly: true,
34685
+ interval: interval4
34686
+ }),
34687
+ coinbaseConnector.kline({
34688
+ symbol: "BTCUSDT",
34689
+ start: preparedRun.preloadStart,
34690
+ end: preparedRun.window.end,
34691
+ cacheOnly: true,
34692
+ interval: interval4
34693
+ })
34694
+ ]);
34695
+ const btcMarketData = await preparedRun.marketConnector.kline({
34696
+ symbol: "BTCUSDT",
34697
+ start: preparedRun.preloadStart,
34698
+ end: preparedRun.window.end,
34699
+ cacheOnly: true,
34700
+ interval: interval4
34701
+ });
34702
+ const ethMarketData = await preparedRun.marketConnector.kline({
34703
+ symbol: "ETHUSDT",
34704
+ start: preparedRun.preloadStart,
34705
+ end: preparedRun.window.end,
34706
+ cacheOnly: true,
34707
+ interval: interval4
34708
+ });
34709
+ const prepareBar = new import_progress12.default(
34710
+ "prepare :current/:total [:bar][:percent] skipped=:skipped :etas(s) :symbol",
34711
+ {
34712
+ total: preparedRun.tickers.length,
34713
+ width: 30
34714
+ }
34715
+ );
34716
+ const plan = await prepareHistoricalReplay(
34717
+ {
34718
+ userName: replayUserName,
34719
+ projectRoot: replayProjectRoot,
34720
+ preparedRun,
34721
+ interval: interval4,
34722
+ connectorName,
34723
+ replayConnector,
34724
+ strategies: loadedStrategies,
34725
+ references: {
34726
+ btcMarketData,
34727
+ ethMarketData,
34728
+ btcBinanceData,
34729
+ btcCoinbaseData
34730
+ }
34731
+ },
34732
+ {
34733
+ progress: {
34734
+ tick: (tokens) => prepareBar.tick(1, tokens)
34735
+ },
34736
+ display: {
34737
+ skipped: (value) => import_chalk47.default.yellow(value),
34738
+ symbol: (value) => import_chalk47.default.gray(value)
34739
+ },
34740
+ buildLineage: buildRuntimeLineage
34741
+ }
34742
+ );
34743
+ const hookContext = buildAfterSignalsContext({
34744
+ connector: replayConnector,
34745
+ connectorName: preparedRun.connectorName,
34746
+ tickers: preparedRun.tickers,
34747
+ runtimeStrategies: loadedStrategies,
34748
+ interval: interval4
34749
+ });
34750
+ const cycleBar = new import_progress12.default(
34751
+ "cycles :current/:total [:bar][:percent] sig=:signals abort=:aborted :etas(s) :ts",
34752
+ {
34753
+ total: plan.orderedTimestamps.length,
34754
+ width: 30
34755
+ }
34756
+ );
34757
+ const execution = await executeHistoricalReplay(
34758
+ {
34759
+ plan,
34760
+ connector: replayConnector,
34761
+ hooks: projectHooks,
34762
+ hookContext
34763
+ },
34764
+ {
34765
+ clock: { now: Date.now },
34766
+ progress: {
34767
+ tick: (tokens) => cycleBar.tick(1, tokens)
34768
+ },
34769
+ display: {
34770
+ signals: (value) => import_chalk47.default.cyan(value),
34771
+ aborted: (value) => import_chalk47.default.yellow(value),
34772
+ timestamp: (value) => import_chalk47.default.gray((0, import_time11.formatUnix)(value))
34773
+ },
34774
+ invokeBeforeSignals: invokeBeforeSignalsHooks,
34775
+ invokeAfterSignals: invokeAfterSignalsHooks,
34776
+ enrichSignal: (signal) => (0, import_strategies13.enrichSignalWithBinanceMarketContext)({ signal, env: "PARITY" }),
34777
+ releaseIndicatorsCache: import_strategies12.releaseStrategyIndicatorsReplayCache,
34778
+ releaseReplayCache: import_strategies12.releaseStrategyReplayCache
34779
+ }
34780
+ );
34781
+ const artifacts = replayConnector.getReplayArtifacts();
34782
+ import_logger11.logger.info(
34783
+ import_chalk47.default.gray(
34784
+ `signals replay historical cycles: ${plan.orderedTimestamps.length} (aborted=${execution.abortedCycles}) done in ${((Date.now() - startedAt) / 1e3).toFixed(1)}s`
34785
+ )
34786
+ );
34787
+ return collectHistoricalReplayResult({
34788
+ strategies: loadedStrategies,
34789
+ artifacts,
34790
+ signals: execution.signals,
34791
+ cycleCount: plan.orderedTimestamps.length,
34792
+ abortedCycles: execution.abortedCycles,
34793
+ runtimeLineages: plan.runtimeLineages
34794
+ });
34795
+ };
34796
+ }
34797
+ });
34798
+
34799
+ // src/lib/replay/chartSnapshot.ts
34800
+ var formatPercent5, formatSigned2, toSimpleOrderLog, toReplayChartOrders, buildReplayChartSnapshot;
34801
+ var init_chartSnapshot = __esm({
34802
+ "src/lib/replay/chartSnapshot.ts"() {
34803
+ "use strict";
34804
+ formatPercent5 = (value) => typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(1)}%` : "n/a";
34805
+ formatSigned2 = (value) => typeof value === "number" && Number.isFinite(value) ? `${value >= 0 ? "+" : ""}${value.toFixed(2)}` : "n/a";
34806
+ toSimpleOrderLog = (orderLog) => orderLog.map((entry) => [entry.timestamp, entry.amount]);
34807
+ toReplayChartOrders = (orderLog) => orderLog.map((entry, index) => {
34640
34808
  const isOpenOrder = entry.type.startsWith("OPEN");
34641
34809
  const equityBefore = orderLog[index - 1]?.amount ?? (Number.isFinite(entry.amount) && Number.isFinite(entry.profit) ? entry.amount - entry.profit : null);
34642
34810
  return {
@@ -34725,11 +34893,11 @@ var init_chartSnapshot = __esm({
34725
34893
  });
34726
34894
 
34727
34895
  // src/lib/replay/resultsReporting.ts
34728
- var import_chalk47, saveAndPrintReplayResultsByStrategy;
34896
+ var import_chalk48, saveAndPrintReplayResultsByStrategy;
34729
34897
  var init_resultsReporting = __esm({
34730
34898
  "src/lib/replay/resultsReporting.ts"() {
34731
34899
  "use strict";
34732
- import_chalk47 = __toESM(require("chalk"));
34900
+ import_chalk48 = __toESM(require("chalk"));
34733
34901
  init_runtimeParity();
34734
34902
  init_runFormatting();
34735
34903
  init_support();
@@ -34768,14 +34936,14 @@ var init_resultsReporting = __esm({
34768
34936
  const rows = summaries.map((summary) => {
34769
34937
  const profit = `${summary.netProfit.toFixed(2)}$`;
34770
34938
  const avgTrade = `${summary.avgTradeProfit.toFixed(2)}$`;
34771
- const profitColor = summary.netProfit > 0 ? import_chalk47.default.green : summary.netProfit < 0 ? import_chalk47.default.red : import_chalk47.default.gray;
34772
- const avgTradeColor = summary.avgTradeProfit > 0 ? import_chalk47.default.green : summary.avgTradeProfit < 0 ? import_chalk47.default.red : import_chalk47.default.gray;
34939
+ const profitColor = summary.netProfit > 0 ? import_chalk48.default.green : summary.netProfit < 0 ? import_chalk48.default.red : import_chalk48.default.gray;
34940
+ const avgTradeColor = summary.avgTradeProfit > 0 ? import_chalk48.default.green : summary.avgTradeProfit < 0 ? import_chalk48.default.red : import_chalk48.default.gray;
34773
34941
  return [
34774
- import_chalk47.default.blue(summary.strategyName),
34775
- import_chalk47.default.yellow(String(summary.tickers)),
34776
- import_chalk47.default.yellow(String(summary.tickersWithTrades)),
34777
- import_chalk47.default.cyan(String(summary.orders)),
34778
- import_chalk47.default.cyan(
34942
+ import_chalk48.default.blue(summary.strategyName),
34943
+ import_chalk48.default.yellow(String(summary.tickers)),
34944
+ import_chalk48.default.yellow(String(summary.tickersWithTrades)),
34945
+ import_chalk48.default.cyan(String(summary.orders)),
34946
+ import_chalk48.default.cyan(
34779
34947
  `${summary.wins}/${summary.losses} (${summary.winRate.toFixed(2)}%)`
34780
34948
  ),
34781
34949
  profitColor(profit),
@@ -34795,12 +34963,12 @@ var init_resultsReporting = __esm({
34795
34963
  });
34796
34964
 
34797
34965
  // src/lib/runtimeTradeSync.ts
34798
- var import_constants18, import_time10, import_redis20, CLOSED_PNL_LIMIT, CLOSED_PNL_RECONCILIATION_LOOKBACK_MS, EXCHANGE_HISTORY_MAX_RANGE_MS, isRuntimeTradeSyncFallbackClose, splitExchangeHistoryTimeRange, formatRuntimeTradeSyncError, loadClosedPnlRows, consumeClosedPnlMatch, isRuntimeTradeInConnectorScope, syncRuntimeTrades;
34966
+ var import_constants18, import_time12, import_redis20, CLOSED_PNL_LIMIT, CLOSED_PNL_RECONCILIATION_LOOKBACK_MS, EXCHANGE_HISTORY_MAX_RANGE_MS, isRuntimeTradeSyncFallbackClose, splitExchangeHistoryTimeRange, formatRuntimeTradeSyncError, loadClosedPnlRows, consumeClosedPnlMatch, isRuntimeTradeInConnectorScope, syncRuntimeTrades;
34799
34967
  var init_runtimeTradeSync = __esm({
34800
34968
  "src/lib/runtimeTradeSync.ts"() {
34801
34969
  "use strict";
34802
34970
  import_constants18 = require("@tradejs/core/constants");
34803
- import_time10 = require("@tradejs/core/time");
34971
+ import_time12 = require("@tradejs/core/time");
34804
34972
  import_redis20 = require("@tradejs/infra/redis");
34805
34973
  CLOSED_PNL_LIMIT = 100;
34806
34974
  CLOSED_PNL_RECONCILIATION_LOOKBACK_MS = 24 * 60 * 60 * 1e3;
@@ -35007,7 +35175,7 @@ var init_runtimeTradeSync = __esm({
35007
35175
  await (0, import_redis20.setHashJsonField)(
35008
35176
  import_redis20.redisKeys.runtimeTradeBucket(
35009
35177
  userName3,
35010
- (0, import_time10.getRuntimeStorageDayKey)(trade.entryTimestamp)
35178
+ (0, import_time12.getRuntimeStorageDayKey)(trade.entryTimestamp)
35011
35179
  ),
35012
35180
  trade.orderId,
35013
35181
  nextTrade2,
@@ -35045,7 +35213,7 @@ var init_runtimeTradeSync = __esm({
35045
35213
  (0, import_redis20.setHashJsonField)(
35046
35214
  import_redis20.redisKeys.runtimeTradeBucket(
35047
35215
  userName3,
35048
- (0, import_time10.getRuntimeStorageDayKey)(trade.entryTimestamp)
35216
+ (0, import_time12.getRuntimeStorageDayKey)(trade.entryTimestamp)
35049
35217
  ),
35050
35218
  trade.orderId,
35051
35219
  nextTrade,
@@ -35054,7 +35222,7 @@ var init_runtimeTradeSync = __esm({
35054
35222
  (0, import_redis20.setHashJsonField)(
35055
35223
  import_redis20.redisKeys.runtimeClosedTradeBucket(
35056
35224
  userName3,
35057
- (0, import_time10.getRuntimeStorageDayKey)(nextTrade.exitTimestamp)
35225
+ (0, import_time12.getRuntimeStorageDayKey)(nextTrade.exitTimestamp)
35058
35226
  ),
35059
35227
  trade.orderId,
35060
35228
  nextTrade,
@@ -35805,49 +35973,14 @@ var init_runtimeParityDetails = __esm({
35805
35973
  }
35806
35974
  });
35807
35975
 
35808
- // src/lib/replay/runtimeComparison.ts
35809
- var import_chalk48, import_time11, getReplayEntryTimestampCompareOffsetMs, buildReplaySignalEvaluations, lineageScopeKey, buildExpectedLineageByScope, hasExpectedLineage, filterReplayComparisonByLineage, formatDrilldownSummary, toFiniteNumberOrNull4, sumOptionalNumbers, firstFiniteNumber, firstString, summarizeBacktestPnlByStrategy, getExchangeExecutionMergeKey, mergeExchangeExecutionGroup, mergeExchangeEntryExecutions, loadExchangeEntryRows, buildPriceDeltaPct3, loadExchangeEntriesForComparison, compareExchangeEntriesToBacktest, findExchangeRuntimeOutcome, splitExchangeMatchesByRuntimeOrderStatus, saveAndPrintReplayExchangeComparison, saveAndPrintReplayRuntimeComparison;
35810
- var init_runtimeComparison = __esm({
35811
- "src/lib/replay/runtimeComparison.ts"() {
35976
+ // src/lib/replay/runtimeComparisonCalculations.ts
35977
+ var lineageScopeKey, buildExpectedLineageByScope, hasExpectedLineage, filterReplayComparisonByLineage, buildPriceDeltaPct3, compareExchangeEntriesToBacktest, findExchangeRuntimeOutcome, hasLineageLinkedRuntimeOutcome, splitExchangeMatchesByRuntimeOrderStatus, summarizeBacktestPnlByStrategy, buildExchangeComparisonRows, buildRuntimeComparisonRows;
35978
+ var init_runtimeComparisonCalculations = __esm({
35979
+ "src/lib/replay/runtimeComparisonCalculations.ts"() {
35812
35980
  "use strict";
35813
- import_chalk48 = __toESM(require("chalk"));
35814
- import_time11 = require("@tradejs/core/time");
35815
35981
  init_runtimeParity();
35816
- init_paritySummary();
35817
- init_runtimeRedis();
35818
- init_runtimeSignalsLoader();
35819
- init_runtimeTradeSync();
35820
- init_runFormatting();
35821
35982
  init_runtimeParityDetails();
35822
- init_runState();
35823
- init_cliConfig2();
35824
- init_runtimeEvidenceSource();
35825
- init_support();
35826
35983
  init_runtimeLineage();
35827
- getReplayEntryTimestampCompareOffsetMs = () => {
35828
- const intervalMinutes = Number(replayInterval);
35829
- return Number.isFinite(intervalMinutes) && intervalMinutes > 0 ? intervalMinutes * 60 * 1e3 : 15 * 60 * 1e3;
35830
- };
35831
- buildReplaySignalEvaluations = (signals2) => signals2.map((signal) => ({
35832
- evaluationId: `${signal.strategy}:${signal.symbol}:${signal.timestamp}`,
35833
- userName: replayUserName,
35834
- strategy: signal.strategy,
35835
- symbol: signal.symbol,
35836
- interval: signal.interval,
35837
- timestamp: signal.timestamp,
35838
- evaluatedAt: signal.timestamp,
35839
- status: "signal",
35840
- reason: signal.orderSkipReason || signal.orderStatus || "SIGNAL",
35841
- signalId: signal.signalId,
35842
- direction: signal.direction,
35843
- accountId: signal.accountId,
35844
- deploymentId: signal.deploymentId,
35845
- runtimeLineage: signal.runtimeLineage,
35846
- orderStatus: signal.orderStatus,
35847
- orderSkipReason: signal.orderSkipReason,
35848
- ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
35849
- ...signal.ml ? { ml: signal.ml } : {}
35850
- }));
35851
35984
  lineageScopeKey = ({
35852
35985
  strategy,
35853
35986
  symbol,
@@ -35867,12 +36000,7 @@ var init_runtimeComparison = __esm({
35867
36000
  }) => runtimeLineagesComparable(
35868
36001
  lineage,
35869
36002
  expectedByScope.get(
35870
- lineageScopeKey({
35871
- strategy,
35872
- symbol,
35873
- deploymentId,
35874
- accountId
35875
- })
36003
+ lineageScopeKey({ strategy, symbol, deploymentId, accountId })
35876
36004
  )
35877
36005
  );
35878
36006
  filterReplayComparisonByLineage = ({
@@ -35994,13 +36122,7 @@ var init_runtimeComparison = __esm({
35994
36122
  reason: comparableScopeKeys.size > 0 ? null : expectedByScope.size === 0 ? "replay_produced_no_lineage_scopes" : "no_runtime_artifacts_with_matching_lineage",
35995
36123
  replay: [...expectedByScope.entries()].map(([scope, lineage]) => {
35996
36124
  const [deploymentId, accountId, strategy, symbol] = scope.split("::");
35997
- return {
35998
- deploymentId,
35999
- accountId,
36000
- strategy,
36001
- symbol,
36002
- lineage
36003
- };
36125
+ return { deploymentId, accountId, strategy, symbol, lineage };
36004
36126
  }).sort(
36005
36127
  (left, right) => left.strategy.localeCompare(right.strategy) || left.symbol.localeCompare(right.symbol) || runtimeLineageKey(left.lineage).localeCompare(
36006
36128
  runtimeLineageKey(right.lineage)
@@ -36009,208 +36131,12 @@ var init_runtimeComparison = __esm({
36009
36131
  }
36010
36132
  };
36011
36133
  };
36012
- formatDrilldownSummary = (summary) => Object.entries(summary).filter(([, count]) => count > 0).map(([classification, count]) => `${classification}=${count}`).join(", ");
36013
- toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
36014
- sumOptionalNumbers = (values) => {
36015
- let sum3 = 0;
36016
- let hasValue = false;
36017
- for (const value of values) {
36018
- if (typeof value !== "number" || !Number.isFinite(value)) {
36019
- continue;
36020
- }
36021
- sum3 += value;
36022
- hasValue = true;
36023
- }
36024
- return hasValue ? Number(sum3.toFixed(12)) : null;
36025
- };
36026
- firstFiniteNumber = (values) => {
36027
- for (const value of values) {
36028
- if (typeof value === "number" && Number.isFinite(value)) {
36029
- return value;
36030
- }
36031
- }
36032
- return null;
36033
- };
36034
- firstString = (values) => values.find((value) => typeof value === "string" && value.trim());
36035
- summarizeBacktestPnlByStrategy = (entries) => {
36036
- const totals = /* @__PURE__ */ new Map();
36037
- for (const entry of entries) {
36038
- const pnl = typeof entry.expectedPnl === "number" && Number.isFinite(entry.expectedPnl) ? entry.expectedPnl : 0;
36039
- totals.set(entry.strategy, (totals.get(entry.strategy) ?? 0) + pnl);
36040
- }
36041
- return totals;
36042
- };
36043
- getExchangeExecutionMergeKey = (entry) => {
36044
- const orderId = firstString([entry.orderId, entry.orderLinkId]);
36045
- return orderId ? `${entry.symbol}::${entry.direction}::${orderId}` : null;
36046
- };
36047
- mergeExchangeExecutionGroup = (group) => {
36048
- const [first] = group;
36049
- const qty = sumOptionalNumbers(group.map((entry) => entry.qty)) ?? first.qty;
36050
- const weightedPriceSum = group.reduce((sum3, entry) => {
36051
- const entryQty = toFiniteNumberOrNull4(entry.qty);
36052
- const entryPrice2 = toFiniteNumberOrNull4(entry.entryPrice);
36053
- return entryQty != null && entryPrice2 != null ? sum3 + entryQty * entryPrice2 : sum3;
36054
- }, 0);
36055
- const entryPrice = qty > 0 && weightedPriceSum > 0 ? Number((weightedPriceSum / qty).toFixed(12)) : firstFiniteNumber(group.map((entry) => entry.entryPrice));
36056
- const openFee = sumOptionalNumbers(group.map((entry) => entry.openFee));
36057
- const closeFee = sumOptionalNumbers(group.map((entry) => entry.closeFee));
36058
- const fundingFee = firstFiniteNumber(group.map((entry) => entry.fundingFee));
36059
- const totalFee = sumOptionalNumbers([openFee, closeFee, fundingFee]) ?? firstFiniteNumber(group.map((entry) => entry.totalFee));
36060
- return {
36061
- symbol: first.symbol,
36062
- direction: first.direction,
36063
- qty,
36064
- entryPrice,
36065
- entryTimestamp: Math.min(...group.map((entry) => entry.entryTimestamp)),
36066
- ...firstString(group.map((entry) => entry.orderId)) ? { orderId: firstString(group.map((entry) => entry.orderId)) } : {},
36067
- ...firstString(group.map((entry) => entry.orderLinkId)) ? { orderLinkId: firstString(group.map((entry) => entry.orderLinkId)) } : {},
36068
- ...firstFiniteNumber(group.map((entry) => entry.takeProfitPrice)) != null ? {
36069
- takeProfitPrice: firstFiniteNumber(
36070
- group.map((entry) => entry.takeProfitPrice)
36071
- )
36072
- } : {},
36073
- ...firstFiniteNumber(group.map((entry) => entry.stopLossPrice)) != null ? {
36074
- stopLossPrice: firstFiniteNumber(
36075
- group.map((entry) => entry.stopLossPrice)
36076
- )
36077
- } : {},
36078
- ...firstFiniteNumber(group.map((entry) => entry.exitPrice)) != null ? { exitPrice: firstFiniteNumber(group.map((entry) => entry.exitPrice)) } : {},
36079
- ...firstFiniteNumber(group.map((entry) => entry.exitTimestamp)) != null ? {
36080
- exitTimestamp: firstFiniteNumber(
36081
- group.map((entry) => entry.exitTimestamp)
36082
- )
36083
- } : {},
36084
- ...sumOptionalNumbers(group.map((entry) => entry.closedPnl)) != null ? { closedPnl: sumOptionalNumbers(group.map((entry) => entry.closedPnl)) } : {},
36085
- ...openFee != null ? { openFee } : {},
36086
- ...closeFee != null ? { closeFee } : {},
36087
- ...fundingFee != null ? { fundingFee } : {},
36088
- ...totalFee != null ? { totalFee } : {}
36089
- };
36090
- };
36091
- mergeExchangeEntryExecutions = (rows) => {
36092
- const mergedRows = [];
36093
- const groups = /* @__PURE__ */ new Map();
36094
- for (const row of rows) {
36095
- const key = getExchangeExecutionMergeKey(row);
36096
- if (!key) {
36097
- mergedRows.push(row);
36098
- continue;
36099
- }
36100
- const group = groups.get(key) ?? [];
36101
- group.push(row);
36102
- groups.set(key, group);
36103
- }
36104
- for (const group of groups.values()) {
36105
- mergedRows.push(
36106
- group.length === 1 ? group[0] : mergeExchangeExecutionGroup(group)
36107
- );
36108
- }
36109
- return mergedRows.sort(
36110
- (left, right) => left.entryTimestamp - right.entryTimestamp
36111
- );
36112
- };
36113
- loadExchangeEntryRows = async ({
36114
- connector,
36115
- startTime,
36116
- endTime
36117
- }) => {
36118
- if (typeof connector.getEntryExecutions !== "function") {
36119
- console.log(
36120
- import_chalk48.default.yellow(
36121
- "runtime compare: connector does not support entry execution history"
36122
- )
36123
- );
36124
- return [];
36125
- }
36126
- const rows = [];
36127
- const chunks = splitExchangeHistoryTimeRange({ startTime, endTime });
36128
- for (const chunk of chunks) {
36129
- try {
36130
- const chunkRows = await connector.getEntryExecutions({
36131
- startTime: chunk.startTime,
36132
- endTime: chunk.endTime,
36133
- limit: 100
36134
- });
36135
- if (chunkRows.length >= 100) {
36136
- console.log(
36137
- import_chalk48.default.yellow(
36138
- "runtime compare: exchange entry executions returned 100 rows (connector cap); older entry trades in this chunk may be truncated"
36139
- )
36140
- );
36141
- }
36142
- rows.push(...chunkRows);
36143
- } catch (error) {
36144
- console.log(
36145
- import_chalk48.default.yellow(
36146
- `runtime compare: getEntryExecutions failed: ${formatRuntimeTradeSyncError(error)}`
36147
- )
36148
- );
36149
- }
36150
- }
36151
- return mergeExchangeEntryExecutions(rows);
36152
- };
36153
36134
  buildPriceDeltaPct3 = (leftPrice, rightPrice) => {
36154
36135
  if (leftPrice == null || rightPrice == null || !Number.isFinite(leftPrice) || !Number.isFinite(rightPrice) || leftPrice === 0) {
36155
36136
  return null;
36156
36137
  }
36157
36138
  return Math.abs((rightPrice - leftPrice) / leftPrice * 100);
36158
36139
  };
36159
- loadExchangeEntriesForComparison = async ({
36160
- connector,
36161
- startTime,
36162
- endTime
36163
- }) => {
36164
- const entryRows = await loadExchangeEntryRows({
36165
- connector,
36166
- startTime,
36167
- endTime
36168
- });
36169
- const closedPnlRows = await loadClosedPnlRows({
36170
- connector,
36171
- startTime,
36172
- endTime,
36173
- callbacks: {
36174
- onUnsupported: () => {
36175
- console.log(
36176
- import_chalk48.default.yellow(
36177
- "runtime compare: connector does not support getClosedPnl, using runtime trade records as-is"
36178
- )
36179
- );
36180
- },
36181
- onCapped: () => {
36182
- console.log(
36183
- import_chalk48.default.yellow(
36184
- "runtime compare: exchange closed pnl returned 100 rows (connector cap); older closed trades in the window may be truncated"
36185
- )
36186
- );
36187
- },
36188
- onError: (error) => {
36189
- console.log(
36190
- import_chalk48.default.yellow(
36191
- `runtime compare: getClosedPnl failed: ${formatRuntimeTradeSyncError(error)}`
36192
- )
36193
- );
36194
- }
36195
- }
36196
- });
36197
- const closedPnlByOrderId = new Map(
36198
- closedPnlRows.filter((row) => typeof row.orderId === "string" && row.orderId.trim()).map((row) => [row.orderId, row])
36199
- );
36200
- return entryRows.map((entry) => {
36201
- const closedPnl = typeof entry.orderId === "string" ? closedPnlByOrderId.get(entry.orderId) : null;
36202
- return {
36203
- ...entry,
36204
- exitPrice: closedPnl?.exitPrice ?? entry.exitPrice ?? null,
36205
- exitTimestamp: closedPnl?.closedAt ?? entry.exitTimestamp ?? null,
36206
- closedPnl: closedPnl?.closedPnl ?? entry.closedPnl ?? null,
36207
- openFee: closedPnl?.openFee ?? entry.openFee ?? null,
36208
- closeFee: closedPnl?.closeFee ?? entry.closeFee ?? null,
36209
- fundingFee: closedPnl?.fundingFee ?? entry.fundingFee ?? null,
36210
- totalFee: closedPnl?.totalFee ?? entry.totalFee ?? null
36211
- };
36212
- });
36213
- };
36214
36140
  compareExchangeEntriesToBacktest = ({
36215
36141
  exchangeEntries,
36216
36142
  backtestEntries,
@@ -36243,30 +36169,20 @@ var init_runtimeComparison = __esm({
36243
36169
  const exchangeGroup = [...groupedExchange.get(key) ?? []].sort(
36244
36170
  (left, right) => left.entryTimestamp - right.entryTimestamp
36245
36171
  );
36246
- const availableBacktest = [...groupedBacktest.get(key) ?? []].sort(
36247
- (left, right) => left.timestamp - right.timestamp
36248
- );
36249
- const unmatchedBacktest = availableBacktest.map((entry) => ({
36250
- entry,
36251
- used: false
36252
- }));
36172
+ const unmatchedBacktest = [...groupedBacktest.get(key) ?? []].sort((left, right) => left.timestamp - right.timestamp).map((entry) => ({ entry, used: false }));
36253
36173
  for (const exchangeEntry of exchangeGroup) {
36254
36174
  let bestIndex = -1;
36255
36175
  let bestDiff = Number.POSITIVE_INFINITY;
36256
36176
  for (let index = 0; index < unmatchedBacktest.length; index += 1) {
36257
36177
  const candidate = unmatchedBacktest[index];
36258
- if (candidate.used) {
36259
- continue;
36260
- }
36178
+ if (candidate.used) continue;
36261
36179
  const diff = Math.abs(
36262
36180
  getBacktestParityComparisonTimestamp(
36263
36181
  candidate.entry,
36264
36182
  backtestTimestampOffsetMs
36265
36183
  ) - exchangeEntry.entryTimestamp
36266
36184
  );
36267
- if (diff > toleranceMs || diff >= bestDiff) {
36268
- continue;
36269
- }
36185
+ if (diff > toleranceMs || diff >= bestDiff) continue;
36270
36186
  bestIndex = index;
36271
36187
  bestDiff = diff;
36272
36188
  }
@@ -36289,9 +36205,7 @@ var init_runtimeComparison = __esm({
36289
36205
  }
36290
36206
  }
36291
36207
  for (const entry of backtestEntries) {
36292
- if (!matchedBacktestEntries.has(entry)) {
36293
- backtestOnly.push(entry);
36294
- }
36208
+ if (!matchedBacktestEntries.has(entry)) backtestOnly.push(entry);
36295
36209
  }
36296
36210
  matched.sort(
36297
36211
  (left, right) => left.exchange.entryTimestamp - right.exchange.entryTimestamp || left.backtest.strategy.localeCompare(right.backtest.strategy)
@@ -36300,11 +36214,7 @@ var init_runtimeComparison = __esm({
36300
36214
  (left, right) => left.entryTimestamp - right.entryTimestamp
36301
36215
  );
36302
36216
  backtestOnly.sort((left, right) => left.timestamp - right.timestamp);
36303
- return {
36304
- matched,
36305
- exchangeOnly,
36306
- backtestOnly
36307
- };
36217
+ return { matched, exchangeOnly, backtestOnly };
36308
36218
  };
36309
36219
  findExchangeRuntimeOutcome = ({
36310
36220
  exchangeEntry,
@@ -36324,59 +36234,496 @@ var init_runtimeComparison = __esm({
36324
36234
  });
36325
36235
  }
36326
36236
  }
36327
- for (const evaluation of runtimeSignalEvaluations) {
36328
- if (evaluation.strategy === strategyName && evaluation.symbol === exchangeEntry.symbol && (evaluation.direction == null || evaluation.direction === exchangeEntry.direction)) {
36329
- candidates.push({
36330
- timestamp: evaluation.timestamp,
36331
- orderStatus: evaluation.orderStatus,
36332
- reason: evaluation.orderSkipReason || evaluation.reason || evaluation.orderStatus
36237
+ for (const evaluation of runtimeSignalEvaluations) {
36238
+ if (evaluation.strategy === strategyName && evaluation.symbol === exchangeEntry.symbol && (evaluation.direction == null || evaluation.direction === exchangeEntry.direction)) {
36239
+ candidates.push({
36240
+ timestamp: evaluation.timestamp,
36241
+ orderStatus: evaluation.orderStatus,
36242
+ reason: evaluation.orderSkipReason || evaluation.reason || evaluation.orderStatus
36243
+ });
36244
+ }
36245
+ }
36246
+ return candidates.map((candidate) => ({
36247
+ ...candidate,
36248
+ timestampDiffMs: Math.abs(
36249
+ candidate.timestamp + signalTimestampOffsetMs - exchangeEntry.entryTimestamp
36250
+ )
36251
+ })).filter((candidate) => candidate.timestampDiffMs <= toleranceMs).sort(
36252
+ (left, right) => left.timestampDiffMs - right.timestampDiffMs || Number(right.orderStatus === "failed") - Number(left.orderStatus === "failed")
36253
+ )[0] ?? null;
36254
+ };
36255
+ hasLineageLinkedRuntimeOutcome = ({
36256
+ exchangeEntry,
36257
+ strategyName,
36258
+ runtimeSignals,
36259
+ runtimeSignalEvaluations,
36260
+ toleranceMs,
36261
+ signalTimestampOffsetMs
36262
+ }) => findExchangeRuntimeOutcome({
36263
+ exchangeEntry,
36264
+ strategyName,
36265
+ runtimeSignals,
36266
+ runtimeSignalEvaluations,
36267
+ toleranceMs,
36268
+ signalTimestampOffsetMs
36269
+ }) != null;
36270
+ splitExchangeMatchesByRuntimeOrderStatus = ({
36271
+ matched,
36272
+ strategyNameByOrderLinkKey,
36273
+ runtimeSignals,
36274
+ runtimeSignalEvaluations,
36275
+ toleranceMs,
36276
+ signalTimestampOffsetMs
36277
+ }) => {
36278
+ const orderFailed = [];
36279
+ const completed = matched.filter((item) => {
36280
+ const strategyName = resolveReplayStrategyNameFromExchangeEntry({
36281
+ exchangeEntry: item.exchange,
36282
+ strategyNameByOrderLinkKey
36283
+ });
36284
+ if (!strategyName) return true;
36285
+ const outcome = findExchangeRuntimeOutcome({
36286
+ exchangeEntry: item.exchange,
36287
+ strategyName,
36288
+ runtimeSignals,
36289
+ runtimeSignalEvaluations,
36290
+ toleranceMs,
36291
+ signalTimestampOffsetMs
36292
+ });
36293
+ if (outcome?.orderStatus !== "failed") return true;
36294
+ orderFailed.push({
36295
+ ...item,
36296
+ reason: outcome.reason || "orderStatus=failed"
36297
+ });
36298
+ return false;
36299
+ });
36300
+ return { completed, orderFailed };
36301
+ };
36302
+ summarizeBacktestPnlByStrategy = (entries) => {
36303
+ const totals = /* @__PURE__ */ new Map();
36304
+ for (const entry of entries) {
36305
+ const pnl = typeof entry.expectedPnl === "number" && Number.isFinite(entry.expectedPnl) ? entry.expectedPnl : 0;
36306
+ totals.set(entry.strategy, (totals.get(entry.strategy) ?? 0) + pnl);
36307
+ }
36308
+ return totals;
36309
+ };
36310
+ buildExchangeComparisonRows = ({
36311
+ liveStrategySummaries,
36312
+ backtestEntries,
36313
+ matched,
36314
+ orderFailed,
36315
+ exchangeOnly,
36316
+ strategyNameByOrderLinkKey
36317
+ }) => {
36318
+ const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(backtestEntries);
36319
+ const rowByStrategy = /* @__PURE__ */ new Map();
36320
+ const ensureRow = (strategyName) => {
36321
+ const existing = rowByStrategy.get(strategyName);
36322
+ if (existing) return existing;
36323
+ const next = {
36324
+ strategyName,
36325
+ backtestEntries: 0,
36326
+ backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
36327
+ runtimeTrades: 0,
36328
+ runtimePnl: 0,
36329
+ matched: 0,
36330
+ orderFailed: 0,
36331
+ runtimeOnly: 0,
36332
+ backtestOnly: 0
36333
+ };
36334
+ rowByStrategy.set(strategyName, next);
36335
+ return next;
36336
+ };
36337
+ for (const summary of liveStrategySummaries) ensureRow(summary.strategyName);
36338
+ for (const entry of backtestEntries) {
36339
+ ensureRow(entry.strategy).backtestEntries += 1;
36340
+ }
36341
+ for (const item of matched) {
36342
+ const row = ensureRow(item.backtest.strategy);
36343
+ row.runtimeTrades += 1;
36344
+ row.matched += 1;
36345
+ if (typeof item.exchange.closedPnl === "number" && Number.isFinite(item.exchange.closedPnl)) {
36346
+ row.runtimePnl += item.exchange.closedPnl;
36347
+ }
36348
+ }
36349
+ for (const item of orderFailed) {
36350
+ const row = ensureRow(item.backtest.strategy);
36351
+ row.runtimeTrades += 1;
36352
+ row.orderFailed += 1;
36353
+ }
36354
+ const matchedBacktestEntries = /* @__PURE__ */ new Set([
36355
+ ...matched.map(({ backtest: backtest2 }) => backtest2),
36356
+ ...orderFailed.map(({ backtest: backtest2 }) => backtest2)
36357
+ ]);
36358
+ for (const entry of backtestEntries) {
36359
+ if (!matchedBacktestEntries.has(entry)) {
36360
+ ensureRow(entry.strategy).backtestOnly += 1;
36361
+ }
36362
+ }
36363
+ for (const entry of exchangeOnly) {
36364
+ const strategyName = resolveReplayStrategyNameFromExchangeEntry({
36365
+ exchangeEntry: entry,
36366
+ strategyNameByOrderLinkKey
36367
+ }) ?? "[exchange-unmatched]";
36368
+ const row = ensureRow(strategyName);
36369
+ row.runtimeTrades += 1;
36370
+ row.runtimeOnly += 1;
36371
+ if (typeof entry.closedPnl === "number" && Number.isFinite(entry.closedPnl)) {
36372
+ row.runtimePnl += entry.closedPnl;
36373
+ }
36374
+ }
36375
+ return [...rowByStrategy.values()].map((row) => ({
36376
+ ...row,
36377
+ runtimePnl: Number(row.runtimePnl.toFixed(2))
36378
+ })).sort((left, right) => left.strategyName.localeCompare(right.strategyName));
36379
+ };
36380
+ buildRuntimeComparisonRows = ({
36381
+ liveStrategySummaries,
36382
+ runtimeSummaries,
36383
+ parityRows,
36384
+ backtestEntries
36385
+ }) => {
36386
+ const runtimeSummaryByStrategy = new Map(
36387
+ runtimeSummaries.map((summary) => [summary.strategyName, summary])
36388
+ );
36389
+ const parityByStrategy = new Map(parityRows);
36390
+ const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(backtestEntries);
36391
+ const strategyNames = /* @__PURE__ */ new Set([
36392
+ ...liveStrategySummaries.map(({ strategyName }) => strategyName),
36393
+ ...runtimeSummaryByStrategy.keys(),
36394
+ ...parityByStrategy.keys()
36395
+ ]);
36396
+ return [...strategyNames].sort((left, right) => left.localeCompare(right)).map((strategyName) => {
36397
+ const runtimeSummary = runtimeSummaryByStrategy.get(strategyName);
36398
+ const parity = parityByStrategy.get(strategyName);
36399
+ return {
36400
+ strategyName,
36401
+ backtestEntries: parity?.backtest ?? 0,
36402
+ backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
36403
+ runtimeTrades: runtimeSummary?.trades ?? 0,
36404
+ runtimePnl: runtimeSummary?.totalPnl ?? 0,
36405
+ matched: parity?.matched ?? 0,
36406
+ orderFailed: 0,
36407
+ runtimeOnly: parity?.runtimeOnly ?? 0,
36408
+ backtestOnly: parity?.backtestOnly ?? 0
36409
+ };
36410
+ });
36411
+ };
36412
+ }
36413
+ });
36414
+
36415
+ // src/lib/replay/runtimeComparisonReporting.ts
36416
+ var import_chalk49, import_time13, formatDrilldownSummary, colorizeRows, buildReplayComparisonReport, buildNoExchangeEntriesReport, buildNoRuntimeTradesReport, writeReplayComparisonReport;
36417
+ var init_runtimeComparisonReporting = __esm({
36418
+ "src/lib/replay/runtimeComparisonReporting.ts"() {
36419
+ "use strict";
36420
+ import_chalk49 = __toESM(require("chalk"));
36421
+ import_time13 = require("@tradejs/core/time");
36422
+ init_runFormatting();
36423
+ init_support();
36424
+ formatDrilldownSummary = (summary) => Object.entries(summary).filter(([, count]) => count > 0).map(([classification, count]) => `${classification}=${count}`).join(", ");
36425
+ colorizeRows = (rows) => rows.map((row) => {
36426
+ const btPnlColor = row.backtestNetProfit > 0 ? import_chalk49.default.green : row.backtestNetProfit < 0 ? import_chalk49.default.red : import_chalk49.default.gray;
36427
+ const rtPnlColor = row.runtimePnl > 0 ? import_chalk49.default.green : row.runtimePnl < 0 ? import_chalk49.default.red : import_chalk49.default.gray;
36428
+ return [
36429
+ import_chalk49.default.blue(row.strategyName),
36430
+ import_chalk49.default.cyan(String(row.backtestEntries)),
36431
+ btPnlColor(`${row.backtestNetProfit.toFixed(2)}$`),
36432
+ import_chalk49.default.yellow(String(row.runtimeTrades)),
36433
+ rtPnlColor(`${row.runtimePnl.toFixed(2)}$`),
36434
+ import_chalk49.default.green(String(row.matched)),
36435
+ import_chalk49.default.red(String(row.orderFailed)),
36436
+ import_chalk49.default.yellow(String(row.runtimeOnly)),
36437
+ import_chalk49.default.magenta(String(row.backtestOnly)),
36438
+ import_chalk49.default.red(String(getReplayRuntimeUnmatchedCount(row)))
36439
+ ];
36440
+ });
36441
+ buildReplayComparisonReport = (context) => {
36442
+ const source = context.mode === "exchange" ? "EXCHANGE" : "RUNTIME";
36443
+ const inference = context.mode === "exchange" ? ", inferredStrategy=orderLinkId | nearest backtest entry" : "";
36444
+ const runtimeOnlyLabel = context.mode === "exchange" ? "exchangeOnly" : "runtimeOnly";
36445
+ const runtimeOnlyDrilldownSummary = formatDrilldownSummary(
36446
+ context.details?.mismatchDrilldown?.summary.runtimeOnly ?? {}
36447
+ );
36448
+ const backtestOnlyDrilldownSummary = formatDrilldownSummary(
36449
+ context.details?.mismatchDrilldown?.summary.backtestOnly ?? {}
36450
+ );
36451
+ const lines = [
36452
+ "",
36453
+ `SIGNALS REPLAY VS ${source} BY STRATEGY (connector=${context.connectorName}${inference}, tolerance=${formatReplayRuntimeCompareTolerance()})`,
36454
+ createTable(REPLAY_RUNTIME_COMPARISON_HEADERS, colorizeRows(context.rows))
36455
+ ];
36456
+ if (runtimeOnlyDrilldownSummary || backtestOnlyDrilldownSummary) {
36457
+ lines.push(
36458
+ import_chalk49.default.gray(
36459
+ `Mismatch drilldown: ${runtimeOnlyLabel}=[${runtimeOnlyDrilldownSummary || "none"}], backtestOnly=[${backtestOnlyDrilldownSummary || "none"}]`
36460
+ )
36461
+ );
36462
+ }
36463
+ lines.push("");
36464
+ return lines;
36465
+ };
36466
+ buildNoExchangeEntriesReport = ({
36467
+ connectorName,
36468
+ window
36469
+ }) => [
36470
+ "",
36471
+ import_chalk49.default.yellow(
36472
+ `SIGNALS REPLAY VS EXCHANGE: no lineage-linked exchange entry executions found for ${connectorName} in ${(0, import_time13.formatUnix)(
36473
+ window.start
36474
+ )} -> ${(0, import_time13.formatUnix)(window.end)}`
36475
+ ),
36476
+ ""
36477
+ ];
36478
+ buildNoRuntimeTradesReport = ({
36479
+ connectorName,
36480
+ window
36481
+ }) => [
36482
+ "",
36483
+ import_chalk49.default.yellow(
36484
+ `SIGNALS REPLAY VS RUNTIME: no local runtime trades found for ${connectorName} in ${(0, import_time13.formatUnix)(
36485
+ window.start
36486
+ )} -> ${(0, import_time13.formatUnix)(window.end)} with matching lineage; checking lineage-linked exchange executions`
36487
+ ),
36488
+ ""
36489
+ ];
36490
+ writeReplayComparisonReport = (lines) => {
36491
+ for (const line of lines) {
36492
+ console.log(line);
36493
+ }
36494
+ };
36495
+ }
36496
+ });
36497
+
36498
+ // src/lib/replay/runtimeComparison.ts
36499
+ var import_chalk50, getReplayEntryTimestampCompareOffsetMs, buildReplaySignalEvaluations, toFiniteNumberOrNull4, sumOptionalNumbers, firstFiniteNumber, firstString, getExchangeExecutionMergeKey, mergeExchangeExecutionGroup, mergeExchangeEntryExecutions, loadExchangeEntryRows, loadExchangeEntriesForComparison, saveAndPrintReplayExchangeComparison, saveAndPrintReplayRuntimeComparison;
36500
+ var init_runtimeComparison = __esm({
36501
+ "src/lib/replay/runtimeComparison.ts"() {
36502
+ "use strict";
36503
+ import_chalk50 = __toESM(require("chalk"));
36504
+ init_runtimeParity();
36505
+ init_paritySummary();
36506
+ init_runtimeRedis();
36507
+ init_runtimeSignalsLoader();
36508
+ init_runtimeTradeSync();
36509
+ init_runtimeParityDetails();
36510
+ init_runState();
36511
+ init_cliConfig2();
36512
+ init_runtimeEvidenceSource();
36513
+ init_support();
36514
+ init_runtimeComparisonCalculations();
36515
+ init_runtimeComparisonReporting();
36516
+ init_runtimeComparisonCalculations();
36517
+ getReplayEntryTimestampCompareOffsetMs = () => {
36518
+ const intervalMinutes = Number(replayInterval);
36519
+ return Number.isFinite(intervalMinutes) && intervalMinutes > 0 ? intervalMinutes * 60 * 1e3 : 15 * 60 * 1e3;
36520
+ };
36521
+ buildReplaySignalEvaluations = (signals2) => signals2.map((signal) => ({
36522
+ evaluationId: `${signal.strategy}:${signal.symbol}:${signal.timestamp}`,
36523
+ userName: replayUserName,
36524
+ strategy: signal.strategy,
36525
+ symbol: signal.symbol,
36526
+ interval: signal.interval,
36527
+ timestamp: signal.timestamp,
36528
+ evaluatedAt: signal.timestamp,
36529
+ status: "signal",
36530
+ reason: signal.orderSkipReason || signal.orderStatus || "SIGNAL",
36531
+ signalId: signal.signalId,
36532
+ direction: signal.direction,
36533
+ accountId: signal.accountId,
36534
+ deploymentId: signal.deploymentId,
36535
+ runtimeLineage: signal.runtimeLineage,
36536
+ orderStatus: signal.orderStatus,
36537
+ orderSkipReason: signal.orderSkipReason,
36538
+ ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
36539
+ ...signal.ml ? { ml: signal.ml } : {}
36540
+ }));
36541
+ toFiniteNumberOrNull4 = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
36542
+ sumOptionalNumbers = (values) => {
36543
+ let sum3 = 0;
36544
+ let hasValue = false;
36545
+ for (const value of values) {
36546
+ if (typeof value !== "number" || !Number.isFinite(value)) {
36547
+ continue;
36548
+ }
36549
+ sum3 += value;
36550
+ hasValue = true;
36551
+ }
36552
+ return hasValue ? Number(sum3.toFixed(12)) : null;
36553
+ };
36554
+ firstFiniteNumber = (values) => {
36555
+ for (const value of values) {
36556
+ if (typeof value === "number" && Number.isFinite(value)) {
36557
+ return value;
36558
+ }
36559
+ }
36560
+ return null;
36561
+ };
36562
+ firstString = (values) => values.find((value) => typeof value === "string" && value.trim());
36563
+ getExchangeExecutionMergeKey = (entry) => {
36564
+ const orderId = firstString([entry.orderId, entry.orderLinkId]);
36565
+ return orderId ? `${entry.symbol}::${entry.direction}::${orderId}` : null;
36566
+ };
36567
+ mergeExchangeExecutionGroup = (group) => {
36568
+ const [first] = group;
36569
+ const qty = sumOptionalNumbers(group.map((entry) => entry.qty)) ?? first.qty;
36570
+ const weightedPriceSum = group.reduce((sum3, entry) => {
36571
+ const entryQty = toFiniteNumberOrNull4(entry.qty);
36572
+ const entryPrice2 = toFiniteNumberOrNull4(entry.entryPrice);
36573
+ return entryQty != null && entryPrice2 != null ? sum3 + entryQty * entryPrice2 : sum3;
36574
+ }, 0);
36575
+ const entryPrice = qty > 0 && weightedPriceSum > 0 ? Number((weightedPriceSum / qty).toFixed(12)) : firstFiniteNumber(group.map((entry) => entry.entryPrice));
36576
+ const openFee = sumOptionalNumbers(group.map((entry) => entry.openFee));
36577
+ const closeFee = sumOptionalNumbers(group.map((entry) => entry.closeFee));
36578
+ const fundingFee = firstFiniteNumber(group.map((entry) => entry.fundingFee));
36579
+ const totalFee = sumOptionalNumbers([openFee, closeFee, fundingFee]) ?? firstFiniteNumber(group.map((entry) => entry.totalFee));
36580
+ return {
36581
+ symbol: first.symbol,
36582
+ direction: first.direction,
36583
+ qty,
36584
+ entryPrice,
36585
+ entryTimestamp: Math.min(...group.map((entry) => entry.entryTimestamp)),
36586
+ ...firstString(group.map((entry) => entry.orderId)) ? { orderId: firstString(group.map((entry) => entry.orderId)) } : {},
36587
+ ...firstString(group.map((entry) => entry.orderLinkId)) ? { orderLinkId: firstString(group.map((entry) => entry.orderLinkId)) } : {},
36588
+ ...firstFiniteNumber(group.map((entry) => entry.takeProfitPrice)) != null ? {
36589
+ takeProfitPrice: firstFiniteNumber(
36590
+ group.map((entry) => entry.takeProfitPrice)
36591
+ )
36592
+ } : {},
36593
+ ...firstFiniteNumber(group.map((entry) => entry.stopLossPrice)) != null ? {
36594
+ stopLossPrice: firstFiniteNumber(
36595
+ group.map((entry) => entry.stopLossPrice)
36596
+ )
36597
+ } : {},
36598
+ ...firstFiniteNumber(group.map((entry) => entry.exitPrice)) != null ? { exitPrice: firstFiniteNumber(group.map((entry) => entry.exitPrice)) } : {},
36599
+ ...firstFiniteNumber(group.map((entry) => entry.exitTimestamp)) != null ? {
36600
+ exitTimestamp: firstFiniteNumber(
36601
+ group.map((entry) => entry.exitTimestamp)
36602
+ )
36603
+ } : {},
36604
+ ...sumOptionalNumbers(group.map((entry) => entry.closedPnl)) != null ? { closedPnl: sumOptionalNumbers(group.map((entry) => entry.closedPnl)) } : {},
36605
+ ...openFee != null ? { openFee } : {},
36606
+ ...closeFee != null ? { closeFee } : {},
36607
+ ...fundingFee != null ? { fundingFee } : {},
36608
+ ...totalFee != null ? { totalFee } : {}
36609
+ };
36610
+ };
36611
+ mergeExchangeEntryExecutions = (rows) => {
36612
+ const mergedRows = [];
36613
+ const groups = /* @__PURE__ */ new Map();
36614
+ for (const row of rows) {
36615
+ const key = getExchangeExecutionMergeKey(row);
36616
+ if (!key) {
36617
+ mergedRows.push(row);
36618
+ continue;
36619
+ }
36620
+ const group = groups.get(key) ?? [];
36621
+ group.push(row);
36622
+ groups.set(key, group);
36623
+ }
36624
+ for (const group of groups.values()) {
36625
+ mergedRows.push(
36626
+ group.length === 1 ? group[0] : mergeExchangeExecutionGroup(group)
36627
+ );
36628
+ }
36629
+ return mergedRows.sort(
36630
+ (left, right) => left.entryTimestamp - right.entryTimestamp
36631
+ );
36632
+ };
36633
+ loadExchangeEntryRows = async ({
36634
+ connector,
36635
+ startTime,
36636
+ endTime
36637
+ }) => {
36638
+ if (typeof connector.getEntryExecutions !== "function") {
36639
+ console.log(
36640
+ import_chalk50.default.yellow(
36641
+ "runtime compare: connector does not support entry execution history"
36642
+ )
36643
+ );
36644
+ return [];
36645
+ }
36646
+ const rows = [];
36647
+ const chunks = splitExchangeHistoryTimeRange({ startTime, endTime });
36648
+ for (const chunk of chunks) {
36649
+ try {
36650
+ const chunkRows = await connector.getEntryExecutions({
36651
+ startTime: chunk.startTime,
36652
+ endTime: chunk.endTime,
36653
+ limit: 100
36333
36654
  });
36655
+ if (chunkRows.length >= 100) {
36656
+ console.log(
36657
+ import_chalk50.default.yellow(
36658
+ "runtime compare: exchange entry executions returned 100 rows (connector cap); older entry trades in this chunk may be truncated"
36659
+ )
36660
+ );
36661
+ }
36662
+ rows.push(...chunkRows);
36663
+ } catch (error) {
36664
+ console.log(
36665
+ import_chalk50.default.yellow(
36666
+ `runtime compare: getEntryExecutions failed: ${formatRuntimeTradeSyncError(error)}`
36667
+ )
36668
+ );
36334
36669
  }
36335
36670
  }
36336
- return candidates.map((candidate) => ({
36337
- ...candidate,
36338
- timestampDiffMs: Math.abs(
36339
- candidate.timestamp + signalTimestampOffsetMs - exchangeEntry.entryTimestamp
36340
- )
36341
- })).filter((candidate) => candidate.timestampDiffMs <= toleranceMs).sort(
36342
- (left, right) => left.timestampDiffMs - right.timestampDiffMs || Number(right.orderStatus === "failed") - Number(left.orderStatus === "failed")
36343
- )[0] ?? null;
36671
+ return mergeExchangeEntryExecutions(rows);
36344
36672
  };
36345
- splitExchangeMatchesByRuntimeOrderStatus = ({
36346
- matched,
36347
- strategyNameByOrderLinkKey,
36348
- runtimeSignals,
36349
- runtimeSignalEvaluations,
36350
- toleranceMs,
36351
- signalTimestampOffsetMs
36673
+ loadExchangeEntriesForComparison = async ({
36674
+ connector,
36675
+ startTime,
36676
+ endTime
36352
36677
  }) => {
36353
- const orderFailed = [];
36354
- const completed = matched.filter((item) => {
36355
- const strategyName = resolveReplayStrategyNameFromExchangeEntry({
36356
- exchangeEntry: item.exchange,
36357
- strategyNameByOrderLinkKey
36358
- });
36359
- if (!strategyName) {
36360
- return true;
36361
- }
36362
- const outcome = findExchangeRuntimeOutcome({
36363
- exchangeEntry: item.exchange,
36364
- strategyName,
36365
- runtimeSignals,
36366
- runtimeSignalEvaluations,
36367
- toleranceMs,
36368
- signalTimestampOffsetMs
36369
- });
36370
- if (outcome?.orderStatus !== "failed") {
36371
- return true;
36678
+ const entryRows = await loadExchangeEntryRows({
36679
+ connector,
36680
+ startTime,
36681
+ endTime
36682
+ });
36683
+ const closedPnlRows = await loadClosedPnlRows({
36684
+ connector,
36685
+ startTime,
36686
+ endTime,
36687
+ callbacks: {
36688
+ onUnsupported: () => {
36689
+ console.log(
36690
+ import_chalk50.default.yellow(
36691
+ "runtime compare: connector does not support getClosedPnl, using runtime trade records as-is"
36692
+ )
36693
+ );
36694
+ },
36695
+ onCapped: () => {
36696
+ console.log(
36697
+ import_chalk50.default.yellow(
36698
+ "runtime compare: exchange closed pnl returned 100 rows (connector cap); older closed trades in the window may be truncated"
36699
+ )
36700
+ );
36701
+ },
36702
+ onError: (error) => {
36703
+ console.log(
36704
+ import_chalk50.default.yellow(
36705
+ `runtime compare: getClosedPnl failed: ${formatRuntimeTradeSyncError(error)}`
36706
+ )
36707
+ );
36708
+ }
36372
36709
  }
36373
- orderFailed.push({
36374
- ...item,
36375
- reason: outcome.reason || "orderStatus=failed"
36376
- });
36377
- return false;
36378
36710
  });
36379
- return { completed, orderFailed };
36711
+ const closedPnlByOrderId = new Map(
36712
+ closedPnlRows.filter((row) => typeof row.orderId === "string" && row.orderId.trim()).map((row) => [row.orderId, row])
36713
+ );
36714
+ return entryRows.map((entry) => {
36715
+ const closedPnl = typeof entry.orderId === "string" ? closedPnlByOrderId.get(entry.orderId) : null;
36716
+ return {
36717
+ ...entry,
36718
+ exitPrice: closedPnl?.exitPrice ?? entry.exitPrice ?? null,
36719
+ exitTimestamp: closedPnl?.closedAt ?? entry.exitTimestamp ?? null,
36720
+ closedPnl: closedPnl?.closedPnl ?? entry.closedPnl ?? null,
36721
+ openFee: closedPnl?.openFee ?? entry.openFee ?? null,
36722
+ closeFee: closedPnl?.closeFee ?? entry.closeFee ?? null,
36723
+ fundingFee: closedPnl?.fundingFee ?? entry.fundingFee ?? null,
36724
+ totalFee: closedPnl?.totalFee ?? entry.totalFee ?? null
36725
+ };
36726
+ });
36380
36727
  };
36381
36728
  saveAndPrintReplayExchangeComparison = async ({
36382
36729
  liveStrategySummaries,
@@ -36396,7 +36743,6 @@ var init_runtimeComparison = __esm({
36396
36743
  const strategyNameByOrderLinkKey = buildStrategyNameByOrderLinkKey(
36397
36744
  liveStrategySummaries.map((summary) => summary.strategyName)
36398
36745
  );
36399
- const exchangeOutcomeByEntry = /* @__PURE__ */ new Map();
36400
36746
  const exchangeEntries = rawExchangeEntries.filter((entry) => {
36401
36747
  const strategyName = resolveReplayStrategyNameFromExchangeEntry({
36402
36748
  exchangeEntry: entry,
@@ -36405,7 +36751,7 @@ var init_runtimeComparison = __esm({
36405
36751
  if (!strategyName) {
36406
36752
  return false;
36407
36753
  }
36408
- const outcome = findExchangeRuntimeOutcome({
36754
+ return hasLineageLinkedRuntimeOutcome({
36409
36755
  exchangeEntry: entry,
36410
36756
  strategyName,
36411
36757
  runtimeSignals,
@@ -36413,24 +36759,18 @@ var init_runtimeComparison = __esm({
36413
36759
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
36414
36760
  signalTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
36415
36761
  });
36416
- exchangeOutcomeByEntry.set(entry, outcome);
36417
- return outcome != null;
36418
36762
  });
36419
36763
  const comparisonLineage = {
36420
36764
  ...lineage,
36421
36765
  excludedExchangeEntries: lineage.excludedExchangeEntries + rawExchangeEntries.length - exchangeEntries.length
36422
36766
  };
36423
- const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(backtestEntries);
36424
36767
  if (!exchangeEntries.length) {
36425
- console.log("");
36426
- console.log(
36427
- import_chalk48.default.yellow(
36428
- `SIGNALS REPLAY VS EXCHANGE: no lineage-linked exchange entry executions found for ${connectorName} in ${(0, import_time11.formatUnix)(
36429
- window.start
36430
- )} -> ${(0, import_time11.formatUnix)(window.end)}`
36431
- )
36768
+ writeReplayComparisonReport(
36769
+ buildNoExchangeEntriesReport({
36770
+ connectorName,
36771
+ window
36772
+ })
36432
36773
  );
36433
- console.log("");
36434
36774
  const details2 = buildReplayExchangeComparisonDetails({
36435
36775
  matched: [],
36436
36776
  exchangeOnly: [],
@@ -36445,6 +36785,16 @@ var init_runtimeComparison = __esm({
36445
36785
  replaySignals,
36446
36786
  replaySignalEvaluations
36447
36787
  });
36788
+ const emptyRowsByStrategy = new Map(
36789
+ buildExchangeComparisonRows({
36790
+ liveStrategySummaries,
36791
+ backtestEntries,
36792
+ matched: [],
36793
+ orderFailed: [],
36794
+ exchangeOnly: [],
36795
+ strategyNameByOrderLinkKey
36796
+ }).map((row) => [row.strategyName, row])
36797
+ );
36448
36798
  return {
36449
36799
  mode: "exchange",
36450
36800
  syncedTradesCount: 0,
@@ -36457,21 +36807,9 @@ var init_runtimeComparison = __esm({
36457
36807
  backtestOnlyCount: backtestEntries.length,
36458
36808
  details: details2,
36459
36809
  lineage: comparisonLineage,
36460
- rows: liveStrategySummaries.map((summary) => ({
36461
- strategyName: summary.strategyName,
36462
- backtestEntries: backtestEntries.filter(
36463
- (entry) => entry.strategy === summary.strategyName
36464
- ).length,
36465
- backtestNetProfit: backtestPnlByStrategy.get(summary.strategyName) ?? 0,
36466
- runtimeTrades: 0,
36467
- runtimePnl: 0,
36468
- matched: 0,
36469
- orderFailed: 0,
36470
- runtimeOnly: 0,
36471
- backtestOnly: backtestEntries.filter(
36472
- (entry) => entry.strategy === summary.strategyName
36473
- ).length
36474
- }))
36810
+ rows: liveStrategySummaries.map(
36811
+ ({ strategyName }) => emptyRowsByStrategy.get(strategyName)
36812
+ )
36475
36813
  };
36476
36814
  }
36477
36815
  const comparison = compareExchangeEntriesToBacktest({
@@ -36488,62 +36826,6 @@ var init_runtimeComparison = __esm({
36488
36826
  toleranceMs: REPLAY_RUNTIME_COMPARE_TOLERANCE_MS,
36489
36827
  signalTimestampOffsetMs: getReplayEntryTimestampCompareOffsetMs()
36490
36828
  });
36491
- const rowByStrategy = /* @__PURE__ */ new Map();
36492
- const ensureRow = (strategyName) => {
36493
- const existing = rowByStrategy.get(strategyName);
36494
- if (existing) {
36495
- return existing;
36496
- }
36497
- const next = {
36498
- strategyName,
36499
- backtestEntries: 0,
36500
- backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
36501
- runtimeTrades: 0,
36502
- runtimePnl: 0,
36503
- matched: 0,
36504
- orderFailed: 0,
36505
- runtimeOnly: 0,
36506
- backtestOnly: 0
36507
- };
36508
- rowByStrategy.set(strategyName, next);
36509
- return next;
36510
- };
36511
- for (const summary of liveStrategySummaries) {
36512
- ensureRow(summary.strategyName);
36513
- }
36514
- for (const entry of backtestEntries) {
36515
- ensureRow(entry.strategy).backtestEntries += 1;
36516
- }
36517
- for (const item of matched) {
36518
- const row = ensureRow(item.backtest.strategy);
36519
- row.runtimeTrades += 1;
36520
- row.matched += 1;
36521
- if (typeof item.exchange.closedPnl === "number" && Number.isFinite(item.exchange.closedPnl)) {
36522
- row.runtimePnl += item.exchange.closedPnl;
36523
- }
36524
- }
36525
- for (const item of orderFailed) {
36526
- const row = ensureRow(item.backtest.strategy);
36527
- row.runtimeTrades += 1;
36528
- row.orderFailed += 1;
36529
- }
36530
- for (const entry of comparison.backtestOnly) {
36531
- ensureRow(entry.strategy).backtestOnly += 1;
36532
- }
36533
- if (comparison.exchangeOnly.length) {
36534
- for (const entry of comparison.exchangeOnly) {
36535
- const strategyName = resolveReplayStrategyNameFromExchangeEntry({
36536
- exchangeEntry: entry,
36537
- strategyNameByOrderLinkKey
36538
- }) ?? "[exchange-unmatched]";
36539
- const row = ensureRow(strategyName);
36540
- row.runtimeTrades += 1;
36541
- row.runtimeOnly += 1;
36542
- if (typeof entry.closedPnl === "number" && Number.isFinite(entry.closedPnl)) {
36543
- row.runtimePnl += entry.closedPnl;
36544
- }
36545
- }
36546
- }
36547
36829
  const details = buildReplayExchangeComparisonDetails({
36548
36830
  matched,
36549
36831
  orderFailed,
@@ -36559,45 +36841,22 @@ var init_runtimeComparison = __esm({
36559
36841
  replaySignals,
36560
36842
  replaySignalEvaluations
36561
36843
  });
36562
- const rows = [...rowByStrategy.values()].map((row) => ({
36563
- ...row,
36564
- runtimePnl: Number(row.runtimePnl.toFixed(2))
36565
- })).sort((left, right) => left.strategyName.localeCompare(right.strategyName));
36566
- const colorizedRows = rows.map((row) => {
36567
- const btPnlColor = row.backtestNetProfit > 0 ? import_chalk48.default.green : row.backtestNetProfit < 0 ? import_chalk48.default.red : import_chalk48.default.gray;
36568
- const rtPnlColor = row.runtimePnl > 0 ? import_chalk48.default.green : row.runtimePnl < 0 ? import_chalk48.default.red : import_chalk48.default.gray;
36569
- return [
36570
- import_chalk48.default.blue(row.strategyName),
36571
- import_chalk48.default.cyan(String(row.backtestEntries)),
36572
- btPnlColor(`${row.backtestNetProfit.toFixed(2)}$`),
36573
- import_chalk48.default.yellow(String(row.runtimeTrades)),
36574
- rtPnlColor(`${row.runtimePnl.toFixed(2)}$`),
36575
- import_chalk48.default.green(String(row.matched)),
36576
- import_chalk48.default.red(String(row.orderFailed)),
36577
- import_chalk48.default.yellow(String(row.runtimeOnly)),
36578
- import_chalk48.default.magenta(String(row.backtestOnly)),
36579
- import_chalk48.default.red(String(getReplayRuntimeUnmatchedCount(row)))
36580
- ];
36844
+ const rows = buildExchangeComparisonRows({
36845
+ liveStrategySummaries,
36846
+ backtestEntries,
36847
+ matched,
36848
+ orderFailed,
36849
+ exchangeOnly: comparison.exchangeOnly,
36850
+ strategyNameByOrderLinkKey
36581
36851
  });
36582
- console.log("");
36583
- console.log(
36584
- `SIGNALS REPLAY VS EXCHANGE BY STRATEGY (connector=${connectorName}, inferredStrategy=orderLinkId | nearest backtest entry, tolerance=${formatReplayRuntimeCompareTolerance()})`
36585
- );
36586
- console.log(createTable(REPLAY_RUNTIME_COMPARISON_HEADERS, colorizedRows));
36587
- const runtimeOnlyDrilldownSummary = formatDrilldownSummary(
36588
- details.mismatchDrilldown?.summary.runtimeOnly ?? {}
36589
- );
36590
- const backtestOnlyDrilldownSummary = formatDrilldownSummary(
36591
- details.mismatchDrilldown?.summary.backtestOnly ?? {}
36852
+ writeReplayComparisonReport(
36853
+ buildReplayComparisonReport({
36854
+ mode: "exchange",
36855
+ connectorName,
36856
+ rows,
36857
+ details
36858
+ })
36592
36859
  );
36593
- if (runtimeOnlyDrilldownSummary || backtestOnlyDrilldownSummary) {
36594
- console.log(
36595
- import_chalk48.default.gray(
36596
- `Mismatch drilldown: exchangeOnly=[${runtimeOnlyDrilldownSummary || "none"}], backtestOnly=[${backtestOnlyDrilldownSummary || "none"}]`
36597
- )
36598
- );
36599
- }
36600
- console.log("");
36601
36860
  return {
36602
36861
  mode: "exchange",
36603
36862
  syncedTradesCount: exchangeEntries.length,
@@ -36664,7 +36923,7 @@ var init_runtimeComparison = __esm({
36664
36923
  ]);
36665
36924
  if (runtimeEvidence2) {
36666
36925
  console.log(
36667
- import_chalk48.default.gray(
36926
+ import_chalk50.default.gray(
36668
36927
  `runtime compare: using immutable evidence ${runtimeEvidence2.path}`
36669
36928
  )
36670
36929
  );
@@ -36678,7 +36937,7 @@ var init_runtimeComparison = __esm({
36678
36937
  openPositionCallbacks: {
36679
36938
  onError: (error) => {
36680
36939
  console.log(
36681
- import_chalk48.default.yellow(
36940
+ import_chalk50.default.yellow(
36682
36941
  `runtime compare: getOpenPositionPnl failed: ${formatRuntimeTradeSyncError(error)}; continuing without open-position mark prices`
36683
36942
  )
36684
36943
  );
@@ -36687,21 +36946,21 @@ var init_runtimeComparison = __esm({
36687
36946
  closedPnlCallbacks: {
36688
36947
  onUnsupported: () => {
36689
36948
  console.log(
36690
- import_chalk48.default.yellow(
36949
+ import_chalk50.default.yellow(
36691
36950
  "runtime compare: connector does not support getClosedPnl, using runtime trade records as-is"
36692
36951
  )
36693
36952
  );
36694
36953
  },
36695
36954
  onCapped: () => {
36696
36955
  console.log(
36697
- import_chalk48.default.yellow(
36956
+ import_chalk50.default.yellow(
36698
36957
  "runtime compare: exchange closed pnl returned 100 rows (connector cap); older closed trades in the window may be truncated"
36699
36958
  )
36700
36959
  );
36701
36960
  },
36702
36961
  onError: (error) => {
36703
36962
  console.log(
36704
- import_chalk48.default.yellow(
36963
+ import_chalk50.default.yellow(
36705
36964
  `runtime compare: getClosedPnl failed: ${formatRuntimeTradeSyncError(error)}`
36706
36965
  )
36707
36966
  );
@@ -36721,15 +36980,9 @@ var init_runtimeComparison = __esm({
36721
36980
  (trade) => trade.entryTimestamp >= window.start && trade.entryTimestamp < window.end && relevantStrategies.has(trade.strategy)
36722
36981
  );
36723
36982
  if (!windowRuntimeTrades.length && !runtimeEvidence2) {
36724
- console.log("");
36725
- console.log(
36726
- import_chalk48.default.yellow(
36727
- `SIGNALS REPLAY VS RUNTIME: no local runtime trades found for ${connectorName} in ${(0, import_time11.formatUnix)(
36728
- window.start
36729
- )} -> ${(0, import_time11.formatUnix)(window.end)} with matching lineage; checking lineage-linked exchange executions`
36730
- )
36983
+ writeReplayComparisonReport(
36984
+ buildNoRuntimeTradesReport({ connectorName, window })
36731
36985
  );
36732
- console.log("");
36733
36986
  return saveAndPrintReplayExchangeComparison({
36734
36987
  liveStrategySummaries,
36735
36988
  backtestEntries: comparableBacktestEntries,
@@ -36741,9 +36994,6 @@ var init_runtimeComparison = __esm({
36741
36994
  });
36742
36995
  }
36743
36996
  const runtimeSummaries = summarizeRuntimeTradesByStrategy(windowRuntimeTrades);
36744
- const runtimeSummaryByStrategy = new Map(
36745
- runtimeSummaries.map((summary) => [summary.strategyName, summary])
36746
- );
36747
36997
  const rawRuntimeEntries = extractRuntimeParityEntries(windowRuntimeTrades);
36748
36998
  const runtimeDedupe = dedupeRuntimeParityEntries(rawRuntimeEntries);
36749
36999
  const comparison = compareTradeParityEntries({
@@ -36773,68 +37023,20 @@ var init_runtimeComparison = __esm({
36773
37023
  runtimeOnlyEntries: comparison.runtimeOnly,
36774
37024
  backtestOnlyEntries: comparison.backtestOnly
36775
37025
  });
36776
- const parityByStrategy = new Map(parityRows);
36777
- const liveSummaryByStrategy = new Map(
36778
- liveStrategySummaries.map((summary) => [summary.strategyName, summary])
36779
- );
36780
- const backtestPnlByStrategy = summarizeBacktestPnlByStrategy(
36781
- comparableBacktestEntries
36782
- );
36783
- const strategyNames = /* @__PURE__ */ new Set([
36784
- ...liveSummaryByStrategy.keys(),
36785
- ...runtimeSummaryByStrategy.keys(),
36786
- ...parityByStrategy.keys()
36787
- ]);
36788
- const rows = [...strategyNames].sort((left, right) => left.localeCompare(right)).map((strategyName) => {
36789
- const runtimeSummary = runtimeSummaryByStrategy.get(strategyName);
36790
- const parity = parityByStrategy.get(strategyName);
36791
- return {
36792
- strategyName,
36793
- backtestEntries: parity?.backtest ?? 0,
36794
- backtestNetProfit: backtestPnlByStrategy.get(strategyName) ?? 0,
36795
- runtimeTrades: runtimeSummary?.trades ?? 0,
36796
- runtimePnl: runtimeSummary?.totalPnl ?? 0,
36797
- matched: parity?.matched ?? 0,
36798
- orderFailed: 0,
36799
- runtimeOnly: parity?.runtimeOnly ?? 0,
36800
- backtestOnly: parity?.backtestOnly ?? 0
36801
- };
36802
- });
36803
- const colorizedRows = rows.map((row) => {
36804
- const btPnlColor = row.backtestNetProfit > 0 ? import_chalk48.default.green : row.backtestNetProfit < 0 ? import_chalk48.default.red : import_chalk48.default.gray;
36805
- const rtPnlColor = row.runtimePnl > 0 ? import_chalk48.default.green : row.runtimePnl < 0 ? import_chalk48.default.red : import_chalk48.default.gray;
36806
- return [
36807
- import_chalk48.default.blue(row.strategyName),
36808
- import_chalk48.default.cyan(String(row.backtestEntries)),
36809
- btPnlColor(`${row.backtestNetProfit.toFixed(2)}$`),
36810
- import_chalk48.default.yellow(String(row.runtimeTrades)),
36811
- rtPnlColor(`${row.runtimePnl.toFixed(2)}$`),
36812
- import_chalk48.default.green(String(row.matched)),
36813
- import_chalk48.default.red(String(row.orderFailed)),
36814
- import_chalk48.default.yellow(String(row.runtimeOnly)),
36815
- import_chalk48.default.magenta(String(row.backtestOnly)),
36816
- import_chalk48.default.red(String(getReplayRuntimeUnmatchedCount(row)))
36817
- ];
37026
+ const rows = buildRuntimeComparisonRows({
37027
+ liveStrategySummaries,
37028
+ runtimeSummaries,
37029
+ parityRows,
37030
+ backtestEntries: comparableBacktestEntries
36818
37031
  });
36819
- console.log("");
36820
- console.log(
36821
- `SIGNALS REPLAY VS RUNTIME BY STRATEGY (connector=${connectorName}, tolerance=${formatReplayRuntimeCompareTolerance()})`
36822
- );
36823
- console.log(createTable(REPLAY_RUNTIME_COMPARISON_HEADERS, colorizedRows));
36824
- const runtimeOnlyDrilldownSummary = formatDrilldownSummary(
36825
- details.mismatchDrilldown?.summary.runtimeOnly ?? {}
36826
- );
36827
- const backtestOnlyDrilldownSummary = formatDrilldownSummary(
36828
- details.mismatchDrilldown?.summary.backtestOnly ?? {}
37032
+ writeReplayComparisonReport(
37033
+ buildReplayComparisonReport({
37034
+ mode: "runtime",
37035
+ connectorName,
37036
+ rows,
37037
+ details
37038
+ })
36829
37039
  );
36830
- if (runtimeOnlyDrilldownSummary || backtestOnlyDrilldownSummary) {
36831
- console.log(
36832
- import_chalk48.default.gray(
36833
- `Mismatch drilldown: runtimeOnly=[${runtimeOnlyDrilldownSummary || "none"}], backtestOnly=[${backtestOnlyDrilldownSummary || "none"}]`
36834
- )
36835
- );
36836
- }
36837
- console.log("");
36838
37040
  return {
36839
37041
  mode: "runtime",
36840
37042
  syncedTradesCount: syncedRuntimeTrades.length,
@@ -37280,14 +37482,14 @@ var init_outputReport = __esm({
37280
37482
  });
37281
37483
 
37282
37484
  // src/scripts/replayRunner.ts
37283
- var import_chalk49, import_constants19, import_data5, import_time12, import_redis21, prepareReplayBinanceMarketContext, finishReplay, replayBacktest;
37485
+ var import_chalk51, import_constants19, import_data5, import_time14, import_redis21, prepareReplayBinanceMarketContext, finishReplay, replayBacktest;
37284
37486
  var init_replayRunner = __esm({
37285
37487
  "src/scripts/replayRunner.ts"() {
37286
37488
  "use strict";
37287
- import_chalk49 = __toESM(require("chalk"));
37489
+ import_chalk51 = __toESM(require("chalk"));
37288
37490
  import_constants19 = require("@tradejs/core/constants");
37289
37491
  import_data5 = require("@tradejs/core/data");
37290
- import_time12 = require("@tradejs/core/time");
37492
+ import_time14 = require("@tradejs/core/time");
37291
37493
  import_redis21 = require("@tradejs/infra/redis");
37292
37494
  init_runFormatting();
37293
37495
  init_runEnvironment();
@@ -37318,7 +37520,7 @@ var init_replayRunner = __esm({
37318
37520
  aiEnabled: preparedRun.aiEnabled,
37319
37521
  mlEnabled: preparedRun.mlEnabled,
37320
37522
  strategyNames: preparedRun.strategyNames,
37321
- log: (message) => console.log(import_chalk49.default.gray(message))
37523
+ log: (message) => console.log(import_chalk51.default.gray(message))
37322
37524
  });
37323
37525
  };
37324
37526
  finishReplay = async ({
@@ -37346,9 +37548,9 @@ var init_replayRunner = __esm({
37346
37548
  const replayChartSnapshot = replayFlags.chart ? buildReplayChartSnapshot({
37347
37549
  replayResult,
37348
37550
  generatedAt: finishedAt.getTime(),
37349
- runLabel: `${(0, import_time12.formatUnix)(
37551
+ runLabel: `${(0, import_time14.formatUnix)(
37350
37552
  replayResult.orderLog[0]?.timestamp ?? finishedAt.getTime()
37351
- )} -> ${(0, import_time12.formatUnix)(
37553
+ )} -> ${(0, import_time14.formatUnix)(
37352
37554
  replayResult.orderLog[replayResult.orderLog.length - 1]?.timestamp ?? finishedAt.getTime()
37353
37555
  )}`
37354
37556
  }) : null;
@@ -37371,8 +37573,8 @@ var init_replayRunner = __esm({
37371
37573
  strategySnapshot: replayStrategySnapshot,
37372
37574
  runtimeComparison: replayRuntimeComparison
37373
37575
  });
37374
- console.log(import_chalk49.default.green(`Replay report: ${outputReport.markdownPath}`));
37375
- console.log(import_chalk49.default.green(`Replay report JSON: ${outputReport.jsonPath}`));
37576
+ console.log(import_chalk51.default.green(`Replay report: ${outputReport.markdownPath}`));
37577
+ console.log(import_chalk51.default.green(`Replay report JSON: ${outputReport.jsonPath}`));
37376
37578
  await (0, import_redis21.setData)(
37377
37579
  replayKey,
37378
37580
  {
@@ -37525,9 +37727,9 @@ var init_replayRunner = __esm({
37525
37727
  ),
37526
37728
  strategyNames: replayStrategies.map(({ strategyName }) => strategyName)
37527
37729
  });
37528
- console.log(import_chalk49.default.yellow(`tickers: ${preparedRun.tickers.length}`));
37730
+ console.log(import_chalk51.default.yellow(`tickers: ${preparedRun.tickers.length}`));
37529
37731
  console.log(
37530
- import_chalk49.default.gray(
37732
+ import_chalk51.default.gray(
37531
37733
  `mode: replay (${replayStrategies.map(({ strategyName }) => strategyName).join(", ")})`
37532
37734
  )
37533
37735
  );
@@ -38410,12 +38612,12 @@ var user_add_exports = {};
38410
38612
  __export(user_add_exports, {
38411
38613
  main: () => main47
38412
38614
  });
38413
- var import_args34, import_chalk50, import_bcryptjs, import_redis22, flags31, main47;
38615
+ var import_args34, import_chalk52, import_bcryptjs, import_redis22, flags31, main47;
38414
38616
  var init_user_add = __esm({
38415
38617
  "src/scripts/user-add.ts"() {
38416
38618
  "use strict";
38417
38619
  import_args34 = __toESM(require("args"));
38418
- import_chalk50 = __toESM(require("chalk"));
38620
+ import_chalk52 = __toESM(require("chalk"));
38419
38621
  import_bcryptjs = __toESM(require("bcryptjs"));
38420
38622
  import_redis22 = require("@tradejs/infra/redis");
38421
38623
  import_args34.default.example("yarn user-add -u myname -p 123456", "Create or update user");
@@ -38426,7 +38628,7 @@ var init_user_add = __esm({
38426
38628
  const userName3 = String(flags31.user || "").trim();
38427
38629
  const password = String(flags31.password || "");
38428
38630
  if (!userName3 || !password) {
38429
- console.error(import_chalk50.default.red("Missing -U <user> or -P <password>"));
38631
+ console.error(import_chalk52.default.red("Missing -U <user> or -P <password>"));
38430
38632
  process.exit(1);
38431
38633
  }
38432
38634
  const passwordHash = await import_bcryptjs.default.hash(password, 10);
@@ -38440,7 +38642,7 @@ var init_user_add = __esm({
38440
38642
  await (0, import_redis22.setData)(import_redis22.redisKeys.user(userName3), next, {
38441
38643
  expire: 0
38442
38644
  });
38443
- console.log(import_chalk50.default.green(`User ${userName3} updated`));
38645
+ console.log(import_chalk52.default.green(`User ${userName3} updated`));
38444
38646
  process.exit(0);
38445
38647
  };
38446
38648
  }