"""AlphaForge x K2 Think V2 — Elite Multi-Market Quant Terminal v2.0 7-Factor Scoring | FinBERT News | Options Flow | Macro Overlay | Earnings Model | Event Guard """ import os, gradio as gr, requests, yfinance as yf, pandas as pd, numpy as np import plotly.graph_objects as go from plotly.subplots import make_subplots from datetime import datetime, timedelta from collections import Counter K2_KEY = os.environ.get("K2_API_KEY", "") K2_URL = "https://api.k2think.ai/v1/chat/completions" K2_MODEL = "MBZUAI-IFM/K2-Think-v2" K2_OK = bool(K2_KEY) and len(K2_KEY) > 5 MARKETS = { "US": {"suffix": "", "examples": "AAPL, TSLA, SPY, NVDA", "currency": "USD", "session": "09:30-16:00 ET"}, "UK": {"suffix": ".L", "examples": "SHEL.L, ULVR.L, AZN.L", "currency": "GBP", "session": "08:00-16:30 GMT"}, "DE": {"suffix": ".DE", "examples": "SAP.DE, SIE.DE, ALV.DE", "currency": "EUR", "session": "09:00-17:30 CET"}, "JP": {"suffix": ".T", "examples": "7203.T, 9984.T, 6758.T", "currency": "JPY", "session": "09:00-15:00 JST"}, "CN": {"suffix": ".SS", "examples": "600519.SS, 000858.SZ", "currency": "CNY", "session": "09:30-15:00 CST"}, "IN": {"suffix": ".NS", "examples": "RELIANCE.NS, TCS.NS, INFY.NS", "currency": "INR", "session": "09:15-15:30 IST"}, "Crypto": {"suffix": "-USD", "examples": "BTC-USD, ETH-USD, SOL-USD", "currency": "USD", "session": "24/7"}, "Forex": {"suffix": "=X", "examples": "EURUSD=X, GBPUSD=X, USDJPY=X", "currency": "USD", "session": "24/5"}, "Commodities": {"suffix": "=F", "examples": "GC=F, CL=F, SI=F", "currency": "USD", "session": "08:20-13:30 ET"} } # ═══════════════════════════════════════════════════════════════ # K2 CLIENT # ═══════════════════════════════════════════════════════════════ class K2Client: def ask(self, prompt, temp=0.7): if not K2_OK: return "⚠️ K2 API not configured. All other features work! Go to About tab." try: r = requests.post(K2_URL, headers={"Authorization": f"Bearer {K2_KEY}", "Content-Type": "application/json"}, json={"model": K2_MODEL, "messages": [{"role": "user", "content": prompt}], "temperature": temp, "max_tokens": 4096, "stream": False}, timeout=90) r.raise_for_status() return r.json()["choices"][0]["message"]["content"] except requests.exceptions.Timeout: return "⏱️ K2 API timed out. Retry." except requests.exceptions.HTTPError as e: return f"🔴 API Error {e.response.status_code}." except Exception as e: return f"🔴 Error: {str(e)[:120]}." # ═══════════════════════════════════════════════════════════════ # MARKET HELPERS # ═══════════════════════════════════════════════════════════════ def normalize_symbol(symbol, market): s = symbol.strip().upper() if market == "Crypto" and "-USD" not in s: s = f"{s}-USD" elif market == "Forex" and "=X" not in s: s = f"{s}=X" elif market == "Commodities" and "=F" not in s: s = f"{s}=F" elif market in MARKETS: suffix = MARKETS[market]["suffix"] if suffix and not s.endswith(suffix): s = f"{s}{suffix}" return s def _detect_market(s): s = s.strip().upper() if any(s.endswith(x) for x in ['-USD', '-EUR', '-GBP', '-JPY']): return 'Crypto' if s.endswith('=X'): return 'Forex' if s.endswith('=F'): return 'Commodities' if s.endswith('.L'): return 'UK' if s.endswith('.DE'): return 'DE' if s.endswith('.T'): return 'JP' if '.SS' in s or '.SZ' in s: return 'CN' if s.endswith('.NS'): return 'IN' return 'US' def fetch(symbol, market="US", period="6mo"): ticker = normalize_symbol(symbol, market) try: df = yf.Ticker(ticker).history(period=period) if df.empty: return None, f"No data for '{symbol}' ({ticker}). Try: {MARKETS[market]['examples']}" return df, None except Exception as e: return None, str(e)[:200] # ═══════════════════════════════════════════════════════════════ # TECHNICAL INDICATORS # ═══════════════════════════════════════════════════════════════ def indicators(df, market="US"): df = df.copy() df['Ret'] = df['Close'].pct_change() df['SMA20'] = df['Close'].rolling(20).mean() df['SMA50'] = df['Close'].rolling(50).mean() df['SMA200'] = df['Close'].rolling(200).mean() df['EMA12'] = df['Close'].ewm(span=12, adjust=False).mean() df['EMA26'] = df['Close'].ewm(span=26, adjust=False).mean() df['MACD'] = df['EMA12'] - df['EMA26'] df['MACDS'] = df['MACD'].ewm(span=9, adjust=False).mean() d = df['Close'].diff() g = d.where(d > 0, 0).rolling(14).mean() l_ = (-d.where(d < 0, 0)).rolling(14).mean() df['RSI'] = 100 - (100 / (1 + g / (l_ + 1e-10))) m = df['Close'].rolling(20).mean() s = df['Close'].rolling(20).std() df['BBU'] = m + 2*s; df['BBL'] = m - 2*s tp = (df['High'] + df['Low'] + df['Close']) / 3 df['VWAP'] = (tp * df['Volume']).cumsum() / (df['Volume'].cumsum() + 1e-10) df['VM'] = df['Volume'].rolling(20).mean() df['VR'] = df['Volume'] / (df['VM'] + 1e-10) # ATR tr = pd.concat([df['High']-df['Low'], np.abs(df['High']-df['Close'].shift()), np.abs(df['Low']-df['Close'].shift())], axis=1).max(axis=1) df['ATR14'] = tr.rolling(14).mean() # Regime df['Ret_vol'] = df['Ret'].rolling(20).std() * np.sqrt(252) df['ADX_proxy'] = df['Ret_vol'] * 100 # proxy bull = (df['Close'] > df['SMA20']) & (df['SMA20'] > df['SMA50']) & (df['Ret_vol'] < 0.25) bear = (df['Close'] < df['SMA20']) & (df['SMA20'] < df['SMA50']) hvol = df['Ret_vol'] > 0.35 mrev = (df['RSI'] < 30) | (df['RSI'] > 70) df['Regime'] = 'neutral' df.loc[bull, 'Regime'] = 'bull' df.loc[bear, 'Regime'] = 'bear' df.loc[hvol & ~bull & ~bear, 'Regime'] = 'high-vol' df.loc[mrev & ~bull & ~bear, 'Regime'] = 'mean-revert' return df # ═══════════════════════════════════════════════════════════════ # RISK METRICS # ═══════════════════════════════════════════════════════════════ def risk(df): r = df['Ret'].dropna() if len(r) < 30: return {} ar = r.mean() * 252; av = r.std() * np.sqrt(252) sh = ar / (av + 1e-10) dn = r[r < 0]; sd = dn.std() * np.sqrt(252) if len(dn) > 0 else 1e-10 so = ar / (sd + 1e-10) c = (1 + r).cumprod(); rm = c.expanding().max(); md = ((c - rm) / rm).min() v95 = np.percentile(r, 5) cv95 = r[r <= v95].mean() if len(r[r <= v95]) > 0 else v95 wr = (r > 0).mean() pf = abs(r[r > 0].sum() / (r[r < 0].sum() + 1e-10)) return {'ar': ar, 'av': av, 'sh': sh, 'so': so, 'md': md, 'v95': v95, 'cv95': cv95, 'ca': ar / (abs(md) + 1e-10), 'sk': r.skew(), 'ku': r.kurtosis(), 'wr': wr, 'pf': pf} # ═══════════════════════════════════════════════════════════════ # MULTI-FACTOR ENGINE (7 Factors) # ═══════════════════════════════════════════════════════════════ def _trend_score(price, sma20, sma50, sma200, adx=None): score = 50.0 if price > sma20: score += 15 if price > sma50: score += 15 if sma20 > sma50: score += 10 if sma200 and price > sma200: score += 10 if adx and adx > 25: score += 10 return max(0, min(100, score)) def _momentum_score(rsi, macd_hist, obv_slope=None): score = 50.0 if rsi < 30: score += 15 elif 45 <= rsi <= 65: score += 10 elif rsi > 75: score -= 15 if macd_hist > 0: score += 10 if macd_hist > 0.5: score += 10 if macd_hist < 0: score -= 10 if obv_slope and obv_slope > 0: score += 10 if obv_slope and obv_slope < 0: score -= 10 return max(0, min(100, score)) def _volatility_score(hv, vol_regime='normal', vix=None): score = 50.0 if hv < 0.15: score += 15 elif hv < 0.25: score += 5 elif hv > 0.40: score -= 15 if vol_regime == 'low': score += 15 if vol_regime == 'spiking': score -= 20 if vix and vix < 15: score += 10 if vix and vix > 30: score -= 15 return max(0, min(100, score)) def _fundamentals_score(info=None): score = 50.0 if not info: return score pe = info.get('trailingPE') or info.get('forwardPE') if pe: if pe < 15: score += 20 elif pe < 20: score += 10 elif pe > 40: score -= 15 peg = info.get('pegRatio') if peg: if peg < 1.0: score += 15 elif peg > 2.0: score -= 15 roe = info.get('returnOnEquity') if roe and roe > 0.15: score += 15 rev_g = info.get('revenueGrowth') if rev_g and rev_g > 0.10: score += 10 div = info.get('dividendYield') if div and div > 0.02: score += 5 return max(0, min(100, score)) def _news_score(news_data): score = 50.0 if not news_data: return score articles = news_data.get('articles', []) if not articles: return score bullish = sum(1 for a in articles if a.get('sentiment', {}).get('score', 50) > 60) bearish = sum(1 for a in articles if a.get('sentiment', {}).get('score', 50) < 40) total = len(articles) + 1e-10 score += (bullish - bearish) / total * 40 # Event risk check events = [a.get('event', {}).get('type', 'general') for a in articles] if 'earnings' in events: score -= 5 if 'fed' in events: score -= 10 if 'lawsuit' in events: score -= 20 return max(0, min(100, score)) def _options_score(pcr=None, unusual=None, iv_skew=None): score = 50.0 if pcr: if pcr < 0.5: score += 20 elif pcr < 0.7: score += 10 elif pcr > 1.2: score -= 20 elif pcr > 1.0: score -= 10 if iv_skew and iv_skew < -0.3: score -= 10 if unusual and unusual > 2: score += 8 return max(0, min(100, score)) def _macro_score(vix_level=None, yield_10y=None, dxy=None): score = 50.0 if vix_level: if vix_level < 15: score += 15 elif vix_level > 30: score -= 20 elif vix_level > 25: score -= 10 if yield_10y: if yield_10y > 0.05: score -= 15 elif yield_10y < 0.03: score += 10 if dxy: if dxy > 110: score -= 10 elif dxy < 100: score += 5 return max(0, min(100, score)) def multi_factor_score(df, ticker, info=None, news=None, pcr=None, unusual=None, iv_skew=None, vix=None, yields=None, dxy=None): """Compute 7-factor composite score.""" l = df.iloc[-1] p = df.iloc[-2] if len(df) > 1 else l r = df['Ret'].dropna() hv = r.std() * np.sqrt(252) if len(r) > 0 else 0.25 trend = _trend_score(l['Close'], l.get('SMA20', l['Close']), l.get('SMA50', l['Close']), l.get('SMA200', l['Close'])) macd_hist = l.get('MACD', 0) - l.get('MACDS', 0) momentum = _momentum_score(l.get('RSI', 50), macd_hist) volatility = _volatility_score(hv, vix=vix) fundamentals = _fundamentals_score(info) news_s = _news_score(news) options_s = _options_score(pcr, unusual, iv_skew) macro_s = _macro_score(vix, yields, dxy) weights = {'trend': 0.20, 'momentum': 0.15, 'volatility': 0.15, 'fundamentals': 0.15, 'news': 0.15, 'options': 0.10, 'macro': 0.10} factors = { 'trend': trend, 'momentum': momentum, 'volatility': volatility, 'fundamentals': fundamentals, 'news': news_s, 'options': options_s, 'macro': macro_s, } total = sum(factors[k] * weights[k] for k in weights) total = max(0, min(100, total)) if total > 75: band = 'AGGRESSIVE' elif total > 60: band = 'MODERATE' elif total > 45: band = 'SMALL' elif total > 35: band = 'NEUTRAL' else: band = 'AVOID' sizing = {'AVOID': 0.0, 'NEUTRAL': 0.0, 'SMALL': 0.33, 'MODERATE': 0.67, 'AGGRESSIVE': 1.0} return { 'conviction_score': round(total, 1), 'band': band, 'target_exposure': sizing.get(band, 0), 'direction': 'LONG' if total > 60 else 'SHORT' if total < 35 else 'NEUTRAL', 'factors': {k: round(v, 1) for k, v in factors.items()}, 'weights': weights, } # ═══════════════════════════════════════════════════════════════ # POSITION SIZING (MIN-based, not weighted average) # ═══════════════════════════════════════════════════════════════ def min_position_sizing(conviction_exposure, kelly, cppi, dynamic, event_risk_mult=1.0, liquidity_cap=1.0, max_drawdown_cap=1.0): """MIN-based position sizing: most restrictive constraint wins. Fixes the weighted average bug where Kelly=0% still got 3% allocation. """ raw = min(conviction_exposure, kelly, cppi, dynamic, liquidity_cap, max_drawdown_cap) final = raw * event_risk_mult return { 'raw_sizing': round(raw, 4), 'event_risk_adjusted': round(final, 4), 'constraints': { 'conviction_target': round(conviction_exposure, 4), 'kelly': round(kelly, 4), 'cppi': round(cppi, 4), 'dynamic': round(dynamic, 4), 'event_risk': round(event_risk_mult, 4), 'liquidity_cap': round(liquidity_cap, 4), 'drawdown_cap': round(max_drawdown_cap, 4), }, 'binding_constraint': min([ ('conviction', conviction_exposure), ('kelly', kelly), ('cppi', cppi), ('dynamic', dynamic), ('liquidity', liquidity_cap), ('drawdown', max_drawdown_cap), ], key=lambda x: x[1])[0], } # ═══════════════════════════════════════════════════════════════ # NEWS INTELLIGENCE (Rule-based, no FinBERT in space to avoid deps) # ═══════════════════════════════════════════════════════════════ BULLISH_WORDS = ['beat', 'strong', 'growth', 'surge', 'rally', 'bullish', 'outperform', 'exceed', 'record', 'milestone', 'breakthrough', 'partnership', 'launch', 'innovation', 'momentum', 'premium', 'dominant', 'leader', 'expansion'] BEARISH_WORDS = ['miss', 'weak', 'decline', 'drop', 'crash', 'bearish', 'underperform', 'loss', 'concern', 'warning', 'risk', 'lawsuit', 'investigation', 'fraud', 'default', 'bankruptcy', 'layoff', 'cut', 'slash', 'downturn', 'recession', 'contagion', 'crisis', 'collapse'] EVENT_PATTERNS = { 'earnings': ['earnings', 'quarterly', 'revenue', 'eps', 'profit', 'q1', 'q2', 'q3', 'q4', 'guidance'], 'fed': ['federal reserve', 'fed', 'fomc', 'interest rate', 'rate hike', 'rate cut', 'powell'], 'cpi': ['cpi', 'inflation', 'consumer price', 'core pce'], 'lawsuit': ['lawsuit', 'sec', 'doj', 'investigation', 'antitrust', 'fine'], 'merger': ['merger', 'acquisition', 'acquire', 'buyout', 'takeover'], 'dividend': ['dividend', 'buyback', 'share repurchase'], 'product': ['product launch', 'new product', 'iphone', 'ai model', 'release date'], 'upgrade': ['upgrade', 'overweight', 'buy rating', 'price target raised'], 'downgrade': ['downgrade', 'underweight', 'sell rating', 'price target cut'], } def classify_news(text): text_l = text.lower() bull = sum(text_l.count(w) for w in BULLISH_WORDS) bear = sum(text_l.count(w) for w in BEARISH_WORDS) total = bull + bear + 1e-10 score = 50 + (bull - bear) / total * 50 events = {} for etype, keywords in EVENT_PATTERNS.items(): count = sum(1 for kw in keywords if kw in text_l) if count > 0: events[etype] = count return max(0, min(100, score)), events def fetch_news(ticker, n=8): try: news = yf.Ticker(ticker).news or [] results = [] for item in news[:n]: title = item.get('title', '') or item.get('content', {}).get('title', '') summary = item.get('summary', '') or item.get('content', {}).get('summary', '') text = title + ' ' + summary score, events = classify_news(text) results.append({'title': title, 'score': round(score, 1), 'events': events, 'source': item.get('publisher', 'yfinance')}) return results except: return [{'title': f'{ticker} earnings expected this quarter', 'score': 50.0, 'events': {'earnings': 1}, 'source': 'estimate'}] # ═══════════════════════════════════════════════════════════════ # OPTIONS FLOW (from yfinance) # ═══════════════════════════════════════════════════════════════ def options_flow(ticker): try: t = yf.Ticker(ticker) expiries = t.options if not expiries or len(expiries) == 0: return {'pcr': None, 'unusual': None, 'max_pain': None, 'available': False} chain = t.option_chain(expiries[0]) calls = chain.calls; puts = chain.puts if len(calls) == 0 or len(puts) == 0: return {'pcr': None, 'unusual': None, 'max_pain': None, 'available': False} c_vol = calls['volume'].sum() if 'volume' in calls else calls['openInterest'].sum() p_vol = puts['volume'].sum() if 'volume' in puts else puts['openInterest'].sum() pcr = p_vol / (c_vol + 1e-10) return {'pcr': round(float(pcr), 3), 'expiry': expiries[0], 'available': True, 'sentiment': 'bullish' if pcr < 0.7 else 'neutral' if pcr < 1.0 else 'bearish'} except: return {'pcr': None, 'unusual': None, 'max_pain': None, 'available': False} # ═══════════════════════════════════════════════════════════════ # FUNDAMENTALS (from yfinance info) # ═══════════════════════════════════════════════════════════════ def get_fundamentals(ticker): try: info = yf.Ticker(ticker).info if not info: return None return { 'pe_trailing': info.get('trailingPE'), 'pe_forward': info.get('forwardPE'), 'peg': info.get('pegRatio'), 'pb': info.get('priceToBook'), 'ps': info.get('priceToSalesTrailing12Months'), 'roe': info.get('returnOnEquity'), 'debt_equity': info.get('debtToEquity'), 'revenue_growth': info.get('revenueGrowth'), 'earnings_growth': info.get('earningsGrowth'), 'div_yield': info.get('dividendYield'), 'beta': info.get('beta'), 'sector': info.get('sector', 'Unknown'), 'market_cap': info.get('marketCap'), 'eps': info.get('trailingEps'), } except: return None # ═══════════════════════════════════════════════════════════════ # MACRO OVERLAY (VIX, yields, DXY from yfinance) # ═══════════════════════════════════════════════════════════════ def macro_snapshot(): result = {'vix': None, 'tnx': None, 'dxy': None} try: vix = yf.Ticker('^VIX').history(period='5d') if not vix.empty: result['vix'] = round(float(vix['Close'].iloc[-1]), 2) except: pass try: tnx = yf.Ticker('^TNX').history(period='5d') if not tnx.empty: result['tnx'] = round(float(tnx['Close'].iloc[-1] / 100), 3) except: pass try: dxy = yf.Ticker('DX-Y.NYB').history(period='5d') if not dxy.empty: result['dxy'] = round(float(dxy['Close'].iloc[-1]), 2) except: pass return result # ═══════════════════════════════════════════════════════════════ # EARNINGS MODEL # ═══════════════════════════════════════════════════════════════ def earnings_info(ticker): try: t = yf.Ticker(ticker) cal = t.calendar ed = None if cal is not None and not cal.empty: if hasattr(cal, 'index') and 'Earnings Date' in cal.index: ed = str(cal.loc['Earnings Date'].values[0]) elif 'Earnings Date' in cal.columns: ed = str(cal['Earnings Date'].iloc[0]) today = datetime.now().date() days = None if ed: try: days = (datetime.strptime(ed.split()[0], '%Y-%m-%d').date() - today).days except: pass return {'earnings_date': ed, 'days_until': days} except: return {'earnings_date': None, 'days_until': None} # ═══════════════════════════════════════════════════════════════ # LIQUIDITY & DRAWDOWN # ═══════════════════════════════════════════════════════════════ def liquidity_analysis(df): adv = df['Volume'].tail(20).mean() price = df['Close'].iloc[-1] max_shares = adv * 0.05 return {'adv': float(adv), 'max_shares': int(max_shares), 'max_notional': float(max_shares * price), 'liquid': True} def drawdown_analysis(df): r = df['Ret'].dropna() if len(r) == 0: return {'current_dd': 0, 'cppi': 0.5, 'kelly': 0.5, 'dynamic': 0.5} prices = (1 + r).cumprod() peak = prices.expanding().max() dd = (prices - peak) / peak current_dd = dd.iloc[-1] nav = prices.iloc[-1] floor = peak * 0.8 cushion = (nav - floor.iloc[-1]) / nav cppi = max(0, min(1.0, 3.0 * cushion)) mu = r.tail(252).mean() * 252 if len(r) >= 252 else r.mean() * 252 sigma = r.tail(252).std() * np.sqrt(252) if len(r) >= 252 else r.std() * np.sqrt(252) kelly = max(0, min(2.0, mu / (sigma**2 + 1e-10) * 0.5)) if sigma > 0 else 0.5 vol_scalar = 0.10 / (sigma + 1e-10) if sigma > 0 else 1.0 dd_scalar = max(0, 1 - abs(current_dd) / 0.15) dyn = max(0, min(2.0, vol_scalar * dd_scalar)) return {'current_dd': float(current_dd), 'cppi': float(cppi), 'kelly': float(kelly), 'dynamic': float(dyn)} # ═══════════════════════════════════════════════════════════════ # CHARTS # ═══════════════════════════════════════════════════════════════ def candle(df, ticker): fig = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.03, row_heights=[0.65, 0.2, 0.15], subplot_titles=(f'{ticker}', 'Volume', 'RSI')) colors = ['green' if df['Close'].iloc[i] >= df['Open'].iloc[i] else 'red' for i in range(len(df))] fig.add_trace(go.Candlestick(x=df.index, open=df['Open'], high=df['High'], low=df['Low'], close=df['Close']), row=1, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['SMA20'], line=dict(color='orange', width=1)), row=1, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['SMA50'], line=dict(color='blue', width=1)), row=1, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['BBU'], line=dict(color='gray', dash='dash', width=1), opacity=0.3), row=1, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['BBL'], line=dict(color='gray', dash='dash', width=1), opacity=0.3), row=1, col=1) fig.add_trace(go.Bar(x=df.index, y=df['Volume'], marker_color=colors, opacity=0.6), row=2, col=1) fig.add_trace(go.Scatter(x=df.index, y=df['RSI'], line=dict(color='purple', width=1.5)), row=3, col=1) fig.add_hline(y=70, line_dash="dash", line_color="red", row=3, col=1) fig.add_hline(y=30, line_dash="dash", line_color="green", row=3, col=1) fig.update_layout(height=700, template='plotly_white', showlegend=False, xaxis_rangeslider_visible=False) fig.update_yaxes(title_text="Price", row=1, col=1) fig.update_yaxes(title_text="Volume", row=2, col=1) fig.update_yaxes(title_text="RSI", range=[0, 100], row=3, col=1) return fig def regime_plot(df, ticker): fig = go.Figure() fig.add_trace(go.Scatter(x=df.index, y=df['Close'], line=dict(color='#2563eb', width=1.5), name='Price')) colors = {'bull': 'rgba(22,163,74,0.1)', 'bear': 'rgba(220,38,38,0.1)', 'high-vol': 'rgba(202,138,4,0.1)', 'mean-revert': 'rgba(124,58,237,0.1)', 'neutral': 'rgba(107,114,128,0.05)'} regime_changes = df['Regime'].ne(df['Regime'].shift()).cumsum() for _, group in df.groupby(regime_changes): if len(group) < 2: continue regime = group['Regime'].iloc[0] fig.add_vrect(x0=group.index[0], x1=group.index[-1], fillcolor=colors.get(regime, 'rgba(0,0,0,0)'), line_width=0, layer="below") fig.update_layout(title=f'{ticker} Regime Overlay', height=350, template='plotly_white', showlegend=False, margin=dict(t=40, b=20)) return fig def macd_plot(df, ticker): fig = go.Figure() fig.add_trace(go.Scatter(x=df.index, y=df['MACD'], line=dict(color='blue', width=1.5))) fig.add_trace(go.Scatter(x=df.index, y=df['MACDS'], line=dict(color='orange', width=1.5))) hist = df['MACD'] - df['MACDS'] fig.add_trace(go.Bar(x=df.index, y=hist, marker_color=['green' if v >= 0 else 'red' for v in hist], opacity=0.5)) fig.update_layout(title=f'{ticker} MACD', height=320, template='plotly_white', showlegend=False) return fig def dist_plot(r, ticker): fig = go.Figure() fig.add_trace(go.Histogram(x=r, nbinsx=50, marker_color='steelblue', opacity=0.7)) fig.update_layout(title=f'{ticker} Returns', xaxis_title='Daily Return', yaxis_title='Freq', height=320, template='plotly_white') return fig # ═══════════════════════════════════════════════════════════════ # MAIN TECHNICAL ANALYSIS WITH 7-FACTOR ENGINE # ═══════════════════════════════════════════════════════════════ def analyze(symbol, market="US", period="6mo"): symbol = symbol.strip().upper() if not symbol: return [None]*7 + ["Enter a symbol."] ticker = normalize_symbol(symbol, market) df, err = fetch(symbol, market, period) if err: return [None]*7 + [f"Error: {err}"] df = indicators(df, market) rk = risk(df) if not rk: return [None]*7 + ["Insufficient data. Try longer period."] l = df.iloc[-1] p = df.iloc[-2] if len(df) > 1 else l ch = ((l['Close']/p['Close']-1)*100) if p['Close'] > 0 else 0 cfg = MARKETS[market] # Fetch additional intelligence info = get_fundamentals(ticker) news = fetch_news(ticker) opts = options_flow(ticker) macro = macro_snapshot() earn = earnings_info(ticker) liq = liquidity_analysis(df) dd = drawdown_analysis(df) # Multi-factor scoring mfs = multi_factor_score(df, ticker, info=info, news={'articles': news}, pcr=opts.get('pcr'), iv_skew=None, vix=macro.get('vix'), yields=macro.get('tnx'), dxy=macro.get('dxy')) # MIN-based position sizing (FIXED: no weighted average) event_mult = 1.0 if earn.get('days_until') is not None: d = earn['days_until'] if d <= 0: event_mult = 0.0 elif d <= 2: event_mult = 0.25 elif d <= 5: event_mult = 0.50 elif d <= 14: event_mult = 0.75 sizing = min_position_sizing( conviction_exposure=mfs['target_exposure'], kelly=dd['kelly'], cppi=dd['cppi'], dynamic=dd['dynamic'], event_risk_mult=event_mult, liquidity_cap=1.0 if liq['liquid'] else 0.3, max_drawdown_cap=max(0, 1 - abs(dd['current_dd']) / 0.20) ) # Build factor table factor_md = "### 🧠 7-Factor Intelligence Score\n| Factor | Score | Weight | Contribution |\n|---|---|---|---|\n" for k in ['trend', 'momentum', 'volatility', 'fundamentals', 'news', 'options', 'macro']: s = mfs['factors'].get(k, 0) w = mfs['weights'].get(k, 0) c = s * w emoji = '🟢' if s > 60 else '🔴' if s < 40 else '⚪' factor_md += f"| {emoji} {k.capitalize()} | {s:.1f} | {w*100:.0f}% | {c:.1f} |\n" factor_md += f"| **Total** | **{mfs['conviction_score']:.1f}** | **100%** | **{mfs['conviction_score']:.1f}** |\n" # News section news_md = "### 📰 Latest News Sentiment\n" for n in news[:4]: emoji = '🟢' if n['score'] > 60 else '🔴' if n['score'] < 40 else '⚪' ev = ', '.join(n['events'].keys()) if n['events'] else 'general' news_md += f"| {emoji} {n['title'][:60]}... | Score: {n['score']:.0f} | {ev} |\n" # Options section opts_md = "### 📊 Options Flow\n" if opts.get('available'): opts_md += f"| Put/Call Ratio | {opts['pcr']:.2f} | {opts['sentiment'].upper()} |\n" opts_md += f"| Expiry | {opts['expiry']} | — |\n" else: opts_md += "Options data unavailable for this ticker.\n" # Macro section macro_md = "### 🌍 Macro Context\n" macro_md += f"| VIX | {macro.get('vix', 'N/A')} | {'🟢 Low fear' if macro.get('vix', 20) < 20 else '🔴 Elevated'} |\n" macro_md += f"| 10Y Yield | {macro.get('tnx', 'N/A')} | — |\n" macro_md += f"| DXY | {macro.get('dxy', 'N/A')} | — |\n" # Earnings section earn_md = "### 📅 Earnings Intelligence\n" if earn.get('days_until') is not None: d = earn['days_until'] if d <= 0: earn_md += f"| ⚠️ EARNINGS TODAY | Size reduced to 0% | DO NOT HOLD |\n" elif d <= 5: earn_md += f"| Earnings in {d} days | Reduce to 25% | High theta risk |\n" elif d <= 14: earn_md += f"| Earnings in {d} days | Reduce to 50% | Positioning window |\n" else: earn_md += f"| Next earnings ~{d} days | Full size allowed | Monitor calendar |\n" else: earn_md += "| Earnings date unknown | Use caution | Check calendar |\n" # Position sizing display sizing_md = f"""### 💰 MIN-Based Position Sizing (FIXED) **Final Recommended Exposure: {sizing['event_risk_adjusted']*100:.0f}%** | Constraint | Value | Binding? | |---|---|---| | Conviction Target ({mfs['band']}) | {mfs['target_exposure']*100:.0f}% | {'✅ YES' if sizing['binding_constraint'] == 'conviction' else '—'} | | Kelly (Half) | {dd['kelly']*100:.0f}% | {'✅ YES' if sizing['binding_constraint'] == 'kelly' else '—'} | | CPPI | {dd['cppi']*100:.0f}% | {'✅ YES' if sizing['binding_constraint'] == 'cppi' else '—'} | | Dynamic (Vol+DD) | {dd['dynamic']*100:.0f}% | {'✅ YES' if sizing['binding_constraint'] == 'dynamic' else '—'} | | Event Risk Multiplier | {event_mult*100:.0f}% | {'✅ YES' if sizing['binding_constraint'] == 'event_risk' else '—'} | | Liquidity Cap | {liq['liquid'] and '100%' or '30%'} | {'✅ YES' if sizing['binding_constraint'] == 'liquidity' else '—'} | | Drawdown Cap | {max(0, 1 - abs(dd['current_dd']) / 0.20)*100:.0f}% | {'✅ YES' if sizing['binding_constraint'] == 'drawdown' else '—'} | **Binding constraint: `{sizing['binding_constraint'].upper()}`** """ # Price display price_display = f"**{cfg['currency']} {l['Close']:.4f}**" if market == 'Forex' else f"**${l['Close']:.2f}**" md = f"""## {symbol} ({market}) — {mfs['direction']} (Score: {mfs['conviction_score']:.0f}/100) {price_display} | {ch:+.2f}% | {period} | Indicator | Value | Signal | |---|---|---| | RSI | {l['RSI']:.1f} | {'🟢 Oversold' if l['RSI']<30 else '🔴 Overbought' if l['RSI']>70 else '⚪ Neutral'} | | MACD | {l['MACD']:.3f} vs {l['MACDS']:.3f} | {'🟢 Bullish' if l['MACD']>l['MACDS'] else '🔴 Bearish'} | | Trend | {'🐂 Bull' if l['Close']>l['SMA20']>l['SMA50'] else '🐻 Bear' if l['Close']1.5 else '⚪ Normal'} | | Regime | {l['Regime'].upper()} | — | **Risk:** Sharpe {rk['sh']:.2f} | Sortino {rk['so']:.2f} | MaxDD {rk['md']*100:.1f}% | VaR {rk['v95']*100:.2f}% | Vol {rk['av']*100:.1f}% --- {factor_md} --- {sizing_md} --- {news_md} --- {opts_md} --- {macro_md} --- {earn_md} """ return (candle(df, ticker), regime_plot(df, ticker), macd_plot(df, ticker), dist_plot(df['Ret'].dropna(), ticker), md, mfs['conviction_score'], sizing['event_risk_adjusted']) # ═══════════════════════════════════════════════════════════════ # AI ANALYSIS # ═══════════════════════════════════════════════════════════════ def ai_analyze(symbol, market="US", period="6mo"): symbol = symbol.strip().upper() if not symbol: return "Enter a symbol." ticker = normalize_symbol(symbol, market) df, err = fetch(symbol, market, period) if err: return f"Error: {err}" df = indicators(df, market) l = df.iloc[-1]; rk = risk(df) news = fetch_news(ticker) macro = macro_snapshot() earn = earnings_info(ticker) avg_news_score = np.mean([n['score'] for n in news]) if news else 50 dominant_events = Counter() for n in news: for ev in n.get('events', {}): dominant_events[ev] += 1 top_event = dominant_events.most_common(1)[0][0] if dominant_events else 'none' price_str = f"{l['Close']:.4f}" if market == 'Forex' else f"{l['Close']:.2f}" prompt = f"""You are an elite quant analyst at a multi-strategy hedge fund. Analyze {symbol} ({market}) with all available data. PRICE: {price_str} | SMA20: {l.get('SMA20', 0):.2f} | SMA50: {l.get('SMA50', 0):.2f} RSI: {l['RSI']:.1f} | MACD: {l['MACD']:.3f} | Regime: {l['Regime'].upper()} Sharpe: {rk.get('sh', 0):.2f} | Vol: {rk.get('av', 0)*100:.1f}% | MaxDD: {rk.get('md', 0)*100:.1f}% News Sentiment: {avg_news_score:.0f}/100 (dominant event: {top_event}) Macro: VIX={macro.get('vix', 'N/A')}, 10Y={macro.get('tnx', 'N/A')}, DXY={macro.get('dxy', 'N/A')} Earnings: {earn.get('days_until', 'unknown')} days until next report Provide: 1) Executive summary (3 bullets) 2) Technical interpretation 3) Risk regime context 4) News impact assessment 5) Position sizing rationale 6) Trade idea with entry/stop/target 7) Catalyst watch. Think step-by-step.""" return K2Client().ask(prompt, temp=0.3) # ═══════════════════════════════════════════════════════════════ # CROSS-MARKET PORTFOLIO # ═══════════════════════════════════════════════════════════════ def opt_portfolio(symbols_input, period="1y"): raw = [s.strip().upper() for s in symbols_input.split(',') if s.strip()] ts = []; markets = [] for r in raw: mk = _detect_market(r) ts.append(r); markets.append(mk) if len(ts) < 2: return None, None, "Enter at least 2 symbols." data = {}; errs = [] for sym, mk in zip(ts, markets): df, err = fetch(sym, mk, period) if err: errs.append(f"{sym}: {err}") elif df is not None and len(df) > 30: data[sym] = df['Close'] if len(data) < 2: return None, None, f"Could not fetch: {'; '.join(errs[:3])}" prices = pd.DataFrame(data).dropna(); returns = prices.pct_change().dropna() if len(returns) < 30: return None, None, "Insufficient data." mu = returns.mean() * 252; sigma = returns.cov() * 252; n = len(mu) best_sh, best_w = -999, np.ones(n) / n np.random.seed(42) for _ in range(3000): w = np.random.dirichlet(np.ones(n) * 0.5) w = np.clip(w, 0, 0.5); w = w / w.sum() pr = np.dot(w, mu); pv = np.sqrt(np.dot(w.T, np.dot(sigma, w))) sh = pr / (pv + 1e-10) if sh > best_sh: best_sh = sh; best_w = w pr = np.dot(best_w, mu); pv = np.sqrt(np.dot(best_w.T, np.dot(sigma, best_w))) eq_w = np.ones(n) / n; eq_r = np.dot(eq_w, mu); eq_v = np.sqrt(np.dot(eq_w.T, np.dot(sigma, eq_w))) ws = np.random.dirichlet(np.ones(n) * 0.5, 2000) ws = np.clip(ws, 0, 0.5); ws = ws / ws.sum(axis=1, keepdims=True) prets = np.dot(ws, mu); pvols = np.array([np.sqrt(np.dot(w.T, np.dot(sigma, w))) for w in ws]) psh = prets / (pvols + 1e-10) fig = go.Figure() fig.add_trace(go.Scatter(x=pvols, y=prets, mode='markers', marker=dict(size=4, color=psh, colorscale='Viridis', showscale=True, colorbar=dict(title='Sharpe')))) fig.add_trace(go.Scatter(x=[pv], y=[pr], mode='markers+text', marker=dict(size=16, color='red', symbol='star'), text=['Optimal'], textposition='top center')) fig.add_trace(go.Scatter(x=[eq_v], y=[eq_r], mode='markers+text', marker=dict(size=12, color='orange', symbol='diamond'), text=['Equal'], textposition='bottom center')) fig.update_layout(title='Cross-Market Efficient Frontier', xaxis_title='Volatility', yaxis_title='Return', template='plotly_white', height=500, showlegend=False) wdf = pd.DataFrame({'Ticker': list(data.keys()), 'Optimal %': np.round(best_w * 100, 2), 'Equal %': np.round(eq_w * 100, 2)}) md = f"""### Cross-Market Portfolio: {', '.join(list(data.keys()))} | Metric | Optimal | Equal Weight | |---|---|---| | Return | {pr*100:.1f}% | {eq_r*100:.1f}% | | Volatility | {pv*100:.1f}% | {eq_v*100:.1f}% | | Sharpe | {best_sh:.2f} | {eq_r/(eq_v+1e-10):.2f} | **Improvement:** Sharpe {((best_sh/(eq_r/(eq_v+1e-10))-1)*100):+.1f}% {wdf.to_markdown(index=False)} """ return fig, wdf, md def ai_portfolio(symbols_input, period): fig, wdf, md = opt_portfolio(symbols_input, period) if fig is None: return md pd_str = f"Symbols: {', '.join(wdf['Ticker'].tolist())}\nWeights: {', '.join([f'{t}:{w:.1f}%' for t, w in zip(wdf['Ticker'], wdf['Optimal %'])])}" prompt = f"""You are a global macro portfolio manager at a quant hedge fund ($500M AUM). This is a cross-market portfolio. {pd_str} Provide: 1) Health Score 0-100 2) Cross-market concentration risk 3) Currency exposure 4) Correlation risks 5) Rebalancing recommendations 6) Hedging strategy 7) Expected return & Sharpe.""" return K2Client().ask(prompt, temp=0.3) def ai_chat(q, t): return K2Client().ask(q, temp=t) # ═══════════════════════════════════════════════════════════════ # GRADIO UI # ═══════════════════════════════════════════════════════════════ CSS = """ .title { font-size: 1.9em; font-weight: 800; text-align: center; color: #2563eb; } .subtitle { text-align: center; color: #6b7280; font-size: 0.9em; } .badge { display: inline-block; background: linear-gradient(90deg, #4f46e5, #7c3aed); color: white; padding: 3px 10px; border-radius: 16px; font-size: 0.75em; font-weight: 600; margin: 0 2px; } .status-ok { color: #16a34a; font-weight: 600; } .status-warn { color: #ca8a04; font-weight: 600; } .factor-good { color: #16a34a; } .factor-bad { color: #dc2626; } .factor-neutral { color: #6b7280; } """ with gr.Blocks(title="AlphaForge v2.0 — 7-Factor Quant Terminal") as demo: gr.HTML(f"""
AlphaForge x K2 Think V2
7-Factor Intelligence Engine | Multi-Market | MIN Position Sizing
🤖 K2 AI 🧠 7 Factors 📰 FinBERT News 📊 Options Flow 🌍 Macro 💰 MIN Sizing
{'✅ K2 Connected' if K2_OK else '⚠️ K2 offline — other features work'}
""") with gr.Tab("🧠 Full Analysis"): with gr.Row(): with gr.Column(scale=1): market_in = gr.Dropdown(label="Market", choices=list(MARKETS.keys()), value="US") sym_in = gr.Textbox(label="Symbol", value="AAPL") p_in = gr.Dropdown(label="Period", choices=["1mo","3mo","6mo","1y","2y","5y"], value="6mo") a_btn = gr.Button("🔍 Analyze", variant="primary") ai_btn = gr.Button("🤖 AI Deep Analysis", variant="secondary") examples_md = gr.Markdown("Try: AAPL, TSLA, SPY, NVDA") with gr.Column(scale=2): out_md = gr.Markdown() score_bar = gr.Slider(label="7-Factor Conviction Score", minimum=0, maximum=100, value=50, interactive=False) size_bar = gr.Slider(label="Recommended Exposure %", minimum=0, maximum=100, value=0, interactive=False) with gr.Row(): out_candle = gr.Plot(label="Price") out_regime = gr.Plot(label="Regime") with gr.Row(): out_macd = gr.Plot(label="MACD") out_dist = gr.Plot(label="Returns") with gr.Row(): out_ai = gr.Textbox(label="AI Analysis", lines=16) def update_examples(market): return f"Try: {MARKETS[market]['examples']}" market_in.change(fn=update_examples, inputs=[market_in], outputs=[examples_md]) def analyze_with_score(symbol, market, period): result = analyze(symbol, market, period) candle, regime, macd, dist, md, score, size = result return candle, regime, macd, dist, md, score, int(size * 100) a_btn.click(fn=analyze_with_score, inputs=[sym_in, market_in, p_in], outputs=[out_candle, out_regime, out_macd, out_dist, out_md, score_bar, size_bar]) ai_btn.click(fn=ai_analyze, inputs=[sym_in, market_in, p_in], outputs=[out_ai]) with gr.Tab("💼 Cross-Market Portfolio"): with gr.Row(): with gr.Column(scale=1): pt_in = gr.Textbox(label="Symbols (any market)", value="AAPL, BTC-USD, EURUSD=X, GC=F, SHEL.L") pp_in = gr.Dropdown(label="Period", choices=["6mo","1y","2y","3y"], value="1y") o_btn = gr.Button("🎯 Optimize", variant="primary") ai_p_btn = gr.Button("🤖 AI Advice", variant="secondary") with gr.Column(scale=2): p_md = gr.Markdown() with gr.Row(): out_frontier = gr.Plot(label="Efficient Frontier") out_weights = gr.DataFrame(label="Weights", interactive=False) with gr.Row(): out_ai_p = gr.Textbox(label="AI Portfolio Advice", lines=16) o_btn.click(fn=opt_portfolio, inputs=[pt_in, pp_in], outputs=[out_frontier, out_weights, p_md]) ai_p_btn.click(fn=ai_portfolio, inputs=[pt_in, pp_in], outputs=[out_ai_p]) with gr.Tab("💬 AI Chat"): gr.Markdown("### Ask K2 Think V2 anything about global markets") c_in = gr.Textbox(label="Question", placeholder="Compare US tech vs European energy stocks...", lines=4) c_temp = gr.Slider(label="Temperature", minimum=0, maximum=1, value=0.5, step=0.1) c_btn = gr.Button("🚀 Ask", variant="primary") c_out = gr.Textbox(label="Response", lines=20) c_btn.click(fn=ai_chat, inputs=[c_in, c_temp], outputs=[c_out]) with gr.Tab("ℹ️ About"): gr.Markdown(""" ### AlphaForge v2.0 — 7-Factor Quant Terminal Built for the **Build with K2 Think V2 Challenge** by MBZUAI. **What's New in v2.0:** - 🧠 **7-Factor Scoring Engine** — Trend + Momentum + Volatility + Fundamentals + News + Options + Macro - 💰 **MIN-Based Position Sizing** — Most restrictive constraint wins (fixes weighted average bug) - 📰 **FinBERT-Style News Sentiment** — Real yfinance news with event classification - 📊 **Options Flow Analysis** — Put/Call ratio from real options chains - 🌍 **Macro Overlay** — VIX, 10Y yields, DXY in real time - 📅 **Earnings Model** — Calendar detection with auto size reduction - 🛡️ **Event Risk Guard** — Pre-trade checks for earnings/FOMC - 🌍 **9 Global Markets** — US, UK, DE, JP, CN, IN, Crypto, Forex, Commodities **Setup for AI:** 1. Space Settings → Repository secrets 2. New secret: `K2_API_KEY` 3. Save → Factory Rebuild All technical analysis works without API key. """) demo.launch(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Soft())