Skip to main content
Glama
Ademscodeisnotsobad

Quant Companion MCP

forecasts.ts6.88 kB
/** * Forecasting utilities and backtest evaluation helpers * * Provides functions for evaluating forecast accuracy and computing * forecast statistics across different models. */ import { mean, stdDev, percentile } from "./utils"; export interface ForecastDistribution { /** Distribution mean */ mean: number; /** Distribution median */ median: number; /** Distribution standard deviation */ std: number; /** 5th percentile */ percentile5: number; /** 25th percentile */ percentile25: number; /** 75th percentile */ percentile75: number; /** 95th percentile */ percentile95: number; /** Probability of price being above target (e.g., spot * 1.2) */ probAboveTarget?: number; } export interface ForecastEvaluation { /** Forecast mean */ forecastMean: number; /** Realized price */ realizedPrice: number; /** Absolute error */ absoluteError: number; /** Relative error (signed) */ relativeError: number; /** Percentile of realized in forecast distribution */ realizedPercentile: number; /** Whether realized was inside 95% CI */ insideCI95: boolean; } export interface ForecastBacktestResult { /** Model name */ model: string; /** Number of forecast periods evaluated */ periods: number; /** Mean absolute error */ mae: number; /** Mean relative error */ mre: number; /** Root mean square error */ rmse: number; /** 95% CI coverage rate (should be ~0.95 for well-calibrated model) */ coverage95: number; /** Average realized percentile (should be ~50% for unbiased model) */ avgRealizedPercentile: number; /** Individual period evaluations (sample) */ evaluations: ForecastEvaluation[]; } /** * Compute distribution statistics from terminal prices */ export function computeDistributionStats( terminalPrices: number[], targetPrice?: number ): ForecastDistribution { if (terminalPrices.length === 0) { throw new Error("Cannot compute stats from empty array"); } const sorted = [...terminalPrices].sort((a, b) => a - b); const n = terminalPrices.length; const result: ForecastDistribution = { mean: mean(terminalPrices), median: percentile(sorted, 0.5), std: stdDev(terminalPrices), percentile5: percentile(sorted, 0.05), percentile25: percentile(sorted, 0.25), percentile75: percentile(sorted, 0.75), percentile95: percentile(sorted, 0.95), }; if (targetPrice !== undefined) { const countAbove = terminalPrices.filter((p) => p >= targetPrice).length; result.probAboveTarget = countAbove / n; } return result; } /** * Evaluate a single forecast against realized outcome */ export function evaluateForecast( terminalPrices: number[], realizedPrice: number ): ForecastEvaluation { const sorted = [...terminalPrices].sort((a, b) => a - b); const n = terminalPrices.length; const forecastMean = mean(terminalPrices); // Find percentile of realized price in distribution let countBelow = 0; for (const p of sorted) { if (p < realizedPrice) countBelow++; else break; } const realizedPercentile = countBelow / n; // 95% CI bounds const ci95Lower = percentile(sorted, 0.025); const ci95Upper = percentile(sorted, 0.975); const insideCI95 = realizedPrice >= ci95Lower && realizedPrice <= ci95Upper; return { forecastMean, realizedPrice, absoluteError: Math.abs(forecastMean - realizedPrice), relativeError: (forecastMean - realizedPrice) / realizedPrice, realizedPercentile, insideCI95, }; } /** * Aggregate multiple forecast evaluations into summary statistics */ export function aggregateForecastEvaluations( evaluations: ForecastEvaluation[], model: string ): ForecastBacktestResult { if (evaluations.length === 0) { return { model, periods: 0, mae: NaN, mre: NaN, rmse: NaN, coverage95: NaN, avgRealizedPercentile: NaN, evaluations: [], }; } const n = evaluations.length; const mae = mean(evaluations.map((e) => e.absoluteError)); const mre = mean(evaluations.map((e) => e.relativeError)); const rmse = Math.sqrt(mean(evaluations.map((e) => e.absoluteError ** 2))); const coverage95 = evaluations.filter((e) => e.insideCI95).length / n; const avgRealizedPercentile = mean(evaluations.map((e) => e.realizedPercentile)); return { model, periods: n, mae, mre, rmse, coverage95, avgRealizedPercentile, evaluations: evaluations.slice(0, 20), // Return sample of individual evaluations }; } /** * Model disagreement metrics */ export interface DisagreementMetrics { /** Spread of medians across models */ medianSpread: number; /** Spread of 95th percentiles */ percentile95Spread: number; /** Spread of probUp20 estimates */ probUp20Spread: number; /** Overall disagreement level */ level: "LOW" | "MEDIUM" | "HIGH"; } /** * Compute disagreement metrics across multiple model forecasts */ export function computeDisagreement( forecasts: Array<{ median: number; percentile95: number; probUp20: number }> ): DisagreementMetrics { if (forecasts.length < 2) { return { medianSpread: 0, percentile95Spread: 0, probUp20Spread: 0, level: "LOW", }; } const medians = forecasts.map((f) => f.median); const p95s = forecasts.map((f) => f.percentile95); const probs = forecasts.map((f) => f.probUp20); const medianMean = mean(medians); const medianSpread = medianMean > 0 ? (Math.max(...medians) - Math.min(...medians)) / medianMean : 0; const p95Mean = mean(p95s); const percentile95Spread = p95Mean > 0 ? (Math.max(...p95s) - Math.min(...p95s)) / p95Mean : 0; const probUp20Spread = Math.max(...probs) - Math.min(...probs); // Classify disagreement level let level: "LOW" | "MEDIUM" | "HIGH"; const avgSpread = (medianSpread + percentile95Spread + probUp20Spread) / 3; if (avgSpread < 0.1) { level = "LOW"; } else if (avgSpread < 0.25) { level = "MEDIUM"; } else { level = "HIGH"; } return { medianSpread, percentile95Spread, probUp20Spread, level, }; } /** * Convert horizon in days to years */ export function daysToYears(days: number): number { return days / 365.25; } /** * Add business days to a date (simple approximation) */ export function addBusinessDays(date: Date, days: number): Date { const result = new Date(date); let remaining = days; while (remaining > 0) { result.setDate(result.getDate() + 1); const dayOfWeek = result.getDay(); if (dayOfWeek !== 0 && dayOfWeek !== 6) { remaining--; } } return result; } /** * Get calendar days between two dates */ export function daysBetween(start: Date, end: Date): number { const msPerDay = 24 * 60 * 60 * 1000; return Math.round((end.getTime() - start.getTime()) / msPerDay); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Ademscodeisnotsobad/Quant-Companion-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server