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%
40.1 KB · 1,390 lines typescript
Raw Blame History
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