All files / calibrate joint.ts

100% Statements 36/36
100% Branches 12/12
100% Functions 3/3
100% Lines 36/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53            1x       1x 18x 18x 18x   18x 18x 18x   18x 33x 33x 33x 33x 33x 31x 2x   33x 27x 6x   33x 33x   33x 33x 33x 33x 33x   33x 33x   33x 33x 33x 33x 33x 33x 33x 18x  
/**
 * JointConfidenceCalibrator — Bayesian fusion of detection + forecast signals.
 *
 * jointConfidence = α·grade + β·(1 - spread) + γ·hitRate + δ·driftPenalty
 */
import type { ICalibrator, DetectionResult, ForecastResult, DataPoint, CalibrationResult, CalibrationMode } from '../types.js'
import { HitRateTracker, clamp } from './forecast-guided.js'
 
export interface JointWeights { grade: number; spread: number; hitRate: number; drift: number }
 
export class JointConfidenceCalibrator implements ICalibrator {
  readonly mode: CalibrationMode = 'joint'
  private weights: JointWeights
  private hitTracker = new HitRateTracker()
 
  constructor(weights?: Partial<JointWeights>) {
    this.weights = { grade: 0.4, spread: 0.3, hitRate: 0.2, drift: 0.1, ...weights }
  }
 
  calibrate(detection: DetectionResult, forecast: ForecastResult, currentPoint: DataPoint): CalibrationResult {
    const predicted = forecast.predicted[0] ?? currentPoint.value
    const q10 = forecast.q10[0] ?? predicted
    const q90 = forecast.q90[0] ?? predicted
    const spread = q90 - q10
    const normalizedSpread = Math.abs(currentPoint.value) > 1e-10
      ? Math.min(spread / Math.abs(currentPoint.value), 1)
      : 0
 
    const residual = spread > 1e-10
      ? Math.abs(currentPoint.value - predicted) / (spread / 2)
      : Math.abs(currentPoint.value - predicted)
 
    const hitRate = this.hitTracker.getHitRate()
    const driftPenalty = detection.driftDetected ? -1 : 0
 
    const joint =
      this.weights.grade * detection.grade +
      this.weights.spread * (1 - normalizedSpread) +
      this.weights.hitRate * hitRate +
      this.weights.drift * driftPenalty
 
    const jointConfidence = clamp(joint, 0, 1)
    this.hitTracker.record(jointConfidence > 0.7, jointConfidence)
 
    return {
      mode: 'joint',
      residual,
      jointConfidence,
      intervalBreached: residual > 1,
    }
  }
}