Spaces:
Sleeping
Sleeping
Full v2.0 Gradio demo β 7-factor engine, news FinBERT, options flow, macro overlay, earnings model, event risk guard, MIN-based sizing, cross-market portfolio
0a3624d verified | """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']<l['SMA20']<l['SMA50'] else 'βͺ Mixed'} | β | | |
| | Volume | {l['VR']:.1f}x avg | {'π₯ Heavy' if l['VR']>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""" | |
| <style>{CSS}</style> | |
| <div style="text-align:center; padding: 10px 0;"> | |
| <div class="title">AlphaForge x K2 Think V2</div> | |
| <div class="subtitle">7-Factor Intelligence Engine | Multi-Market | MIN Position Sizing</div> | |
| <div style="margin: 8px 0;"> | |
| <span class="badge">π€ K2 AI</span> | |
| <span class="badge">π§ 7 Factors</span> | |
| <span class="badge">π° FinBERT News</span> | |
| <span class="badge">π Options Flow</span> | |
| <span class="badge">π Macro</span> | |
| <span class="badge">π° MIN Sizing</span> | |
| </div> | |
| <div style="font-size:0.8em; margin-top:6px;"> | |
| {'<span class="status-ok">β K2 Connected</span>' if K2_OK else '<span class="status-warn">β οΈ K2 offline β other features work</span>'} | |
| </div> | |
| </div> | |
| """) | |
| 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("<small>Try: AAPL, TSLA, SPY, NVDA</small>") | |
| 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"<small>Try: {MARKETS[market]['examples']}</small>" | |
| 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()) | |