export interface VolatilityEstimatorOptions { windowMinutes?: number; minSamples?: number; maxCadenceMs?: number; } interface PricePoint { ts: number; price: number; } /** * Estimates short-term volatility based on rolling mid-price snapshots. */ export class VolatilityEstimator { private readonly windowMinutes: number; private readonly minSamples: number; private readonly maxCadenceMs: number; private readonly history: PricePoint[] = []; constructor(options: VolatilityEstimatorOptions = {}) { this.windowMinutes = options.windowMinutes ?? 30; this.minSamples = options.minSamples ?? 10; this.maxCadenceMs = options.maxCadenceMs ?? 5_000; } update(price: number, ts: number = Date.now()): void { if (!Number.isFinite(price) || price <= 0) return; const last = this.history[this.history.length - 1]; if (last && ts - last.ts < this.maxCadenceMs) { this.history[this.history.length - 1] = { ts, price }; } else { this.history.push({ ts, price }); } const cutoff = ts - this.windowMinutes * 60 * 1000; while (this.history.length > 0 && this.history[0]!.ts < cutoff) { this.history.shift(); } } getAnnualizedVolatility(): number | undefined { if (this.history.length < this.minSamples) return undefined; const returns = this.computeLogReturns(this.history); if (returns.length === 0) return undefined; const stdDev = this.standardDeviation(returns); const periodsPerYear = (365 * 24 * 60) / this.windowMinutes; return stdDev * Math.sqrt(periodsPerYear); } getHourlyVolatilityBps(): number | undefined { if (this.history.length < 2) return undefined; const windowMs = 60 * 60 * 1000; const latest = this.history[this.history.length - 1]!; const cutoff = latest.ts - windowMs; const relevant = this.history.filter(point => point.ts >= cutoff); if (relevant.length < 2) return undefined; const first = relevant[0]!.price; const last = relevant[relevant.length - 1]!.price; const range = Math.abs(last - first) / first; return range * 10_000; } getStatus() { const oldest = this.history[0]; const latest = this.history[this.history.length - 1]; return { historySize: this.history.length, oldestTs: oldest?.ts, latestPrice: latest?.price, annualizedVol: this.getAnnualizedVolatility(), hourlyVolBps: this.getHourlyVolatilityBps() }; } private computeLogReturns(series: PricePoint[]): number[] { const returns: number[] = []; for (let i = 1; i < series.length; i += 1) { const prev = series[i - 1]!.price; const curr = series[i]!.price; if (prev <= 0 || curr <= 0) continue; returns.push(Math.log(curr / prev)); } return returns; } private standardDeviation(values: number[]): number { if (values.length === 0) return 0; const mean = values.reduce((sum, v) => sum + v, 0) / values.length; const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length; return Math.sqrt(variance); } }