Skip to main content

Overview

The MomentumAnalyzer extracts short-term directional signals from a rolling buffer of price ticks. It produces two types of signals:
  1. Momentum (ROC): Rate of change across 10s, 30s, and 60s windows
  2. Mean Reversion: Deviation from a 2-minute simple moving average
These signals adjust the Black-Scholes base probability via logit-space fusion.

Architecture

Tick Buffer

The analyzer maintains a fixed-size FIFO buffer:
this._buffer = []  // Array of { timestamp, price }
this._bufferSize = 300  // Default max ticks (configurable)
New ticks are appended; oldest ticks are evicted when the buffer exceeds capacity:
momentum.js
addTick({ timestamp, price }) {
  this._buffer.push({ timestamp, price })
  while (this._buffer.length > this._bufferSize) {
    this._buffer.shift()
  }
}
Buffer Size: 300 ticks at ~1 Hz → ~5 minutes of history. This is sufficient for computing 60s ROC windows with margin.

Momentum Signals (ROC)

Rate of Change Definition

The Rate of Change (ROC) measures the percentage change in price over a time window: ROC(Δt)=StStΔtStΔt\text{ROC}(\Delta t) = \frac{S_t - S_{t-\Delta t}}{S_{t-\Delta t}} Where:
  • S_t = current price
  • S_ = price Δt seconds ago
  • ROC is a signed value (positive = price increased, negative = price decreased)

Multi-Window Combination

The analyzer computes ROC for three windows and combines them with fixed weights: ROCcombined=0.5ROC10s+0.3ROC30s+0.2ROC60s\text{ROC}_{\text{combined}} = 0.5 \cdot \text{ROC}_{10s} + 0.3 \cdot \text{ROC}_{30s} + 0.2 \cdot \text{ROC}_{60s} Weighting rationale:
  • 10s window (50%): Captures very recent momentum (most predictive)
  • 30s window (30%): Medium-term trend confirmation
  • 60s window (20%): Longer-term context (less reactive)

Implementation

momentum.js
getMomentum() {
  if (this._buffer.length === 0) {
    return { roc60s: 0, roc30s: 0, roc10s: 0, combined: 0 }
  }

  const currentTick = this._buffer[this._buffer.length - 1]
  const currentPrice = currentTick.price
  const now = currentTick.timestamp

  const roc60s = this._rocForWindow(now, currentPrice, 60)
  const roc30s = this._rocForWindow(now, currentPrice, 30)
  const roc10s = this._rocForWindow(now, currentPrice, 10)

  // Weighted combination
  const combined = 0.5 * roc10s + 0.3 * roc30s + 0.2 * roc60s

  return { roc60s, roc30s, roc10s, combined }
}

Window Lookup Logic

momentum.js
_rocForWindow(now, currentPrice, windowSec) {
  const windowMs = windowSec * 1000
  const cutoff = now - windowMs

  // Walk forward to find the oldest tick within the window
  let oldTick = null
  for (let i = 0; i < this._buffer.length; i++) {
    if (this._buffer[i].timestamp <= cutoff) {
      oldTick = this._buffer[i]
    } else {
      break
    }
  }

  // Fallback: if no tick at cutoff, use oldest tick if it's ≥50% of window age
  if (oldTick === null) {
    const oldest = this._buffer[0]
    if (now - oldest.timestamp >= windowMs * 0.5) {
      oldTick = oldest
    }
  }

  if (oldTick === null || oldTick.price === 0) {
    return 0
  }

  return (currentPrice - oldTick.price) / oldTick.price
}
Graceful Degradation: If the buffer doesn’t contain a tick old enough for the full window (e.g., only 40s of data when computing 60s ROC), the oldest tick is used if it’s at least 50% of the window age. This allows the system to produce partial signals during warm-up.

Signal Magnitude

Typical ROC values:
ROC ValueInterpretation
+0.001+0.1% price increase
+0.01+1.0% price increase (strong momentum)
-0.003-0.3% price decrease
0.0No movement
Raw, Unscaled Signals: The ROC values are intentionally not normalized. The logit-space weights in the config control how much these raw signals influence the final probability.

Mean Reversion Signal

Deviation from SMA

The mean-reversion signal detects when price deviates significantly from a 2-minute simple moving average (SMA): SMA2m=1Ni=1NSiwhere tit120s\text{SMA}_{2m} = \frac{1}{N} \sum_{i=1}^N S_i \quad \text{where } t_i \geq t - 120s deviation=StSMA2mSMA2m\text{deviation} = \frac{S_t - \text{SMA}_{2m}}{\text{SMA}_{2m}} If |deviation| > 0.3%, a reversion signal fires: signal={deviationif deviation>0.0030otherwise\text{signal} = \begin{cases} -\text{deviation} & \text{if } |\text{deviation}| > 0.003 \\ 0 & \text{otherwise} \end{cases} Sign convention: The signal opposes the deviation direction (reversion hypothesis).

Implementation

momentum.js
getMeanReversion() {
  if (this._buffer.length === 0) {
    return { sma2m: 0, currentPrice: 0, deviation: 0, signal: 0 }
  }

  const currentTick = this._buffer[this._buffer.length - 1]
  const currentPrice = currentTick.price
  const now = currentTick.timestamp

  // Gather ticks from the last 120 seconds
  const cutoff = now - 120_000
  let sum = 0
  let count = 0
  for (let i = this._buffer.length - 1; i >= 0; i--) {
    if (this._buffer[i].timestamp >= cutoff) {
      sum += this._buffer[i].price
      count++
    } else {
      break
    }
  }

  if (count === 0) {
    return { sma2m: currentPrice, currentPrice, deviation: 0, signal: 0 }
  }

  const sma2m = sum / count
  const deviation = (currentPrice - sma2m) / sma2m

  let signal = 0
  if (Math.abs(deviation) > 0.003) {
    // Reversion signal opposes the deviation direction (raw, unscaled)
    signal = -deviation
  }

  return { sma2m, currentPrice, deviation, signal }
}

Signal Interpretation

DeviationSignalInterpretation
+0.5% (price above SMA)-0.005Bearish reversion (price likely to fall)
-0.4% (price below SMA)+0.004Bullish reversion (price likely to rise)
+0.2%0No reversion (deviation too small)
Threshold = 0.3%: Only deviations exceeding 0.3% trigger a signal. This prevents noise from microstructure fluctuations.

Integration with Engine

Feeding Ticks

The prediction engine forwards ticks to the momentum analyzer:
predictor.js
feedTick({ timestamp, price }) {
  this._volatility.update(price, timestamp)
  this._momentum.addTick({ timestamp, price })  // ← Here
}

Signal Extraction

During prediction, both signals are extracted:
predictor.js
const { combined: momentumFactor } = this._momentum.getMomentum()
const { signal: reversionFactor } = this._momentum.getMeanReversion()

Logit-Space Fusion

Signals are combined with the Black-Scholes base probability via logit-space arithmetic:
predictor.js
const logitBase = logit(baseProb)
const logitAdj = logitBase
  + config.engine.prediction.logitMomentumWeight * momentumFactor
  + config.engine.prediction.logitReversionWeight * reversionFactor
finalProb = sigmoid(logitAdj)
Default weights:
logitMomentumWeight: 2.0
logitReversionWeight: 1.5
Logit Weights: These are not probabilities. They are additive adjustments on the log-odds scale. A weight of 2.0 means “shift the log-odds by 2 × the raw signal value.”

Usage Example

import { MomentumAnalyzer } from './momentum.js'

const analyzer = new MomentumAnalyzer({ bufferSize: 300 })

// Stream price ticks
const ticks = [
  { timestamp: 1000, price: 0.500 },
  { timestamp: 2000, price: 0.502 },
  { timestamp: 3000, price: 0.498 },
  // ... (60+ seconds of data)
]

for (const tick of ticks) {
  analyzer.addTick(tick)
}

// Extract momentum signals
const { roc10s, roc30s, roc60s, combined } = analyzer.getMomentum()
console.log('Momentum signals:', { roc10s, roc30s, roc60s, combined })

// Extract mean-reversion signal
const { sma2m, currentPrice, deviation, signal } = analyzer.getMeanReversion()
console.log('Reversion signal:', { sma2m, currentPrice, deviation, signal })

// Use in logit-space combination
const momentumAdj = 2.0 * combined
const reversionAdj = 1.5 * signal
console.log('Log-odds adjustments:', { momentumAdj, reversionAdj })

Design Rationale

Why Multiple Windows?

Different time scales capture different market dynamics:
  • 10s: Microstructure momentum (order flow, tick-level trends)
  • 30s: Short-term trend confirmation (reduces noise)
  • 60s: Context for distinguishing genuine trends from spikes

Why Mean Reversion?

Polymarket prices exhibit mean-reverting behavior due to:
  1. Liquidity provision: Market makers push price back toward fair value
  2. Information decay: Short-lived news spikes fade
  3. Bounded range: Probabilities can’t exceed [0,1]
The 2-minute SMA provides a local “fair value” anchor.

Why Raw Signals?

Normalizing signals (e.g., z-scores) would require tracking historical distributions, adding complexity. Instead:
  • Raw signals capture magnitude directly (0.01 = 1% move)
  • Logit weights act as learned scaling factors (tuned via backtest)

State Management

Reset

momentum.js
reset() {
  this._buffer = []
}
The prediction engine calls resetMomentum() at the start of each new interval:
predictor.js
resetMomentum() {
  this._momentum.reset()
}
Volatility Persistence: Unlike the momentum analyzer, the EWMA volatility estimator is not reset at interval boundaries. Volatility should carry forward across epochs.

Buffer Inspection

momentum.js
getTickBuffer() {
  return [...this._buffer]  // Shallow copy
}
Useful for debugging or exporting tick data.

Advanced Topics

Adaptive Windows

Fixed windows (10s, 30s, 60s) work well for typical market conditions, but you could implement adaptive windows that scale with realized volatility: Δtadaptive=k1σ\Delta t_{\text{adaptive}} = k \cdot \frac{1}{\sigma} High volatility → shorter windows (faster reaction). Low volatility → longer windows (filter noise).

Triple Exponential Moving Average (TEMA)

Instead of SMA for mean reversion, TEMA reduces lag: TEMA=3EMA13EMA2+EMA3\text{TEMA} = 3 \cdot \text{EMA}_1 - 3 \cdot \text{EMA}_2 + \text{EMA}_3 This is more responsive to recent changes but requires more state.

Volume-Weighted ROC

If tick data includes volume, you could weight returns by volume: ROCVW=virivi\text{ROC}_{\text{VW}} = \frac{\sum v_i \cdot r_i}{\sum v_i} This emphasizes moves with high conviction (large volume).

Performance Characteristics

  • Time Complexity: O(N) per getMomentum() or getMeanReversion() call, where N = buffer size
  • Space Complexity: O(N) for tick buffer (default: 300 ticks)
  • Worst Case: O(N) = O(300) → negligible for real-time prediction
Optimization Opportunity: If profiling shows these calls are bottlenecks, you could maintain incremental SMA state (O(1) update) at the cost of additional bookkeeping.

References

  • Wilder, J. W. (1978). New Concepts in Technical Trading Systems. Trend Research.
  • Pring, M. J. (2002). Technical Analysis Explained (4th ed.). McGraw-Hill.
  • Murphy, J. J. (1999). Technical Analysis of the Financial Markets. New York Institute of Finance.

Next Steps

Prediction Engine

How momentum and reversion signals integrate via logit-space fusion

Calibration

Post-hoc probability adjustment using Platt scaling

Build docs developers (and LLMs) love