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%
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