|
|
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) |
|
|
|