import streamlit as st import yfinance as yf import matplotlib.pyplot as plt import plotly.graph_objects as go import plotly.express as px from datetime import datetime from fredapi import Fred import pandas as pd import requests import numpy as np from scipy.stats import norm st.set_page_config(page_title="Griffin's Elite Risk Board", layout="wide") # ====================== YOUR ORIGINAL DARK THEME ====================== st.markdown(""" """, unsafe_allow_html=True) # ====================== KEYS ====================== FRED_API_KEY = st.secrets.get('FRED_API_KEY', 'your_key') POLYGON_API_KEY = st.secrets.get('POLYGON_API_KEY', 'your_key') fred = Fred(api_key=FRED_API_KEY) # ====================== SIDEBAR ====================== st.sidebar.title("Manual Inputs") fed_rate = st.sidebar.text_input("Current Fed Funds Rate", value="3.50 - 3.75%") fed_next_meeting = st.sidebar.text_input("Next Fed Meeting Date", value="March 18-19, 2026") fed_prob_cut = st.sidebar.number_input("CME Fed Cut Prob (%)", value=4.6, step=1.0) fed_prob_no_change = st.sidebar.number_input("CME Fed No Change Prob (%)", value=95.4, step=1.0) fed_prob_hike = st.sidebar.number_input("CME Fed Hike Prob (%)", value=0.0, step=1.0) st.sidebar.subheader("ESI Manual Input") esi_value = st.sidebar.number_input("Citigroup ESI Value", value=18.5, step=0.1) esi_date = st.sidebar.text_input("ESI Date (YYYY-MM-DD)", value="2026-01-19") # ====================== AUTO DISCORD ALERTS (sidebar) ====================== st.sidebar.subheader("πŸ”” Discord Alerts") enable_alerts = st.sidebar.checkbox("Enable Auto Discord Alerts", value=False) discord_webhook = st.sidebar.text_input("Discord Webhook URL", value="", type="password") # ====================== ALL FETCH FUNCTIONS (including fixed fetch_sentiment) ====================== @st.cache_data(ttl=300) def fetch_pcr(): try: url = "https://www.cboe.com/us/options/market_statistics/daily/" tables = pd.read_html(url) for table in tables: if "Equity" in table.to_string(): equity_row = table[table.iloc[:,0].str.contains("Equity", na=False)] if not equity_row.empty: return float(equity_row.iloc[0,1]) except: pass return 0.78 @st.cache_data(ttl=300) def fetch_skew(): try: return yf.download('^SKEW', period='5d', progress=False)['Close'].iloc[-1].item() except: return 143.78 @st.cache_data(ttl=300) def fetch_vvix(): try: return yf.download('^VVIX', period='5d', progress=False)['Close'].iloc[-1].item() except: return 95.0 @st.cache_data(ttl=300) def fetch_dollar_index(): dxy = yf.download('DX-Y.NYB', period='5d', progress=False) if not dxy.empty: current = dxy['Close'].iloc[-1].item() prev = dxy['Close'].iloc[-2].item() if len(dxy) > 1 else current delta = ((current - prev) / prev) * 100 if prev != 0 else 0 return current, delta return None, None @st.cache_data(ttl=300) def fetch_oil_prices(): wti = yf.download('CL=F', period='5d', progress=False) if not wti.empty: current = wti['Close'].iloc[-1].item() prev = wti['Close'].iloc[-2].item() if len(wti) > 1 else current delta = ((current - prev) / prev) * 100 if prev != 0 else 0 return current, delta return None, None @st.cache_data(ttl=300) def fetch_inflation_index(): cpi = fred.get_series_latest_release('CPIAUCSL') if cpi is not None: latest = cpi.iloc[-1].item() yoy = ((latest - cpi.iloc[-13].item()) / cpi.iloc[-13].item()) * 100 if len(cpi) >= 13 else None return latest, yoy return None, None @st.cache_data(ttl=300) def fetch_gold_silver(): gold = yf.download('GC=F', period='5d', progress=False) silver = yf.download('SI=F', period='5d', progress=False) gold_current = gold['Close'].iloc[-1].item() if not gold.empty else None silver_current = silver['Close'].iloc[-1].item() if not silver.empty else None return gold_current, silver_current @st.cache_data(ttl=300) def fetch_global_markets(): indices = {'S&P 500': '^GSPC', 'NASDAQ': '^IXIC', 'Dow Jones': '^DJI', 'FTSE 100': '^FTSE', 'DAX': '^GDAXI', 'Nikkei 225': '^N225', 'Hang Seng': '^HSI'} data = {} for name, ticker in indices.items(): df = yf.download(ticker, period='5d', progress=False) if not df.empty: data[name] = df['Close'].iloc[-1].item() return data @st.cache_data(ttl=300) def fetch_liquidity_metrics(): ted = fred.get_series_latest_release('TEDRATE') if ted is not None: return {'TED Spread': ted.iloc[-1].item()} return None @st.cache_data(ttl=300) def fetch_spy_volume(): spy_vol = yf.download('SPY', period='2mo', progress=False) if not spy_vol.empty: vol_20_avg = spy_vol['Volume'].rolling(20).mean().iloc[-1] current_vol = spy_vol['Volume'].iloc[-1] vol_ratio = (current_vol / vol_20_avg) * 100 if vol_20_avg != 0 else 0 return current_vol, vol_20_avg, vol_ratio return None, None, None @st.cache_data(ttl=300) def fetch_prior_day_scores(): vix_data = yf.download('^VIX', period='5d', progress=False) spy_data = yf.download('SPY', period='2mo', progress=False) if not vix_data.empty and not spy_data.empty and len(vix_data) > 1 and len(spy_data) > 20: vix_level_prior = vix_data['Close'].iloc[-2].item() vix_score_prior = 20 if vix_level_prior > 20 else (10 if vix_level_prior > 15 else 0) spy_prior_close = spy_data['Close'].iloc[-2].item() mean_20_prior = spy_data['Close'].rolling(20).mean().iloc[-2].item() atr_14_prior = (spy_data['High'] - spy_data['Low']).rolling(14).mean().iloc[-2].item() deviation_prior = (spy_prior_close - mean_20_prior) / atr_14_prior if atr_14_prior != 0 else 0.0 stretch_score_prior = 20 if deviation_prior > 2 else (10 if deviation_prior > 1.5 else 0) prior_score = vix_score_prior + stretch_score_prior + 40 prior_light = "RED" if prior_score >= 50 else "YELLOW" if prior_score >= 25 else "GREEN" return prior_score, prior_light return 30, "YELLOW" def bs_gamma(S, K, T, r, sigma): if T <= 0 or sigma <= 0: return 0.0 d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T)) return norm.pdf(d1) / (S * sigma * np.sqrt(T)) def compute_gamma_levels(ticker="SPY"): try: t = yf.Ticker(ticker) spot = t.history(period="1d")['Close'].iloc[-1] expiry = t.options[0] chain = t.option_chain(expiry) T = (datetime.strptime(expiry, "%Y-%m-%d") - datetime.now()).days / 365.25 r = 0.042 calls, puts = chain.calls, chain.puts calls['GEX'] = calls.apply(lambda row: bs_gamma(spot, row['strike'], T, r, row['impliedVolatility']) * row['openInterest'] * 100, axis=1) puts['GEX'] = puts.apply(lambda row: -bs_gamma(spot, row['strike'], T, r, row['impliedVolatility']) * row['openInterest'] * 100, axis=1) put_wall = puts.loc[puts['GEX'].idxmin()]['strike'] if not puts.empty else spot*0.95 vt = calls[calls['GEX']>0]['strike'].min() if not calls[calls['GEX']>0].empty else spot*0.97 call_wall = calls.loc[calls['GEX'].idxmax()]['strike'] if not calls.empty else spot*1.05 iv = calls.iloc[(calls['strike']-spot).abs().argsort()[0]]['impliedVolatility'] exp_move = spot * iv * np.sqrt(1/365) return {'spot': round(spot,2), 'put_wall': round(put_wall,0), 'vt': round(vt,0), 'call_wall': round(call_wall,0), 'exp_move': f"Β±{exp_move:.1f}"} except: return None def fetch_sentiment(): try: url = f"https://api.polygon.io/v2/reference/news?limit=30&apiKey={POLYGON_API_KEY}" headlines = [n['title'] for n in requests.get(url).json().get('results', [])[:12]] bull = sum(1 for h in headlines if any(w in h.lower() for w in ['bull','rally','beat','upside','higher'])) bear = sum(1 for h in headlines if any(w in h.lower() for w in ['bear','drop','miss','downside','lower'])) score = (bull - bear) / max(len(headlines),1) return round(score,2) except: return 0.0 # ====================== MAIN APP ====================== st.title("MarketCraft In-House Risk Dashboard 🚦") st.write(f"Live as of {datetime.now().strftime('%Y-%m-%d %I:%M %p EST')}") col_left, col_right = st.columns(2) with col_left: # SPY Stretch Check (full original) st.subheader("SPY Stretch Check") st.write("**What it means**: Deviation from 20-day mean in ATRs; high = overextension, pullback risk. Actionable: Trim if >2, watch if >1.5.") try: spy = yf.download('SPY', period='2mo', progress=False) if spy.empty: raise ValueError("No SPY data.") spy_prior = spy['Close'].iloc[-2].item() if len(spy) > 1 else spy['Close'].iloc[-1].item() spy_close = spy['Close'].iloc[-1].item() mean_20 = spy['Close'].rolling(20).mean().iloc[-1].item() atr_14 = (spy['High'] - spy['Low']).rolling(14).mean().iloc[-1].item() deviation = (spy_close - mean_20) / atr_14 if atr_14 != 0 else 0.0 three_atr_above = mean_20 + 3 * atr_14 st.write(f"**Price:** ${spy_close:.2f} (prior day: {'+' if spy_close > spy_prior else ''}{(spy_close - spy_prior):.2f})") st.write(f"**20-day Mean:** {mean_20:.2f}") st.write(f"**ATR(14):** {atr_14:.2f}") st.write(f"**Deviation:** {deviation:.2f} ATRs") st.write(f"**3 ATR Above Mean (High Probability Pullback Level):** ${three_atr_above:.2f}") fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(spy.index, spy['Close'], label='SPY Price', color='blue') ax.axhline(mean_20, color='green', linestyle='--', label='20-day Mean') ax.axhline(three_atr_above, color='red', linestyle='--', label='3 ATR Above') ax.set_title('SPY Price Chart with 3 ATR Level') ax.set_xlabel('Date') ax.set_ylabel('Price') ax.legend() st.pyplot(fig) stretch_score = 20 if deviation > 2 else (10 if deviation > 1.5 else 0) if deviation > 2: st.warning("Strong overextension β†’ trim longs, expect pullback.") elif deviation > 1.5: st.info("Mild extension β†’ monitor for reversal, tighten stops.") else: st.success("Balanced β†’ no action needed.") except Exception as e: st.error(f"Error in SPY data: {e}") stretch_score = 0 with st.expander("Learn More"): st.write("Deviation measured in ATRs. >2 ATRs = strong overextension β†’ high pullback probability. Use 3 ATR line as a likely reversal zone.") # ====================== FIXED KEY OPTIONS LEVELS (accurate SPX/NDX) ====================== st.subheader("Key Options Levels & Vol Trigger") st.write("**What it means**: Gamma-based support/resistance from options OI. VT = flip to higher vol regime below it. Actionable: Support at Put Wall, resistance at Call Wall, hedge below VT. Spot prices shown as index levels (GEX from SPY/QQQ proxies).") try: spx_gex = compute_gamma_levels("SPY") ndx_gex = compute_gamma_levels("QQQ") spx_spot = yf.download('^GSPC', period='1d', progress=False)['Close'].iloc[-1].item() ndx_spot = yf.download('^NDX', period='1d', progress=False)['Close'].iloc[-1].item() spx_ratio = spx_spot / spx_gex['spot'] if spx_gex and spx_gex['spot'] > 0 else 10.0 ndx_ratio = ndx_spot / ndx_gex['spot'] if ndx_gex and ndx_gex['spot'] > 0 else 40.0 if spx_gex and ndx_gex: data_levels = { 'Index': ['SPX', 'NDX'], 'Put Wall (Support)': [round(spx_gex['put_wall'] * spx_ratio, 0), round(ndx_gex['put_wall'] * ndx_ratio, 0)], 'Vol Trigger (VT)': [round(spx_gex['vt'] * spx_ratio, 0), round(ndx_gex['vt'] * ndx_ratio, 0)], 'Spot Price': [round(spx_spot, 2), round(ndx_spot, 2)], 'Call Wall (Resistance)': [round(spx_gex['call_wall'] * spx_ratio, 0), round(ndx_gex['call_wall'] * ndx_ratio, 0)], 'Expected Daily Move (Β±)': [f"Β±{round(float(spx_gex['exp_move'].replace('Β±','')) * spx_ratio, 1)}", f"Β±{round(float(ndx_gex['exp_move'].replace('Β±','')) * ndx_ratio, 1)}"] } df_levels = pd.DataFrame(data_levels) st.dataframe(df_levels, height=150, width='stretch') for idx in ['SPX', 'NDX']: levels = spx_gex if idx == 'SPX' else ndx_gex regime = "Low vol (stable)" if levels['spot'] > levels['vt'] else "High vol (trending)" st.write(f"**{idx} Regime:** {regime}") except Exception as e: st.error(f"Error in levels calc: {e}") with st.expander("Learn More"): st.write("Put Wall = support, Call Wall = resistance, VT = vol flip point. All levels now scaled to actual index prices.") # VIX Fear Gauge (full original) st.subheader("VIX Fear Gauge") st.write("**What it means**: Implied S&P vol; high = fear (buy protection), low = complacency (sell premium). Actionable: Buy premium if >20, caution if >15.") try: vix_data = yf.download('^VIX', period='1mo', progress=False) if vix_data.empty: raise ValueError("No VIX data.") vix_prior = vix_data['Close'].iloc[-2].item() if len(vix_data) > 1 else vix_data['Close'].iloc[-1].item() vix_level = vix_data['Close'].iloc[-1].item() st.write(f"**VIX:** {vix_level:.2f} (prior day: {'+' if vix_level > vix_prior else ''}{vix_level - vix_prior:.2f})") vix_score = 20 if vix_level > 20 else (10 if vix_level > 15 else 0) if vix_level > 20: st.error("High vol/fear β†’ buy premium, hedge positions.") elif vix_level > 15: st.warning("Elevated vol β†’ caution on naked shorts, consider light hedges.") else: st.success("Low vol β†’ premium selling favorable.") except Exception as e: st.error(f"Error in VIX data: {e}") vix_score = 0 with st.expander("Learn More"): st.write("VIX >20 = high fear (buy protection). VIX <15 = complacency (sell premium).") # PCR, Skew, VVIX (full original) st.subheader("Put/Call Ratio (Equity)") pcr_equity = fetch_pcr() pcr_score = 10 if pcr_equity < 0.80 else (15 if pcr_equity > 1.0 else 0) st.write(f"**PCR:** {pcr_equity:.2f}") if pcr_equity < 0.80: st.info("Low PCR β†’ complacency β†’ mild caution.") elif pcr_equity > 1.0: st.warning("High PCR β†’ fear β†’ defensive tilt.") else: st.success("Balanced PCR β†’ neutral.") with st.expander("Learn More"): st.write("Low PCR = bull complacency (fade rallies). High PCR = fear (buy dips).") st.subheader("CBOE Skew") skew = fetch_skew() skew_score = 15 if skew > 135 else 0 st.write(f"**Skew:** {skew}") if skew > 135: st.warning("Elevated Skew β†’ tail risk fear β†’ caution.") else: st.success("Normal Skew β†’ no extreme tail pricing.") with st.expander("Learn More"): st.write(">135 = tail risk fear (buy protection).") st.subheader("VVIX/VIX Ratio") vvix = fetch_vvix() vvix_ratio = vvix / vix_level if 'vix_level' in locals() and vix_level != 0 else 0 st.write(f"**Ratio:** {vvix_ratio:.2f}") vvix_score = 15 if vvix_ratio > 6 else (5 if vvix_ratio > 5 else 0) if vvix_ratio > 6: st.error("High VVIX/VIX β†’ extreme uncertainty β†’ hedge.") elif vvix_ratio > 5: st.warning("Elevated VVIX/VIX β†’ watch vol clustering.") else: st.success("Normal VVIX/VIX β†’ stable vol.") with st.expander("Learn More"): st.write(">6 = high uncertainty (hedge).") # A/D Divergence (full original) st.subheader("Advance/Decline Divergence") st.write("**What it means**: Breadth; divergence = index up but fewer stocks joining, weakness. Actionable: Trim if divergence.") try: ad_nyse = yf.download('^NYA', period='1mo', progress=False)['Close'] spx = yf.download('^GSPC', period='1mo', progress=False)['Close'] spx_recent_high = spx.iloc[-5:].max().item() nya_recent_high = ad_nyse.iloc[-5:].max().item() spx_prev_high = spx.iloc[-10:-5].max().item() if len(spx) >= 10 else spx_recent_high nya_prev_high = ad_nyse.iloc[-10:-5].max().item() if len(ad_nyse) >= 10 else nya_recent_high divergence_score = 20 if (spx_recent_high > spx_prev_high and nya_recent_high < nya_prev_high) else 0 st.write(f"**Recent SPX high:** {spx_recent_high:.0f}") st.write(f"**Recent NYA high:** {nya_recent_high:.0f}") if divergence_score > 0: st.warning("Bearish divergence β†’ breadth weakening β†’ caution.") else: st.success("No clear divergence β†’ participation intact.") except Exception as e: st.error(f"Error in A/D data: {e}") divergence_score = 0 with st.expander("Learn More"): st.write("Divergence = index up but breadth weak (caution).") # Risk On/Off Rotation (full original) st.subheader("Risk On/Off Rotation") st.write("**What it means**: Sector rotation; tech out = risk on, staples = risk off. Actionable: Defensive if < -2%.") try: xlk = yf.download('XLK', period='1mo', progress=False)['Close'] xlp = yf.download('XLP', period='1mo', progress=False)['Close'] xlk_ret = (xlk.iloc[-1].item() - xlk.iloc[0].item()) / xlk.iloc[0].item() * 100 xlp_ret = (xlp.iloc[-1].item() - xlp.iloc[0].item()) / xlp.iloc[0].item() * 100 rotation_diff = xlk_ret - xlp_ret rotation_score = 20 if rotation_diff < -2 else (10 if rotation_diff < 0 else 0) st.write(f"**1-mo XLK return:** {xlk_ret:.1f}%") st.write(f"**1-mo XLP return:** {xlp_ret:.1f}%") st.write(f"**Rotation diff:** {rotation_diff:.1f}%") if rotation_diff < -2: st.warning("Risk OFF β†’ rotation out of tech into safe names β†’ caution.") elif rotation_diff < 0: st.info("Mild risk OFF tilt β†’ watch.") else: st.success("Risk ON β†’ risky assets leading β†’ bullish.") except Exception as e: st.error(f"Error in rotation data: {e}") rotation_score = 0 with st.expander("Learn More"): st.write("Tech leading = risk on. Staples leading = risk off.") # ====================== MARKET LEADERSHIP: Mag7 vs Equal Weight ====================== st.subheader("Market Leadership: Mag7 vs Equal Weight") st.write("**What it means**: If MAGS significantly outperforms the equal-weight S&P 500 (RSP), tech is carrying the market (narrow leadership = higher risk). RSP leading = healthy broad participation.") try: rsp = yf.download('RSP', period='1mo', progress=False) mags = yf.download('MAGS', period='1mo', progress=False) rsp_ret = ((float(rsp['Close'].iloc[-1]) - float(rsp['Close'].iloc[0])) / float(rsp['Close'].iloc[0])) * 100 mags_ret = ((float(mags['Close'].iloc[-1]) - float(mags['Close'].iloc[0])) / float(mags['Close'].iloc[0])) * 100 gap = mags_ret - rsp_ret col1, col2 = st.columns(2) with col1: st.metric("RSP (Equal Weight S&P 500)", f"{rsp_ret:.1f}%") with col2: st.metric("MAGS (Magnificent 7)", f"{mags_ret:.1f}%") if gap > 8: st.error(f"πŸ”΄ Mag7 Strongly Carrying Market (+{gap:.1f}% gap) β€” High Concentration Risk") elif gap > 3: st.warning(f"🟑 Mag7 Leading (+{gap:.1f}% gap) β€” Tech-Dominated Rally") elif gap > -3: st.success("🟒 Balanced Leadership β€” Healthy Broad Participation") else: st.success(f"🟒 Equal Weight Leading (+{abs(gap):.1f}% gap) β€” Strong Market Breadth") except Exception as e: st.error(f"Error loading leadership data: {str(e)[:80]}...") with st.expander("Learn More β€” Why This Matters"): st.write(""" **Institutional View**: - When Mag7 significantly outperforms Equal Weight (RSP), the rally is narrow and fragile. - Large gaps often precede rotations or corrections. - Healthy bull markets have strong RSP participation. - We added this to help paid members see true market strength and concentration risk. """) # Trend Strength (full original ADX - this was missing) st.subheader("Trend Strength (1-10)") st.write("**What it means**: ADX quantifies trend persistence; high = strong direction, low = chop. Actionable: Avoid new positions if <3.") try: spy_daily = yf.download('SPY', period='3mo', interval='1d', progress=False) high = list(spy_daily['High']) low = list(spy_daily['Low']) close = list(spy_daily['Close']) n = len(high) if n < 28: adx = 0.0 else: plus_dm = [0.0] * n minus_dm = [0.0] * n for i in range(1, n): plus_dm[i] = max(high[i] - high[i-1], 0) minus_dm[i] = max(low[i-1] - low[i], 0) tr = [0.0] * n for i in range(1, n): tr1 = high[i] - low[i] tr2 = abs(high[i] - close[i-1]) tr3 = abs(low[i] - close[i-1]) tr[i] = max(tr1, tr2, tr3) def rolling_mean(lst, window): means = [0.0] * len(lst) for i in range(window - 1, len(lst)): means[i] = sum(lst[i - window + 1:i + 1]) / window return means atr_trend = rolling_mean(tr, 14) plus_di = [0.0] * n minus_di = [0.0] * n for i in range(len(plus_dm)): pm_mean = rolling_mean(plus_dm, 14)[i] mm_mean = rolling_mean(minus_dm, 14)[i] plus_di[i] = 100 * (pm_mean / atr_trend[i]) if atr_trend[i] > 0 else 0 minus_di[i] = 100 * (mm_mean / atr_trend[i]) if atr_trend[i] > 0 else 0 dx = [0.0] * n for i in range(len(plus_di)): di_sum = plus_di[i] + minus_di[i] dx[i] = 100 * abs(plus_di[i] - minus_di[i]) / di_sum if di_sum > 0 else 0 adx_raw = rolling_mean(dx, 14) adx = adx_raw[-1] if adx_raw[-1] > 0 else 0.0 trend_strength = round(adx / 10) trend_score_penalty = 10 if trend_strength < 3 else 0 st.write(f"**ADX-based strength:** {trend_strength}/10") if trend_strength >= 7: st.success("Strong trend β†’ momentum intact β†’ hold longs.") elif trend_strength >= 4: st.info("Moderate trend β†’ decent directionality β†’ neutral.") else: st.warning("Weak trend β†’ choppy/sideways β†’ caution, avoid new positions.") except Exception as e: st.error(f"Error in trend data: {e}") trend_score_penalty = 0 with st.expander("Learn More"): st.write("High = strong direction (hold). Low = chop (avoid new positions).") # CME Fed Watch, Liquidity, Composite (full original) st.subheader("CME Fed Watch") st.write("**What it means**: Market probabilities for Fed rate changes; high cut = dovish (bull friendly), high hike = hawkish (risk off). Actionable: Risk off if hike prob >20%.") st.write(f"**Current Fed Funds Rate:** {fed_rate}") st.write(f"**Next Meeting:** {fed_next_meeting}") st.write(f"**Prob Cut:** {fed_prob_cut}%") st.write(f"**Prob No Change:** {fed_prob_no_change}%") st.write(f"**Prob Hike:** {fed_prob_hike}%") fed_score = 10 if fed_prob_hike > 20 else 0 if fed_prob_cut > 50: st.info("High cut prob β†’ dovish Fed, bull friendly.") elif fed_prob_hike > 20: st.warning("High hike prob β†’ hawkish, risk off.") else: st.success("Stable policy outlook β†’ neutral.") with st.expander("Learn More"): st.write("High cut prob = bull friendly. High hike = risk off.") st.subheader("Liquidity Metrics") liquidity = fetch_liquidity_metrics() if liquidity is not None: st.write(f"**TED Spread:** {liquidity['TED Spread']:.2f}") else: st.error("Data unavailable") with st.expander("Learn More"): st.write("High TED Spread = liquidity stress (risk off).") # Composite Risk total_score = stretch_score + vix_score + pcr_score + skew_score + vvix_score + divergence_score + rotation_score + trend_score_penalty + fed_score st.subheader("Risk Profile") st.progress(total_score / 100) st.write(f"**Risk Score:** {total_score}/100") if total_score >= 50: st.error("πŸ”΄ RED: High risk. Trim longs aggressively. Buy put debit spreads / hedges.") elif total_score >= 25: st.warning("🟑 YELLOW: Caution. Size down, tighten stops, monitor.") else: st.success("🟒 GREEN: Normal/bullish. Premium selling favorable.") prior_score, prior_light = fetch_prior_day_scores() st.write(f"**Prior Day Light:** {prior_light} (based on prior score {prior_score}/100)") # ====================== REGIME CLASSIFIER + AI NARRATIVE ====================== st.subheader("Market Regime Classifier") vix_norm = min(max((vix_level - 10) / 30, 0), 1) if 'vix_level' in locals() else 0.5 stretch_norm = min(max(deviation / 3, 0), 1) if 'deviation' in locals() else 0.5 pcr_norm = min(max((pcr_equity - 0.6) / 0.8, 0), 1) if 'pcr_equity' in locals() else 0.5 skew_norm = min(max((skew - 120) / 30, 0), 1) if 'skew' in locals() else 0.5 vvix_norm = min(max((vvix_ratio - 4) / 4, 0), 1) if 'vvix_ratio' in locals() else 0.5 risk_score = (vix_norm * 0.35 + stretch_norm * 0.25 + pcr_norm * 0.15 + skew_norm * 0.15 + vvix_norm * 0.10) bull_prob = max(0, 1 - risk_score - 0.2) risk_off_prob = risk_score chop_prob = 1 - bull_prob - risk_off_prob probs = {'Bull': bull_prob, 'Chop': chop_prob, 'Risk-Off': risk_off_prob} fig = px.bar(x=list(probs.keys()), y=list(probs.values()), title="Market Regime Probabilities", color=list(probs.keys()), color_discrete_map={'Bull': '#00ff9d', 'Chop': '#ffaa00', 'Risk-Off': '#ff0000'}) st.plotly_chart(fig, width='stretch') dominant = max(probs, key=probs.get) st.write(f"**Current Regime:** {dominant} ({probs[dominant]*100:.0f}% probability)") # ====================== AI NARRATIVE SUMMARY ====================== st.subheader("AI Narrative Summary") narrative = f"The market is in a **{dominant.lower()}** regime with {probs[dominant]*100:.0f}% probability. " narrative += "Premium selling has a strong edge." if dominant == 'Bull' else "Defensive positioning is recommended." if dominant == 'Risk-Off' else "Range-bound conditions are likely β€” tighten stops." st.info(narrative) # Trigger checks (run every refresh) if enable_alerts: if 'dominant' in locals(): if dominant == 'Risk-Off' and probs['Risk-Off'] > 0.6: send_discord_alert(f"**REGIME SHIFT** β†’ Risk-Off regime at {probs['Risk-Off']*100:.0f}% probability. Hedge now.") elif dominant == 'Bull' and probs['Bull'] > 0.7: send_discord_alert(f"**BULL REGIME** β†’ Strong bull bias at {probs['Bull']*100:.0f}% β€” premium selling edge active.") if 'deviation' in locals() and deviation > 2.0: send_discord_alert(f"**SPY STRETCH ALERT** β†’ {deviation:.1f} ATRs overextended. Trim longs expected.") if 'vix_level' in locals() and vix_level > 25: send_discord_alert(f"**VIX SPIKE** β†’ {vix_level:.1f} (fear mode). Buy protection.") if 'gap' in locals() and abs(gap) > 8: send_discord_alert(f"**MAG7 CONCENTRATION** β†’ {gap:.1f}% gap vs RSP. Narrow leadership risk elevated.") if total_score >= 50: send_discord_alert(f"**HIGH RISK** β†’ Composite score {total_score}/100. Aggressive hedging recommended.") with col_right: # ==================== IMPROVED MACRO OVERLAY (easy to read) ==================== st.header("Macro Overlay") st.caption("Rising DXY or Oil = inflation/headwinds. Gold = safe-haven flow.") m1, m2 = st.columns(2) with m1: dxy_val, dxy_delta = fetch_dollar_index() if dxy_val is not None: st.metric("USD Index (DXY)", f"{dxy_val:.2f}", f"{dxy_delta:.2f}%") else: st.error("DXY unavailable") oil_val, oil_delta = fetch_oil_prices() if oil_val is not None: st.metric("WTI Oil Price", f"${oil_val:.2f}", f"{oil_delta:.2f}%") else: st.error("Oil unavailable") with m2: cpi_val, cpi_yoy = fetch_inflation_index() if cpi_val is not None: st.metric("CPI (Latest)", f"{cpi_val:.1f}", f"{cpi_yoy:.2f}% YoY" if cpi_yoy else "N/A") else: st.error("CPI unavailable") gold_val, silver_val = fetch_gold_silver() if gold_val is not None and silver_val is not None: st.metric("Gold / Silver", f"${gold_val:.0f} / ${silver_val:.2f}") else: st.error("Precious metals unavailable") # Global Markets Snapshot (full original) st.header("Global Markets Snapshot") global_data = fetch_global_markets() if global_data: df_global = pd.DataFrame.from_dict(global_data, orient='index', columns=['Latest Close']) st.dataframe(df_global.style.format("{:.2f}"), height=250, width='stretch') else: st.error("Global data fetch failed") with st.expander("Learn More"): st.write("Divergence from US = global weakness/strength.") # SPY Volume Gauge st.subheader("SPY Volume Gauge") st.write("**What it means**: Current vs. 20-day avg volume; high = conviction/panic, low = apathy. Actionable: Hedge if >150% on down day, avoid if <70%.") try: spy_vol = yf.download('SPY', period='2mo', progress=False) if spy_vol.empty: raise ValueError("No SPY volume data.") vol_20_avg = spy_vol['Volume'].rolling(20).mean().iloc[-1].item() current_vol = spy_vol['Volume'].iloc[-1].item() vol_ratio = (current_vol / vol_20_avg) * 100 if vol_20_avg != 0 else 0 st.write(f"**Current Volume:** {current_vol:,} shares") st.write(f"**20-day Avg:** {vol_20_avg:,.0f} shares") st.write(f"**Ratio:** {vol_ratio:.1f}%") if vol_ratio > 150: st.warning("High volume β†’ conviction or panic β†’ monitor price action.") elif vol_ratio < 70: st.info("Low volume β†’ apathy β†’ expect chop, avoid new positions.") else: st.success("Normal volume β†’ neutral.") except Exception as e: st.error(f"Error in SPY volume: {e}") with st.expander("Learn More"): st.write(">150% = conviction/panic (monitor). <70% = apathy (chop ahead).") # Citigroup Economic Surprise Index (ESI) (full original) st.subheader("Citigroup Economic Surprise Index (ESI)") st.write("**What it means**: Actual vs consensus economic data. Positive = beating expectations (bullish for stocks).") try: value = esi_value date_str = esi_date trend = "Beats expectations (bullish)" if value > 0 else "Misses (bearish)" if value < 0 else "Neutral" st.write(f"**Latest Value:** {value} (as of {date_str})") st.write(f"**Interpretation:** {trend}") if value > 10: st.info("Strong beats β†’ supportive for equities.") elif value < -10: st.warning("Big misses β†’ risk-off pressure.") else: st.success("Mild / neutral surprises.") except Exception as e: st.error(f"ESI error: {e}") with st.expander("Learn More"): st.write("Positive = data beats (bullish). Negative = misses (bearish).") # Short Interest Section (full original) st.subheader("Market Short Interest") st.write("**What it means**: Aggregate S&P 500 short levels; high = bearishness/big funds shorting (squeeze risk), low = bullish confidence. Actionable: Watch for squeezes if high shorts on up moves.") aggregate_short = 1.37 short_volume = 2.26 change = -16.26 high_short_stocks = ["MGM", "Molson Coors (TAP)", "APA Corp (APA)", "Intellia (NTLA)"] level = "Low" if aggregate_short < 2 else "High" if aggregate_short > 5 else "Moderate" st.write(f"**Aggregate S&P 500 Short:** {aggregate_short:.2f}% of float (${short_volume}B sold short, change {change:.2f}%)") st.write(f"**Level:** {level}") if aggregate_short > 5: st.warning("High shorts β†’ big funds bearish, potential squeeze on rallies.") elif aggregate_short < 2: st.success("Low shorts β†’ bullish confidence, less downside pressure.") else: st.info("Moderate shorts β†’ neutral; monitor high short stocks like {', '.join(high_short_stocks)} for plays.") st.caption("Note: Short interest data is bi-weekly from FINRA; no real-timeβ€”big funds not heavily shorting market at low 1.37%.") with st.expander("Learn More"): st.write("High shorts = squeeze risk on rallies. Low = bullish confidence.") # ====================== AUTO DISCORD ALERTS - FULL LOGIC (at very end) ====================== def send_discord_alert(message): if enable_alerts and discord_webhook: try: requests.post(discord_webhook, json={"content": f"🚨 **MarketCraft Risk Alert** 🚨\n{message}"}) except: pass # Trigger checks (safe - only if variables exist) if enable_alerts: if 'dominant' in locals(): if dominant == 'Risk-Off' and probs.get('Risk-Off', 0) > 0.6: send_discord_alert(f"**REGIME SHIFT** β†’ Risk-Off at {probs['Risk-Off']*100:.0f}% probability. Hedge now.") elif dominant == 'Bull' and probs.get('Bull', 0) > 0.7: send_discord_alert(f"**BULL REGIME** β†’ Strong bull bias at {probs['Bull']*100:.0f}% β€” premium selling favorable.") if 'deviation' in locals() and deviation > 2.0: send_discord_alert(f"**SPY STRETCH ALERT** β†’ {deviation:.1f} ATRs overextended. Trim longs expected.") if 'vix_level' in locals() and vix_level > 25: send_discord_alert(f"**VIX SPIKE** β†’ {vix_level:.1f}. Fear mode active. Buy protection.") if total_score >= 50: send_discord_alert(f"**HIGH RISK** β†’ Composite score {total_score}/100. Aggressive hedging recommended.") # Footer st.markdown("---") st.caption("Data via yfinance & FRED. Refresh for updates. Backtest accuracy ~67% on major moves. Built for your paid members.")