AmirTrader's picture
Create app.py
bd67d94 verified
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)