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%
10.1 KB · 322 lines typescript
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      server/services/python/monteCarlo.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';2930export interface MonteCarloInput {31  symbol?: string;  // NEW: Automatically fetch data from FMP API32  historical_prices?: number[];  // ALTERNATIVE: Manual data mode33  num_simulations?: number;34  time_horizon?: number;35  initial_investment?: number;36}3738export interface MonteCarloResult {39  success: boolean;40  error?: string;41  parameters?: {42    num_simulations: number;43    time_horizon: number;44    initial_investment: number;45    mean_return: number;46    std_return: number;47  };48  statistics?: {49    mean_final_value: number;50    median_final_value: number;51    std_final_value: number;52    min_final_value: number;53    max_final_value: number;54    percentile_5: number;55    percentile_25: number;56    percentile_75: number;57    percentile_95: number;58    probability_of_profit: number;59  };60  sample_paths?: number[][];61  final_values_distribution?: {62    bins: number[];63    counts: number[];64  };65}6667export interface PythonExecutionResult {68  success: boolean;69  result?: MonteCarloResult;70  code: string;71  error?: string;72}7374/**75 * Validate Monte Carlo input76 */77function validateInput(input: MonteCarloInput): { valid: boolean; error?: string } {78  // Check if either symbol or historical_prices is provided79  if (!input.symbol && !input.historical_prices) {80    return { valid: false, error: 'Either symbol or historical_prices must be provided' };81  }8283  // If historical_prices provided, validate it84  if (input.historical_prices) {85    if (!Array.isArray(input.historical_prices)) {86      return { valid: false, error: 'historical_prices must be an array' };87    }8889    if (input.historical_prices.length < 2) {90      return { valid: false, error: 'historical_prices must have at least 2 values' };91    }9293    if (!input.historical_prices.every(p => typeof p === 'number' && !isNaN(p))) {94      return { valid: false, error: 'historical_prices must contain only valid numbers' };95    }96  }9798  // If symbol provided, validate it99  if (input.symbol && typeof input.symbol !== 'string') {100    return { valid: false, error: 'symbol must be a string' };101  }102  103  if (input.num_simulations !== undefined && (input.num_simulations < 1 || input.num_simulations > 100000)) {104    return { valid: false, error: 'num_simulations must be between 1 and 100000' };105  }106  107  if (input.time_horizon !== undefined && (input.time_horizon < 1 || input.time_horizon > 10000)) {108    return { valid: false, error: 'time_horizon must be between 1 and 10000' };109  }110  111  if (input.initial_investment !== undefined && input.initial_investment <= 0) {112    return { valid: false, error: 'initial_investment must be greater than 0' };113  }114  115  return { valid: true };116}117118/**119 * Execute Monte Carlo simulation using Python (secure spawn-based implementation)120 */121export async function executeMonteCarloSimulation(122  input: MonteCarloInput123): Promise<PythonExecutionResult> {124  try {125    // Validate input126    const validation = validateInput(input);127    if (!validation.valid) {128      return {129        success: false,130        code: generatePythonCode(input),131        error: validation.error132      };133    }134    135    const pythonScriptPath = path.join(__dirname, 'monteCarloService.py');136    const inputJson = JSON.stringify(input);137    138    return new Promise((resolve) => {139      // Use spawn without shell to prevent command injection140      const pythonProcess = spawn(PYTHON_PATH, [pythonScriptPath], {141        stdio: ['pipe', 'pipe', 'pipe']142      });143      144      let stdout = '';145      let stderr = '';146      147      pythonProcess.stdout.on('data', (data) => {148        stdout += data.toString();149      });150      151      pythonProcess.stderr.on('data', (data) => {152        stderr += data.toString();153      });154      155      pythonProcess.on('close', (code) => {156        if (code !== 0 || (stderr && !stdout)) {157          resolve({158            success: false,159            code: generatePythonCode(input),160            error: stderr || `Python process exited with code ${code}`161          });162          return;163        }164        165        try {166          const result = JSON.parse(stdout) as MonteCarloResult;167          resolve({168            success: result.success,169            result,170            code: generatePythonCode(input),171            error: result.error172          });173        } catch (parseError) {174          resolve({175            success: false,176            code: generatePythonCode(input),177            error: 'Failed to parse Python output: ' + (parseError instanceof Error ? parseError.message : 'Unknown error')178          });179        }180      });181      182      pythonProcess.on('error', (error) => {183        resolve({184          success: false,185          code: generatePythonCode(input),186          error: 'Failed to start Python process: ' + error.message187        });188      });189      190      // Write JSON to stdin (secure - no shell interpretation)191      pythonProcess.stdin.write(inputJson);192      pythonProcess.stdin.end();193    });194  } catch (error) {195    return {196      success: false,197      code: generatePythonCode(input),198      error: error instanceof Error ? error.message : 'Unknown error occurred'199    };200  }201}202203/**204 * Generate readable Python code for display205 */206function generatePythonCode(input: MonteCarloInput): string {207  const numSims = input.num_simulations || 10000;208  const timeHorizon = input.time_horizon || 252;209  const initialInv = input.initial_investment || 10000;210211  if (input.symbol) {212    // Generate code for symbol-based mode213    return `import numpy as np214from scipy import stats215import pandas as pd216from fmpClient import get_historical_prices217218# Fetch historical prices from FMP API219symbol = '${input.symbol}'220print(f"Fetching historical prices for {symbol}...")221hist_data = get_historical_prices(symbol)222historical_prices = np.array([h['close'] for h in reversed(hist_data['historical'])])223print(f"Fetched {len(historical_prices)} historical prices")224225# Simulation parameters226num_simulations = ${numSims}227time_horizon = ${timeHorizon}  # days228initial_investment = ${initialInv}229230# Calculate daily returns231returns = np.diff(historical_prices) / historical_prices[:-1]232mean_return = np.mean(returns)233std_return = np.std(returns)234235# Run Monte Carlo simulations236simulation_results = np.zeros((num_simulations, time_horizon))237final_values = np.zeros(num_simulations)238239for i in range(num_simulations):240    # Generate random returns based on historical distribution241    daily_returns = np.random.normal(mean_return, std_return, time_horizon)242243    # Calculate price path244    price_path = initial_investment * np.cumprod(1 + daily_returns)245    simulation_results[i] = price_path246    final_values[i] = price_path[-1]247248# Calculate statistics249mean_final_value = np.mean(final_values)250median_final_value = np.median(final_values)251std_final_value = np.std(final_values)252253# Calculate percentiles254percentile_5 = np.percentile(final_values, 5)255percentile_95 = np.percentile(final_values, 95)256257# Calculate probability of profit258prob_profit = np.sum(final_values > initial_investment) / num_simulations * 100259260print(f"Mean Final Value: $${'{'}mean_final_value:,.2f{'}'}")261print(f"Median Final Value: $${'{'}median_final_value:,.2f{'}'}")262print(f"5th Percentile: $${'{'}percentile_5:,.2f{'}'}")263print(f"95th Percentile: $${'{'}percentile_95:,.2f{'}'}")264print(f"Probability of Profit: ${'{'}prob_profit:.2f{'}'}%")`;265  } else {266    // Generate code for manual historical_prices mode267    const historicalPrices = input.historical_prices || [];268    return `import numpy as np269from scipy import stats270import pandas as pd271272# Historical prices from FMP data273historical_prices = np.array(${JSON.stringify(historicalPrices)})274275# Simulation parameters276num_simulations = ${numSims}277time_horizon = ${timeHorizon}  # days278initial_investment = ${initialInv}279280# Calculate daily returns281returns = np.diff(historical_prices) / historical_prices[:-1]282mean_return = np.mean(returns)283std_return = np.std(returns)284285# Run Monte Carlo simulations286simulation_results = np.zeros((num_simulations, time_horizon))287final_values = np.zeros(num_simulations)288289for i in range(num_simulations):290    # Generate random returns based on historical distribution291    daily_returns = np.random.normal(mean_return, std_return, time_horizon)292    293    # Calculate price path294    price_path = initial_investment * np.cumprod(1 + daily_returns)295    simulation_results[i] = price_path296    final_values[i] = price_path[-1]297298# Calculate statistics299mean_final_value = np.mean(final_values)300median_final_value = np.median(final_values)301std_final_value = np.std(final_values)302303# Calculate percentiles304percentile_5 = np.percentile(final_values, 5)305percentile_95 = np.percentile(final_values, 95)306307# Calculate probability of profit308prob_profit = np.sum(final_values > initial_investment) / num_simulations * 100309310print(f"Mean Final Value: $${'{'}mean_final_value:,.2f{'}'}")311print(f"Median Final Value: $${'{'}median_final_value:,.2f{'}'}")312print(f"5th Percentile: $${'{'}percentile_5:,.2f{'}'}")313print(f"95th Percentile: $${'{'}percentile_95:,.2f{'}'}")314print(f"Probability of Profit: ${'{'}prob_profit:.2f{'}'}%")`;315  }316}317318// ============================================================================319// Options Pricing with Black-Scholes Model320// ============================================================================321322