SPB Git

spb/vquant Public MIT

VibeQuant — AI-powered institutional-grade financial intelligence platform.

TypeScript 84.3% Python 11.7% JavaScript 1.6% CSS 1.5% HTML 0.7%
8.2 KB · 298 lines typescript
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      server/services/python/garch.ts6 *7 *  Author:    Simon-Pierre Boucher8 *  Contact:   contact@spboucher.ai9 *  Website:   https://www.spboucher.ai10 *  Demo:      https://www.vquant.ai11 *  License:   MIT (see LICENSE)12 *13 *  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import { spawn } from 'child_process';18import path from 'path';19import { fileURLToPath } from 'url';20import { dirname } from 'path';2122const __filename = fileURLToPath(import.meta.url);23const __dirname = dirname(__filename);2425// Determine Python executable path - use venv if available26const PYTHON_PATH = process.env.PYTHON_PATH ||27                    path.join(process.cwd(), '.venv', 'bin', 'python') ||28                    'python3';293031export interface GarchInput {32  symbol: string;33  p?: number;  // GARCH p parameter (default: 1)34  q?: number;  // GARCH q parameter (default: 1)35  forecast_horizon?: number;  // Number of days to forecast (default: 30)36  data_period?: number;  // Historical data period in days (default: 504)37}3839export interface GarchParameters {40  all_params: Record<string, number>;41  omega: number;42  alpha: number[];43  beta: number[];44  persistence: number;45  mean: number;46}4748export interface GarchVolatilityStats {49  current_volatility: number;50  average_volatility: number;51  min_volatility: number;52  max_volatility: number;53  std_volatility: number;54}5556export interface GarchHistoricalVolatility {57  dates: string[];58  values: number[];59}6061export interface GarchForecast {62  dates: string[];63  values: number[];64  horizon: number;65}6667export interface GarchInterpretation {68  is_mean_reverting: boolean;69  persistence_level: string;70  shock_impact: string;71  volatility_clustering: string;72}7374export interface GarchResult {75  success: boolean;76  error?: string;77  symbol?: string;78  model_specification?: {79    type: string;80    p: number;81    q: number;82    mean_model: string;83    distribution: string;84  };85  parameters?: GarchParameters;86  model_fit?: {87    aic: number;88    bic: number;89    log_likelihood: number;90    num_observations: number;91  };92  volatility_statistics?: GarchVolatilityStats;93  historical_volatility?: GarchHistoricalVolatility;94  volatility_forecast?: GarchForecast;95  interpretation?: GarchInterpretation;96}9798export interface GarchExecutionResult {99  success: boolean;100  result?: GarchResult;101  code: string;102  error?: string;103}104105/**106 * Validate GARCH input107 */108function validateGarchInput(input: GarchInput): { valid: boolean; error?: string } {109  if (!input.symbol || typeof input.symbol !== 'string') {110    return { valid: false, error: 'symbol must be a string' };111  }112113  if (input.p !== undefined && (input.p < 1 || input.p > 5)) {114    return { valid: false, error: 'p must be between 1 and 5' };115  }116117  if (input.q !== undefined && (input.q < 1 || input.q > 5)) {118    return { valid: false, error: 'q must be between 1 and 5' };119  }120121  if (input.forecast_horizon !== undefined && (input.forecast_horizon < 1 || input.forecast_horizon > 252)) {122    return { valid: false, error: 'forecast_horizon must be between 1 and 252' };123  }124125  if (input.data_period !== undefined && (input.data_period < 30 || input.data_period > 2520)) {126    return { valid: false, error: 'data_period must be between 30 and 2520' };127  }128129  return { valid: true };130}131132/**133 * Execute GARCH volatility model using Python134 */135export async function executeGarchModel(136  input: GarchInput137): Promise<GarchExecutionResult> {138  try {139    // Validate input140    const validation = validateGarchInput(input);141    if (!validation.valid) {142      return {143        success: false,144        code: generateGarchCode(input),145        error: validation.error146      };147    }148149    const pythonScriptPath = path.join(__dirname, 'garchService.py');150    const inputJson = JSON.stringify(input);151152    return new Promise((resolve) => {153      const pythonProcess = spawn(PYTHON_PATH, [pythonScriptPath], {154        stdio: ['pipe', 'pipe', 'pipe'],155        env: process.env156      });157158      let stdout = '';159      let stderr = '';160161      pythonProcess.stdout.on('data', (data) => {162        stdout += data.toString();163      });164165      pythonProcess.stderr.on('data', (data) => {166        stderr += data.toString();167      });168169      pythonProcess.on('close', (code) => {170        if (code !== 0 || (stderr && !stdout)) {171          resolve({172            success: false,173            code: generateGarchCode(input),174            error: stderr || `Python process exited with code ${code}`175          });176          return;177        }178179        try {180          const result = JSON.parse(stdout) as GarchResult;181          resolve({182            success: result.success,183            result,184            code: generateGarchCode(input),185            error: result.error186          });187        } catch (parseError) {188          resolve({189            success: false,190            code: generateGarchCode(input),191            error: 'Failed to parse Python output: ' + (parseError instanceof Error ? parseError.message : 'Unknown error')192          });193        }194      });195196      pythonProcess.on('error', (error) => {197        resolve({198          success: false,199          code: generateGarchCode(input),200          error: 'Failed to start Python process: ' + error.message201        });202      });203204      pythonProcess.stdin.write(inputJson);205      pythonProcess.stdin.end();206    });207  } catch (error) {208    return {209      success: false,210      code: generateGarchCode(input),211      error: error instanceof Error ? error.message : 'Unknown error occurred'212    };213  }214}215216/**217 * Generate readable Python code for display218 */219function generateGarchCode(input: GarchInput): string {220  const p = input.p || 1;221  const q = input.q || 1;222  const horizon = input.forecast_horizon || 30;223  const period = input.data_period || 504;224225  return `import numpy as np226import pandas as pd227from arch import arch_model228from fmpClient import get_historical_prices229230# Fetch historical prices for ${input.symbol}231symbol = '${input.symbol}'232print(f"Fetching historical prices for {symbol}...")233hist_data = get_historical_prices(symbol)234235# Extract closing prices236prices = [h['close'] for h in reversed(hist_data['historical'])]237dates = [h['date'] for h in reversed(hist_data['historical'])]238239# Limit to ${period} most recent days240if len(prices) > ${period}:241    prices = prices[-${period}:]242    dates = dates[-${period}:]243244# Convert to pandas Series245price_series = pd.Series(prices, index=pd.to_datetime(dates))246247# Calculate percentage returns248returns = 100 * price_series.pct_change().dropna()249250# Fit GARCH(${p},${q}) model251print(f"Fitting GARCH(${p},${q}) model...")252model = arch_model(253    returns,254    vol='Garch',255    p=${p},256    q=${q},257    mean='constant',258    dist='normal'259)260261model_fit = model.fit(disp='off')262263# Display model parameters264print("\\nModel Parameters:")265print(model_fit.params)266267# Extract GARCH parameters268omega = model_fit.params['omega']269alpha = [model_fit.params[f'alpha[{i+1}]'] for i in range(${q})]270beta = [model_fit.params[f'beta[{i+1}]'] for i in range(${p})]271272# Calculate persistence273persistence = sum(alpha) + sum(beta)274print(f"\\nPersistence: {persistence:.4f}")275print(f"Mean reverting: {persistence < 1}")276277# Get conditional volatility (annualized)278conditional_vol = model_fit.conditional_volatility * np.sqrt(252) / 100279current_volatility = conditional_vol.iloc[-1]280print(f"\\nCurrent Volatility: {current_volatility*100:.2f}%")281282# Forecast volatility283print(f"\\nForecasting volatility for ${horizon} days...")284forecasts = model_fit.forecast(horizon=${horizon})285forecast_variance = forecasts.variance.values[-1, :]286forecast_volatility = np.sqrt(forecast_variance) * np.sqrt(252) / 100287288print(f"\\nForecast Mean Volatility: {np.mean(forecast_volatility)*100:.2f}%")289print(f"Forecast Min Volatility: {np.min(forecast_volatility)*100:.2f}%")290print(f"Forecast Max Volatility: {np.max(forecast_volatility)*100:.2f}%")291292# Model fit statistics293print(f"\\nModel Fit:")294print(f"  AIC: {model_fit.aic:.2f}")295print(f"  BIC: {model_fit.bic:.2f}")296print(f"  Log-Likelihood: {model_fit.loglikelihood:.2f}")`;297}298