Overview
The MomentumAnalyzer extracts short-term directional signals from a rolling buffer of price ticks. It produces two types of signals:
Momentum (ROC) : Rate of change across 10s, 30s, and 60s windows
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:
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 ) = S t − S t − Δ t S t − Δ t \text{ROC}(\Delta t) = \frac{S_t - S_{t-\Delta t}}{S_{t-\Delta t}} ROC ( Δ t ) = S t − Δ t S t − S t − Δ 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:
ROC combined = 0.5 ⋅ ROC 10 s + 0.3 ⋅ ROC 30 s + 0.2 ⋅ ROC 60 s \text{ROC}_{\text{combined}} = 0.5 \cdot \text{ROC}_{10s} + 0.3 \cdot \text{ROC}_{30s} + 0.2 \cdot \text{ROC}_{60s} ROC combined = 0.5 ⋅ ROC 10 s + 0.3 ⋅ ROC 30 s + 0.2 ⋅ ROC 60 s
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
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
_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 Value Interpretation +0.001 +0.1% price increase +0.01 +1.0% price increase (strong momentum) -0.003 -0.3% price decrease 0.0 No 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) :
SMA 2 m = 1 N ∑ i = 1 N S i where t i ≥ t − 120 s \text{SMA}_{2m} = \frac{1}{N} \sum_{i=1}^N S_i \quad \text{where } t_i \geq t - 120s SMA 2 m = N 1 i = 1 ∑ N S i where t i ≥ t − 120 s
deviation = S t − SMA 2 m SMA 2 m \text{deviation} = \frac{S_t - \text{SMA}_{2m}}{\text{SMA}_{2m}} deviation = SMA 2 m S t − SMA 2 m
If |deviation| > 0.3%, a reversion signal fires:
signal = { − deviation if ∣ deviation ∣ > 0.003 0 otherwise \text{signal} = \begin{cases}
-\text{deviation} & \text{if } |\text{deviation}| > 0.003 \\
0 & \text{otherwise}
\end{cases} signal = { − deviation 0 if ∣ deviation ∣ > 0.003 otherwise
Sign convention : The signal opposes the deviation direction (reversion hypothesis).
Implementation
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
Deviation Signal Interpretation +0.5% (price above SMA) -0.005 Bearish reversion (price likely to fall) -0.4% (price below SMA) +0.004 Bullish reversion (price likely to rise) +0.2% 0 No 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:
feedTick ({ timestamp , price }) {
this . _volatility . update ( price , timestamp )
this . _momentum . addTick ({ timestamp , price }) // ← Here
}
During prediction, both signals are extracted:
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:
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:
Liquidity provision : Market makers push price back toward fair value
Information decay : Short-lived news spikes fade
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
reset () {
this . _buffer = []
}
The prediction engine calls resetMomentum() at the start of each new interval:
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
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:
Δ t adaptive = k ⋅ 1 σ \Delta t_{\text{adaptive}} = k \cdot \frac{1}{\sigma} Δ t adaptive = k ⋅ σ 1
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 = 3 ⋅ EMA 1 − 3 ⋅ EMA 2 + EMA 3 \text{TEMA} = 3 \cdot \text{EMA}_1 - 3 \cdot \text{EMA}_2 + \text{EMA}_3 TEMA = 3 ⋅ EMA 1 − 3 ⋅ EMA 2 + 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:
ROC VW = ∑ v i ⋅ r i ∑ v i \text{ROC}_{\text{VW}} = \frac{\sum v_i \cdot r_i}{\sum v_i} ROC VW = ∑ v i ∑ v i ⋅ r i
This emphasizes moves with high conviction (large volume).
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