Trebor1009 commited on
Commit
413a42f
Β·
verified Β·
1 Parent(s): 4f29658

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +64 -196
src/streamlit_app.py CHANGED
@@ -89,12 +89,24 @@ def append_hits_to_log(hits_df: pd.DataFrame, settings: dict) -> bool:
89
  # =========================================================
90
  # TAB 1: WHALE MONITOR
91
  # =========================================================
 
 
 
 
 
 
 
 
 
 
92
 
93
  @st.cache_data(ttl=120, show_spinner="Fetching data...")
94
  def fetch_whale_data(sym: str) -> pd.DataFrame:
95
  try:
96
  ticker = yf.Ticker(sym)
97
- return ticker.history(period="5d", interval="1m")
 
 
98
  except Exception:
99
  return pd.DataFrame()
100
 
@@ -132,193 +144,13 @@ def render_whale_monitor():
132
  render_premarket_scan()
133
 
134
 
135
- @st.cache_data(ttl=24 * 3600, show_spinner="Loading curated ticker list...")
136
- def get_clean_premarket_universe():
137
- try:
138
- local_path = hf_hub_download(
139
- repo_id="Trebor1009/clean-premarket-data", repo_type="dataset",
140
- filename="clean_premarket_data.csv", token=HF_TOKEN,
141
- )
142
- df = pd.read_csv(local_path, usecols=["Ticker"])
143
- return sorted(df["Ticker"].dropna().unique().tolist())
144
- except Exception as e:
145
- st.warning(f"Could not load curated ticker list from Trebor1009/clean-premarket-data: {e}")
146
- return []
147
-
148
-
149
- @st.cache_data(ttl=3600, show_spinner="Fetching prior-close reference prices...")
150
- def get_daily_reference_data(tickers_tuple):
151
- ref = {}
152
- batch_size = 10
153
- total_batches = max(1, (len(tickers_tuple) + batch_size - 1) // batch_size)
154
- progress = st.progress(0, text="Fetching reference prices...")
155
- today_date = pd.Timestamp.now(tz="America/New_York").normalize().tz_localize(None)
156
-
157
- for b, i in enumerate(range(0, len(tickers_tuple), batch_size)):
158
- batch = tickers_tuple[i:i + batch_size]
159
- try:
160
- data = yf.download(list(batch), period="1mo", interval="1d", group_by="ticker",
161
- progress=False, threads=True)
162
- except Exception:
163
- progress.progress((b + 1) / total_batches)
164
- continue
165
-
166
- for ticker in batch:
167
- try:
168
- try:
169
- raw = data[ticker]
170
- except Exception:
171
- raw = data
172
- closes = raw["Close"].dropna()
173
- vol = raw["Volume"].dropna()
174
- if closes.empty:
175
- continue
176
- idx = closes.index.tz_localize(None) if closes.index.tz is not None else closes.index
177
- prior = closes[idx.normalize() < today_date]
178
- if not prior.empty:
179
- prev_close = float(prior.iloc[-1])
180
- elif len(closes) >= 2:
181
- prev_close = float(closes.iloc[-2])
182
- else:
183
- prev_close = float(closes.iloc[-1])
184
- avg_vol = float(vol.mean()) if len(vol) >= 5 else None
185
- ref[ticker] = {"prev_close": prev_close, "avg_vol": avg_vol}
186
- except Exception:
187
- continue
188
-
189
- progress.progress((b + 1) / total_batches, text=f"Reference data: batch {b + 1}/{total_batches}...")
190
-
191
- progress.empty()
192
- return ref
193
-
194
-
195
- @st.cache_data(ttl=300, show_spinner=False)
196
- def run_premarket_scan(tickers_tuple, min_gap_pct, direction):
197
- ref_data = get_daily_reference_data(tickers_tuple)
198
- results = []
199
- batch_size = 10
200
- total_batches = max(1, (len(tickers_tuple) + batch_size - 1) // batch_size)
201
- progress = st.progress(0, text="Scanning premarket activity...")
202
-
203
- for b, i in enumerate(range(0, len(tickers_tuple), batch_size)):
204
- batch = tickers_tuple[i:i + batch_size]
205
- try:
206
- data = yf.download(list(batch), period="1d", interval="1m", prepost=True,
207
- group_by="ticker", progress=False, threads=True)
208
- except Exception:
209
- progress.progress((b + 1) / total_batches, text=f"Batch {b + 1}/{total_batches} failed, continuing...")
210
- continue
211
-
212
- for ticker in batch:
213
- try:
214
- try:
215
- raw = data[ticker]
216
- except Exception:
217
- raw = data
218
- df = raw.dropna(subset=["Close"])
219
- if df.empty:
220
- continue
221
- if df.index.tz is not None:
222
- df = df.copy()
223
- df.index = df.index.tz_convert("America/New_York")
224
- else:
225
- df = df.copy()
226
- df.index = df.index.tz_localize("America/New_York")
227
- premarket = df[df.index.time < dt_time(9, 30)]
228
- if premarket.empty:
229
- continue
230
- ref = ref_data.get(ticker)
231
- if not ref or not ref.get("prev_close"):
232
- continue
233
- prev_close = ref["prev_close"]
234
- last_price = float(premarket["Close"].iloc[-1])
235
- if not prev_close:
236
- continue
237
- gap_pct = (last_price - prev_close) / prev_close * 100
238
- if abs(gap_pct) < min_gap_pct:
239
- continue
240
- if direction == "Up only" and gap_pct <= 0:
241
- continue
242
- if direction == "Down only" and gap_pct >= 0:
243
- continue
244
- pm_volume = int(premarket["Volume"].sum())
245
- results.append({
246
- "Ticker": ticker,
247
- "Prev Close": round(prev_close, 2),
248
- "Premarket Price": round(last_price, 2),
249
- "Gap %": round(gap_pct, 2),
250
- "Premarket Volume": pm_volume,
251
- "Avg Daily Vol": int(ref["avg_vol"]) if ref.get("avg_vol") else None,
252
- })
253
- except Exception:
254
- continue
255
-
256
- progress.progress((b + 1) / total_batches, text=f"Scanned batch {b + 1}/{total_batches}...")
257
-
258
- progress.empty()
259
- return pd.DataFrame(results)
260
-
261
-
262
- def render_premarket_scan():
263
- st.title("πŸŒ… Premarket Scan")
264
- st.caption(
265
- "Scans for tickers gapping meaningfully in premarket, using price change from the prior "
266
- "close as the signal."
267
- )
268
- st.info(
269
- "πŸ“Š Why price, not volume: Yahoo's free feed updates the price tick during extended hours "
270
- "but doesn't populate real trade volume. This scan uses premarket price gap instead. "
271
- "Premarket volume is still shown when available, just not filtered on."
272
- )
273
- st.warning(
274
- "⚠️ A premarket gap is not a reliable 'buy/short the open' signal by itself β€” it's exactly "
275
- "the setup behind the classic 'gap and fade.' Treat a hit here as 'worth checking the news "
276
- "on,' not a standalone trade signal."
277
- )
278
-
279
- with st.sidebar:
280
- st.subheader("Premarket Scan Settings")
281
- min_gap_pct = st.slider("Min premarket gap (%, either direction)", 1, 30, 5, 1, key="pm_mingap")
282
- direction = st.radio("Direction", ["Both", "Up only", "Down only"], key="pm_direction")
283
-
284
- universe = get_clean_premarket_universe()
285
- st.caption(f"Universe: {len(universe)} tickers from Trebor1009/clean-premarket-data.")
286
-
287
- if st.button("πŸ” Run Premarket Scan", key="pm_scan_btn"):
288
- if not universe:
289
- st.error("No tickers loaded β€” check the dataset is reachable.")
290
- else:
291
- results_df = run_premarket_scan(tuple(universe), min_gap_pct, direction)
292
- st.session_state["pm_results"] = results_df
293
-
294
- if "pm_results" in st.session_state:
295
- results_df = st.session_state["pm_results"]
296
- if results_df.empty:
297
- st.info("No tickers cleared the gap threshold. Try lowering the minimum %.")
298
- else:
299
- EXTREME_GAP_CUTOFF = 50
300
- extreme = results_df[results_df["Gap %"].abs() >= EXTREME_GAP_CUTOFF].copy()
301
- normal = results_df[results_df["Gap %"].abs() < EXTREME_GAP_CUTOFF].copy()
302
- sorted_df = normal.reindex(
303
- normal["Gap %"].abs().sort_values(ascending=False).index
304
- ).reset_index(drop=True)
305
- gainers = sorted_df[sorted_df["Gap %"] > 0].reset_index(drop=True)
306
- losers = sorted_df[sorted_df["Gap %"] < 0].reset_index(drop=True)
307
- st.success(f"{len(sorted_df)} ticker(s) past the gap threshold β€” {len(gainers)} up, {len(losers)} down.")
308
- st.markdown("### Premarket Gainers")
309
- st.dataframe(gainers, use_container_width=True, hide_index=True)
310
- st.markdown("### Premarket Losers")
311
- st.dataframe(losers, use_container_width=True, hide_index=True)
312
- if not extreme.empty:
313
- st.markdown("### ⚠️ Extreme moves β€” verify before trusting")
314
- st.caption(f"Gaps of {EXTREME_GAP_CUTOFF}%+ are almost always a stock split or data artefact.")
315
- st.dataframe(extreme.reset_index(drop=True), use_container_width=True, hide_index=True)
316
 
317
 
318
  def render_whale_single_ticker():
319
  st.title("πŸ‹ Whale Monitor β€” Single Ticker")
320
  st.caption(
321
- "Flags unusual 1-minute volume on the public exchange tape. "
322
  "This is a lit-market 'unusual volume' heuristic, not off-exchange dark pool print data."
323
  )
324
 
@@ -328,7 +160,7 @@ def render_whale_single_ticker():
328
  multiplier = st.slider("Sensitivity (x median volume)", 1.0, 10.0, 2.0, 0.5, key="wm_mult")
329
  whale_threshold = st.number_input("Whale alert threshold (volume)", value=50000, step=5000, key="wm_thresh")
330
  show_all = st.checkbox("Show all candles (ignore threshold)", value=False, key="wm_all")
331
- exclude_open = st.checkbox("Exclude opening auction print (09:30 bar)", value=True, key="wm_exopen")
332
  if st.button("πŸ”„ Refresh data", key="wm_refresh"):
333
  fetch_whale_data.clear()
334
 
@@ -341,15 +173,19 @@ def render_whale_single_ticker():
341
  st.error(f"No data available for '{symbol}'. Check the ticker symbol and try again.")
342
  return
343
 
344
- last_session = df.index.normalize().max()
345
- df = df[df.index.normalize() == last_session].copy()
 
 
 
 
 
 
 
346
  if df.empty:
347
- st.error("No data for the most recent session.")
348
  return
349
 
350
- if exclude_open and len(df) > 1:
351
- df = df.iloc[1:]
352
-
353
  df = df[df["Volume"] >= 0]
354
  if (df["Volume"] > 0).sum() == 0:
355
  st.error("Not enough volume data after filtering.")
@@ -367,7 +203,7 @@ def render_whale_single_ticker():
367
  df["Cum_Delta"] = df["Delta"].cumsum()
368
  df["Size"] = df["Volume"].apply(lambda v: classify_size(v, median_vol))
369
 
370
- st.subheader(f"{symbol} β€” Session: {last_session.date()}")
371
  c1, c2, c3, c4 = st.columns(4)
372
  c1.metric("Median 1-min Volume", f"{median_vol:,.0f}")
373
  c2.metric("Mean 1-min Volume", f"{mean_vol:,.0f}")
@@ -381,7 +217,7 @@ def render_whale_single_ticker():
381
  st.markdown("### Notable Activity")
382
  notable = df[df["is_notable"]].copy()
383
  if notable.empty:
384
- st.info("No candles crossed the sensitivity threshold for this session.")
385
  else:
386
  notable["Whale Alert"] = notable["Volume"] >= whale_threshold
387
  table = notable[["Volume", "Vol_Ratio", "Size", "Sentiment", "Close", "Pct_Change", "Whale Alert"]]
@@ -393,10 +229,42 @@ def render_whale_single_ticker():
393
  use_container_width=True, height=400,
394
  )
395
 
 
 
 
396
  st.markdown("### Cumulative Buy/Sell Delta")
397
- chart_df = df[["Cum_Delta"]].copy()
398
- chart_df.index = chart_df.index.strftime("%H:%M")
399
- st.line_chart(chart_df)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
  total_buy = df[df["Delta"] > 0]["Delta"].sum()
402
  total_sell = abs(df[df["Delta"] < 0]["Delta"].sum())
@@ -439,7 +307,7 @@ def render_whale_single_ticker():
439
  st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True)
440
 
441
  st.caption(
442
- "Data source: yfinance (Yahoo Finance), unofficial and delayed. "
443
  "Cached for 2 minutes β€” use the Refresh button in the sidebar for the latest data."
444
  )
445
 
 
89
  # =========================================================
90
  # TAB 1: WHALE MONITOR
91
  # =========================================================
92
+ import plotly.graph_objects as go # add this import near the top of the file if not already present
93
+
94
+ SIZE_COLORS = {
95
+ "WHALE": "#8e44ad", # purple
96
+ "INSTITUTIONAL": "#e67e22", # orange
97
+ "LARGE": "#2980b9", # blue
98
+ "MEDIUM": "#27ae60", # green
99
+ "RETAIL": "#7f8c8d", # grey
100
+ }
101
+
102
 
103
  @st.cache_data(ttl=120, show_spinner="Fetching data...")
104
  def fetch_whale_data(sym: str) -> pd.DataFrame:
105
  try:
106
  ticker = yf.Ticker(sym)
107
+ # 1-minute granularity preserved for accurate size-tier classification;
108
+ # Yahoo only serves 1m bars for the trailing 7 days.
109
+ return ticker.history(period="7d", interval="1m")
110
  except Exception:
111
  return pd.DataFrame()
112
 
 
144
  render_premarket_scan()
145
 
146
 
147
+ # --- Premarket scan section unchanged, omitted here for brevity ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
 
150
  def render_whale_single_ticker():
151
  st.title("πŸ‹ Whale Monitor β€” Single Ticker")
152
  st.caption(
153
+ "Flags unusual 1-minute volume on the public exchange tape over the trailing 7 days. "
154
  "This is a lit-market 'unusual volume' heuristic, not off-exchange dark pool print data."
155
  )
156
 
 
160
  multiplier = st.slider("Sensitivity (x median volume)", 1.0, 10.0, 2.0, 0.5, key="wm_mult")
161
  whale_threshold = st.number_input("Whale alert threshold (volume)", value=50000, step=5000, key="wm_thresh")
162
  show_all = st.checkbox("Show all candles (ignore threshold)", value=False, key="wm_all")
163
+ exclude_open = st.checkbox("Exclude opening auction print (each day's first bar)", value=True, key="wm_exopen")
164
  if st.button("πŸ”„ Refresh data", key="wm_refresh"):
165
  fetch_whale_data.clear()
166
 
 
173
  st.error(f"No data available for '{symbol}'. Check the ticker symbol and try again.")
174
  return
175
 
176
+ df = df.copy()
177
+ first_day = df.index.normalize().min()
178
+ last_day = df.index.normalize().max()
179
+
180
+ if exclude_open:
181
+ df["_day"] = df.index.normalize()
182
+ df = df.groupby("_day", group_keys=False).apply(lambda g: g.iloc[1:] if len(g) > 1 else g)
183
+ df = df.drop(columns="_day")
184
+
185
  if df.empty:
186
+ st.error("No data for the selected 7-day window.")
187
  return
188
 
 
 
 
189
  df = df[df["Volume"] >= 0]
190
  if (df["Volume"] > 0).sum() == 0:
191
  st.error("Not enough volume data after filtering.")
 
203
  df["Cum_Delta"] = df["Delta"].cumsum()
204
  df["Size"] = df["Volume"].apply(lambda v: classify_size(v, median_vol))
205
 
206
+ st.subheader(f"{symbol} β€” Last 7 Days: {first_day.date()} to {last_day.date()}")
207
  c1, c2, c3, c4 = st.columns(4)
208
  c1.metric("Median 1-min Volume", f"{median_vol:,.0f}")
209
  c2.metric("Mean 1-min Volume", f"{mean_vol:,.0f}")
 
217
  st.markdown("### Notable Activity")
218
  notable = df[df["is_notable"]].copy()
219
  if notable.empty:
220
+ st.info("No candles crossed the sensitivity threshold for this window.")
221
  else:
222
  notable["Whale Alert"] = notable["Volume"] >= whale_threshold
223
  table = notable[["Volume", "Vol_Ratio", "Size", "Sentiment", "Close", "Pct_Change", "Whale Alert"]]
 
229
  use_container_width=True, height=400,
230
  )
231
 
232
+ # ---------------------------------------------------------------
233
+ # Cumulative Buy/Sell Delta β€” per size-tier colored lines
234
+ # ---------------------------------------------------------------
235
  st.markdown("### Cumulative Buy/Sell Delta")
236
+
237
+ delta_by_size = pd.DataFrame(index=df.index)
238
+ for size in ["WHALE", "INSTITUTIONAL", "LARGE", "MEDIUM", "RETAIL"]:
239
+ masked_delta = df["Delta"].where(df["Size"] == size, 0)
240
+ delta_by_size[size] = masked_delta.cumsum()
241
+
242
+ fig = go.Figure()
243
+ for size, color in SIZE_COLORS.items():
244
+ fig.add_trace(go.Scatter(
245
+ x=delta_by_size.index,
246
+ y=delta_by_size[size],
247
+ mode="lines",
248
+ name=size.title(),
249
+ line=dict(color=color, width=2),
250
+ ))
251
+ # optional: thin dotted total line for reference
252
+ fig.add_trace(go.Scatter(
253
+ x=df.index,
254
+ y=df["Cum_Delta"],
255
+ mode="lines",
256
+ name="Total (all tiers)",
257
+ line=dict(color="#2c3e50", width=1.5, dash="dot"),
258
+ ))
259
+ fig.update_layout(
260
+ xaxis_title="Time",
261
+ yaxis_title="Cumulative Delta (shares)",
262
+ legend_title="Size Tier",
263
+ hovermode="x unified",
264
+ height=450,
265
+ margin=dict(l=10, r=10, t=10, b=10),
266
+ )
267
+ st.plotly_chart(fig, use_container_width=True)
268
 
269
  total_buy = df[df["Delta"] > 0]["Delta"].sum()
270
  total_sell = abs(df[df["Delta"] < 0]["Delta"].sum())
 
307
  st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True)
308
 
309
  st.caption(
310
+ "Data source: yfinance (Yahoo Finance), unofficial and delayed. 1-minute bars, 7-day window. "
311
  "Cached for 2 minutes β€” use the Refresh button in the sidebar for the latest data."
312
  )
313