@tradejs/cli 3.1.26 → 3.1.27-beta.247

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.
@@ -758,6 +758,20 @@ var countBy = (items, getter) => {
758
758
  Object.entries(counts).sort(([left], [right]) => left.localeCompare(right))
759
759
  );
760
760
  };
761
+ var resolveGateDecision = (evaluation) => {
762
+ const analysis = asRecord2(evaluation.aiAnalysis);
763
+ const explicit = finiteString(analysis?.gateDecision);
764
+ if (explicit === "approved" || explicit === "rejected") {
765
+ return { decision: explicit, source: "gate_decision" };
766
+ }
767
+ if (typeof analysis?.approved === "boolean") {
768
+ return {
769
+ decision: analysis.approved ? "approved" : "rejected",
770
+ source: "legacy_approved_fallback"
771
+ };
772
+ }
773
+ return null;
774
+ };
761
775
  var buildDistributions = ({
762
776
  evaluations,
763
777
  signals
@@ -938,18 +952,15 @@ var buildRuntimeScorecard = ({
938
952
  const signalEvaluations = rows.evaluations.filter(
939
953
  (evaluation) => evaluation.status === "signal"
940
954
  );
941
- const gateDecisions = signalEvaluations.map((evaluation) => ({
942
- evaluation,
943
- decision: finiteString(asRecord2(evaluation.aiAnalysis)?.gateDecision)
944
- })).filter(
945
- ({ decision }) => decision === "approved" || decision === "rejected"
955
+ const gateDecisions = signalEvaluations.map(resolveGateDecision).filter(
956
+ (decision) => Boolean(decision)
946
957
  );
947
958
  const llmEligibleEvaluations = llmComparatorPolicy === "disabled" ? [] : llmComparatorPolicy === "ai_approved_only" ? signalEvaluations.filter(
948
- (evaluation) => finiteString(asRecord2(evaluation.aiAnalysis)?.gateDecision) === "approved"
959
+ (evaluation) => resolveGateDecision(evaluation)?.decision === "approved"
949
960
  ) : signalEvaluations;
950
961
  const gateComparisons = llmEligibleEvaluations.map((evaluation) => {
951
962
  const analysis = asRecord2(evaluation.aiAnalysis);
952
- const gateDecision = finiteString(analysis?.gateDecision);
963
+ const gateDecision = resolveGateDecision(evaluation)?.decision ?? null;
953
964
  const llmDecision = finiteString(analysis?.llmDecision);
954
965
  if (gateDecision !== "approved" && gateDecision !== "rejected" || llmDecision !== "approved" && llmDecision !== "rejected") {
955
966
  return null;
@@ -984,6 +995,27 @@ var buildRuntimeScorecard = ({
984
995
  };
985
996
  const actualSlippageBps = averageSampleMetric("signalToFillAdverseBps");
986
997
  const residualVsModelBps = averageSampleMetric("residualVsCurrentModelBps");
998
+ const signalToArrivalAdverseBps = averageSampleMetric(
999
+ "signalToArrivalAdverseBps"
1000
+ );
1001
+ const arrivalToFillAdverseBps = averageSampleMetric(
1002
+ "arrivalToFillAdverseBps"
1003
+ );
1004
+ const averageSignalCloseToSubmitMs = averageSampleMetric(
1005
+ "signalCloseToSubmitMs"
1006
+ );
1007
+ const averageSubmitToFillMs = averageSampleMetric("submitToFillMs");
1008
+ const fullTelemetryTrades = calibrationSamples.filter(
1009
+ (sample) => finiteString(sample.telemetryQuality) === "full"
1010
+ ).length;
1011
+ const arrivalQuoteTrades = calibrationSamples.filter(
1012
+ (sample) => finiteNumber(sample.arrivalMid) != null
1013
+ ).length;
1014
+ const arrivalQuoteCoverage = calibrationSamples.length ? round(arrivalQuoteTrades / calibrationSamples.length) : null;
1015
+ const hasTelemetryQuality = calibrationSamples.some(
1016
+ (sample) => finiteString(sample.telemetryQuality) != null
1017
+ );
1018
+ const maximumSignalCloseToSubmitMs = thresholds.maximumSignalCloseToSubmitMs ?? 3e4;
987
1019
  const prospective = getProspectiveSummary(prospectiveEvidenceArtifact);
988
1020
  const historyTrades = dedupeTrades([
989
1021
  ...scopedHistoryArtifacts,
@@ -1040,7 +1072,28 @@ var buildRuntimeScorecard = ({
1040
1072
  reactions.push({
1041
1073
  code: "SLIPPAGE_DRIFT",
1042
1074
  severity: "warning",
1043
- message: `Execution residual ${round(residualVsModelBps, 2)} bps exceeds ${thresholds.maximumSlippageResidualBps} bps.`
1075
+ message: `Signal-close-to-fill residual ${round(residualVsModelBps, 2)} bps exceeds ${thresholds.maximumSlippageResidualBps} bps.`
1076
+ });
1077
+ }
1078
+ if (averageSignalCloseToSubmitMs != null && averageSignalCloseToSubmitMs > maximumSignalCloseToSubmitMs) {
1079
+ reactions.push({
1080
+ code: "PRE_SUBMIT_LATENCY",
1081
+ severity: "warning",
1082
+ message: `Average signal-close-to-submit latency ${round(averageSignalCloseToSubmitMs, 3)} ms exceeds ${maximumSignalCloseToSubmitMs} ms.`
1083
+ });
1084
+ }
1085
+ if (hasTelemetryQuality && calibrationSamples.length > 0 && fullTelemetryTrades < calibrationSamples.length) {
1086
+ reactions.push({
1087
+ code: "EXECUTION_TELEMETRY_INCOMPLETE",
1088
+ severity: "info",
1089
+ message: `Full arrival and fill telemetry is available for ${fullTelemetryTrades}/${calibrationSamples.length} trades.`
1090
+ });
1091
+ }
1092
+ if (riskDecisions.some((decision) => decision.status === "rejected")) {
1093
+ reactions.push({
1094
+ code: "RISK_PLAN_EXCEEDS_DECLARED_LIMIT",
1095
+ severity: "warning",
1096
+ message: "At least one observed order plan exceeds its declared MAX_LOSS_VALUE; telemetry is not yet enforced."
1044
1097
  });
1045
1098
  }
1046
1099
  if (sevenDay.closedTrades >= thresholds.minimumClosedTrades && sevenDay.expectancy != null && sevenDay.expectancy < thresholds.minimumExpectancy) {
@@ -1069,7 +1122,10 @@ var buildRuntimeScorecard = ({
1069
1122
  deployment: deploymentBinding,
1070
1123
  lineage: runtimeLineage,
1071
1124
  promotionStatus,
1072
- thresholds,
1125
+ thresholds: {
1126
+ ...thresholds,
1127
+ maximumSignalCloseToSubmitMs
1128
+ },
1073
1129
  funnel: {
1074
1130
  evaluations: rows.evaluations.length,
1075
1131
  coreCandidates: signalEvaluations.length,
@@ -1081,25 +1137,38 @@ var buildRuntimeScorecard = ({
1081
1137
  rejected: gateDecisions.filter(
1082
1138
  ({ decision }) => decision === "rejected"
1083
1139
  ).length,
1084
- coverage: signalEvaluations.length ? round(gateDecisions.length / signalEvaluations.length) : null
1140
+ coverage: signalEvaluations.length ? round(gateDecisions.length / signalEvaluations.length) : null,
1141
+ legacyApprovedFallback: gateDecisions.filter(
1142
+ ({ source }) => source === "legacy_approved_fallback"
1143
+ ).length
1085
1144
  },
1086
1145
  allocator: {
1087
1146
  available: allocatorDecisions.length > 0,
1088
1147
  approved: allocatorDecisions.filter(
1089
- (decision) => decision.decision === "approved"
1148
+ (decision) => decision.status === "approved"
1090
1149
  ).length,
1091
1150
  rejected: allocatorDecisions.filter(
1092
- (decision) => decision.decision === "rejected"
1151
+ (decision) => decision.status === "rejected"
1152
+ ).length,
1153
+ notApplicable: allocatorDecisions.filter(
1154
+ (decision) => decision.status === "not_applicable"
1155
+ ).length,
1156
+ unavailable: allocatorDecisions.filter(
1157
+ (decision) => decision.status === "unavailable"
1093
1158
  ).length
1094
1159
  },
1095
1160
  risk: {
1096
1161
  available: riskDecisions.length > 0,
1097
1162
  approved: riskDecisions.filter(
1098
- (decision) => decision.decision === "approved"
1163
+ (decision) => decision.status === "approved"
1099
1164
  ).length,
1100
1165
  rejected: riskDecisions.filter(
1101
- (decision) => decision.decision === "rejected"
1102
- ).length
1166
+ (decision) => decision.status === "rejected"
1167
+ ).length,
1168
+ unavailable: riskDecisions.filter(
1169
+ (decision) => decision.status === "unavailable"
1170
+ ).length,
1171
+ enforced: riskDecisions.some((decision) => decision.enforced === true)
1103
1172
  },
1104
1173
  orderAttempts: orderAttempts.length,
1105
1174
  orderFailures: orderAttempts.filter(
@@ -1161,8 +1230,16 @@ var buildRuntimeScorecard = ({
1161
1230
  },
1162
1231
  execution: {
1163
1232
  available: actualSlippageBps != null || residualVsModelBps != null,
1233
+ metricScope: arrivalToFillAdverseBps == null ? "signal_close_to_fill" : "signal_and_exchange_components",
1234
+ samples: calibrationSamples.length,
1235
+ fullTelemetryTrades,
1236
+ arrivalQuoteCoverage,
1164
1237
  actualSignalToFillSlippageBps: actualSlippageBps,
1165
- residualVsCurrentModelBps: residualVsModelBps
1238
+ residualVsCurrentModelBps: residualVsModelBps,
1239
+ signalToArrivalAdverseBps,
1240
+ arrivalToFillAdverseBps,
1241
+ averageSignalCloseToSubmitMs,
1242
+ averageSubmitToFillMs
1166
1243
  },
1167
1244
  prospective,
1168
1245
  rolling,
@@ -1216,10 +1293,10 @@ Window: ${new Date(scorecard.window.startTime).toISOString()} \u2014 ${new Date(
1216
1293
 
1217
1294
  - Evaluations: ${scorecard.funnel.evaluations}
1218
1295
  - Core candidates: ${scorecard.funnel.coreCandidates}
1219
- - Gate approvals/rejects: ${scorecard.funnel.gate.approved}/${scorecard.funnel.gate.rejected}
1296
+ - Gate approvals/rejects: ${scorecard.funnel.gate.approved}/${scorecard.funnel.gate.rejected} (legacy fallback: ${scorecard.funnel.gate.legacyApprovedFallback})
1220
1297
  - AI / LLM disagreement: ${scorecard.gateComparison.disagreements}/${scorecard.gateComparison.compared}
1221
- - Allocator approvals/rejects: ${scorecard.funnel.allocator.available ? `${scorecard.funnel.allocator.approved}/${scorecard.funnel.allocator.rejected}` : "unavailable"}
1222
- - Risk approvals/rejects: ${scorecard.funnel.risk.available ? `${scorecard.funnel.risk.approved}/${scorecard.funnel.risk.rejected}` : "unavailable"}
1298
+ - Runtime admission approved/rejected/not-applicable: ${scorecard.funnel.allocator.available ? `${scorecard.funnel.allocator.approved}/${scorecard.funnel.allocator.rejected}/${scorecard.funnel.allocator.notApplicable}` : "unavailable"}
1299
+ - Risk assessment within/exceeds/unavailable: ${scorecard.funnel.risk.available ? `${scorecard.funnel.risk.approved}/${scorecard.funnel.risk.rejected}/${scorecard.funnel.risk.unavailable}` : "unavailable"} (enforced: ${scorecard.funnel.risk.enforced})
1223
1300
  - Order attempts/failures: ${scorecard.funnel.orderAttempts}/${scorecard.funnel.orderFailures}
1224
1301
  - Fills: ${scorecard.funnel.fills}
1225
1302
  - Closed trades: ${scorecard.funnel.closedTrades}
@@ -1228,8 +1305,14 @@ Window: ${new Date(scorecard.window.startTime).toISOString()} \u2014 ${new Date(
1228
1305
 
1229
1306
  - Parity: ${scorecard.parity.ratio == null ? "n/a" : `${round(scorecard.parity.ratio * 100, 2)}%`}
1230
1307
  - Runtime/replay mismatches: ${scorecard.parity.backtestOnly + scorecard.parity.runtimeOnly}
1231
- - Actual signal-to-fill slippage: ${scorecard.execution.actualSignalToFillSlippageBps ?? "n/a"} bps
1308
+ - Execution metric scope: ${scorecard.execution.metricScope}
1309
+ - Telemetry samples/full/arrival coverage: ${scorecard.execution.samples}/${scorecard.execution.fullTelemetryTrades}/${scorecard.execution.arrivalQuoteCoverage == null ? "n/a" : `${round(scorecard.execution.arrivalQuoteCoverage * 100, 2)}%`}
1310
+ - Signal-close-to-fill adverse move: ${scorecard.execution.actualSignalToFillSlippageBps ?? "n/a"} bps
1311
+ - Signal-to-arrival adverse move: ${scorecard.execution.signalToArrivalAdverseBps ?? "n/a"} bps
1312
+ - Arrival-to-fill exchange slippage: ${scorecard.execution.arrivalToFillAdverseBps ?? "n/a"} bps
1232
1313
  - Residual vs current model: ${scorecard.execution.residualVsCurrentModelBps ?? "n/a"} bps
1314
+ - Average signal-close-to-submit: ${scorecard.execution.averageSignalCloseToSubmitMs ?? "n/a"} ms
1315
+ - Average submit-to-fill: ${scorecard.execution.averageSubmitToFillMs ?? "n/a"} ms
1233
1316
 
1234
1317
  ## Rolling performance
1235
1318
 
@@ -1938,6 +2021,11 @@ import_args.default.option(
1938
2021
  "Maximum acceptable execution residual in bps",
1939
2022
  3
1940
2023
  );
2024
+ import_args.default.option(
2025
+ ["L", "maximumSignalCloseToSubmitMs"],
2026
+ "Maximum acceptable average signal-close-to-submit latency in milliseconds",
2027
+ 3e4
2028
+ );
1941
2029
  import_args.default.option(
1942
2030
  ["T", "minimumClosedTrades"],
1943
2031
  "Minimum 7d closed trades for expectancy reactions",
@@ -2030,6 +2118,7 @@ var runtimeScorecard = async () => {
2030
2118
  thresholds: {
2031
2119
  minimumParityRatio: Number(flags.minimumParityRatio),
2032
2120
  maximumSlippageResidualBps: Number(flags.maximumSlippageResidualBps),
2121
+ maximumSignalCloseToSubmitMs: Number(flags.maximumSignalCloseToSubmitMs),
2033
2122
  minimumClosedTrades: Number(flags.minimumClosedTrades),
2034
2123
  minimumExpectancy: Number(flags.minimumExpectancy)
2035
2124
  },
@@ -3778,7 +3778,38 @@ var import_logger = require("@tradejs/infra/logger");
3778
3778
 
3779
3779
  // src/lib/runtimeSignalsStorage.ts
3780
3780
  var import_constants2 = require("@tradejs/core/constants");
3781
+ var import_data = require("@tradejs/core/data");
3781
3782
  var import_time2 = require("@tradejs/core/time");
3783
+ var appendRuntimeSignalEvaluationRun = (runs = [], evaluation) => {
3784
+ if (evaluation.status !== "skip") {
3785
+ return [...runs];
3786
+ }
3787
+ const stepMs = (0, import_data.intervalToMs)(evaluation.interval);
3788
+ const reason = evaluation.reason?.trim() || "NO_SIGNAL";
3789
+ const existing = runs.find(
3790
+ (run) => run.status === "skip" && run.reason === reason && run.stepMs === stepMs && evaluation.timestamp >= run.firstTimestamp && evaluation.timestamp <= run.lastTimestamp && (evaluation.timestamp - run.firstTimestamp) % stepMs === 0
3791
+ );
3792
+ if (existing) {
3793
+ return [...runs];
3794
+ }
3795
+ const last = runs.at(-1);
3796
+ if (last?.status === "skip" && last.reason === reason && last.stepMs === stepMs && evaluation.timestamp === last.lastTimestamp + stepMs) {
3797
+ return [
3798
+ ...runs.slice(0, -1),
3799
+ { ...last, lastTimestamp: evaluation.timestamp }
3800
+ ];
3801
+ }
3802
+ return [
3803
+ ...runs,
3804
+ {
3805
+ status: "skip",
3806
+ reason,
3807
+ firstTimestamp: evaluation.timestamp,
3808
+ lastTimestamp: evaluation.timestamp,
3809
+ stepMs
3810
+ }
3811
+ ];
3812
+ };
3782
3813
  var SECONDS_PER_DAY = 86400;
3783
3814
  var RUNTIME_SIGNAL_RETENTION_DAYS_ENV = "RUNTIME_SIGNAL_RETENTION_DAYS";
3784
3815
  var DEFAULT_RUNTIME_SIGNAL_RETENTION_TTL_SECONDS = import_constants2.TTL_3D;
@@ -4091,6 +4122,10 @@ var flushRuntimeSignalEvaluations = async (evaluations) => {
4091
4122
  lastTimestamp: Math.max(
4092
4123
  existing?.lastTimestamp ?? evaluation.timestamp,
4093
4124
  evaluation.timestamp
4125
+ ),
4126
+ evaluationRuns: appendRuntimeSignalEvaluationRun(
4127
+ existing?.evaluationRuns,
4128
+ evaluation
4094
4129
  )
4095
4130
  };
4096
4131
  runtimeLineageScopeCache.set(cacheKey, scope);
@@ -4812,6 +4847,8 @@ var createSignalsTickerEvaluator = ({
4812
4847
  orderStatus: signal.orderStatus,
4813
4848
  orderSkipReason: signal.orderSkipReason,
4814
4849
  ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
4850
+ ...signal.allocatorDecision ? { allocatorDecision: signal.allocatorDecision } : {},
4851
+ ...signal.riskDecision ? { riskDecision: signal.riskDecision } : {},
4815
4852
  ...signal.ml ? { ml: signal.ml } : {}
4816
4853
  });
4817
4854
  import_logger3.logger.info("Signal found %s by strategy %s", symbol, strategyName);
@@ -3776,7 +3776,38 @@ var import_logger = require("@tradejs/infra/logger");
3776
3776
 
3777
3777
  // src/lib/runtimeSignalsStorage.ts
3778
3778
  var import_constants2 = require("@tradejs/core/constants");
3779
+ var import_data = require("@tradejs/core/data");
3779
3780
  var import_time2 = require("@tradejs/core/time");
3781
+ var appendRuntimeSignalEvaluationRun = (runs = [], evaluation) => {
3782
+ if (evaluation.status !== "skip") {
3783
+ return [...runs];
3784
+ }
3785
+ const stepMs = (0, import_data.intervalToMs)(evaluation.interval);
3786
+ const reason = evaluation.reason?.trim() || "NO_SIGNAL";
3787
+ const existing = runs.find(
3788
+ (run) => run.status === "skip" && run.reason === reason && run.stepMs === stepMs && evaluation.timestamp >= run.firstTimestamp && evaluation.timestamp <= run.lastTimestamp && (evaluation.timestamp - run.firstTimestamp) % stepMs === 0
3789
+ );
3790
+ if (existing) {
3791
+ return [...runs];
3792
+ }
3793
+ const last = runs.at(-1);
3794
+ if (last?.status === "skip" && last.reason === reason && last.stepMs === stepMs && evaluation.timestamp === last.lastTimestamp + stepMs) {
3795
+ return [
3796
+ ...runs.slice(0, -1),
3797
+ { ...last, lastTimestamp: evaluation.timestamp }
3798
+ ];
3799
+ }
3800
+ return [
3801
+ ...runs,
3802
+ {
3803
+ status: "skip",
3804
+ reason,
3805
+ firstTimestamp: evaluation.timestamp,
3806
+ lastTimestamp: evaluation.timestamp,
3807
+ stepMs
3808
+ }
3809
+ ];
3810
+ };
3780
3811
  var SECONDS_PER_DAY = 86400;
3781
3812
  var RUNTIME_SIGNAL_RETENTION_DAYS_ENV = "RUNTIME_SIGNAL_RETENTION_DAYS";
3782
3813
  var DEFAULT_RUNTIME_SIGNAL_RETENTION_TTL_SECONDS = import_constants2.TTL_3D;
@@ -4089,6 +4120,10 @@ var flushRuntimeSignalEvaluations = async (evaluations) => {
4089
4120
  lastTimestamp: Math.max(
4090
4121
  existing?.lastTimestamp ?? evaluation.timestamp,
4091
4122
  evaluation.timestamp
4123
+ ),
4124
+ evaluationRuns: appendRuntimeSignalEvaluationRun(
4125
+ existing?.evaluationRuns,
4126
+ evaluation
4092
4127
  )
4093
4128
  };
4094
4129
  runtimeLineageScopeCache.set(cacheKey, scope);
@@ -4810,6 +4845,8 @@ var createSignalsTickerEvaluator = ({
4810
4845
  orderStatus: signal.orderStatus,
4811
4846
  orderSkipReason: signal.orderSkipReason,
4812
4847
  ...signal.aiAnalysis ? { aiAnalysis: signal.aiAnalysis } : {},
4848
+ ...signal.allocatorDecision ? { allocatorDecision: signal.allocatorDecision } : {},
4849
+ ...signal.riskDecision ? { riskDecision: signal.riskDecision } : {},
4813
4850
  ...signal.ml ? { ml: signal.ml } : {}
4814
4851
  });
4815
4852
  import_logger3.logger.info("Signal found %s by strategy %s", symbol, strategyName);
@@ -436,6 +436,7 @@ var import_redis3 = require("@tradejs/infra/redis");
436
436
 
437
437
  // src/lib/runtimeSignalsStorage.ts
438
438
  var import_constants2 = require("@tradejs/core/constants");
439
+ var import_data = require("@tradejs/core/data");
439
440
  var import_time3 = require("@tradejs/core/time");
440
441
  var isRuntimeSignalBucketRef = (value) => {
441
442
  if (!value || typeof value !== "object") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/cli",
3
- "version": "3.1.26",
3
+ "version": "3.1.27-beta.247",
4
4
  "description": "Official CLI for the TradeJS TypeScript framework: infra setup, backtests, signals, bots, and ML workflows.",
5
5
  "keywords": [
6
6
  "tradejs",
@@ -36,13 +36,13 @@
36
36
  "tradejs": "dist/cli.js"
37
37
  },
38
38
  "dependencies": {
39
- "@tradejs/connectors": "^3.1.26",
40
- "@tradejs/core": "^3.1.26",
41
- "@tradejs/indicators": "^3.1.26",
42
- "@tradejs/infra": "^3.1.26",
43
- "@tradejs/node": "^3.1.26",
39
+ "@tradejs/connectors": "^3.1.27-beta.247",
40
+ "@tradejs/core": "^3.1.27-beta.247",
41
+ "@tradejs/indicators": "^3.1.27-beta.247",
42
+ "@tradejs/infra": "^3.1.27-beta.247",
43
+ "@tradejs/node": "^3.1.27-beta.247",
44
44
  "@tradejs/strategy-kit": "3.0.4",
45
- "@tradejs/types": "^3.1.26",
45
+ "@tradejs/types": "^3.1.27-beta.247",
46
46
  "args": "^5.0.3",
47
47
  "bcryptjs": "^2.4.3",
48
48
  "chalk": "4.1.2",