@tradejs/cli 3.1.19 → 3.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,9 +38,9 @@ module.exports = __toCommonJS(runtimeParity_exports);
38
38
  var import_config = require("dotenv/config");
39
39
  var import_node_crypto = require("crypto");
40
40
  var import_args = __toESM(require("args"));
41
- var import_chalk6 = __toESM(require("chalk"));
41
+ var import_chalk7 = __toESM(require("chalk"));
42
42
  var import_constants3 = require("@tradejs/core/constants");
43
- var import_time3 = require("@tradejs/core/time");
43
+ var import_time5 = require("@tradejs/core/time");
44
44
  var import_logger = require("@tradejs/infra/logger");
45
45
  var import_redis3 = require("@tradejs/infra/redis");
46
46
  var import_cli3 = require("@tradejs/node/cli");
@@ -332,51 +332,6 @@ var normalizeCliArgv = (argv, legacyFlagMap) => argv.map((arg) => {
332
332
  return `${normalizedFlag}=${rest.join("=")}`;
333
333
  });
334
334
 
335
- // src/lib/paritySummary.ts
336
- var summarizeTradeParityByStrategy = ({
337
- runtimeEntries,
338
- runtimeDuplicateEntries,
339
- backtestEntries,
340
- matchedEntries,
341
- runtimeOnlyEntries,
342
- backtestOnlyEntries
343
- }) => {
344
- const rows = /* @__PURE__ */ new Map();
345
- const ensureRow = (strategyName) => {
346
- const row = rows.get(strategyName) ?? {
347
- runtime: 0,
348
- runtimeDuplicates: 0,
349
- backtest: 0,
350
- matched: 0,
351
- runtimeOnly: 0,
352
- backtestOnly: 0
353
- };
354
- rows.set(strategyName, row);
355
- return row;
356
- };
357
- for (const entry of runtimeEntries) {
358
- ensureRow(entry.strategy).runtime += 1;
359
- }
360
- for (const entry of runtimeDuplicateEntries) {
361
- ensureRow(entry.strategy).runtimeDuplicates += 1;
362
- }
363
- for (const entry of backtestEntries) {
364
- ensureRow(entry.strategy).backtest += 1;
365
- }
366
- for (const entry of matchedEntries) {
367
- ensureRow(entry.runtime.strategy).matched += 1;
368
- }
369
- for (const entry of runtimeOnlyEntries) {
370
- ensureRow(entry.strategy).runtimeOnly += 1;
371
- }
372
- for (const entry of backtestOnlyEntries) {
373
- ensureRow(entry.strategy).backtestOnly += 1;
374
- }
375
- return [...rows.entries()].sort(
376
- ([left], [right]) => left.localeCompare(right)
377
- );
378
- };
379
-
380
335
  // src/lib/runtimeRedis.ts
381
336
  var import_redis = require("@tradejs/infra/redis");
382
337
  var import_time = require("@tradejs/core/time");
@@ -4460,6 +4415,76 @@ var buildReplayTargets = async ({
4460
4415
  );
4461
4416
  };
4462
4417
 
4418
+ // src/lib/runtimeParity/evidence.ts
4419
+ var matchesSelection = (context, artifact) => (!context.strategy || artifact.strategy === context.strategy) && (!context.symbols || context.symbols.has(artifact.symbol));
4420
+ var loadRuntimeParityEvidence = async (context, adapters) => {
4421
+ const range = {
4422
+ startTime: context.window.start,
4423
+ endTime: context.window.end
4424
+ };
4425
+ const [allRuntimeTrades, allRuntimeSignals, allRuntimeSignalEvaluations] = await Promise.all([
4426
+ adapters.loadRuntimeTrades(context.userName, range),
4427
+ adapters.loadRuntimeSignals(context.userName, range),
4428
+ adapters.loadRuntimeSignalEvaluations(context.userName, range)
4429
+ ]);
4430
+ return {
4431
+ runtimeTrades: allRuntimeTrades.filter(
4432
+ (trade) => trade.entryTimestamp >= context.window.start && trade.entryTimestamp <= context.window.end && matchesSelection(context, trade)
4433
+ ),
4434
+ runtimeSignals: allRuntimeSignals.filter(
4435
+ (signal) => signal.timestamp >= context.window.start && signal.timestamp <= context.window.end && matchesSelection(context, signal)
4436
+ ),
4437
+ runtimeSignalEvaluations: allRuntimeSignalEvaluations.filter(
4438
+ (evaluation) => evaluation.timestamp >= context.window.start && evaluation.timestamp <= context.window.end && matchesSelection(context, evaluation)
4439
+ )
4440
+ };
4441
+ };
4442
+
4443
+ // src/lib/paritySummary.ts
4444
+ var summarizeTradeParityByStrategy = ({
4445
+ runtimeEntries,
4446
+ runtimeDuplicateEntries,
4447
+ backtestEntries,
4448
+ matchedEntries,
4449
+ runtimeOnlyEntries,
4450
+ backtestOnlyEntries
4451
+ }) => {
4452
+ const rows = /* @__PURE__ */ new Map();
4453
+ const ensureRow = (strategyName) => {
4454
+ const row = rows.get(strategyName) ?? {
4455
+ runtime: 0,
4456
+ runtimeDuplicates: 0,
4457
+ backtest: 0,
4458
+ matched: 0,
4459
+ runtimeOnly: 0,
4460
+ backtestOnly: 0
4461
+ };
4462
+ rows.set(strategyName, row);
4463
+ return row;
4464
+ };
4465
+ for (const entry of runtimeEntries) {
4466
+ ensureRow(entry.strategy).runtime += 1;
4467
+ }
4468
+ for (const entry of runtimeDuplicateEntries) {
4469
+ ensureRow(entry.strategy).runtimeDuplicates += 1;
4470
+ }
4471
+ for (const entry of backtestEntries) {
4472
+ ensureRow(entry.strategy).backtest += 1;
4473
+ }
4474
+ for (const entry of matchedEntries) {
4475
+ ensureRow(entry.runtime.strategy).matched += 1;
4476
+ }
4477
+ for (const entry of runtimeOnlyEntries) {
4478
+ ensureRow(entry.strategy).runtimeOnly += 1;
4479
+ }
4480
+ for (const entry of backtestOnlyEntries) {
4481
+ ensureRow(entry.strategy).backtestOnly += 1;
4482
+ }
4483
+ return [...rows.entries()].sort(
4484
+ ([left], [right]) => left.localeCompare(right)
4485
+ );
4486
+ };
4487
+
4463
4488
  // src/lib/runtimeParity/classification.ts
4464
4489
  var BACKTEST_ONLY_CLASSIFICATIONS = [
4465
4490
  "gated_out",
@@ -4789,250 +4814,117 @@ var summarizeRuntimeOnlyClassifications = (classifiedEntries) => {
4789
4814
  return nonZero.join(", ");
4790
4815
  };
4791
4816
 
4792
- // src/lib/runtimeModeConfig.ts
4793
- var hasRuntimeEntryGateEnabled = (strategyConfig) => strategyConfig.AI_ENABLED === true || strategyConfig.ML_ENABLED === true;
4794
- var resolveReplayStrategyEnv = ({
4795
- strategyConfig,
4796
- forceRuntimeGates = false
4797
- }) => forceRuntimeGates || hasRuntimeEntryGateEnabled(strategyConfig) ? "PARITY" : "BACKTEST";
4798
- var buildRuntimeModeStrategyConfig = ({
4799
- strategyConfig,
4800
- env,
4801
- interval: interval2,
4802
- makeOrders,
4803
- recordRuntimeTrades,
4804
- aiReplayAnalyses
4805
- }) => ({
4806
- ...strategyConfig,
4807
- ENV: env,
4808
- MAKE_ORDERS: makeOrders,
4809
- INTERVAL: strategyConfig.INTERVAL ?? interval2,
4810
- ...typeof recordRuntimeTrades === "boolean" ? { RECORD_RUNTIME_TRADES: recordRuntimeTrades } : {},
4811
- ...aiReplayAnalyses?.length ? { AI_REPLAY_ANALYSES: aiReplayAnalyses } : {}
4812
- });
4813
-
4814
- // src/scripts/runtimeParity.ts
4815
- import_args.default.option(["u", "user"], "Use user config", "root");
4816
- import_args.default.option(
4817
- ["o", "connector"],
4818
- "Connector provider or name for parity replay (e.g. bybit, binance, coinbase, custom)",
4819
- "bybit"
4820
- );
4821
- import_args.default.option(["d", "days"], "Replay window in days", 3);
4822
- import_args.default.option(
4823
- ["b", "startTime"],
4824
- "Explicit replay start timestamp (ms or seconds)"
4825
- );
4826
- import_args.default.option(["e", "endTime"], "Explicit replay end timestamp (ms or seconds)");
4827
- import_args.default.option(["s", "strategy"], "Only compare one strategy");
4828
- import_args.default.option(
4829
- ["t", "tickers"],
4830
- "Replay comma-separated symbols for all configured strategies"
4831
- );
4832
- import_args.default.option(
4833
- ["C", "cacheOnly"],
4834
- "Do not refresh market history before replay",
4835
- false
4836
- );
4837
- import_args.default.option(
4838
- ["a", "toleranceBars"],
4839
- "Allowed entry timestamp drift in bars when matching runtime vs backtest",
4840
- 1
4841
- );
4842
- import_args.default.option(
4843
- "fullUniverse",
4844
- "Replay every configured strategy across the full connector universe. By default parity replays only runtime-traded and strategy-results symbols unless --tickers is provided.",
4845
- false
4846
- );
4847
- import_args.default.option(
4848
- "runtimeGates",
4849
- "Force runtime AI/ML gates for all replay targets. Runtime-gated configs are replayed with gates by default.",
4850
- false
4851
- );
4852
- import_args.default.option(["N", "notify"], "Send parity summary to Telegram", false);
4853
- import_args.default.option(["D", "details"], "Print unmatched entry details (capped)", false);
4854
- process.argv = normalizeCliArgv(process.argv, {
4855
- "-C": "--cacheOnly",
4856
- "-D": "--details",
4857
- "-E": "--endTime",
4858
- "-N": "--notify",
4859
- "-S": "--startTime",
4860
- "-T": "--toleranceBars"
4861
- });
4862
- var flags = import_args.default.parse(process.argv);
4863
- var interval = "15";
4864
- var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
4865
- var DEFAULT_LOOKBACK_DAYS = 3;
4866
- var DETAIL_LIMIT = 10;
4867
- var TELEGRAM_DETAIL_LIMIT = 5;
4868
- var SUMMARY_TIMEZONE = "Europe/Moscow";
4869
- var SUMMARY_TIMEZONE_LABEL = "MSK";
4870
- var DEFAULT_REPLAY_ENV = flags.runtimeGates ? "PARITY" : "BACKTEST";
4871
- var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4872
- var formatMskDateTime = (timestamp) => new Intl.DateTimeFormat("ru-RU", {
4873
- timeZone: SUMMARY_TIMEZONE,
4874
- year: "numeric",
4875
- month: "2-digit",
4876
- day: "2-digit",
4877
- hour: "2-digit",
4878
- minute: "2-digit",
4879
- hour12: false
4880
- }).format(new Date(timestamp));
4881
- var resolveParityConnectorName = async (value) => {
4882
- const connectorName = await (0, import_connectors5.resolveConnectorName)(value, projectRoot);
4883
- if (connectorName) {
4884
- return connectorName;
4817
+ // src/lib/runtimeParity/analysis.ts
4818
+ var summarizeByStrategy = ({
4819
+ targets,
4820
+ successfulTargetKeys,
4821
+ replayErrors,
4822
+ runtimeEntries,
4823
+ runtimeDuplicateEntries,
4824
+ backtestEntries,
4825
+ matchedEntries,
4826
+ runtimeOnlyEntries,
4827
+ backtestOnlyEntries
4828
+ }) => {
4829
+ const rows = /* @__PURE__ */ new Map();
4830
+ const baseRows = summarizeTradeParityByStrategy({
4831
+ runtimeEntries,
4832
+ runtimeDuplicateEntries,
4833
+ backtestEntries,
4834
+ matchedEntries,
4835
+ runtimeOnlyEntries,
4836
+ backtestOnlyEntries
4837
+ });
4838
+ for (const [strategy, baseRow] of baseRows) {
4839
+ rows.set(strategy, {
4840
+ ...baseRow,
4841
+ targets: 0,
4842
+ compared: 0,
4843
+ errors: 0
4844
+ });
4885
4845
  }
4886
- import_logger.logger.warn(
4887
- 'Unknown connector "%s". Fallback to %s.',
4888
- String(value || "").trim() || String(value),
4889
- import_connectors5.DEFAULT_CONNECTOR_NAME
4846
+ const ensureRow = (strategy) => {
4847
+ const row = rows.get(strategy) ?? {
4848
+ runtime: 0,
4849
+ runtimeDuplicates: 0,
4850
+ backtest: 0,
4851
+ matched: 0,
4852
+ runtimeOnly: 0,
4853
+ backtestOnly: 0,
4854
+ targets: 0,
4855
+ compared: 0,
4856
+ errors: 0
4857
+ };
4858
+ rows.set(strategy, row);
4859
+ return row;
4860
+ };
4861
+ for (const target of targets) {
4862
+ const row = ensureRow(target.strategy);
4863
+ row.targets += 1;
4864
+ if (successfulTargetKeys.has(toTargetKey(target))) {
4865
+ row.compared += 1;
4866
+ }
4867
+ }
4868
+ for (const error of replayErrors) {
4869
+ ensureRow(error.strategy).errors += 1;
4870
+ }
4871
+ return [...rows.entries()].sort(
4872
+ ([left], [right]) => left.localeCompare(right)
4890
4873
  );
4891
- return import_connectors5.DEFAULT_CONNECTOR_NAME;
4892
4874
  };
4893
- var createConnector = async ({
4894
- userName,
4895
- connectorName
4896
- }) => {
4897
- const connectorFactory = await (0, import_connectors5.getConnectorCreatorByName)(
4898
- connectorName,
4899
- projectRoot
4875
+ var analyzeRuntimeParity = (context) => {
4876
+ const comparableRuntimeTrades = context.runtimeTrades.filter(
4877
+ (trade) => context.successfulTargetKeys.has(toTargetKey(trade))
4900
4878
  );
4901
- if (!connectorFactory) {
4902
- throw new Error(`Connector "${connectorName}" is not registered`);
4903
- }
4904
- return await connectorFactory({ userName });
4905
- };
4906
- var buildReplayAiAnalyses = ({
4907
- runtimeTrades,
4908
- runtimeSignalEvaluations,
4909
- toleranceMs
4910
- }) => {
4911
- const snapshots = [
4912
- ...runtimeTrades.filter(
4913
- (trade) => trade.aiAnalysis && typeof trade.entryTimestamp === "number" && Number.isFinite(trade.entryTimestamp)
4914
- ).map((trade) => ({
4915
- strategy: trade.strategy,
4916
- symbol: trade.symbol,
4917
- direction: trade.direction,
4918
- timestamp: trade.entryTimestamp,
4919
- toleranceMs,
4920
- analysis: trade.aiAnalysis
4921
- })),
4922
- ...runtimeSignalEvaluations.filter(
4923
- (evaluation) => evaluation.aiAnalysis && evaluation.direction && typeof evaluation.timestamp === "number" && Number.isFinite(evaluation.timestamp)
4924
- ).map((evaluation) => ({
4925
- strategy: evaluation.strategy,
4926
- symbol: evaluation.symbol,
4927
- direction: evaluation.direction,
4928
- timestamp: evaluation.timestamp,
4929
- toleranceMs,
4930
- analysis: evaluation.aiAnalysis
4931
- }))
4932
- ];
4933
- const seen = /* @__PURE__ */ new Set();
4934
- return snapshots.filter((snapshot) => {
4935
- const key = `${snapshot.strategy}:${snapshot.symbol}:${snapshot.direction}:${snapshot.timestamp}`;
4936
- if (seen.has(key)) {
4937
- return false;
4938
- }
4939
- seen.add(key);
4940
- return true;
4941
- });
4942
- };
4943
- var formatReplayEnvSummary = (configs) => {
4944
- let backtest = 0;
4945
- let parity = 0;
4946
- for (const config of configs) {
4947
- if (config.ENV === "PARITY") {
4948
- parity += 1;
4949
- } else {
4950
- backtest += 1;
4951
- }
4952
- }
4953
- if (parity > 0 && backtest > 0) {
4954
- return `MIXED (PARITY=${parity}, BACKTEST=${backtest})`;
4955
- }
4956
- if (parity > 0) {
4957
- return "PARITY";
4958
- }
4959
- return "BACKTEST";
4960
- };
4961
- var buildReplayConfig = async ({
4962
- userName,
4963
- strategy,
4964
- symbol,
4965
- replayInputsIndex,
4966
- toleranceMs
4967
- }) => {
4968
- const [userConfig, strategyResults] = await Promise.all([
4969
- (0, import_redis3.getData)(import_redis3.redisKeys.strategyConfig(userName, strategy), {}),
4970
- (0, import_redis3.getData)(import_redis3.redisKeys.strategyResults(userName, strategy), {})
4971
- ]);
4972
- const typedResults = strategyResults ?? {};
4973
- const symbolResult = typedResults?.[symbol];
4974
- const symbolConfig = symbolResult && symbolResult.config && typeof symbolResult.config === "object" ? symbolResult.config : {};
4975
- const replayInputs = replayInputsIndex.get(toTargetKey({ strategy, symbol }));
4976
- const aiReplayAnalyses = buildReplayAiAnalyses({
4977
- runtimeTrades: replayInputs?.runtimeTrades ?? [],
4978
- runtimeSignalEvaluations: replayInputs?.runtimeSignalEvaluations ?? [],
4979
- strategy,
4980
- symbol,
4981
- toleranceMs
4879
+ const rawRuntimeEntries = extractRuntimeParityEntries(
4880
+ comparableRuntimeTrades
4881
+ );
4882
+ const runtimeDedupe = dedupeRuntimeParityEntries(rawRuntimeEntries);
4883
+ const runtimeEntries = runtimeDedupe.entries;
4884
+ const comparison = compareTradeParityEntries({
4885
+ runtimeEntries,
4886
+ backtestEntries: context.backtestEntries,
4887
+ toleranceMs: context.toleranceMs
4982
4888
  });
4983
- const strategyConfig = {
4984
- ...userConfig,
4985
- ...symbolConfig
4986
- };
4987
- return buildRuntimeModeStrategyConfig({
4988
- strategyConfig,
4989
- env: resolveReplayStrategyEnv({
4990
- strategyConfig,
4991
- forceRuntimeGates: Boolean(flags.runtimeGates)
4992
- }),
4993
- interval,
4994
- makeOrders: true,
4995
- recordRuntimeTrades: false,
4996
- aiReplayAnalyses
4889
+ const classifiedBacktestOnly = classifyBacktestOnlyEntries({
4890
+ entries: comparison.backtestOnly,
4891
+ runtimeSignals: context.runtimeSignals,
4892
+ runtimeSignalEvaluations: context.runtimeSignalEvaluations,
4893
+ toleranceMs: context.toleranceMs
4997
4894
  });
4998
- };
4999
- var warmReplayHistory = async ({
5000
- userName,
5001
- connectorName,
5002
- targets,
5003
- preloadStart,
5004
- preloadEnd
5005
- }) => {
5006
- const connector = await createConnector({ userName, connectorName });
5007
- const symbols = [...new Set(targets.map((target) => target.symbol))];
5008
- const warmStartedAt = Date.now();
5009
- console.log(
5010
- import_chalk6.default.gray(
5011
- `Warm history: ${symbols.length} symbol(s) on ${connectorName}, preload ${(0, import_time3.formatUnix)(preloadStart)} -> ${(0, import_time3.formatUnix)(preloadEnd)}`
5012
- )
5013
- );
5014
- const btcReferences = await loadBtcReferenceConnectors({
5015
- connectorName,
5016
- marketConnector: connector,
5017
- userName,
5018
- projectRoot,
5019
- shouldUseDedicatedReferences: true,
5020
- warn: (message) => import_logger.logger.warn(message)
4895
+ const classifiedRuntimeOnly = classifyRuntimeOnlyEntries({
4896
+ entries: comparison.runtimeOnly,
4897
+ replaySignalEvaluations: context.replaySignalEvaluations,
4898
+ backtestEntries: context.backtestEntries,
4899
+ toleranceMs: context.toleranceMs
5021
4900
  });
5022
- await updateMarketHistoryWithBtcReferences({
5023
- marketConnector: connector,
5024
- connectorName,
5025
- btcReferences,
5026
- interval,
5027
- symbols,
5028
- preloadStart,
5029
- preloadEnd,
5030
- log: (message) => console.log(import_chalk6.default.gray(message))
4901
+ const matchedSummary = summarizeMatchedParity(comparison.matched);
4902
+ const strategyRows = summarizeByStrategy({
4903
+ targets: context.replayTargets,
4904
+ successfulTargetKeys: context.successfulTargetKeys,
4905
+ replayErrors: context.replayErrors,
4906
+ runtimeEntries,
4907
+ runtimeDuplicateEntries: runtimeDedupe.duplicateEntries,
4908
+ backtestEntries: context.backtestEntries,
4909
+ matchedEntries: comparison.matched,
4910
+ runtimeOnlyEntries: comparison.runtimeOnly,
4911
+ backtestOnlyEntries: comparison.backtestOnly
5031
4912
  });
5032
- console.log(
5033
- import_chalk6.default.gray(`Warm history: done in ${formatDuration(warmStartedAt)}`)
5034
- );
4913
+ return {
4914
+ comparableRuntimeTrades,
4915
+ rawRuntimeEntries,
4916
+ runtimeDedupe,
4917
+ runtimeEntries,
4918
+ comparison,
4919
+ classifiedBacktestOnly,
4920
+ classifiedRuntimeOnly,
4921
+ matchedSummary,
4922
+ strategyRows
4923
+ };
5035
4924
  };
4925
+
4926
+ // src/lib/runtimeParity/reportingShared.ts
4927
+ var import_time3 = require("@tradejs/core/time");
5036
4928
  var formatPercent = (value) => value == null || !Number.isFinite(value) ? "n/a" : `${value.toFixed(2)}%`;
5037
4929
  var formatMinutes2 = (value) => value == null || !Number.isFinite(value) ? "n/a" : `${(value / 6e4).toFixed(2)}m`;
5038
4930
  var formatEntryLabel = (entry) => `${entry.strategy} ${entry.symbol} ${entry.direction} ${(0, import_time3.formatUnix)(entry.timestamp)}`;
@@ -5052,16 +4944,11 @@ var formatSourceCountsSummary = (sourceCounts) => {
5052
4944
  };
5053
4945
  var normalizeSummaryText = (value, maxLength = 160) => {
5054
4946
  const normalized = value.replace(/\s+/g, " ").trim();
5055
- if (normalized.length <= maxLength) {
5056
- return normalized;
5057
- }
5058
- return `${normalized.slice(0, maxLength - 3)}...`;
4947
+ return normalized.length <= maxLength ? normalized : `${normalized.slice(0, maxLength - 3)}...`;
5059
4948
  };
5060
4949
  var pickFirstText = (...values) => {
5061
4950
  for (const value of values) {
5062
- if (typeof value === "string" && value.trim()) {
5063
- return value.trim();
5064
- }
4951
+ if (typeof value === "string" && value.trim()) return value.trim();
5065
4952
  }
5066
4953
  return null;
5067
4954
  };
@@ -5081,9 +4968,7 @@ var buildMismatchSummaryRows = ({
5081
4968
  item.nearestBacktestEntry?.id
5082
4969
  );
5083
4970
  const refs = [`signalId=${signalId ?? "n/a"}`];
5084
- if (orderId && orderId !== signalId) {
5085
- refs.push(`orderId=${orderId}`);
5086
- }
4971
+ if (orderId && orderId !== signalId) refs.push(`orderId=${orderId}`);
5087
4972
  if (item.evaluation?.evaluationId) {
5088
4973
  refs.push(`evaluationId=${item.evaluation.evaluationId}`);
5089
4974
  }
@@ -5145,20 +5030,16 @@ var buildMismatchSummaryRows = ({
5145
5030
  };
5146
5031
  var buildStrategyIssueRows = (strategyRows) => strategyRows.map(([strategy, row]) => {
5147
5032
  const issues = [];
5148
- if (row.runtimeOnly) {
5149
- issues.push(`runtimeOnly=${row.runtimeOnly}`);
5150
- }
5151
- if (row.backtestOnly) {
5152
- issues.push(`backtestOnly=${row.backtestOnly}`);
5153
- }
5033
+ if (row.runtimeOnly) issues.push(`runtimeOnly=${row.runtimeOnly}`);
5034
+ if (row.backtestOnly) issues.push(`backtestOnly=${row.backtestOnly}`);
5154
5035
  if (row.runtimeDuplicates) {
5155
5036
  issues.push(`runtimeDuplicates=${row.runtimeDuplicates}`);
5156
5037
  }
5157
- if (row.errors) {
5158
- issues.push(`errors=${row.errors}`);
5159
- }
5038
+ if (row.errors) issues.push(`errors=${row.errors}`);
5160
5039
  return issues.length ? `- ${strategy}: ${issues.join(", ")}` : null;
5161
5040
  }).filter((line) => Boolean(line));
5041
+
5042
+ // src/lib/runtimeParity/reportingJson.ts
5162
5043
  var toSerializableTradeParityEntry = (entry) => entry ? {
5163
5044
  id: entry.id,
5164
5045
  source: entry.source,
@@ -5301,34 +5182,12 @@ var getBacktestOnlyRecommendedChecks = (classification) => {
5301
5182
  ];
5302
5183
  }
5303
5184
  };
5304
- var buildRuntimeParityMismatchAttachment = ({
5305
- window,
5306
- connectorName,
5307
- replayEnv,
5308
- toleranceBars,
5309
- toleranceMs,
5310
- replayTargetsCount,
5311
- comparedTargetsCount,
5312
- replayErrors,
5313
- sourceCounts,
5314
- rawRuntimeEntriesCount,
5315
- runtimeEntriesCount,
5316
- runtimeDuplicateEntriesCount,
5317
- backtestEntriesCount,
5318
- matchedCount,
5319
- runtimeOnlyCount,
5320
- backtestOnlyCount,
5321
- matchedSummary,
5322
- classifiedRuntimeOnly,
5323
- classifiedBacktestOnly,
5324
- runtimeSignalEvaluationsCount,
5325
- strategyRows
5326
- }) => {
5327
- if (!classifiedRuntimeOnly.length && !classifiedBacktestOnly.length) {
5185
+ var buildRuntimeParityMismatchAttachment = (context) => {
5186
+ if (!context.classifiedRuntimeOnly.length && !context.classifiedBacktestOnly.length) {
5328
5187
  return null;
5329
5188
  }
5330
5189
  const cases = [
5331
- ...classifiedRuntimeOnly.map((item) => ({
5190
+ ...context.classifiedRuntimeOnly.map((item) => ({
5332
5191
  kind: "runtimeOnly",
5333
5192
  strategy: item.entry.strategy,
5334
5193
  symbol: item.entry.symbol,
@@ -5367,7 +5226,7 @@ var buildRuntimeParityMismatchAttachment = ({
5367
5226
  )
5368
5227
  }
5369
5228
  })),
5370
- ...classifiedBacktestOnly.map((item) => ({
5229
+ ...context.classifiedBacktestOnly.map((item) => ({
5371
5230
  kind: "backtestOnly",
5372
5231
  strategy: item.entry.strategy,
5373
5232
  symbol: item.entry.symbol,
@@ -5413,40 +5272,37 @@ var buildRuntimeParityMismatchAttachment = ({
5413
5272
  generatedAt: Date.now(),
5414
5273
  codexQuestion: "For each mismatch case, explain why runtime and replay/backtest diverged. Use why.classification first, then confirm with decisionTrace, timing, and artifacts.",
5415
5274
  window: {
5416
- start: window.start,
5417
- end: window.end,
5418
- source: window.source
5419
- },
5420
- connectorName,
5421
- replayEnv,
5422
- tolerance: {
5423
- bars: toleranceBars,
5424
- ms: toleranceMs
5275
+ start: context.window.start,
5276
+ end: context.window.end,
5277
+ source: context.window.source
5425
5278
  },
5279
+ connectorName: context.connectorName,
5280
+ replayEnv: context.replayEnv,
5281
+ tolerance: { bars: context.toleranceBars, ms: context.toleranceMs },
5426
5282
  summary: {
5427
- replayTargets: replayTargetsCount,
5428
- comparedTargets: comparedTargetsCount,
5429
- replayErrors: replayErrors.length,
5430
- sourceCounts,
5431
- runtimeEntriesRaw: rawRuntimeEntriesCount,
5432
- runtimeEntries: runtimeEntriesCount,
5433
- runtimeDuplicateEntries: runtimeDuplicateEntriesCount,
5434
- backtestEntries: backtestEntriesCount,
5435
- matchedEntries: matchedCount,
5436
- runtimeOnlyEntries: runtimeOnlyCount,
5437
- backtestOnlyEntries: backtestOnlyCount,
5438
- runtimeSignalEvaluations: runtimeSignalEvaluationsCount,
5283
+ replayTargets: context.replayTargetsCount,
5284
+ comparedTargets: context.comparedTargetsCount,
5285
+ replayErrors: context.replayErrors.length,
5286
+ sourceCounts: context.sourceCounts,
5287
+ runtimeEntriesRaw: context.rawRuntimeEntriesCount,
5288
+ runtimeEntries: context.runtimeEntriesCount,
5289
+ runtimeDuplicateEntries: context.runtimeDuplicateEntriesCount,
5290
+ backtestEntries: context.backtestEntriesCount,
5291
+ matchedEntries: context.matchedCount,
5292
+ runtimeOnlyEntries: context.runtimeOnlyCount,
5293
+ backtestOnlyEntries: context.backtestOnlyCount,
5294
+ runtimeSignalEvaluations: context.runtimeSignalEvaluationsCount,
5439
5295
  matchedDeltas: {
5440
- priceAvgPct: matchedSummary.avgPriceDeltaPct,
5441
- priceMaxPct: matchedSummary.maxPriceDeltaPct,
5442
- timeAvgMs: matchedSummary.avgTimestampDiffMs,
5443
- timeMaxMs: matchedSummary.maxTimestampDiffMs
5296
+ priceAvgPct: context.matchedSummary.avgPriceDeltaPct,
5297
+ priceMaxPct: context.matchedSummary.maxPriceDeltaPct,
5298
+ timeAvgMs: context.matchedSummary.avgTimestampDiffMs,
5299
+ timeMaxMs: context.matchedSummary.maxTimestampDiffMs
5444
5300
  },
5445
- strategyIssues: buildStrategyIssueRows(strategyRows).map(
5301
+ strategyIssues: buildStrategyIssueRows(context.strategyRows).map(
5446
5302
  (line) => line.slice(2)
5447
5303
  )
5448
5304
  },
5449
- replayErrors: replayErrors.map((error) => ({
5305
+ replayErrors: context.replayErrors.map((error) => ({
5450
5306
  strategy: error.strategy,
5451
5307
  symbol: error.symbol,
5452
5308
  sources: error.sources,
@@ -5454,7 +5310,7 @@ var buildRuntimeParityMismatchAttachment = ({
5454
5310
  })),
5455
5311
  cases,
5456
5312
  mismatches: {
5457
- runtimeOnly: classifiedRuntimeOnly.map((item) => ({
5313
+ runtimeOnly: context.classifiedRuntimeOnly.map((item) => ({
5458
5314
  classification: item.classification,
5459
5315
  reason: item.reason,
5460
5316
  runtimeEntry: toSerializableTradeParityEntry(item.entry),
@@ -5467,7 +5323,7 @@ var buildRuntimeParityMismatchAttachment = ({
5467
5323
  ),
5468
5324
  nearestBacktestDriftMs: item.nearestBacktestEntryTimestampDiffMs
5469
5325
  })),
5470
- backtestOnly: classifiedBacktestOnly.map((item) => ({
5326
+ backtestOnly: context.classifiedBacktestOnly.map((item) => ({
5471
5327
  classification: item.classification,
5472
5328
  reason: item.reason,
5473
5329
  backtestEntry: toSerializableTradeParityEntry(item.entry),
@@ -5481,15 +5337,93 @@ var buildRuntimeParityMismatchAttachment = ({
5481
5337
  }
5482
5338
  };
5483
5339
  return {
5484
- filename: `runtime-parity-mismatches-${connectorName}-${window.start}-${window.end}.json`,
5340
+ filename: `runtime-parity-mismatches-${context.connectorName}-${context.window.start}-${context.window.end}.json`,
5485
5341
  content: JSON.stringify(payload, null, 2),
5486
5342
  caption: "Runtime parity mismatch JSON"
5487
5343
  };
5488
5344
  };
5489
- var printRuntimeDuplicateDetails = (groups) => {
5490
- if (!groups.length) {
5491
- return;
5345
+
5346
+ // src/lib/runtimeParity/reportingTerminal.ts
5347
+ var import_chalk6 = __toESM(require("chalk"));
5348
+ var import_time4 = require("@tradejs/core/time");
5349
+ var DETAIL_LIMIT = 10;
5350
+ var buildRuntimeParityTerminalReport = (context) => {
5351
+ const mismatchRows = buildMismatchSummaryRows(context);
5352
+ const strategyIssueRows = buildStrategyIssueRows(context.strategyRows);
5353
+ const lines = [
5354
+ import_chalk6.default.cyan("TradeJS runtime parity"),
5355
+ `Window: ${(0, import_time4.formatUnix)(context.window.start)} -> ${(0, import_time4.formatUnix)(context.window.end)} (${context.window.source})`,
5356
+ `Connector: ${context.connectorName}`,
5357
+ `Replay env: ${context.replayEnv}${context.runtimeGatesEnabled ? " (runtime AI/ML gates enabled)" : " (core/backtest gates only)"}`,
5358
+ `Tolerance: ${context.toleranceBars} bar(s) / ${(context.toleranceMs / 6e4).toFixed(0)}m`,
5359
+ `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}`,
5360
+ `Sources: ${formatSourceCountsSummary(context.sourceCounts)}`,
5361
+ `Runtime entries: ${formatRuntimeEntriesSummary(context)}`,
5362
+ `Matched deltas: price avg/max=${formatPercent(context.matchedSummary.avgPriceDeltaPct)} / ${formatPercent(context.matchedSummary.maxPriceDeltaPct)}, time avg/max=${formatMinutes2(context.matchedSummary.avgTimestampDiffMs)} / ${formatMinutes2(context.matchedSummary.maxTimestampDiffMs)}`
5363
+ ];
5364
+ if (context.runtimeDuplicateGroupsCount) {
5365
+ lines.push(
5366
+ `Runtime duplicate groups: ${context.runtimeDuplicateGroupsCount}, duplicate entries: ${context.runtimeDuplicateEntriesCount}`
5367
+ );
5368
+ }
5369
+ if (context.classifiedRuntimeOnly.length) {
5370
+ lines.push(
5371
+ `Runtime only classifications: ${summarizeRuntimeOnlyClassifications(context.classifiedRuntimeOnly)}`
5372
+ );
5373
+ }
5374
+ if (context.classifiedBacktestOnly.length) {
5375
+ lines.push(
5376
+ `Backtest only classifications: ${summarizeBacktestOnlyClassifications(context.classifiedBacktestOnly)}`
5377
+ );
5378
+ }
5379
+ if (context.runtimeSignalEvaluationsCount) {
5380
+ lines.push(`Runtime evaluations: ${context.runtimeSignalEvaluationsCount}`);
5381
+ }
5382
+ if (mismatchRows.length) {
5383
+ lines.push("", import_chalk6.default.yellow("Signal mismatches"));
5384
+ for (const item of mismatchRows.slice(0, context.detailLimit)) {
5385
+ lines.push(`- ${item.line}`);
5386
+ }
5387
+ if (mismatchRows.length > context.detailLimit) {
5388
+ lines.push(`- ... ${mismatchRows.length - context.detailLimit} more`);
5389
+ }
5390
+ }
5391
+ lines.push("");
5392
+ if (strategyIssueRows.length) {
5393
+ lines.push(import_chalk6.default.cyan("Strategy issues"), ...strategyIssueRows);
5394
+ } else if (context.strategyRows.length) {
5395
+ lines.push(
5396
+ `Strategies: clean ${context.strategyRows.length}/${context.strategyRows.length}`
5397
+ );
5398
+ }
5399
+ if (context.runtimeGateWarningCounts.size && !context.runtimeGatesRequested) {
5400
+ lines.push("", import_chalk6.default.yellow("Warnings"));
5401
+ for (const [strategy, count] of [
5402
+ ...context.runtimeGateWarningCounts.entries()
5403
+ ].sort(([left], [right]) => left.localeCompare(right))) {
5404
+ lines.push(
5405
+ `- ${strategy} uses AI/ML runtime gates on ${count} replay target(s); BACKTEST replay covers core execution, not live gating.`
5406
+ );
5407
+ }
5408
+ }
5409
+ if (context.replayErrors.length) {
5410
+ lines.push("", import_chalk6.default.red("Replay errors"));
5411
+ for (const error of context.replayErrors.slice(0, context.detailLimit)) {
5412
+ lines.push(`- ${error.strategy} ${error.symbol}: ${error.message}`);
5413
+ }
5414
+ if (context.replayErrors.length > context.detailLimit) {
5415
+ lines.push(
5416
+ `- ... ${context.replayErrors.length - context.detailLimit} more`
5417
+ );
5418
+ }
5492
5419
  }
5420
+ return lines;
5421
+ };
5422
+ var writeRuntimeParityTerminalReport = (lines) => {
5423
+ for (const line of lines) console.log(line);
5424
+ };
5425
+ var printRuntimeDuplicateDetails = (groups) => {
5426
+ if (!groups.length) return;
5493
5427
  console.log("");
5494
5428
  console.log(import_chalk6.default.yellow("Runtime duplicates"));
5495
5429
  for (const group of groups.slice(0, DETAIL_LIMIT)) {
@@ -5503,9 +5437,7 @@ var printRuntimeDuplicateDetails = (groups) => {
5503
5437
  }
5504
5438
  };
5505
5439
  var printClassifiedBacktestOnlyDetails = (classifiedEntries) => {
5506
- if (!classifiedEntries.length) {
5507
- return;
5508
- }
5440
+ if (!classifiedEntries.length) return;
5509
5441
  console.log("");
5510
5442
  console.log(import_chalk6.default.yellow("Backtest only"));
5511
5443
  for (const item of classifiedEntries.slice(0, DETAIL_LIMIT)) {
@@ -5520,9 +5452,7 @@ var printClassifiedBacktestOnlyDetails = (classifiedEntries) => {
5520
5452
  }
5521
5453
  };
5522
5454
  var printClassifiedRuntimeOnlyDetails = (classifiedEntries) => {
5523
- if (!classifiedEntries.length) {
5524
- return;
5525
- }
5455
+ if (!classifiedEntries.length) return;
5526
5456
  console.log("");
5527
5457
  console.log(import_chalk6.default.yellow("Runtime only"));
5528
5458
  for (const item of classifiedEntries.slice(0, DETAIL_LIMIT)) {
@@ -5536,149 +5466,78 @@ var printClassifiedRuntimeOnlyDetails = (classifiedEntries) => {
5536
5466
  console.log(`- ... ${classifiedEntries.length - DETAIL_LIMIT} more`);
5537
5467
  }
5538
5468
  };
5539
- var summarizeByStrategy = ({
5540
- targets,
5541
- successfulTargetKeys,
5542
- replayErrors,
5543
- runtimeEntries,
5544
- runtimeDuplicateEntries,
5545
- backtestEntries,
5546
- matchedEntries,
5547
- runtimeOnlyEntries,
5548
- backtestOnlyEntries
5549
- }) => {
5550
- const rows = /* @__PURE__ */ new Map();
5551
- const baseRows = summarizeTradeParityByStrategy({
5552
- runtimeEntries,
5553
- runtimeDuplicateEntries,
5554
- backtestEntries,
5555
- matchedEntries,
5556
- runtimeOnlyEntries,
5557
- backtestOnlyEntries
5558
- });
5559
- for (const [strategy, baseRow] of baseRows) {
5560
- rows.set(strategy, {
5561
- ...baseRow,
5562
- targets: 0,
5563
- compared: 0,
5564
- errors: 0
5565
- });
5566
- }
5567
- const ensureRow = (strategy) => {
5568
- const row = rows.get(strategy) ?? {
5569
- runtime: 0,
5570
- runtimeDuplicates: 0,
5571
- backtest: 0,
5572
- matched: 0,
5573
- runtimeOnly: 0,
5574
- backtestOnly: 0,
5575
- targets: 0,
5576
- compared: 0,
5577
- errors: 0
5578
- };
5579
- rows.set(strategy, row);
5580
- return row;
5581
- };
5582
- for (const target of targets) {
5583
- const row = ensureRow(target.strategy);
5584
- row.targets += 1;
5585
- if (successfulTargetKeys.has(toTargetKey(target))) {
5586
- row.compared += 1;
5587
- }
5588
- }
5589
- for (const error of replayErrors) {
5590
- ensureRow(error.strategy).errors += 1;
5591
- }
5592
- return [...rows.entries()].sort(
5593
- ([left], [right]) => left.localeCompare(right)
5594
- );
5595
- };
5596
- var buildRuntimeParityMessage = ({
5597
- window,
5598
- connectorName,
5599
- replayEnv,
5600
- runtimeGatesEnabled,
5601
- toleranceBars,
5602
- toleranceMs,
5603
- replayTargetsCount,
5604
- comparedTargetsCount,
5605
- replayErrors,
5606
- sourceCounts,
5607
- rawRuntimeEntriesCount,
5608
- runtimeEntriesCount,
5609
- runtimeDuplicateEntriesCount,
5610
- backtestEntriesCount,
5611
- matchedCount: _matchedCount,
5612
- runtimeOnlyCount,
5613
- backtestOnlyCount,
5614
- matchedSummary,
5615
- classifiedRuntimeOnly,
5616
- classifiedBacktestOnly,
5617
- runtimeSignalEvaluationsCount,
5618
- strategyRows,
5619
- runtimeGateWarningCounts
5469
+
5470
+ // src/lib/runtimeParity/reportingTelegram.ts
5471
+ var TELEGRAM_DETAIL_LIMIT = 5;
5472
+ var SUMMARY_TIMEZONE = "Europe/Moscow";
5473
+ var SUMMARY_TIMEZONE_LABEL = "MSK";
5474
+ var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5475
+ var formatMskDateTime = (timestamp) => new Intl.DateTimeFormat("ru-RU", {
5476
+ timeZone: SUMMARY_TIMEZONE,
5477
+ year: "numeric",
5478
+ month: "2-digit",
5479
+ day: "2-digit",
5480
+ hour: "2-digit",
5481
+ minute: "2-digit",
5482
+ hour12: false
5483
+ }).format(new Date(timestamp));
5484
+ var buildRuntimeParityMessage = ({
5485
+ runtimeGatesEnabled,
5486
+ runtimeGateWarningCounts,
5487
+ ...context
5620
5488
  }) => {
5621
5489
  const lines = [];
5622
5490
  lines.push("\u{1F9EA} <b>TradeJS runtime parity</b>");
5623
5491
  lines.push("");
5624
5492
  lines.push("\u{1F552} <b>Window</b>");
5625
5493
  lines.push(
5626
- `<b>${escapeHtml(formatMskDateTime(window.start))} - ${escapeHtml(formatMskDateTime(window.end))} ${SUMMARY_TIMEZONE_LABEL}</b>`
5494
+ `<b>${escapeHtml(formatMskDateTime(context.window.start))} - ${escapeHtml(formatMskDateTime(context.window.end))} ${SUMMARY_TIMEZONE_LABEL}</b>`
5627
5495
  );
5628
5496
  lines.push("");
5629
- lines.push(`\u{1F50C} Connector: <b>${escapeHtml(connectorName)}</b>`);
5630
- lines.push(`\u{1F9EC} Replay env: <b>${escapeHtml(replayEnv)}</b>`);
5497
+ lines.push(`\u{1F50C} Connector: <b>${escapeHtml(context.connectorName)}</b>`);
5498
+ lines.push(`\u{1F9EC} Replay env: <b>${escapeHtml(context.replayEnv)}</b>`);
5631
5499
  lines.push(
5632
- `\u{1F3AF} Tolerance: <b>${toleranceBars} bar(s) / ${(toleranceMs / 6e4).toFixed(0)}m</b>`
5500
+ `\u{1F3AF} Tolerance: <b>${context.toleranceBars} bar(s) / ${(context.toleranceMs / 6e4).toFixed(0)}m</b>`
5633
5501
  );
5634
5502
  lines.push("");
5635
5503
  lines.push("\u{1F4CC} <b>Overview</b>");
5636
5504
  lines.push(
5637
- `\u2022 Targets: <b>${replayTargetsCount}</b> / compared <b>${comparedTargetsCount}</b> / errors <b>${replayErrors.length}</b>`
5505
+ `\u2022 Targets: <b>${context.replayTargetsCount}</b> / compared <b>${context.comparedTargetsCount}</b> / errors <b>${context.replayErrors.length}</b>`
5638
5506
  );
5639
5507
  lines.push(
5640
- `\u2022 Sources: <b>${escapeHtml(formatSourceCountsSummary(sourceCounts))}</b>`
5508
+ `\u2022 Sources: <b>${escapeHtml(formatSourceCountsSummary(context.sourceCounts))}</b>`
5641
5509
  );
5642
5510
  lines.push("");
5643
5511
  lines.push("\u{1F4C8} <b>Entries</b>");
5644
5512
  lines.push(
5645
- `\u2022 Runtime: <b>${escapeHtml(
5646
- formatRuntimeEntriesSummary({
5647
- rawRuntimeEntriesCount,
5648
- runtimeEntriesCount,
5649
- runtimeDuplicateEntriesCount
5650
- })
5651
- )}</b>`
5513
+ `\u2022 Runtime: <b>${escapeHtml(formatRuntimeEntriesSummary(context))}</b>`
5652
5514
  );
5653
- lines.push(`\u2022 Backtest: <b>${backtestEntriesCount}</b>`);
5515
+ lines.push(`\u2022 Backtest: <b>${context.backtestEntriesCount}</b>`);
5654
5516
  lines.push(
5655
- `\u2022 Runtime only: <b>${runtimeOnlyCount}</b> / Backtest only: <b>${backtestOnlyCount}</b>`
5517
+ `\u2022 Runtime only: <b>${context.runtimeOnlyCount}</b> / Backtest only: <b>${context.backtestOnlyCount}</b>`
5656
5518
  );
5657
5519
  lines.push(
5658
- `\u2022 Matched deltas: price avg/max=<b>${escapeHtml(formatPercent(matchedSummary.avgPriceDeltaPct))} / ${escapeHtml(formatPercent(matchedSummary.maxPriceDeltaPct))}</b>, time avg/max=<b>${escapeHtml(formatMinutes2(matchedSummary.avgTimestampDiffMs))} / ${escapeHtml(formatMinutes2(matchedSummary.maxTimestampDiffMs))}</b>`
5520
+ `\u2022 Matched deltas: price avg/max=<b>${escapeHtml(formatPercent(context.matchedSummary.avgPriceDeltaPct))} / ${escapeHtml(formatPercent(context.matchedSummary.maxPriceDeltaPct))}</b>, time avg/max=<b>${escapeHtml(formatMinutes2(context.matchedSummary.avgTimestampDiffMs))} / ${escapeHtml(formatMinutes2(context.matchedSummary.maxTimestampDiffMs))}</b>`
5659
5521
  );
5660
- if (classifiedRuntimeOnly.length) {
5522
+ if (context.classifiedRuntimeOnly.length) {
5661
5523
  lines.push(
5662
- `\u2022 Runtime-only classes: <code>${escapeHtml(summarizeRuntimeOnlyClassifications(classifiedRuntimeOnly))}</code>`
5524
+ `\u2022 Runtime-only classes: <code>${escapeHtml(summarizeRuntimeOnlyClassifications(context.classifiedRuntimeOnly))}</code>`
5663
5525
  );
5664
5526
  }
5665
- if (classifiedBacktestOnly.length) {
5527
+ if (context.classifiedBacktestOnly.length) {
5666
5528
  lines.push(
5667
- `\u2022 Backtest-only classes: <code>${escapeHtml(summarizeBacktestOnlyClassifications(classifiedBacktestOnly))}</code>`
5529
+ `\u2022 Backtest-only classes: <code>${escapeHtml(summarizeBacktestOnlyClassifications(context.classifiedBacktestOnly))}</code>`
5668
5530
  );
5669
5531
  }
5670
- if (runtimeSignalEvaluationsCount) {
5532
+ if (context.runtimeSignalEvaluationsCount) {
5671
5533
  lines.push(
5672
- `\u2022 Runtime evaluations: <b>${runtimeSignalEvaluationsCount}</b>`
5534
+ `\u2022 Runtime evaluations: <b>${context.runtimeSignalEvaluationsCount}</b>`
5673
5535
  );
5674
5536
  }
5675
- const mismatchRows = buildMismatchSummaryRows({
5676
- classifiedRuntimeOnly,
5677
- classifiedBacktestOnly
5678
- });
5537
+ const mismatchRows = buildMismatchSummaryRows(context);
5679
5538
  if (mismatchRows.length) {
5680
5539
  lines.push("");
5681
- lines.push(`\u{1F50E} <b>Mismatches</b>`);
5540
+ lines.push("\u{1F50E} <b>Mismatches</b>");
5682
5541
  for (const item of mismatchRows.slice(0, TELEGRAM_DETAIL_LIMIT)) {
5683
5542
  lines.push(`\u2022 <code>${escapeHtml(item.line)}</code>`);
5684
5543
  }
@@ -5688,17 +5547,17 @@ var buildRuntimeParityMessage = ({
5688
5547
  );
5689
5548
  }
5690
5549
  }
5691
- const strategyIssueRows = buildStrategyIssueRows(strategyRows);
5550
+ const strategyIssueRows = buildStrategyIssueRows(context.strategyRows);
5692
5551
  if (strategyIssueRows.length) {
5693
5552
  lines.push("");
5694
5553
  lines.push("\u{1F4CA} <b>Strategy issues</b>");
5695
5554
  for (const line of strategyIssueRows) {
5696
5555
  lines.push(`\u2022 ${escapeHtml(line.slice(2))}`);
5697
5556
  }
5698
- } else if (strategyRows.length) {
5557
+ } else if (context.strategyRows.length) {
5699
5558
  lines.push("");
5700
5559
  lines.push(
5701
- `\u{1F4CA} <b>Strategies</b>: clean <b>${strategyRows.length}</b> / total <b>${strategyRows.length}</b>`
5560
+ `\u{1F4CA} <b>Strategies</b>: clean <b>${context.strategyRows.length}</b> / total <b>${context.strategyRows.length}</b>`
5702
5561
  );
5703
5562
  }
5704
5563
  if (runtimeGateWarningCounts.size && !runtimeGatesEnabled) {
@@ -5712,17 +5571,17 @@ var buildRuntimeParityMessage = ({
5712
5571
  );
5713
5572
  }
5714
5573
  }
5715
- if (replayErrors.length) {
5574
+ if (context.replayErrors.length) {
5716
5575
  lines.push("");
5717
5576
  lines.push("\u274C <b>Replay errors</b>");
5718
- for (const error of replayErrors.slice(0, TELEGRAM_DETAIL_LIMIT)) {
5577
+ for (const error of context.replayErrors.slice(0, TELEGRAM_DETAIL_LIMIT)) {
5719
5578
  lines.push(
5720
5579
  `\u2022 <b>${escapeHtml(error.strategy)}</b> ${escapeHtml(error.symbol)}: <code>${escapeHtml(error.message)}</code>`
5721
5580
  );
5722
5581
  }
5723
- if (replayErrors.length > TELEGRAM_DETAIL_LIMIT) {
5582
+ if (context.replayErrors.length > TELEGRAM_DETAIL_LIMIT) {
5724
5583
  lines.push(
5725
- `... <b>${replayErrors.length - TELEGRAM_DETAIL_LIMIT}</b> more`
5584
+ `... <b>${context.replayErrors.length - TELEGRAM_DETAIL_LIMIT}</b> more`
5726
5585
  );
5727
5586
  }
5728
5587
  }
@@ -5732,7 +5591,6 @@ var buildRuntimeParityNoTargetsMessage = ({
5732
5591
  window,
5733
5592
  connectorName,
5734
5593
  replayEnv,
5735
- runtimeGatesEnabled: _runtimeGatesEnabled,
5736
5594
  userName
5737
5595
  }) => [
5738
5596
  "\u{1F9EA} <b>TradeJS runtime parity</b>",
@@ -5745,6 +5603,238 @@ var buildRuntimeParityNoTargetsMessage = ({
5745
5603
  "",
5746
5604
  `\u26A0\uFE0F No replay targets found for user <b>${escapeHtml(userName)}</b>.`
5747
5605
  ].join("\n");
5606
+
5607
+ // src/lib/runtimeModeConfig.ts
5608
+ var hasRuntimeEntryGateEnabled = (strategyConfig) => strategyConfig.AI_ENABLED === true || strategyConfig.ML_ENABLED === true;
5609
+ var resolveReplayStrategyEnv = ({
5610
+ strategyConfig,
5611
+ forceRuntimeGates = false
5612
+ }) => forceRuntimeGates || hasRuntimeEntryGateEnabled(strategyConfig) ? "PARITY" : "BACKTEST";
5613
+ var buildRuntimeModeStrategyConfig = ({
5614
+ strategyConfig,
5615
+ env,
5616
+ interval: interval2,
5617
+ makeOrders,
5618
+ recordRuntimeTrades,
5619
+ aiReplayAnalyses
5620
+ }) => ({
5621
+ ...strategyConfig,
5622
+ ENV: env,
5623
+ MAKE_ORDERS: makeOrders,
5624
+ INTERVAL: strategyConfig.INTERVAL ?? interval2,
5625
+ ...typeof recordRuntimeTrades === "boolean" ? { RECORD_RUNTIME_TRADES: recordRuntimeTrades } : {},
5626
+ ...aiReplayAnalyses?.length ? { AI_REPLAY_ANALYSES: aiReplayAnalyses } : {}
5627
+ });
5628
+
5629
+ // src/scripts/runtimeParity.ts
5630
+ import_args.default.option(["u", "user"], "Use user config", "root");
5631
+ import_args.default.option(
5632
+ ["o", "connector"],
5633
+ "Connector provider or name for parity replay (e.g. bybit, binance, coinbase, custom)",
5634
+ "bybit"
5635
+ );
5636
+ import_args.default.option(["d", "days"], "Replay window in days", 3);
5637
+ import_args.default.option(
5638
+ ["b", "startTime"],
5639
+ "Explicit replay start timestamp (ms or seconds)"
5640
+ );
5641
+ import_args.default.option(["e", "endTime"], "Explicit replay end timestamp (ms or seconds)");
5642
+ import_args.default.option(["s", "strategy"], "Only compare one strategy");
5643
+ import_args.default.option(
5644
+ ["t", "tickers"],
5645
+ "Replay comma-separated symbols for all configured strategies"
5646
+ );
5647
+ import_args.default.option(
5648
+ ["C", "cacheOnly"],
5649
+ "Do not refresh market history before replay",
5650
+ false
5651
+ );
5652
+ import_args.default.option(
5653
+ ["a", "toleranceBars"],
5654
+ "Allowed entry timestamp drift in bars when matching runtime vs backtest",
5655
+ 1
5656
+ );
5657
+ import_args.default.option(
5658
+ "fullUniverse",
5659
+ "Replay every configured strategy across the full connector universe. By default parity replays only runtime-traded and strategy-results symbols unless --tickers is provided.",
5660
+ false
5661
+ );
5662
+ import_args.default.option(
5663
+ "runtimeGates",
5664
+ "Force runtime AI/ML gates for all replay targets. Runtime-gated configs are replayed with gates by default.",
5665
+ false
5666
+ );
5667
+ import_args.default.option(["N", "notify"], "Send parity summary to Telegram", false);
5668
+ import_args.default.option(["D", "details"], "Print unmatched entry details (capped)", false);
5669
+ process.argv = normalizeCliArgv(process.argv, {
5670
+ "-C": "--cacheOnly",
5671
+ "-D": "--details",
5672
+ "-E": "--endTime",
5673
+ "-N": "--notify",
5674
+ "-S": "--startTime",
5675
+ "-T": "--toleranceBars"
5676
+ });
5677
+ var flags = import_args.default.parse(process.argv);
5678
+ var interval = "15";
5679
+ var projectRoot = String(process.env.PROJECT_CWD || process.cwd()).trim() || process.cwd();
5680
+ var DEFAULT_LOOKBACK_DAYS = 3;
5681
+ var DETAIL_LIMIT2 = 10;
5682
+ var DEFAULT_REPLAY_ENV = flags.runtimeGates ? "PARITY" : "BACKTEST";
5683
+ var resolveParityConnectorName = async (value) => {
5684
+ const connectorName = await (0, import_connectors5.resolveConnectorName)(value, projectRoot);
5685
+ if (connectorName) {
5686
+ return connectorName;
5687
+ }
5688
+ import_logger.logger.warn(
5689
+ 'Unknown connector "%s". Fallback to %s.',
5690
+ String(value || "").trim() || String(value),
5691
+ import_connectors5.DEFAULT_CONNECTOR_NAME
5692
+ );
5693
+ return import_connectors5.DEFAULT_CONNECTOR_NAME;
5694
+ };
5695
+ var createConnector = async ({
5696
+ userName,
5697
+ connectorName
5698
+ }) => {
5699
+ const connectorFactory = await (0, import_connectors5.getConnectorCreatorByName)(
5700
+ connectorName,
5701
+ projectRoot
5702
+ );
5703
+ if (!connectorFactory) {
5704
+ throw new Error(`Connector "${connectorName}" is not registered`);
5705
+ }
5706
+ return await connectorFactory({ userName });
5707
+ };
5708
+ var buildReplayAiAnalyses = ({
5709
+ runtimeTrades,
5710
+ runtimeSignalEvaluations,
5711
+ toleranceMs
5712
+ }) => {
5713
+ const snapshots = [
5714
+ ...runtimeTrades.filter(
5715
+ (trade) => trade.aiAnalysis && typeof trade.entryTimestamp === "number" && Number.isFinite(trade.entryTimestamp)
5716
+ ).map((trade) => ({
5717
+ strategy: trade.strategy,
5718
+ symbol: trade.symbol,
5719
+ direction: trade.direction,
5720
+ timestamp: trade.entryTimestamp,
5721
+ toleranceMs,
5722
+ analysis: trade.aiAnalysis
5723
+ })),
5724
+ ...runtimeSignalEvaluations.filter(
5725
+ (evaluation) => evaluation.aiAnalysis && evaluation.direction && typeof evaluation.timestamp === "number" && Number.isFinite(evaluation.timestamp)
5726
+ ).map((evaluation) => ({
5727
+ strategy: evaluation.strategy,
5728
+ symbol: evaluation.symbol,
5729
+ direction: evaluation.direction,
5730
+ timestamp: evaluation.timestamp,
5731
+ toleranceMs,
5732
+ analysis: evaluation.aiAnalysis
5733
+ }))
5734
+ ];
5735
+ const seen = /* @__PURE__ */ new Set();
5736
+ return snapshots.filter((snapshot) => {
5737
+ const key = `${snapshot.strategy}:${snapshot.symbol}:${snapshot.direction}:${snapshot.timestamp}`;
5738
+ if (seen.has(key)) {
5739
+ return false;
5740
+ }
5741
+ seen.add(key);
5742
+ return true;
5743
+ });
5744
+ };
5745
+ var formatReplayEnvSummary = (configs) => {
5746
+ let backtest = 0;
5747
+ let parity = 0;
5748
+ for (const config of configs) {
5749
+ if (config.ENV === "PARITY") {
5750
+ parity += 1;
5751
+ } else {
5752
+ backtest += 1;
5753
+ }
5754
+ }
5755
+ if (parity > 0 && backtest > 0) {
5756
+ return `MIXED (PARITY=${parity}, BACKTEST=${backtest})`;
5757
+ }
5758
+ if (parity > 0) {
5759
+ return "PARITY";
5760
+ }
5761
+ return "BACKTEST";
5762
+ };
5763
+ var buildReplayConfig = async ({
5764
+ userName,
5765
+ strategy,
5766
+ symbol,
5767
+ replayInputsIndex,
5768
+ toleranceMs
5769
+ }) => {
5770
+ const [userConfig, strategyResults] = await Promise.all([
5771
+ (0, import_redis3.getData)(import_redis3.redisKeys.strategyConfig(userName, strategy), {}),
5772
+ (0, import_redis3.getData)(import_redis3.redisKeys.strategyResults(userName, strategy), {})
5773
+ ]);
5774
+ const typedResults = strategyResults ?? {};
5775
+ const symbolResult = typedResults?.[symbol];
5776
+ const symbolConfig = symbolResult && symbolResult.config && typeof symbolResult.config === "object" ? symbolResult.config : {};
5777
+ const replayInputs = replayInputsIndex.get(toTargetKey({ strategy, symbol }));
5778
+ const aiReplayAnalyses = buildReplayAiAnalyses({
5779
+ runtimeTrades: replayInputs?.runtimeTrades ?? [],
5780
+ runtimeSignalEvaluations: replayInputs?.runtimeSignalEvaluations ?? [],
5781
+ strategy,
5782
+ symbol,
5783
+ toleranceMs
5784
+ });
5785
+ const strategyConfig = {
5786
+ ...userConfig,
5787
+ ...symbolConfig
5788
+ };
5789
+ return buildRuntimeModeStrategyConfig({
5790
+ strategyConfig,
5791
+ env: resolveReplayStrategyEnv({
5792
+ strategyConfig,
5793
+ forceRuntimeGates: Boolean(flags.runtimeGates)
5794
+ }),
5795
+ interval,
5796
+ makeOrders: true,
5797
+ recordRuntimeTrades: false,
5798
+ aiReplayAnalyses
5799
+ });
5800
+ };
5801
+ var warmReplayHistory = async ({
5802
+ userName,
5803
+ connectorName,
5804
+ targets,
5805
+ preloadStart,
5806
+ preloadEnd
5807
+ }) => {
5808
+ const connector = await createConnector({ userName, connectorName });
5809
+ const symbols = [...new Set(targets.map((target) => target.symbol))];
5810
+ const warmStartedAt = Date.now();
5811
+ console.log(
5812
+ import_chalk7.default.gray(
5813
+ `Warm history: ${symbols.length} symbol(s) on ${connectorName}, preload ${(0, import_time5.formatUnix)(preloadStart)} -> ${(0, import_time5.formatUnix)(preloadEnd)}`
5814
+ )
5815
+ );
5816
+ const btcReferences = await loadBtcReferenceConnectors({
5817
+ connectorName,
5818
+ marketConnector: connector,
5819
+ userName,
5820
+ projectRoot,
5821
+ shouldUseDedicatedReferences: true,
5822
+ warn: (message) => import_logger.logger.warn(message)
5823
+ });
5824
+ await updateMarketHistoryWithBtcReferences({
5825
+ marketConnector: connector,
5826
+ connectorName,
5827
+ btcReferences,
5828
+ interval,
5829
+ symbols,
5830
+ preloadStart,
5831
+ preloadEnd,
5832
+ log: (message) => console.log(import_chalk7.default.gray(message))
5833
+ });
5834
+ console.log(
5835
+ import_chalk7.default.gray(`Warm history: done in ${formatDuration(warmStartedAt)}`)
5836
+ );
5837
+ };
5748
5838
  var runtimeParity = async () => {
5749
5839
  const window = resolveTimeWindow({
5750
5840
  days: flags.days ?? DEFAULT_LOOKBACK_DAYS,
@@ -5767,28 +5857,18 @@ var runtimeParity = async () => {
5767
5857
  userName: flags.user,
5768
5858
  connectorName
5769
5859
  });
5770
- const [allRuntimeTrades, allRuntimeSignals, allRuntimeSignalEvaluations] = await Promise.all([
5771
- loadRuntimeTrades(flags.user, {
5772
- startTime: window.start,
5773
- endTime: window.end
5774
- }),
5775
- loadRuntimeSignals(flags.user, {
5776
- startTime: window.start,
5777
- endTime: window.end
5778
- }),
5779
- loadRuntimeSignalEvaluations(flags.user, {
5780
- startTime: window.start,
5781
- endTime: window.end
5782
- })
5783
- ]);
5784
- const runtimeTrades = allRuntimeTrades.filter(
5785
- (trade) => trade.entryTimestamp >= window.start && trade.entryTimestamp <= window.end && (!flags.strategy || trade.strategy === flags.strategy) && (!requestedSymbolSet || requestedSymbolSet.has(trade.symbol))
5786
- );
5787
- const runtimeSignals = allRuntimeSignals.filter(
5788
- (signal) => signal.timestamp >= window.start && signal.timestamp <= window.end && (!flags.strategy || signal.strategy === flags.strategy) && (!requestedSymbolSet || requestedSymbolSet.has(signal.symbol))
5789
- );
5790
- const runtimeSignalEvaluations = allRuntimeSignalEvaluations.filter(
5791
- (evaluation) => evaluation.timestamp >= window.start && evaluation.timestamp <= window.end && (!flags.strategy || evaluation.strategy === flags.strategy) && (!requestedSymbolSet || requestedSymbolSet.has(evaluation.symbol))
5860
+ const { runtimeTrades, runtimeSignals, runtimeSignalEvaluations } = await loadRuntimeParityEvidence(
5861
+ {
5862
+ userName: flags.user,
5863
+ window,
5864
+ strategy: flags.strategy,
5865
+ symbols: requestedSymbolSet
5866
+ },
5867
+ {
5868
+ loadRuntimeTrades,
5869
+ loadRuntimeSignals,
5870
+ loadRuntimeSignalEvaluations
5871
+ }
5792
5872
  );
5793
5873
  const connectorSymbols = requestedSymbols.length ? requestedSymbols : await (0, import_cli3.getTickers)(parityConnector);
5794
5874
  const replayInputsIndex = buildReplayInputsIndex({
@@ -5805,8 +5885,8 @@ var runtimeParity = async () => {
5805
5885
  });
5806
5886
  if (!replayTargets.length) {
5807
5887
  console.log(
5808
- import_chalk6.default.yellow(
5809
- `No replay targets found for ${flags.user} in ${(0, import_time3.formatUnix)(window.start)} -> ${(0, import_time3.formatUnix)(window.end)}.`
5888
+ import_chalk7.default.yellow(
5889
+ `No replay targets found for ${flags.user} in ${(0, import_time5.formatUnix)(window.start)} -> ${(0, import_time5.formatUnix)(window.end)}.`
5810
5890
  )
5811
5891
  );
5812
5892
  if (flags.notify) {
@@ -5824,7 +5904,7 @@ var runtimeParity = async () => {
5824
5904
  return;
5825
5905
  }
5826
5906
  const sourceCounts = countReplayTargetSources(replayTargets);
5827
- const preloadStart = (0, import_time3.getBacktestPreloadStart)(
5907
+ const preloadStart = (0, import_time5.getBacktestPreloadStart)(
5828
5908
  window.start,
5829
5909
  import_constants3.BACKTEST_PRELOAD_DAYS
5830
5910
  );
@@ -5833,7 +5913,7 @@ var runtimeParity = async () => {
5833
5913
  replayTargets.map((target) => target.symbol)
5834
5914
  ).size;
5835
5915
  console.log(
5836
- import_chalk6.default.gray(
5916
+ import_chalk7.default.gray(
5837
5917
  `Replay queue: ${replayTargets.length} target(s), ${replaySymbolsCount} symbol(s), mode=${flags.fullUniverse ? "full-universe" : requestedSymbols.length ? "explicit-tickers" : "runtime+results"}`
5838
5918
  )
5839
5919
  );
@@ -5868,7 +5948,7 @@ var runtimeParity = async () => {
5868
5948
  aiEnabled: hasParityAiTargets,
5869
5949
  mlEnabled: hasParityMlTargets,
5870
5950
  strategyNames: replayTargets.map((target) => target.strategy),
5871
- log: (message) => console.log(import_chalk6.default.gray(message))
5951
+ log: (message) => console.log(import_chalk7.default.gray(message))
5872
5952
  });
5873
5953
  if (!flags.cacheOnly) {
5874
5954
  await warmReplayHistory({
@@ -5936,139 +6016,61 @@ var runtimeParity = async () => {
5936
6016
  }
5937
6017
  if (progressPosition === 1 || progressPosition === replayTargets.length || progressPosition % 25 === 0) {
5938
6018
  console.log(
5939
- import_chalk6.default.gray(
6019
+ import_chalk7.default.gray(
5940
6020
  `Replay progress: ${progressPosition}/${replayTargets.length}, ok=${successfulTargetKeys.size}, errors=${replayErrors.length}, current=${target.strategy} ${target.symbol}, elapsed=${formatDuration(replayStartedAt)}`
5941
6021
  )
5942
6022
  );
5943
6023
  }
5944
6024
  }
5945
- const comparableRuntimeTrades = runtimeTrades.filter(
5946
- (trade) => successfulTargetKeys.has(toTargetKey(trade))
5947
- );
5948
- const rawRuntimeEntries = extractRuntimeParityEntries(
5949
- comparableRuntimeTrades
5950
- );
5951
- const runtimeDedupe = dedupeRuntimeParityEntries(rawRuntimeEntries);
5952
- const runtimeEntries = runtimeDedupe.entries;
5953
- const comparison = compareTradeParityEntries({
6025
+ const {
6026
+ rawRuntimeEntries,
6027
+ runtimeDedupe,
5954
6028
  runtimeEntries,
5955
- backtestEntries,
5956
- toleranceMs
5957
- });
5958
- const classifiedBacktestOnly = classifyBacktestOnlyEntries({
5959
- entries: comparison.backtestOnly,
6029
+ comparison,
6030
+ classifiedBacktestOnly,
6031
+ classifiedRuntimeOnly,
6032
+ matchedSummary: summary,
6033
+ strategyRows
6034
+ } = analyzeRuntimeParity({
6035
+ runtimeTrades,
5960
6036
  runtimeSignals,
5961
6037
  runtimeSignalEvaluations,
5962
- toleranceMs
5963
- });
5964
- const classifiedRuntimeOnly = classifyRuntimeOnlyEntries({
5965
- entries: comparison.runtimeOnly,
5966
- replaySignalEvaluations,
5967
6038
  backtestEntries,
6039
+ replaySignalEvaluations,
6040
+ replayTargets,
6041
+ successfulTargetKeys,
6042
+ replayErrors,
5968
6043
  toleranceMs
5969
6044
  });
5970
- const summary = summarizeMatchedParity(comparison.matched);
5971
- console.log(import_chalk6.default.cyan("TradeJS runtime parity"));
5972
- console.log(
5973
- `Window: ${(0, import_time3.formatUnix)(window.start)} -> ${(0, import_time3.formatUnix)(window.end)} (${window.source})`
5974
- );
5975
- console.log(`Connector: ${connectorName}`);
5976
- console.log(
5977
- `Replay env: ${replayEnvSummary}${flags.runtimeGates || replayEnvSummary.includes("PARITY") ? " (runtime AI/ML gates enabled)" : " (core/backtest gates only)"}`
5978
- );
5979
- console.log(
5980
- `Tolerance: ${toleranceBars} bar(s) / ${(toleranceMs / 6e4).toFixed(0)}m`
5981
- );
5982
- console.log(
5983
- `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}`
5984
- );
5985
- console.log(`Sources: ${formatSourceCountsSummary(sourceCounts)}`);
5986
- console.log(
5987
- `Runtime entries: ${formatRuntimeEntriesSummary({
6045
+ writeRuntimeParityTerminalReport(
6046
+ buildRuntimeParityTerminalReport({
6047
+ window,
6048
+ connectorName,
6049
+ replayEnv: replayEnvSummary,
6050
+ runtimeGatesEnabled: Boolean(flags.runtimeGates) || replayEnvSummary.includes("PARITY"),
6051
+ runtimeGatesRequested: Boolean(flags.runtimeGates),
6052
+ toleranceBars,
6053
+ toleranceMs,
6054
+ replayTargetsCount: replayTargets.length,
6055
+ comparedTargetsCount: successfulTargetKeys.size,
6056
+ replayErrors,
6057
+ sourceCounts,
5988
6058
  rawRuntimeEntriesCount: rawRuntimeEntries.length,
5989
6059
  runtimeEntriesCount: runtimeEntries.length,
5990
- runtimeDuplicateEntriesCount: runtimeDedupe.duplicateEntries.length
5991
- })}`
5992
- );
5993
- console.log(
5994
- `Matched deltas: price avg/max=${formatPercent(summary.avgPriceDeltaPct)} / ${formatPercent(summary.maxPriceDeltaPct)}, time avg/max=${formatMinutes2(summary.avgTimestampDiffMs)} / ${formatMinutes2(summary.maxTimestampDiffMs)}`
6060
+ runtimeDuplicateGroupsCount: runtimeDedupe.duplicateGroups.length,
6061
+ runtimeDuplicateEntriesCount: runtimeDedupe.duplicateEntries.length,
6062
+ backtestEntriesCount: backtestEntries.length,
6063
+ runtimeOnlyCount: comparison.runtimeOnly.length,
6064
+ backtestOnlyCount: comparison.backtestOnly.length,
6065
+ matchedSummary: summary,
6066
+ classifiedRuntimeOnly,
6067
+ classifiedBacktestOnly,
6068
+ runtimeSignalEvaluationsCount: runtimeSignalEvaluations.length,
6069
+ strategyRows,
6070
+ runtimeGateWarningCounts,
6071
+ detailLimit: DETAIL_LIMIT2
6072
+ })
5995
6073
  );
5996
- if (runtimeDedupe.duplicateGroups.length) {
5997
- console.log(
5998
- `Runtime duplicate groups: ${runtimeDedupe.duplicateGroups.length}, duplicate entries: ${runtimeDedupe.duplicateEntries.length}`
5999
- );
6000
- }
6001
- if (classifiedRuntimeOnly.length) {
6002
- console.log(
6003
- `Runtime only classifications: ${summarizeRuntimeOnlyClassifications(classifiedRuntimeOnly)}`
6004
- );
6005
- }
6006
- if (classifiedBacktestOnly.length) {
6007
- console.log(
6008
- `Backtest only classifications: ${summarizeBacktestOnlyClassifications(classifiedBacktestOnly)}`
6009
- );
6010
- }
6011
- if (runtimeSignalEvaluations.length) {
6012
- console.log(`Runtime evaluations: ${runtimeSignalEvaluations.length}`);
6013
- }
6014
- const strategyRows = summarizeByStrategy({
6015
- targets: replayTargets,
6016
- successfulTargetKeys,
6017
- replayErrors,
6018
- runtimeEntries,
6019
- runtimeDuplicateEntries: runtimeDedupe.duplicateEntries,
6020
- backtestEntries,
6021
- matchedEntries: comparison.matched,
6022
- runtimeOnlyEntries: comparison.runtimeOnly,
6023
- backtestOnlyEntries: comparison.backtestOnly
6024
- });
6025
- const mismatchRows = buildMismatchSummaryRows({
6026
- classifiedRuntimeOnly,
6027
- classifiedBacktestOnly
6028
- });
6029
- const strategyIssueRows = buildStrategyIssueRows(strategyRows);
6030
- if (mismatchRows.length) {
6031
- console.log("");
6032
- console.log(import_chalk6.default.yellow("Signal mismatches"));
6033
- for (const item of mismatchRows.slice(0, DETAIL_LIMIT)) {
6034
- console.log(`- ${item.line}`);
6035
- }
6036
- if (mismatchRows.length > DETAIL_LIMIT) {
6037
- console.log(`- ... ${mismatchRows.length - DETAIL_LIMIT} more`);
6038
- }
6039
- }
6040
- console.log("");
6041
- if (strategyIssueRows.length) {
6042
- console.log(import_chalk6.default.cyan("Strategy issues"));
6043
- for (const line of strategyIssueRows) {
6044
- console.log(line);
6045
- }
6046
- } else if (strategyRows.length) {
6047
- console.log(
6048
- `Strategies: clean ${strategyRows.length}/${strategyRows.length}`
6049
- );
6050
- }
6051
- if (runtimeGateWarningCounts.size && !flags.runtimeGates) {
6052
- console.log("");
6053
- console.log(import_chalk6.default.yellow("Warnings"));
6054
- for (const [strategy, count] of [
6055
- ...runtimeGateWarningCounts.entries()
6056
- ].sort(([left], [right]) => left.localeCompare(right))) {
6057
- console.log(
6058
- `- ${strategy} uses AI/ML runtime gates on ${count} replay target(s); BACKTEST replay covers core execution, not live gating.`
6059
- );
6060
- }
6061
- }
6062
- if (replayErrors.length) {
6063
- console.log("");
6064
- console.log(import_chalk6.default.red("Replay errors"));
6065
- for (const error of replayErrors.slice(0, DETAIL_LIMIT)) {
6066
- console.log(`- ${error.strategy} ${error.symbol}: ${error.message}`);
6067
- }
6068
- if (replayErrors.length > DETAIL_LIMIT) {
6069
- console.log(`- ... ${replayErrors.length - DETAIL_LIMIT} more`);
6070
- }
6071
- }
6072
6074
  if (flags.details) {
6073
6075
  printRuntimeDuplicateDetails(runtimeDedupe.duplicateGroups);
6074
6076
  printClassifiedRuntimeOnlyDetails(classifiedRuntimeOnly);