alphaforge-k2 / app.py
Premchan369's picture
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())