Skip to main content

Overview

The PredictionEngine is the heart of the Polymarket Bot’s prediction system. It fuses multiple quantitative signals into a single probability prediction using mathematically sound logit-space transformations. Key features:
  • Black-Scholes binary option probability calculation
  • EWMA (Exponentially Weighted Moving Average) volatility estimation
  • Multi-timeframe momentum analysis (10s, 30s, 60s)
  • Mean-reversion detection
  • Platt scaling for probability calibration
  • Intelligent abstention when conditions don’t favor prediction
Source: src/engine/predictor.js:37

Constructor

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

const engine = new PredictionEngine()
The constructor takes no parameters and initializes all internal components using values from config.engine. Internal components initialized:
  • EWMAVolatility estimator with lambda from config
  • MomentumAnalyzer with buffer size from config
  • PlattScaler for probability calibration
  • Recent outcomes tracker for cold-streak detection

Methods

feedTick()

Ingest a price tick into both the volatility estimator and momentum analyzer.
tick
object
required
The price tick to process
tick.timestamp
number
required
Unix timestamp in milliseconds
tick.price
number
required
BTC/USD price at this timestamp
returns
void
No return value. Updates internal state.
Example:
// From src/index.js:52-62
const feed = new ChainlinkFeed({
  onTick: (tick) => {
    tickLogger.write(tick)
    lastPrice = tick.price
    engine.feedTick(tick)  // Feed every price update
  },
  onStatus: (status) => {
    connectionStatus = status
  }
})
feed.start()
Side effects:
  • Updates EWMA volatility estimate
  • Adds tick to momentum analyzer’s rolling buffer
  • Maintains last 300 ticks (configurable via config.engine.momentum.bufferSize)

predict()

Produce a combined probability prediction for “Will price be above strike at expiry?” Returns either a full prediction with probability and direction, or an abstention result when the bot has no statistical edge.
params
object
required
Prediction parameters
params.currentPrice
number
required
Latest underlying BTC/USD price
params.strikePrice
number
required
Target/threshold price from Vatic API
params.timeRemainingSeconds
number
required
Seconds until 5-minute interval expiry
returns
object
Prediction result or abstention
probability
number
Model’s predicted probability of UP outcome (0.01-0.99). Present in both predictions and some abstentions.
direction
'UP' | 'DOWN'
Predicted direction based on probability >= 0.5
volatility
number
Current EWMA volatility estimate (per-second σ)
momentum
number
Combined momentum factor (weighted ROC from 10s/30s/60s windows)
reversion
number
Mean-reversion signal strength
calibrated
boolean
Whether Platt scaling was applied (true after 200+ samples)
abstained
boolean
True when prediction is withheld due to unfavorable conditions
reason
string
Abstention reason code when abstained=true:
  • insufficient_data: < minTicks received
  • dead_zone: probability too close to 0.5
  • anomalous_regime: volatility spike detected
  • cold_streak: recent accuracy below threshold
Example:
// From src/index.js:134-150
let prediction = null
let rawPrediction = null
let abstention = null

if (lastPrice && strikePrice) {
  const result = engine.predict({
    currentPrice: lastPrice,
    strikePrice,
    timeRemainingSeconds: remainingSec
  })
  
  if (result.abstained) {
    abstention = result
    if (result.probability != null) {
      rawPrediction = { 
        probability: result.probability, 
        direction: result.direction, 
        volatility: result.volatility 
      }
    }
  } else {
    prediction = result
    rawPrediction = result
  }
}
Prediction pipeline:
  1. Abstention checks (early exit if conditions not met):
    • Insufficient data: tickCount < config.engine.abstention.minTicks (default: 50)
    • Dead zone: |baseProb - 0.5| < config.engine.abstention.deadZone (default: 0.10)
    • Anomalous regime: sigma > config.engine.abstention.sigmaMultiplier * meanSigma (default: 2.0x)
    • Cold streak: recent accuracy < config.engine.abstention.minAccuracy (default: 0.40) over last 20 predictions
  2. Base probability: Black-Scholes binary call probability using current price, strike, volatility, and time remaining
  3. Signal fusion (logit-space combination):
    • Convert base probability to log-odds (logit)
    • Add weighted momentum adjustment: logitMomentumWeight * momentumFactor (weight: 150)
    • Add weighted reversion adjustment: logitReversionWeight * reversionFactor (weight: 80)
    • Convert back to probability space via sigmoid
  4. Near-expiry guard: Skip momentum/reversion adjustments when ≤ 5 seconds remain
  5. Platt calibration: Apply Platt scaling if 200+ outcomes recorded
  6. Safety clamp: Final probability clamped to [0.01, 0.99]

recordOutcome()

Record a prediction outcome for cold-streak tracking and Platt calibration. Call this method after each 5-minute interval closes to feed the engine’s learning systems.
correct
boolean
required
Whether the prediction was correct (true) or incorrect (false)
predictedProb
number
The probability that was predicted (0-1). Optional; if omitted, uses the last prediction stored internally.
returns
void
No return value. Updates internal calibration and accuracy tracking.
Example:
// From src/index.js:29-40
const tracker = new IntervalTracker({
  onIntervalClose: async (record) => {
    await store.append(record)
    
    // Feed outcome to engine
    if (record.earlyPrediction && record.earlyPredictionCorrect != null) {
      engine.recordOutcome(
        record.earlyPredictionCorrect,
        record.earlyPrediction.probability
      )
    }
  }
})
Side effects:
  • Updates rolling window of recent outcomes (last 20, configurable)
  • Feeds Platt scaler with prediction/outcome pair for calibration
  • Platt scaler auto-fits after collecting 200+ samples

getRecentAccuracy()

Calculate accuracy over the most recent prediction window.
returns
number
Accuracy ratio in [0, 1]. Returns 1.0 if no outcomes recorded yet.
Example:
const accuracy = engine.getRecentAccuracy()
console.log(`Recent accuracy: ${(accuracy * 100).toFixed(1)}%`)
Used internally during predict() for cold-streak detection (abstention condition 4).

reset()

Reset all internal state to initial conditions.
returns
void
No return value.
Side effects:
  • Resets EWMA volatility estimator
  • Clears momentum analyzer buffer
  • Clears recent outcomes history
  • Creates new Platt scaler instance
  • Clears last prediction cache
Use cases:
  • Starting a new trading session
  • Recovering from data corruption
  • Testing/development

resetMomentum()

Reset only the momentum analyzer, preserving volatility state.
returns
void
No return value.
Example:
// From src/index.js:41-44
const tracker = new IntervalTracker({
  onIntervalStart: () => {
    engine.resetMomentum()  // Clear momentum at interval boundaries
  }
})
Rationale: Volatility should persist across 5-minute interval boundaries (it’s a market-level property), but momentum signals are interval-specific and should reset at each new epoch.

Configuration

The engine reads configuration from config/default.json:
{
  "engine": {
    "ewma": {
      "lambda": 0.94  // EWMA decay factor (higher = more historical weight)
    },
    "momentum": {
      "bufferSize": 300  // Max ticks to retain (5 minutes at 1 tick/sec)
    },
    "abstention": {
      "minTicks": 50,           // Min ticks before making predictions
      "deadZone": 0.10,         // Abstain if |p-0.5| < this
      "sigmaMultiplier": 2.0,   // Abstain if sigma > 2x mean
      "minAccuracy": 0.40,      // Abstain if recent accuracy < 40%
      "minAccuracyWindow": 20,  // Window size for accuracy calculation
      "minEV": 0.05,            // Min expected value (post-prediction filter)
      "minMargin": 0.15         // Min safety margin (post-prediction filter)
    },
    "prediction": {
      "logitMomentumWeight": 150,  // Momentum adjustment strength
      "logitReversionWeight": 80,  // Reversion adjustment strength
      "nearExpiryGuardSec": 5      // Skip adjustments when time < this
    }
  }
}
Post-prediction filters (EV and margin checks) are implemented in src/index.js:154-166 because they require market price data from Polymarket.

Internal Components

EWMAVolatility

Exponentially weighted moving average volatility estimator.
  • Produces per-second volatility (σ) from streaming price ticks
  • Uses log returns normalized by time delta
  • Maintains rolling history of last 100 σ values for anomaly detection
Source: src/engine/volatility.js:4

MomentumAnalyzer

Multi-timeframe momentum and mean-reversion detector.
  • Calculates ROC (Rate of Change) over 10s, 30s, 60s windows
  • Combines timeframes with weights: 50% (10s) + 30% (30s) + 20% (60s)
  • Detects mean-reversion when price deviates >0.3% from 2-minute SMA
Source: src/engine/momentum.js:6

PlattScaler

Logistic calibration for probability predictions.
  • Auto-activates after collecting 200+ prediction/outcome pairs
  • Fits logistic regression: p_calibrated = sigmoid(A * p_raw + B)
  • Improves probability calibration over time
Source: src/engine/calibration.js

Usage Patterns

Full Integration Example

import { PredictionEngine } from './engine/predictor.js'
import { ChainlinkFeed } from './feeds/chainlink.js'
import { getCurrent5mTimestamp, fetchStrikePrice } from './feeds/vatic.js'

// 1. Create engine
const engine = new PredictionEngine()

let lastPrice = null

// 2. Start price feed
const feed = new ChainlinkFeed({
  onTick: (tick) => {
    lastPrice = tick.price
    engine.feedTick(tick)
  }
})
feed.start()

// 3. Main prediction loop
while (true) {
  const epochTs = getCurrent5mTimestamp()
  const { strikePrice } = await fetchStrikePrice(epochTs)
  const nowSec = Math.floor(Date.now() / 1000)
  const timeRemaining = (epochTs + 300) - nowSec
  
  if (lastPrice && strikePrice) {
    const result = engine.predict({
      currentPrice: lastPrice,
      strikePrice,
      timeRemainingSeconds: timeRemaining
    })
    
    if (result.abstained) {
      console.log(`Abstaining: ${result.reason}`)
    } else {
      console.log(`Prediction: ${result.direction} (${(result.probability * 100).toFixed(1)}%)`)
    }
  }
  
  await new Promise(r => setTimeout(r, 1000))
}

See Also

Build docs developers (and LLMs) love