Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import panel as pn
|
| 2 |
+
import hvplot.pandas
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import numpy as np
|
| 5 |
+
from utils import load_data, DEFAULT_FILTER_QUERY
|
| 6 |
+
from backtester import run_backtest, analyze_day_trading
|
| 7 |
+
|
| 8 |
+
pn.extension("tabulator")
|
| 9 |
+
|
| 10 |
+
# --- 1. Load Data ---
|
| 11 |
+
# Initial load
|
| 12 |
+
print("Loading data... (this might take a moment if downloading)")
|
| 13 |
+
try:
|
| 14 |
+
# Cache the data in memory so we don't reload on every callback
|
| 15 |
+
# In a production app you might handle this differently
|
| 16 |
+
GLOBAL_DF = load_data()
|
| 17 |
+
print(f"Data loaded. Rows: {len(GLOBAL_DF)}")
|
| 18 |
+
except Exception as e:
|
| 19 |
+
GLOBAL_DF = pd.DataFrame()
|
| 20 |
+
print(f"Error loading data: {e}")
|
| 21 |
+
|
| 22 |
+
# --- 2. Widgets ---
|
| 23 |
+
query_input = pn.widgets.TextAreaInput(
|
| 24 |
+
name="Filter Query (Pandas Syntax)",
|
| 25 |
+
value=DEFAULT_FILTER_QUERY,
|
| 26 |
+
height=100,
|
| 27 |
+
sizing_mode="stretch_width",
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
risk_per_trade_input = pn.widgets.FloatSlider(
|
| 31 |
+
name="Risk Per Trade (%)", start=0.01, end=1.00, step=0.01, value=0.15
|
| 32 |
+
)
|
| 33 |
+
stop_loss_input = pn.widgets.FloatSlider(
|
| 34 |
+
name="Stop Loss (%)", start=0.01, end=1.00, step=0.01, value=0.35
|
| 35 |
+
)
|
| 36 |
+
take_profit_input = pn.widgets.FloatSlider(
|
| 37 |
+
name="Take Profit (%)", start=0.01, end=1.00, step=0.01, value=0.55
|
| 38 |
+
)
|
| 39 |
+
initial_capital_input = pn.widgets.FloatInput(
|
| 40 |
+
name="Initial Capital ($)", value=10000.0, step=100
|
| 41 |
+
)
|
| 42 |
+
max_trades_input = pn.widgets.IntSlider(
|
| 43 |
+
name="Max Trades Per Day", start=1, end=20, value=6
|
| 44 |
+
)
|
| 45 |
+
commission_amount_input = pn.widgets.FloatInput(
|
| 46 |
+
name="Commission Amount ($ per 200 shares)", value=2.0, step=0.1
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
# Date Range (default based on user script)
|
| 50 |
+
default_start = pd.Timestamp("2024-10-07").date()
|
| 51 |
+
default_end = pd.Timestamp("2026-01-01").date() # Future date from user script
|
| 52 |
+
|
| 53 |
+
start_date_input = pn.widgets.DatePicker(
|
| 54 |
+
name="Start Date",
|
| 55 |
+
value=default_start,
|
| 56 |
+
start=pd.Timestamp("2020-01-01").date(),
|
| 57 |
+
end=pd.Timestamp("2026-01-01").date(),
|
| 58 |
+
)
|
| 59 |
+
end_date_input = pn.widgets.DatePicker(
|
| 60 |
+
name="End Date",
|
| 61 |
+
value=default_end,
|
| 62 |
+
start=pd.Timestamp("2020-01-01").date(),
|
| 63 |
+
end=pd.Timestamp("2026-01-01").date(),
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
high_spike_checkbox = pn.widgets.Checkbox(name="Include High Spike (marketsession_high)", value=False)
|
| 67 |
+
high_spike_text = pn.pane.Markdown("this is high spike", styles={'font-size': '0.9em', 'color': 'gray', 'margin-top': '-10px'})
|
| 68 |
+
|
| 69 |
+
run_button = pn.widgets.Button(name="Run Backtest", button_type="primary")
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# --- 3. Callbacks & Logic ---
|
| 73 |
+
def execute_backtest(event=None):
|
| 74 |
+
# Determine if we need to reload data based on query
|
| 75 |
+
# To be safe and simple, we reload if the user changed the query or if we just want to ensure consistency.
|
| 76 |
+
# Given the file is small (parquet), we can reload or filter.
|
| 77 |
+
# Note: load_data() handles reading and filtering.
|
| 78 |
+
|
| 79 |
+
current_query = query_input.value
|
| 80 |
+
|
| 81 |
+
# We will reload the data with the specific query
|
| 82 |
+
# If this becomes slow, we can optimize to cache the unfiltered raw data and filter here.
|
| 83 |
+
try:
|
| 84 |
+
current_df = load_data(filter_query=current_query)
|
| 85 |
+
except Exception as e:
|
| 86 |
+
return pn.pane.Markdown(f"## Error loading data/applying query: {e}")
|
| 87 |
+
|
| 88 |
+
if current_df.empty:
|
| 89 |
+
return pn.pane.Markdown("## Error: No Data Loaded (Empty after filter)")
|
| 90 |
+
|
| 91 |
+
# Get values
|
| 92 |
+
rpt = risk_per_trade_input.value
|
| 93 |
+
sl = stop_loss_input.value
|
| 94 |
+
tp = take_profit_input.value
|
| 95 |
+
init_cap = initial_capital_input.value
|
| 96 |
+
max_trades = max_trades_input.value
|
| 97 |
+
comm_amt = commission_amount_input.value
|
| 98 |
+
start_date = start_date_input.value
|
| 99 |
+
end_date = end_date_input.value
|
| 100 |
+
include_high = high_spike_checkbox.value
|
| 101 |
+
|
| 102 |
+
trades_df = run_backtest(
|
| 103 |
+
current_df,
|
| 104 |
+
rpt,
|
| 105 |
+
sl,
|
| 106 |
+
tp,
|
| 107 |
+
init_cap,
|
| 108 |
+
start_date,
|
| 109 |
+
end_date,
|
| 110 |
+
max_trades,
|
| 111 |
+
commission_amount=comm_amt,
|
| 112 |
+
include_high_spike=include_high,
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
if trades_df.empty:
|
| 116 |
+
return pn.pane.Markdown("## No trades found for this configuration")
|
| 117 |
+
|
| 118 |
+
# Analyze
|
| 119 |
+
results, analysis_df = analyze_day_trading(trades_df)
|
| 120 |
+
|
| 121 |
+
# --- Visuals ---
|
| 122 |
+
|
| 123 |
+
# 1. Equity Curve (Net vs Gross)
|
| 124 |
+
equity_plot = analysis_df.hvplot.line(
|
| 125 |
+
x="index",
|
| 126 |
+
y=["capital_net", "capital_gross"],
|
| 127 |
+
value_label="Capital ($)",
|
| 128 |
+
title="Account Growth (Net vs Gross)",
|
| 129 |
+
ylabel="Capital ($)",
|
| 130 |
+
xlabel="Trade #",
|
| 131 |
+
grid=True,
|
| 132 |
+
height=400,
|
| 133 |
+
responsive=True,
|
| 134 |
+
color=["#4CAF50", "#2196F3"],
|
| 135 |
+
hover_cols=["ticker", "pnl"],
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
# 1b. Capital & Profit over Days
|
| 139 |
+
daily_stats = analysis_df.groupby("date").agg({
|
| 140 |
+
"capital_net": "last",
|
| 141 |
+
"capital_gross": "last",
|
| 142 |
+
"pnl": "sum",
|
| 143 |
+
"pnl_gross": "sum"
|
| 144 |
+
}).reset_index()
|
| 145 |
+
|
| 146 |
+
capital_days_plot = daily_stats.hvplot.line(
|
| 147 |
+
x="date",
|
| 148 |
+
y=["capital_net", "capital_gross"],
|
| 149 |
+
title="Capital over Days",
|
| 150 |
+
ylabel="Capital ($)",
|
| 151 |
+
grid=True,
|
| 152 |
+
height=300,
|
| 153 |
+
responsive=True,
|
| 154 |
+
color=["#4CAF50", "#2196F3"],
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
profit_days_plot = daily_stats.hvplot.bar(
|
| 158 |
+
x="date",
|
| 159 |
+
y=["pnl", "pnl_gross"],
|
| 160 |
+
title="Daily Profit (Net vs Gross)",
|
| 161 |
+
ylabel="Profit ($)",
|
| 162 |
+
grid=True,
|
| 163 |
+
height=300,
|
| 164 |
+
responsive=True,
|
| 165 |
+
alpha=0.6,
|
| 166 |
+
color=["#4CAF50", "#2196F3"],
|
| 167 |
+
yformatter="%.0f",
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
# 1c. Monthly Performance
|
| 171 |
+
# We need to calculate monthly return %.
|
| 172 |
+
# Strategy: Group by Month.
|
| 173 |
+
# Monthly PnL = Sum(pnl)
|
| 174 |
+
# Monthly Return % = Monthly PnL / Start of Month Capital * 100
|
| 175 |
+
# Start of Month Capital can be approximated by:
|
| 176 |
+
# First trade of month 'capital_net' - trade 'pnl' -> This is capital before the first trade of the month.
|
| 177 |
+
# (Note: This neglects capital changes if there were no trades for a while, but it's a good approximation for active trading)
|
| 178 |
+
|
| 179 |
+
analysis_df["month"] = pd.to_datetime(analysis_df["date"]).dt.to_period("M")
|
| 180 |
+
monthly_stats = (
|
| 181 |
+
analysis_df.groupby("month")
|
| 182 |
+
.agg(
|
| 183 |
+
{
|
| 184 |
+
"pnl": "sum",
|
| 185 |
+
"pnl_gross": "sum",
|
| 186 |
+
"capital_net": "first", # We'll adjust this
|
| 187 |
+
"pnl": "sum", # Re-asserting sum
|
| 188 |
+
}
|
| 189 |
+
)
|
| 190 |
+
.reset_index()
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
# 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.
|
| 194 |
+
# A more robust way:
|
| 195 |
+
# For each month, find the first trade index.
|
| 196 |
+
monthly_data = []
|
| 197 |
+
for m in analysis_df["month"].unique():
|
| 198 |
+
month_trades = analysis_df[analysis_df["month"] == m]
|
| 199 |
+
if month_trades.empty:
|
| 200 |
+
continue
|
| 201 |
+
|
| 202 |
+
first_trade = month_trades.iloc[0]
|
| 203 |
+
start_cap = first_trade["capital_net"] - first_trade["pnl"]
|
| 204 |
+
|
| 205 |
+
total_pnl = month_trades["pnl"].sum()
|
| 206 |
+
return_pct = (total_pnl / start_cap * 100) if start_cap != 0 else 0
|
| 207 |
+
|
| 208 |
+
monthly_data.append({
|
| 209 |
+
"month": str(m),
|
| 210 |
+
"pnl": total_pnl,
|
| 211 |
+
"return_pct": return_pct
|
| 212 |
+
})
|
| 213 |
+
|
| 214 |
+
monthly_df = pd.DataFrame(monthly_data)
|
| 215 |
+
|
| 216 |
+
monthly_plot = monthly_df.hvplot.bar(
|
| 217 |
+
x="month",
|
| 218 |
+
y="return_pct",
|
| 219 |
+
title="Monthly Performance (%)",
|
| 220 |
+
ylabel="Return (%)",
|
| 221 |
+
xlabel="Month",
|
| 222 |
+
grid=True,
|
| 223 |
+
height=300,
|
| 224 |
+
responsive=True,
|
| 225 |
+
color="#9C27B0",
|
| 226 |
+
yformatter="%.1f%%",
|
| 227 |
+
rot=45,
|
| 228 |
+
) if not monthly_df.empty else pn.pane.Markdown("No monthly data")
|
| 229 |
+
|
| 230 |
+
# 2. Cumulative Commission
|
| 231 |
+
comm_plot = analysis_df.hvplot.line(
|
| 232 |
+
x="index",
|
| 233 |
+
y="cumulative_comm",
|
| 234 |
+
title="Cumulative Commissions Paid",
|
| 235 |
+
ylabel="Total Commission ($)",
|
| 236 |
+
xlabel="Trade #",
|
| 237 |
+
grid=True,
|
| 238 |
+
height=200,
|
| 239 |
+
responsive=True,
|
| 240 |
+
color="#FF9800",
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
# 2. Drawdown
|
| 244 |
+
drawdown_plot = analysis_df.hvplot.area(
|
| 245 |
+
y="drawdown",
|
| 246 |
+
title="Drawdown",
|
| 247 |
+
ylabel="Drawdown ($)",
|
| 248 |
+
grid=True,
|
| 249 |
+
height=200,
|
| 250 |
+
responsive=True,
|
| 251 |
+
color="red",
|
| 252 |
+
alpha=0.3,
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
# 2b. Drawdown %
|
| 256 |
+
drawdown_pct_plot = analysis_df.hvplot.area(
|
| 257 |
+
y="drawdown_pct",
|
| 258 |
+
title="Drawdown %",
|
| 259 |
+
ylabel="Drawdown (%)",
|
| 260 |
+
grid=True,
|
| 261 |
+
height=200,
|
| 262 |
+
responsive=True,
|
| 263 |
+
color="red",
|
| 264 |
+
alpha=0.3,
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
# 3. P&L Distribution
|
| 268 |
+
pnl_dist_plot = analysis_df.hvplot.hist(
|
| 269 |
+
y="pnl", title="P&L Distribution", bins=30, height=300, responsive=True
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
# 4. Ticker Performance (Top/Bottom 10)
|
| 273 |
+
ticker_stats = analysis_df.groupby("ticker")["pnl"].sum().sort_values()
|
| 274 |
+
if len(ticker_stats) > 20:
|
| 275 |
+
# Show top 10 and bottom 10
|
| 276 |
+
top = ticker_stats.tail(10)
|
| 277 |
+
bottom = ticker_stats.head(10)
|
| 278 |
+
subset = pd.concat([bottom, top])
|
| 279 |
+
else:
|
| 280 |
+
subset = ticker_stats
|
| 281 |
+
|
| 282 |
+
ticker_plot = subset.hvplot.bar(
|
| 283 |
+
title="P&L by Ticker (Best/Worst)", rot=45, height=400, responsive=True
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
# 5. Metrics Table
|
| 287 |
+
# Format metrics for display
|
| 288 |
+
metrics_df = pd.DataFrame(
|
| 289 |
+
[
|
| 290 |
+
{"Metric": k, "Value": f"{v:.2f}" if isinstance(v, float) else v}
|
| 291 |
+
for k, v in results.items()
|
| 292 |
+
if not isinstance(v, pd.DataFrame)
|
| 293 |
+
]
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
metrics_table = pn.widgets.Tabulator(metrics_df, disabled=True, show_index=False)
|
| 297 |
+
|
| 298 |
+
# 6. Trades Table (Paginated)
|
| 299 |
+
display_trades_df = trades_df.copy()
|
| 300 |
+
for col in display_trades_df.select_dtypes(include=['float', 'float64']).columns:
|
| 301 |
+
display_trades_df[col] = display_trades_df[col].fillna(0).astype(int)
|
| 302 |
+
|
| 303 |
+
trades_table = pn.widgets.Tabulator(
|
| 304 |
+
display_trades_df,
|
| 305 |
+
pagination="local",
|
| 306 |
+
page_size=10,
|
| 307 |
+
sizing_mode="stretch_width",
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
# Layout
|
| 311 |
+
dashboard = pn.Column(
|
| 312 |
+
pn.Row(
|
| 313 |
+
pn.Column(metrics_table, width=300),
|
| 314 |
+
pn.Column(
|
| 315 |
+
equity_plot,
|
| 316 |
+
drawdown_plot,
|
| 317 |
+
drawdown_pct_plot,
|
| 318 |
+
comm_plot,
|
| 319 |
+
capital_days_plot,
|
| 320 |
+
profit_days_plot,
|
| 321 |
+
monthly_plot,
|
| 322 |
+
),
|
| 323 |
+
),
|
| 324 |
+
pn.Row(pnl_dist_plot, ticker_plot),
|
| 325 |
+
pn.layout.Divider(),
|
| 326 |
+
"### Trade Log",
|
| 327 |
+
trades_table,
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
return dashboard
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
# Bind the function to the button
|
| 334 |
+
# We effectively want to replace the main content when button is clicked
|
| 335 |
+
# pn.bind is one way, or just updating a dynamic map.
|
| 336 |
+
# Simplest: use a Column that we clear and append to.
|
| 337 |
+
|
| 338 |
+
output_area = pn.Column()
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def on_click(event):
|
| 342 |
+
output_area.clear()
|
| 343 |
+
output_area.append(pn.indicators.LoadingSpinner(value=True, width=50, height=50))
|
| 344 |
+
try:
|
| 345 |
+
content = execute_backtest()
|
| 346 |
+
output_area.clear()
|
| 347 |
+
output_area.append(content)
|
| 348 |
+
except Exception as e:
|
| 349 |
+
output_area.clear()
|
| 350 |
+
output_area.append(pn.pane.Markdown(f"## Error during execution: {e}"))
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
run_button.on_click(on_click)
|
| 354 |
+
|
| 355 |
+
# --- Layout ---
|
| 356 |
+
sidebar = pn.Column(
|
| 357 |
+
"## Configuration",
|
| 358 |
+
query_input,
|
| 359 |
+
risk_per_trade_input,
|
| 360 |
+
stop_loss_input,
|
| 361 |
+
take_profit_input,
|
| 362 |
+
initial_capital_input,
|
| 363 |
+
max_trades_input,
|
| 364 |
+
commission_amount_input,
|
| 365 |
+
start_date_input,
|
| 366 |
+
end_date_input,
|
| 367 |
+
high_spike_checkbox,
|
| 368 |
+
high_spike_text,
|
| 369 |
+
run_button,
|
| 370 |
+
pn.layout.Divider(),
|
| 371 |
+
"**Note**: Ensure `HF_TOKEN` is set in `.env` to download data.",
|
| 372 |
+
)
|
| 373 |
+
|
| 374 |
+
template = pn.template.FastListTemplate(
|
| 375 |
+
title="Penny Stock Short GAP UP Strategy Backtester",
|
| 376 |
+
sidebar=[sidebar],
|
| 377 |
+
main=[output_area],
|
| 378 |
+
accent_base_color="#1f77b4",
|
| 379 |
+
header_background="#1f77b4",
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
# Servable
|
| 383 |
+
template.servable()
|
| 384 |
+
|
| 385 |
+
if __name__ == "__main__":
|
| 386 |
+
# If run as script
|
| 387 |
+
pn.serve(template, show=False, port=5010)
|