Spaces:
Running
Running
Update src/streamlit_app.py
Browse files- 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 |
-
|
|
|
|
|
|
|
| 98 |
except Exception:
|
| 99 |
return pd.DataFrame()
|
| 100 |
|
|
@@ -132,193 +144,13 @@ def render_whale_monitor():
|
|
| 132 |
render_premarket_scan()
|
| 133 |
|
| 134 |
|
| 135 |
-
|
| 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 (
|
| 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 |
-
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
if df.empty:
|
| 347 |
-
st.error("No data for the
|
| 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} β
|
| 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
|
| 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 |
-
|
| 398 |
-
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|