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#!/usr/bin/env python32# =============================================================================3# VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4# -----------------------------------------------------------------------------5# File: server/services/python/customPythonExecutor.py6#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# =============================================================================1516"""17Generic Python Executor with FMP API Access18Allows Claude to write custom Python code with direct access to FMP endpoints19"""2021import sys22import json23import io24import base6425import traceback26import os27import re28import time29import tempfile30from contextlib import redirect_stdout, redirect_stderr31import matplotlib32matplotlib.use('Agg') # Non-interactive backend33import matplotlib.pyplot as plt34import numpy as np35import pandas as pd36from datetime import datetime, timedelta37import warnings38warnings.filterwarnings('ignore') # Suppress warnings for cleaner output3940# Create a comprehensive FMP API client class41class FMPClient:42 """Comprehensive FMP API client for Python custom code"""4344 def __init__(self, api_key):45 self.api_key = api_key46 self.base_url = 'https://financialmodelingprep.com/stable'47 self.base_url_stable = 'https://financialmodelingprep.com/stable'4849 def _request(self, endpoint, params=None):50 """Make API request"""51 import requests52 if params is None:53 params = {}54 params['apikey'] = self.api_key5556 url = f"{self.base_url}{endpoint}"57 response = requests.get(url, params=params, timeout=15)58 response.raise_for_status()59 return response.json()6061 def _request_stable(self, endpoint, params=None):62 """Make API request to stable endpoint"""63 import requests64 if params is None:65 params = {}66 params['apikey'] = self.api_key6768 url = f"{self.base_url_stable}{endpoint}"69 response = requests.get(url, params=params, timeout=15)70 response.raise_for_status()71 return response.json()7273 # ===== MOST USED METHODS =====7475 def get_quote(self, symbol):76 """Get current stock quote"""77 data = self._request('/quote', {'symbol': symbol})78 return data[0] if data else None7980 def get_historical_prices(self, symbol, from_date=None, to_date=None):81 """Get historical prices - returns dict with 'historical' array"""82 params = {'symbol': symbol}83 if from_date:84 params['from'] = from_date85 if to_date:86 params['to'] = to_date87 data = self._request('/historical-price-eod/full', params)88 # Stable API returns flat array; wrap to match legacy format89 if isinstance(data, list):90 return {'symbol': symbol, 'historical': data}91 return data9293 def get_income_statement(self, symbol, period='annual', limit=5):94 """Get income statement"""95 return self._request('/income-statement', {'symbol': symbol, 'period': period, 'limit': limit})9697 def get_balance_sheet(self, symbol, period='annual', limit=5):98 """Get balance sheet"""99 return self._request('/balance-sheet-statement', {'symbol': symbol, 'period': period, 'limit': limit})100101 def get_cash_flow(self, symbol, period='annual', limit=5):102 """Get cash flow statement"""103 return self._request('/cash-flow-statement', {'symbol': symbol, 'period': period, 'limit': limit})104105 def get_key_metrics(self, symbol, period='annual', limit=5):106 """Get key metrics"""107 return self._request('/key-metrics', {'symbol': symbol, 'period': period, 'limit': limit})108109 def get_financial_ratios(self, symbol, period='annual', limit=5):110 """Get financial ratios"""111 return self._request('/ratios', {'symbol': symbol, 'period': period, 'limit': limit})112113 def get_company_profile(self, symbol):114 """Get company profile"""115 return self._request('/profile', {'symbol': symbol})116117 def get_stock_news(self, symbol, limit=20):118 """Get stock news"""119 return self._request('/news/stock', {'symbols': symbol, 'limit': limit})120121 def get_rsi(self, symbol, period=14, time_period='daily'):122 """Get RSI indicator"""123 data = self._request('/technical-indicators/rsi', {'symbol': symbol, 'timeframe': time_period, 'periodLength': period})124 return data[:10] # Limit for performance125126 def get_macd(self, symbol, time_period='daily'):127 """Get MACD indicator"""128 data = self._request('/technical-indicators/macd', {'symbol': symbol, 'timeframe': time_period})129 return data[:10]130131 def search(self, query, limit=10):132 """Search for companies"""133 return self._request('/search-symbol', {'query': query, 'limit': limit})134135# Import additional libraries (optional - fail silently if not available)136AVAILABLE_LIBS = {}137138try:139 import seaborn as sns140 AVAILABLE_LIBS['sns'] = sns141 AVAILABLE_LIBS['seaborn'] = sns142except ImportError:143 pass144145try:146 import plotly.graph_objects as go147 import plotly.express as px148 AVAILABLE_LIBS['go'] = go149 AVAILABLE_LIBS['px'] = px150except ImportError:151 pass152153try:154 from scipy import stats, optimize, signal155 AVAILABLE_LIBS['stats'] = stats156 AVAILABLE_LIBS['optimize'] = optimize157 AVAILABLE_LIBS['signal'] = signal158except ImportError:159 pass160161try:162 from sklearn.linear_model import LinearRegression163 from sklearn.preprocessing import StandardScaler164 from sklearn.decomposition import PCA165 AVAILABLE_LIBS['LinearRegression'] = LinearRegression166 AVAILABLE_LIBS['StandardScaler'] = StandardScaler167 AVAILABLE_LIBS['PCA'] = PCA168except ImportError:169 pass170171try:172 import ta # Technical analysis library173 AVAILABLE_LIBS['ta'] = ta174except ImportError:175 pass176177try:178 import yfinance as yf179 AVAILABLE_LIBS['yf'] = yf180except ImportError:181 pass182183try:184 from statsmodels.tsa.stattools import adfuller, acf, pacf185 from statsmodels.tsa.arima.model import ARIMA186 AVAILABLE_LIBS['adfuller'] = adfuller187 AVAILABLE_LIBS['acf'] = acf188 AVAILABLE_LIBS['pacf'] = pacf189 AVAILABLE_LIBS['ARIMA'] = ARIMA190except ImportError:191 pass192193try:194 import cvxpy as cp195 AVAILABLE_LIBS['cp'] = cp196 AVAILABLE_LIBS['cvxpy'] = cp197except ImportError:198 pass199200# === ARCH / GARCH Models ===201try:202 from arch import arch_model203 from arch.__future__ import reindexing204 AVAILABLE_LIBS['arch_model'] = arch_model205except ImportError:206 pass207208# === Numba (JIT compilation) ===209try:210 import numba211 from numba import jit, njit, prange212 AVAILABLE_LIBS['numba'] = numba213 AVAILABLE_LIBS['jit'] = jit214 AVAILABLE_LIBS['njit'] = njit215 AVAILABLE_LIBS['prange'] = prange216except ImportError:217 pass218219# === NetworkX (Graph analysis) ===220try:221 import networkx as nx222 AVAILABLE_LIBS['nx'] = nx223 AVAILABLE_LIBS['networkx'] = nx224except ImportError:225 pass226227# === SymPy (Symbolic math) ===228try:229 import sympy230 AVAILABLE_LIBS['sympy'] = sympy231except ImportError:232 pass233234# === XGBoost ===235try:236 import xgboost as xgb237 AVAILABLE_LIBS['xgb'] = xgb238 AVAILABLE_LIBS['xgboost'] = xgb239except ImportError:240 pass241242# === LightGBM ===243try:244 import lightgbm as lgb245 AVAILABLE_LIBS['lgb'] = lgb246 AVAILABLE_LIBS['lightgbm'] = lgb247except ImportError:248 pass249250# === PyPortfolioOpt (Portfolio optimization) ===251try:252 from pypfopt import EfficientFrontier, risk_models, expected_returns, HRPOpt, BlackLittermanModel253 from pypfopt.discrete_allocation import DiscreteAllocation254 AVAILABLE_LIBS['EfficientFrontier'] = EfficientFrontier255 AVAILABLE_LIBS['risk_models'] = risk_models256 AVAILABLE_LIBS['expected_returns'] = expected_returns257 AVAILABLE_LIBS['HRPOpt'] = HRPOpt258 AVAILABLE_LIBS['BlackLittermanModel'] = BlackLittermanModel259 AVAILABLE_LIBS['DiscreteAllocation'] = DiscreteAllocation260except ImportError:261 pass262263# === Riskfolio-lib (Advanced portfolio optimization) ===264try:265 import riskfolio as rp266 AVAILABLE_LIBS['rp'] = rp267 AVAILABLE_LIBS['riskfolio'] = rp268except ImportError:269 pass270271# === Prophet (Time series forecasting) ===272try:273 from prophet import Prophet274 AVAILABLE_LIBS['Prophet'] = Prophet275except ImportError:276 pass277278# === pmdarima (Auto ARIMA) ===279try:280 import pmdarima as pm281 from pmdarima import auto_arima282 AVAILABLE_LIBS['pm'] = pm283 AVAILABLE_LIBS['pmdarima'] = pm284 AVAILABLE_LIBS['auto_arima'] = auto_arima285except ImportError:286 pass287288# === DuckDB ===289try:290 import duckdb291 AVAILABLE_LIBS['duckdb'] = duckdb292except ImportError:293 pass294295# === Polars (Fast DataFrames) ===296try:297 import polars as pl298 AVAILABLE_LIBS['pl'] = pl299 AVAILABLE_LIBS['polars'] = pl300except ImportError:301 pass302303# === mplfinance (Financial charts) ===304try:305 import mplfinance as mpf306 AVAILABLE_LIBS['mpf'] = mpf307 AVAILABLE_LIBS['mplfinance'] = mpf308except ImportError:309 pass310311# === VectorBT (Backtesting) ===312try:313 import vectorbt as vbt314 AVAILABLE_LIBS['vbt'] = vbt315 AVAILABLE_LIBS['vectorbt'] = vbt316except ImportError:317 pass318319# === HMMLearn (Hidden Markov Models) ===320try:321 from hmmlearn import hmm322 AVAILABLE_LIBS['hmm'] = hmm323 AVAILABLE_LIBS['hmmlearn'] = hmm324except ImportError:325 pass326327# === Additional sklearn modules ===328try:329 from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier, GradientBoostingRegressor330 from sklearn.cluster import KMeans, DBSCAN331 from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV332 from sklearn.metrics import mean_squared_error, accuracy_score, r2_score333 AVAILABLE_LIBS['RandomForestRegressor'] = RandomForestRegressor334 AVAILABLE_LIBS['RandomForestClassifier'] = RandomForestClassifier335 AVAILABLE_LIBS['GradientBoostingRegressor'] = GradientBoostingRegressor336 AVAILABLE_LIBS['KMeans'] = KMeans337 AVAILABLE_LIBS['DBSCAN'] = DBSCAN338 AVAILABLE_LIBS['train_test_split'] = train_test_split339 AVAILABLE_LIBS['cross_val_score'] = cross_val_score340 AVAILABLE_LIBS['GridSearchCV'] = GridSearchCV341 AVAILABLE_LIBS['mean_squared_error'] = mean_squared_error342 AVAILABLE_LIBS['accuracy_score'] = accuracy_score343 AVAILABLE_LIBS['r2_score'] = r2_score344except ImportError:345 pass346347# === Additional scipy modules ===348try:349 from scipy.interpolate import interp1d, griddata, CubicSpline350 from scipy.integrate import quad, odeint351 from scipy.fft import fft, ifft352 AVAILABLE_LIBS['interp1d'] = interp1d353 AVAILABLE_LIBS['griddata'] = griddata354 AVAILABLE_LIBS['CubicSpline'] = CubicSpline355 AVAILABLE_LIBS['quad'] = quad356 AVAILABLE_LIBS['odeint'] = odeint357 AVAILABLE_LIBS['fft'] = fft358 AVAILABLE_LIBS['ifft'] = ifft359except ImportError:360 pass361362# === Additional statsmodels ===363try:364 from statsmodels.tsa.seasonal import seasonal_decompose365 from statsmodels.tsa.holtwinters import ExponentialSmoothing366 from statsmodels.tsa.vector_ar.var_model import VAR367 from statsmodels.regression.rolling import RollingOLS368 AVAILABLE_LIBS['seasonal_decompose'] = seasonal_decompose369 AVAILABLE_LIBS['ExponentialSmoothing'] = ExponentialSmoothing370 AVAILABLE_LIBS['VAR'] = VAR371 AVAILABLE_LIBS['RollingOLS'] = RollingOLS372except ImportError:373 pass374375# === python-docx (Word documents) ===376try:377 from docx import Document as DocxDocument378 from docx.shared import Pt, RGBColor, Inches, Cm379 from docx.enum.text import WD_ALIGN_PARAGRAPH380 from docx.enum.table import WD_TABLE_ALIGNMENT381 AVAILABLE_LIBS['DocxDocument'] = DocxDocument382except ImportError:383 pass384385386class CustomPythonExecutor:387 """Executor for custom Python code with FMP API access"""388389 def __init__(self, fmp_api_key=None):390 self.fmp_api_key = fmp_api_key or os.getenv('FMP_API_KEY')391 self.fmp = FMPClient(self.fmp_api_key) if FMPClient and self.fmp_api_key else None392 self.output_text = []393 self.figures = []394 self.files = []395396 def execute(self, code: str, context: dict = None) -> dict:397 """398 Execute custom Python code with access to FMP API399400 Args:401 code: Python code to execute402 context: Optional context variables to inject403404 Returns:405 dict with:406 - success: bool407 - output: str (printed output)408 - result: any (return value if function defined)409 - figures: list of base64 encoded images410 - error: str (if failed)411 """412 try:413 # Prepare safe execution environment with all available libraries414415 def _convert_numpy(val):416 """Convert numpy types to Python native types for openpyxl compatibility"""417 if val is None or (hasattr(val, '__class__') and val.__class__.__name__ in ('NaT', 'NaTType')):418 return None419 if hasattr(np, 'integer') and isinstance(val, np.integer):420 return int(val)421 if hasattr(np, 'floating') and isinstance(val, np.floating):422 if np.isnan(val) or np.isinf(val):423 return None424 return float(val)425 if hasattr(np, 'bool_') and isinstance(val, np.bool_):426 return bool(val)427 if hasattr(np, 'ndarray') and isinstance(val, np.ndarray):428 return val.tolist()429 if isinstance(val, pd.Timestamp):430 return val.to_pydatetime()431 if hasattr(val, 'item'):432 return val.item()433 return val434435 def _detect_number_format(col_name):436 """Auto-detect number format based on column name"""437 col_lower = str(col_name).lower()438 pct_keywords = ['pct', 'percent', 'return', 'margin', 'yield', 'rate', 'ratio',439 'growth', 'change', 'weight', 'allocation', 'drawdown', 'sharpe',440 'sortino', 'alpha', 'beta', 'volatility', 'vol']441 if any(kw in col_lower for kw in pct_keywords):442 return '0.00%'443 price_keywords = ['price', 'close', 'open', 'high', 'low', 'adj', 'value',444 'revenue', 'income', 'profit', 'cost', 'expense', 'ebitda',445 'earnings', 'sales', 'assets', 'liabilities', 'equity',446 'market_cap', 'marketcap', 'nav', 'amount']447 if any(kw in col_lower for kw in price_keywords):448 return '#,##0.00'449 vol_keywords = ['volume', 'shares', 'count', 'qty', 'quantity']450 if any(kw in col_lower for kw in vol_keywords):451 return '#,##0'452 return None453454 def save_excel(data, filename='analysis.xlsx', sheet_name='Sheet1', index=False,455 title=None, summary=True, conditional_formatting=True, formulas=None):456 """Save DataFrame(s) as a professionally styled Excel file with formulas and conditional formatting.457458 Args:459 data: DataFrame for single sheet, or dict {sheet_name: DataFrame} for multi-sheet460 filename: Output filename (.xlsx)461 sheet_name: Sheet name (only used if data is a single DataFrame)462 index: Whether to include the DataFrame index463 title: Title row text (defaults to sheet name)464 summary: True (all), False (none), or list like ['sum', 'average'] for selected stats465 conditional_formatting: True to auto-apply color scales and data bars466 formulas: List of dicts [{"col": "Total", "formula": "=B{row}+C{row}", "format": "#,##0.00"}]467 """468 from openpyxl import Workbook469 from openpyxl.styles import Font, PatternFill, Border, Side, Alignment470 from openpyxl.utils import get_column_letter471 from openpyxl.formatting.rule import ColorScaleRule, DataBarRule472473 if not filename.endswith('.xlsx'):474 filename += '.xlsx'475 safe_name = re.sub(r'[^a-zA-Z0-9._-]', '_', filename)476 filepath = os.path.join(tempfile.gettempdir(), f'vq_{int(time.time())}_{safe_name}')477478 # Normalize input to dict of DataFrames479 if isinstance(data, pd.DataFrame):480 sheets = {sheet_name: data}481 elif isinstance(data, dict):482 sheets = data483 else:484 raise ValueError("save_excel expects a DataFrame or dict of DataFrames")485486 wb = Workbook()487 first = True488489 # Styles490 teal_hex = '119993'491 header_fill = PatternFill(start_color=teal_hex, end_color=teal_hex, fill_type='solid')492 header_font = Font(bold=True, color='FFFFFF', size=11, name='Calibri')493 header_alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)494 thin_border = Border(495 left=Side(style='thin', color='D0D0D0'),496 right=Side(style='thin', color='D0D0D0'),497 top=Side(style='thin', color='D0D0D0'),498 bottom=Side(style='thin', color='D0D0D0'),499 )500 even_fill = PatternFill(start_color='F2F9F9', end_color='F2F9F9', fill_type='solid')501 data_font = Font(size=10, name='Calibri')502 data_alignment = Alignment(vertical='center')503 title_font = Font(bold=True, color=teal_hex, size=14, name='Calibri')504 subtitle_font = Font(italic=True, color='888888', size=9, name='Calibri')505 summary_label_font = Font(bold=True, size=10, name='Calibri', color='333333')506 summary_value_font = Font(bold=True, size=10, name='Calibri', color=teal_hex)507 summary_fill = PatternFill(start_color='F0F0F0', end_color='F0F0F0', fill_type='solid')508 teal_top_border = Border(top=Side(style='thin', color=teal_hex))509510 # Determine which summary rows to include511 all_summary_funcs = ['sum', 'average', 'min', 'max', 'count']512 if summary is True:513 active_summaries = all_summary_funcs514 elif summary is False or summary is None:515 active_summaries = []516 elif isinstance(summary, (list, tuple)):517 active_summaries = [s.lower() for s in summary if s.lower() in all_summary_funcs]518 else:519 active_summaries = all_summary_funcs520521 summary_labels = {522 'sum': 'SUM', 'average': 'AVERAGE', 'min': 'MIN', 'max': 'MAX', 'count': 'COUNT'523 }524 summary_excel_funcs = {525 'sum': 'SUM', 'average': 'AVERAGE', 'min': 'MIN', 'max': 'MAX', 'count': 'COUNT'526 }527528 def _classify_column(col_name):529 """Classify column for conditional formatting: 'return', 'volume', 'price', or None"""530 col_lower = str(col_name).lower()531 return_kw = ['return', 'performance', 'pnl', 'gain', 'loss', 'change', 'alpha',532 'excess', 'spread', 'drawdown']533 if any(kw in col_lower for kw in return_kw):534 return 'return'535 vol_kw = ['volume', 'quantity', 'qty', 'count', 'shares', 'trades', 'transactions']536 if any(kw in col_lower for kw in vol_kw):537 return 'volume'538 price_kw = ['price', 'close', 'open', 'high', 'low', 'value', 'nav', 'amount',539 'revenue', 'income', 'cost', 'expense', 'assets', 'equity', 'market_cap']540 if any(kw in col_lower for kw in price_kw):541 return 'price'542 return None543544 for sname, df in sheets.items():545 if not isinstance(df, pd.DataFrame):546 continue547 if first:548 ws = wb.active549 ws.title = sname[:31]550 first = False551 else:552 ws = wb.create_sheet(title=sname[:31])553554 # Tab color (teal)555 try:556 ws.sheet_properties.tabColor = teal_hex557 except Exception:558 pass559560 # Optionally reset index561 if index and df.index.name:562 df = df.reset_index()563564 cols = list(df.columns)565566 # Handle empty DataFrame567 if len(df) == 0 or len(cols) == 0:568 ws.cell(row=1, column=1, value=title or sname).font = title_font569 ws.cell(row=2, column=1, value=f"Generated: {datetime.now().strftime('%B %d, %Y')}").font = subtitle_font570 ws.cell(row=4, column=1, value="No data").font = data_font571 continue572573 num_formats = [_detect_number_format(c) for c in cols]574575 # === TITLE ROWS (rows 1-3) ===576 sheet_title = title or sname577 # Row 1: Merged title578 ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(cols))579 title_cell = ws.cell(row=1, column=1, value=sheet_title)580 title_cell.font = title_font581 title_cell.alignment = Alignment(horizontal='left', vertical='center')582583 # Row 2: Subtitle with date584 ws.merge_cells(start_row=2, start_column=1, end_row=2, end_column=len(cols))585 subtitle_cell = ws.cell(row=2, column=1, value=f"Generated: {datetime.now().strftime('%B %d, %Y')}")586 subtitle_cell.font = subtitle_font587 subtitle_cell.alignment = Alignment(horizontal='left', vertical='center')588589 # Row 3: Spacer590 ws.row_dimensions[3].height = 6591592 # === HEADERS (row 4) ===593 header_row = 4594 data_start_row = 5595596 for col_idx, col_name in enumerate(cols, 1):597 cell = ws.cell(row=header_row, column=col_idx, value=str(col_name))598 cell.font = header_font599 cell.fill = header_fill600 cell.alignment = header_alignment601 cell.border = thin_border602603 # === CUSTOM FORMULA COLUMNS ===604 formula_cols = []605 if formulas and isinstance(formulas, list):606 for f_def in formulas:607 try:608 f_col_name = f_def.get('col', 'Calculated')609 f_formula_tpl = f_def.get('formula', '')610 f_format = f_def.get('format', None)611 cols.append(f_col_name)612 formula_cols.append({613 'col_idx': len(cols),614 'name': f_col_name,615 'template': f_formula_tpl,616 'format': f_format617 })618 num_formats.append(None)619 # Write header for formula column620 cell = ws.cell(row=header_row, column=len(cols), value=str(f_col_name))621 cell.font = header_font622 cell.fill = header_fill623 cell.alignment = header_alignment624 cell.border = thin_border625 except Exception:626 pass627628 # === DATA ROWS (starting at row 5) ===629 for row_idx_0, (_, row) in enumerate(df.iterrows()):630 excel_row = data_start_row + row_idx_0631 for col_idx, col_name in enumerate(list(df.columns), 1):632 val = _convert_numpy(row[col_name])633 cell = ws.cell(row=excel_row, column=col_idx, value=val)634 cell.font = data_font635 cell.alignment = data_alignment636 cell.border = thin_border637 # Zebra striping638 if row_idx_0 % 2 == 1:639 cell.fill = even_fill640 # Number format641 fmt = num_formats[col_idx - 1]642 if fmt and isinstance(val, (int, float)):643 cell.number_format = fmt644645 # Write formula columns for this row646 for fc in formula_cols:647 try:648 formula_str = fc['template'].replace('{row}', str(excel_row))649 cell = ws.cell(row=excel_row, column=fc['col_idx'], value=formula_str)650 cell.font = data_font651 cell.alignment = data_alignment652 cell.border = thin_border653 if row_idx_0 % 2 == 1:654 cell.fill = even_fill655 if fc['format']:656 cell.number_format = fc['format']657 except Exception:658 pass659660 last_data_row = data_start_row + len(df) - 1661662 # === SUMMARY FORMULAS ===663 # Detect numeric columns664 numeric_col_indices = []665 for col_idx, col_name in enumerate(list(df.columns), 1):666 try:667 if pd.api.types.is_numeric_dtype(df[col_name]):668 numeric_col_indices.append(col_idx)669 except Exception:670 pass671 # Also include formula columns as numeric for summary672 for fc in formula_cols:673 numeric_col_indices.append(fc['col_idx'])674675 if active_summaries and numeric_col_indices and len(df) > 0:676 summary_start = last_data_row + 2 # One blank row separator677678 for s_offset, func_key in enumerate(active_summaries):679 s_row = summary_start + s_offset680 excel_func = summary_excel_funcs[func_key]681 label_text = summary_labels[func_key]682683 # Label in column A684 label_cell = ws.cell(row=s_row, column=1, value=label_text)685 label_cell.font = summary_label_font686 label_cell.fill = summary_fill687 if s_offset == 0:688 label_cell.border = teal_top_border689690 # Formulas for numeric columns691 for col_idx in numeric_col_indices:692 col_letter = get_column_letter(col_idx)693 formula = f"={excel_func}({col_letter}{data_start_row}:{col_letter}{last_data_row})"694 cell = ws.cell(row=s_row, column=col_idx, value=formula)695 cell.font = summary_value_font696 cell.fill = summary_fill697 if s_offset == 0:698 cell.border = teal_top_border699 # Apply same number format as the column700 if col_idx <= len(num_formats) and num_formats[col_idx - 1]:701 cell.number_format = num_formats[col_idx - 1]702703 # === CONDITIONAL FORMATTING ===704 if conditional_formatting and len(df) > 0:705 for col_idx, col_name in enumerate(list(df.columns), 1):706 try:707 if not pd.api.types.is_numeric_dtype(df[col_name]):708 continue709 except Exception:710 continue711712 col_letter = get_column_letter(col_idx)713 cell_range = f"{col_letter}{data_start_row}:{col_letter}{last_data_row}"714 col_type = _classify_column(col_name)715716 try:717 if col_type == 'return':718 # Green/white/red scale for returns719 ws.conditional_formatting.add(cell_range, ColorScaleRule(720 start_type='min', start_color='F8696B',721 mid_type='num', mid_value=0, mid_color='FFFFFF',722 end_type='max', end_color='63BE7B'723 ))724 elif col_type == 'volume':725 # Teal data bars for volume726 ws.conditional_formatting.add(cell_range, DataBarRule(727 start_type='min', end_type='max',728 color=teal_hex729 ))730 elif col_type == 'price':731 # Teal gradient for price/value732 ws.conditional_formatting.add(cell_range, ColorScaleRule(733 start_type='min', start_color='E8F5F4',734 end_type='max', end_color='119993'735 ))736 except Exception:737 pass738739 # === FREEZE PANES on A5 ===740 ws.freeze_panes = 'A5'741742 # === AUTO-WIDTH COLUMNS ===743 for col_idx, col_name in enumerate(cols, 1):744 max_len = len(str(col_name))745 for r in range(data_start_row, min(last_data_row + 1, data_start_row + 100)):746 cell_val = ws.cell(row=r, column=col_idx).value747 if cell_val is not None:748 max_len = max(max_len, len(str(cell_val)))749 adjusted_width = min(max_len + 3, 50)750 ws.column_dimensions[get_column_letter(col_idx)].width = adjusted_width751752 wb.save(filepath)753 self.files.append({'filename': safe_name, 'filepath': filepath})754 sheet_count = len(sheets)755 features = []756 if active_summaries:757 features.append(f"formulas: {', '.join(active_summaries).upper()}")758 if conditional_formatting:759 features.append("conditional formatting")760 if formulas:761 features.append(f"{len(formulas)} calculated column(s)")762 feat_str = f" — {', '.join(features)}" if features else ""763 return f"Excel saved: {safe_name} ({sheet_count} sheet{'s' if sheet_count > 1 else ''}{feat_str})"764765 def save_word(content, filename='document.docx'):766 """Generate a professionally styled Word document.767768 Args:769 content: Either a markdown string OR a dict with structure:770 {"title": "...", "sections": [{"heading": "...", "body": "...", "table": DataFrame}]}771 filename: Output filename (.docx)772 """773 try:774 from docx import Document as _DocxDoc775 from docx.shared import Pt as _Pt, RGBColor as _RGB, Inches as _Inches776 from docx.enum.text import WD_ALIGN_PARAGRAPH as _ALIGN777 except ImportError:778 return "Error: python-docx is not installed. Cannot generate Word documents."779780 if not filename.endswith('.docx'):781 filename += '.docx'782 safe_name = re.sub(r'[^a-zA-Z0-9._-]', '_', filename)783 filepath = os.path.join(tempfile.gettempdir(), f'vq_{int(time.time())}_{safe_name}')784785 doc = _DocxDoc()786787 # Set default font788 style = doc.styles['Normal']789 font = style.font790 font.name = 'Calibri'791 font.size = _Pt(10)792793 teal = _RGB(0x11, 0x99, 0x93)794 dark_gray = _RGB(0x33, 0x33, 0x33)795796 def _add_branded_header(doc, title_text='VQuant Report'):797 """Add VQuant branded header"""798 title_para = doc.add_paragraph()799 title_para.alignment = _ALIGN.LEFT800 run = title_para.add_run(title_text)801 run.font.size = _Pt(24)802 run.font.color.rgb = teal803 run.font.bold = True804805 subtitle = doc.add_paragraph()806 subtitle.alignment = _ALIGN.LEFT807 run = subtitle.add_run('VQuant Financial Analytics')808 run.font.size = _Pt(10)809 run.font.color.rgb = _RGB(0x88, 0x88, 0x88)810811 date_para = doc.add_paragraph()812 date_para.alignment = _ALIGN.LEFT813 run = date_para.add_run(f'Generated: {datetime.now().strftime("%B %d, %Y")}')814 run.font.size = _Pt(9)815 run.font.color.rgb = _RGB(0xAA, 0xAA, 0xAA)816 run.font.italic = True817818 # Add a thin line separator819 border_para = doc.add_paragraph()820 border_para.alignment = _ALIGN.LEFT821 run = border_para.add_run('_' * 70)822 run.font.color.rgb = _RGB(0xDD, 0xDD, 0xDD)823 run.font.size = _Pt(6)824 doc.add_paragraph() # Spacer825826 def _add_df_table(doc, df, max_rows=200):827 """Add a styled DataFrame table to the document"""828 if not isinstance(df, pd.DataFrame) or df.empty:829 return830 df_display = df.head(max_rows)831 cols = list(df_display.columns)832 table = doc.add_table(rows=1 + len(df_display), cols=len(cols))833 table.style = 'Table Grid'834835 # Header row836 for i, col in enumerate(cols):837 cell = table.rows[0].cells[i]838 cell.text = str(col)839 for paragraph in cell.paragraphs:840 for run in paragraph.runs:841 run.font.bold = True842 run.font.color.rgb = _RGB(0xFF, 0xFF, 0xFF)843 run.font.size = _Pt(9)844 from docx.oxml.ns import qn845 shading = cell._element.get_or_add_tcPr()846 shading_elm = shading.makeelement(qn('w:shd'), {847 qn('w:fill'): '119993',848 qn('w:val'): 'clear',849 })850 shading.append(shading_elm)851852 # Data rows853 for row_idx, (_, row) in enumerate(df_display.iterrows()):854 for col_idx, col in enumerate(cols):855 val = _convert_numpy(row[col])856 cell = table.rows[row_idx + 1].cells[col_idx]857 cell.text = str(val) if val is not None else ''858 for paragraph in cell.paragraphs:859 for run in paragraph.runs:860 run.font.size = _Pt(8)861862 def _parse_markdown(doc, text):863 """Parse basic markdown and add to document"""864 lines = text.split('\n')865 i = 0866 while i < len(lines):867 line = lines[i]868 stripped = line.strip()869870 # Headings871 if stripped.startswith('### '):872 h = doc.add_heading(stripped[4:], level=3)873 for run in h.runs:874 run.font.color.rgb = dark_gray875 elif stripped.startswith('## '):876 h = doc.add_heading(stripped[3:], level=2)877 for run in h.runs:878 run.font.color.rgb = teal879 elif stripped.startswith('# '):880 h = doc.add_heading(stripped[2:], level=1)881 for run in h.runs:882 run.font.color.rgb = teal883 # List items884 elif stripped.startswith('- ') or stripped.startswith('* '):885 text_content = stripped[2:]886 para = doc.add_paragraph(style='List Bullet')887 _add_formatted_runs(para, text_content)888 elif re.match(r'^\d+\.\s', stripped):889 text_content = re.sub(r'^\d+\.\s', '', stripped)890 para = doc.add_paragraph(style='List Number')891 _add_formatted_runs(para, text_content)892 # Empty line893 elif not stripped:894 pass # Skip empty lines (paragraph spacing handles this)895 # Regular text896 else:897 para = doc.add_paragraph()898 _add_formatted_runs(para, stripped)899 i += 1900901 def _add_formatted_runs(para, text):902 """Parse bold (**text**) and add as runs"""903 parts = re.split(r'(\*\*[^*]+\*\*)', text)904 for part in parts:905 if part.startswith('**') and part.endswith('**'):906 run = para.add_run(part[2:-2])907 run.bold = True908 run.font.size = _Pt(10)909 else:910 run = para.add_run(part)911 run.font.size = _Pt(10)912913 # Handle structured dict input914 if isinstance(content, dict):915 title = content.get('title', 'VQuant Report')916 _add_branded_header(doc, title)917918 for section in content.get('sections', []):919 if 'heading' in section:920 h = doc.add_heading(section['heading'], level=2)921 for run in h.runs:922 run.font.color.rgb = teal923 if 'body' in section:924 _parse_markdown(doc, section['body'])925 if 'table' in section and isinstance(section['table'], pd.DataFrame):926 _add_df_table(doc, section['table'])927 doc.add_paragraph() # Spacer after table928929 # Handle markdown string input930 elif isinstance(content, str):931 # Extract title from first heading if present932 first_line = content.strip().split('\n')[0]933 if first_line.startswith('# '):934 title = first_line[2:].strip()935 _add_branded_header(doc, title)936 remaining = '\n'.join(content.strip().split('\n')[1:])937 _parse_markdown(doc, remaining)938 else:939 _add_branded_header(doc)940 _parse_markdown(doc, content)941 else:942 return "Error: save_word expects a string or dict"943944 doc.save(filepath)945 self.files.append({'filename': safe_name, 'filepath': filepath})946 return f"Word document saved: {safe_name}"947948 exec_globals = {949 '__builtins__': __builtins__,950 # Core libraries951 'np': np,952 'numpy': np,953 'pd': pd,954 'pandas': pd,955 'plt': plt,956 'matplotlib': matplotlib,957 'datetime': datetime,958 'timedelta': timedelta,959 # FMP API client960 'fmp': self.fmp,961 # Custom print962 'print': self._custom_print,963 # File export helpers964 'save_excel': save_excel,965 'save_word': save_word,966 # Helper functions for common tasks967 'convert_to_datetime': lambda x: pd.to_datetime(x, errors='coerce'),968 'safe_strftime': lambda dt, fmt: dt.strftime(fmt) if pd.notna(dt) and hasattr(dt, 'strftime') else str(dt),969 }970971 # Add all available optional libraries972 exec_globals.update(AVAILABLE_LIBS)973974 # Add context variables if provided975 if context:976 exec_globals.update(context)977978 # Capture stdout/stderr979 stdout_capture = io.StringIO()980 stderr_capture = io.StringIO()981982 result_value = None983984 with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):985 # Execute the code using a single namespace so variables986 # persist across steps and are visible in nested scopes987 # (functions, comprehensions, lambdas, etc.)988 exec(code, exec_globals)989990 # If a 'main' function is defined, call it991 if 'main' in exec_globals and callable(exec_globals['main']):992 result_value = exec_globals['main']()993994 # Capture any matplotlib figures995 self._capture_figures()996997 # Get captured output998 stdout_text = stdout_capture.getvalue()999 stderr_text = stderr_capture.getvalue()10001001 combined_output = '\n'.join(self.output_text)1002 if stdout_text:1003 combined_output += '\n' + stdout_text1004 if stderr_text and not stderr_text.strip().startswith('WARNING'):1005 combined_output += '\nStderr: ' + stderr_text10061007 return {1008 'success': True,1009 'output': combined_output.strip(),1010 'result': result_value,1011 'figures': self.figures,1012 'files': self.files,1013 'error': None1014 }10151016 except Exception as e:1017 error_msg = f"{type(e).__name__}: {str(e)}\n\n{traceback.format_exc()}"1018 return {1019 'success': False,1020 'output': '\n'.join(self.output_text),1021 'result': None,1022 'figures': self.figures,1023 'files': self.files,1024 'error': error_msg1025 }10261027 def _custom_print(self, *args, **kwargs):1028 """Custom print function to capture output"""1029 output = ' '.join(str(arg) for arg in args)1030 self.output_text.append(output)10311032 def _capture_figures(self):1033 """Capture all matplotlib figures as base64 images"""1034 figs = [plt.figure(i) for i in plt.get_fignums()]10351036 for fig in figs:1037 # Save figure to bytes buffer1038 buf = io.BytesIO()1039 fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')1040 buf.seek(0)10411042 # Encode to base641043 img_base64 = base64.b64encode(buf.read()).decode('utf-8')1044 self.figures.append(img_base64)10451046 buf.close()10471048 # Close all figures to free memory1049 plt.close('all')105010511052def main():1053 """Main entry point for the executor"""1054 try:1055 # Read input from stdin1056 input_data = json.loads(sys.stdin.read())10571058 code = input_data.get('code', '')1059 context = input_data.get('context', {})1060 fmp_api_key = input_data.get('fmp_api_key') or os.getenv('FMP_API_KEY')10611062 if not code:1063 print(json.dumps({1064 'success': False,1065 'error': 'No code provided'1066 }))1067 return10681069 # Execute the code1070 executor = CustomPythonExecutor(fmp_api_key=fmp_api_key)1071 result = executor.execute(code, context)10721073 # Return result as JSON1074 print(json.dumps(result, default=str)) # default=str to handle datetime, etc.10751076 except Exception as e:1077 error_result = {1078 'success': False,1079 'error': f'Executor error: {str(e)}\n{traceback.format_exc()}',1080 'output': '',1081 'result': None,1082 'figures': [],1083 'files': []1084 }1085 print(json.dumps(error_result))108610871088if __name__ == '__main__':1089 main()1090