volatilityEstimator.ts 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. export interface VolatilityEstimatorOptions {
  2. windowMinutes?: number;
  3. minSamples?: number;
  4. maxCadenceMs?: number;
  5. }
  6. interface PricePoint {
  7. ts: number;
  8. price: number;
  9. }
  10. /**
  11. * Estimates short-term volatility based on rolling mid-price snapshots.
  12. */
  13. export class VolatilityEstimator {
  14. private readonly windowMinutes: number;
  15. private readonly minSamples: number;
  16. private readonly maxCadenceMs: number;
  17. private readonly history: PricePoint[] = [];
  18. constructor(options: VolatilityEstimatorOptions = {}) {
  19. this.windowMinutes = options.windowMinutes ?? 30;
  20. this.minSamples = options.minSamples ?? 10;
  21. this.maxCadenceMs = options.maxCadenceMs ?? 5_000;
  22. }
  23. update(price: number, ts: number = Date.now()): void {
  24. if (!Number.isFinite(price) || price <= 0) return;
  25. const last = this.history[this.history.length - 1];
  26. if (last && ts - last.ts < this.maxCadenceMs) {
  27. this.history[this.history.length - 1] = { ts, price };
  28. } else {
  29. this.history.push({ ts, price });
  30. }
  31. const cutoff = ts - this.windowMinutes * 60 * 1000;
  32. while (this.history.length > 0 && this.history[0]!.ts < cutoff) {
  33. this.history.shift();
  34. }
  35. }
  36. getAnnualizedVolatility(): number | undefined {
  37. if (this.history.length < this.minSamples) return undefined;
  38. const returns = this.computeLogReturns(this.history);
  39. if (returns.length === 0) return undefined;
  40. const stdDev = this.standardDeviation(returns);
  41. const periodsPerYear = (365 * 24 * 60) / this.windowMinutes;
  42. return stdDev * Math.sqrt(periodsPerYear);
  43. }
  44. getHourlyVolatilityBps(): number | undefined {
  45. if (this.history.length < 2) return undefined;
  46. const windowMs = 60 * 60 * 1000;
  47. const latest = this.history[this.history.length - 1]!;
  48. const cutoff = latest.ts - windowMs;
  49. const relevant = this.history.filter(point => point.ts >= cutoff);
  50. if (relevant.length < 2) return undefined;
  51. const first = relevant[0]!.price;
  52. const last = relevant[relevant.length - 1]!.price;
  53. const range = Math.abs(last - first) / first;
  54. return range * 10_000;
  55. }
  56. getStatus() {
  57. const oldest = this.history[0];
  58. const latest = this.history[this.history.length - 1];
  59. return {
  60. historySize: this.history.length,
  61. oldestTs: oldest?.ts,
  62. latestPrice: latest?.price,
  63. annualizedVol: this.getAnnualizedVolatility(),
  64. hourlyVolBps: this.getHourlyVolatilityBps()
  65. };
  66. }
  67. private computeLogReturns(series: PricePoint[]): number[] {
  68. const returns: number[] = [];
  69. for (let i = 1; i < series.length; i += 1) {
  70. const prev = series[i - 1]!.price;
  71. const curr = series[i]!.price;
  72. if (prev <= 0 || curr <= 0) continue;
  73. returns.push(Math.log(curr / prev));
  74. }
  75. return returns;
  76. }
  77. private standardDeviation(values: number[]): number {
  78. if (values.length === 0) return 0;
  79. const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
  80. const variance =
  81. values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;
  82. return Math.sqrt(variance);
  83. }
  84. }