# -*- coding: utf-8 -*- """plotting.py Automatically generated by Colab. Original file is located at https://colab.research.google.com/drive/1ILADgRrYqkAEj5jyymO50ZvzDzVdfD6g """ # plotting.py # Functions for generating Plotly figures from processed strategy data. import plotly.express as px import plotly.graph_objects as go import pandas as pd import numpy as np import traceback from utils import create_empty_figure # Import helper def generate_figures_for_strategy(strategy_results): """ Generates standard Plotly figures for a single strategy's results. Args: strategy_results: Dictionary containing processed data for one strategy, as returned by process_single_file. Expected keys include: 'filename', 'equity_df', 'drawdown_df', 'benchmark_df', 'trades_df', 'exposure_series', 'turnover_df'. Returns: A dictionary containing Plotly figure objects: 'equity_fig', 'drawdown_fig', 'benchmark_fig', 'pnl_hist_fig', 'duration_hist_fig', 'exposure_fig', 'turnover_fig'. Uses empty figures if data is missing or invalid. """ figures = { "equity_fig": create_empty_figure("Equity Curve"), "drawdown_fig": create_empty_figure("Drawdown Curve"), "benchmark_fig": create_empty_figure("Equity vs Benchmark"), "pnl_hist_fig": create_empty_figure("P/L Distribution"), "duration_hist_fig": create_empty_figure("Trade Duration Distribution"), "exposure_fig": create_empty_figure("Exposure"), "turnover_fig": create_empty_figure("Portfolio Turnover") } filename = strategy_results.get("filename", "Strategy") # Get filename for titles try: # --- Equity Curve --- equity_df = strategy_results.get("equity_df") if equity_df is not None and not equity_df.empty and 'Time' in equity_df.columns and 'Equity' in equity_df.columns: # Ensure Time is datetime equity_df['Time'] = pd.to_datetime(equity_df['Time']) fig = px.line(equity_df, x='Time', y='Equity', title=f'Equity Curve ({filename})') fig.update_layout(yaxis_title="Portfolio Value") figures["equity_fig"] = fig # --- Drawdown Curve --- drawdown_df = strategy_results.get("drawdown_df") if drawdown_df is not None and not drawdown_df.empty and 'Time' in drawdown_df.columns and 'Drawdown' in drawdown_df.columns: # Ensure Time is datetime drawdown_df['Time'] = pd.to_datetime(drawdown_df['Time']) # Convert drawdown to percentage for plotting drawdown_df['Drawdown_pct'] = drawdown_df['Drawdown'] * 100 fig = px.area(drawdown_df, x='Time', y='Drawdown_pct', title=f'Drawdown Curve (%) ({filename})', labels={'Drawdown_pct': 'Drawdown (%)'}) fig.update_layout(yaxis_title="Drawdown (%)") figures["drawdown_fig"] = fig # --- Equity vs Benchmark --- benchmark_df = strategy_results.get("benchmark_df") # Requires both equity and benchmark data if equity_df is not None and not equity_df.empty and 'Time' in equity_df.columns and 'Equity' in equity_df.columns and \ benchmark_df is not None and not benchmark_df.empty and 'Time' in benchmark_df.columns and 'Benchmark' in benchmark_df.columns: try: # Ensure Time columns are datetime equity_df['Time'] = pd.to_datetime(equity_df['Time']) benchmark_df['Time'] = pd.to_datetime(benchmark_df['Time']) # Merge on Time after setting as index equity_indexed = equity_df.set_index('Time')['Equity'] benchmark_indexed = benchmark_df.set_index('Time')['Benchmark'] # Combine, handling potential different start/end dates combined = pd.concat([equity_indexed, benchmark_indexed], axis=1, keys=['Equity', 'Benchmark'], join='outer') # Normalize to start at 1 (or 100) for comparison # Check if first row has NaN values after outer join first_valid_index = combined.first_valid_index() if first_valid_index is not None: # Normalize using the first non-NaN value for each column normalized_equity = (combined['Equity'] / combined['Equity'].loc[combined['Equity'].first_valid_index()])#.fillna(method='ffill') # Optional fill normalized_benchmark = (combined['Benchmark'] / combined['Benchmark'].loc[combined['Benchmark'].first_valid_index()])#.fillna(method='ffill') # Optional fill # Create figure and add traces fig = go.Figure() fig.add_trace(go.Scatter(x=normalized_equity.index, y=normalized_equity, mode='lines', name='Strategy Equity')) fig.add_trace(go.Scatter(x=normalized_benchmark.index, y=normalized_benchmark, mode='lines', name='Benchmark')) fig.update_layout(title=f'Normalized Equity vs Benchmark ({filename})', xaxis_title='Date', yaxis_title='Normalized Value (Start = 1)') figures["benchmark_fig"] = fig else: print("Could not normalize Equity vs Benchmark: No valid starting point found after merge.") figures["benchmark_fig"] = create_empty_figure(f"Equity vs Benchmark ({filename}) - Normalization Failed") except Exception as merge_err: print(f"Error merging/plotting Equity vs Benchmark: {merge_err}") figures["benchmark_fig"] = create_empty_figure(f"Equity vs Benchmark ({filename}) - Error") # --- Trade P/L Distribution --- trades_df = strategy_results.get("trades_df") if trades_df is not None and not trades_df.empty and 'profitLoss' in trades_df.columns: # Ensure profitLoss is numeric trades_df['profitLoss'] = pd.to_numeric(trades_df['profitLoss'], errors='coerce') valid_pnl = trades_df['profitLoss'].dropna() if not valid_pnl.empty: fig = px.histogram(valid_pnl, title=f'Trade Profit/Loss Distribution ({filename})', labels={'value': 'Profit/Loss'}) figures["pnl_hist_fig"] = fig # --- Trade Duration Distribution --- # Uses 'duration_days' calculated in processing.py if trades_df is not None and not trades_df.empty and 'duration_days' in trades_df.columns: # Ensure duration_days is numeric trades_df['duration_days'] = pd.to_numeric(trades_df['duration_days'], errors='coerce') valid_duration = trades_df['duration_days'].dropna() if not valid_duration.empty: fig = px.histogram(valid_duration, title=f'Trade Duration Distribution (Days) ({filename})', labels={'value': 'Duration (Days)'}) figures["duration_hist_fig"] = fig # --- Exposure Chart --- # Exposure data format varies; this is a basic example assuming a dict of series exposure_series_dict = strategy_results.get("exposure_series") if exposure_series_dict and isinstance(exposure_series_dict, dict): fig = go.Figure() exposure_plotted = False for series_name, series_data in exposure_series_dict.items(): if 'values' in series_data and isinstance(series_data['values'], list): # Process this specific series using the timeseries helper exposure_df = process_timeseries_chart(series_data['values'], series_name) if not exposure_df.empty: # Plot as area chart if 'Exposure' in name, else line plot_type = 'area' if 'Exposure' in series_name else 'scatter' fill_type = 'tozeroy' if plot_type == 'area' else None fig.add_trace(go.Scatter(x=exposure_df.index, y=exposure_df[series_name], mode='lines', name=series_name, fill=fill_type)) exposure_plotted = True if exposure_plotted: fig.update_layout(title=f'Exposure ({filename})', xaxis_title='Date', yaxis_title='Value / % Exposure') figures["exposure_fig"] = fig else: figures["exposure_fig"] = create_empty_figure(f"Exposure ({filename}) - No PlotData") else: figures["exposure_fig"] = create_empty_figure(f"Exposure ({filename}) - Data Missing/Invalid") # --- Portfolio Turnover --- turnover_df = strategy_results.get("turnover_df") if turnover_df is not None and not turnover_df.empty and 'Time' in turnover_df.columns and 'Turnover' in turnover_df.columns: # Ensure Time is datetime turnover_df['Time'] = pd.to_datetime(turnover_df['Time']) fig = px.line(turnover_df, x='Time', y='Turnover', title=f'Portfolio Turnover ({filename})') fig.update_layout(yaxis_title="Turnover") figures["turnover_fig"] = fig except Exception as e: print(f"Error generating figures for {filename}: {e}") traceback.print_exc() # Keep default empty figures on error return figures def generate_manual_risk_figures(analysis_results, filename="Strategy"): """ Generates Plotly figures from manually calculated risk analysis results. Args: analysis_results: Dictionary containing results from calculate_manual_risk_stats. Expected keys: 'monthly_returns_table_for_heatmap', 'rolling_vol_df'. filename: Name of the strategy for figure titles. Returns: A dictionary containing Plotly figure objects: 'monthly_heatmap_fig', 'rolling_vol_fig'. Uses empty figures if data is missing or invalid. """ figures = { "monthly_heatmap_fig": create_empty_figure(f"Monthly Returns Heatmap ({filename})"), "rolling_vol_fig": create_empty_figure(f"Rolling Volatility ({filename})") } try: # --- Monthly Returns Heatmap --- # Expects percentages (values * 100) from calculate_manual_risk_stats monthly_ret_table = analysis_results.get("monthly_returns_table_for_heatmap") if monthly_ret_table is not None and not monthly_ret_table.empty: z = monthly_ret_table.values # The percentage values x = monthly_ret_table.columns # Month names y = monthly_ret_table.index # Years # Create heatmap fig = go.Figure(data=go.Heatmap( z=z, x=x, y=y, colorscale='RdYlGn', # Red-Yellow-Green scale, good for returns zmid=0, # Center color scale around zero # Format text labels shown on the heatmap cells text=monthly_ret_table.applymap(lambda v: f'{v:.1f}%' if pd.notna(v) else '').values, texttemplate="%{text}", # Use the formatted text hoverongaps=False, # Don't show hover info for gaps colorbar=dict(title='Monthly Return (%)') # Add color bar title )) fig.update_layout( title=f'Monthly Returns (%) ({filename})', yaxis_nticks=len(y), # Ensure all years are shown as ticks yaxis_title="Year", yaxis_autorange='reversed' # Show earlier years at the top ) figures["monthly_heatmap_fig"] = fig # --- Rolling Volatility Plot --- rolling_vol_df = analysis_results.get("rolling_vol_df") # Check if DataFrame exists, is not empty, and has the 'Time' column if rolling_vol_df is not None and not rolling_vol_df.empty and 'Time' in rolling_vol_df.columns: # Ensure Time is datetime rolling_vol_df['Time'] = pd.to_datetime(rolling_vol_df['Time']) fig = go.Figure() colors = px.colors.qualitative.Plotly # Get a qualitative color sequence i = 0 # Color index vol_plotted = False # Iterate through columns starting with 'vol_' for col in rolling_vol_df.columns: if col.startswith('vol_'): window_label = col.split('_')[1] # Extract window label (e.g., '3M') # Plot volatility as percentage fig.add_trace(go.Scatter( x=rolling_vol_df['Time'], y=rolling_vol_df[col] * 100, # Convert to percentage mode='lines', name=f'Rolling Vol ({window_label})', line=dict(color=colors[i % len(colors)]) # Cycle through colors )) i += 1 vol_plotted = True # Update layout if at least one volatility series was plotted if vol_plotted: fig.update_layout( title=f'Annualized Rolling Volatility ({filename})', xaxis_title='Date', yaxis_title='Volatility (%)' # Y-axis label as percentage ) figures["rolling_vol_fig"] = fig else: figures["rolling_vol_fig"] = create_empty_figure(f"Rolling Volatility ({filename}) - No Plot Data") else: figures["rolling_vol_fig"] = create_empty_figure(f"Rolling Volatility ({filename}) - Data Missing/Invalid") except Exception as e: print(f"Error generating manual risk figures for {filename}: {e}") traceback.print_exc() # Keep default empty figures on error return figures