Skip to main content

Overview

The interval tracking system manages the lifecycle of 5-minute trading intervals:
  • IntervalTracker: State machine that detects epoch transitions, captures prediction snapshots, evaluates outcomes, and triggers callbacks
  • HistoryStore: Persistence layer for interval records (JSON file on disk)
Together they provide a complete audit trail of all predictions, outcomes, and trading decisions.

IntervalTracker

Interval lifecycle manager - tracks 5-minute interval transitions, closes finished intervals with results, and starts new ones. Source: src/tracker/interval.js:58

Constructor

import { IntervalTracker } from './tracker/interval.js'

const tracker = new IntervalTracker({
  onIntervalClose: async (record) => {
    console.log(`Interval ${record.index} closed: ${record.result}`)
    if (record.earlyPredictionCorrect != null) {
      console.log(`Prediction was ${record.earlyPredictionCorrect ? 'correct' : 'wrong'}`)
    }
  },
  onIntervalStart: (epochTimestamp, index) => {
    console.log(`Interval ${index} started at epoch ${epochTimestamp}`)
  }
})
opts
object
Configuration options (all optional)
opts.onIntervalClose
function
Called with the closed IntervalRecord when an interval ends. Can be async. Use this to persist records, feed outcomes to the prediction engine, etc.
opts.onIntervalStart
function
Called with (epochTimestamp: number, index: number) when a new interval begins. Use this to reset momentum, log transitions, etc.
From src/index.js:
// Create interval tracker with lifecycle hooks
const tracker = new IntervalTracker({
  onIntervalClose: async (record) => {
    await store.append(record)
    
    // Feed outcome to engine for cold_streak tracking + Platt calibration
    if (record.earlyPrediction && record.earlyPredictionCorrect != null) {
      engine.recordOutcome(
        record.earlyPredictionCorrect, 
        record.earlyPrediction.probability
      )
      
      // Feed risk tracker
      drawdownTracker.recordOutcome(
        record.earlyPredictionCorrect, 
        record.earlyPrediction.probability
      )
      if (record.betSize != null && record.betSize > 0) {
        drawdownTracker.recordTrade(
          record.betSize, 
          record.earlyPredictionCorrect
        )
      }
    }
  },
  onIntervalStart: () => {
    engine.resetMomentum()
  }
})

Methods

tick()

Called every second from the main loop. Detects epoch transitions to close the previous interval and open a new one. Between transitions, keeps the current interval’s prediction and strike price up to date.
params
object
required
Tick parameters
params.currentPrice
number | null
required
Latest BTC price (null when WebSocket not connected yet)
params.strikePrice
number | null
required
Strike from Vatic (null when API failed or hasn’t responded yet)
params.prediction
object | null
required
Post-filter prediction: { probability: number, direction: 'UP'|'DOWN' } or null. Used for trading data (bet size, EV capture).
params.epochTimestamp
number
required
Current 5-minute epoch boundary (seconds, divisible by 300)
params.timeRemainingSeconds
number
Seconds left in interval
params.qMarket
number | null
Polymarket price for this market (0-1)
params.evResult
object | null
EV calculation result: { ev, edge, margin, bestSide } from calculateEV()
params.betResult
object | null
Bet sizing result: { bet, fullKelly, alpha, side, capped } from PositionSizer
params.drawdownLevel
string | null
Risk level: 'green' | 'yellow' | 'red' | 'critical' from DrawdownTracker
params.rawPrediction
object | null
Model output before trading filters (for recording even when filters abstain): { probability, direction, volatility }
params.abstentionReason
string | null
Reason for trading abstention (null if trading)
params.drawdownState
object | null
Full state from DrawdownTracker.getState(): { bankroll, drawdownPct, coldStreak }
returns
Promise<void>
Async - waits for onIntervalClose callback to complete when intervals transition
Example:
// From src/index.js:206-219
await tracker.tick({
  currentPrice: lastPrice,
  strikePrice,
  prediction,
  rawPrediction,
  epochTimestamp: epochTs,
  timeRemainingSeconds: remainingSec,
  qMarket,
  evResult,
  betResult,
  drawdownLevel: drawdownTracker.getLevel(),
  abstentionReason: abstention?.reason ?? null,
  drawdownState: drawdownTracker.getState(),
})
Behavior:
  1. Epoch transition detection: When epochTimestamp changes:
    • Close previous interval (if exists and currentPrice is available):
      • Sets finalPrice = currentPrice
      • Determines result: 'UP' if currentPrice > strikePrice, else 'DOWN'
      • Evaluates predictionCorrect and earlyPredictionCorrect
      • Calculates priceDelta and priceMovePct
      • Sets closedAt timestamp (ISO string)
      • Adds to history
      • Calls onIntervalClose(record) (awaits completion)
    • Start new interval:
      • Creates fresh IntervalRecord with index = nextIndex++
      • Initializes all fields to null
      • Calls onIntervalStart(epochTimestamp, index)
  2. Between transitions:
    • Updates livePrediction every tick (for display)
    • Back-fills strikePrice when it arrives late
    • Early capture (once at ≤60s remaining):
      • Stores earlyPrediction snapshot (uses rawPrediction to avoid filter blocking)
      • Records model factors: volatility, momentum, reversion, calibrated
      • Records market data: qMarket, evAtCapture, edge, margin, evSide
      • Records bet details: betSize, fullKelly, alpha, betSide, betCapped (only when prediction exists)
      • Records risk state: drawdownLevel, bankroll, drawdownPct, coldStreak
      • Records abstentionReason and timeRemainingAtCapture
    • Final capture (once at ≤30s remaining):
      • Stores prediction snapshot for final evaluation
Why two prediction snapshots?
  • earlyPrediction (60s): Captured early for recording/calibration (uses rawPrediction before filters)
  • prediction (30s): Captured closer to expiry for outcome evaluation

getCurrentInterval()

Get the currently active interval record.
returns
object | null
The currently active IntervalRecord, or null if no tick has been processed yet. See IntervalRecord structure for complete field reference.
Example:
const current = tracker.getCurrentInterval()
if (current) {
  console.log(`Interval ${current.index} - ${current.result}`)
  if (current.livePrediction) {
    console.log(`Live prediction: ${current.livePrediction.direction} (${current.livePrediction.probability})`)
  }
}
From src/index.js:
// Render display with current interval
display.render({
  livePrice: lastPrice,
  strikePrice,
  timeRemaining: { minutes, seconds },
  prediction,
  rawPrediction,
  abstention,
  history: tracker.getHistory(),
  currentInterval: tracker.getCurrentInterval(),  // Current interval for header
  connectionStatus,
  qMarket,
  evResult,
  polymarketStatus,
  betResult,
  drawdownState: drawdownTracker.getState()
})

getHistory()

Get all closed intervals from this session.
returns
array
Array of all closed IntervalRecord objects accumulated this session. Empty array if no intervals have closed yet.
Example:
const history = tracker.getHistory()
console.log(`Total intervals: ${history.length}`)

const correct = history.filter(h => h.earlyPredictionCorrect === true).length
const total = history.filter(h => h.earlyPredictionCorrect != null).length
if (total > 0) {
  console.log(`Accuracy: ${(correct / total * 100).toFixed(1)}%`)
}
From src/index.js:
// Calculate Brier score from history
const history = tracker.getHistory()
const earlyPreds = history.filter(h => h.earlyPredictionCorrect != null)
const predCount = earlyPreds.length
const brierScore = predCount > 0
  ? earlyPreds.reduce((sum, h) => {
      const p = h.earlyPrediction?.probability ?? 0.5
      const o = h.earlyPredictionCorrect ? 1 : 0
      return sum + (p - o) ** 2
    }, 0) / predCount
  : 1.0

loadHistory()

Restores previously persisted records into the tracker. Use this on startup to load saved history from disk so that indices continue from where they left off.
records
array
required
Array of IntervalRecord objects loaded from HistoryStore
returns
void
No return value. Updates internal state.
Example:
import { HistoryStore } from './tracker/history.js'
import { IntervalTracker } from './tracker/interval.js'

const store = new HistoryStore({ filePath: 'data/history.json' })
const savedHistory = await store.load()

const tracker = new IntervalTracker({
  onIntervalClose: async (record) => {
    await store.append(record)
  }
})

tracker.loadHistory(savedHistory)  // Restore previous session
From src/index.js:
// Load history on startup
const store = new HistoryStore({ filePath: config.storage.historyPath })
const savedHistory = await store.load()

const tracker = new IntervalTracker({ /* ... */ })
tracker.loadHistory(savedHistory)
Side effects:
  • Replaces internal history array
  • Sets next index to max(record.index) + 1 to continue sequential numbering

HistoryStore

Persistence layer for interval history records (JSON file on disk). Source: src/tracker/history.js:7

Constructor

import { HistoryStore } from './tracker/history.js'

const store = new HistoryStore({ 
  filePath: 'data/history.json' 
})
opts
object
Configuration options
opts.filePath
string
Path to the JSON file where interval records are persisted. Defaults to 'data/history.json'. Parent directories are created automatically.

Methods

load()

Reads the persisted history from disk.
returns
Promise<array>
Array of IntervalRecord objects. Returns an empty array when the file does not exist yet.
Throws: Re-throws any filesystem error that is not ENOENT (file not found) Example:
const store = new HistoryStore({ filePath: 'data/history.json' })

try {
  const records = await store.load()
  console.log(`Loaded ${records.length} intervals from disk`)
} catch (error) {
  console.error(`Failed to load history: ${error.message}`)
}
From src/index.js:
const store = new HistoryStore({ filePath: config.storage.historyPath })
const savedHistory = await store.load()

save()

Overwrites the history file with the given records array.
records
array
required
Full array of IntervalRecord objects to persist
returns
Promise<void>
Async - completes when file is written
Side effects:
  • Creates parent directory tree if it doesn’t exist (recursive)
  • Overwrites existing file
  • Writes formatted JSON with 2-space indentation
Example:
const records = tracker.getHistory()
await store.save(records)
console.log('History saved to disk')

append()

Convenience method: loads existing records, appends one, and saves.
record
object
required
A single IntervalRecord to append
returns
Promise<void>
Async - completes when file is updated
Example:
const tracker = new IntervalTracker({
  onIntervalClose: async (record) => {
    await store.append(record)  // Atomically append to file
  }
})
From src/index.js:
const tracker = new IntervalTracker({
  onIntervalClose: async (record) => {
    await store.append(record)
    // ... feed engine, etc.
  }
})
Implementation:
async append(record) {
  const records = await this.load()
  records.push(record)
  await this.save(records)
}
append() performs a full read-modify-write cycle. For high-frequency appends, consider batching or using a database instead of JSON files.

Integration Example

Complete setup with engine feedback and persistence:
import { IntervalTracker } from './tracker/interval.js'
import { HistoryStore } from './tracker/history.js'
import { PredictionEngine } from './engine/predictor.js'
import { DrawdownTracker } from './risk/drawdown-tracker.js'
import config from './config.js'

// 1. Create persistence layer
const store = new HistoryStore({ 
  filePath: config.storage.historyPath 
})
const savedHistory = await store.load()

// 2. Create prediction engine
const engine = new PredictionEngine()

// 3. Create risk tracker
const drawdownTracker = new DrawdownTracker()

// 4. Create interval tracker with lifecycle hooks
const tracker = new IntervalTracker({
  onIntervalClose: async (record) => {
    // Persist to disk
    await store.append(record)
    
    // Feed outcome to engine for learning
    if (record.earlyPrediction && record.earlyPredictionCorrect != null) {
      engine.recordOutcome(
        record.earlyPredictionCorrect,
        record.earlyPrediction.probability
      )
      
      // Update risk tracking
      drawdownTracker.recordOutcome(
        record.earlyPredictionCorrect,
        record.earlyPrediction.probability
      )
      
      if (record.betSize != null && record.betSize > 0) {
        drawdownTracker.recordTrade(
          record.betSize,
          record.earlyPredictionCorrect
        )
      }
    }
  },
  
  onIntervalStart: () => {
    // Reset momentum (not volatility) at interval boundaries
    engine.resetMomentum()
  }
})

// 5. Load previous history
tracker.loadHistory(savedHistory)

// 6. Main loop
while (true) {
  const epochTs = getCurrent5mTimestamp()
  const nowSec = Math.floor(Date.now() / 1000)
  const remainingSec = (epochTs + 300) - nowSec
  
  // ... fetch prices, make prediction ...
  
  await tracker.tick({
    currentPrice: lastPrice,
    strikePrice,
    prediction,
    rawPrediction,
    epochTimestamp: epochTs,
    timeRemainingSeconds: remainingSec,
    qMarket,
    evResult,
    betResult,
    drawdownLevel: drawdownTracker.getLevel(),
    abstentionReason: abstention?.reason ?? null,
    drawdownState: drawdownTracker.getState()
  })
  
  await new Promise(r => setTimeout(r, 1000))
}

Performance Considerations

File I/O

  • Read latency: ~1-5ms for typical history files (< 1MB)
  • Write latency: ~5-20ms depending on disk speed
  • Append operation: Full read-modify-write (O(n) where n = record count)

Memory Usage

  • ~1KB per IntervalRecord (60 fields, mostly nulls)
  • 1 day = 288 intervals = ~300KB
  • 1 month = ~9MB
  • 1 year = ~105MB

Optimization Tips

  1. Use append() for real-time: It’s designed for onIntervalClose callbacks
  2. Batch saves for bulk operations: Use save() directly when importing/exporting
  3. Archive old data: Move records older than 30 days to separate files
  4. Consider SQLite: For >100K records, switch to a database for better performance

See Also

Build docs developers (and LLMs) love