Skip to main content

Overview

The PredictionEngine class is the top-level orchestrator that combines:
  1. EWMA Volatility — per-second volatility estimation
  2. Black-Scholes — base probability from geometric Brownian motion
  3. Momentum Analyzer — ROC signals across multiple windows
  4. Mean Reversion — deviation from 2-minute SMA
  5. Platt Calibration — post-hoc probability recalibration
It produces a final probability estimate or abstains when conditions are unfavorable.

API Reference

Constructor

import { PredictionEngine } from './engine/predictor.js'
import config from './config.js'

const engine = new PredictionEngine()
Internal state initialization:
  • Creates EWMAVolatility instance with λ from config
  • Creates MomentumAnalyzer with buffer size from config
  • Initializes PlattScaler (starts unfitted)
  • Allocates rolling outcome buffer for cold-streak detection

feedTick()

Ingest a price tick into both the volatility estimator and momentum analyzer.
engine.feedTick({ timestamp, price })
Parameters:
ParameterTypeDescription
timestampnumberTick timestamp in milliseconds (Unix epoch)
pricenumberCurrent market price (e.g., 0.52 for 52%)
Returns: void Example:
const tick = {
  timestamp: Date.now(),
  price: 0.5234
}
engine.feedTick(tick)
Call this method for every price tick received from the market. The more ticks, the better the volatility and momentum estimates.

predict()

Produce a probability prediction for a binary market outcome.
const result = engine.predict({
  currentPrice,
  strikePrice,
  timeRemainingSeconds
})
Parameters:
ParameterTypeDescription
currentPricenumberLatest market price
strikePricenumberTarget threshold (“Will price exceed this?”)
timeRemainingSecondsnumberSeconds until market close
Returns:
type PredictionResult = 
  | {
      probability: number       // Calibrated probability [0.01, 0.99]
      direction: 'UP' | 'DOWN'  // Predicted direction
      volatility: number        // Current per-second volatility
      momentum: number          // Combined ROC signal (raw)
      reversion: number         // Mean-reversion signal (raw)
      calibrated: boolean       // Whether Platt calibration was applied
      abstained?: false
    }
  | {
      abstained: true
      reason: string            // Abstention reason
      volatility?: number
      probability?: number      // Partial result (if available)
      direction?: 'UP' | 'DOWN'
    }
Abstention Reasons:
ReasonTrigger
insufficient_dataσ = 0 or ticks < minTicks
dead_zone|p - 0.5| < deadZone
anomalous_regimeσ > sigmaMultiplier × mean(σ)
cold_streakRecent accuracy < minAccuracy
Example:
const result = engine.predict({
  currentPrice: 0.52,
  strikePrice: 0.55,
  timeRemainingSeconds: 300  // 5 minutes
})

if (result.abstained) {
  console.log(`Abstained: ${result.reason}`)
} else {
  console.log(`Prediction: ${result.direction} at ${(result.probability * 100).toFixed(2)}%`)
  console.log(`Volatility: ${result.volatility.toFixed(6)}, Calibrated: ${result.calibrated}`)
}

recordOutcome()

Record the outcome of a prediction for cold-streak tracking and Platt calibration.
engine.recordOutcome(correct, predictedProb)
Parameters:
ParameterTypeDescription
correctbooleanWas the prediction correct?
predictedProbnumber?The probability that was predicted (optional)
Returns: void Example:
// After market closes
const prediction = engine.predict({...})
if (!prediction.abstained) {
  // ... execute trade ...
  
  // Later, record outcome
  const actualResult = 'UP'  // from market API
  const correct = (prediction.direction === actualResult)
  engine.recordOutcome(correct, prediction.probability)
}
Critical for Calibration: If you never call recordOutcome(), the Platt scaler will never accumulate data and calibration will never activate.

getRecentAccuracy()

Get accuracy over the most recent outcomes window.
const accuracy = engine.getRecentAccuracy()
Returns: number — accuracy in [0, 1], or 1 if no outcomes recorded. Example:
if (engine.getRecentAccuracy() < 0.5) {
  console.warn('Model is in a cold streak — predictions may be unreliable')
}

reset()

Reset all internal state (volatility + momentum + calibration).
engine.reset()
Returns: void Use cases:
  • Market close / start of new trading session
  • Context switch (different market or asset)
  • Emergency flush (e.g., detected data corruption)
Destructive Operation: This clears all accumulated data, including EWMA volatility history and Platt calibration. The engine will need to re-warm-up.

resetMomentum()

Reset only the momentum analyzer, preserving volatility and calibration state.
engine.resetMomentum()
Returns: void Use cases:
  • Start of a new prediction interval (volatility should persist)
  • Clear momentum signals without disrupting long-term volatility estimate
Example:
// At the start of each new market interval
engine.resetMomentum()
Typical Pattern: Call resetMomentum() at interval boundaries, but only call reset() at market close or when switching contexts.

Prediction Pipeline

Step-by-Step Flow

Code Walkthrough

predictor.js
predict({ currentPrice, strikePrice, timeRemainingSeconds }) {
  const sigma = this._volatility.getVolatility()
  const abstentionCfg = config.engine.abstention

  // ── Abstention condition 1: insufficient data ──
  if (sigma === 0 || this._volatility.getTickCount() < abstentionCfg.minTicks) {
    return { abstained: true, reason: 'insufficient_data', volatility: sigma }
  }

  // ── Base probability from Black-Scholes ──
  const baseProb = binaryCallProbability({
    currentPrice,
    strikePrice,
    volatility: sigma,
    timeRemainingSeconds,
    riskFreeRate: 0,
  })

  // ── Abstention condition 2: dead zone ──
  if (Math.abs(baseProb - 0.5) < abstentionCfg.deadZone) {
    return { abstained: true, reason: 'dead_zone', volatility: sigma, probability: baseProb }
  }

  // ── Abstention condition 3: anomalous regime ──
  const meanSigma = this._volatility.getMeanSigma()
  if (meanSigma > 0 && sigma > abstentionCfg.sigmaMultiplier * meanSigma) {
    return { abstained: true, reason: 'anomalous_regime', volatility: sigma, probability: baseProb }
  }

  // ── Abstention condition 4: cold streak ──
  const window = abstentionCfg.minAccuracyWindow
  if (this._recentOutcomes.length >= window && this.getRecentAccuracy() < abstentionCfg.minAccuracy) {
    return { abstained: true, reason: 'cold_streak', volatility: sigma, probability: baseProb }
  }

  // ── Momentum and reversion factors ──
  const { combined: momentumFactor } = this._momentum.getMomentum()
  const { signal: reversionFactor } = this._momentum.getMeanReversion()

  // ── Near-expiry guard: skip adjustments when <= 5 seconds remain ──
  let finalProb
  if (timeRemainingSeconds <= config.engine.prediction.nearExpiryGuardSec) {
    finalProb = baseProb
  } else {
    // Combine in logit space
    const logitBase = logit(baseProb)
    const logitAdj = logitBase
      + config.engine.prediction.logitMomentumWeight * momentumFactor
      + config.engine.prediction.logitReversionWeight * reversionFactor
    finalProb = sigmoid(logitAdj)
  }

  // ── Safety clamp to [0.01, 0.99] ──
  finalProb = clamp(finalProb, 0.01, 0.99)

  // ── Platt calibration (auto-activates at 200+ samples) ──
  let calibrated = false
  if (this._scaler.canFit()) {
    if (!this._scaler.getStats().fitted) {
      this._scaler.fit()
    }
    finalProb = this._scaler.calibrate(finalProb)
    finalProb = clamp(finalProb, 0.01, 0.99)
    calibrated = true
  }

  const direction = finalProb >= 0.5 ? 'UP' : 'DOWN'
  this._lastPrediction = finalProb

  return {
    probability: finalProb,
    direction,
    volatility: sigma,
    momentum: momentumFactor,
    reversion: reversionFactor,
    calibrated,
  }
}

Configuration

Key engine parameters from config.js:
engine: {
  ewma: {
    lambda: 0.94  // EWMA decay factor
  },
  momentum: {
    bufferSize: 300  // Max ticks retained
  },
  prediction: {
    logitMomentumWeight: 2.0,     // Momentum signal weight (logit space)
    logitReversionWeight: 1.5,    // Reversion signal weight (logit space)
    nearExpiryGuardSec: 5         // Disable adjustments when t ≤ 5s
  },
  abstention: {
    minTicks: 5,                  // Minimum ticks before predicting
    deadZone: 0.05,               // ±5% around 0.5
    sigmaMultiplier: 3.0,         // Anomaly threshold (3× mean volatility)
    minAccuracyWindow: 20,        // Rolling accuracy window size
    minAccuracy: 0.50             // Cold-streak threshold
  }
}

Usage Example

Complete Workflow

import { PredictionEngine } from './engine/predictor.js'

const engine = new PredictionEngine()

// ── Phase 1: Data Collection ──
// Stream price ticks (e.g., from WebSocket)
marketStream.on('tick', (tick) => {
  engine.feedTick({ timestamp: tick.time, price: tick.price })
})

// ── Phase 2: Prediction ──
setInterval(() => {
  const result = engine.predict({
    currentPrice: marketStream.getLatestPrice(),
    strikePrice: 0.55,
    timeRemainingSeconds: marketStream.getTimeRemaining()
  })

  if (result.abstained) {
    console.log(`[ABSTAIN] ${result.reason}`)
    return
  }

  console.log(`[PREDICT] ${result.direction} @ ${(result.probability * 100).toFixed(2)}%`)
  console.log(`  Volatility: ${result.volatility.toFixed(6)}`)  
  console.log(`  Momentum: ${result.momentum.toFixed(4)}, Reversion: ${result.reversion.toFixed(4)}`)
  console.log(`  Calibrated: ${result.calibrated}`)

  // Decide whether to trade based on EV, Kelly, etc.
  const shouldTrade = evaluateTrade(result)
  if (shouldTrade) {
    executeTrade(result.direction, result.probability)
  }
}, 10_000)  // Every 10 seconds

// ── Phase 3: Outcome Recording ──
marketStream.on('close', (outcome) => {
  const lastResult = getLastPrediction()
  if (lastResult && !lastResult.abstained) {
    const correct = (lastResult.direction === outcome)
    engine.recordOutcome(correct, lastResult.probability)
  }

  // Reset momentum for next interval
  engine.resetMomentum()
})

// ── Phase 4: Session End ──
process.on('SIGINT', () => {
  engine.reset()  // Full reset on shutdown
  process.exit(0)
})

Advanced Topics

Custom Abstention Filters

You can implement additional abstention logic after calling predict():
const result = engine.predict({...})

if (!result.abstained) {
  // Custom filter: refuse to predict if market is too illiquid
  if (marketDepth < MIN_DEPTH) {
    result.abstained = true
    result.reason = 'insufficient_liquidity'
  }

  // Custom filter: refuse if spread is too wide
  if (marketSpread > MAX_SPREAD) {
    result.abstained = true
    result.reason = 'wide_spread'
  }
}

Multi-Model Ensemble

You can run multiple prediction engines with different configurations and ensemble their outputs:
const engineA = new PredictionEngine()  // Conservative (λ=0.97)
const engineB = new PredictionEngine()  // Aggressive (λ=0.90)

const resultA = engineA.predict({...})
const resultB = engineB.predict({...})

if (!resultA.abstained && !resultB.abstained) {
  // Weighted average in logit space
  const logitA = logit(resultA.probability)
  const logitB = logit(resultB.probability)
  const ensembleLogit = 0.6 * logitA + 0.4 * logitB
  const ensembleProb = sigmoid(ensembleLogit)
  
  console.log(`Ensemble: ${(ensembleProb * 100).toFixed(2)}%`)
}

Backtesting Integration

For backtesting, disable real-time dependencies and feed historical ticks:
const engine = new PredictionEngine()

for (const interval of historicalIntervals) {
  engine.resetMomentum()

  for (const tick of interval.ticks) {
    engine.feedTick(tick)
  }

  const result = engine.predict({
    currentPrice: interval.closePrice,
    strikePrice: interval.strikePrice,
    timeRemainingSeconds: 60
  })

  if (!result.abstained) {
    const correct = (result.direction === interval.outcome)
    engine.recordOutcome(correct, result.probability)
    
    // Log for analysis
    backtestLog.push({ interval: interval.id, result, correct })
  }
}

// Analyze backtest results
const accuracy = backtestLog.filter(x => x.correct).length / backtestLog.length
console.log(`Backtest accuracy: ${(accuracy * 100).toFixed(2)}%`)

Performance Considerations

Time Complexity

OperationComplexityNotes
feedTick()O(1)EWMA update + buffer append
predict()O(N)N = momentum buffer size (~300)
recordOutcome()O(1)Array push + Platt collect
Platt fit()O(M × I)M = samples (~200-2000), I = iterations (1000)
Typical prediction latency: ~1-2ms on modern hardware.

Memory Usage

ComponentMemory
EWMA stateO(1) + 100 × 8 bytes (sigma history) = ~1 KB
Momentum buffer300 ticks × 16 bytes = ~5 KB
Platt data2000 samples × 16 bytes = ~32 KB
Total~40 KB per engine instance
Scalability: You can run hundreds of engine instances in parallel (e.g., one per market) without memory concerns.

Error Handling

The engine is designed to be robust:
  • Invalid inputs: Clamped or defaulted (e.g., negative prices → return 0.5)
  • Edge cases: Handled gracefully (e.g., zero volatility → abstain)
  • Numerical stability: Logit/sigmoid use clamping to avoid log(0) or exp(∞)
Exception-Free Design: The engine does not throw exceptions under normal circumstances. Abstention is the primary error-signaling mechanism.

Testing

Unit tests for the prediction engine:
import { PredictionEngine } from './engine/predictor.js'
import assert from 'assert'

// Test: Abstain on insufficient data
const engine = new PredictionEngine()
const result = engine.predict({ currentPrice: 0.5, strikePrice: 0.5, timeRemainingSeconds: 60 })
assert(result.abstained === true)
assert(result.reason === 'insufficient_data')

// Test: Prediction after warm-up
for (let i = 0; i < 10; i++) {
  engine.feedTick({ timestamp: i * 1000, price: 0.5 + Math.random() * 0.01 })
}
const result2 = engine.predict({ currentPrice: 0.52, strikePrice: 0.50, timeRemainingSeconds: 60 })
assert(result2.abstained === false || result2.reason !== 'insufficient_data')

// Test: Outcome recording
engine.recordOutcome(true, 0.65)
assert(engine.getRecentAccuracy() === 1.0)

References

Next Steps

Architecture Overview

High-level system design and data flow

Configuration

Tuning engine parameters for your market

Tuning Parameters

Historical evaluation and parameter optimization

Metrics

Performance evaluation using proper scoring rules

Build docs developers (and LLMs) love