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

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +51 -18
src/streamlit_app.py CHANGED
@@ -104,7 +104,7 @@ SIZE_COLORS = {
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:
@@ -150,14 +150,15 @@ def render_whale_monitor():
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
 
157
  with st.sidebar:
158
  st.subheader("Single Ticker Settings")
159
  symbol = st.text_input("Ticker", value="AIIO", key="wm_ticker").strip().upper()
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")
@@ -174,13 +175,27 @@ def render_whale_single_ticker():
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.")
@@ -191,28 +206,45 @@ def render_whale_single_ticker():
191
  st.error("Not enough volume data after filtering.")
192
  return
193
 
194
- median_vol = df[df["Volume"] > 0]["Volume"].median()
195
- mean_vol = df[df["Volume"] > 0]["Volume"].mean()
 
 
 
 
 
 
 
 
196
 
197
  df["Price_Change"] = df["Close"].diff()
198
  df["Pct_Change"] = df["Close"].pct_change() * 100
199
- df["Vol_Ratio"] = df["Volume"] / median_vol
200
- df["is_notable"] = df["Volume"] > (median_vol * multiplier) if not show_all else True
201
 
202
  df[["Delta", "Sentiment"]] = df.apply(lambda r: pd.Series(get_sentiment(r)), axis=1)
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}")
210
- c3.metric("Flag Threshold", f"{median_vol * multiplier:,.0f}")
211
  c4.metric(
212
  "Last Price",
213
  f"${df['Close'].iloc[-1]:.2f}",
214
  f"{df['Pct_Change'].iloc[-1]:+.2f}%" if pd.notna(df["Pct_Change"].iloc[-1]) else None,
215
  )
 
 
 
 
216
 
217
  st.markdown("### Notable Activity")
218
  notable = df[df["is_notable"]].copy()
@@ -248,7 +280,7 @@ def render_whale_single_ticker():
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"],
@@ -307,7 +339,8 @@ def render_whale_single_ticker():
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
 
 
104
  def fetch_whale_data(sym: str) -> pd.DataFrame:
105
  try:
106
  ticker = yf.Ticker(sym)
107
+ # 1-minute granularity 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:
 
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
+ "compared against each day's own volume baseline. "
155
  "This is a lit-market 'unusual volume' heuristic, not off-exchange dark pool print data."
156
  )
157
 
158
  with st.sidebar:
159
  st.subheader("Single Ticker Settings")
160
  symbol = st.text_input("Ticker", value="AIIO", key="wm_ticker").strip().upper()
161
+ multiplier = st.slider("Sensitivity (x daily median volume)", 1.0, 10.0, 2.0, 0.5, key="wm_mult")
162
  whale_threshold = st.number_input("Whale alert threshold (volume)", value=50000, step=5000, key="wm_thresh")
163
  show_all = st.checkbox("Show all candles (ignore threshold)", value=False, key="wm_all")
164
  exclude_open = st.checkbox("Exclude opening auction print (each day's first bar)", value=True, key="wm_exopen")
 
175
  return
176
 
177
  df = df.copy()
178
+
179
+ # Compute the day-grouping column once, up front, and keep it around
180
+ # for both the "exclude opening bar" step and the daily-median calc
181
+ # below. Only drop it right at the end (with errors="ignore" as a
182
+ # safety net in case a pandas version strips it during groupby.apply).
183
+ df["_day"] = df.index.normalize()
184
+
185
+ first_day = df["_day"].min()
186
+ last_day = df["_day"].max()
187
 
188
  if exclude_open:
189
+ def _drop_first_bar(g):
190
+ return g.iloc[1:] if len(g) > 1 else g
191
+
192
+ df = df.groupby("_day", group_keys=False).apply(_drop_first_bar, include_groups=True)
193
+
194
+ # Some pandas versions strip the grouping column from the result
195
+ # of groupby(...).apply(...) even with include_groups=True on
196
+ # certain releases; rebuild it defensively if that happened.
197
+ if "_day" not in df.columns:
198
+ df["_day"] = df.index.normalize()
199
 
200
  if df.empty:
201
  st.error("No data for the selected 7-day window.")
 
206
  st.error("Not enough volume data after filtering.")
207
  return
208
 
209
+ # --- Per-day median/mean, not pooled across the whole week ---
210
+ # This keeps "WHALE"/"INSTITUTIONAL"/etc. classification accurate to
211
+ # each day's own volume regime, rather than diluting a high-volume day
212
+ # into a quiet week's baseline (or vice versa).
213
+ vol_positive = df["Volume"] > 0
214
+ daily_median = df[vol_positive].groupby(df["_day"])["Volume"].median()
215
+ daily_mean = df[vol_positive].groupby(df["_day"])["Volume"].mean()
216
+
217
+ df["Daily_Median_Vol"] = df["_day"].map(daily_median)
218
+ df["Daily_Mean_Vol"] = df["_day"].map(daily_mean)
219
 
220
  df["Price_Change"] = df["Close"].diff()
221
  df["Pct_Change"] = df["Close"].pct_change() * 100
222
+ df["Vol_Ratio"] = df["Volume"] / df["Daily_Median_Vol"]
223
+ df["is_notable"] = df["Volume"] > (df["Daily_Median_Vol"] * multiplier) if not show_all else True
224
 
225
  df[["Delta", "Sentiment"]] = df.apply(lambda r: pd.Series(get_sentiment(r)), axis=1)
226
  df["Cum_Delta"] = df["Delta"].cumsum()
227
+ df["Size"] = df.apply(lambda r: classify_size(r["Volume"], r["Daily_Median_Vol"]), axis=1)
228
+
229
+ # Overall (pooled) figures shown in the top metrics β€” labeled clearly
230
+ # as an average-of-days summary, not the classification baseline.
231
+ median_vol = daily_median.mean()
232
+ mean_vol = daily_mean.mean()
233
 
234
  st.subheader(f"{symbol} β€” Last 7 Days: {first_day.date()} to {last_day.date()}")
235
  c1, c2, c3, c4 = st.columns(4)
236
+ c1.metric("Avg of Daily Median Vol", f"{median_vol:,.0f}")
237
+ c2.metric("Avg of Daily Mean Vol", f"{mean_vol:,.0f}")
238
+ c3.metric("Flag Threshold (avg)", f"{median_vol * multiplier:,.0f}")
239
  c4.metric(
240
  "Last Price",
241
  f"${df['Close'].iloc[-1]:.2f}",
242
  f"{df['Pct_Change'].iloc[-1]:+.2f}%" if pd.notna(df["Pct_Change"].iloc[-1]) else None,
243
  )
244
+ st.caption(
245
+ "Size-tier classification (WHALE/INSTITUTIONAL/etc.) and the Notable Activity table below use "
246
+ "each bar's **own day's** median volume, not the pooled 7-day figure shown above."
247
+ )
248
 
249
  st.markdown("### Notable Activity")
250
  notable = df[df["is_notable"]].copy()
 
280
  name=size.title(),
281
  line=dict(color=color, width=2),
282
  ))
283
+ # thin dotted total line for reference
284
  fig.add_trace(go.Scatter(
285
  x=df.index,
286
  y=df["Cum_Delta"],
 
339
  st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True)
340
 
341
  st.caption(
342
+ "Data source: yfinance (Yahoo Finance), unofficial and delayed. 1-minute bars, 7-day window, "
343
+ "classified against each day's own volume baseline. "
344
  "Cached for 2 minutes β€” use the Refresh button in the sidebar for the latest data."
345
  )
346