import panel as pn import hvplot.pandas import pandas as pd import numpy as np from utils import load_data, DEFAULT_FILTER_QUERY from backtester import run_backtest, analyze_day_trading pn.extension("tabulator") # --- 1. Load Data --- # Initial load print("Loading data... (this might take a moment if downloading)") try: # Cache the data in memory so we don't reload on every callback # In a production app you might handle this differently GLOBAL_DF = load_data() print(f"Data loaded. Rows: {len(GLOBAL_DF)}") except Exception as e: GLOBAL_DF = pd.DataFrame() print(f"Error loading data: {e}") # --- 2. Widgets --- query_input = pn.widgets.TextAreaInput( name="Filter Query (Pandas Syntax)", value=DEFAULT_FILTER_QUERY, height=100, sizing_mode="stretch_width", ) risk_per_trade_input = pn.widgets.FloatSlider( name="Risk Per Trade (%)", start=0.01, end=1.00, step=0.01, value=0.15 ) stop_loss_input = pn.widgets.FloatSlider( name="Stop Loss (%)", start=0.01, end=1.00, step=0.01, value=0.35 ) take_profit_input = pn.widgets.FloatSlider( name="Take Profit (%)", start=0.01, end=1.00, step=0.01, value=0.55 ) initial_capital_input = pn.widgets.FloatInput( name="Initial Capital ($)", value=10000.0, step=100 ) max_trades_input = pn.widgets.IntSlider( name="Max Trades Per Day", start=1, end=20, value=6 ) commission_amount_input = pn.widgets.FloatInput( name="Commission Amount ($ per 200 shares)", value=2.0, step=0.1 ) # Date Range (default based on user script) default_start = pd.Timestamp("2024-10-07").date() default_end = pd.Timestamp("2026-01-01").date() # Future date from user script start_date_input = pn.widgets.DatePicker( name="Start Date", value=default_start, start=pd.Timestamp("2020-01-01").date(), end=pd.Timestamp("2026-01-01").date(), ) end_date_input = pn.widgets.DatePicker( name="End Date", value=default_end, start=pd.Timestamp("2020-01-01").date(), end=pd.Timestamp("2026-01-01").date(), ) high_spike_checkbox = pn.widgets.Checkbox(name="Include High Spike (marketsession_high)", value=False) high_spike_text = pn.pane.Markdown("this is high spike", styles={'font-size': '0.9em', 'color': 'gray', 'margin-top': '-10px'}) run_button = pn.widgets.Button(name="Run Backtest", button_type="primary") # --- 3. Callbacks & Logic --- def execute_backtest(event=None): # Determine if we need to reload data based on query # To be safe and simple, we reload if the user changed the query or if we just want to ensure consistency. # Given the file is small (parquet), we can reload or filter. # Note: load_data() handles reading and filtering. current_query = query_input.value # We will reload the data with the specific query # If this becomes slow, we can optimize to cache the unfiltered raw data and filter here. try: current_df = load_data(filter_query=current_query) except Exception as e: return pn.pane.Markdown(f"## Error loading data/applying query: {e}") if current_df.empty: return pn.pane.Markdown("## Error: No Data Loaded (Empty after filter)") # Get values rpt = risk_per_trade_input.value sl = stop_loss_input.value tp = take_profit_input.value init_cap = initial_capital_input.value max_trades = max_trades_input.value comm_amt = commission_amount_input.value start_date = start_date_input.value end_date = end_date_input.value include_high = high_spike_checkbox.value trades_df = run_backtest( current_df, rpt, sl, tp, init_cap, start_date, end_date, max_trades, commission_amount=comm_amt, include_high_spike=include_high, ) if trades_df.empty: return pn.pane.Markdown("## No trades found for this configuration") # Analyze results, analysis_df = analyze_day_trading(trades_df) # --- Visuals --- # 1. Equity Curve (Net vs Gross) equity_plot = analysis_df.hvplot.line( x="index", y=["capital_net", "capital_gross"], value_label="Capital ($)", title="Account Growth (Net vs Gross)", ylabel="Capital ($)", xlabel="Trade #", grid=True, height=400, responsive=True, color=["#4CAF50", "#2196F3"], hover_cols=["ticker", "pnl"], ) # 1b. Capital & Profit over Days daily_stats = analysis_df.groupby("date").agg({ "capital_net": "last", "capital_gross": "last", "pnl": "sum", "pnl_gross": "sum" }).reset_index() capital_days_plot = daily_stats.hvplot.line( x="date", y=["capital_net", "capital_gross"], title="Capital over Days", ylabel="Capital ($)", grid=True, height=300, responsive=True, color=["#4CAF50", "#2196F3"], ) profit_days_plot = daily_stats.hvplot.bar( x="date", y=["pnl", "pnl_gross"], title="Daily Profit (Net vs Gross)", ylabel="Profit ($)", grid=True, height=300, responsive=True, alpha=0.6, color=["#4CAF50", "#2196F3"], yformatter="%.0f", ) # 1c. Monthly Performance # We need to calculate monthly return %. # Strategy: Group by Month. # Monthly PnL = Sum(pnl) # Monthly Return % = Monthly PnL / Start of Month Capital * 100 # Start of Month Capital can be approximated by: # First trade of month 'capital_net' - trade 'pnl' -> This is capital before the first trade of the month. # (Note: This neglects capital changes if there were no trades for a while, but it's a good approximation for active trading) analysis_df["month"] = pd.to_datetime(analysis_df["date"]).dt.to_period("M") monthly_stats = ( analysis_df.groupby("month") .agg( { "pnl": "sum", "pnl_gross": "sum", "capital_net": "first", # We'll adjust this "pnl": "sum", # Re-asserting sum } ) .reset_index() ) # To get the true start capital for the month, we find the first trade of that month and subtract its PnL from its ending capital_net. # A more robust way: # For each month, find the first trade index. monthly_data = [] for m in analysis_df["month"].unique(): month_trades = analysis_df[analysis_df["month"] == m] if month_trades.empty: continue first_trade = month_trades.iloc[0] start_cap = first_trade["capital_net"] - first_trade["pnl"] total_pnl = month_trades["pnl"].sum() return_pct = (total_pnl / start_cap * 100) if start_cap != 0 else 0 monthly_data.append({ "month": str(m), "pnl": total_pnl, "return_pct": return_pct }) monthly_df = pd.DataFrame(monthly_data) monthly_plot = monthly_df.hvplot.bar( x="month", y="return_pct", title="Monthly Performance (%)", ylabel="Return (%)", xlabel="Month", grid=True, height=300, responsive=True, color="#9C27B0", yformatter="%.1f%%", rot=45, ) if not monthly_df.empty else pn.pane.Markdown("No monthly data") # 2. Cumulative Commission comm_plot = analysis_df.hvplot.line( x="index", y="cumulative_comm", title="Cumulative Commissions Paid", ylabel="Total Commission ($)", xlabel="Trade #", grid=True, height=200, responsive=True, color="#FF9800", ) # 2. Drawdown drawdown_plot = analysis_df.hvplot.area( y="drawdown", title="Drawdown", ylabel="Drawdown ($)", grid=True, height=200, responsive=True, color="red", alpha=0.3, ) # 2b. Drawdown % drawdown_pct_plot = analysis_df.hvplot.area( y="drawdown_pct", title="Drawdown %", ylabel="Drawdown (%)", grid=True, height=200, responsive=True, color="red", alpha=0.3, ) # 3. P&L Distribution pnl_dist_plot = analysis_df.hvplot.hist( y="pnl", title="P&L Distribution", bins=30, height=300, responsive=True ) # 4. Ticker Performance (Top/Bottom 10) ticker_stats = analysis_df.groupby("ticker")["pnl"].sum().sort_values() if len(ticker_stats) > 20: # Show top 10 and bottom 10 top = ticker_stats.tail(10) bottom = ticker_stats.head(10) subset = pd.concat([bottom, top]) else: subset = ticker_stats ticker_plot = subset.hvplot.bar( title="P&L by Ticker (Best/Worst)", rot=45, height=400, responsive=True ) # 5. Metrics Table # Format metrics for display metrics_df = pd.DataFrame( [ {"Metric": k, "Value": f"{v:.2f}" if isinstance(v, float) else v} for k, v in results.items() if not isinstance(v, pd.DataFrame) ] ) metrics_table = pn.widgets.Tabulator(metrics_df, disabled=True, show_index=False) # 6. Trades Table (Paginated) display_trades_df = trades_df.copy() for col in display_trades_df.select_dtypes(include=['float', 'float64']).columns: display_trades_df[col] = display_trades_df[col].fillna(0).astype(int) trades_table = pn.widgets.Tabulator( display_trades_df, pagination="local", page_size=10, sizing_mode="stretch_width", ) # Layout dashboard = pn.Column( pn.Row( pn.Column(metrics_table, width=300), pn.Column( equity_plot, drawdown_plot, drawdown_pct_plot, comm_plot, capital_days_plot, profit_days_plot, monthly_plot, ), ), pn.Row(pnl_dist_plot, ticker_plot), pn.layout.Divider(), "### Trade Log", trades_table, ) return dashboard # Bind the function to the button # We effectively want to replace the main content when button is clicked # pn.bind is one way, or just updating a dynamic map. # Simplest: use a Column that we clear and append to. output_area = pn.Column() def on_click(event): output_area.clear() output_area.append(pn.indicators.LoadingSpinner(value=True, width=50, height=50)) try: content = execute_backtest() output_area.clear() output_area.append(content) except Exception as e: output_area.clear() output_area.append(pn.pane.Markdown(f"## Error during execution: {e}")) run_button.on_click(on_click) # --- Layout --- sidebar = pn.Column( "## Configuration", query_input, risk_per_trade_input, stop_loss_input, take_profit_input, initial_capital_input, max_trades_input, commission_amount_input, start_date_input, end_date_input, high_spike_checkbox, high_spike_text, run_button, pn.layout.Divider(), "**Note**: Ensure `HF_TOKEN` is set in `.env` to download data.", ) template = pn.template.FastListTemplate( title="Penny Stock Short GAP UP Strategy Backtester", sidebar=[sidebar], main=[output_area], accent_base_color="#1f77b4", header_background="#1f77b4", ) # Servable template.servable() if __name__ == "__main__": # If run as script pn.serve(template, show=False, port=5010)