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/var.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 VarInput {32 symbol: string;33 portfolio_value?: number;34 confidence_levels?: number[];35 time_horizon?: number;36 num_simulations?: number;37 data_period?: number;38}3940export interface VarByConfidence {41 confidence_level: number;42 historical: {43 var_percentage: number;44 var_dollar: number;45 };46 parametric: {47 var_percentage: number;48 var_dollar: number;49 };50 monte_carlo: {51 var_percentage: number;52 var_dollar: number;53 };54 conditional_var: {55 cvar_percentage: number;56 cvar_dollar: number;57 };58}5960export interface VarResult {61 success: boolean;62 error?: string;63 symbol?: string;64 parameters?: {65 portfolio_value: number;66 time_horizon: number;67 time_horizon_description: string;68 num_simulations: number;69 data_points: number;70 data_period_days: number;71 };72 var_by_confidence?: Record<string, VarByConfidence>;73 distribution_statistics?: {74 mean_daily_return: number;75 std_daily_return: number;76 annual_return: number;77 annual_volatility: number;78 skewness: number;79 kurtosis: number;80 is_normally_distributed: boolean;81 jarque_bera_pvalue: number;82 };83 extreme_values?: {84 worst_daily_loss_pct: number;85 worst_daily_loss_dollar: number;86 best_daily_gain_pct: number;87 best_daily_gain_dollar: number;88 };89 interpretation?: {90 distribution_type: string;91 tail_risk: string;92 recommended_method: string;93 notes: string[];94 };95}9697export interface VarExecutionResult {98 success: boolean;99 result?: VarResult;100 code: string;101 error?: string;102}103104function validateVarInput(input: VarInput): { valid: boolean; error?: string } {105 if (!input.symbol || typeof input.symbol !== 'string') {106 return { valid: false, error: 'symbol must be a string' };107 }108109 if (input.portfolio_value !== undefined && input.portfolio_value <= 0) {110 return { valid: false, error: 'portfolio_value must be positive' };111 }112113 if (input.time_horizon !== undefined && (input.time_horizon < 1 || input.time_horizon > 252)) {114 return { valid: false, error: 'time_horizon must be between 1 and 252' };115 }116117 if (input.num_simulations !== undefined && (input.num_simulations < 1000 || input.num_simulations > 100000)) {118 return { valid: false, error: 'num_simulations must be between 1000 and 100000' };119 }120121 return { valid: true };122}123124export async function executeVarCalculation(125 input: VarInput126): Promise<VarExecutionResult> {127 try {128 const validation = validateVarInput(input);129 if (!validation.valid) {130 return {131 success: false,132 code: generateVarCode(input),133 error: validation.error134 };135 }136137 const pythonScriptPath = path.join(__dirname, 'varService.py');138 const inputJson = JSON.stringify(input);139140 return new Promise((resolve) => {141 const pythonProcess = spawn(PYTHON_PATH, [pythonScriptPath], {142 stdio: ['pipe', 'pipe', 'pipe'],143 env: process.env144 });145146 let stdout = '';147 let stderr = '';148149 pythonProcess.stdout.on('data', (data) => {150 stdout += data.toString();151 });152153 pythonProcess.stderr.on('data', (data) => {154 stderr += data.toString();155 });156157 pythonProcess.on('close', (code) => {158 if (code !== 0 || (stderr && !stdout)) {159 resolve({160 success: false,161 code: generateVarCode(input),162 error: stderr || `Python process exited with code ${code}`163 });164 return;165 }166167 try {168 const result = JSON.parse(stdout) as VarResult;169 resolve({170 success: result.success,171 result,172 code: generateVarCode(input),173 error: result.error174 });175 } catch (parseError) {176 resolve({177 success: false,178 code: generateVarCode(input),179 error: 'Failed to parse Python output: ' + (parseError instanceof Error ? parseError.message : 'Unknown error')180 });181 }182 });183184 pythonProcess.on('error', (error) => {185 resolve({186 success: false,187 code: generateVarCode(input),188 error: 'Failed to start Python process: ' + error.message189 });190 });191192 pythonProcess.stdin.write(inputJson);193 pythonProcess.stdin.end();194 });195 } catch (error) {196 return {197 success: false,198 code: generateVarCode(input),199 error: error instanceof Error ? error.message : 'Unknown error occurred'200 };201 }202}203204function generateVarCode(input: VarInput): string {205 const portfolioValue = input.portfolio_value || 100000;206 const confidenceLevels = input.confidence_levels || [0.90, 0.95, 0.99];207 const timeHorizon = input.time_horizon || 1;208 const numSims = input.num_simulations || 10000;209210 return `import numpy as np211from scipy import stats212from fmpClient import get_historical_prices213214# Fetch historical data for ${input.symbol}215symbol = '${input.symbol}'216print(f"Fetching historical prices for {symbol}...")217hist_data = get_historical_prices(symbol)218prices = np.array([h['close'] for h in reversed(hist_data['historical'])])219220# Calculate daily returns221returns = np.diff(prices) / prices[:-1]222223# Portfolio parameters224portfolio_value = ${portfolioValue}225confidence_levels = ${JSON.stringify(confidenceLevels)}226time_horizon = ${timeHorizon} # days227228# Scale returns for time horizon229if time_horizon > 1:230 returns_scaled = returns * np.sqrt(time_horizon)231else:232 returns_scaled = returns233234# Calculate VaR using different methods235for conf_level in confidence_levels:236 # Historical VaR237 hist_var = -np.percentile(returns_scaled, (1 - conf_level) * 100)238 hist_var_dollar = hist_var * portfolio_value239240 # Parametric VaR (assumes normal distribution)241 mean = np.mean(returns_scaled)242 std = np.std(returns_scaled)243 z_score = stats.norm.ppf(1 - conf_level)244 param_var = -(mean + z_score * std)245 param_var_dollar = param_var * portfolio_value246247 # Monte Carlo VaR248 simulated = np.random.normal(mean, std, ${numSims})249 mc_var = -np.percentile(simulated, (1 - conf_level) * 100)250 mc_var_dollar = mc_var * portfolio_value251252 # Conditional VaR (CVaR)253 var_threshold = -hist_var254 tail_losses = returns_scaled[returns_scaled <= var_threshold]255 cvar = -np.mean(tail_losses) if len(tail_losses) > 0 else hist_var256 cvar_dollar = cvar * portfolio_value257258 print(f"\\n{int(conf_level*100)}% Confidence Level:")259 print(f" Historical VaR: {hist_var*100:.2f}% ($\{hist_var_dollar:,.2f})")260 print(f" Parametric VaR: {param_var*100:.2f}% ($\{param_var_dollar:,.2f})")261 print(f" Monte Carlo VaR: {mc_var*100:.2f}% ($\{mc_var_dollar:,.2f})")262 print(f" CVaR (Expected Shortfall): {cvar*100:.2f}% ($\{cvar_dollar:,.2f})")`;263}264265// ============================================================================266// Portfolio Optimizer267// ============================================================================268269export interface PortfolioOptimizerInput {270 symbols: string[];271 risk_free_rate?: number;272 min_weight?: number;273 max_weight?: number;274 data_period?: number;275 generate_frontier?: boolean;276 frontier_points?: number;277}278279export interface PortfolioAllocation {280 description: string;281 allocation: Record<string, number>;282 expected_return: number;283 volatility: number;284 sharpe_ratio: number;285 optimization_success?: boolean;286}287288export interface PortfolioOptimizerResult {289 success: boolean;290 error?: string;291 symbols?: string[];292 parameters?: {293 num_assets: number;294 risk_free_rate: number;295 min_weight: number;296 max_weight: number;297 data_points: number;298 data_period_days: number;299 };300 asset_statistics?: Record<string, {301 expected_return: number;302 volatility: number;303 sharpe_ratio: number;304 }>;305 correlation_matrix?: Record<string, Record<string, number>>;306 optimal_portfolios?: {307 max_sharpe_ratio: PortfolioAllocation;308 min_volatility: PortfolioAllocation;309 equal_weight: PortfolioAllocation;310 };311 efficient_frontier?: {312 returns: number[];313 volatilities: number[];314 sharpe_ratios: number[];315 };316 interpretation?: {317 diversification_benefit: number;318 notes: string[];319 };320}321322export interface PortfolioOptimizerExecutionResult {323 success: boolean;324 result?: PortfolioOptimizerResult;325 code: string;326 error?: string;327}328329function validatePortfolioInput(input: PortfolioOptimizerInput): { valid: boolean; error?: string } {330 if (!input.symbols || !Array.isArray(input.symbols) || input.symbols.length < 2) {331 return { valid: false, error: 'At least 2 symbols are required' };332 }333334 if (input.risk_free_rate !== undefined && (input.risk_free_rate < 0 || input.risk_free_rate > 1)) {335 return { valid: false, error: 'risk_free_rate must be between 0 and 1' };336 }337338 if (input.min_weight !== undefined && (input.min_weight < 0 || input.min_weight > 1)) {339 return { valid: false, error: 'min_weight must be between 0 and 1' };340 }341342 if (input.max_weight !== undefined && (input.max_weight < 0 || input.max_weight > 1)) {343 return { valid: false, error: 'max_weight must be between 0 and 1' };344 }345346 return { valid: true };347}348349export async function executePortfolioOptimization(350 input: PortfolioOptimizerInput351): Promise<PortfolioOptimizerExecutionResult> {352 try {353 const validation = validatePortfolioInput(input);354 if (!validation.valid) {355 return {356 success: false,357 code: generatePortfolioCode(input),358 error: validation.error359 };360 }361362 const pythonScriptPath = path.join(__dirname, 'portfolioOptimizer.py');363 const inputJson = JSON.stringify(input);364365 return new Promise((resolve) => {366 const pythonProcess = spawn(PYTHON_PATH, [pythonScriptPath], {367 stdio: ['pipe', 'pipe', 'pipe'],368 env: process.env369 });370371 let stdout = '';372 let stderr = '';373374 pythonProcess.stdout.on('data', (data) => {375 stdout += data.toString();376 });377378 pythonProcess.stderr.on('data', (data) => {379 stderr += data.toString();380 });381382 pythonProcess.on('close', (code) => {383 if (code !== 0 || (stderr && !stdout)) {384 resolve({385 success: false,386 code: generatePortfolioCode(input),387 error: stderr || `Python process exited with code ${code}`388 });389 return;390 }391392 try {393 const result = JSON.parse(stdout) as PortfolioOptimizerResult;394 resolve({395 success: result.success,396 result,397 code: generatePortfolioCode(input),398 error: result.error399 });400 } catch (parseError) {401 resolve({402 success: false,403 code: generatePortfolioCode(input),404 error: 'Failed to parse Python output: ' + (parseError instanceof Error ? parseError.message : 'Unknown error')405 });406 }407 });408409 pythonProcess.on('error', (error) => {410 resolve({411 success: false,412 code: generatePortfolioCode(input),413 error: 'Failed to start Python process: ' + error.message414 });415 });416417 pythonProcess.stdin.write(inputJson);418 pythonProcess.stdin.end();419 });420 } catch (error) {421 return {422 success: false,423 code: generatePortfolioCode(input),424 error: error instanceof Error ? error.message : 'Unknown error occurred'425 };426 }427}428429function generatePortfolioCode(input: PortfolioOptimizerInput): string {430 const riskFreeRate = input.risk_free_rate || 0.02;431 const symbols = input.symbols || [];432433 return `import numpy as np434import pandas as pd435from scipy.optimize import minimize436from fmpClient import get_historical_prices437438# Portfolio assets439symbols = ${JSON.stringify(symbols)}440risk_free_rate = ${riskFreeRate}441442# Fetch historical data for all symbols443prices_dict = {}444for symbol in symbols:445 print(f"Fetching data for {symbol}...")446 hist_data = get_historical_prices(symbol)447 prices = [h['close'] for h in reversed(hist_data['historical'])]448 dates = [h['date'] for h in reversed(hist_data['historical'])]449 prices_dict[symbol] = pd.Series(prices, index=pd.to_datetime(dates))450451# Create DataFrame and calculate returns452prices_df = pd.DataFrame(prices_dict).dropna()453returns_df = prices_df.pct_change().dropna()454455# Calculate mean returns (annualized) and covariance matrix456mean_returns = returns_df.mean() * 252457cov_matrix = returns_df.cov() * 252458459print("\\nExpected Annual Returns:")460for symbol, ret in mean_returns.items():461 print(f" {symbol}: {ret*100:.2f}%")462463# Portfolio optimization function464def portfolio_stats(weights):465 portfolio_return = np.sum(weights * mean_returns)466 portfolio_std = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))467 return portfolio_return, portfolio_std468469def negative_sharpe(weights):470 ret, std = portfolio_stats(weights)471 return -(ret - risk_free_rate) / std472473# Constraints and bounds474constraints = {'type': 'eq', 'fun': lambda x: np.sum(x) - 1}475bounds = tuple((0, 1) for _ in range(len(symbols)))476init_guess = np.array([1/len(symbols)] * len(symbols))477478# Optimize for maximum Sharpe ratio479result = minimize(negative_sharpe, init_guess, method='SLSQP',480 bounds=bounds, constraints=constraints)481482optimal_weights = result.x483opt_return, opt_std = portfolio_stats(optimal_weights)484sharpe = (opt_return - risk_free_rate) / opt_std485486print("\\nOptimal Portfolio (Maximum Sharpe Ratio):")487for i, symbol in enumerate(symbols):488 print(f" {symbol}: {optimal_weights[i]*100:.2f}%")489print(f"\\nExpected Return: {opt_return*100:.2f}%")490print(f"Volatility: {opt_std*100:.2f}%")491print(f"Sharpe Ratio: {sharpe:.4f}")`;492}493494// ============================================================================495// Risk Metrics Analyzer496// ============================================================================497498export interface RiskMetricsInput {499 symbol: string;500 benchmark_symbol?: string;501 risk_free_rate?: number;502 data_period?: number;503}504505export interface RiskMetricsResult {506 success: boolean;507 error?: string;508 symbol?: string;509 benchmark?: string;510 parameters?: {511 risk_free_rate: number;512 data_points: number;513 data_period_days: number;514 start_date: string;515 end_date: string;516 };517 return_metrics?: {518 annualized_return: number;519 annualized_volatility: number;520 benchmark_return: number;521 benchmark_volatility: number;522 excess_return: number;523 };524 risk_adjusted_metrics?: {525 sharpe_ratio: number;526 sortino_ratio: number;527 calmar_ratio: number;528 information_ratio: number;529 treynor_ratio: number;530 };531 market_metrics?: {532 beta: number;533 alpha_annualized: number;534 correlation: number;535 tracking_error: number;536 r_squared: number;537 };538 drawdown_metrics?: {539 max_drawdown_pct: number;540 max_drawdown_start_idx: number;541 max_drawdown_end_idx: number;542 recovery_days: number;543 currently_in_drawdown: boolean;544 current_drawdown_pct: number;545 };546 capture_ratios?: {547 upside_capture_pct: number;548 downside_capture_pct: number;549 capture_ratio: number;550 };551 trading_statistics?: {552 win_rate_pct: number;553 average_win_pct: number;554 average_loss_pct: number;555 profit_factor: number;556 win_loss_ratio: number;557 };558 interpretation?: {559 risk_rating: string;560 performance_vs_benchmark: string;561 risk_adjusted_performance: string;562 market_sensitivity: string;563 notes: string[];564 };565}566567export interface RiskMetricsExecutionResult {568 success: boolean;569 result?: RiskMetricsResult;570 code: string;571 error?: string;572}573574function validateRiskMetricsInput(input: RiskMetricsInput): { valid: boolean; error?: string } {575 if (!input.symbol || typeof input.symbol !== 'string') {576 return { valid: false, error: 'symbol must be a string' };577 }578579 if (input.risk_free_rate !== undefined && (input.risk_free_rate < 0 || input.risk_free_rate > 1)) {580 return { valid: false, error: 'risk_free_rate must be between 0 and 1' };581 }582583 return { valid: true };584}585586export async function executeRiskMetricsAnalysis(587 input: RiskMetricsInput588): Promise<RiskMetricsExecutionResult> {589 try {590 const validation = validateRiskMetricsInput(input);591 if (!validation.valid) {592 return {593 success: false,594 code: generateRiskMetricsCode(input),595 error: validation.error596 };597 }598599 const pythonScriptPath = path.join(__dirname, 'riskMetrics.py');600 const inputJson = JSON.stringify(input);601602 return new Promise((resolve) => {603 const pythonProcess = spawn(PYTHON_PATH, [pythonScriptPath], {604 stdio: ['pipe', 'pipe', 'pipe'],605 env: process.env606 });607608 let stdout = '';609 let stderr = '';610611 pythonProcess.stdout.on('data', (data) => {612 stdout += data.toString();613 });614615 pythonProcess.stderr.on('data', (data) => {616 stderr += data.toString();617 });618619 pythonProcess.on('close', (code) => {620 if (code !== 0 || (stderr && !stdout)) {621 resolve({622 success: false,623 code: generateRiskMetricsCode(input),624 error: stderr || `Python process exited with code ${code}`625 });626 return;627 }628629 try {630 const result = JSON.parse(stdout) as RiskMetricsResult;631 resolve({632 success: result.success,633 result,634 code: generateRiskMetricsCode(input),635 error: result.error636 });637 } catch (parseError) {638 resolve({639 success: false,640 code: generateRiskMetricsCode(input),641 error: 'Failed to parse Python output: ' + (parseError instanceof Error ? parseError.message : 'Unknown error')642 });643 }644 });645646 pythonProcess.on('error', (error) => {647 resolve({648 success: false,649 code: generateRiskMetricsCode(input),650 error: 'Failed to start Python process: ' + error.message651 });652 });653654 pythonProcess.stdin.write(inputJson);655 pythonProcess.stdin.end();656 });657 } catch (error) {658 return {659 success: false,660 code: generateRiskMetricsCode(input),661 error: error instanceof Error ? error.message : 'Unknown error occurred'662 };663 }664}665666function generateRiskMetricsCode(input: RiskMetricsInput): string {667 const benchmarkSymbol = input.benchmark_symbol || 'SPY';668 const riskFreeRate = input.risk_free_rate || 0.02;669670 return `import numpy as np671from fmpClient import get_historical_prices672673# Fetch asset and benchmark data674asset_symbol = '${input.symbol}'675benchmark_symbol = '${benchmarkSymbol}'676risk_free_rate = ${riskFreeRate}677678print(f"Fetching data for {asset_symbol}...")679asset_hist = get_historical_prices(asset_symbol)680asset_prices = np.array([h['close'] for h in reversed(asset_hist['historical'])])681asset_returns = np.diff(asset_prices) / asset_prices[:-1]682683print(f"Fetching benchmark data for {benchmark_symbol}...")684benchmark_hist = get_historical_prices(benchmark_symbol)685benchmark_prices = np.array([h['close'] for h in reversed(benchmark_hist['historical'])])686benchmark_returns = np.diff(benchmark_prices) / benchmark_prices[:-1]687688# Align returns689min_len = min(len(asset_returns), len(benchmark_returns))690asset_returns = asset_returns[-min_len:]691benchmark_returns = benchmark_returns[-min_len:]692693# Calculate return metrics694asset_annual_return = np.mean(asset_returns) * 252695asset_volatility = np.std(asset_returns) * np.sqrt(252)696benchmark_annual_return = np.mean(benchmark_returns) * 252697698print(f"\\nAnnualized Return: {asset_annual_return*100:.2f}%")699print(f"Annualized Volatility: {asset_volatility*100:.2f}%")700701# Sharpe Ratio702sharpe = (asset_annual_return - risk_free_rate) / asset_volatility703print(f"Sharpe Ratio: {sharpe:.4f}")704705# Beta and Alpha706covariance = np.cov(asset_returns, benchmark_returns)[0, 1]707benchmark_variance = np.var(benchmark_returns)708beta = covariance / benchmark_variance709alpha = asset_annual_return - (risk_free_rate + beta * (benchmark_annual_return - risk_free_rate))710711print(f"\\nBeta: {beta:.4f}")712print(f"Alpha: {alpha*100:.2f}%")713714# Maximum Drawdown715cummax = np.maximum.accumulate(asset_prices)716drawdown = (asset_prices - cummax) / cummax717max_drawdown = np.min(drawdown)718719print(f"\\nMaximum Drawdown: {max_drawdown*100:.2f}%")720721# Sortino Ratio (downside deviation)722downside_returns = asset_returns[asset_returns < 0]723downside_std = np.std(downside_returns) * np.sqrt(252)724sortino = (asset_annual_return - risk_free_rate) / downside_std if len(downside_returns) > 0 else 0725print(f"Sortino Ratio: {sortino:.4f}")726727# Tracking Error728excess_returns = asset_returns - benchmark_returns729tracking_error = np.std(excess_returns) * np.sqrt(252)730print(f"Tracking Error: {tracking_error*100:.2f}%")731732# Information Ratio733information_ratio = (np.mean(excess_returns) * 252) / tracking_error if tracking_error > 0 else 0734print(f"Information Ratio: {information_ratio:.4f}")`;735}736737// ============================================================================738// Plot Service - Customizable Data Visualization739// ============================================================================740741export interface PlotInput {742 plot_type: 'line' | 'bar' | 'scatter' | 'histogram' | 'candlestick' | 'area' | 'pie' | 'heatmap';743 data: any; // Can be dict with x/y, multi-series, or symbol string744 title?: string;745 xlabel?: string;746 ylabel?: string;747 color?: string | string[];748 figsize?: [number, number];749 grid?: boolean;750 legend?: boolean;751 style?: '-' | '--' | '-.' | ':';752 marker?: 'o' | 's' | '^' | 'v' | 'D' | '*' | '+' | 'x';753 alpha?: number;754 theme?: 'default' | 'dark' | 'colorful';755}756757export interface PlotResult {758 success: boolean;759 error?: string;760 plot_type?: string;761 image?: string; // Base64-encoded PNG762 format?: string;763 title?: string;764}765766export interface PlotExecutionResult {767 success: boolean;768 result?: PlotResult;769 code: string;770 error?: string;771}772773/**774 * Validate plot input775 */776function validatePlotInput(input: PlotInput): { valid: boolean; error?: string } {777 if (!input.plot_type) {778 return { valid: false, error: 'plot_type is required' };779 }780781 const validPlotTypes = ['line', 'bar', 'scatter', 'histogram', 'candlestick', 'area', 'pie', 'heatmap'];782 if (!validPlotTypes.includes(input.plot_type)) {783 return { valid: false, error: `plot_type must be one of: ${validPlotTypes.join(', ')}` };784 }785786 if (!input.data) {787 return { valid: false, error: 'data is required' };788 }789790 if (input.alpha !== undefined && (input.alpha < 0 || input.alpha > 1)) {791 return { valid: false, error: 'alpha must be between 0 and 1' };792 }793794 return { valid: true };795}796797/**798 * Execute plot generation using Python799 */800export async function executePlot(801 input: PlotInput802): Promise<PlotExecutionResult> {803 try {804 // Validate input805 const validation = validatePlotInput(input);806 if (!validation.valid) {807 return {808 success: false,809 code: generatePlotCode(input),810 error: validation.error811 };812 }813814 const pythonScriptPath = path.join(__dirname, 'plotService.py');815 const inputJson = JSON.stringify(input);816817 return new Promise((resolve) => {818 const pythonProcess = spawn(PYTHON_PATH, [pythonScriptPath], {819 stdio: ['pipe', 'pipe', 'pipe'],820 env: process.env821 });822823 let stdout = '';824 let stderr = '';825826 pythonProcess.stdout.on('data', (data) => {827 stdout += data.toString();828 });829830 pythonProcess.stderr.on('data', (data) => {831 stderr += data.toString();832 });833834 pythonProcess.on('close', (code) => {835 if (code !== 0 || (stderr && !stdout)) {836 resolve({837 success: false,838 code: generatePlotCode(input),839 error: stderr || `Python process exited with code ${code}`840 });841 return;842 }843844 try {845 const result = JSON.parse(stdout) as PlotResult;846 resolve({847 success: result.success,848 result,849 code: generatePlotCode(input),850 error: result.error851 });852 } catch (parseError) {853 resolve({854 success: false,855 code: generatePlotCode(input),856 error: 'Failed to parse Python output: ' + (parseError instanceof Error ? parseError.message : 'Unknown error')857 });858 }859 });860861 pythonProcess.on('error', (error) => {862 resolve({863 success: false,864 code: generatePlotCode(input),865 error: 'Failed to start Python process: ' + error.message866 });867 });868869 pythonProcess.stdin.write(inputJson);870 pythonProcess.stdin.end();871 });872 } catch (error) {873 return {874 success: false,875 code: generatePlotCode(input),876 error: error instanceof Error ? error.message : 'Unknown error occurred'877 };878 }879}880881/**882 * Generate readable Python code for display883 */884function generatePlotCode(input: PlotInput): string {885 const plotType = input.plot_type;886 const title = input.title || 'Financial Chart';887 const xlabel = input.xlabel || '';888 const ylabel = input.ylabel || '';889 const figsize = input.figsize || [12, 6];890 const showGrid = input.grid !== false;891 const alpha = input.alpha || 0.8;892 const theme = input.theme || 'default';893894 // Check if data is a symbol (string) or actual data895 const isSymbol = typeof input.data === 'string';896897 let dataSetupCode = '';898 let plotCode = '';899900 if (isSymbol) {901 // Symbol-based plotting902 const symbol = input.data;903 dataSetupCode = `# Fetch historical data from FMP API904symbol = '${symbol}'905hist_data = get_historical_prices(symbol)906historical = hist_data['historical']907908# Extract data for plotting909dates = [h['date'] for h in reversed(historical)]910closes = [h['close'] for h in reversed(historical)]911volumes = [h['volume'] for h in reversed(historical)]`;912913 plotCode = `# Plot closing prices914plt.plot(dates, closes, linewidth=2, color='#2563eb', label='Close Price')`;915916 } else {917 // Custom data plotting918 dataSetupCode = `# Custom data919data = ${JSON.stringify(input.data, null, 2)}920921# Extract x and y values922if 'x' in data and 'y' in data:923 x_data = data['x']924 y_data = data['y']925elif 'x' in data:926 # Multi-series with shared x-axis927 x_data = data['x']928 series = {k: v for k, v in data.items() if k != 'x'}`;929930 if (plotType === 'line') {931 plotCode = `# Plot line chart932for label, y_values in series.items():933 plt.plot(x_data, y_values, linewidth=2, label=label, alpha=${alpha})`;934 } else if (plotType === 'bar') {935 plotCode = `# Plot bar chart936plt.bar(x_data, y_data, alpha=${alpha}, color='#2563eb')`;937 } else if (plotType === 'scatter') {938 plotCode = `# Plot scatter chart939plt.scatter(x_data, y_data, alpha=${alpha}, s=100, color='#2563eb')`;940 } else if (plotType === 'area') {941 plotCode = `# Plot area chart942plt.fill_between(x_data, y_data, alpha=${alpha * 0.6}, color='#2563eb')943plt.plot(x_data, y_data, linewidth=2, color='#2563eb')`;944 } else if (plotType === 'histogram') {945 plotCode = `# Plot histogram946plt.hist(y_data, bins=30, alpha=${alpha}, color='#2563eb', edgecolor='black')`;947 } else if (plotType === 'pie') {948 plotCode = `# Plot pie chart949plt.pie(y_data, labels=x_data, autopct='%1.1f%%', startangle=90)950plt.axis('equal')`;951 }952 }953954 return `import matplotlib.pyplot as plt955import numpy as np956${isSymbol ? 'from fmpClient import get_historical_prices' : ''}957${theme === 'dark' ? "plt.style.use('dark_background')" : ''}958959${dataSetupCode}960961# Create figure962fig, ax = plt.subplots(figsize=(${figsize[0]}, ${figsize[1]}))963964${plotCode}965966# Customize plot967ax.set_title('${title}', fontsize=16, fontweight='bold')968${xlabel ? `ax.set_xlabel('${xlabel}', fontsize=12)` : ''}969${ylabel ? `ax.set_ylabel('${ylabel}', fontsize=12)` : ''}970${showGrid ? "ax.grid(True, alpha=0.3, linestyle='--')" : ''}971${input.legend !== false ? "ax.legend(loc='best', framealpha=0.9)" : ''}972973plt.tight_layout()974plt.show()`;975}976977// ==================== Volatility Surface Interfaces ====================978979export interface VolatilitySurfaceInput {980 symbol: string;981 title?: string;982 figsize?: [number, number];983 color_map?: string;984}985986export interface VolatilitySurfaceResult {987 success: boolean;988 image_url?: string;989 format?: string;990 title?: string;991 error?: string;992 stats?: {993 total_contracts: number;994 min_iv: number;995 max_iv: number;996 avg_iv: number;997 strike_range: [number, number];998 expiration_range_days: [number, number];999 };1000}10011002export interface VolatilitySurfaceExecutionResult {1003 success: boolean;1004 result?: VolatilitySurfaceResult;1005 code: string;1006 error?: string;1007}10081009/**1010 * Validate volatility surface input1011 */1012function validateVolatilitySurfaceInput(input: VolatilitySurfaceInput): { valid: boolean; error?: string } {1013 if (!input.symbol || typeof input.symbol !== 'string' || input.symbol.trim().length === 0) {1014 return { valid: false, error: 'Symbol is required and must be a non-empty string' };1015 }10161017 if (input.figsize && (!Array.isArray(input.figsize) || input.figsize.length !== 2)) {1018 return { valid: false, error: 'figsize must be an array of two numbers [width, height]' };1019 }10201021 return { valid: true };1022}10231024/**1025 * Execute volatility surface generation using Python1026 */1027export async function executeVolatilitySurface(1028 input: VolatilitySurfaceInput1029): Promise<VolatilitySurfaceExecutionResult> {1030 try {1031 // Validate input1032 const validation = validateVolatilitySurfaceInput(input);1033 if (!validation.valid) {1034 return {1035 success: false,1036 code: `# Volatility Surface for ${input.symbol}`,1037 error: validation.error1038 };1039 }10401041 const pythonScriptPath = path.join(__dirname, 'volatilitySurfaceService.py');1042 const inputJson = JSON.stringify(input);10431044 return new Promise((resolve) => {1045 const pythonProcess = spawn(PYTHON_PATH, [pythonScriptPath], {1046 stdio: ['pipe', 'pipe', 'pipe'],1047 env: process.env1048 });10491050 let stdout = '';1051 let stderr = '';10521053 pythonProcess.stdout.on('data', (data) => {1054 stdout += data.toString();1055 });10561057 pythonProcess.stderr.on('data', (data) => {1058 stderr += data.toString();1059 });10601061 pythonProcess.on('close', (code) => {1062 if (code !== 0 || (stderr && !stdout)) {1063 resolve({1064 success: false,1065 code: `# Volatility Surface for ${input.symbol}`,1066 error: stderr || `Python process exited with code ${code}`1067 });1068 return;1069 }10701071 try {1072 const result = JSON.parse(stdout) as VolatilitySurfaceResult;1073 resolve({1074 success: result.success,1075 result,1076 code: `# Generated 3D Volatility Surface for ${input.symbol}`,1077 error: result.error1078 });1079 } catch (parseError) {1080 resolve({1081 success: false,1082 code: `# Volatility Surface for ${input.symbol}`,1083 error: 'Failed to parse Python output: ' + (parseError instanceof Error ? parseError.message : 'Unknown error')1084 });1085 }1086 });10871088 pythonProcess.on('error', (error) => {1089 resolve({1090 success: false,1091 code: `# Volatility Surface for ${input.symbol}`,1092 error: 'Failed to start Python process: ' + error.message1093 });1094 });10951096 pythonProcess.stdin.write(inputJson);1097 pythonProcess.stdin.end();1098 });1099 } catch (error) {1100 return {1101 success: false,1102 code: `# Volatility Surface for ${input.symbol}`,1103 error: error instanceof Error ? error.message : 'Unknown error occurred'1104 };1105 }1106}11071108// ============================================================================1109// Data Download Service - Export FMP data to various formats1110// ============================================================================11111112export interface DataDownloadInput {1113 data_type: string;1114 symbol?: string;1115 format: 'csv' | 'xlsx' | 'json' | 'txt';1116 period?: 'annual' | 'quarter';1117 limit?: number;1118 from_date?: string;1119 to_date?: string;1120 indicator_period?: number;1121 time_period?: string;1122 interval?: string;1123 news_limit?: number;1124 pair?: string;1125}11261127export interface DataDownloadResult {1128 success: boolean;1129 error?: string;1130 filename?: string;1131 filepath?: string;1132 format?: string;1133 file_size?: number;1134 metadata?: {1135 data_type: string;1136 symbol?: string;1137 fetched_at: string;1138 params: any;1139 description: string;1140 record_count: number;1141 };1142}11431144export interface DataDownloadExecutionResult {1145 success: boolean;1146 result?: DataDownloadResult;1147 error?: string;1148}11491150/**1151 * Validate data download input1152 */1153function validateDataDownloadInput(input: DataDownloadInput): { valid: boolean; error?: string } {1154 if (!input.data_type || typeof input.data_type !== 'string') {1155 return { valid: false, error: 'data_type is required and must be a string' };1156 }11571158 const validFormats = ['csv', 'xlsx', 'json', 'txt'];1159 if (!input.format || !validFormats.includes(input.format)) {1160 return { valid: false, error: `format must be one of: ${validFormats.join(', ')}` };1161 }11621163 // Data types that require a symbol1164 const symbolRequiredTypes = [1165 'company_profile', 'income_statement', 'balance_sheet', 'cash_flow',1166 'key_metrics', 'financial_ratios', 'financial_growth', 'stock_quote',1167 'historical_price', 'intraday_price', 'rsi', 'macd', 'ema', 'sma',1168 'adx', 'williams_r', 'cci', 'stochastic', 'financial_news',1169 'earnings_surprises', 'analyst_estimates', 'price_target',1170 'upgrades_downgrades', 'dividend_history', 'stock_splits',1171 'insider_trading', 'institutional_holders', 'esg_score'1172 ];11731174 if (symbolRequiredTypes.includes(input.data_type) && !input.symbol) {1175 return { valid: false, error: `symbol is required for data_type: ${input.data_type}` };1176 }11771178 return { valid: true };1179}11801181/**1182 * Execute data download and export to file1183 */1184export async function executeDataDownload(1185 input: DataDownloadInput1186): Promise<DataDownloadExecutionResult> {1187 try {1188 // Validate input1189 const validation = validateDataDownloadInput(input);1190 if (!validation.valid) {1191 return {1192 success: false,1193 error: validation.error1194 };1195 }11961197 const pythonScriptPath = path.join(__dirname, 'dataDownloadService.py');1198 const inputJson = JSON.stringify(input);11991200 return new Promise((resolve) => {1201 const pythonProcess = spawn(PYTHON_PATH, [pythonScriptPath], {1202 stdio: ['pipe', 'pipe', 'pipe'],1203 env: process.env1204 });12051206 let stdout = '';1207 let stderr = '';12081209 pythonProcess.stdout.on('data', (data) => {1210 stdout += data.toString();1211 });12121213 pythonProcess.stderr.on('data', (data) => {1214 stderr += data.toString();1215 });12161217 pythonProcess.on('close', (code) => {1218 if (code !== 0 || (stderr && !stdout)) {1219 resolve({1220 success: false,1221 error: stderr || `Python process exited with code ${code}`1222 });1223 return;1224 }12251226 try {1227 const result = JSON.parse(stdout) as DataDownloadResult;1228 resolve({1229 success: result.success,1230 result,1231 error: result.error1232 });1233 } catch (parseError) {1234 resolve({1235 success: false,1236 error: 'Failed to parse Python output: ' + (parseError instanceof Error ? parseError.message : 'Unknown error')1237 });1238 }1239 });12401241 pythonProcess.on('error', (error) => {1242 resolve({1243 success: false,1244 error: 'Failed to start Python process: ' + error.message1245 });1246 });12471248 pythonProcess.stdin.write(inputJson);1249 pythonProcess.stdin.end();1250 });1251 } catch (error) {1252 return {1253 success: false,1254 error: error instanceof Error ? error.message : 'Unknown error occurred'1255 };1256 }1257}12581259// ==================== CUSTOM PYTHON EXECUTOR (FREE-FORM) ====================12601261export interface CustomPythonInput {1262 code: string;1263 context?: Record<string, any>;1264 description?: string;1265}12661267export interface CustomPythonResult {1268 success: boolean;1269 output?: string;1270 result?: any;1271 figures?: string[]; // Base64 encoded images1272 files?: Array<{ filename: string; filepath: string }>; // Generated files (Excel, etc.)1273 error?: string;1274 code?: string;1275}12761277/**1278 * Execute custom Python code written by Claude with access to FMP API1279 * This is a "free-form" tool that allows Claude to write any Python analysis1280 * that doesn't fit into the predefined tools1281 */1282export async function executeCustomPython(1283 input: CustomPythonInput1284): Promise<{ success: boolean; result?: CustomPythonResult; code: string; error?: string }> {1285 try {1286 if (!input.code) {1287 return {1288 success: false,1289 code: '',1290 error: 'No Python code provided'1291 };1292 }12931294 console.log('\n🐍 ========== CUSTOM PYTHON EXECUTION ==========');1295 console.log('📝 Description:', input.description || 'Custom analysis');1296 console.log('📊 Code length:', input.code.length, 'chars');1297 console.log('🔧 Context vars:', Object.keys(input.context || {}).join(', ') || 'none');12981299 const pythonScriptPath = path.join(__dirname, 'customPythonExecutor.py');13001301 // Prepare input with FMP API key1302 const executorInput = {1303 code: input.code,1304 context: input.context || {},1305 fmp_api_key: process.env.FMP_API_KEY1306 };13071308 const inputJson = JSON.stringify(executorInput);13091310 return new Promise((resolve) => {1311 const pythonProcess = spawn(PYTHON_PATH, [pythonScriptPath], {1312 stdio: ['pipe', 'pipe', 'pipe'],1313 env: { ...process.env, PYTHONUNBUFFERED: '1' },1314 timeout: 90000 // 90 second timeout for custom code1315 });13161317 let stdout = '';1318 let stderr = '';13191320 pythonProcess.stdout.on('data', (data) => {1321 stdout += data.toString();1322 });13231324 pythonProcess.stderr.on('data', (data) => {1325 stderr += data.toString();1326 });13271328 pythonProcess.on('close', (code) => {1329 console.log('🏁 Python process completed with exit code:', code);13301331 if (code !== 0 || (stderr && !stdout)) {1332 console.error('❌ Python execution failed');1333 console.error('stderr:', stderr);13341335 resolve({1336 success: false,1337 code: input.code,1338 error: stderr || `Python process exited with code ${code}`1339 });1340 return;1341 }13421343 try {1344 const result = JSON.parse(stdout) as CustomPythonResult;13451346 console.log('✅ Custom Python execution successful');1347 console.log('📤 Output length:', result.output?.length || 0, 'chars');1348 console.log('🖼️ Figures generated:', result.figures?.length || 0);13491350 resolve({1351 success: result.success,1352 result,1353 code: input.code,1354 error: result.error1355 });1356 } catch (parseError) {1357 console.error('❌ Failed to parse Python output');1358 console.error('stdout:', stdout.substring(0, 500));13591360 resolve({1361 success: false,1362 code: input.code,1363 error: 'Failed to parse Python output: ' + (parseError instanceof Error ? parseError.message : 'Unknown error')1364 });1365 }1366 });13671368 pythonProcess.on('error', (error) => {1369 console.error('❌ Failed to start Python process:', error.message);13701371 resolve({1372 success: false,1373 code: input.code,1374 error: 'Failed to start Python process: ' + error.message1375 });1376 });13771378 // Send input to Python process1379 pythonProcess.stdin.write(inputJson);1380 pythonProcess.stdin.end();1381 });1382 } catch (error) {1383 return {1384 success: false,1385 code: input.code,1386 error: 'Failed to execute custom Python: ' + (error instanceof Error ? error.message : 'Unknown error')1387 };1388 }1389}1390