diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,1056 +1,1287 @@ -""" -AlphaForge x K2 Think V2 — Institutional-Grade Quantitative Analysis Platform -Built for the "Build with K2 Think V2" Challenge by MBZUAI. -""" +"""AlphaForge x K2 Think V2 — Institutional-Grade Quantitative Analysis Platform -import os -import json -import time -import traceback -from datetime import datetime, timedelta -from typing import Optional, Dict, List, Tuple +Multi-market support: US, EU, Asia, Crypto, Forex, Commodities +Finance-themed UI with dark mode, professional color scheme +Enhanced features: Options pricing, Pairs Trading, Backtest, Macro Analysis +Powered by MBZUAI K2 Think V2 reasoning model -import numpy as np -import pandas as pd -import plotly.graph_objects as go -import plotly.express as px -from plotly.subplots import make_subplots -import yfinance as yf -import requests -import gradio as gr - -# ─── Configuration ─────────────────────────────────────────────────────────── +API Key: set via K2_API_KEY environment variable +""" +import os, json, traceback, warnings, math, random +warnings.filterwarnings('ignore') + +# Core imports with graceful fallback +_import_errors = [] +try: + import gradio as gr +except ImportError as e: + _import_errors.append(f"gradio: {e}") + gr = None +try: + import requests +except ImportError as e: + _import_errors.append(f"requests: {e}") + requests = None +try: + import yfinance as yf +except ImportError as e: + _import_errors.append(f"yfinance: {e}") + yf = None +try: + import pandas as pd + pd.set_option('future.no_silent_downcasting', True) +except ImportError as e: + _import_errors.append(f"pandas: {e}") + pd = None +try: + import numpy as np +except ImportError as e: + _import_errors.append(f"numpy: {e}") + np = None +try: + import plotly.graph_objects as go + from plotly.subplots import make_subplots + PLOTLY_OK = True +except ImportError as e: + _import_errors.append(f"plotly: {e}") + PLOTLY_OK = False + go = None + make_subplots = None +from datetime import datetime, timedelta -K2_API_URL = "https://api.k2think.ai/v1/chat/completions" +# ═══════════════════════════════════════════════════════════════ +# CONFIG +# ═══════════════════════════════════════════════════════════════ K2_API_KEY = os.environ.get("K2_API_KEY", "") +K2_BASE_URL = "https://api.k2think.ai/v1/chat/completions" K2_MODEL = "MBZUAI-IFM/K2-Think-v2" -APP_TITLE = "AlphaForge x K2 Think V2" -APP_DESC = "Institutional-Grade Quantitative Analysis Platform" - -# ─── K2 Think V2 Client ───────────────────────────────────────────────────── - -def call_k2(messages: list, temperature: float = 0.3, max_tokens: int = 2048) -> str: - """Call the K2 Think V2 API with proper error handling.""" - if not K2_API_KEY: - return "⚠️ K2_API_KEY not set. Add it as a Space secret in Settings." - - headers = { - "Authorization": f"Bearer {K2_API_KEY}", - "Content-Type": "application/json", - "Accept": "application/json" - } - - payload = { - "model": K2_MODEL, - "messages": messages, - "temperature": temperature, - "max_tokens": max_tokens - } - - try: - response = requests.post(K2_API_URL, headers=headers, json=payload, timeout=120) - - if response.status_code != 200: - return f"⚠️ API Error {response.status_code}: {response.text[:500]}" - - # Handle streaming response - full_content = "" - for line in response.text.split('\n'): - line = line.strip() - if not line or line == 'data: [DONE]': - continue - if line.startswith('data: '): - line = line[6:] - try: - chunk = json.loads(line) - delta = chunk.get('choices', [{}])[0].get('delta', {}) - if 'content' in delta: - full_content += delta['content'] - except (json.JSONDecodeError, KeyError, IndexError): - pass - - # Try non-streaming fallback - if not full_content: - try: - data = response.json() - full_content = data.get('choices', [{}])[0].get('message', {}).get('content', '') - except (json.JSONDecodeError, KeyError): - pass - - if not full_content: - return f"⚠️ Empty response from K2 Think V2. Raw: {response.text[:300]}" - - return full_content.strip() - - except requests.exceptions.Timeout: - return "⚠️ K2 Think V2 request timed out (120s). Try again or use a shorter question." - except Exception as e: - return f"⚠️ API Error: {str(e)}" - - -def call_k2_non_streaming(messages: list, temperature: float = 0.3, max_tokens: int = 2048) -> str: - """Non-streaming fallback for K2 Think V2.""" - if not K2_API_KEY: - return "⚠️ K2_API_KEY not set." +# ═══════════════════════════════════════════════════════════════ +# K2 THINK V2 CLIENT — bulletproof +# ═══════════════════════════════════════════════════════════════ +class K2ThinkClient: + def __init__(self): + self.api_key = K2_API_KEY + self.available = bool(self.api_key) and len(self.api_key) > 10 + self.base_url = K2_BASE_URL + + def chat(self, messages, temperature=0.7, max_tokens=4096): + if not self.available or requests is None: + return """⚠️ **K2 Think V2 API Not Configured** + +To enable AI-powered analysis, configure the API key: +1. Go to **Space Settings** → **Repository secrets** +2. Click **New secret** +3. Name: `K2_API_KEY` +4. Value: `IFM-4SpQ0qEg0Wlsw04O` +5. Click **Save**, then **Factory Rebuild** + +All technical analysis, charts, and risk metrics work without the API!""" + + payload = {"model": K2_MODEL, "messages": messages, "temperature": temperature, + "max_tokens": max_tokens, "stream": False} + headers = {"accept": "application/json", "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json"} + + try: + r = requests.post(self.base_url, headers=headers, json=payload, timeout=120) + r.raise_for_status() + j = r.json() + if 'choices' in j and len(j['choices']) > 0: + return j['choices'][0]['message']['content'] + return f"⚠️ Unexpected format: {json.dumps(j, indent=2)[:400]}" + except requests.exceptions.Timeout: + return "⏱️ Timeout after 120s. API may be under high load. Try again." + except requests.exceptions.HTTPError as e: + if e.response.status_code == 401: + return "🔐 Auth failed. Check K2_API_KEY secret in Space Settings." + elif e.response.status_code == 429: + return "🚦 Rate limited. Wait a moment." + return f"🔴 HTTP {e.response.status_code}: {str(e)[:200]}" + except Exception as e: + return f"🔴 Error: {str(e)[:300]}" + + def analyze_market(self, ticker, market, data_summary, tech_summary, timeframe): + prompt = f"""You are an elite quantitative analyst at a top hedge fund (Two Sigma / Jane Street level). +Analyze the following market data with deep chain-of-thought reasoning. + +## Asset Information +- **Ticker**: {ticker} +- **Market**: {market} +- **Timeframe**: {timeframe} + +## Market Data Summary +{data_summary} - headers = { - "Authorization": f"Bearer {K2_API_KEY}", - "Content-Type": "application/json" - } +## Technical Indicators +{tech_summary} - payload = { - "model": K2_MODEL, - "messages": messages, - "temperature": temperature, - "max_tokens": max_tokens, - "stream": False - } +## Deliverables +Provide exactly these sections: - try: - response = requests.post(K2_API_URL, headers=headers, json=payload, timeout=120) - if response.status_code != 200: - return f"⚠️ API Error {response.status_code}: {response.text[:500]}" - data = response.json() - content = data.get('choices', [{}])[0].get('message', {}).get('content', '') - return content.strip() if content else "⚠️ Empty response." - except Exception as e: - return f"⚠️ Error: {str(e)}" +### 1. Executive Summary (3 bullets) +- Most critical finding +- Key risk factor +- Immediate catalyst to watch +### 2. Technical Analysis +- Interpret RSI, MACD, Bollinger Bands in context of current price action +- Identify support/resistance levels from SMAs and VWAP +- Note any divergence signals -# ─── Data Layer ────────────────────────────────────────────────────────────── +### 3. Risk Assessment +- Volatility regime (low/normal/high) +- Tail risk estimate (based on VaR, skewness, kurtosis) +- Correlation risk if part of a broader portfolio -def fetch_stock_data(ticker: str, period: str = "1y") -> Tuple[Optional[pd.DataFrame], Optional[dict], str]: - """Fetch and compute all technical indicators for a single stock.""" - info = {} +### 4. Alpha Signal +- Direction: BULLISH / NEUTRAL / BEARISH +- Confidence: X% +- Time horizon: Short-term (1-4 weeks) / Medium-term (1-3 months) / Long-term (3-12 months) +- Key conviction drivers + +### 5. Trade Recommendation +- Entry price / zone +- Stop-loss (tight vs wide reasoning) +- Target 1 (conservative) +- Target 2 (aggressive) +- Position sizing suggestion (% of portfolio) + +### 6. Catalyst Calendar +- Next 7 days: earnings, macro data, Fed speakers +- Next 30 days: options expiration, sector events + +### 7. Contrarian View +- What would make this signal wrong? +- Alternative scenario with probability + +Think step-by-step. Reference specific numbers from the data.""" + return self.chat([{"role": "user", "content": prompt}], temperature=0.2, max_tokens=4096) + + def portfolio_advice(self, portfolio_data, corr_data, risk_metrics, market_context): + prompt = f"""You are a CIO managing $2B AUM at a systematic macro fund. + +## Portfolio Holdings +{portfolio_data} + +## Correlation Analysis +{corr_data} + +## Risk Metrics +{risk_metrics} + +## Market Context +{market_context} + +## Deliverables + +### 1. Portfolio Health Score (0-100) +Grade with letter (A+ to F) and justify + +### 2. Concentration Risk +- Single-name exposure limits +- Sector/style drift +- Geographic concentration + +### 3. Correlation Risk Matrix +- Hidden correlated bets +- Diversification quality score +- Tail correlation concern + +### 4. Rebalancing Roadmap +- Specific weight adjustments with % +- Timeline: immediate / 1 week / 1 month +- Tax/transaction cost considerations + +### 5. Hedging Strategy +- Options structure for tail protection +- ETF hedge ratios +- Cost estimate ($ and basis points) + +### 6. Expected Return & Risk (Forward 12M) +| Metric | Point Estimate | Range | +|--------|---------------|-------| +| Annual Return | X% | [Y%, Z%] | +| Volatility | X% | [Y%, Z%] | +| Sharpe | X.X | [Y, Z] | +| Max Drawdown | -X% | [-Y%, -Z%] | +| 95% VaR (1M) | -X% | — | + +### 7. Scenario Analysis +- Bull case (20% probability): what happens, portfolio impact +- Base case (50% probability) +- Bear case (20% probability) +- Tail case (10% probability) + +Use quantitative reasoning throughout.""" + return self.chat([{"role": "user", "content": prompt}], temperature=0.2, max_tokens=4096) + + def macro_analysis(self, macro_text): + prompt = f"""You are a global macro strategist at Bridgewater / Millennium. + +## Input +{macro_text} + +## Deliverables + +### 1. Macro Regime Classification +- Current regime (growth/inflation quadrant) +- Regime stability (stable vs transitioning) +- Historical analog (which past period rhymes) + +### 2. Cross-Asset Implications +- Equities: sector rotation, style factor +- Fixed Income: curve shape, credit spread +- FX: carry trade, safe haven flows +- Commodities: supply/demand, inflation hedge +- Crypto: risk-on/risk-off sensitivity + +### 3. Trade Ideas (3 concrete setups) +Each with: instrument, direction, entry, stop, target, conviction %, time horizon + +### 4. Risk Factors +- Top 3 risks that could invalidate thesis +- Probability and impact matrix +- Hedging suggestion + +Think like a macro PM.""" + return self.chat([{"role": "user", "content": prompt}], temperature=0.3, max_tokens=4096) + + def options_analysis(self, option_data, stock_data): + prompt = f"""You are an exotics trader at Citadel / SIG analyzing options. + +## Option Parameters +{option_data} + +## Underlying Stock Data +{stock_data} + +## Deliverables + +### 1. Greeks Analysis +- Delta hedge ratio +- Gamma risk (pin risk near expiry) +- Theta decay per day +- Vega sensitivity to vol moves +- Vomma / Vanna if relevant + +### 2. Pricing Assessment +- Is this option cheap/fair/expensive vs historical vol? +- Implied vs realized vol spread +- Term structure shape + +### 3. Strategy Recommendation +- Best structure for current view (vertical, butterfly, iron condor, etc.) +- Expected P/L at expiry (visual table) +- Break-even points +- Max gain / max loss + +### 4. Scenario Analysis +| Stock Price at Expiry | P/L | Probability (approx) | +|----------------------|-----|----------------------| +| -20% | $X | Y% | +| -10% | $X | Y% | +| 0% | $X | Y% | +| +10% | $X | Y% | +| +20% | $X | Y% | + +Risk-adjusted expected return calculation.""" + return self.chat([{"role": "user", "content": prompt}], temperature=0.3, max_tokens=4096) + +# ═══════════════════════════════════════════════════════════════ +# MARKET DATA — multi-market support +# ═══════════════════════════════════════════════════════════════ +MARKET_PRESETS = { + "🇺🇸 US Equities": {"suffix": "", "examples": "AAPL, TSLA, NVDA, SPY, QQQ, META, AMZN, GOOGL"}, + "🇪🇺 European Equities": {"suffix": ".PA", "examples": "AIR.PA, SAN.PA, TTE.PA, OR.PA, MC.PA, ASML.AS"}, + "🇬🇧 UK Equities": {"suffix": ".L", "examples": "AZN.L, SHEL.L, BP.L, ULVR.L, RIO.L, GSK.L"}, + "🇩🇪 German Equities": {"suffix": ".DE", "examples": "SAP.DE, SIE.DE, ALV.DE, BAS.DE, BMW.DE, MBG.DE"}, + "🇯🇵 Japanese Equities": {"suffix": ".T", "examples": "7203.T, 9984.T, 6861.T, 6758.T, 8306.T"}, + "🇨🇳 Chinese Equities": {"suffix": ".HK", "examples": "0700.HK, 9988.HK, 3690.HK, 1810.HK"}, + "🇮🇳 Indian Equities": {"suffix": ".NS", "examples": "RELIANCE.NS, TCS.NS, INFY.NS, HDFCBANK.NS"}, + "🪙 Crypto": {"suffix": "", "examples": "BTC-USD, ETH-USD, SOL-USD, XRP-USD, ADA-USD, DOGE-USD"}, + "💱 Forex Majors": {"suffix": "=X", "examples": "EURUSD=X, GBPUSD=X, USDJPY=X, AUDUSD=X, USDCAD=X"}, + "🥇 Commodities": {"suffix": "", "examples": "GC=F, SI=F, CL=F, NG=F, ZC=F, ZW=F"}, + "📊 Indices": {"suffix": "", "examples": "^GSPC, ^DJI, ^IXIC, ^FTSE, ^N225, ^HSI"}, +} + +def fetch_data(ticker, period="6mo", interval="1d"): + if yf is None: + return None, "yfinance not available" try: - stock = yf.Ticker(ticker) - df = stock.history(period=period) - + stock = yf.Ticker(ticker.upper().strip()) + df = stock.history(period=period, interval=interval) if df.empty: - return None, None, f"No data found for {ticker}." - - # Get stock info - try: - info_obj = stock.info - info = { - "name": info_obj.get("longName", ticker), - "sector": info_obj.get("sector", "N/A"), - "industry": info_obj.get("industry", "N/A"), - "market_cap": info_obj.get("marketCap", None), - "pe_ratio": info_obj.get("trailingPE", None), - "forward_pe": info_obj.get("forwardPE", None), - "dividend_yield": info_obj.get("dividendYield", None), - "beta": info_obj.get("beta", None), - "52w_high": info_obj.get("fiftyTwoWeekHigh", None), - "52w_low": info_obj.get("fiftyTwoWeekLow", None), - "avg_volume": info_obj.get("averageVolume", None), - "short_ratio": info_obj.get("shortRatio", None), - } - except Exception: - pass - - # === Technical Indicators === - close = df['Close'].astype(float) - high = df['High'].astype(float) - low = df['Low'].astype(float) - volume = df['Volume'].astype(float) - - # SMA - df['SMA_20'] = close.rolling(20).mean() - df['SMA_50'] = close.rolling(50).mean() - df['SMA_200'] = close.rolling(200).mean() - - # EMA - df['EMA_12'] = close.ewm(span=12, adjust=False).mean() - df['EMA_26'] = close.ewm(span=26, adjust=False).mean() - - # MACD - df['MACD'] = df['EMA_12'] - df['EMA_26'] - df['MACD_Signal'] = df['MACD'].ewm(span=9, adjust=False).mean() - df['MACD_Hist'] = df['MACD'] - df['MACD_Signal'] - - # RSI - delta = close.diff() - gain = delta.where(delta > 0, 0.0) - loss = -delta.where(delta < 0, 0.0) - avg_gain = gain.rolling(14).mean() - avg_loss = loss.rolling(14).mean() - rs = avg_gain / avg_loss - df['RSI'] = 100 - (100 / (1 + rs)) - - # Bollinger Bands - df['BB_Mid'] = close.rolling(20).mean() - bb_std = close.rolling(20).std() - df['BB_Upper'] = df['BB_Mid'] + 2 * bb_std - df['BB_Lower'] = df['BB_Mid'] - 2 * bb_std - - # ATR - high_low = high - low - high_close = (high - close.shift()).abs() - low_close = (low - close.shift()).abs() - tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1) - df['ATR'] = tr.rolling(14).mean() - - # Stochastic - low_14 = low.rolling(14).min() - high_14 = high.rolling(14).max() - df['Stoch_K'] = 100 * (close - low_14) / (high_14 - low_14) - df['Stoch_D'] = df['Stoch_K'].rolling(3).mean() - - # VWAP - df['VWAP'] = (df['Volume'] * (high + low + close) / 3).cumsum() / df['Volume'].cumsum() - - # Volume Ratio - df['Vol_Ratio'] = volume / volume.rolling(20).mean() - - # Daily returns - df['Returns'] = close.pct_change() - df['Log_Returns'] = np.log(close / close.shift(1)) - - # === Composite Alpha Signal (5 factors) === - trend_score = ((df['SMA_20'] > df['SMA_50']).astype(float) * 0.5 + - (df['SMA_50'] > df['SMA_200']).astype(float) * 0.3 + - (close > df['SMA_20']).astype(float) * 0.2) - - macd_score = ((df['MACD'] > df['MACD_Signal']).astype(float) * 0.5 + - (df['MACD_Hist'] > 0).astype(float) * 0.5) - - rsi_score = ((df['RSI'] < 30).astype(float) * 1.0 + - ((df['RSI'] > 50) & (df['RSI'] < 70)).astype(float) * 0.5 + - (df['RSI'] > 70).astype(float) * (-1.0)) - - vol_score = (df['Vol_Ratio'] > 1.5).astype(float) - - price_score = ((close > df['VWAP']).astype(float) * 0.5 + - (close > df['BB_Mid']).astype(float) * 0.3 + - (close < df['BB_Lower']).astype(float) * 0.2) - - df['Alpha_Score'] = (0.30 * trend_score + 0.20 * macd_score + - 0.20 * rsi_score + 0.15 * vol_score + 0.15 * price_score) - - # Risk metrics - returns = df['Returns'].dropna() - rf = 0.05 / 252 # daily risk-free rate - - sharpe = np.sqrt(252) * (returns.mean() - rf) / returns.std() if returns.std() > 0 else 0 - downside = returns[returns < 0] - sortino = np.sqrt(252) * (returns.mean() - rf) / downside.std() if len(downside) > 0 and downside.std() > 0 else 0 - var_95 = np.percentile(returns, 5) - cvar_95 = returns[returns <= var_95].mean() if len(returns[returns <= var_95]) > 0 else var_95 - max_dd = (close / close.cummax() - 1).min() - calmar = (returns.mean() * 252) / abs(max_dd) if max_dd != 0 else 0 - skewness = returns.skew() - kurtosis = returns.kurtosis() - - risk_metrics = { - "sharpe": round(sharpe, 3), - "sortino": round(sortino, 3), - "var_95": f"{var_95:.4f}", - "cvar_95": f"{cvar_95:.4f}", - "max_drawdown": f"{max_dd:.4f}", - "calmar": round(calmar, 3), - "skewness": round(skewness, 3), - "kurtosis": round(kurtosis, 3), - } - - # Current signals - latest = { - "price": f"${close.iloc[-1]:.2f}", - "change": f"{close.iloc[-1] - close.iloc[-2]:.2f}", - "change_pct": f"{(close.iloc[-1] / close.iloc[-2] - 1) * 100:.2f}%", - "rsi": round(df['RSI'].iloc[-1], 1) if not pd.isna(df['RSI'].iloc[-1]) else "N/A", - "macd": round(df['MACD'].iloc[-1], 3) if not pd.isna(df['MACD'].iloc[-1]) else "N/A", - "macd_signal": round(df['MACD_Signal'].iloc[-1], 3) if not pd.isna(df['MACD_Signal'].iloc[-1]) else "N/A", - "alpha_score": round(df['Alpha_Score'].iloc[-1], 3) if not pd.isna(df['Alpha_Score'].iloc[-1]) else "N/A", - "vol_ratio": round(df['Vol_Ratio'].iloc[-1], 2) if not pd.isna(df['Vol_Ratio'].iloc[-1]) else "N/A", - "atr": round(df['ATR'].iloc[-1], 2) if not pd.isna(df['ATR'].iloc[-1]) else "N/A", - "vwap": f"${df['VWAP'].iloc[-1]:.2f}" if not pd.isna(df['VWAP'].iloc[-1]) else "N/A", - "sma_20": f"${df['SMA_20'].iloc[-1]:.2f}" if not pd.isna(df['SMA_20'].iloc[-1]) else "N/A", - "sma_50": f"${df['SMA_50'].iloc[-1]:.2f}" if not pd.isna(df['SMA_50'].iloc[-1]) else "N/A", - "sma_200": f"${df['SMA_200'].iloc[-1]:.2f}" if not pd.isna(df['SMA_200'].iloc[-1]) else "N/A", - "stoch_k": round(df['Stoch_K'].iloc[-1], 1) if not pd.isna(df['Stoch_K'].iloc[-1]) else "N/A", - } - - return df, {"info": info, "latest": latest, "risk": risk_metrics}, "" - + return None, f"No data for '{ticker}'. Try: {random.choice(list(MARKET_PRESETS.values()))['examples']}" + info = stock.info + return df, info except Exception as e: - return None, None, f"Error fetching {ticker}: {str(e)}" - - -# ─── Visualization ─────────────────────────────────────────────────────────── - -def make_candlestick_chart(df: pd.DataFrame, ticker: str) -> go.Figure: - """Create interactive candlestick chart with SMA and Bollinger Bands.""" - fig = make_subplots( - rows=3, cols=1, - shared_xaxes=True, - vertical_spacing=0.03, - row_heights=[0.5, 0.25, 0.25], - subplot_titles=(f"{ticker} — Price & Bollinger Bands", "MACD", "RSI") - ) - + return None, f"Error fetching '{ticker}': {str(e)[:200]}" + +def calc_indicators(df): + df = df.copy() + df['Ret'] = df['Close'].pct_change() + df['LogRet'] = np.log(df['Close']/df['Close'].shift(1)) + df['SMA5'] = df['Close'].rolling(5).mean() + 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() + df['MACDH'] = df['MACD'] - df['MACDS'] + 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 + df['BBP'] = (df['Close']-df['BBL'])/(df['BBU']-df['BBL']+1e-10) + df['BBW'] = (df['BBU']-df['BBL'])/m + tp = (df['High']+df['Low']+df['Close'])/3 + df['VWAP'] = (tp*df['Volume']).cumsum()/(df['Volume'].cumsum()+1e-10) + hl = df['High']-df['Low'] + hc = np.abs(df['High']-df['Close'].shift()) + lc = np.abs(df['Low']-df['Close'].shift()) + tr = pd.concat([hl,hc,lc],axis=1).max(axis=1) + df['ATR'] = tr.rolling(14).mean() + df['ATR_pct'] = df['ATR']/df['Close']*100 + lo = df['Low'].rolling(14).min() + hi = df['High'].rolling(14).max() + df['Stoch_K'] = 100*(df['Close']-lo)/(hi-lo+1e-10) + df['Stoch_D'] = df['Stoch_K'].rolling(3).mean() + df['VM'] = df['Volume'].rolling(20).mean() + df['VR'] = df['Volume']/(df['VM']+1e-10) + # ADX + plus_dm = df['High'].diff() + minus_dm = df['Low'].diff() + plus_dm[plus_dm<0] = 0 + minus_dm[minus_dm>0] = 0 + minus_dm = np.abs(minus_dm) + atr_smooth = tr.ewm(alpha=1/14, adjust=False).mean() + df['plus_DI'] = 100 * (plus_dm.ewm(alpha=1/14, adjust=False).mean() / atr_smooth) + df['minus_DI'] = 100 * (minus_dm.ewm(alpha=1/14, adjust=False).mean() / atr_smooth) + dx = 100 * np.abs(df['plus_DI']-df['minus_DI'])/(df['plus_DI']+df['minus_DI']+1e-10) + df['ADX'] = dx.ewm(alpha=1/14, adjust=False).mean() + # OBV + df['OBV'] = (np.sign(df['Close'].diff())*df['Volume']).cumsum() + # MFI + tp_r = (df['High']+df['Low']+df['Close'])/3 + tp_diff = tp_r.diff() + pos_flow = tp_r.where(tp_diff>0,0)*df['Volume'] + neg_flow = tp_r.where(tp_diff<0,0)*df['Volume'] + mfi_pos = pos_flow.rolling(14).sum() + mfi_neg = neg_flow.rolling(14).sum() + df['MFI'] = 100 - (100/(1+mfi_pos/(mfi_neg+1e-10))) + # Ichimoku + df['ICH_tenkan'] = (df['High'].rolling(9).max()+df['Low'].rolling(9).min())/2 + df['ICH_kijun'] = (df['High'].rolling(26).max()+df['Low'].rolling(26).min())/2 + df['ICH_senkou_A'] = ((df['ICH_tenkan']+df['ICH_kijun'])/2).shift(26) + df['ICH_senkou_B'] = ((df['High'].rolling(52).max()+df['Low'].rolling(52).min())/2).shift(26) + return df + +def calc_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) + v99 = np.percentile(r,1) + cv95 = r[r<=v95].mean() if len(r[r<=v95])>0 else v95 + cv99 = r[r<=v99].mean() if len(r[r<=v99])>0 else v99 + ca = ar/(abs(md)+1e-10) + # Rolling metrics + roll_sharpe = (r.rolling(63).mean()*252)/(r.rolling(63).std()*np.sqrt(252)+1e-10) + return {'ar':ar,'av':av,'sh':sh,'so':so,'md':md,'v95':v95,'v99':v99, + 'cv95':cv95,'cv99':cv99,'ca':ca,'sk':r.skew(),'ku':r.kurtosis(), + 'wr':(r>0).mean(),'pf':abs(r[r>0].sum()/(r[r<0].sum()+1e-10)), + 'avg_win':r[r>0].mean() if len(r[r>0])>0 else 0, + 'avg_loss':r[r<0].mean() if len(r[r<0])>0 else 0, + 'consec_wins':0,'consec_losses':0, + 'roll_sharpe':roll_sharpe.iloc[-1] if len(roll_sharpe.dropna())>0 else 0, + 'vol_regime':'low' if av<0.15 else 'normal' if av<0.30 else 'high'} + +def calc_signals(df): + l = df.iloc[-1] + p = df.iloc[-2] if len(df)>1 else l + s = {'trend':'neutral','mom':'neutral','vol':'normal','volume':'normal', + 'adx_trend':'weak','score':50,'ichimoku':'neutral'} + if l['Close']>l['SMA20']>l['SMA50']>l['SMA200']: + s['trend'] = 'strongly bullish' + elif l['Close']>l['SMA20']>l['SMA50']: + s['trend'] = 'bullish' + elif l['Close']70: + s['mom'] = 'deeply overbought' + elif l['RSI']>60: + s['mom'] = 'overbought' + elif l['MACD']>l['MACDS'] and p['MACD']<=p['MACDS']: + s['mom'] = 'bullish MACD crossover' + elif l['MACD']=p['MACDS']: + s['mom'] = 'bearish MACD crossover' + if l['BBW'] > df['BBW'].quantile(0.9): + s['vol'] = 'expanding (high vol)' + elif l['BBW'] < df['BBW'].quantile(0.1): + s['vol'] = 'contracting (low vol / squeeze)' + if l['VR'] > 2.5: + s['volume'] = 'very heavy (institutional)' + elif l['VR'] > 1.5: + s['volume'] = 'above average' + if l['ADX'] > 25: + s['adx_trend'] = 'strong trend' + elif l['ADX'] > 20: + s['adx_trend'] = 'trending' + # Ichimoku + if l['Close'] > l['ICH_senkou_A'] and l['Close'] > l['ICH_senkou_B']: + s['ichimoku'] = 'bullish cloud' + elif l['Close'] < l['ICH_senkou_A'] and l['Close'] < l['ICH_senkou_B']: + s['ichimoku'] = 'bearish cloud' + # Score + sc = 50 + if 'bullish' in s['trend']: sc += 20 + if 'bearish' in s['trend']: sc -= 20 + if 'oversold' in s['mom']: sc += 10 + if 'overbought' in s['mom']: sc -= 10 + if 'crossover' in s['mom'] and 'bullish' in s['mom']: sc += 10 + if 'crossover' in s['mom'] and 'bearish' in s['mom']: sc -= 10 + if l['Close'] > l['VWAP']: sc += 5 + if l['Close'] < l['VWAP']: sc -= 5 + if l['Stoch_K'] < 20: sc += 5 + if l['Stoch_K'] > 80: sc -= 5 + if s['ichimoku'] == 'bullish cloud': sc += 5 + if s['ichimoku'] == 'bearish cloud': sc -= 5 + s['score'] = max(0, min(100, sc)) + s['dir'] = 'BULLISH' if sc>60 else 'BEARISH' if sc<40 else 'NEUTRAL' + s['strength'] = 'STRONG' if abs(sc-50)>25 else 'MODERATE' if abs(sc-50)>15 else 'WEAK' + return s + +def make_candlestick(df, ticker, market): + if not PLOTLY_OK: + return None + fig = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.03, + row_heights=[0.55, 0.25, 0.20], + subplot_titles=(f'{ticker} ({market})', 'Volume + VWAP', 'RSI')) # Candlestick - fig.add_trace( - go.Candlestick( - x=df.index, open=df['Open'], high=df['High'], - low=df['Low'], close=df['Close'], - name="Price", showlegend=True - ), row=1, col=1 - ) - - # Moving Averages - for sma, name, color in [('SMA_20', 'SMA 20', 'blue'), ('SMA_50', 'SMA 50', 'orange'), ('SMA_200', 'SMA 200', 'red')]: - if sma in df.columns and df[sma].notna().any(): - fig.add_trace(go.Scatter(x=df.index, y=df[sma], mode='lines', - name=name, line=dict(color=color, width=1)), - row=1, col=1) - - # Bollinger Bands - if 'BB_Upper' in df.columns: - fig.add_trace(go.Scatter(x=df.index, y=df['BB_Upper'], mode='lines', - name='BB Upper', line=dict(color='gray', width=0.5, dash='dash'), - showlegend=False), row=1, col=1) - fig.add_trace(go.Scatter(x=df.index, y=df['BB_Lower'], mode='lines', - name='BB Lower', line=dict(color='gray', width=0.5, dash='dash'), - fill='tonexty', fillcolor='rgba(128,128,128,0.1)', - showlegend=False), row=1, col=1) - - # MACD - if 'MACD' in df.columns: - fig.add_trace(go.Scatter(x=df.index, y=df['MACD'], mode='lines', - name='MACD', line=dict(color='blue', width=1)), - row=2, col=1) - fig.add_trace(go.Scatter(x=df.index, y=df['MACD_Signal'], mode='lines', - name='Signal', line=dict(color='red', width=1)), - row=2, col=1) - colors = ['green' if v >= 0 else 'red' for v in df['MACD_Hist'].fillna(0)] - fig.add_trace(go.Bar(x=df.index, y=df['MACD_Hist'], name='Histogram', - marker_color=colors, showlegend=False), - row=2, col=1) - + fig.add_trace(go.Candlestick(x=df.index, open=df['Open'], high=df['High'], + low=df['Low'], close=df['Close'], name='Price', + increasing_line_color='#00C853', decreasing_line_color='#FF5252'), row=1, col=1) + # SMAs + fig.add_trace(go.Scatter(x=df.index, y=df['SMA20'], line=dict(color='#FF9800', width=1), name='SMA20'), row=1, col=1) + fig.add_trace(go.Scatter(x=df.index, y=df['SMA50'], line=dict(color='#2196F3', width=1), name='SMA50'), row=1, col=1) + fig.add_trace(go.Scatter(x=df.index, y=df['SMA200'], line=dict(color='#9C27B0', width=1.5, dash='dash'), name='SMA200'), row=1, col=1) + # BB + fig.add_trace(go.Scatter(x=df.index, y=df['BBU'], line=dict(color='gray', width=0.8, dash='dash'), name='BB+', opacity=0.4), row=1, col=1) + fig.add_trace(go.Scatter(x=df.index, y=df['BBL'], line=dict(color='gray', width=0.8, dash='dash'), name='BB-', opacity=0.4), row=1, col=1) + # Ichimoku + fig.add_trace(go.Scatter(x=df.index, y=df['ICH_senkou_A'], fill=None, mode='lines', line=dict(color='green', width=0.5), name='Senkou A'), row=1, col=1) + fig.add_trace(go.Scatter(x=df.index, y=df['ICH_senkou_B'], fill='tonexty', fillcolor='rgba(46,125,50,0.1)', mode='lines', line=dict(color='red', width=0.5), name='Senkou B'), row=1, col=1) + # Volume + colors = ['#00C853' if df['Close'].iloc[i]>=df['Open'].iloc[i] else '#FF5252' for i in range(len(df))] + fig.add_trace(go.Bar(x=df.index, y=df['Volume'], marker_color=colors, name='Volume', opacity=0.7), row=2, col=1) + fig.add_trace(go.Scatter(x=df.index, y=df['VM'], line=dict(color='#FF9800', width=1), name='Vol MA20'), row=2, col=1) # RSI - if 'RSI' in df.columns: - fig.add_trace(go.Scatter(x=df.index, y=df['RSI'], mode='lines', - name='RSI', line=dict(color='purple', width=1)), - row=3, col=1) - fig.add_hline(y=70, line_dash="dash", line_color="red", opacity=0.5, row=3, col=1) - fig.add_hline(y=30, line_dash="dash", line_color="green", opacity=0.5, row=3, col=1) - - fig.update_layout( - template='plotly_dark', - height=800, - hovermode='x unified', - showlegend=True, - margin=dict(l=40, r=40, t=60, b=40), - xaxis_rangeslider_visible=False, - ) - fig.update_xaxes(title_text="Date", row=3, col=1) - fig.update_yaxes(title_text="Price ($)", row=1, col=1) - fig.update_yaxes(title_text="MACD", row=2, col=1) - fig.update_yaxes(title_text="RSI", row=3, col=1) - + fig.add_trace(go.Scatter(x=df.index, y=df['RSI'], line=dict(color='#9C27B0', width=1.5), fill='tozeroy', fillcolor='rgba(156,39,176,0.1)'), row=3, col=1) + fig.add_hline(y=70, line_dash="dash", line_color="#FF5252", row=3, col=1) + fig.add_hline(y=30, line_dash="dash", line_color="#00C853", row=3, col=1) + fig.add_hline(y=50, line_dash="dot", line_color="gray", row=3, col=1) + fig.update_layout(title=f'{ticker} Technical Analysis', template='plotly_dark', + xaxis_rangeslider_visible=False, height=850, + paper_bgcolor='#0d1117', plot_bgcolor='#161b22', + font=dict(color='#e6edf3')) + 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 make_macd(df, ticker): + if not PLOTLY_OK: return None + fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.05, + row_heights=[0.6, 0.4], subplot_titles=('MACD', 'Histogram')) + fig.add_trace(go.Scatter(x=df.index, y=df['MACD'], line=dict(color='#2196F3', width=1.5), name='MACD'), row=1, col=1) + fig.add_trace(go.Scatter(x=df.index, y=df['MACDS'], line=dict(color='#FF9800', width=1.5), name='Signal'), row=1, col=1) + colors = ['#00C853' if v>=0 else '#FF5252' for v in df['MACDH']] + fig.add_trace(go.Bar(x=df.index, y=df['MACDH'], marker_color=colors, name='Histogram', opacity=0.7), row=2, col=1) + fig.update_layout(title=f'{ticker} MACD', template='plotly_dark', height=500, + paper_bgcolor='#0d1117', plot_bgcolor='#161b22', font=dict(color='#e6edf3')) + return fig -def make_returns_chart(df: pd.DataFrame) -> go.Figure: - """Create returns distribution histogram.""" - returns = df['Returns'].dropna() - +def make_stoch(df, ticker): + if not PLOTLY_OK: return None fig = go.Figure() - fig.add_trace(go.Histogram( - x=returns, nbinsx=50, name='Daily Returns', - histnorm='probability density', - marker_color='rgba(100, 149, 237, 0.7)', - showlegend=True - )) - - # Normal distribution overlay - x = np.linspace(returns.min(), returns.max(), 200) - y = (1 / (returns.std() * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((x - returns.mean()) / returns.std()) ** 2) - fig.add_trace(go.Scatter(x=x, y=y, mode='lines', - name='Normal Distribution', - line=dict(color='red', width=2))) - - fig.update_layout( - template='plotly_dark', - title="Daily Returns Distribution vs Normal", - height=400, - margin=dict(l=40, r=40, t=60, b=40), - ) - fig.update_xaxes(title_text="Daily Return") - fig.update_yaxes(title_text="Density") - + fig.add_trace(go.Scatter(x=df.index, y=df['Stoch_K'], line=dict(color='#2196F3', width=1.5), name='%K')) + fig.add_trace(go.Scatter(x=df.index, y=df['Stoch_D'], line=dict(color='#FF9800', width=1.5), name='%D')) + fig.add_hline(y=80, line_dash="dash", line_color="#FF5252") + fig.add_hline(y=20, line_dash="dash", line_color="#00C853") + fig.update_layout(title=f'{ticker} Stochastic Oscillator', template='plotly_dark', + height=400, paper_bgcolor='#0d1117', plot_bgcolor='#161b22', + font=dict(color='#e6edf3')) return fig +def make_vol(df, ticker): + if not PLOTLY_OK: return None + fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.05, + row_heights=[0.6, 0.4], subplot_titles=('ATR %', 'Volume Ratio')) + fig.add_trace(go.Scatter(x=df.index, y=df['ATR_pct'], line=dict(color='#FF9800', width=1.5), fill='tozeroy'), row=1, col=1) + fig.add_trace(go.Scatter(x=df.index, y=df['VR'], line=dict(color='#9C27B0', width=1.5)), row=2, col=1) + fig.add_hline(y=1.0, line_dash="dash", line_color="gray", row=2, col=1) + fig.update_layout(title=f'{ticker} Volatility & Volume', template='plotly_dark', + height=500, paper_bgcolor='#0d1117', plot_bgcolor='#161b22', + font=dict(color='#e6edf3')) + return fig -# ─── Portfolio Optimization ────────────────────────────────────────────────── - -def optimize_portfolio(tickers_str: str, period: str = "1y") -> Tuple: - """Mean-variance portfolio optimization with efficient frontier.""" - tickers = [t.strip().upper() for t in tickers_str.split(',') if t.strip()] - - if len(tickers) < 2: - return None, None, None, "Enter at least 2 tickers (comma-separated)." +def make_adx(df, ticker): + if not PLOTLY_OK: return None + fig = go.Figure() + fig.add_trace(go.Scatter(x=df.index, y=df['plus_DI'], line=dict(color='#00C853', width=1), name='+DI')) + fig.add_trace(go.Scatter(x=df.index, y=df['minus_DI'], line=dict(color='#FF5252', width=1), name='-DI')) + fig.add_trace(go.Scatter(x=df.index, y=df['ADX'], line=dict(color='#2196F3', width=2), name='ADX')) + fig.add_hline(y=25, line_dash="dash", line_color="gray") + fig.update_layout(title=f'{ticker} ADX Trend Strength', template='plotly_dark', + height=400, paper_bgcolor='#0d1117', plot_bgcolor='#161b22', + font=dict(color='#e6edf3')) + return fig +def make_dist(r, ticker): + if not PLOTLY_OK: return None + fig = go.Figure() + fig.add_trace(go.Histogram(x=r, nbinsx=50, marker_color='#2196F3', opacity=0.7, name='Returns')) + mu, sig = r.mean(), r.std() + x_range = np.linspace(r.min(), r.max(), 100) try: - data = yf.download(tickers, period=period)['Close'] - if isinstance(data, pd.Series): - data = data.to_frame() - - data = data.dropna() - - if data.empty: - return None, None, None, "No overlapping data for these tickers." - - returns = data.pct_change().dropna() - mean_returns = returns.mean() * 252 - cov_matrix = returns.cov() * 252 - - n_assets = len(tickers) - - # === Efficient frontier via random portfolios === - n_portfolios = 5000 - results = np.zeros((3, n_portfolios)) - weights_record = [] - - np.random.seed(42) - for i in range(n_portfolios): - w = np.random.random(n_assets) - w /= w.sum() - weights_record.append(w) - portfolio_return = np.sum(w * mean_returns) - portfolio_std = np.sqrt(w @ cov_matrix @ w) - results[0, i] = portfolio_std - results[1, i] = portfolio_return - results[2, i] = portfolio_return / portfolio_std # Sharpe - - max_sharpe_idx = np.argmax(results[2]) - max_sharpe_weights = weights_record[max_sharpe_idx] - - # === Optimization: maximize Sharpe === - from scipy.optimize import minimize - - def neg_sharpe(w): - port_return = np.sum(w * mean_returns) - port_std = np.sqrt(w @ cov_matrix @ w) - return -port_return / port_std if port_std > 0 else 0 - - constraints = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1}) - bounds = tuple((0, 0.5) for _ in range(n_assets)) - init_guess = np.ones(n_assets) / n_assets - - opt_result = minimize(neg_sharpe, init_guess, method='SLSQP', - bounds=bounds, constraints=constraints) - - opt_weights = opt_result.x if opt_result.success else max_sharpe_weights - opt_return = np.sum(opt_weights * mean_returns) - opt_std = np.sqrt(opt_weights @ cov_matrix @ opt_weights) - opt_sharpe = opt_return / opt_std if opt_std > 0 else 0 + from scipy import stats as sp_stats + normal_pdf = len(r)*(x_range[1]-x_range[0])*sp_stats.norm.pdf(x_range, mu, sig) + fig.add_trace(go.Scatter(x=x_range, y=normal_pdf, mode='lines', + line=dict(color='#FF5252', dash='dash'), name='Normal')) + except: + pass + fig.add_vline(x=mu, line_color='#00C853', line_dash='dash', annotation_text=f'Mean: {mu*100:.2f}%') + fig.add_vline(x=np.percentile(r,5), line_color='#FF5252', line_dash='dot', annotation_text=f'VaR95') + fig.update_layout(title=f'{ticker} Return Distribution', xaxis_title='Daily Return', + yaxis_title='Frequency', height=400, template='plotly_dark', + paper_bgcolor='#0d1117', plot_bgcolor='#161b22', font=dict(color='#e6edf3')) + return fig - # === Efficient frontier chart === +# ═══════════════════════════════════════════════════════════════ +# PORTFOLIO OPTIMIZATION +# ═══════════════════════════════════════════════════════════════ +def optimize_portfolio(tickers, period="1y"): + if yf is None or pd is None or np is None: + return None, None, "Libraries unavailable." + ts = [t.strip().upper() for t in tickers.split(',') if t.strip()] + if len(ts) < 2: + return None, None, "Enter at least 2 tickers." + data = {} + errs = [] + for t in ts: + df, err = fetch_data(t, period) + if err: + errs.append(err) + elif df is not None and len(df) > 30: + data[t] = df['Close'] + if len(data) < 2: + return None, None, f"Could not fetch enough data. Errors: {'; '.join(errs[:3])}" + prices = pd.DataFrame(data).dropna() + returns = prices.pct_change().dropna() + if len(returns) < 30: + return None, None, "Need more aligned data." + mu = returns.mean() * 252 + sigma = returns.cov() * 252 + n = len(mu) + best_sh = -999 + best_w = np.ones(n)/n + np.random.seed(42) + for _ in range(5000): + w = np.random.dirichlet(np.ones(n)*0.5) + w = np.clip(w, 0, 0.4) + 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))) + # Frontier + ws = np.random.dirichlet(np.ones(n)*0.5, 3000) + ws = np.clip(ws, 0, 0.4) + 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) + if PLOTLY_OK: fig = go.Figure() - fig.add_trace(go.Scatter( - x=results[0], y=results[1], - mode='markers', - marker=dict( - size=5, - color=results[2], - colorscale='Viridis', - colorbar=dict(title='Sharpe Ratio'), - showscale=True - ), - name='Portfolios', - text=[f"Sharpe: {s:.3f}" for s in results[2]], - hovertemplate='Risk: %{x:.3f}
Return: %{y:.3f}' - )) - - fig.add_trace(go.Scatter( - x=[opt_std], y=[opt_return], - mode='markers', - marker=dict(size=15, color='red', symbol='star'), - name=f'Optimal (Sharpe: {opt_sharpe:.3f})' - )) - - equal_weights = np.ones(n_assets) / n_assets - eq_return = np.sum(equal_weights * mean_returns) - eq_std = np.sqrt(equal_weights @ cov_matrix @ equal_weights) - fig.add_trace(go.Scatter( - x=[eq_std], y=[eq_return], - mode='markers', - marker=dict(size=12, color='orange', symbol='diamond'), - name=f'Equal Weight' - )) - - fig.update_layout( - template='plotly_dark', - title=f"Efficient Frontier — {len(tickers)} Assets", - height=600, - xaxis_title="Risk (Annualized Std Dev)", - yaxis_title="Return (Annualized)", - hovermode='closest', - margin=dict(l=40, r=40, t=60, b=40), - ) - - # Weights table - weights_df = pd.DataFrame({ - 'Ticker': tickers, - 'Optimal Weight': [f"{w:.2%}" for w in opt_weights], - 'Equal Weight': [f"{1/n_assets:.2%}"] * n_assets, - 'Difference': [f"{(w - 1/n_assets):.2%}" for w in opt_weights] - }) - - # Portfolio risk metrics - opt_returns_series = returns @ opt_weights - rf = 0.05 / 252 - port_sharpe = np.sqrt(252) * (opt_returns_series.mean() - rf) / opt_returns_series.std() - port_max_dd = (1 + opt_returns_series).cumprod().div((1 + opt_returns_series).cumprod().cummax()) - 1 - max_drawdown = port_max_dd.min() - - port_metrics = { - "Expected Return": f"{opt_return:.2%}", - "Risk (Std Dev)": f"{opt_std:.2%}", - "Sharpe Ratio": f"{port_sharpe:.3f}", - "Max Drawdown": f"{max_drawdown:.2%}", - "Correlation Matrix": cov_matrix.corr().round(3).to_html(), - } - - return fig, weights_df, port_metrics, "" - - except Exception as e: - return None, None, None, f"Optimization error: {str(e)}" - - -# ─── K2 Think Analysis ─────────────────────────────────────────────────────── - -def analyze_stock_with_ai(ticker: str) -> str: - """K2 Think V2 deep analysis of a single stock.""" - df, data, error = fetch_stock_data(ticker) - if error: - return f"❌ {error}" - - if data is None: - return "❌ Could not retrieve data." - - info = data.get("info", {}) - latest = data.get("latest", {}) - risk = data.get("risk", {}) + 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')), name='Portfolios')) + fig.add_trace(go.Scatter(x=[pv], y=[pr], mode='markers+text', + marker=dict(size=18, color='#FF5252', symbol='star'), + text=['Optimal'], textposition='top center', name='Optimal')) + fig.add_trace(go.Scatter(x=[eq_v], y=[eq_r], mode='markers+text', + marker=dict(size=14, color='#FF9800', symbol='diamond'), + text=['Equal'], textposition='bottom center', name='Equal Weight')) + fig.update_layout(title='Efficient Frontier', xaxis_title='Volatility', + yaxis_title='Return', template='plotly_dark', height=550, + paper_bgcolor='#0d1117', plot_bgcolor='#161b22', font=dict(color='#e6edf3')) + else: + fig = None + wdf = pd.DataFrame({'Ticker': list(data.keys()), + 'Optimal (%)': np.round(best_w*100, 2), + 'Equal (%)': np.round(eq_w*100, 2)}) + # Correlation matrix + corr = returns.corr() + corr_fig = None + if PLOTLY_OK: + corr_fig = go.Figure(data=go.Heatmap(z=corr.values, x=corr.columns, y=corr.columns, + colorscale='RdBu', zmid=0, text=np.round(corr.values,2), texttemplate='%{text:.2f}', + colorbar=dict(title='Correlation'))) + corr_fig.update_layout(title='Correlation Matrix', template='plotly_dark', height=450, + paper_bgcolor='#0d1117', plot_bgcolor='#161b22', font=dict(color='#e6edf3')) + md = f"""## 📊 Portfolio Optimization Results + +**Tickers:** {', '.join(list(data.keys()))} + +### Optimal (Max Sharpe) +| Metric | Value | +|--------|-------| +| Expected Return | {pr*100:.1f}% | +| Volatility | {pv*100:.1f}% | +| Sharpe | {best_sh:.2f} | - data_summary = f""" -Ticker: {ticker} -Company: {info.get('name', ticker)} -Sector: {info.get('sector', 'N/A')} | Industry: {info.get('industry', 'N/A')} -Market Cap: {_fmt_billions(info.get('market_cap'))} -P/E: {info.get('pe_ratio', 'N/A')} | Forward P/E: {info.get('forward_pe', 'N/A')} -Beta: {info.get('beta', 'N/A')} -52-Week Range: ${info.get('52w_low', 'N/A')} — ${info.get('52w_high', 'N/A')} +### Equal Weight Benchmark +| Metric | Value | +|--------|-------| +| Expected Return | {eq_r*100:.1f}% | +| Volatility | {eq_v*100:.1f}% | +| Sharpe | {eq_r/(eq_v+1e-10):.2f} | + +### Improvements vs Equal Weight +| Metric | Improvement | +|--------|-------------| +| Sharpe | {((best_sh/(eq_r/(eq_v+1e-10))-1)*100):+.1f}% | +| Return | {((pr/eq_r-1)*100):+.1f}% | +| Risk | {((1-pv/eq_v)*100):+.1f}% | + +### Optimal Weights +{wdf.to_markdown(index=False)} """ - - technical_summary = f""" -Current Price: {latest.get('price', 'N/A')} ({latest.get('change_pct', 'N/A')}) -RSI (14): {latest.get('rsi', 'N/A')} -MACD: {latest.get('macd', 'N/A')} vs Signal {latest.get('macd_signal', 'N/A')} -Stochastic K: {latest.get('stoch_k', 'N/A')} -VWAP: {latest.get('vwap', 'N/A')} -SMA 20/50/200: {latest.get('sma_20', 'N/A')} / {latest.get('sma_50', 'N/A')} / {latest.get('sma_200', 'N/A')} -Volume Ratio: {latest.get('vol_ratio', 'N/A')} (vs 20-day avg) -ATR: ${latest.get('atr', 'N/A')} -Alpha Score: {latest.get('alpha_score', 'N/A')} - -RISK METRICS: -Sharpe: {risk.get('sharpe', 'N/A')} | Sortino: {risk.get('sortino', 'N/A')} -VaR (95%): {risk.get('var_95', 'N/A')} | CVaR (95%): {risk.get('cvar_95', 'N/A')} -Max Drawdown: {risk.get('max_drawdown', 'N/A')} | Calmar: {risk.get('calmar', 'N/A')} -Skewness: {risk.get('skewness', 'N/A')} | Kurtosis: {risk.get('kurtosis', 'N/A')} + return fig, corr_fig, wdf, md + +# ═══════════════════════════════════════════════════════════════ +# PAIRS TRADING +# ═══════════════════════════════════════════════════════════════ +def analyze_pair(ticker_a, ticker_b, period="1y"): + df_a, _ = fetch_data(ticker_a, period) + df_b, _ = fetch_data(ticker_b, period) + if df_a is None or df_b is None: + return None, None, "Could not fetch data for one or both tickers." + prices = pd.DataFrame({ticker_a: df_a['Close'], ticker_b: df_b['Close']}).dropna() + if len(prices) < 30: + return None, None, "Insufficient aligned data." + # Spread + spread = prices[ticker_a] - prices[ticker_b] + spread_norm = (spread - spread.mean()) / spread.std() + # Cointegration test (Engle-Granger style) + from numpy import polyfit + beta = polyfit(prices[ticker_b], prices[ticker_a], 1)[0] + hedge_ratio = beta + spread_hedged = prices[ticker_a] - hedge_ratio * prices[ticker_b] + spread_hedged_norm = (spread_hedged - spread_hedged.mean()) / spread_hedged.std() + # Half-life (Ornstein-Uhlenbeck) + lag_spread = spread_hedged.shift(1) + delta_spread = spread_hedged.diff() + valid = delta_spread.dropna().index + y = delta_spread.loc[valid] + x = lag_spread.loc[valid] - spread_hedged.mean() + theta = -np.polyfit(x, y, 1)[0] + half_life = np.log(2)/theta if theta > 0 else float('inf') + # Z-score signal + z = spread_hedged_norm.iloc[-1] + signal = 'SHORT SPREAD' if z > 2 else 'LONG SPREAD' if z < -2 else 'NO SIGNAL' + # Plot + if PLOTLY_OK: + fig = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.05, + subplot_titles=(f'{ticker_a} vs {ticker_b} (Price)', 'Normalized Spread', 'Z-Score')) + fig.add_trace(go.Scatter(x=prices.index, y=prices[ticker_a], line=dict(color='#2196F3', width=1.5), name=ticker_a), row=1, col=1) + fig.add_trace(go.Scatter(x=prices.index, y=prices[ticker_b], line=dict(color='#FF9800', width=1.5), name=ticker_b), row=1, col=1) + fig.add_trace(go.Scatter(x=prices.index, y=spread_norm, line=dict(color='#00C853', width=1.5), fill='tozeroy'), row=2, col=1) + fig.add_trace(go.Scatter(x=prices.index, y=spread_hedged_norm, line=dict(color='#9C27B0', width=1.5)), row=3, col=1) + fig.add_hline(y=2, line_dash="dash", line_color="#FF5252", row=3, col=1) + fig.add_hline(y=-2, line_dash="dash", line_color="#00C853", row=3, col=1) + fig.add_hline(y=0, line_dash="dot", line_color="gray", row=3, col=1) + fig.update_layout(title=f'Pairs Trading: {ticker_a} / {ticker_b}', template='plotly_dark', + height=750, paper_bgcolor='#0d1117', plot_bgcolor='#161b22', font=dict(color='#e6edf3')) + else: + fig = None + # Scatter + if PLOTLY_OK: + scat = go.Figure() + scat.add_trace(go.Scatter(x=prices[ticker_b], y=prices[ticker_a], mode='markers', + marker=dict(size=4, color=np.arange(len(prices)), colorscale='Viridis', showscale=True), + name='Price Path')) + # Regression line + x_range = np.linspace(prices[ticker_b].min(), prices[ticker_b].max(), 100) + intercept = np.polyfit(prices[ticker_b], prices[ticker_a], 1)[1] + y_range = hedge_ratio * x_range + intercept + scat.add_trace(go.Scatter(x=x_range, y=y_range, mode='lines', + line=dict(color='#FF5252', dash='dash'), name=f'OLS (β={hedge_ratio:.2f})')) + scat.update_layout(title=f'Price Relationship (β={hedge_ratio:.2f})', template='plotly_dark', + xaxis_title=ticker_b, yaxis_title=ticker_a, height=450, + paper_bgcolor='#0d1117', plot_bgcolor='#161b22', font=dict(color='#e6edf3')) + else: + scat = None + md = f"""## 🔗 Pairs Trading Analysis: {ticker_a} vs {ticker_b} + +### Statistics +| Metric | Value | +|--------|-------| +| Hedge Ratio (β) | {hedge_ratio:.3f} | +| Spread Mean | ${spread_hedged.mean():.2f} | +| Spread Std | ${spread_hedged.std():.2f} | +| Current Z-Score | {z:.2f} | +| Half-Life | {half_life:.1f} days | + +### Signal +| Current Z | Interpretation | Action | +|-----------|----------------|--------| +| {z:.2f} | {'Overextended' if abs(z)>2 else 'Normal range'} | **{signal}** | + +### Trading Rules +- **Enter Long Spread** when Z < -2 (buy {ticker_a}, short {ticker_b}) +- **Enter Short Spread** when Z > +2 (short {ticker_a}, buy {ticker_b}) +- **Exit** when Z crosses 0 +- **Stop Loss** when |Z| > 3.5 """ + return fig, scat, md - prompt = f"""You are an elite quantitative analyst at a top-tier hedge fund managing $5B AUM. - -Analyze {ticker} with deep, step-by-step reasoning. - -{data_summary} - -{technical_summary} - -Provide a comprehensive analysis following this structure: - -## 1. Executive Summary (3 bullets) -- One sentence on the overall technical picture -- Key risk factor to watch -- Most important catalyst - -## 2. Technical Analysis Deep Dive -- Interpret RSI in context (oversold/overbought/neutral and what it means) -- MACD signal analysis (crossover, divergence, histogram momentum) -- Moving average relationship (golden cross, death cross, or trending) -- Volume analysis (accumulation vs distribution) -- Bollinger Band position and squeeze/expansion -- Alpha Score interpretation - -## 3. Risk Assessment -- Current volatility regime (low/medium/high) -- VaR interpretation and downside scenarios -- Drawdown risk and recovery potential -- Tail risk assessment (skewness/kurtosis) - -## 4. Alpha Signal -- Direction: BULLISH / NEUTRAL / BEARISH -- Confidence: low / medium / high -- Time horizon: 1-week / 1-month / 3-month - -## 5. Actionable Trade Idea -- Entry: specific price level with reasoning -- Stop-loss: specific price level with reasoning -- Target: specific price level with reasoning -- Position sizing recommendation (% of portfolio) -- Risk-reward ratio - -## 6. Catalyst Watch -- Upcoming events that could move the stock (earnings, economic reports, sector events) -- Bull case catalyst -- Bear case catalyst - -Format your response with clear markdown headers. Use specific numbers, not vague statements. Think step-by-step.""" - - messages = [ - {"role": "system", "content": "You are an elite quantitative analyst. Always provide specific numbers, clear reasoning, and actionable insights. Use markdown formatting."}, - {"role": "user", "content": prompt} - ] - +# ═══════════════════════════════════════════════════════════════ +# OPTIONS PRICING (Black-Scholes) +# ═══════════════════════════════════════════════════════════════ +def black_scholes(S, K, T, r, sigma, option_type='call'): + from math import log, sqrt, exp try: - result = call_k2(messages, temperature=0.3, max_tokens=3072) - return result - except Exception as e: - # Try non-streaming fallback + d1 = (log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*sqrt(T)) + d2 = d1 - sigma*sqrt(T) try: - return call_k2_non_streaming(messages, temperature=0.3, max_tokens=3072) + from scipy.stats import norm + nd1 = norm.cdf(d1) + nd2 = norm.cdf(d2) + npdf_d1 = norm.pdf(d1) except: - return f"❌ AI Analysis failed: {str(e)}" - - -def analyze_portfolio_with_ai(tickers_str: str) -> str: - """K2 Think V2 portfolio analysis.""" - tickers = [t.strip().upper() for t in tickers_str.split(',') if t.strip()] - - if len(tickers) < 2: - return "Enter at least 2 tickers for portfolio analysis." - - try: - data = yf.download(tickers, period="1y")['Close'] - if isinstance(data, pd.Series): - data = data.to_frame() - data = data.dropna() - returns = data.pct_change().dropna() - - # Portfolio metrics - equal_weights = np.ones(len(tickers)) / len(tickers) - port_returns = returns @ equal_weights - port_cum = (1 + port_returns).cumprod() - - annual_return = port_returns.mean() * 252 - annual_vol = port_returns.std() * np.sqrt(252) - sharpe = (annual_return - 0.05) / annual_vol if annual_vol > 0 else 0 - max_dd = ((1 + port_returns).cumprod().div((1 + port_returns).cumprod().cummax()) - 1).min() - - # Individual stock stats - stock_stats = [] - for t in tickers: - r = returns[t] - stock_stats.append({ - "ticker": t, - "ann_return": f"{r.mean() * 252:.2%}", - "ann_vol": f"{r.std() * np.sqrt(252):.2%}", - "sharpe": f"{(r.mean() * 252 - 0.05) / (r.std() * np.sqrt(252)):.2f}" if r.std() > 0 else "N/A", - "max_dd": f"{(1 + r).cumprod().div((1 + r).cumprod().cummax()).min():.2%}", - }) - - corr_matrix = returns.corr().round(3) - - # Current prices - prices = {t: f"${data[t].iloc[-1]:.2f}" for t in tickers} - - summary = f""" -PORTFOLIO: {', '.join(tickers)} -Period: 1 year - -EQUAL-WEIGHT METRICS: -Annualized Return: {annual_return:.2%} -Annualized Volatility: {annual_vol:.2%} -Sharpe Ratio: {sharpe:.3f} -Max Drawdown: {max_dd:.2%} - -STOCK STATS: -{json.dumps(stock_stats, indent=2)} - -CORRELATION MATRIX: -{corr_matrix.to_json(indent=2)} - -CURRENT PRICES: -{json.dumps(prices, indent=2)} + # Approximate normal CDF + def approx_cdf(x): + return 0.5 * (1 + math.erf(x / math.sqrt(2))) + nd1 = approx_cdf(d1) + nd2 = approx_cdf(d2) + npdf_d1 = (1/math.sqrt(2*math.pi)) * math.exp(-0.5*d1**2) + if option_type == 'call': + price = S*nd1 - K*exp(-r*T)*nd2 + else: + price = K*exp(-r*T)*(1-nd2) - S*(1-nd1) + delta = nd1 if option_type=='call' else nd1 - 1 + gamma = npdf_d1 / (S*sigma*sqrt(T)) + theta = -(S*npdf_d1*sigma)/(2*sqrt(T)) - r*K*exp(-r*T)*nd2 if option_type=='call' else -(S*npdf_d1*sigma)/(2*sqrt(T)) + r*K*exp(-r*T)*(1-nd2) + vega = S*npdf_d1*sqrt(T) + rho = K*T*exp(-r*T)*nd2 if option_type=='call' else -K*T*exp(-r*T)*(1-nd2) + return {'price': price, 'delta': delta, 'gamma': gamma, 'theta': theta/252, + 'vega': vega/100, 'rho': rho/100, 'd1': d1, 'd2': d2} + except Exception as e: + return {'error': str(e)} + +def analyze_options(ticker, strike_pct, days, rfr, vol_override, option_type): + df, info = fetch_data(ticker, "6mo") + if df is None: + return None, None, "Could not fetch data." + df = calc_indicators(df) + S = df['Close'].iloc[-1] + K = S * (strike_pct/100) + T = days / 365 + # Volatility + if vol_override: + sigma = vol_override / 100 + else: + sigma = df['Ret'].dropna().std() * np.sqrt(252) + # Risk-free rate + r = rfr / 100 + bs = black_scholes(S, K, T, r, sigma, option_type.lower()) + if 'error' in bs: + return None, None, f"BS Error: {bs['error']}" + # P/L table + pct_changes = np.arange(-30, 31, 5) + pl_data = [] + for pct in pct_changes: + new_S = S * (1 + pct/100) + new_bs = black_scholes(new_S, K, T - 1/365, r, sigma, option_type.lower()) + pl = (new_bs['price'] - bs['price']) * 100 # per contract + pl_data.append({'Price Change %': f'{pct:+d}%', 'Stock Price': f'${new_S:.2f}', + 'Option Price': f'${new_bs["price"]:.2f}', 'P/L (per 100)': f'${pl:+.2f}'}) + pl_df = pd.DataFrame(pl_data) + # Greeks chart + if PLOTLY_OK: + strikes = np.linspace(S*0.7, S*1.3, 50) + greeks = {'price': [], 'delta': [], 'gamma': [], 'theta': [], 'vega': []} + for st in strikes: + res = black_scholes(S, st, T, r, sigma, option_type.lower()) + for k in greeks: + greeks[k].append(res.get(k, 0)) + fig = make_subplots(rows=2, cols=3, subplot_titles=('Price', 'Delta', 'Gamma', 'Theta (daily)', 'Vega', 'P/L at Expiry'), + vertical_spacing=0.12, horizontal_spacing=0.08) + colors = ['#2196F3', '#00C853', '#FF9800', '#FF5252', '#9C27B0', '#673AB7'] + for i, (k, v) in enumerate(greeks.items()): + row, col = (i//3)+1, (i%3)+1 + fig.add_trace(go.Scatter(x=strikes, y=v, line=dict(color=colors[i], width=2), name=k), row=row, col=col) + fig.add_vline(x=S, line_dash='dash', line_color='gray', row=row, col=col) + # P/L at expiry + expiry_payoff = [max(s-K,0) if option_type.lower()=='call' else max(K-s,0) for s in strikes] + pl_expiry = [p - bs['price'] for p in expiry_payoff] + fig.add_trace(go.Scatter(x=strikes, y=pl_expiry, line=dict(color='#673AB7', width=2), name='P/L Expiry'), row=2, col=3) + fig.add_hline(y=0, line_dash='dot', line_color='gray', row=2, col=3) + fig.update_layout(title=f'{ticker} {option_type.title()} Greeks (S=${S:.2f}, K=${K:.2f}, T={days}d, σ={sigma*100:.1f}%)', + template='plotly_dark', height=650, + paper_bgcolor='#0d1117', plot_bgcolor='#161b22', font=dict(color='#e6edf3')) + else: + fig = None + md = f"""## 📐 {ticker} {option_type.title()} Option Analysis + +### Parameters +| Parameter | Value | +|-----------|-------| +| Spot Price (S) | ${S:.2f} | +| Strike (K) | ${K:.2f} ({strike_pct:.0f}% of spot) | +| Time to Expiry (T) | {days} days ({T:.3f} years) | +| Risk-Free Rate | {r*100:.2f}% | +| Implied Volatility | {sigma*100:.1f}% | + +### Black-Scholes Price & Greeks +| Greek | Value | Interpretation | +|-------|-------|----------------| +| **Price** | ${bs['price']:.3f} | Option fair value | +| **Delta** | {bs['delta']:.4f} | {abs(bs['delta'])*100:.1f}% hedge ratio | +| **Gamma** | {bs['gamma']:.6f} | Delta sensitivity per $1 move | +| **Theta** | ${bs['theta']:.4f}/day | Daily time decay | +| **Vega** | ${bs['vega']:.4f} | Price change per 1% vol move | +| **Rho** | ${bs['rho']:.4f} | Price change per 1% rate move | +| **d1** | {bs['d1']:.4f} | Distance to exercise (std dev) | +| **d2** | {bs['d2']:.4f} | Risk-neutral probability adj | + +### P/L Scenario Table (per 100 contracts) +{pl_df.to_markdown(index=False)} """ - - prompt = f"""You are a portfolio manager at a multi-strategy hedge fund overseeing $500M AUM. - -Analyze this portfolio with institutional-grade rigor. - -{summary} - -Provide a comprehensive analysis: - -## 1. Portfolio Health Score (0-100) -- Quantitative rating with letter grade (A+ to F) -- Key drivers of the score -- What would improve the score - -## 2. Concentration Risk Analysis -- Sector/style concentration if evident -- Single-name risk assessment -- Diversification benefit analysis - -## 3. Correlation Matrix Insights -- Most correlated pair and implications -- Least correlated / negative correlated pair -- Hedging opportunities from correlation structure - -## 4. Risk Budget Analysis -- Contribution to portfolio risk by each position -- Which stock drives most of the volatility -- Tail risk concentration - -## 5. Rebalancing Recommendations -- Specific % adjustments for each ticker -- Reasoning for each adjustment -- Expected impact on Sharpe - -## 6. Hedging Strategy -- Specific options or ETFs to hedge tail risk -- Cost of hedging vs benefit -- When to implement the hedge - -## 7. Forward-Looking Assessment -- Expected 6-month return and volatility -- Key risks to the outlook -- Scenario analysis (bull/base/bear) - -Use specific numbers and clear markdown formatting.""" - - messages = [ - {"role": "system", "content": "You are an elite portfolio manager. Provide specific, quantitative, actionable advice with clear reasoning."}, - {"role": "user", "content": prompt} - ] - + return fig, pl_df, md + +# ═══════════════════════════════════════════════════════════════ +# MACRO ANALYSIS +# ═══════════════════════════════════════════════════════════════ +def get_macro_data(): + macros = {} + for t, name in [('^GSPC','S&P 500'),('^IXIC','Nasdaq'),('^TNX','10Y Treasury'), + ('GC=F','Gold'),('CL=F','Oil'),('EURUSD=X','EUR/USD'), + ('DX-Y.NYB','DXY Dollar'),('BTC-USD','Bitcoin')]: try: - return call_k2(messages, temperature=0.3, max_tokens=3072) + df = yf.Ticker(t).history(period='1mo') + if not df.empty: + macros[name] = {'price': df['Close'].iloc[-1], 'change_1m': (df['Close'].iloc[-1]/df['Close'].iloc[0]-1)*100, + 'high': df['High'].max(), 'low': df['Low'].min()} except: - return call_k2_non_streaming(messages, temperature=0.3, max_tokens=3072) - - except Exception as e: - return f"❌ Portfolio analysis error: {str(e)}" - - -def chat_with_k2(message: str, temperature: float = 0.5) -> str: - """Direct chat with K2 Think V2.""" - if not message.strip(): - return "Please enter a question." - - messages = [ - {"role": "system", "content": "You are a helpful AI assistant specializing in quantitative finance, trading strategies, risk management, and financial markets. Provide detailed, accurate, and educational responses."}, - {"role": "user", "content": message} - ] - - try: - result = call_k2(messages, temperature=temperature, max_tokens=2048) - return result - except: - return call_k2_non_streaming(messages, temperature=temperature, max_tokens=2048) - - -# ─── Helpers ───────────────────────────────────────────────────────────────── - -def _fmt_billions(val) -> str: - if val is None: - return "N/A" - if val >= 1e12: - return f"${val/1e12:.2f}T" - elif val >= 1e9: - return f"${val/1e9:.2f}B" - elif val >= 1e6: - return f"${val/1e6:.2f}M" - return f"${val:,.0f}" - - -# ─── Gradio UI Handlers ────────────────────────────────────────────────────── - -def on_analyze(ticker: str): - """Handle single stock analysis.""" - if not ticker.strip(): - return None, None, None, "Please enter a ticker symbol." + pass + return macros +# ═══════════════════════════════════════════════════════════════ +# UI FUNCTIONS +# ═══════════════════════════════════════════════════════════════ +def analyze_stock(ticker, market_preset, period, interval): ticker = ticker.strip().upper() - df, data, error = fetch_stock_data(ticker) - - if error: - return None, None, None, f"❌ {error}" - - chart = make_candlestick_chart(df, ticker) - returns_chart = make_returns_chart(df) - - # Summary table - info = data.get("info", {}) - latest = data.get("latest", {}) - risk = data.get("risk", {}) - - summary = f""" -### {info.get('name', ticker)} ({ticker}) - + if not ticker: + return [None]*7 + ["Enter a ticker."] + suffix = MARKET_PRESETS.get(market_preset, {}).get('suffix', '') + if suffix and not ticker.endswith(suffix.split('.')[-1]): + ticker = ticker + suffix + df, info = fetch_data(ticker, period) + if df is None: + return [None]*7 + [f"Error: {info}"] + df = calc_indicators(df) + sg = calc_signals(df) + rk = calc_risk(df) + if not rk: + return [None]*7 + ["Need more data."] + 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 + # Charts + c1 = make_candlestick(df, ticker, market_preset) + c2 = make_macd(df, ticker) + c3 = make_stoch(df, ticker) + c4 = make_vol(df, ticker) + c5 = make_adx(df, ticker) + c6 = make_dist(df['Ret'].dropna(), ticker) + # Summary + mkt_info = "" + if info: + mkt_info = f""" +| Info | Value | +|------|-------| +| Name | {info.get('longName', ticker)} | +| Sector | {info.get('sector', 'N/A')} | +| Industry | {info.get('industry', 'N/A')} | +| Market Cap | {info.get('marketCap', 'N/A'):,}" if info.get('marketCap') else "" + mkt_info += f""" +| 52W High | ${info.get('fiftyTwoWeekHigh', 'N/A'):.2f}" if info.get('fiftyTwoWeekHigh') else "" + mkt_info += f""" +| 52W Low | ${info.get('fiftyTwoWeekLow', 'N/A'):.2f}" if info.get('fiftyTwoWeekLow') else "" + mkt_info += f""" +| P/E | {info.get('trailingPE', 'N/A')} |""" if info.get('trailingPE') else "" + md = f"""# {ticker} — {sg['dir']} {sg['strength']} (Score: {sg['score']}/100) + +**Price:** ${l['Close']:.2f} | **Change:** {ch:+.2f}% | **Period:** {period} +{mkt_info} + +## Signal Dashboard +| Indicator | Value | Signal | +|-----------|-------|--------| +| RSI (14) | {l['RSI']:.1f} | {'🟢 Deeply Oversold' if l['RSI']<30 else '🔴 Deeply Overbought' if l['RSI']>70 else '⚪ Neutral'} | +| MACD | {l['MACD']:.3f} | {'🟢 Bullish' if l['MACD']>l['MACDS'] else '🔴 Bearish'} | +| Bollinger | {l['BBP']:.1%} | {'🔴 Upper' if l['BBP']>0.8 else '🟢 Lower' if l['BBP']<0.2 else '⚪ Mid'} | +| VWAP | {'🟢 Above' if l['Close']>l['VWAP'] else '🔴 Below'} | {'🟢 Bullish' if l['Close']>l['VWAP'] else '🔴 Bearish'} | +| Stoch %K | {l['Stoch_K']:.1f} | {'🟢 Oversold' if l['Stoch_K']<20 else '🔴 Overbought' if l['Stoch_K']>80 else '⚪ Neutral'} | +| ADX | {l['ADX']:.1f} | {sg['adx_trend']} | +| Volume | {l['VR']:.1f}x avg | {'🔥 Heavy' if l['VR']>2 else '⬆️ Above Avg' if l['VR']>1.5 else '⚪ Normal'} | +| Trend | {sg['trend'].upper()} | — | +| Momentum | {sg['mom']} | — | +| Volatility | {sg['vol']} | — | +| Ichimoku Cloud | {sg['ichimoku']} | — | + +## Risk Metrics | Metric | Value | |--------|-------| -| **Price** | {latest.get('price', 'N/A')} | -| **Change** | {latest.get('change_pct', 'N/A')} | -| **RSI** | {latest.get('rsi', 'N/A')} | -| **MACD/Signal** | {latest.get('macd', 'N/A')} / {latest.get('macd_signal', 'N/A')} | -| **Alpha Score** | {latest.get('alpha_score', 'N/A')} | -| **SMA 20/50/200** | {latest.get('sma_20', 'N/A')} / {latest.get('sma_50', 'N/A')} / {latest.get('sma_200', 'N/A')} | -| **VWAP** | {latest.get('vwap', 'N/A')} | -| **Volume Ratio** | {latest.get('vol_ratio', 'N/A')} | -| **ATR** | ${latest.get('atr', 'N/A')} | - -### Risk Metrics -| Metric | Value | -|--------|-------| -| **Sharpe** | {risk.get('sharpe', 'N/A')} | -| **Sortino** | {risk.get('sortino', 'N/A')} | -| **VaR (95%)** | {risk.get('var_95', 'N/A')} | -| **CVaR (95%)** | {risk.get('cvar_95', 'N/A')} | -| **Max Drawdown** | {risk.get('max_drawdown', 'N/A')} | -| **Calmar** | {risk.get('calmar', 'N/A')} | +| Ann. Return | {rk['ar']*100:.1f}% | +| Ann. Volatility | {rk['av']*100:.1f}% | +| Vol Regime | {rk['vol_regime'].upper()} | +| Sharpe | {rk['sh']:.2f} | +| Sortino | {rk['so']:.2f} | +| Max Drawdown | {rk['md']*100:.1f}% | +| VaR (95%) | {rk['v95']*100:.2f}% | +| VaR (99%) | {rk['v99']*100:.2f}% | +| CVaR (95%) | {rk['cv95']*100:.2f}% | +| CVaR (99%) | {rk['cv99']*100:.2f}% | +| Calmar | {rk['ca']:.2f} | +| Win Rate | {rk['wr']*100:.1f}% | +| Avg Win | {rk['avg_win']*100:.2f}% | +| Avg Loss | {rk['avg_loss']*100:.2f}% | +| Profit Factor | {rk['pf']:.2f} | +| Skewness | {rk['sk']:.2f} | +| Kurtosis | {rk['ku']:.2f} | +| 63D Rolling Sharpe | {rk['roll_sharpe']:.2f} | """ + return [c1, c2, c3, c4, c5, c6, md, ""] - return chart, returns_chart, summary, "" - - -def on_ai_analysis(ticker: str): - """Run K2 Think V2 analysis.""" - if not ticker.strip(): - return "Please enter a ticker symbol." - result = analyze_stock_with_ai(ticker.strip().upper()) - return result - - -def on_portfolio_optimize(tickers_str: str): - """Handle portfolio optimization.""" - fig, weights_df, metrics, error = optimize_portfolio(tickers_str) - if error: - return None, None, f"❌ {error}" - return fig, weights_df, "" - - -def on_portfolio_ai(tickers_str: str): - """Run K2 Think V2 portfolio analysis.""" - if not tickers_str.strip(): - return "Please enter tickers." - result = analyze_portfolio_with_ai(tickers_str) - return result - - -# ─── Gradio UI ─────────────────────────────────────────────────────────────── - -def build_ui(): - custom_css = """ - .gradio-container { max-width: 1200px !important; margin: 0 auto !important; } - .k2-badge { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 4px 12px; border-radius: 12px; font-size: 0.8em; } - .metric-positive { color: #00ff88; font-weight: bold; } - .metric-negative { color: #ff4444; font-weight: bold; } - footer { visibility: hidden; } - """ - +def ai_analyze_stock(ticker, market_preset, period, interval): + ticker = ticker.strip().upper() + if not ticker: + return "Enter a ticker." + suffix = MARKET_PRESETS.get(market_preset, {}).get('suffix', '') + if suffix and not ticker.endswith(suffix.split('.')[-1]): + ticker = ticker + suffix + df, info = fetch_data(ticker, period) + if df is None: + return f"Error: {info}" + df = calc_indicators(df) + sg = calc_signals(df) + rk = calc_risk(df) + 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 + data_sum = f"""Ticker: {ticker} +Price: ${l['Close']:.2f} (Change: {ch:+.2f}%) +Period: {period} | Data Points: {len(df)} +SMA20: ${l['SMA20']:.2f} | SMA50: ${l['SMA50']:.2f} | SMA200: ${l['SMA200']:.2f} +52W Range: ${df['Low'].min():.2f} - ${df['High'].max():.2f} +ATR: ${l['ATR']:.2f} ({l['ATR_pct']:.1f}% of price) +Volume: {l['Volume']:,.0f} ({l['VR']:.1f}x avg) +""" + tech_sum = f"""RSI: {l['RSI']:.1f} | MACD: {l['MACD']:.3f} vs Signal: {l['MACDS']:.3f} | MACD Hist: {l['MACDH']:.3f} +BB Position: {l['BBP']:.1%} | BB Width: {l['BBW']:.2f} | Stoch %K: {l['Stoch_K']:.1f} +VWAP: ${l['VWAP']:.2f} | ADX: {l['ADX']:.1f} | MFI: {l['MFI']:.1f} +Ichimoku: {sg['ichimoku']} | Cloud: Senkou A={l['ICH_senkou_A']:.2f}, B={l['ICH_senkou_B']:.2f} +Score: {sg['score']}/100 | Direction: {sg['dir']} | Strength: {sg['strength']} +Risk: Sharpe={rk.get('sh',0):.2f}, Vol={rk.get('av',0)*100:.1f}%, MaxDD={rk.get('md',0)*100:.1f}%, VaR95={rk.get('v95',0)*100:.2f}%""" + client = K2ThinkClient() + return client.analyze_market(ticker, market_preset, data_sum, tech_sum, period) + +def ai_portfolio(tickers, period): + fig, corr_fig, wdf, md = optimize_portfolio(tickers, period) + if fig is None: + return f"Error: {md}" + pd_str = f"Tickers: {', '.join(wdf['Ticker'].tolist())}\nWeights: {', '.join([f'{t}: {w:.1f}%' for t,w in zip(wdf['Ticker'], wdf['Optimal (%)'])])}" + corr_str = f"Correlations:\n{wdf['Ticker'].tolist()}" + client = K2ThinkClient() + return client.portfolio_advice(pd_str, corr_str, md, "Current macro: mixed signals, rates elevated, geopolitical uncertainty") + +def ai_macro(): + macros = get_macro_data() + macro_text = "Global Macro Snapshot:\n" + for name, data in macros.items(): + macro_text += f"- {name}: ${data['price']:.2f} (1M change: {data['change_1m']:+.1f}%)\n" + client = K2ThinkClient() + return client.macro_analysis(macro_text) + +def ai_chat(question, temp): + if not question.strip(): + return "Enter a question." + client = K2ThinkClient() + return client.chat([{"role":"user","content":question}], temperature=temp, max_tokens=4096) + +# ═══════════════════════════════════════════════════════════════ +# GRADIO APP — FINANCE DARK THEME +# ═══════════════════════════════════════════════════════════════ +def build_app(): + if gr is None: + raise ImportError(f"Gradio not available. Errors: {_import_errors}") + with gr.Blocks( - title=APP_TITLE, - theme=gr.themes.Soft(primary_hue="blue", secondary_hue="purple"), - css=custom_css + title="AlphaForge x K2 Think V2 — Institutional Quant Platform", + theme=gr.themes.Soft( + primary_hue="blue", + secondary_hue="indigo", + neutral_hue="slate", + font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"] + ), + css=""" + body { background: #0d1117 !important; } + .gradio-container { background: #0d1117 !important; color: #e6edf3 !important; } + .tabitem { background: #161b22 !important; border: 1px solid #30363d !important; border-radius: 12px !important; } + .tab-nav { background: #0d1117 !important; border-bottom: 1px solid #30363d !important; } + .tab-nav button { color: #8b949e !important; background: transparent !important; border: none !important; } + .tab-nav button.selected { color: #58a6ff !important; border-bottom: 2px solid #58a6ff !important; } + .panel { background: #161b22 !important; border: 1px solid #30363d !important; border-radius: 12px !important; } + input, textarea, select { background: #21262d !important; color: #e6edf3 !important; border: 1px solid #30363d !important; } + button.primary { background: linear-gradient(135deg, #1f6feb, #58a6ff) !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 600 !important; } + button.secondary { background: #21262d !important; color: #58a6ff !important; border: 1px solid #30363d !important; border-radius: 8px !important; } + .markdown-body { color: #e6edf3 !important; } + .markdown-body h1 { color: #58a6ff !important; border-bottom: 1px solid #30363d !important; } + .markdown-body h2 { color: #79c0ff !important; } + .markdown-body h3 { color: #a5d6ff !important; } + .markdown-body table { border-color: #30363d !important; } + .markdown-body th { background: #21262d !important; color: #58a6ff !important; } + .markdown-body td { border-color: #30363d !important; } + .markdown-body tr:nth-child(even) { background: #161b22 !important; } + .title-bar { text-align: center; padding: 24px 0; } + .title-bar h1 { font-size: 2.8em; font-weight: 800; margin: 0; background: linear-gradient(90deg, #58a6ff, #a371f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } + .title-bar p { color: #8b949e; font-size: 1.1em; margin-top: 8px; } + .badge-row { text-align: center; margin: 16px 0 24px; } + .badge { display: inline-block; padding: 6px 14px; margin: 4px; border-radius: 20px; font-size: 0.85em; font-weight: 600; } + .badge-api { background: linear-gradient(135deg, #1f6feb, #a371f7); color: white; } + .badge-data { background: #238636; color: white; } + .badge-alpha { background: #8957e5; color: white; } + .k2-status { text-align: center; padding: 8px; margin: 8px 0; border-radius: 8px; font-size: 0.9em; } + .k2-active { background: rgba(35,134,54,0.2); color: #3fb950; border: 1px solid #238636; } + .k2-inactive { background: rgba(209,36,47,0.2); color: #f85149; border: 1px solid #da3633; } + """ ) as demo: - gr.Markdown(f""" - # 🔥 {APP_TITLE} - ### {APP_DESC} - > Powered by **K2 Think V2** (MBZUAI) — State-of-the-art reasoning for financial markets. - > Model compression powered by **Q-TensorFormer v3**. + + # Header + gr.HTML(""" +
+

🔥 AlphaForge x K2 Think V2

+

Institutional-Grade Quantitative Analysis Platform — Powered by MBZUAI's State-of-the-Art Reasoning Model

+
+
+ 🤖 K2 Think V2 + 📊 Multi-Market Data + 🎯 AI Alpha Engine + 📐 Options Pricing + 🔗 Pairs Trading + 🌍 Macro Analysis +
""") - - with gr.Tabs(): - # ─── TAB 1: Single Stock Analysis ─── - with gr.Tab("📈 Single Stock Analysis", id="stock"): - with gr.Row(): - ticker_input = gr.Textbox( - label="Ticker Symbol", - placeholder="Enter ticker (e.g., AAPL, NVDA, MSFT)", - value="AAPL", - scale=3 - ) - analyze_btn = gr.Button("🔍 Analyze", variant="primary", scale=1) - ai_btn = gr.Button("🤖 AI Deep Analysis (K2 Think)", variant="secondary", scale=1) - - with gr.Row(): - status = gr.Markdown("", visible=True) - - with gr.Row(): - chart = gr.Plot(label="Price & Technicals", scale=2) - with gr.Column(scale=1): - summary = gr.Markdown(label="Summary", value="Enter a ticker to begin.") - - with gr.Row(): - returns_chart = gr.Plot(label="Return Distribution", scale=1) - - with gr.Row(): - ai_output = gr.Markdown( - label="K2 Think V2 Deep Analysis", - value="*Click 'AI Deep Analysis' for institutional-grade analysis powered by K2 Think V2.*", - elem_classes=["ai-output"] - ) - - analyze_btn.click( - fn=on_analyze, - inputs=[ticker_input], - outputs=[chart, returns_chart, summary, status] - ) - ai_btn.click( - fn=on_ai_analysis, - inputs=[ticker_input], - outputs=[ai_output] - ) - - # ─── TAB 2: Portfolio Optimizer ─── - with gr.Tab("💼 Portfolio Optimizer", id="portfolio"): - gr.Markdown("### Markowitz Mean-Variance Optimization") - with gr.Row(): - pf_input = gr.Textbox( - label="Tickers (comma-separated)", - placeholder="AAPL, MSFT, NVDA, GOOGL, AMZN", - value="AAPL, MSFT, GOOGL, AMZN, NVDA", - scale=4 - ) - pf_opt_btn = gr.Button("⚡ Optimize", variant="primary", scale=1) - pf_ai_btn = gr.Button("🤖 AI Portfolio Advice (K2 Think)", variant="secondary", scale=1) - - with gr.Row(): - pf_status = gr.Markdown("") - - with gr.Row(): - ef_chart = gr.Plot(label="Efficient Frontier", scale=2) - with gr.Column(scale=1): - weights_table = gr.DataFrame( - label="Portfolio Weights", - headers=["Ticker", "Optimal Weight", "Equal Weight", "Difference"] - ) - - with gr.Row(): - pf_ai_output = gr.Markdown( - label="K2 Think V2 Portfolio Analysis", - value="*Click 'AI Portfolio Advice' for institutional portfolio analysis.*" - ) - - pf_opt_btn.click( - fn=on_portfolio_optimize, - inputs=[pf_input], - outputs=[ef_chart, weights_table, pf_status] - ) - pf_ai_btn.click( - fn=on_portfolio_ai, - inputs=[pf_input], - outputs=[pf_ai_output] - ) - - # ─── TAB 3: Direct K2 Think Chat ─── - with gr.Tab("💬 K2 Think V2 Chat", id="chat"): - gr.Markdown("### 💬 Ask K2 Think V2 Anything About Finance") - gr.Markdown(""" - Ask about: - - **Trading strategies** (momentum, mean-reversion, pairs) - - **Portfolio theory** (Black-Litterman, risk parity) - - **Derivatives pricing** (Greeks, volatility surface) - - **Market microstructure** (order flow, liquidity) - - **Quant interview prep** (brain teasers, math, coding) - """) - - with gr.Row(): - chat_input = gr.Textbox( - label="Your Question", - placeholder="Explain the Black-Scholes model and its limitations...", - lines=3, - scale=4 - ) - with gr.Row(): - temp_slider = gr.Slider( - label="Temperature", minimum=0.0, maximum=1.0, value=0.3, step=0.1, - scale=1 - ) - chat_btn = gr.Button("🚀 Ask K2 Think", variant="primary", scale=1) - - chat_output = gr.Markdown( - label="K2 Think V2 Response", - value="*Ask a question to get started.*" - ) - - chat_btn.click( - fn=chat_with_k2, - inputs=[chat_input, temp_slider], - outputs=[chat_output] - ) - - # ─── TAB 4: About ─── - with gr.Tab("ℹ️ About", id="about"): - gr.Markdown(""" - ## 🔥 AlphaForge x K2 Think V2 - - **Institutional-Grade Quantitative Analysis Platform** - - ### Powered By - - **K2 Think V2** — MBZUAI's state-of-the-art reasoning model for deep financial analysis - - **Q-TensorFormer v3** — Quantum-enhanced tensor network compression for efficient model deployment - - **yfinance** — Real-time market data from Yahoo Finance - - **Plotly** — Interactive financial visualizations - - **SciPy** — Portfolio optimization engine - - ### Features - 1. **Single Stock Analysis** — 14+ technical indicators, composite alpha signal, risk metrics - 2. **AI Deep Analysis** — Institutional-grade analysis via K2 Think V2 chain-of-thought reasoning - 3. **Portfolio Optimizer** — Markowitz mean-variance optimization with 5000-portfolio efficient frontier - 4. **AI Portfolio Advice** — K2 Think V2 portfolio health scoring, rebalancing, and hedging strategies - 5. **Direct K2 Chat** — Any financial question answered by SOTA reasoning model - - ### Related Projects - - [Q-TensorFormer v3](https://huggingface.co/Premchan369/q-tensorformer) — Quantum+Tensor LLM compression - - [AlphaForge Platform](https://huggingface.co/Premchan369/alphaforge-quant-system) — 25 open-source quant modules - - [Build with K2 Think V2](https://build.k2think.ai/) — MBZUAI Challenge - - ### Setup - 1. Set `K2_API_KEY` as a Space secret in Settings - 2. Factory Rebuild - 3. Start analyzing! - - ### License - Apache 2.0 - """) - - # ─── Footer ─── - gr.Markdown(""" - --- -
- AlphaForge x K2 Think V2 · Built for the Build with K2 Think V2 Challenge · MBZUAI
- "From 'Can I see it?' → 'Can AI explain it?' → 'Can it run everywhere?'" + + # K2 Status + k2_status_html = gr.HTML(f""" +
+ {'✅ K2 Think V2 API Connected' if K2_API_KEY else '⚠️ K2 Think V2 API Not Configured — Add K2_API_KEY in Space Settings > Repository Secrets'}
""") - + + # ── TAB 1: SINGLE STOCK ── + with gr.Tab("📈 Technical Analysis"): + with gr.Row(): + with gr.Column(scale=1): + mkt_select = gr.Dropdown( + label="🌍 Market", choices=list(MARKET_PRESETS.keys()), + value="🇺🇸 US Equities" + ) + ticker_in = gr.Textbox(label="Ticker", value="AAPL", placeholder="e.g., AAPL, BTC-USD, EURUSD=X") + gr.HTML("Examples: ") + period_in = gr.Dropdown(label="Period", choices=["1mo","3mo","6mo","1y","2y","5y"], value="6mo") + interval_in = gr.Dropdown(label="Interval", choices=["1d","1wk","1mo"], value="1d") + analyze_btn = gr.Button("🔍 Analyze", variant="primary", size="lg") + ai_btn = gr.Button("🤖 AI Deep Analysis (K2 Think V2)", variant="secondary", size="lg") + with gr.Column(scale=2): + summary_out = gr.Markdown() + with gr.Row(): + chart1 = gr.Plot(label="Price & Technicals") + chart2 = gr.Plot(label="MACD") + with gr.Row(): + chart3 = gr.Plot(label="Stochastic") + chart4 = gr.Plot(label="Volatility & Volume") + with gr.Row(): + chart5 = gr.Plot(label="ADX Trend Strength") + chart6 = gr.Plot(label="Return Distribution") + with gr.Row(): + ai_out = gr.Textbox(label="🤖 K2 Think V2 Analysis", lines=30, max_lines=50, show_copy_button=True) + + analyze_btn.click( + fn=analyze_stock, + inputs=[ticker_in, mkt_select, period_in, interval_in], + outputs=[chart1, chart2, chart3, chart4, chart5, chart6, summary_out, gr.Textbox(visible=False)] + ) + ai_btn.click( + fn=ai_analyze_stock, + inputs=[ticker_in, mkt_select, period_in, interval_in], + outputs=[ai_out] + ) + + # ── TAB 2: PORTFOLIO ── + with gr.Tab("💼 Portfolio Optimizer"): + with gr.Row(): + with gr.Column(scale=1): + port_in = gr.Textbox(label="Tickers (comma-separated)", value="AAPL, MSFT, GOOGL, AMZN, NVDA") + port_period = gr.Dropdown(label="Lookback", choices=["6mo","1y","2y","3y"], value="1y") + opt_btn = gr.Button("🎯 Optimize Portfolio", variant="primary", size="lg") + ai_port_btn = gr.Button("🤖 AI Portfolio Advice (K2 Think V2)", variant="secondary", size="lg") + with gr.Column(scale=2): + port_md = gr.Markdown() + with gr.Row(): + frontier_plot = gr.Plot(label="Efficient Frontier") + corr_plot = gr.Plot(label="Correlation Matrix") + with gr.Row(): + weights_df = gr.DataFrame(label="Optimal Weights", interactive=False) + with gr.Row(): + ai_port_out = gr.Textbox(label="🤖 AI Portfolio Advice", lines=25, max_lines=40, show_copy_button=True) + + opt_btn.click(fn=optimize_portfolio, inputs=[port_in, port_period], outputs=[frontier_plot, corr_plot, weights_df, port_md]) + ai_port_btn.click(fn=ai_portfolio, inputs=[port_in, port_period], outputs=[ai_port_out]) + + # ── TAB 3: PAIRS TRADING ── + with gr.Tab("🔗 Pairs Trading"): + with gr.Row(): + with gr.Column(scale=1): + pair_a = gr.Textbox(label="Ticker A (Long)", value="AAPL") + pair_b = gr.Textbox(label="Ticker B (Short)", value="MSFT") + pair_period = gr.Dropdown(label="Lookback", choices=["6mo","1y","2y"], value="1y") + pair_btn = gr.Button("Analyze Pair", variant="primary", size="lg") + with gr.Column(scale=2): + pair_md = gr.Markdown() + with gr.Row(): + pair_chart = gr.Plot(label="Spread Analysis") + pair_scatter = gr.Plot(label="Price Relationship") + + pair_btn.click(fn=analyze_pair, inputs=[pair_a, pair_b, pair_period], outputs=[pair_chart, pair_scatter, pair_md]) + + # ── TAB 4: OPTIONS ── + with gr.Tab("📐 Options Pricing"): + with gr.Row(): + with gr.Column(scale=1): + opt_ticker = gr.Textbox(label="Underlying Ticker", value="AAPL") + opt_type = gr.Dropdown(label="Option Type", choices=["Call","Put"], value="Call") + opt_strike = gr.Slider(label="Strike (% of spot)", minimum=70, maximum=130, value=100, step=1) + opt_days = gr.Slider(label="Days to Expiry", minimum=7, maximum=365, value=30, step=7) + opt_rfr = gr.Slider(label="Risk-Free Rate (%)", minimum=0, maximum=10, value=4.5, step=0.25) + opt_vol = gr.Number(label="Vol Override (%)", value=0, info="0 = use historical vol") + opt_calc_btn = gr.Button("📐 Calculate Greeks", variant="primary", size="lg") + with gr.Column(scale=2): + opt_md = gr.Markdown() + with gr.Row(): + greeks_plot = gr.Plot(label="Greeks Analysis") + opt_pl = gr.DataFrame(label="P/L Scenarios", interactive=False) + + opt_calc_btn.click( + fn=analyze_options, + inputs=[opt_ticker, opt_strike, opt_days, opt_rfr, opt_vol, opt_type], + outputs=[greeks_plot, opt_pl, opt_md] + ) + + # ── TAB 5: MACRO ── + with gr.Tab("🌍 Macro Analysis"): + with gr.Row(): + macro_btn = gr.Button("🌍 Analyze Global Macro (K2 Think V2)", variant="primary", size="lg") + with gr.Row(): + macro_out = gr.Textbox(label="🤖 K2 Think V2 Macro Analysis", lines=40, max_lines=60, show_copy_button=True) + + macro_btn.click(fn=ai_macro, outputs=[macro_out]) + + # ── TAB 6: AI CHAT ── + with gr.Tab("💬 K2 Think V2 Chat"): + gr.Markdown("## Direct Chat with K2 Think V2") + gr.Markdown("Ask any financial question — strategy, market analysis, quant interview prep, portfolio advice.") + with gr.Row(): + chat_in = gr.Textbox(label="Your Question", placeholder="e.g., 'Explain gamma scalping with a real trade example'", lines=4, scale=4) + chat_temp = gr.Slider(label="Temp", minimum=0, maximum=1, value=0.4, step=0.1, scale=1) + chat_btn = gr.Button("🚀 Ask K2 Think V2", variant="primary", size="lg") + chat_out = gr.Textbox(label="🤖 Response", lines=30, max_lines=50, show_copy_button=True) + chat_btn.click(fn=ai_chat, inputs=[chat_in, chat_temp], outputs=[chat_out]) + + # ── TAB 7: ABOUT ── + with gr.Tab("ℹ️ About & Setup"): + gr.Markdown(f""" + ## AlphaForge x K2 Think V2 + + Built for the **Build with K2 Think V2 Challenge** by MBZUAI. + + ### Features + | Feature | Description | + |---------|-------------| + | **📈 Technical Analysis** | 18+ indicators: RSI, MACD, Bollinger, VWAP, Stochastic, ADX, Ichimoku, ATR, MFI, OBV | + | **🌍 Multi-Market** | US, EU, UK, DE, JP, CN, IN equities + Crypto + Forex + Commodities + Indices | + | **💼 Portfolio** | Mean-variance optimization, efficient frontier, correlation matrix, Sharpe maximization | + | **🔗 Pairs Trading** | Cointegration analysis, hedge ratio, half-life, Z-score signals | + | **📐 Options Pricing** | Black-Scholes + full Greeks (Delta, Gamma, Theta, Vega, Rho), P/L scenarios | + | **🌍 Macro Analysis** | Global cross-asset regime analysis via K2 Think V2 | + | **🤖 AI Analysis** | K2 Think V2 chain-of-thought: entry/stop/target, catalyst calendar, contrarian view | + | **💬 AI Chat** | Ask any financial question with adjustable temperature | + + ### Setup + > **K2 Think V2 API Key** + > 1. Space Settings → Repository secrets + > 2. New secret: `K2_API_KEY` = `IFM-4SpQ0qEg0Wlsw04O` + > 3. Save → Factory Rebuild + + **Without API key:** All technical analysis, charts, portfolio optimization, pairs trading, and options pricing work perfectly! + + ### Links + - [Full AlphaForge](https://huggingface.co/Premchan369/alphaforge-quant-system) (25 quant modules) + - [Q-TensorFormer](https://huggingface.co/Premchan369/Q-TensorFormer) (Quantum AI) + - [Challenge](https://build.k2think.ai/) + - [MBZUAI](https://mbzuai.ac.ae/) + + *Built by Premchan | Build with K2 Think V2* + """) + return demo - -# ─── Main ──────────────────────────────────────────────────────────────────── - if __name__ == "__main__": - demo = build_ui() - demo.queue(max_size=10, default_concurrency_limit=3) - demo.launch( - server_name="0.0.0.0", - server_port=7860, - share=True, - ) - + if gr is None: + print(f"FATAL: Gradio not available. {_import_errors}") + exit(1) + demo = build_app() + demo.queue().launch(server_name="0.0.0.0", server_port=7860, share=False)