AmirTrader commited on
Commit
bd67d94
·
verified ·
1 Parent(s): df6b3da

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +387 -0
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)