Overview
The IntervalTracker is a state machine that orchestrates the lifecycle of 5-minute trading intervals. It detects epoch transitions, captures prediction snapshots at strategic moments, and closes completed intervals with comprehensive result data.
Core Concepts
Epoch Boundaries
Intervals align to 5-minute epoch boundaries (300-second divisions of Unix time):
// Example: 1678901400 = 2023-03-15 14:30:00 UTC
const epochTimestamp = Math . floor ( Date . now () / 1000 / 300 ) * 300
When the epoch changes, the tracker automatically:
Closes the previous interval with a result
Starts a new interval with a fresh index
Prediction Snapshots
The tracker captures two prediction snapshots during each interval:
Snapshot Timing Purpose Early Prediction 60s remaining Risk/EV capture for trading decisions Final Prediction 30s remaining Last-moment model confidence
Why two snapshots? The early prediction (60s) includes full trading context (bet size, EV, drawdown state) for position sizing decisions. The final prediction (30s) captures the model’s latest confidence before the interval closes.
IntervalRecord Structure
Each interval generates a comprehensive record with 40+ data points:
{
// ── Identity ──
index : 42 , // Sequential interval number
epochTimestamp : 1678901400 , // 5-min boundary (seconds)
// ── Outcome ──
strikePrice : 24150.00 , // Price at interval start
finalPrice : 24175.50 , // Price at interval end
result : 'UP' , // 'UP' | 'DOWN' | 'ACTIVE'
priceDelta : 25.50 , // finalPrice - strikePrice
priceMovePct : 0.106 , // % price change
closedAt : '2023-03-15T14:35:00.123Z' ,
// ── Predictions ──
earlyPrediction : {
probability : 0.6843 ,
direction : 'UP'
},
earlyPredictionCorrect : true ,
prediction : {
probability : 0.7021 ,
direction : 'UP'
},
predictionCorrect : true ,
// ── Model Factors (at 60s) ──
volatility : 0.00042 , // EWMA sigma per-second
momentum : 0.0031 , // ROC momentum factor
reversion : - 0.0012 , // Mean reversion factor
calibrated : true , // Platt calibration active?
// ── Market & EV (at 60s) ──
qMarket : 0.52 , // Polymarket UP token price
evAtCapture : 0.0843 , // Expected value
edge : 0.1643 , // p - q (model advantage)
margin : 0.05 , // Safety margin
evSide : 'YES' , // Best EV side
// ── Position Sizing (at 60s) ──
betSize : 125.00 , // USD bet amount
fullKelly : 0.3286 , // Full Kelly fraction
alpha : 0.25 , // Kelly fraction (1/4 Kelly)
betSide : 'YES' , // 'YES' | 'NO'
betCapped : false , // Hit max bet limit?
// ── Risk State (at 60s) ──
drawdownLevel : 'green' , // 'green'|'yellow'|'red'|'critical'
bankroll : 5000.00 , // Current bankroll USD
drawdownPct : 2.3 , // % drawdown from peak
coldStreak : 0 , // Consecutive misses
// ── Abstention ──
abstentionReason : null , // Why no trade (null = traded)
// ── Timing ──
timeRemainingAtCapture : 59 // Seconds remaining at capture
}
All market/trading fields are captured at the early prediction moment (60s remaining) to reflect the exact state when position sizing decisions are made.
State Machine Flow
Transition Logic
The tracker detects epoch changes on every tick:
async tick ({ currentPrice , strikePrice , prediction , epochTimestamp , ... }) {
// Transition detection
if ( epochTimestamp !== this . _currentEpoch ) {
// CLOSE previous interval
if ( this . _currentInterval && currentPrice !== null ) {
const closing = this . _currentInterval
closing . finalPrice = currentPrice
closing . result = currentPrice > closing . strikePrice ? 'UP' : 'DOWN'
closing . closedAt = new Date (). toISOString ()
// Evaluate prediction accuracy
if ( closing . earlyPrediction ) {
const predictedUp = closing . earlyPrediction . direction === 'UP'
const actualUp = currentPrice > closing . strikePrice
closing . earlyPredictionCorrect = predictedUp === actualUp
}
this . _history . push ( closing )
await this . _onIntervalClose ( closing )
}
// START new interval
this . _currentEpoch = epochTimestamp
this . _currentInterval = {
index: this . _nextIndex ++ ,
epochTimestamp ,
result: 'ACTIVE' ,
// ... initialize all fields to null
}
await this . _onIntervalStart ( epochTimestamp , this . _currentInterval . index )
}
// Update current interval with latest data
// ...
}
src/tracker/interval.js (lines 100-144)
src/tracker/interval.js (lines 146-191)
// CLOSE previous interval
if ( this . _currentInterval && currentPrice !== null ) {
const closing = this . _currentInterval
closing . finalPrice = currentPrice
// Result is determined against the CLOSING interval's own strikePrice
if ( closing . strikePrice !== null ) {
closing . result = currentPrice > closing . strikePrice ? 'UP' : 'DOWN'
} else {
// Cannot determine result without a strikePrice — mark as FAIL
closing . result = 'DOWN'
}
// Evaluate whether the prediction was correct
if ( closing . prediction ) {
const predictedUp = closing . prediction . direction === 'UP'
const actualUp = currentPrice > closing . strikePrice
closing . predictionCorrect = predictedUp === actualUp
} else {
closing . predictionCorrect = null
}
// Evaluate early prediction accuracy
if ( closing . earlyPrediction && closing . strikePrice !== null ) {
const earlyPredictedUp = closing . earlyPrediction . direction === 'UP'
const actualUp = currentPrice > closing . strikePrice
closing . earlyPredictionCorrect = earlyPredictedUp === actualUp
} else {
closing . earlyPredictionCorrect = null
}
// Price dynamics
if ( closing . strikePrice !== null ) {
closing . priceDelta = currentPrice - closing . strikePrice
closing . priceMovePct = ( closing . priceDelta / closing . strikePrice ) * 100
}
closing . closedAt = new Date (). toISOString ()
this . _history . push ( closing )
await this . _onIntervalClose ( closing )
}
Snapshot Capture
Early Prediction (60s Remaining)
Captures full trading context for position sizing:
// Capture early prediction snapshot ONCE at ~1 min mark (60s remaining)
if (
modelPred &&
! this . _currentInterval . earlyPrediction &&
timeRemainingSeconds != null &&
timeRemainingSeconds <= 60
) {
this . _currentInterval . earlyPrediction = {
probability: modelPred . probability ,
direction: modelPred . direction ,
}
// Model factors from prediction result
this . _currentInterval . volatility = modelPred . volatility ?? null
this . _currentInterval . momentum = modelPred . momentum ?? null
this . _currentInterval . reversion = modelPred . reversion ?? null
this . _currentInterval . calibrated = modelPred . calibrated ?? null
// Market & EV
if ( qMarket != null ) this . _currentInterval . qMarket = qMarket
if ( evResult != null ) {
this . _currentInterval . evAtCapture = evResult . ev ?? null
this . _currentInterval . edge = evResult . edge ?? null
this . _currentInterval . margin = evResult . margin ?? null
this . _currentInterval . evSide = evResult . bestSide ?? null
}
// Bet details (only when trade-eligible)
if ( prediction && betResult != null ) {
this . _currentInterval . betSize = betResult . bet ?? null
this . _currentInterval . fullKelly = betResult . fullKelly ?? null
this . _currentInterval . alpha = betResult . alpha ?? null
this . _currentInterval . betSide = betResult . side ?? null
this . _currentInterval . betCapped = betResult . capped ?? null
}
// Risk state
if ( drawdownLevel != null ) this . _currentInterval . drawdownLevel = drawdownLevel
if ( drawdownState != null ) {
this . _currentInterval . bankroll = drawdownState . bankroll ?? null
this . _currentInterval . drawdownPct = drawdownState . drawdownPct ?? null
this . _currentInterval . coldStreak = drawdownState . coldStreak ?? null
}
// Abstention reason at capture moment
this . _currentInterval . abstentionReason = abstentionReason ?? null
// Exact capture timing
this . _currentInterval . timeRemainingAtCapture = timeRemainingSeconds
}
src/tracker/interval.js (lines 214-262)
// Capture early prediction snapshot ONCE at ~1 min mark (60s remaining)
// Uses raw model prediction so recording isn't blocked by trading filters
if (
modelPred &&
! this . _currentInterval . earlyPrediction &&
timeRemainingSeconds != null &&
timeRemainingSeconds <= 60
) {
this . _currentInterval . earlyPrediction = {
probability: modelPred . probability ,
direction: modelPred . direction ,
}
// Model factors from prediction result
this . _currentInterval . volatility = modelPred . volatility ?? null
this . _currentInterval . momentum = modelPred . momentum ?? null
this . _currentInterval . reversion = modelPred . reversion ?? null
this . _currentInterval . calibrated = modelPred . calibrated ?? null
// Market & EV
if ( qMarket != null ) this . _currentInterval . qMarket = qMarket
if ( evResult != null ) {
this . _currentInterval . evAtCapture = evResult . ev ?? null
this . _currentInterval . edge = evResult . edge ?? null
this . _currentInterval . margin = evResult . margin ?? null
this . _currentInterval . evSide = evResult . bestSide ?? null
}
// Bet details (only when trade-eligible)
if ( prediction && betResult != null ) {
this . _currentInterval . betSize = betResult . bet ?? null
this . _currentInterval . fullKelly = betResult . fullKelly ?? null
this . _currentInterval . alpha = betResult . alpha ?? null
this . _currentInterval . betSide = betResult . side ?? null
this . _currentInterval . betCapped = betResult . capped ?? null
}
// Risk state
if ( drawdownLevel != null ) this . _currentInterval . drawdownLevel = drawdownLevel
if ( drawdownState != null ) {
this . _currentInterval . bankroll = drawdownState . bankroll ?? null
this . _currentInterval . drawdownPct = drawdownState . drawdownPct ?? null
this . _currentInterval . coldStreak = drawdownState . coldStreak ?? null
}
// Abstention reason at capture moment
this . _currentInterval . abstentionReason = abstentionReason ?? null
// Exact capture timing
this . _currentInterval . timeRemainingAtCapture = timeRemainingSeconds
}
Final Prediction (30s Remaining)
Captures last-moment model confidence:
// Capture final prediction snapshot ONCE at ~30s remaining
if (
modelPred &&
! this . _currentInterval . prediction &&
timeRemainingSeconds != null &&
timeRemainingSeconds <= 30
) {
this . _currentInterval . prediction = {
probability: modelPred . probability ,
direction: modelPred . direction ,
}
}
src/tracker/interval.js (lines 264-276)
// Capture final prediction snapshot ONCE at ~30s remaining
if (
modelPred &&
! this . _currentInterval . prediction &&
timeRemainingSeconds != null &&
timeRemainingSeconds <= 30
) {
this . _currentInterval . prediction = {
probability: modelPred . probability ,
direction: modelPred . direction ,
}
}
Usage Example
Integration with the main bot loop:
import { IntervalTracker } from './tracker/interval.js'
import { HistoryStore } from './tracker/history.js'
const history = new HistoryStore ({ filePath: 'data/history.json' })
const tracker = new IntervalTracker ({
onIntervalClose : async ( record ) => {
console . log ( `Interval ${ record . index } closed: ${ record . result } ` )
await history . append ( record )
},
onIntervalStart : async ( epoch , index ) => {
console . log ( `Interval ${ index } started at epoch ${ epoch } ` )
}
})
// Load previous history to continue interval numbering
const previousRecords = await history . load ()
tracker . loadHistory ( previousRecords )
// Main loop (runs every second)
setInterval ( async () => {
const currentPrice = await priceSource . getPrice ()
const strikePrice = await vaticAPI . getStrikePrice ()
const prediction = await model . predict ()
const epochTimestamp = Math . floor ( Date . now () / 1000 / 300 ) * 300
const timeRemainingSeconds = 300 - ( Math . floor ( Date . now () / 1000 ) % 300 )
await tracker . tick ({
currentPrice ,
strikePrice ,
prediction ,
epochTimestamp ,
timeRemainingSeconds ,
qMarket: await polymarket . getPrice (),
evResult: await evCalculator . compute (),
betResult: await positionSizer . size (),
drawdownLevel: drawdownTracker . getLevel (),
drawdownState: drawdownTracker . getState (),
})
}, 1000 )
API Reference
Constructor
new IntervalTracker ({ onIntervalClose , onIntervalStart })
Parameters:
onIntervalClose (function): Called with the closed IntervalRecord when an interval ends
onIntervalStart (function): Called with (epochTimestamp, index) when a new interval begins
Methods
tick(params)
Processes one second of interval lifecycle. Detects epoch transitions, captures snapshots, and updates state.
Parameters:
currentPrice (number|null): Latest BTC price
strikePrice (number|null): Strike from Vatic API
prediction (object|null): Post-filter prediction { probability, direction }
rawPrediction (object|null): Raw model output before trading filters
epochTimestamp (number): Current 5-min epoch boundary (seconds)
timeRemainingSeconds (number): Seconds left in interval
qMarket (number|null): Polymarket price
evResult (object|null): Expected value calculation result
betResult (object|null): Position sizing result
drawdownLevel (string|null): Current drawdown level
drawdownState (object|null): Full drawdown state
abstentionReason (string|null): Why no trade was made
getCurrentInterval()
Returns the currently active IntervalRecord, or null if no tick has been processed yet.
getHistory()
Returns an array of all closed IntervalRecord objects accumulated this session.
loadHistory(records)
Restores previously persisted records so that interval indices continue from where they left off.
Parameters:
records (array): Array of IntervalRecord objects loaded from disk
Edge Cases
Missing Strike Price
If the Vatic API fails and no strike price is available:
if ( closing . strikePrice !== null ) {
closing . result = currentPrice > closing . strikePrice ? 'UP' : 'DOWN'
} else {
// Cannot determine result without a strikePrice — mark as FAIL
closing . result = 'DOWN'
}
Late Strike Price Arrival
The tracker back-fills the strike price when it arrives late:
// Back-fill strikePrice when it arrives late
if ( strikePrice && ! this . _currentInterval . strikePrice ) {
this . _currentInterval . strikePrice = strikePrice
}
Abstention Tracking
When the bot abstains from trading:
// Abstention reason at capture moment
this . _currentInterval . abstentionReason = abstentionReason ?? null
// Examples:
// "low_confidence" - Model probability < min threshold
// "negative_ev" - Expected value < 0
// "drawdown_red" - In red/critical drawdown state
// "cold_streak" - Too many consecutive misses
// "insufficient_edge" - Edge too small vs market
History Store JSON persistence for interval records
Metrics Scoring rules and performance analysis
Logging Structured logs and tick data