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%
45.1 KB · 1,090 lines python
Raw Blame History
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