@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.
@@ -104,6 +104,7 @@ var import_redis2 = require("@tradejs/infra/redis");
104
104
 
105
105
  // src/lib/runtimeSignalsStorage.ts
106
106
  var import_constants = require("@tradejs/core/constants");
107
+ var import_data = require("@tradejs/core/data");
107
108
  var import_time2 = require("@tradejs/core/time");
108
109
  var isRuntimeSignalBucketRef = (value) => {
109
110
  if (!value || typeof value !== "object") {
@@ -208,6 +208,90 @@ var findNearestEvaluation = ({
208
208
  timestampDiffMs: bestDiff
209
209
  } : null;
210
210
  };
211
+ var findNearestRuntimeEvaluationOutcome = ({
212
+ entry,
213
+ lineageScopes,
214
+ toleranceMs
215
+ }) => {
216
+ const strategy = getEntryStrategy(entry);
217
+ const timestamp = entry.signalTimestamp ?? entry.timestamp;
218
+ if (!strategy) return null;
219
+ let nearest = null;
220
+ for (const scope of lineageScopes) {
221
+ if (scope.strategy !== strategy || scope.symbol !== entry.symbol) {
222
+ continue;
223
+ }
224
+ for (const run of scope.evaluationRuns ?? []) {
225
+ if (run.stepMs <= 0 || timestamp < run.firstTimestamp - toleranceMs || timestamp > run.lastTimestamp + toleranceMs) {
226
+ continue;
227
+ }
228
+ const step = Math.round((timestamp - run.firstTimestamp) / run.stepMs);
229
+ const candidateTimestamp = run.firstTimestamp + step * run.stepMs;
230
+ const timestampDiffMs = Math.abs(candidateTimestamp - timestamp);
231
+ if (candidateTimestamp < run.firstTimestamp || candidateTimestamp > run.lastTimestamp || timestampDiffMs > toleranceMs || nearest && timestampDiffMs >= nearest.timestampDiffMs) {
232
+ continue;
233
+ }
234
+ nearest = {
235
+ status: run.status,
236
+ reason: run.reason,
237
+ timestamp: candidateTimestamp,
238
+ timestampDiffMs,
239
+ source: "lineage_scope_compact"
240
+ };
241
+ }
242
+ }
243
+ return nearest;
244
+ };
245
+ var findNearestReplayEvaluationOutcome = ({
246
+ entry,
247
+ lineageScopes,
248
+ toleranceMs,
249
+ timestampOffsetMs
250
+ }) => {
251
+ const strategy = getEntryStrategy(entry);
252
+ if (!strategy) return null;
253
+ let nearest = null;
254
+ for (const scope of lineageScopes) {
255
+ if (scope.strategy !== strategy || scope.symbol !== entry.symbol) {
256
+ continue;
257
+ }
258
+ for (const run of scope.evaluationRuns ?? []) {
259
+ if (run.stepMs <= 0) continue;
260
+ const targetTimestamp = entry.timestamp - timestampOffsetMs;
261
+ if (targetTimestamp < run.firstTimestamp - toleranceMs || targetTimestamp > run.lastTimestamp + toleranceMs) {
262
+ continue;
263
+ }
264
+ const step = Math.round(
265
+ (targetTimestamp - run.firstTimestamp) / run.stepMs
266
+ );
267
+ const candidateTimestamp = run.firstTimestamp + step * run.stepMs;
268
+ const timestampDiffMs = Math.abs(
269
+ candidateTimestamp + timestampOffsetMs - entry.timestamp
270
+ );
271
+ if (candidateTimestamp < run.firstTimestamp || candidateTimestamp > run.lastTimestamp || timestampDiffMs > toleranceMs || nearest && timestampDiffMs >= nearest.timestampDiffMs) {
272
+ continue;
273
+ }
274
+ nearest = {
275
+ status: run.status,
276
+ reason: run.reason,
277
+ timestamp: candidateTimestamp,
278
+ timestampDiffMs,
279
+ source: "replay_scope_compact"
280
+ };
281
+ }
282
+ }
283
+ return nearest;
284
+ };
285
+ var hasRuntimeLineageCoverage = ({
286
+ entry,
287
+ lineageScopes
288
+ }) => {
289
+ const strategy = getEntryStrategy(entry);
290
+ const timestamp = entry.signalTimestamp ?? entry.timestamp;
291
+ return lineageScopes.some(
292
+ (scope) => scope.strategy === strategy && scope.symbol === entry.symbol && timestamp >= scope.firstTimestamp && timestamp <= scope.lastTimestamp
293
+ );
294
+ };
211
295
  var classifySignalDiagnostic = (signal) => {
212
296
  if (signal.orderStatus === "failed") {
213
297
  return {
@@ -452,6 +536,8 @@ var buildReplayMismatchDrilldown = ({
452
536
  runtimeSignalEvaluations = [],
453
537
  replaySignals = [],
454
538
  replaySignalEvaluations = [],
539
+ replayLineageScopes = [],
540
+ runtimeLineageScopes = [],
455
541
  toleranceMs,
456
542
  backtestTimestampOffsetMs,
457
543
  limit
@@ -477,7 +563,16 @@ var buildReplayMismatchDrilldown = ({
477
563
  toleranceMs,
478
564
  timestampOffsetMs: backtestTimestampOffsetMs
479
565
  });
480
- const classification = nearestReplaySignal ? classifySignalDiagnostic(nearestReplaySignal.signal) : nearestReplayEvaluation ? classifyEvaluationDiagnostic(nearestReplayEvaluation.evaluation) : nearestCandidate?.nearest ? {
566
+ const nearestReplayEvaluationOutcome = findNearestReplayEvaluationOutcome({
567
+ entry,
568
+ lineageScopes: replayLineageScopes,
569
+ toleranceMs,
570
+ timestampOffsetMs: backtestTimestampOffsetMs
571
+ });
572
+ const classification = nearestReplaySignal ? classifySignalDiagnostic(nearestReplaySignal.signal) : nearestReplayEvaluation ? classifyEvaluationDiagnostic(nearestReplayEvaluation.evaluation) : nearestReplayEvaluationOutcome ? {
573
+ classification: "core_skipped",
574
+ reason: nearestReplayEvaluationOutcome.reason
575
+ } : nearestCandidate?.nearest ? {
481
576
  classification: "timing_or_price_drift",
482
577
  reason: nearestCandidate.reason
483
578
  } : {
@@ -501,7 +596,8 @@ var buildReplayMismatchDrilldown = ({
501
596
  timestampDiffMs: nearestReplayEvaluation.timestampDiffMs
502
597
  }),
503
598
  ...includeArtifacts ? { replayEvaluationArtifact: nearestReplayEvaluation.evaluation } : {}
504
- } : {}
599
+ } : {},
600
+ ...nearestReplayEvaluationOutcome ? { replayEvaluationOutcome: nearestReplayEvaluationOutcome } : {}
505
601
  };
506
602
  });
507
603
  const backtestOnlyDiagnostics = backtestOnly.map((entry, index) => {
@@ -519,9 +615,26 @@ var buildReplayMismatchDrilldown = ({
519
615
  toleranceMs,
520
616
  timestampOffsetMs: 0
521
617
  });
522
- const classification = nearestRuntimeSignal ? classifySignalDiagnostic(nearestRuntimeSignal.signal) : nearestRuntimeEvaluation ? classifyEvaluationDiagnostic(nearestRuntimeEvaluation.evaluation) : nearestCandidate?.nearest ? {
618
+ const nearestRuntimeEvaluationOutcome = findNearestRuntimeEvaluationOutcome(
619
+ {
620
+ entry,
621
+ lineageScopes: runtimeLineageScopes,
622
+ toleranceMs
623
+ }
624
+ );
625
+ const runtimeLineageCoversEntry = hasRuntimeLineageCoverage({
626
+ entry,
627
+ lineageScopes: runtimeLineageScopes
628
+ });
629
+ const classification = nearestRuntimeSignal ? classifySignalDiagnostic(nearestRuntimeSignal.signal) : nearestRuntimeEvaluation ? classifyEvaluationDiagnostic(nearestRuntimeEvaluation.evaluation) : nearestRuntimeEvaluationOutcome ? {
630
+ classification: "core_skipped",
631
+ reason: nearestRuntimeEvaluationOutcome.reason
632
+ } : nearestCandidate?.nearest ? {
523
633
  classification: "timing_or_price_drift",
524
634
  reason: nearestCandidate.reason
635
+ } : runtimeLineageCoversEntry ? {
636
+ classification: "runtime_evaluation_detail_unavailable",
637
+ reason: "runtime_scope_covers_timestamp_but_skip_detail_was_not_stored"
525
638
  } : {
526
639
  classification: "no_runtime_evaluation",
527
640
  reason: "no_runtime_signal_or_evaluation"
@@ -545,7 +658,8 @@ var buildReplayMismatchDrilldown = ({
545
658
  ...includeArtifacts ? {
546
659
  runtimeEvaluationArtifact: nearestRuntimeEvaluation.evaluation
547
660
  } : {}
548
- } : {}
661
+ } : {},
662
+ ...nearestRuntimeEvaluationOutcome ? { runtimeEvaluationOutcome: nearestRuntimeEvaluationOutcome } : {}
549
663
  };
550
664
  });
551
665
  for (const diagnostic of runtimeOnlyDiagnostics) {
@@ -592,7 +706,9 @@ var buildReplayRuntimeComparisonDetails = ({
592
706
  runtimeSignals,
593
707
  runtimeSignalEvaluations,
594
708
  replaySignals,
595
- replaySignalEvaluations
709
+ replaySignalEvaluations,
710
+ replayLineageScopes,
711
+ runtimeLineageScopes
596
712
  }) => {
597
713
  const runtimeDetails = runtimeEntries.map(toRuntimeParityDetail);
598
714
  const backtestDetails = backtestEntries.map(
@@ -651,6 +767,8 @@ var buildReplayRuntimeComparisonDetails = ({
651
767
  runtimeSignalEvaluations,
652
768
  replaySignals,
653
769
  replaySignalEvaluations,
770
+ replayLineageScopes,
771
+ runtimeLineageScopes,
654
772
  toleranceMs,
655
773
  backtestTimestampOffsetMs,
656
774
  limit
@@ -671,7 +789,9 @@ var buildReplayExchangeComparisonDetails = ({
671
789
  runtimeSignals,
672
790
  runtimeSignalEvaluations,
673
791
  replaySignals,
674
- replaySignalEvaluations
792
+ replaySignalEvaluations,
793
+ replayLineageScopes,
794
+ runtimeLineageScopes
675
795
  }) => {
676
796
  const toExchangeDetail = (entry) => toExchangeParityDetail({
677
797
  entry,
@@ -758,6 +878,8 @@ var buildReplayExchangeComparisonDetails = ({
758
878
  runtimeSignalEvaluations,
759
879
  replaySignals,
760
880
  replaySignalEvaluations,
881
+ replayLineageScopes,
882
+ runtimeLineageScopes,
761
883
  toleranceMs,
762
884
  backtestTimestampOffsetMs,
763
885
  limit
@@ -426,6 +426,20 @@ var countBy = (items, getter) => {
426
426
  Object.entries(counts).sort(([left], [right]) => left.localeCompare(right))
427
427
  );
428
428
  };
429
+ var resolveGateDecision = (evaluation) => {
430
+ const analysis = asRecord(evaluation.aiAnalysis);
431
+ const explicit = finiteString(analysis?.gateDecision);
432
+ if (explicit === "approved" || explicit === "rejected") {
433
+ return { decision: explicit, source: "gate_decision" };
434
+ }
435
+ if (typeof analysis?.approved === "boolean") {
436
+ return {
437
+ decision: analysis.approved ? "approved" : "rejected",
438
+ source: "legacy_approved_fallback"
439
+ };
440
+ }
441
+ return null;
442
+ };
429
443
  var buildDistributions = ({
430
444
  evaluations,
431
445
  signals
@@ -606,18 +620,15 @@ var buildRuntimeScorecard = ({
606
620
  const signalEvaluations = rows.evaluations.filter(
607
621
  (evaluation) => evaluation.status === "signal"
608
622
  );
609
- const gateDecisions = signalEvaluations.map((evaluation) => ({
610
- evaluation,
611
- decision: finiteString(asRecord(evaluation.aiAnalysis)?.gateDecision)
612
- })).filter(
613
- ({ decision }) => decision === "approved" || decision === "rejected"
623
+ const gateDecisions = signalEvaluations.map(resolveGateDecision).filter(
624
+ (decision) => Boolean(decision)
614
625
  );
615
626
  const llmEligibleEvaluations = llmComparatorPolicy === "disabled" ? [] : llmComparatorPolicy === "ai_approved_only" ? signalEvaluations.filter(
616
- (evaluation) => finiteString(asRecord(evaluation.aiAnalysis)?.gateDecision) === "approved"
627
+ (evaluation) => resolveGateDecision(evaluation)?.decision === "approved"
617
628
  ) : signalEvaluations;
618
629
  const gateComparisons = llmEligibleEvaluations.map((evaluation) => {
619
630
  const analysis = asRecord(evaluation.aiAnalysis);
620
- const gateDecision = finiteString(analysis?.gateDecision);
631
+ const gateDecision = resolveGateDecision(evaluation)?.decision ?? null;
621
632
  const llmDecision = finiteString(analysis?.llmDecision);
622
633
  if (gateDecision !== "approved" && gateDecision !== "rejected" || llmDecision !== "approved" && llmDecision !== "rejected") {
623
634
  return null;
@@ -652,6 +663,27 @@ var buildRuntimeScorecard = ({
652
663
  };
653
664
  const actualSlippageBps = averageSampleMetric("signalToFillAdverseBps");
654
665
  const residualVsModelBps = averageSampleMetric("residualVsCurrentModelBps");
666
+ const signalToArrivalAdverseBps = averageSampleMetric(
667
+ "signalToArrivalAdverseBps"
668
+ );
669
+ const arrivalToFillAdverseBps = averageSampleMetric(
670
+ "arrivalToFillAdverseBps"
671
+ );
672
+ const averageSignalCloseToSubmitMs = averageSampleMetric(
673
+ "signalCloseToSubmitMs"
674
+ );
675
+ const averageSubmitToFillMs = averageSampleMetric("submitToFillMs");
676
+ const fullTelemetryTrades = calibrationSamples.filter(
677
+ (sample) => finiteString(sample.telemetryQuality) === "full"
678
+ ).length;
679
+ const arrivalQuoteTrades = calibrationSamples.filter(
680
+ (sample) => finiteNumber(sample.arrivalMid) != null
681
+ ).length;
682
+ const arrivalQuoteCoverage = calibrationSamples.length ? round(arrivalQuoteTrades / calibrationSamples.length) : null;
683
+ const hasTelemetryQuality = calibrationSamples.some(
684
+ (sample) => finiteString(sample.telemetryQuality) != null
685
+ );
686
+ const maximumSignalCloseToSubmitMs = thresholds.maximumSignalCloseToSubmitMs ?? 3e4;
655
687
  const prospective = getProspectiveSummary(prospectiveEvidenceArtifact);
656
688
  const historyTrades = dedupeTrades([
657
689
  ...scopedHistoryArtifacts,
@@ -708,7 +740,28 @@ var buildRuntimeScorecard = ({
708
740
  reactions.push({
709
741
  code: "SLIPPAGE_DRIFT",
710
742
  severity: "warning",
711
- message: `Execution residual ${round(residualVsModelBps, 2)} bps exceeds ${thresholds.maximumSlippageResidualBps} bps.`
743
+ message: `Signal-close-to-fill residual ${round(residualVsModelBps, 2)} bps exceeds ${thresholds.maximumSlippageResidualBps} bps.`
744
+ });
745
+ }
746
+ if (averageSignalCloseToSubmitMs != null && averageSignalCloseToSubmitMs > maximumSignalCloseToSubmitMs) {
747
+ reactions.push({
748
+ code: "PRE_SUBMIT_LATENCY",
749
+ severity: "warning",
750
+ message: `Average signal-close-to-submit latency ${round(averageSignalCloseToSubmitMs, 3)} ms exceeds ${maximumSignalCloseToSubmitMs} ms.`
751
+ });
752
+ }
753
+ if (hasTelemetryQuality && calibrationSamples.length > 0 && fullTelemetryTrades < calibrationSamples.length) {
754
+ reactions.push({
755
+ code: "EXECUTION_TELEMETRY_INCOMPLETE",
756
+ severity: "info",
757
+ message: `Full arrival and fill telemetry is available for ${fullTelemetryTrades}/${calibrationSamples.length} trades.`
758
+ });
759
+ }
760
+ if (riskDecisions.some((decision) => decision.status === "rejected")) {
761
+ reactions.push({
762
+ code: "RISK_PLAN_EXCEEDS_DECLARED_LIMIT",
763
+ severity: "warning",
764
+ message: "At least one observed order plan exceeds its declared MAX_LOSS_VALUE; telemetry is not yet enforced."
712
765
  });
713
766
  }
714
767
  if (sevenDay.closedTrades >= thresholds.minimumClosedTrades && sevenDay.expectancy != null && sevenDay.expectancy < thresholds.minimumExpectancy) {
@@ -737,7 +790,10 @@ var buildRuntimeScorecard = ({
737
790
  deployment: deploymentBinding,
738
791
  lineage: runtimeLineage,
739
792
  promotionStatus,
740
- thresholds,
793
+ thresholds: {
794
+ ...thresholds,
795
+ maximumSignalCloseToSubmitMs
796
+ },
741
797
  funnel: {
742
798
  evaluations: rows.evaluations.length,
743
799
  coreCandidates: signalEvaluations.length,
@@ -749,25 +805,38 @@ var buildRuntimeScorecard = ({
749
805
  rejected: gateDecisions.filter(
750
806
  ({ decision }) => decision === "rejected"
751
807
  ).length,
752
- coverage: signalEvaluations.length ? round(gateDecisions.length / signalEvaluations.length) : null
808
+ coverage: signalEvaluations.length ? round(gateDecisions.length / signalEvaluations.length) : null,
809
+ legacyApprovedFallback: gateDecisions.filter(
810
+ ({ source }) => source === "legacy_approved_fallback"
811
+ ).length
753
812
  },
754
813
  allocator: {
755
814
  available: allocatorDecisions.length > 0,
756
815
  approved: allocatorDecisions.filter(
757
- (decision) => decision.decision === "approved"
816
+ (decision) => decision.status === "approved"
758
817
  ).length,
759
818
  rejected: allocatorDecisions.filter(
760
- (decision) => decision.decision === "rejected"
819
+ (decision) => decision.status === "rejected"
820
+ ).length,
821
+ notApplicable: allocatorDecisions.filter(
822
+ (decision) => decision.status === "not_applicable"
823
+ ).length,
824
+ unavailable: allocatorDecisions.filter(
825
+ (decision) => decision.status === "unavailable"
761
826
  ).length
762
827
  },
763
828
  risk: {
764
829
  available: riskDecisions.length > 0,
765
830
  approved: riskDecisions.filter(
766
- (decision) => decision.decision === "approved"
831
+ (decision) => decision.status === "approved"
767
832
  ).length,
768
833
  rejected: riskDecisions.filter(
769
- (decision) => decision.decision === "rejected"
770
- ).length
834
+ (decision) => decision.status === "rejected"
835
+ ).length,
836
+ unavailable: riskDecisions.filter(
837
+ (decision) => decision.status === "unavailable"
838
+ ).length,
839
+ enforced: riskDecisions.some((decision) => decision.enforced === true)
771
840
  },
772
841
  orderAttempts: orderAttempts.length,
773
842
  orderFailures: orderAttempts.filter(
@@ -829,8 +898,16 @@ var buildRuntimeScorecard = ({
829
898
  },
830
899
  execution: {
831
900
  available: actualSlippageBps != null || residualVsModelBps != null,
901
+ metricScope: arrivalToFillAdverseBps == null ? "signal_close_to_fill" : "signal_and_exchange_components",
902
+ samples: calibrationSamples.length,
903
+ fullTelemetryTrades,
904
+ arrivalQuoteCoverage,
832
905
  actualSignalToFillSlippageBps: actualSlippageBps,
833
- residualVsCurrentModelBps: residualVsModelBps
906
+ residualVsCurrentModelBps: residualVsModelBps,
907
+ signalToArrivalAdverseBps,
908
+ arrivalToFillAdverseBps,
909
+ averageSignalCloseToSubmitMs,
910
+ averageSubmitToFillMs
834
911
  },
835
912
  prospective,
836
913
  rolling,
@@ -884,10 +961,10 @@ Window: ${new Date(scorecard.window.startTime).toISOString()} \u2014 ${new Date(
884
961
 
885
962
  - Evaluations: ${scorecard.funnel.evaluations}
886
963
  - Core candidates: ${scorecard.funnel.coreCandidates}
887
- - Gate approvals/rejects: ${scorecard.funnel.gate.approved}/${scorecard.funnel.gate.rejected}
964
+ - Gate approvals/rejects: ${scorecard.funnel.gate.approved}/${scorecard.funnel.gate.rejected} (legacy fallback: ${scorecard.funnel.gate.legacyApprovedFallback})
888
965
  - AI / LLM disagreement: ${scorecard.gateComparison.disagreements}/${scorecard.gateComparison.compared}
889
- - Allocator approvals/rejects: ${scorecard.funnel.allocator.available ? `${scorecard.funnel.allocator.approved}/${scorecard.funnel.allocator.rejected}` : "unavailable"}
890
- - Risk approvals/rejects: ${scorecard.funnel.risk.available ? `${scorecard.funnel.risk.approved}/${scorecard.funnel.risk.rejected}` : "unavailable"}
966
+ - Runtime admission approved/rejected/not-applicable: ${scorecard.funnel.allocator.available ? `${scorecard.funnel.allocator.approved}/${scorecard.funnel.allocator.rejected}/${scorecard.funnel.allocator.notApplicable}` : "unavailable"}
967
+ - 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})
891
968
  - Order attempts/failures: ${scorecard.funnel.orderAttempts}/${scorecard.funnel.orderFailures}
892
969
  - Fills: ${scorecard.funnel.fills}
893
970
  - Closed trades: ${scorecard.funnel.closedTrades}
@@ -896,8 +973,14 @@ Window: ${new Date(scorecard.window.startTime).toISOString()} \u2014 ${new Date(
896
973
 
897
974
  - Parity: ${scorecard.parity.ratio == null ? "n/a" : `${round(scorecard.parity.ratio * 100, 2)}%`}
898
975
  - Runtime/replay mismatches: ${scorecard.parity.backtestOnly + scorecard.parity.runtimeOnly}
899
- - Actual signal-to-fill slippage: ${scorecard.execution.actualSignalToFillSlippageBps ?? "n/a"} bps
976
+ - Execution metric scope: ${scorecard.execution.metricScope}
977
+ - Telemetry samples/full/arrival coverage: ${scorecard.execution.samples}/${scorecard.execution.fullTelemetryTrades}/${scorecard.execution.arrivalQuoteCoverage == null ? "n/a" : `${round(scorecard.execution.arrivalQuoteCoverage * 100, 2)}%`}
978
+ - Signal-close-to-fill adverse move: ${scorecard.execution.actualSignalToFillSlippageBps ?? "n/a"} bps
979
+ - Signal-to-arrival adverse move: ${scorecard.execution.signalToArrivalAdverseBps ?? "n/a"} bps
980
+ - Arrival-to-fill exchange slippage: ${scorecard.execution.arrivalToFillAdverseBps ?? "n/a"} bps
900
981
  - Residual vs current model: ${scorecard.execution.residualVsCurrentModelBps ?? "n/a"} bps
982
+ - Average signal-close-to-submit: ${scorecard.execution.averageSignalCloseToSubmitMs ?? "n/a"} ms
983
+ - Average submit-to-fill: ${scorecard.execution.averageSubmitToFillMs ?? "n/a"} ms
901
984
 
902
985
  ## Rolling performance
903
986
 
@@ -32,6 +32,7 @@ var import_redis = require("@tradejs/infra/redis");
32
32
 
33
33
  // src/lib/runtimeSignalsStorage.ts
34
34
  var import_constants = require("@tradejs/core/constants");
35
+ var import_data = require("@tradejs/core/data");
35
36
  var import_time = require("@tradejs/core/time");
36
37
  var isRuntimeSignalBucketRef = (value) => {
37
38
  if (!value || typeof value !== "object") {
@@ -23,6 +23,7 @@ __export(runtimeSignalsStorage_exports, {
23
23
  DEFAULT_RUNTIME_SIGNAL_RETENTION_TTL_SECONDS: () => DEFAULT_RUNTIME_SIGNAL_RETENTION_TTL_SECONDS,
24
24
  RUNTIME_LINEAGE_SCOPE_RETENTION_TTL_SECONDS: () => RUNTIME_LINEAGE_SCOPE_RETENTION_TTL_SECONDS,
25
25
  RUNTIME_SIGNAL_RETENTION_DAYS_ENV: () => RUNTIME_SIGNAL_RETENTION_DAYS_ENV,
26
+ appendRuntimeSignalEvaluationRun: () => appendRuntimeSignalEvaluationRun,
26
27
  buildRuntimeSignalStatsIncrements: () => buildRuntimeSignalStatsIncrements,
27
28
  getRuntimeSignalRetentionTtlSeconds: () => getRuntimeSignalRetentionTtlSeconds,
28
29
  getRuntimeStorageDayKey: () => import_time.getRuntimeStorageDayKey,
@@ -37,7 +38,38 @@ __export(runtimeSignalsStorage_exports, {
37
38
  });
38
39
  module.exports = __toCommonJS(runtimeSignalsStorage_exports);
39
40
  var import_constants = require("@tradejs/core/constants");
41
+ var import_data = require("@tradejs/core/data");
40
42
  var import_time = require("@tradejs/core/time");
43
+ var appendRuntimeSignalEvaluationRun = (runs = [], evaluation) => {
44
+ if (evaluation.status !== "skip") {
45
+ return [...runs];
46
+ }
47
+ const stepMs = (0, import_data.intervalToMs)(evaluation.interval);
48
+ const reason = evaluation.reason?.trim() || "NO_SIGNAL";
49
+ const existing = runs.find(
50
+ (run) => run.status === "skip" && run.reason === reason && run.stepMs === stepMs && evaluation.timestamp >= run.firstTimestamp && evaluation.timestamp <= run.lastTimestamp && (evaluation.timestamp - run.firstTimestamp) % stepMs === 0
51
+ );
52
+ if (existing) {
53
+ return [...runs];
54
+ }
55
+ const last = runs.at(-1);
56
+ if (last?.status === "skip" && last.reason === reason && last.stepMs === stepMs && evaluation.timestamp === last.lastTimestamp + stepMs) {
57
+ return [
58
+ ...runs.slice(0, -1),
59
+ { ...last, lastTimestamp: evaluation.timestamp }
60
+ ];
61
+ }
62
+ return [
63
+ ...runs,
64
+ {
65
+ status: "skip",
66
+ reason,
67
+ firstTimestamp: evaluation.timestamp,
68
+ lastTimestamp: evaluation.timestamp,
69
+ stepMs
70
+ }
71
+ ];
72
+ };
41
73
  var SECONDS_PER_DAY = 86400;
42
74
  var RUNTIME_SIGNAL_RETENTION_DAYS_ENV = "RUNTIME_SIGNAL_RETENTION_DAYS";
43
75
  var DEFAULT_RUNTIME_SIGNAL_RETENTION_TTL_SECONDS = import_constants.TTL_3D;
@@ -175,6 +207,7 @@ var parseRuntimeSignalStatsBucket = (value) => {
175
207
  DEFAULT_RUNTIME_SIGNAL_RETENTION_TTL_SECONDS,
176
208
  RUNTIME_LINEAGE_SCOPE_RETENTION_TTL_SECONDS,
177
209
  RUNTIME_SIGNAL_RETENTION_DAYS_ENV,
210
+ appendRuntimeSignalEvaluationRun,
178
211
  buildRuntimeSignalStatsIncrements,
179
212
  getRuntimeSignalRetentionTtlSeconds,
180
213
  getRuntimeStorageDayKey,