| | 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") |
| |
|
| | |
| | |
| | print("Loading data... (this might take a moment if downloading)") |
| | try: |
| | |
| | |
| | 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}") |
| |
|
| | |
| | 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 |
| | ) |
| |
|
| | |
| | default_start = pd.Timestamp("2024-10-07").date() |
| | default_end = pd.Timestamp("2026-01-01").date() |
| |
|
| | 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") |
| |
|
| |
|
| | |
| | def execute_backtest(event=None): |
| | |
| | |
| | |
| | |
| |
|
| | current_query = query_input.value |
| |
|
| | |
| | |
| | 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)") |
| |
|
| | |
| | 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") |
| |
|
| | |
| | results, analysis_df = analyze_day_trading(trades_df) |
| |
|
| | |
| |
|
| | |
| | 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"], |
| | ) |
| |
|
| | |
| | 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", |
| | ) |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | 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", |
| | "pnl": "sum", |
| | } |
| | ) |
| | .reset_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") |
| |
|
| | |
| | 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", |
| | ) |
| |
|
| | |
| | drawdown_plot = analysis_df.hvplot.area( |
| | y="drawdown", |
| | title="Drawdown", |
| | ylabel="Drawdown ($)", |
| | grid=True, |
| | height=200, |
| | responsive=True, |
| | color="red", |
| | alpha=0.3, |
| | ) |
| |
|
| | |
| | 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, |
| | ) |
| |
|
| | |
| | pnl_dist_plot = analysis_df.hvplot.hist( |
| | y="pnl", title="P&L Distribution", bins=30, height=300, responsive=True |
| | ) |
| |
|
| | |
| | ticker_stats = analysis_df.groupby("ticker")["pnl"].sum().sort_values() |
| | if len(ticker_stats) > 20: |
| | |
| | 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 |
| | ) |
| |
|
| | |
| | |
| | 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) |
| |
|
| | |
| | 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", |
| | ) |
| |
|
| | |
| | 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 |
| |
|
| |
|
| | |
| | |
| | |
| | |
| |
|
| | 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) |
| |
|
| | |
| | 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", |
| | ) |
| |
|
| | |
| | template.servable() |
| |
|
| | if __name__ == "__main__": |
| | |
| | pn.serve(template, show=False, port=5010) |
| |
|