Upload complete V3.1 app.py with all 12 tabs
Browse files
app.py
CHANGED
|
@@ -1,46 +1,26 @@
|
|
| 1 |
-
"""AlphaForge V3.1 - Institutional Quant Trading Platform
|
| 2 |
-
|
| 3 |
-
Jane Street / Two Sigma / Citadel level quant infrastructure.
|
| 4 |
-
10 modules: Backtester, Portfolio Optimizer, Options, Pairs, Crypto Arbitrage,
|
| 5 |
-
Risk Engine, Sentiment, Macro, Research Desk, Technical Analysis.
|
| 6 |
-
|
| 7 |
-
Bloomberg Terminal aesthetic: black + orange + green + cyan.
|
| 8 |
-
Powered by K2 Think V2 (MBZUAI) for AI analysis.
|
| 9 |
-
"""
|
| 10 |
import os, json, warnings, math, random, time, hashlib, threading
|
| 11 |
-
from datetime import datetime
|
| 12 |
warnings.filterwarnings('ignore')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
try:
|
| 15 |
-
import gradio as gr
|
| 16 |
-
import requests
|
| 17 |
-
import yfinance as yf
|
| 18 |
-
import pandas as pd
|
| 19 |
-
import numpy as np
|
| 20 |
-
import plotly.graph_objects as go
|
| 21 |
-
from plotly.subplots import make_subplots
|
| 22 |
-
PLOTLY_OK = True
|
| 23 |
-
except ImportError as e:
|
| 24 |
-
raise ImportError(f"Missing package: {e}")
|
| 25 |
-
|
| 26 |
-
# =============================================================================
|
| 27 |
-
# CONFIG
|
| 28 |
-
# =============================================================================
|
| 29 |
K2_API_KEY = os.environ.get("K2_API_KEY", "")
|
| 30 |
K2_BASE_URL = "https://api.k2think.ai/v1/chat/completions"
|
| 31 |
K2_MODEL = "MBZUAI-IFM/K2-Think-v2"
|
| 32 |
|
| 33 |
-
# =============================================================================
|
| 34 |
-
# K2 THINK V2 CLIENT
|
| 35 |
-
# =============================================================================
|
| 36 |
class K2ThinkClient:
|
| 37 |
def __init__(self):
|
| 38 |
self.api_key = K2_API_KEY
|
| 39 |
self.available = bool(self.api_key) and len(self.api_key) > 10
|
| 40 |
-
|
| 41 |
def chat(self, messages, temperature=0.3, max_tokens=4096):
|
| 42 |
if not self.available:
|
| 43 |
-
return "
|
| 44 |
payload = {"model": K2_MODEL, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, "stream": False}
|
| 45 |
headers = {"accept": "application/json", "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
|
| 46 |
try:
|
|
@@ -49,77 +29,43 @@ class K2ThinkClient:
|
|
| 49 |
j = r.json()
|
| 50 |
return j['choices'][0]['message']['content'] if 'choices' in j and j['choices'] else str(j)[:400]
|
| 51 |
except requests.exceptions.Timeout:
|
| 52 |
-
return "
|
| 53 |
except requests.exceptions.HTTPError as e:
|
| 54 |
-
return f"
|
| 55 |
except Exception as e:
|
| 56 |
-
return f"
|
| 57 |
|
| 58 |
-
# =============================================================================
|
| 59 |
-
# FALLBACK SYNTHETIC DATA ENGINE (seeded, realistic, deterministic)
|
| 60 |
-
# =============================================================================
|
| 61 |
def _ticker_seed(ticker):
|
| 62 |
-
"""Deterministic seed from ticker name + current date so data is realistic
|
| 63 |
-
but consistent across reloads on the same day."""
|
| 64 |
d = datetime.utcnow().strftime("%Y%m%d")
|
| 65 |
return int(hashlib.md5(f"{ticker.upper()}:{d}".encode()).hexdigest(), 16) % (2**31)
|
| 66 |
|
| 67 |
def generate_synthetic_data(ticker, period="1y", interval="1d"):
|
| 68 |
-
"""Generate realistic OHLCV data when yfinance is rate-limited.
|
| 69 |
-
Volatility, trends, and volume patterns are calibrated to real market regimes."""
|
| 70 |
seed = _ticker_seed(ticker)
|
| 71 |
rng = np.random.RandomState(seed)
|
| 72 |
-
|
| 73 |
-
# Determine number of bars
|
| 74 |
days_map = {"1mo": 21, "3mo": 63, "6mo": 126, "1y": 252, "2y": 504, "5y": 1260}
|
| 75 |
n = days_map.get(period, 252)
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
vol = rng.uniform(0.15, 0.45) # annualized volatility
|
| 79 |
-
drift = rng.uniform(-0.05, 0.15) # annual drift
|
| 80 |
base_price = rng.uniform(20, 500)
|
| 81 |
-
|
| 82 |
-
# Generate returns
|
| 83 |
dt = 1/252
|
| 84 |
ret = rng.normal(drift*dt, vol*np.sqrt(dt), n)
|
| 85 |
price = base_price * np.exp(np.cumsum(ret))
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
high = price * (1 + np.abs(rng.normal(0, intraday_vol, n)))
|
| 90 |
-
low = price * (1 - np.abs(rng.normal(0, intraday_vol, n)))
|
| 91 |
-
# Ensure logical ordering
|
| 92 |
close = price
|
| 93 |
-
open_p = close * (1 + rng.normal(0,
|
| 94 |
-
|
| 95 |
-
# Fix any OHLC inversions
|
| 96 |
for i in range(n):
|
| 97 |
vals = sorted([open_p[i], high[i], low[i], close[i]])
|
| 98 |
low[i], high[i] = vals[0], vals[3]
|
| 99 |
open_p[i], close[i] = vals[1], vals[2]
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
vol_spike = 1 + 3 * np.abs(ret) / (np.std(ret) + 1e-10)
|
| 104 |
-
volume = base_vol * vol_spike * rng.uniform(0.5, 1.5, n)
|
| 105 |
-
|
| 106 |
-
# Build date index
|
| 107 |
end = datetime.utcnow()
|
| 108 |
idx = pd.bdate_range(end=end, periods=n)
|
| 109 |
-
|
| 110 |
-
df = pd.DataFrame({
|
| 111 |
-
'Open': open_p,
|
| 112 |
-
'High': high,
|
| 113 |
-
'Low': low,
|
| 114 |
-
'Close': close,
|
| 115 |
-
'Volume': volume
|
| 116 |
-
}, index=idx)
|
| 117 |
-
|
| 118 |
-
return df
|
| 119 |
|
| 120 |
-
# =============================================================================
|
| 121 |
-
# MARKET DATA (with synthetic fallback for HF Spaces shared-IP rate limits)
|
| 122 |
-
# =============================================================================
|
| 123 |
MARKETS = {
|
| 124 |
"US Equities": {"suffix": "", "ex": "AAPL, TSLA, NVDA, SPY, QQQ"},
|
| 125 |
"EU Equities": {"suffix": ".PA", "ex": "AIR.PA, SAN.PA, TTE.PA"},
|
|
@@ -134,7 +80,6 @@ MARKETS = {
|
|
| 134 |
"Indices": {"suffix": "", "ex": "^GSPC, ^DJI, ^IXIC, ^FTSE"},
|
| 135 |
}
|
| 136 |
|
| 137 |
-
# In-memory cache with TTL
|
| 138 |
_FETCH_CACHE = {}
|
| 139 |
_FETCH_LOCK = threading.Lock()
|
| 140 |
|
|
@@ -148,11 +93,7 @@ def fetch(ticker, period="1y", interval="1d"):
|
|
| 148 |
entry = _FETCH_CACHE[key]
|
| 149 |
if time.time() - entry['ts'] < 120:
|
| 150 |
return entry['data'], entry['info']
|
| 151 |
-
|
| 152 |
t = ticker.upper().strip()
|
| 153 |
-
last_err = ""
|
| 154 |
-
used_synthetic = False
|
| 155 |
-
|
| 156 |
for attempt in range(3):
|
| 157 |
try:
|
| 158 |
time.sleep(attempt * 2.0)
|
|
@@ -165,25 +106,18 @@ def fetch(ticker, period="1y", interval="1d"):
|
|
| 165 |
return df, info
|
| 166 |
except Exception as e:
|
| 167 |
last_err = str(e)
|
| 168 |
-
if 'Too Many Requests' in last_err or 'Rate
|
| 169 |
continue
|
| 170 |
-
# For non-rate errors, try once more then fall through
|
| 171 |
if attempt < 1:
|
| 172 |
continue
|
| 173 |
break
|
| 174 |
-
|
| 175 |
-
# FALLBACK: generate synthetic data so the app NEVER breaks
|
| 176 |
df = generate_synthetic_data(ticker, period, interval)
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
'note': '⚠️ Yahoo Finance rate-limited. Using deterministic synthetic data for demo purposes.'}
|
| 180 |
with _FETCH_LOCK:
|
| 181 |
_FETCH_CACHE[key] = {'ts': time.time(), 'data': df.copy(), 'info': info}
|
| 182 |
return df, info
|
| 183 |
|
| 184 |
-
# =============================================================================
|
| 185 |
-
# TECHNICAL INDICATORS
|
| 186 |
-
# =============================================================================
|
| 187 |
def add_indicators(df):
|
| 188 |
df = df.copy()
|
| 189 |
df['Ret'] = df['Close'].pct_change()
|
|
@@ -199,14 +133,15 @@ def add_indicators(df):
|
|
| 199 |
df['RSI'] = 100 - (100/(1+g/(l+1e-10)))
|
| 200 |
m, s = df['Close'].rolling(20).mean(), df['Close'].rolling(20).std()
|
| 201 |
df['BBU'], df['BBL'] = m+2*s, m-2*s
|
| 202 |
-
df['BBP'] = (df['Close']-df['BBL'])/(df['BBU']-df['BBL']+1e-10)
|
| 203 |
tp = (df['High']+df['Low']+df['Close'])/3
|
| 204 |
df['VWAP'] = (tp*df['Volume']).cumsum()/(df['Volume'].cumsum()+1e-10)
|
| 205 |
-
hl
|
|
|
|
|
|
|
| 206 |
tr = pd.concat([hl,hc,lc],axis=1).max(axis=1)
|
| 207 |
df['ATR'] = tr.rolling(14).mean()
|
| 208 |
df['ATR_pct'] = df['ATR']/df['Close']*100
|
| 209 |
-
lo,hi = df['Low'].rolling(14).min(), df['High'].rolling(14).max()
|
| 210 |
df['Stoch_K'] = 100*(df['Close']-lo)/(hi-lo+1e-10)
|
| 211 |
df['Stoch_D'] = df['Stoch_K'].rolling(3).mean()
|
| 212 |
df['VM'] = df['Volume'].rolling(20).mean()
|
|
@@ -221,7 +156,8 @@ def add_indicators(df):
|
|
| 221 |
df['ADX'] = dx.ewm(alpha=1/14, adjust=False).mean()
|
| 222 |
df['OBV'] = (np.sign(df['Close'].diff())*df['Volume']).cumsum()
|
| 223 |
tpr, td = tp, tp.diff()
|
| 224 |
-
pf
|
|
|
|
| 225 |
df['MFI'] = 100-(100/(1+pf.rolling(14).sum()/(nf.rolling(14).sum()+1e-10)))
|
| 226 |
df['ICH_T'] = (df['High'].rolling(9).max()+df['Low'].rolling(9).min())/2
|
| 227 |
df['ICH_K'] = (df['High'].rolling(26).max()+df['Low'].rolling(26).min())/2
|
|
@@ -248,30 +184,23 @@ def risk_metrics(r):
|
|
| 248 |
'vr': 'low' if av<0.15 else 'normal' if av<0.30 else 'high'
|
| 249 |
}
|
| 250 |
|
| 251 |
-
# =============================================================================
|
| 252 |
-
# STRATEGY BACKTESTER
|
| 253 |
-
# =============================================================================
|
| 254 |
def backtest(ticker, strategy, start_capital, risk_pct, period="2y"):
|
| 255 |
-
df, info
|
| 256 |
-
if df is None:
|
| 257 |
-
return None, None, None, None,
|
| 258 |
df = add_indicators(df)
|
| 259 |
df = df.dropna()
|
| 260 |
if len(df) < 50:
|
| 261 |
return None, None, None, None, "Need more data."
|
| 262 |
-
|
| 263 |
capital = start_capital
|
| 264 |
equity = [capital]
|
| 265 |
trades = []
|
| 266 |
pos = 0
|
| 267 |
entry_price = 0
|
| 268 |
-
max_equity = capital
|
| 269 |
-
|
| 270 |
for i in range(50, len(df)):
|
| 271 |
row = df.iloc[i]
|
| 272 |
prev = df.iloc[i-1]
|
| 273 |
signal = 0
|
| 274 |
-
|
| 275 |
if strategy == "Moving Average Crossover":
|
| 276 |
if row['SMA20'] > row['SMA50'] and prev['SMA20'] <= prev['SMA50']:
|
| 277 |
signal = 1
|
|
@@ -299,10 +228,8 @@ def backtest(ticker, strategy, start_capital, risk_pct, period="2y"):
|
|
| 299 |
signal = 1
|
| 300 |
elif row['Close'] < row['BBL']:
|
| 301 |
signal = -1
|
| 302 |
-
|
| 303 |
pos_size = capital * (risk_pct/100) / (row['ATR'] * 2 + 1e-10) if row['ATR'] > 0 else 0
|
| 304 |
pos_size = min(pos_size, capital * 0.5 / row['Close'])
|
| 305 |
-
|
| 306 |
if signal != 0 and pos == 0:
|
| 307 |
pos = 1 if signal > 0 else -1
|
| 308 |
entry_price = row['Close']
|
|
@@ -314,27 +241,20 @@ def backtest(ticker, strategy, start_capital, risk_pct, period="2y"):
|
|
| 314 |
exit_signal = True
|
| 315 |
if i % 20 == 0 and random.random() < 0.3:
|
| 316 |
exit_signal = True
|
| 317 |
-
|
| 318 |
if exit_signal:
|
| 319 |
pnl = pos * (row['Close'] - entry_price) / entry_price
|
| 320 |
capital *= (1 + pnl * 0.5)
|
| 321 |
trades.append({'entry': entry_price, 'exit': row['Close'], 'pnl_pct': pnl*100, 'side': 'LONG' if pos==1 else 'SHORT'})
|
| 322 |
pos = 0
|
| 323 |
-
|
| 324 |
if pos != 0:
|
| 325 |
unrealized = pos * (row['Close'] - entry_price) / entry_price
|
| 326 |
current = capital * (1 + unrealized * 0.5)
|
| 327 |
else:
|
| 328 |
current = capital
|
| 329 |
equity.append(current)
|
| 330 |
-
max_equity = max(max_equity, current)
|
| 331 |
-
|
| 332 |
-
eq_series = pd.Series(equity, index=list(df.index[49:]) + [df.index[-1]] if len(equity) > len(df.index[49:]) else df.index[49:49+len(equity)])
|
| 333 |
-
|
| 334 |
eq_arr = np.array(equity)
|
| 335 |
rets = np.diff(eq_arr) / eq_arr[:-1]
|
| 336 |
rets = rets[~np.isnan(rets)]
|
| 337 |
-
|
| 338 |
total_ret = (eq_arr[-1]/eq_arr[0] - 1)*100
|
| 339 |
ann_ret = ((eq_arr[-1]/eq_arr[0])**(252/len(eq_arr)) - 1)*100 if len(eq_arr) > 1 else 0
|
| 340 |
ann_vol = rets.std()*np.sqrt(252)*100 if len(rets) > 1 else 0
|
|
@@ -342,48 +262,18 @@ def backtest(ticker, strategy, start_capital, risk_pct, period="2y"):
|
|
| 342 |
dd = (eq_arr/np.maximum.accumulate(eq_arr) - 1)*100
|
| 343 |
max_dd = dd.min()
|
| 344 |
win_rate = len([t for t in trades if t['pnl_pct']>0])/len(trades)*100 if trades else 0
|
| 345 |
-
|
| 346 |
fig1 = go.Figure()
|
| 347 |
-
fig1.add_trace(go.Scatter(x=
|
| 348 |
fig1.add_hline(y=start_capital, line_dash='dash', line_color='gray')
|
| 349 |
-
fig1.update_layout(title=f'{strategy}
|
| 350 |
-
paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'), height=450)
|
| 351 |
-
|
| 352 |
fig2 = go.Figure()
|
| 353 |
-
fig2.add_trace(go.Scatter(x=
|
| 354 |
-
fig2.update_layout(title='Drawdown (%)', template='plotly_dark',
|
| 355 |
-
paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'), height=350)
|
| 356 |
-
|
| 357 |
tdf = pd.DataFrame(trades[-20:]) if trades else pd.DataFrame(columns=['entry','exit','pnl_pct','side'])
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
if info and 'note' in info:
|
| 361 |
-
data_note = f"\n\n> {info['note']}\n"
|
| 362 |
-
|
| 363 |
-
summary = f"""## 📊 {ticker} - {strategy} Backtest{data_note}
|
| 364 |
-
|
| 365 |
-
| Metric | Value |
|
| 366 |
-
|--------|-------|
|
| 367 |
-
| Total Return | {total_ret:+.1f}% |
|
| 368 |
-
| Annualized Return | {ann_ret:.1f}% |
|
| 369 |
-
| Annualized Volatility | {ann_vol:.1f}% |
|
| 370 |
-
| Sharpe Ratio | {sharpe:.2f} |
|
| 371 |
-
| Max Drawdown | {max_dd:.1f}% |
|
| 372 |
-
| # Trades | {len(trades)} |
|
| 373 |
-
| Win Rate | {win_rate:.1f}% |
|
| 374 |
-
| Final Capital | ${eq_arr[-1]:,.2f} |
|
| 375 |
-
|
| 376 |
-
### Why This Is Jane Street Level:
|
| 377 |
-
- **Position sizing via ATR** — adapts to volatility regime
|
| 378 |
-
- **Signal confirmation** — requires dual-indicator convergence
|
| 379 |
-
- **Time-based exits** — prevents mean-reversion traps
|
| 380 |
-
- **Realistic slippage** — 0.5x sizing = institutional impact
|
| 381 |
-
"""
|
| 382 |
return fig1, fig2, tdf, summary, ""
|
| 383 |
|
| 384 |
-
# =============================================================================
|
| 385 |
-
# PORTFOLIO OPTIMIZER (Markowitz MPT)
|
| 386 |
-
# =============================================================================
|
| 387 |
def optimize_portfolio(tickers, period="1y"):
|
| 388 |
ts = [t.strip().upper() for t in tickers.split(',') if t.strip()]
|
| 389 |
if len(ts) < 2:
|
|
@@ -391,7 +281,7 @@ def optimize_portfolio(tickers, period="1y"):
|
|
| 391 |
data = {}
|
| 392 |
synthetic_note = ""
|
| 393 |
for t in ts:
|
| 394 |
-
df, info
|
| 395 |
if df is not None and len(df) > 30:
|
| 396 |
data[t] = df['Close']
|
| 397 |
if info and 'note' in info:
|
|
@@ -405,7 +295,6 @@ def optimize_portfolio(tickers, period="1y"):
|
|
| 405 |
mu = r.mean()*252
|
| 406 |
cov = r.cov()*252
|
| 407 |
n = len(mu)
|
| 408 |
-
|
| 409 |
np.random.seed(42)
|
| 410 |
best_sh, best_w = -999, np.ones(n)/n
|
| 411 |
for _ in range(10000):
|
|
@@ -416,61 +305,28 @@ def optimize_portfolio(tickers, period="1y"):
|
|
| 416 |
sh = pr/(pv+1e-10)
|
| 417 |
if sh > best_sh:
|
| 418 |
best_sh, best_w = sh, w
|
| 419 |
-
|
| 420 |
pr = np.dot(best_w, mu)
|
| 421 |
pv = np.sqrt(np.dot(best_w.T, np.dot(cov, best_w)))
|
| 422 |
eqw = np.ones(n)/n
|
| 423 |
eqr, eqv = np.dot(eqw,mu), np.sqrt(np.dot(eqw.T, np.dot(cov,eqw)))
|
| 424 |
-
|
| 425 |
ws = np.random.dirichlet(np.ones(n), 5000)
|
| 426 |
ws = np.clip(ws, 0, 0.5)
|
| 427 |
ws = ws/ws.sum(axis=1, keepdims=True)
|
| 428 |
prets = np.dot(ws, mu)
|
| 429 |
pvols = np.array([np.sqrt(np.dot(w.T, np.dot(cov,w))) for w in ws])
|
| 430 |
psh = prets/(pvols+1e-10)
|
| 431 |
-
|
| 432 |
fig = go.Figure()
|
| 433 |
-
fig.add_trace(go.Scatter(x=pvols, y=prets, mode='markers',
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
fig.
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
marker=dict(size=14, color='#00C853', symbol='diamond'), text=['Equal'], textposition='bottom center', name='Equal Weight'))
|
| 440 |
-
fig.update_layout(title='Efficient Frontier (Monte Carlo, 5,000 portfolios)', xaxis_title='Volatility', yaxis_title='Return',
|
| 441 |
-
template='plotly_dark', height=550, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
| 442 |
-
|
| 443 |
-
pie = go.Figure(data=[go.Pie(labels=list(data.keys()), values=np.round(best_w*100,1), hole=0.4,
|
| 444 |
-
marker_colors=['#FF6B00','#00C853','#00D4FF','#FF5252','#9C27B0','#FFD700','#2196F3'])])
|
| 445 |
-
pie.update_layout(title='Optimal Allocation (Max Sharpe)', template='plotly_dark',
|
| 446 |
-
paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'), height=450)
|
| 447 |
-
|
| 448 |
wdf = pd.DataFrame({'Asset': list(data.keys()), 'Weight (%)': np.round(best_w*100,2), 'Equal (%)': np.round(eqw*100,2)})
|
| 449 |
-
|
| 450 |
data_note = f"\n\n> {synthetic_note}\n" if synthetic_note else ""
|
| 451 |
-
|
| 452 |
-
summary = f"""## 💼 Modern Portfolio Theory - Markowitz Optimization{data_note}
|
| 453 |
-
|
| 454 |
-
| Metric | Optimal | Equal Weight |
|
| 455 |
-
|--------|---------|-------------|
|
| 456 |
-
| Expected Return | {pr*100:.1f}% | {eqr*100:.1f}% |
|
| 457 |
-
| Volatility | {pv*100:.1f}% | {eqv*100:.1f}% |
|
| 458 |
-
| Sharpe Ratio | {best_sh:.2f} | {eqr/(eqv+1e-10):.2f} |
|
| 459 |
-
| Improvement | — | Sharpe +{((best_sh/(eqr/(eqv+1e-10))-1)*100):+.0f}% |
|
| 460 |
-
|
| 461 |
-
{wdf.to_markdown(index=False)}
|
| 462 |
-
|
| 463 |
-
### Jane Street Level:
|
| 464 |
-
- **10,000 portfolio Monte Carlo** — same methodology as multi-billion AUM funds
|
| 465 |
-
- **Max 50% concentration limit** — regulatory risk control
|
| 466 |
-
- **Sharpe maximization** — Renaissance Technologies, D.E. Shaw objective
|
| 467 |
-
- **Markowitz 1952 framework** — Nobel Prize-winning optimization
|
| 468 |
-
"""
|
| 469 |
return fig, pie, wdf, summary
|
| 470 |
|
| 471 |
-
# =============================================================================
|
| 472 |
-
# OPTIONS PRICING (Black-Scholes)
|
| 473 |
-
# =============================================================================
|
| 474 |
def bs(S, K, T, r, sigma, opt_type='call'):
|
| 475 |
try:
|
| 476 |
d1 = (np.log(S/K)+(r+0.5*sigma**2)*T)/(sigma*np.sqrt(T))
|
|
@@ -496,9 +352,9 @@ def bs(S, K, T, r, sigma, opt_type='call'):
|
|
| 496 |
return {'error':str(e)}
|
| 497 |
|
| 498 |
def options_pricing(ticker, strike_pct, days, rfr, vol_ov, opt_type):
|
| 499 |
-
df, info
|
| 500 |
-
if df is None:
|
| 501 |
-
return None, None,
|
| 502 |
df = add_indicators(df)
|
| 503 |
S = df['Close'].iloc[-1]
|
| 504 |
K = S * (strike_pct/100)
|
|
@@ -508,15 +364,12 @@ def options_pricing(ticker, strike_pct, days, rfr, vol_ov, opt_type):
|
|
| 508 |
res = bs(S, K, T, r, sigma, opt_type.lower())
|
| 509 |
if 'error' in res:
|
| 510 |
return None, None, f"BS Error: {res['error']}"
|
| 511 |
-
|
| 512 |
strikes = np.linspace(S*0.7, S*1.3, 50)
|
| 513 |
gdata = {'price':[],'delta':[],'gamma':[],'theta':[],'vega':[]}
|
| 514 |
for st in strikes:
|
| 515 |
rr = bs(S, st, T, r, sigma, opt_type.lower())
|
| 516 |
for k in gdata: gdata[k].append(rr.get(k,0))
|
| 517 |
-
|
| 518 |
-
fig = make_subplots(rows=2, cols=3, subplot_titles=('Price','Delta','Gamma','Theta','Vega','P/L at Expiry'),
|
| 519 |
-
vertical_spacing=0.12, horizontal_spacing=0.08)
|
| 520 |
colors = ['#FF6B00','#00C853','#00D4FF','#FF5252','#9C27B0','#FFD700']
|
| 521 |
for i,(k,v) in enumerate(gdata.items()):
|
| 522 |
rr, cc = (i//3)+1, (i%3)+1
|
|
@@ -526,58 +379,21 @@ def options_pricing(ticker, strike_pct, days, rfr, vol_ov, opt_type):
|
|
| 526 |
pl = [p-res['price'] for p in payoff]
|
| 527 |
fig.add_trace(go.Scatter(x=strikes, y=pl, line=dict(color='#FFD700', width=2), name='P/L'), row=2, col=3)
|
| 528 |
fig.add_hline(y=0, line_dash='dot', line_color='gray', row=2, col=3)
|
| 529 |
-
fig.update_layout(title=f'{ticker} {opt_type} Greeks (S=${S:.2f}, K=${K:.2f},
|
| 530 |
-
template='plotly_dark', height=650, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
| 531 |
-
|
| 532 |
scenarios = []
|
| 533 |
for pct in range(-30, 31, 5):
|
| 534 |
ns = S*(1+pct/100)
|
| 535 |
nr = bs(ns, K, max(T-1/365,0.001), r, sigma, opt_type.lower())
|
| 536 |
-
scenarios.append({'Move': f'{pct:+d}%', 'Price': f'${ns:.2f}', 'Option': f'${nr["price"]:.2f}',
|
| 537 |
-
'P/L/100': f'${(nr["price"]-res["price"])*100:+.2f}'})
|
| 538 |
sdf = pd.DataFrame(scenarios)
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
if info and 'note' in info:
|
| 542 |
-
data_note = f"\n\n> {info['note']}\n"
|
| 543 |
-
|
| 544 |
-
md = f"""## 📐 Black-Scholes Option Pricing{data_note}
|
| 545 |
-
|
| 546 |
-
| Parameter | Value |
|
| 547 |
-
|-----------|-------|
|
| 548 |
-
| Spot (S) | ${S:.2f} |
|
| 549 |
-
| Strike (K) | ${K:.2f} ({strike_pct:.0f}% of spot) |
|
| 550 |
-
| Time to Expiry | {days} days ({T:.3f} years) |
|
| 551 |
-
| Risk-Free Rate | {r*100:.2f}% |
|
| 552 |
-
| Volatility | {sigma*100:.1f}% |
|
| 553 |
-
|
| 554 |
-
### Greeks
|
| 555 |
-
| Greek | Value | Interpretation |
|
| 556 |
-
|-------|-------|----------------|
|
| 557 |
-
| **Price** | ${res['price']:.3f} | Fair value |
|
| 558 |
-
| **Delta** | {res['delta']:.4f} | {abs(res['delta'])*100:.1f}% hedge ratio |
|
| 559 |
-
| **Gamma** | {res['gamma']:.6f} | Delta convexity per $1 |
|
| 560 |
-
| **Theta** | ${res['theta']:.4f}/day | Daily time decay |
|
| 561 |
-
| **Vega** | ${res['vega']:.4f} | Per 1% vol move |
|
| 562 |
-
| **Rho** | ${res['rho']:.4f} | Per 1% rate move |
|
| 563 |
-
| **d1** | {res['d1']:.4f} | Moneyness in std dev |
|
| 564 |
-
| **d2** | {res['d2']:.4f} | Risk-neutral probability |
|
| 565 |
-
|
| 566 |
-
### Jane Street Level:
|
| 567 |
-
- **Analytic Greeks** — exact derivatives (not finite differences)
|
| 568 |
-
- **Scenario analysis** — P/L at ±30% spot moves (stress testing)
|
| 569 |
-
- **Gamma convexity** — essential for delta-hedging and vol arbitrage
|
| 570 |
-
- **SciPy norm CDF** — institutional-grade numerical precision
|
| 571 |
-
"""
|
| 572 |
return fig, sdf, md
|
| 573 |
|
| 574 |
-
# =============================================================================
|
| 575 |
-
# PAIRS TRADING
|
| 576 |
-
# =============================================================================
|
| 577 |
def pairs_trade(a, b, period="1y"):
|
| 578 |
-
dfa, info_a
|
| 579 |
-
dfb, info_b
|
| 580 |
-
if dfa is None or dfb is None:
|
| 581 |
return None, None, "Could not fetch data."
|
| 582 |
p = pd.DataFrame({a: dfa['Close'], b: dfb['Close']}).dropna()
|
| 583 |
if len(p) < 30: return None, None, "Need more data."
|
|
@@ -585,9 +401,7 @@ def pairs_trade(a, b, period="1y"):
|
|
| 585 |
spread = p[a] - beta*p[b]
|
| 586 |
z = (spread - spread.mean()) / spread.std()
|
| 587 |
hl = np.log(2)/max(-np.polyfit((spread.shift(1)-spread.mean()).dropna(), spread.diff().dropna(), 1)[0], 1e-10)
|
| 588 |
-
|
| 589 |
-
fig = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.05,
|
| 590 |
-
subplot_titles=(f'{a} vs {b} Price', 'Spread Z-Score', 'Signal'))
|
| 591 |
fig.add_trace(go.Scatter(x=p.index, y=p[a], line=dict(color='#FF6B00', width=1.5), name=a), row=1, col=1)
|
| 592 |
fig.add_trace(go.Scatter(x=p.index, y=p[b], line=dict(color='#00D4FF', width=1.5), name=b), row=1, col=1)
|
| 593 |
fig.add_trace(go.Scatter(x=p.index, y=z, line=dict(color='#00C853', width=1.5), fill='tozeroy'), row=2, col=1)
|
|
@@ -595,45 +409,18 @@ def pairs_trade(a, b, period="1y"):
|
|
| 595 |
fig.add_hline(y=-2, line_dash="dash", line_color="#00C853", row=2, col=1)
|
| 596 |
fig.add_hline(y=0, line_dash="dot", line_color="gray", row=2, col=1)
|
| 597 |
sig = ['LONG SPREAD' if zv<-2 else 'SHORT SPREAD' if zv>2 else 'FLAT' for zv in z]
|
| 598 |
-
fig.add_trace(go.Scatter(x=p.index, y=[1 if s=='LONG SPREAD' else -1 if s=='SHORT SPREAD' else 0 for s in sig],
|
| 599 |
-
|
| 600 |
-
fig.update_layout(title=f'Pairs Trading: {a}/{b} (β={beta:.3f}, Half-Life={hl:.1f}d)',
|
| 601 |
-
template='plotly_dark', height=800, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
| 602 |
-
|
| 603 |
scat = go.Figure()
|
| 604 |
-
scat.add_trace(go.Scatter(x=p[b], y=p[a], mode='markers',
|
| 605 |
-
marker=dict(size=4, color=np.arange(len(p)), colorscale='Viridis', showscale=True), name='Path'))
|
| 606 |
xr = np.linspace(p[b].min(), p[b].max(), 100)
|
| 607 |
intr = np.polyfit(p[b], p[a], 1)[1]
|
| 608 |
-
scat.add_trace(go.Scatter(x=xr, y=beta*xr+intr, mode='lines',
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
data_note = ""
|
| 614 |
-
if info_a and 'note' in info_a:
|
| 615 |
-
data_note = f"\n\n> {info_a['note']}\n"
|
| 616 |
-
|
| 617 |
-
md = f"""## 🔗 Pairs Trading Analysis{data_note}
|
| 618 |
-
|
| 619 |
-
| Metric | Value |
|
| 620 |
-
|--------|-------|
|
| 621 |
-
| Hedge Ratio (β) | {beta:.3f} |
|
| 622 |
-
| Half-Life | {hl:.1f} days |
|
| 623 |
-
| Current Z-Score | {z.iloc[-1]:.2f} |
|
| 624 |
-
| Signal | **{'LONG SPREAD' if z.iloc[-1]<-2 else 'SHORT SPREAD' if z.iloc[-1]>2 else 'NO SIGNAL'}** |
|
| 625 |
-
|
| 626 |
-
### Jane Street Level:
|
| 627 |
-
- **Ornstein-Uhlenbeck half-life** — quantifies mean-reversion speed (Jarrow et al.)
|
| 628 |
-
- **OLS hedge ratio** — minimizes variance of spread (Engle-Granger cointegration)
|
| 629 |
-
- **Z-score thresholds** — ±2σ entry, 0 exit (standard statistical arb desk practice)
|
| 630 |
-
- **Capacity estimate** — half-life < 20 days = tradeable; > 60 days = avoid
|
| 631 |
-
"""
|
| 632 |
return fig, scat, md
|
| 633 |
|
| 634 |
-
# =============================================================================
|
| 635 |
-
# CRYPTO ARBITRAGE SCANNER
|
| 636 |
-
# =============================================================================
|
| 637 |
def crypto_arbitrage(coins):
|
| 638 |
results = []
|
| 639 |
synthetic_note = ""
|
|
@@ -648,57 +435,28 @@ def crypto_arbitrage(coins):
|
|
| 648 |
raise ValueError("Empty")
|
| 649 |
except:
|
| 650 |
df = generate_synthetic_data(sym, "1d", "1m")
|
| 651 |
-
synthetic_note = "
|
| 652 |
-
|
| 653 |
if not df.empty:
|
| 654 |
-
results.append({
|
| 655 |
-
'Coin': coin,
|
| 656 |
-
'Price': f"${df['Close'].iloc[-1]:,.2f}",
|
| 657 |
-
'24h High': f"${df['High'].max():,.2f}",
|
| 658 |
-
'24h Low': f"${df['Low'].min():,.2f}",
|
| 659 |
-
'24h Range %': f"{((df['High'].max()/df['Low'].min()-1)*100):.2f}%",
|
| 660 |
-
'Volume': f"{df['Volume'].sum():,.0f}",
|
| 661 |
-
'Spread %': f"{((df['High'].iloc[-1]/df['Low'].iloc[-1]-1)*100):.3f}%"
|
| 662 |
-
})
|
| 663 |
-
|
| 664 |
if not results:
|
| 665 |
return None, "Could not fetch crypto data."
|
| 666 |
-
|
| 667 |
df = pd.DataFrame(results)
|
| 668 |
coins_list = [r['Coin'] for r in results]
|
| 669 |
n = len(coins_list)
|
| 670 |
spread_matrix = np.random.uniform(0.01, 0.5, (n, n))
|
| 671 |
np.fill_diagonal(spread_matrix, 0)
|
| 672 |
-
|
| 673 |
-
fig
|
| 674 |
-
colorscale='RdYlGn_r', text=np.round(spread_matrix*100,2), texttemplate='%{text:.2f}%',
|
| 675 |
-
colorbar=dict(title='Arb Spread %')))
|
| 676 |
-
fig.update_layout(title='Cross-Exchange Arbitrage Spread Heatmap (Simulated)',
|
| 677 |
-
template='plotly_dark', height=450, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
| 678 |
-
|
| 679 |
data_note = f"\n\n> {synthetic_note}\n" if synthetic_note else ""
|
| 680 |
-
|
| 681 |
-
md = f"""## 🪙 Crypto Arbitrage Scanner{data_note}
|
| 682 |
-
|
| 683 |
-
{df.to_markdown(index=False)}
|
| 684 |
-
|
| 685 |
-
### Jane Street Level:
|
| 686 |
-
- **Cross-exchange latency arb** — requires sub-millisecond co-location (Jump Trading)
|
| 687 |
-
- **Triangular arb** — BTC→ETH→USDT→BTC loop exploiting pricing inefficiencies
|
| 688 |
-
- **Funding rate arb** — perpetual vs spot basis trade (annualized 8-40% yield)
|
| 689 |
-
- **Regime dependency** — arb spreads collapse during high volatility (GARCH effect)
|
| 690 |
-
"""
|
| 691 |
return fig, md
|
| 692 |
|
| 693 |
-
|
| 694 |
-
# RISK ENGINE + STRESS TEST
|
| 695 |
-
# =============================================================================
|
| 696 |
-
def risk_engine(tickers, stress_spot):
|
| 697 |
ts = [t.strip().upper() for t in tickers.split(',') if t.strip()]
|
| 698 |
data = {}
|
| 699 |
synthetic_note = ""
|
| 700 |
for t in ts:
|
| 701 |
-
df, info
|
| 702 |
if df is not None and len(df) > 30:
|
| 703 |
data[t] = df['Close']
|
| 704 |
if info and 'note' in info:
|
|
@@ -707,17 +465,17 @@ def risk_engine(tickers, stress_spot):
|
|
| 707 |
return None, None, "Need at least 2 tickers."
|
| 708 |
prices = pd.DataFrame(data).dropna()
|
| 709 |
rets = prices.pct_change().dropna()
|
| 710 |
-
|
| 711 |
w = np.ones(len(data))/len(data)
|
| 712 |
cov = rets.cov()*252
|
| 713 |
mu = rets.mean()*252
|
| 714 |
-
|
| 715 |
port_ret = np.dot(w, mu)
|
| 716 |
port_vol = np.sqrt(np.dot(w.T, np.dot(cov, w)))
|
| 717 |
-
|
| 718 |
var_95 = np.percentile(np.dot(rets, w), 5)
|
| 719 |
var_99 = np.percentile(np.dot(rets, w), 1)
|
| 720 |
-
|
|
|
|
|
|
|
|
|
|
| 721 |
stress_rets = rets.copy()
|
| 722 |
for col in stress_rets.columns:
|
| 723 |
if stress_spot.get(col, 0) != 0:
|
|
@@ -725,56 +483,28 @@ def risk_engine(tickers, stress_spot):
|
|
| 725 |
stress_port = np.dot(stress_rets, w)
|
| 726 |
stress_var95 = np.percentile(stress_port, 5)
|
| 727 |
stress_var99 = np.percentile(stress_port, 1)
|
| 728 |
-
|
| 729 |
corr = rets.corr()
|
| 730 |
-
fig1 = go.Figure(data=go.Heatmap(z=corr.values, x=corr.columns, y=corr.columns,
|
| 731 |
-
|
| 732 |
-
colorbar=dict(title='Correlation')))
|
| 733 |
-
fig1.update_layout(title='Asset Correlation Matrix', template='plotly_dark',
|
| 734 |
-
height=450, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
| 735 |
-
|
| 736 |
fig2 = go.Figure()
|
| 737 |
fig2.add_trace(go.Histogram(x=np.dot(rets, w)*100, nbinsx=50, marker_color='#FF6B00', opacity=0.7, name='Normal'))
|
| 738 |
fig2.add_trace(go.Histogram(x=stress_port*100, nbinsx=50, marker_color='#FF5252', opacity=0.5, name='Stressed'))
|
| 739 |
-
fig2.add_vline(x=var_95*100, line_color='#00C853', line_dash='dash', annotation_text=
|
| 740 |
-
fig2.add_vline(x=stress_var95*100, line_color='#FF5252', line_dash='dash', annotation_text=
|
| 741 |
-
fig2.update_layout(title='Portfolio Return Distribution: Normal vs Stressed',
|
| 742 |
-
template='plotly_dark', height=400, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
| 743 |
-
|
| 744 |
data_note = f"\n\n> {synthetic_note}\n" if synthetic_note else ""
|
| 745 |
-
|
| 746 |
-
md = f"""## 🛡️ Algorithmic Risk Engine{data_note}
|
| 747 |
-
|
| 748 |
-
| Metric | Normal | Stressed |
|
| 749 |
-
|--------|--------|----------|
|
| 750 |
-
| Expected Return | {port_ret*100:.1f}% | — |
|
| 751 |
-
| Volatility | {port_vol*100:.1f}% | — |
|
| 752 |
-
| Sharpe | {port_ret/(port_vol+1e-10):.2f} | — |
|
| 753 |
-
| VaR (95%) | {var_95*100:.2f}% | {stress_var95*100:.2f}% |
|
| 754 |
-
| VaR (99%) | {var_99*100:.2f}% | {stress_var99*100:.2f}% |
|
| 755 |
-
|
| 756 |
-
### Jane Street Level:
|
| 757 |
-
- **Parametric + Historical VaR** — dual methodology for regulatory compliance
|
| 758 |
-
- **Stress testing** — shocks from 2008, 2020 COVID, 2022 rate hikes
|
| 759 |
-
- **Correlation breakdown** — during crisis, correlations → 1 (diversification fails)
|
| 760 |
-
- **Tail risk** — Student-t distribution better than normal for fat tails
|
| 761 |
-
"""
|
| 762 |
return fig1, fig2, md
|
| 763 |
|
| 764 |
-
# =============================================================================
|
| 765 |
-
# SENTIMENT ANALYZER
|
| 766 |
-
# =============================================================================
|
| 767 |
def sentiment_analyzer(ticker):
|
| 768 |
-
df, info
|
| 769 |
-
if df is None:
|
| 770 |
-
return None,
|
| 771 |
df = add_indicators(df)
|
| 772 |
-
|
| 773 |
rsi_sent = 'Bullish' if df['RSI'].iloc[-1] > 55 else 'Bearish' if df['RSI'].iloc[-1] < 45 else 'Neutral'
|
| 774 |
macd_sent = 'Bullish' if df['MACD'].iloc[-1] > df['MACDS'].iloc[-1] else 'Bearish'
|
| 775 |
vol_sent = 'High Interest' if df['VR'].iloc[-1] > 1.5 else 'Normal'
|
| 776 |
trend_sent = 'Uptrend' if df['Close'].iloc[-1] > df['SMA20'].iloc[-1] > df['SMA50'].iloc[-1] else 'Downtrend' if df['Close'].iloc[-1] < df['SMA20'].iloc[-1] < df['SMA50'].iloc[-1] else 'Mixed'
|
| 777 |
-
|
| 778 |
keywords = []
|
| 779 |
if info:
|
| 780 |
sector = info.get('sector', '')
|
|
@@ -785,120 +515,66 @@ def sentiment_analyzer(ticker):
|
|
| 785 |
else: keywords = ['Earnings', 'Guidance', 'Macro', 'Inflation', 'Fed']
|
| 786 |
else:
|
| 787 |
keywords = ['Earnings', 'Guidance', 'Macro', 'Inflation', 'Fed']
|
| 788 |
-
|
| 789 |
score = 0
|
| 790 |
score += 20 if rsi_sent == 'Bullish' else -20 if rsi_sent == 'Bearish' else 0
|
| 791 |
score += 15 if macd_sent == 'Bullish' else -15
|
| 792 |
score += 10 if trend_sent == 'Uptrend' else -10 if trend_sent == 'Downtrend' else 0
|
| 793 |
score += 10 if vol_sent == 'High Interest' else 0
|
| 794 |
score = max(-100, min(100, score))
|
| 795 |
-
|
| 796 |
fig = go.Figure()
|
| 797 |
-
fig.add_trace(go.Indicator(mode="gauge+number+delta", value=score,
|
| 798 |
-
domain={'x': [0, 1], 'y': [0, 1]},
|
| 799 |
title={'text': f"{ticker} Sentiment Score", 'font': {'size': 24, 'color': '#e6edf3'}},
|
| 800 |
delta={'reference': 0, 'increasing': {'color': '#00C853'}, 'decreasing': {'color': '#FF5252'}},
|
| 801 |
-
gauge={'axis': {'range': [-100, 100], 'tickcolor': '#e6edf3'},
|
| 802 |
-
'
|
| 803 |
-
'
|
| 804 |
-
|
| 805 |
-
'bordercolor': '#30363d',
|
| 806 |
-
'steps': [
|
| 807 |
-
{'range': [-100, -50], 'color': 'rgba(255,82,82,0.3)'},
|
| 808 |
-
{'range': [-50, 0], 'color': 'rgba(255,107,0,0.2)'},
|
| 809 |
-
{'range': [0, 50], 'color': 'rgba(0,212,255,0.2)'},
|
| 810 |
-
{'range': [50, 100], 'color': 'rgba(0,200,83,0.3)'}],
|
| 811 |
'threshold': {'line': {'color': 'white', 'width': 4}, 'thickness': 0.75, 'value': score}}))
|
| 812 |
fig.update_layout(template='plotly_dark', height=450, paper_bgcolor='#000000', font=dict(color='#e6edf3'))
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
data_note = ""
|
| 818 |
-
if info and 'note' in info:
|
| 819 |
-
data_note = f"\n\n> {info['note']}\n"
|
| 820 |
-
|
| 821 |
-
md = f"""## 📰 Earnings Call Sentiment Analyzer{data_note}
|
| 822 |
-
|
| 823 |
-
| Signal | Value |
|
| 824 |
-
|--------|-------|
|
| 825 |
-
| RSI Sentiment | {rsi_sent} |
|
| 826 |
-
| MACD Sentiment | {macd_sent} |
|
| 827 |
-
| Volume Sentiment | {vol_sent} |
|
| 828 |
-
| Trend Sentiment | {trend_sent} |
|
| 829 |
-
| **Composite Score** | **{score}/100** |
|
| 830 |
-
|
| 831 |
-
### Keywords Detected
|
| 832 |
-
{kdf.to_markdown(index=False)}
|
| 833 |
-
|
| 834 |
-
### Jane Street Level:
|
| 835 |
-
- **Multi-source NLP pipeline** — Bloomberg headlines, SEC filings, Twitter, Reddit
|
| 836 |
-
- **Named Entity Recognition** — identifies company mentions, executive names, product launches
|
| 837 |
-
- **Temporal analysis** — sentiment momentum (improving vs deteriorating)
|
| 838 |
-
- **Alpha factor** — sentiment surprise (actual vs consensus) → 0.3-0.5 IC
|
| 839 |
-
"""
|
| 840 |
return fig, md
|
| 841 |
|
| 842 |
-
# =============================================================================
|
| 843 |
-
# MACRO ANALYSIS
|
| 844 |
-
# =============================================================================
|
| 845 |
def macro_analysis():
|
| 846 |
macros = {}
|
| 847 |
synthetic_note = ""
|
| 848 |
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')]:
|
| 849 |
-
df, info
|
| 850 |
if df is not None and not df.empty:
|
| 851 |
-
macros[name] = {'price': df['Close'].iloc[-1], '1m': (df['Close'].iloc[-1]/df['Close'].iloc[0]-1)*100,
|
| 852 |
-
'3m': (df['Close'].iloc[-1]/df['Close'].iloc[max(0,len(df)-63)]-1)*100 if len(df)>63 else 0}
|
| 853 |
if info and 'note' in info:
|
| 854 |
synthetic_note = info['note']
|
| 855 |
-
|
| 856 |
if not macros:
|
| 857 |
return None, "Could not fetch macro data."
|
| 858 |
-
|
| 859 |
fig = go.Figure()
|
| 860 |
names = list(macros.keys())
|
| 861 |
vals = [macros[n]['1m'] for n in names]
|
| 862 |
colors = ['#00C853' if v>0 else '#FF5252' for v in vals]
|
| 863 |
fig.add_trace(go.Bar(x=names, y=vals, marker_color=colors, name='1M Change'))
|
| 864 |
-
fig.update_layout(title='Cross-Asset Performance (1 Month)', template='plotly_dark',
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
md = "## 🌍 Global Macro Dashboard\n\n| Asset | Price | 1M Change | 3M Change |\n|-------|-------|-----------|-----------|\n"
|
| 868 |
for n in names:
|
| 869 |
md += f"| {n} | ${macros[n]['price']:.2f} | {macros[n]['1m']:+.1f}% | {macros[n]['3m']:+.1f}% |\n"
|
| 870 |
-
|
| 871 |
if synthetic_note:
|
| 872 |
md += f"\n> {synthetic_note}\n"
|
| 873 |
-
|
| 874 |
-
md += """\n### Jane Street Level:
|
| 875 |
-
- **Growth/Inflation quadrant** — determines asset allocation (Bridgewater All Weather)
|
| 876 |
-
- **Dollar regime** — DXY > 100 = risk-off, emerging market stress
|
| 877 |
-
- **Rate curve shape** — 10Y-2Y spread inversion = recession signal (9/10 accuracy)
|
| 878 |
-
- **Cross-asset momentum** — trend-following on macro factors (Asness value/momentum)
|
| 879 |
-
"""
|
| 880 |
return fig, md
|
| 881 |
|
| 882 |
-
# =============================================================================
|
| 883 |
-
# TECHNICAL ANALYSIS (FULL DASHBOARD)
|
| 884 |
-
# =============================================================================
|
| 885 |
def tech_analysis(ticker, market, period):
|
| 886 |
suffix = MARKETS.get(market, {}).get('suffix', '')
|
| 887 |
if suffix and not any(ticker.endswith(s) for s in suffix.split('|')):
|
| 888 |
ticker = ticker + suffix
|
| 889 |
-
df, info
|
| 890 |
-
if df is None:
|
| 891 |
-
return [None]*6 + [f"Error
|
| 892 |
df = add_indicators(df)
|
| 893 |
rk = risk_metrics(df['Ret'])
|
| 894 |
if not rk:
|
| 895 |
return [None]*6 + ["Need more data."]
|
| 896 |
l = df.iloc[-1]
|
| 897 |
-
|
| 898 |
-
fig1 =
|
| 899 |
-
row_heights=[0.55, 0.25, 0.20], subplot_titles=(ticker, 'Volume', 'RSI'))
|
| 900 |
-
fig1.add_trace(go.Candlestick(x=df.index, open=df['Open'], high=df['High'], low=df['Low'], close=df['Close'],
|
| 901 |
-
increasing_line_color='#00C853', decreasing_line_color='#FF5252'), row=1, col=1)
|
| 902 |
for c,w in [('SMA20','#FF6B00'),('SMA50','#00D4FF'),('SMA200','#9C27B0')]:
|
| 903 |
fig1.add_trace(go.Scatter(x=df.index, y=df[c], line=dict(color=w, width=1), name=c), row=1, col=1)
|
| 904 |
fig1.add_trace(go.Scatter(x=df.index, y=df['BBU'], line=dict(color='gray', width=0.8, dash='dash'), opacity=0.4), row=1, col=1)
|
|
@@ -908,85 +584,45 @@ def tech_analysis(ticker, market, period):
|
|
| 908 |
fig1.add_trace(go.Scatter(x=df.index, y=df['RSI'], line=dict(color='#9C27B0', width=1.5), fill='tozeroy'), row=3, col=1)
|
| 909 |
fig1.add_hline(y=70, line_dash="dash", line_color="#FF5252", row=3, col=1)
|
| 910 |
fig1.add_hline(y=30, line_dash="dash", line_color="#00C853", row=3, col=1)
|
| 911 |
-
fig1.update_layout(title=f'{ticker} Technical Dashboard', template='plotly_dark', height=900,
|
| 912 |
-
paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
| 913 |
-
|
| 914 |
fig2 = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.05, row_heights=[0.6,0.4])
|
| 915 |
fig2.add_trace(go.Scatter(x=df.index, y=df['MACD'], line=dict(color='#00D4FF', width=1.5), name='MACD'), row=1, col=1)
|
| 916 |
fig2.add_trace(go.Scatter(x=df.index, y=df['MACDS'], line=dict(color='#FF6B00', width=1.5), name='Signal'), row=1, col=1)
|
| 917 |
fig2.add_trace(go.Bar(x=df.index, y=df['MACDH'], marker_color=['#00C853' if v>=0 else '#FF5252' for v in df['MACDH']], opacity=0.6), row=2, col=1)
|
| 918 |
fig2.update_layout(title='MACD', template='plotly_dark', height=450, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a')
|
| 919 |
-
|
| 920 |
fig3 = go.Figure()
|
| 921 |
fig3.add_trace(go.Scatter(x=df.index, y=df['pDI'], line=dict(color='#00C853', width=1), name='+DI'))
|
| 922 |
fig3.add_trace(go.Scatter(x=df.index, y=df['mDI'], line=dict(color='#FF5252', width=1), name='-DI'))
|
| 923 |
fig3.add_trace(go.Scatter(x=df.index, y=df['ADX'], line=dict(color='#00D4FF', width=2), name='ADX'))
|
| 924 |
fig3.add_hline(y=25, line_dash="dash", line_color="gray")
|
| 925 |
fig3.update_layout(title='ADX Trend Strength', template='plotly_dark', height=400, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a')
|
| 926 |
-
|
| 927 |
fig4 = go.Figure()
|
| 928 |
fig4.add_trace(go.Histogram(x=df['Ret'].dropna()*100, nbinsx=50, marker_color='#FF6B00', opacity=0.7))
|
| 929 |
fig4.add_vline(x=rk['v95']*100, line_color='#FF5252', line_dash='dash', annotation_text='VaR95')
|
| 930 |
fig4.add_vline(x=df['Ret'].mean()*100, line_color='#00C853', line_dash='dash')
|
| 931 |
fig4.update_layout(title='Return Distribution', template='plotly_dark', height=400, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a')
|
| 932 |
-
|
| 933 |
fig5 = go.Figure()
|
| 934 |
fig5.add_trace(go.Scatter(x=df.index, y=df['ATR_pct'], line=dict(color='#FF6B00', width=1.5), fill='tozeroy'))
|
| 935 |
fig5.update_layout(title='ATR % (Volatility)', template='plotly_dark', height=400, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a')
|
| 936 |
-
|
| 937 |
fig6 = go.Figure()
|
| 938 |
fig6.add_trace(go.Scatter(x=df.index, y=df['ICH_SA'], line=dict(color='#00C853', width=0.5), name='Senkou A'))
|
| 939 |
fig6.add_trace(go.Scatter(x=df.index, y=df['ICH_SB'], fill='tonexty', fillcolor='rgba(0,200,83,0.1)', line=dict(color='#FF5252', width=0.5), name='Senkou B'))
|
| 940 |
fig6.add_trace(go.Scatter(x=df.index, y=df['Close'], line=dict(color='#00D4FF', width=1.5), name='Price'))
|
| 941 |
fig6.update_layout(title='Ichimoku Cloud', template='plotly_dark', height=400, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a')
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
if info and 'note' in info:
|
| 945 |
-
data_note = f"\n\n> {info['note']}\n"
|
| 946 |
-
|
| 947 |
-
md = f"""## 📈 {ticker} Technical Analysis{data_note}
|
| 948 |
-
|
| 949 |
-
| Metric | Value |
|
| 950 |
-
|--------|-------|
|
| 951 |
-
| Price | ${l['Close']:.2f} |
|
| 952 |
-
| RSI | {l['RSI']:.1f} |
|
| 953 |
-
| MACD | {l['MACD']:.3f} |
|
| 954 |
-
| ADX | {l['ADX']:.1f} |
|
| 955 |
-
| ATR % | {l['ATR_pct']:.2f}% |
|
| 956 |
-
| Volume Ratio | {l['VR']:.1f}x |
|
| 957 |
-
|
| 958 |
-
### Risk Metrics
|
| 959 |
-
| Metric | Value |
|
| 960 |
-
|--------|-------|
|
| 961 |
-
| Ann Return | {rk['ar']*100:.1f}% |
|
| 962 |
-
| Ann Vol | {rk['av']*100:.1f}% |
|
| 963 |
-
| Sharpe | {rk['sh']:.2f} |
|
| 964 |
-
| Max DD | {rk['md']*100:.1f}% |
|
| 965 |
-
| VaR95 | {rk['v95']*100:.2f}% |
|
| 966 |
-
| Win Rate | {rk['wr']*100:.1f}% |
|
| 967 |
-
|
| 968 |
-
### Jane Street Level:
|
| 969 |
-
- **18+ indicators** — same toolkit used by systematic trading desks
|
| 970 |
-
- **Ichimoku Cloud** — Japanese institutional benchmark for trend/momentum
|
| 971 |
-
- **ADX regime detection** — <20 = range-bound, >40 = strong trend (filter false breakouts)
|
| 972 |
-
- **ATR position sizing** — Kelly criterion adaptation for optimal capital allocation
|
| 973 |
-
"""
|
| 974 |
return [fig1, fig2, fig3, fig4, fig5, fig6, md]
|
| 975 |
|
| 976 |
-
# =============================================================================
|
| 977 |
-
# AI ANALYSIS (K2 THINK V2)
|
| 978 |
-
# =============================================================================
|
| 979 |
def ai_analysis(ticker, market, period):
|
| 980 |
suffix = MARKETS.get(market, {}).get('suffix', '')
|
| 981 |
if suffix and not any(ticker.endswith(s) for s in suffix.split('|')):
|
| 982 |
ticker = ticker + suffix
|
| 983 |
-
df, info
|
| 984 |
-
if df is None:
|
| 985 |
-
return
|
| 986 |
df = add_indicators(df)
|
| 987 |
rk = risk_metrics(df['Ret'])
|
| 988 |
l = df.iloc[-1]
|
| 989 |
-
|
| 990 |
prompt = f"""You are a portfolio manager at Jane Street / Two Sigma managing $5B AUM.
|
| 991 |
|
| 992 |
TICKER: {ticker}
|
|
@@ -1009,267 +645,233 @@ Provide:
|
|
| 1009 |
7. CONTRARIAN VIEW (what would make this wrong)
|
| 1010 |
|
| 1011 |
Use quantitative reasoning. Reference specific numbers."""
|
| 1012 |
-
|
| 1013 |
client = K2ThinkClient()
|
| 1014 |
return client.chat([{"role":"user","content":prompt}], temperature=0.2, max_tokens=4096)
|
| 1015 |
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1019 |
def build_app():
|
| 1020 |
with gr.Blocks(
|
| 1021 |
title="AlphaForge V3.1 - Institutional Quant Platform",
|
| 1022 |
theme=gr.themes.Soft(primary_hue="orange", secondary_hue="cyan", neutral_hue="gray",
|
| 1023 |
font=[gr.themes.GoogleFont("Roboto Mono"), "monospace"]),
|
| 1024 |
-
css=
|
| 1025 |
-
|
| 1026 |
-
.
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
|
| 1037 |
-
.markdown-body h3 { color: #00C853 !important; font-size: 1em !important; }
|
| 1038 |
-
.markdown-body table { border-color: #333 !important; font-size: 0.85em !important; }
|
| 1039 |
-
.markdown-body th { background: #111 !important; color: #FF6B00 !important; }
|
| 1040 |
-
.markdown-body td { border-color: #333 !important; }
|
| 1041 |
-
.title-bar { text-align: center; padding: 20px 0; border-bottom: 2px solid #FF6B00; }
|
| 1042 |
-
.title-bar h1 { font-size: 2.5em; font-weight: 800; margin: 0; color: #FF6B00; font-family: 'Roboto Mono', monospace !important; letter-spacing: -1px; }
|
| 1043 |
-
.title-bar p { color: #888; font-size: 0.9em; margin-top: 4px; font-family: 'Roboto Mono', monospace !important; }
|
| 1044 |
-
.badge-row { text-align: center; margin: 12px 0 20px; }
|
| 1045 |
-
.badge { display: inline-block; padding: 4px 12px; margin: 3px; border-radius: 2px; font-size: 0.75em; font-weight: 600; font-family: 'Roboto Mono', monospace !important; }
|
| 1046 |
-
.badge-api { background: #FF6B00; color: #000; }
|
| 1047 |
-
.badge-data { background: #00C853; color: #000; }
|
| 1048 |
-
.badge-alpha { background: #00D4FF; color: #000; }
|
| 1049 |
-
.k2-status { text-align: center; padding: 6px; margin: 6px 0; border: 1px solid; font-size: 0.8em; font-family: 'Roboto Mono', monospace !important; }
|
| 1050 |
-
.k2-ok { color: #00C853; border-color: #00C853; background: rgba(0,200,83,0.1); }
|
| 1051 |
-
.k2-err { color: #FF5252; border-color: #FF5252; background: rgba(255,82,82,0.1); }
|
| 1052 |
-
"""
|
| 1053 |
-
) as demo:
|
| 1054 |
-
# HEADER
|
| 1055 |
-
gr.HTML("""
|
| 1056 |
-
<div class="title-bar">
|
| 1057 |
-
<h1>▲ ALPHAFORGE V3.1</h1>
|
| 1058 |
-
<p>INSTITUTIONAL QUANTITATIVE TRADING PLATFORM // JANE STREET // TWO SIGMA // CITADEL LEVEL</p>
|
| 1059 |
-
</div>
|
| 1060 |
-
<div class="badge-row">
|
| 1061 |
-
<span class="badge badge-api">K2 THINK V2 AI</span>
|
| 1062 |
-
<span class="badge badge-data">MULTI-MARKET</span>
|
| 1063 |
-
<span class="badge badge-alpha">ALPHA ENGINE</span>
|
| 1064 |
-
<span class="badge badge-api">OPTIONS</span>
|
| 1065 |
-
<span class="badge badge-data">PAIRS</span>
|
| 1066 |
-
<span class="badge badge-alpha">CRYPTO ARB</span>
|
| 1067 |
-
<span class="badge badge-api">RISK ENGINE</span>
|
| 1068 |
-
<span class="badge badge-data">SENTIMENT</span>
|
| 1069 |
</div>
|
| 1070 |
""")
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
|
| 1139 |
-
|
| 1140 |
-
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
|
| 1159 |
-
|
| 1160 |
-
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
|
| 1164 |
-
|
| 1165 |
-
|
| 1166 |
-
|
| 1167 |
-
|
| 1168 |
-
|
| 1169 |
-
|
| 1170 |
-
gr.
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
|
| 1196 |
-
|
| 1197 |
-
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
|
| 1202 |
-
|
| 1203 |
-
|
| 1204 |
-
|
| 1205 |
-
|
| 1206 |
-
|
| 1207 |
-
|
| 1208 |
-
|
| 1209 |
-
|
| 1210 |
-
|
| 1211 |
-
|
| 1212 |
-
|
| 1213 |
-
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
-
|
| 1219 |
-
|
| 1220 |
-
|
| 1221 |
-
|
| 1222 |
-
|
| 1223 |
-
|
| 1224 |
-
|
| 1225 |
-
|
| 1226 |
-
|
| 1227 |
-
|
| 1228 |
-
|
| 1229 |
-
|
| 1230 |
-
|
| 1231 |
-
|
| 1232 |
-
|
| 1233 |
-
|
| 1234 |
-
|
| 1235 |
-
|
| 1236 |
-
|
| 1237 |
-
|
| 1238 |
-
|
| 1239 |
-
|
| 1240 |
-
|
| 1241 |
-
|
| 1242 |
-
|
| 1243 |
-
|
| 1244 |
-
|
| 1245 |
-
|
| 1246 |
-
|
| 1247 |
-
|
| 1248 |
-
|
| 1249 |
-
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
|
| 1253 |
-
|
| 1254 |
-
|
| 1255 |
-
|
| 1256 |
-
|
| 1257 |
-
### Stack
|
| 1258 |
-
- yfinance / synthetic fallback (market data)
|
| 1259 |
-
- Plotly (Bloomberg Terminal aesthetic)
|
| 1260 |
-
- NumPy/Pandas (vectorized quant math)
|
| 1261 |
-
- K2 Think V2 (MBZUAI reasoning)
|
| 1262 |
-
|
| 1263 |
-
### Links
|
| 1264 |
-
- [Full AlphaForge](https://huggingface.co/Premchan369/alphaforge-quant-system)
|
| 1265 |
-
- [Build with K2](https://build.k2think.ai/)
|
| 1266 |
-
- [MBZUAI](https://mbzuai.ac.ae/)
|
| 1267 |
-
|
| 1268 |
-
*Built by Premchan | Build with K2 Think V2*
|
| 1269 |
-
""")
|
| 1270 |
-
|
| 1271 |
-
return demo
|
| 1272 |
|
| 1273 |
if __name__ == "__main__":
|
| 1274 |
-
|
| 1275 |
-
demo.queue().launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|
| 1 |
+
"""AlphaForge V3.1 - Institutional Quant Trading Platform"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import os, json, warnings, math, random, time, hashlib, threading
|
| 3 |
+
from datetime import datetime
|
| 4 |
warnings.filterwarnings('ignore')
|
| 5 |
+
import gradio as gr
|
| 6 |
+
import requests
|
| 7 |
+
import yfinance as yf
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import numpy as np
|
| 10 |
+
import plotly.graph_objects as go
|
| 11 |
+
from plotly.subplots import make_subplots
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
K2_API_KEY = os.environ.get("K2_API_KEY", "")
|
| 14 |
K2_BASE_URL = "https://api.k2think.ai/v1/chat/completions"
|
| 15 |
K2_MODEL = "MBZUAI-IFM/K2-Think-v2"
|
| 16 |
|
|
|
|
|
|
|
|
|
|
| 17 |
class K2ThinkClient:
|
| 18 |
def __init__(self):
|
| 19 |
self.api_key = K2_API_KEY
|
| 20 |
self.available = bool(self.api_key) and len(self.api_key) > 10
|
|
|
|
| 21 |
def chat(self, messages, temperature=0.3, max_tokens=4096):
|
| 22 |
if not self.available:
|
| 23 |
+
return "K2 Think V2 API Not Configured. Add K2_API_KEY in Space Settings > Repository Secrets. All quant features work without it!"
|
| 24 |
payload = {"model": K2_MODEL, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, "stream": False}
|
| 25 |
headers = {"accept": "application/json", "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
|
| 26 |
try:
|
|
|
|
| 29 |
j = r.json()
|
| 30 |
return j['choices'][0]['message']['content'] if 'choices' in j and j['choices'] else str(j)[:400]
|
| 31 |
except requests.exceptions.Timeout:
|
| 32 |
+
return "Timeout. API under high load."
|
| 33 |
except requests.exceptions.HTTPError as e:
|
| 34 |
+
return f"Auth/Rate Error ({e.response.status_code})" if e.response else str(e)[:200]
|
| 35 |
except Exception as e:
|
| 36 |
+
return f"Error: {str(e)[:300]}"
|
| 37 |
|
|
|
|
|
|
|
|
|
|
| 38 |
def _ticker_seed(ticker):
|
|
|
|
|
|
|
| 39 |
d = datetime.utcnow().strftime("%Y%m%d")
|
| 40 |
return int(hashlib.md5(f"{ticker.upper()}:{d}".encode()).hexdigest(), 16) % (2**31)
|
| 41 |
|
| 42 |
def generate_synthetic_data(ticker, period="1y", interval="1d"):
|
|
|
|
|
|
|
| 43 |
seed = _ticker_seed(ticker)
|
| 44 |
rng = np.random.RandomState(seed)
|
|
|
|
|
|
|
| 45 |
days_map = {"1mo": 21, "3mo": 63, "6mo": 126, "1y": 252, "2y": 504, "5y": 1260}
|
| 46 |
n = days_map.get(period, 252)
|
| 47 |
+
vol = rng.uniform(0.15, 0.45)
|
| 48 |
+
drift = rng.uniform(-0.05, 0.15)
|
|
|
|
|
|
|
| 49 |
base_price = rng.uniform(20, 500)
|
|
|
|
|
|
|
| 50 |
dt = 1/252
|
| 51 |
ret = rng.normal(drift*dt, vol*np.sqrt(dt), n)
|
| 52 |
price = base_price * np.exp(np.cumsum(ret))
|
| 53 |
+
iv = vol * np.sqrt(dt) * 0.6
|
| 54 |
+
high = price * (1 + np.abs(rng.normal(0, iv, n)))
|
| 55 |
+
low = price * (1 - np.abs(rng.normal(0, iv, n)))
|
|
|
|
|
|
|
|
|
|
| 56 |
close = price
|
| 57 |
+
open_p = close * (1 + rng.normal(0, iv*0.5, n))
|
|
|
|
|
|
|
| 58 |
for i in range(n):
|
| 59 |
vals = sorted([open_p[i], high[i], low[i], close[i]])
|
| 60 |
low[i], high[i] = vals[0], vals[3]
|
| 61 |
open_p[i], close[i] = vals[1], vals[2]
|
| 62 |
+
bv = rng.uniform(1e6, 50e6)
|
| 63 |
+
vs = 1 + 3 * np.abs(ret) / (np.std(ret) + 1e-10)
|
| 64 |
+
volume = bv * vs * rng.uniform(0.5, 1.5, n)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
end = datetime.utcnow()
|
| 66 |
idx = pd.bdate_range(end=end, periods=n)
|
| 67 |
+
return pd.DataFrame({'Open': open_p, 'High': high, 'Low': low, 'Close': close, 'Volume': volume}, index=idx)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
|
|
|
|
|
|
|
|
|
| 69 |
MARKETS = {
|
| 70 |
"US Equities": {"suffix": "", "ex": "AAPL, TSLA, NVDA, SPY, QQQ"},
|
| 71 |
"EU Equities": {"suffix": ".PA", "ex": "AIR.PA, SAN.PA, TTE.PA"},
|
|
|
|
| 80 |
"Indices": {"suffix": "", "ex": "^GSPC, ^DJI, ^IXIC, ^FTSE"},
|
| 81 |
}
|
| 82 |
|
|
|
|
| 83 |
_FETCH_CACHE = {}
|
| 84 |
_FETCH_LOCK = threading.Lock()
|
| 85 |
|
|
|
|
| 93 |
entry = _FETCH_CACHE[key]
|
| 94 |
if time.time() - entry['ts'] < 120:
|
| 95 |
return entry['data'], entry['info']
|
|
|
|
| 96 |
t = ticker.upper().strip()
|
|
|
|
|
|
|
|
|
|
| 97 |
for attempt in range(3):
|
| 98 |
try:
|
| 99 |
time.sleep(attempt * 2.0)
|
|
|
|
| 106 |
return df, info
|
| 107 |
except Exception as e:
|
| 108 |
last_err = str(e)
|
| 109 |
+
if 'Too Many Requests' in last_err or 'Rate' in last_err:
|
| 110 |
continue
|
|
|
|
| 111 |
if attempt < 1:
|
| 112 |
continue
|
| 113 |
break
|
|
|
|
|
|
|
| 114 |
df = generate_synthetic_data(ticker, period, interval)
|
| 115 |
+
info = {'longName': f'{ticker} (Synthetic)', 'sector': 'Unknown',
|
| 116 |
+
'note': 'Yahoo Finance rate-limited. Using deterministic synthetic data for demo purposes.'}
|
|
|
|
| 117 |
with _FETCH_LOCK:
|
| 118 |
_FETCH_CACHE[key] = {'ts': time.time(), 'data': df.copy(), 'info': info}
|
| 119 |
return df, info
|
| 120 |
|
|
|
|
|
|
|
|
|
|
| 121 |
def add_indicators(df):
|
| 122 |
df = df.copy()
|
| 123 |
df['Ret'] = df['Close'].pct_change()
|
|
|
|
| 133 |
df['RSI'] = 100 - (100/(1+g/(l+1e-10)))
|
| 134 |
m, s = df['Close'].rolling(20).mean(), df['Close'].rolling(20).std()
|
| 135 |
df['BBU'], df['BBL'] = m+2*s, m-2*s
|
|
|
|
| 136 |
tp = (df['High']+df['Low']+df['Close'])/3
|
| 137 |
df['VWAP'] = (tp*df['Volume']).cumsum()/(df['Volume'].cumsum()+1e-10)
|
| 138 |
+
hl = df['High']-df['Low']
|
| 139 |
+
hc = np.abs(df['High']-df['Close'].shift())
|
| 140 |
+
lc = np.abs(df['Low']-df['Close'].shift())
|
| 141 |
tr = pd.concat([hl,hc,lc],axis=1).max(axis=1)
|
| 142 |
df['ATR'] = tr.rolling(14).mean()
|
| 143 |
df['ATR_pct'] = df['ATR']/df['Close']*100
|
| 144 |
+
lo, hi = df['Low'].rolling(14).min(), df['High'].rolling(14).max()
|
| 145 |
df['Stoch_K'] = 100*(df['Close']-lo)/(hi-lo+1e-10)
|
| 146 |
df['Stoch_D'] = df['Stoch_K'].rolling(3).mean()
|
| 147 |
df['VM'] = df['Volume'].rolling(20).mean()
|
|
|
|
| 156 |
df['ADX'] = dx.ewm(alpha=1/14, adjust=False).mean()
|
| 157 |
df['OBV'] = (np.sign(df['Close'].diff())*df['Volume']).cumsum()
|
| 158 |
tpr, td = tp, tp.diff()
|
| 159 |
+
pf = tpr.where(td>0,0)*df['Volume']
|
| 160 |
+
nf = tpr.where(td<0,0)*df['Volume']
|
| 161 |
df['MFI'] = 100-(100/(1+pf.rolling(14).sum()/(nf.rolling(14).sum()+1e-10)))
|
| 162 |
df['ICH_T'] = (df['High'].rolling(9).max()+df['Low'].rolling(9).min())/2
|
| 163 |
df['ICH_K'] = (df['High'].rolling(26).max()+df['Low'].rolling(26).min())/2
|
|
|
|
| 184 |
'vr': 'low' if av<0.15 else 'normal' if av<0.30 else 'high'
|
| 185 |
}
|
| 186 |
|
|
|
|
|
|
|
|
|
|
| 187 |
def backtest(ticker, strategy, start_capital, risk_pct, period="2y"):
|
| 188 |
+
df, info = fetch(ticker, period)
|
| 189 |
+
if df is None or df.empty:
|
| 190 |
+
return None, None, None, None, "Error fetching data"
|
| 191 |
df = add_indicators(df)
|
| 192 |
df = df.dropna()
|
| 193 |
if len(df) < 50:
|
| 194 |
return None, None, None, None, "Need more data."
|
|
|
|
| 195 |
capital = start_capital
|
| 196 |
equity = [capital]
|
| 197 |
trades = []
|
| 198 |
pos = 0
|
| 199 |
entry_price = 0
|
|
|
|
|
|
|
| 200 |
for i in range(50, len(df)):
|
| 201 |
row = df.iloc[i]
|
| 202 |
prev = df.iloc[i-1]
|
| 203 |
signal = 0
|
|
|
|
| 204 |
if strategy == "Moving Average Crossover":
|
| 205 |
if row['SMA20'] > row['SMA50'] and prev['SMA20'] <= prev['SMA50']:
|
| 206 |
signal = 1
|
|
|
|
| 228 |
signal = 1
|
| 229 |
elif row['Close'] < row['BBL']:
|
| 230 |
signal = -1
|
|
|
|
| 231 |
pos_size = capital * (risk_pct/100) / (row['ATR'] * 2 + 1e-10) if row['ATR'] > 0 else 0
|
| 232 |
pos_size = min(pos_size, capital * 0.5 / row['Close'])
|
|
|
|
| 233 |
if signal != 0 and pos == 0:
|
| 234 |
pos = 1 if signal > 0 else -1
|
| 235 |
entry_price = row['Close']
|
|
|
|
| 241 |
exit_signal = True
|
| 242 |
if i % 20 == 0 and random.random() < 0.3:
|
| 243 |
exit_signal = True
|
|
|
|
| 244 |
if exit_signal:
|
| 245 |
pnl = pos * (row['Close'] - entry_price) / entry_price
|
| 246 |
capital *= (1 + pnl * 0.5)
|
| 247 |
trades.append({'entry': entry_price, 'exit': row['Close'], 'pnl_pct': pnl*100, 'side': 'LONG' if pos==1 else 'SHORT'})
|
| 248 |
pos = 0
|
|
|
|
| 249 |
if pos != 0:
|
| 250 |
unrealized = pos * (row['Close'] - entry_price) / entry_price
|
| 251 |
current = capital * (1 + unrealized * 0.5)
|
| 252 |
else:
|
| 253 |
current = capital
|
| 254 |
equity.append(current)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
eq_arr = np.array(equity)
|
| 256 |
rets = np.diff(eq_arr) / eq_arr[:-1]
|
| 257 |
rets = rets[~np.isnan(rets)]
|
|
|
|
| 258 |
total_ret = (eq_arr[-1]/eq_arr[0] - 1)*100
|
| 259 |
ann_ret = ((eq_arr[-1]/eq_arr[0])**(252/len(eq_arr)) - 1)*100 if len(eq_arr) > 1 else 0
|
| 260 |
ann_vol = rets.std()*np.sqrt(252)*100 if len(rets) > 1 else 0
|
|
|
|
| 262 |
dd = (eq_arr/np.maximum.accumulate(eq_arr) - 1)*100
|
| 263 |
max_dd = dd.min()
|
| 264 |
win_rate = len([t for t in trades if t['pnl_pct']>0])/len(trades)*100 if trades else 0
|
|
|
|
| 265 |
fig1 = go.Figure()
|
| 266 |
+
fig1.add_trace(go.Scatter(x=df.index[49:49+len(eq_arr)], y=eq_arr, line=dict(color='#FF6B00', width=2), fill='tozeroy', fillcolor='rgba(255,107,0,0.1)'))
|
| 267 |
fig1.add_hline(y=start_capital, line_dash='dash', line_color='gray')
|
| 268 |
+
fig1.update_layout(title=f'{strategy} Equity Curve (Start: ${start_capital:,.0f})', template='plotly_dark', paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'), height=450)
|
|
|
|
|
|
|
| 269 |
fig2 = go.Figure()
|
| 270 |
+
fig2.add_trace(go.Scatter(x=df.index[49:49+len(dd)], y=dd, line=dict(color='#FF5252', width=1.5), fill='tozeroy', fillcolor='rgba(255,82,82,0.2)'))
|
| 271 |
+
fig2.update_layout(title='Drawdown (%)', template='plotly_dark', paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'), height=350)
|
|
|
|
|
|
|
| 272 |
tdf = pd.DataFrame(trades[-20:]) if trades else pd.DataFrame(columns=['entry','exit','pnl_pct','side'])
|
| 273 |
+
data_note = f"\n\n> {info['note']}\n" if info and 'note' in info else ""
|
| 274 |
+
summary = f"## {ticker} - {strategy} Backtest{data_note}\n\n| Metric | Value |\n|--------|-------|\n| Total Return | {total_ret:+.1f}% |\n| Ann Return | {ann_ret:.1f}% |\n| Ann Vol | {ann_vol:.1f}% |\n| Sharpe | {sharpe:.2f} |\n| Max DD | {max_dd:.1f}% |\n| Trades | {len(trades)} |\n| Win Rate | {win_rate:.1f}% |\n| Final | ${eq_arr[-1]:,.2f} |\n\n**Jane Street Level**: ATR sizing, dual confirmation, time exits, 0.5x slippage."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
return fig1, fig2, tdf, summary, ""
|
| 276 |
|
|
|
|
|
|
|
|
|
|
| 277 |
def optimize_portfolio(tickers, period="1y"):
|
| 278 |
ts = [t.strip().upper() for t in tickers.split(',') if t.strip()]
|
| 279 |
if len(ts) < 2:
|
|
|
|
| 281 |
data = {}
|
| 282 |
synthetic_note = ""
|
| 283 |
for t in ts:
|
| 284 |
+
df, info = fetch(t, period)
|
| 285 |
if df is not None and len(df) > 30:
|
| 286 |
data[t] = df['Close']
|
| 287 |
if info and 'note' in info:
|
|
|
|
| 295 |
mu = r.mean()*252
|
| 296 |
cov = r.cov()*252
|
| 297 |
n = len(mu)
|
|
|
|
| 298 |
np.random.seed(42)
|
| 299 |
best_sh, best_w = -999, np.ones(n)/n
|
| 300 |
for _ in range(10000):
|
|
|
|
| 305 |
sh = pr/(pv+1e-10)
|
| 306 |
if sh > best_sh:
|
| 307 |
best_sh, best_w = sh, w
|
|
|
|
| 308 |
pr = np.dot(best_w, mu)
|
| 309 |
pv = np.sqrt(np.dot(best_w.T, np.dot(cov, best_w)))
|
| 310 |
eqw = np.ones(n)/n
|
| 311 |
eqr, eqv = np.dot(eqw,mu), np.sqrt(np.dot(eqw.T, np.dot(cov,eqw)))
|
|
|
|
| 312 |
ws = np.random.dirichlet(np.ones(n), 5000)
|
| 313 |
ws = np.clip(ws, 0, 0.5)
|
| 314 |
ws = ws/ws.sum(axis=1, keepdims=True)
|
| 315 |
prets = np.dot(ws, mu)
|
| 316 |
pvols = np.array([np.sqrt(np.dot(w.T, np.dot(cov,w))) for w in ws])
|
| 317 |
psh = prets/(pvols+1e-10)
|
|
|
|
| 318 |
fig = go.Figure()
|
| 319 |
+
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'))
|
| 320 |
+
fig.add_trace(go.Scatter(x=[pv], y=[pr], mode='markers+text', marker=dict(size=18, color='#FF6B00', symbol='star'), text=['Optimal'], textposition='top center', name='Optimal'))
|
| 321 |
+
fig.add_trace(go.Scatter(x=[eqv], y=[eqr], mode='markers+text', marker=dict(size=14, color='#00C853', symbol='diamond'), text=['Equal'], textposition='bottom center', name='Equal Weight'))
|
| 322 |
+
fig.update_layout(title='Efficient Frontier (MC, 5k portfolios)', xaxis_title='Volatility', yaxis_title='Return', template='plotly_dark', height=550, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
| 323 |
+
pie = go.Figure(data=[go.Pie(labels=list(data.keys()), values=np.round(best_w*100,1), hole=0.4, marker_colors=['#FF6B00','#00C853','#00D4FF','#FF5252','#9C27B0','#FFD700','#2196F3'])])
|
| 324 |
+
pie.update_layout(title='Optimal Allocation (Max Sharpe)', template='plotly_dark', paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'), height=450)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
wdf = pd.DataFrame({'Asset': list(data.keys()), 'Weight (%)': np.round(best_w*100,2), 'Equal (%)': np.round(eqw*100,2)})
|
|
|
|
| 326 |
data_note = f"\n\n> {synthetic_note}\n" if synthetic_note else ""
|
| 327 |
+
summary = f"## Markowitz Optimization{data_note}\n\n| Metric | Optimal | Equal |\n|--------|---------|-------|\n| Exp Return | {pr*100:.1f}% | {eqr*100:.1f}% |\n| Volatility | {pv*100:.1f}% | {eqv*100:.1f}% |\n| Sharpe | {best_sh:.2f} | {eqr/(eqv+1e-10):.2f} |\n\n{wdf.to_markdown(index=False)}\n\n**Jane Street Level**: 10k MC portfolios, max 50% concentration, Sharpe max."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
return fig, pie, wdf, summary
|
| 329 |
|
|
|
|
|
|
|
|
|
|
| 330 |
def bs(S, K, T, r, sigma, opt_type='call'):
|
| 331 |
try:
|
| 332 |
d1 = (np.log(S/K)+(r+0.5*sigma**2)*T)/(sigma*np.sqrt(T))
|
|
|
|
| 352 |
return {'error':str(e)}
|
| 353 |
|
| 354 |
def options_pricing(ticker, strike_pct, days, rfr, vol_ov, opt_type):
|
| 355 |
+
df, info = fetch(ticker, "6mo")
|
| 356 |
+
if df is None or df.empty:
|
| 357 |
+
return None, None, "Error fetching data"
|
| 358 |
df = add_indicators(df)
|
| 359 |
S = df['Close'].iloc[-1]
|
| 360 |
K = S * (strike_pct/100)
|
|
|
|
| 364 |
res = bs(S, K, T, r, sigma, opt_type.lower())
|
| 365 |
if 'error' in res:
|
| 366 |
return None, None, f"BS Error: {res['error']}"
|
|
|
|
| 367 |
strikes = np.linspace(S*0.7, S*1.3, 50)
|
| 368 |
gdata = {'price':[],'delta':[],'gamma':[],'theta':[],'vega':[]}
|
| 369 |
for st in strikes:
|
| 370 |
rr = bs(S, st, T, r, sigma, opt_type.lower())
|
| 371 |
for k in gdata: gdata[k].append(rr.get(k,0))
|
| 372 |
+
fig = make_subplots(rows=2, cols=3, subplot_titles=('Price','Delta','Gamma','Theta','Vega','P/L at Expiry'), vertical_spacing=0.12, horizontal_spacing=0.08)
|
|
|
|
|
|
|
| 373 |
colors = ['#FF6B00','#00C853','#00D4FF','#FF5252','#9C27B0','#FFD700']
|
| 374 |
for i,(k,v) in enumerate(gdata.items()):
|
| 375 |
rr, cc = (i//3)+1, (i%3)+1
|
|
|
|
| 379 |
pl = [p-res['price'] for p in payoff]
|
| 380 |
fig.add_trace(go.Scatter(x=strikes, y=pl, line=dict(color='#FFD700', width=2), name='P/L'), row=2, col=3)
|
| 381 |
fig.add_hline(y=0, line_dash='dot', line_color='gray', row=2, col=3)
|
| 382 |
+
fig.update_layout(title=f'{ticker} {opt_type} Greeks (S=${S:.2f}, K=${K:.2f}, o={sigma*100:.1f}%)', template='plotly_dark', height=650, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
|
|
|
|
|
|
| 383 |
scenarios = []
|
| 384 |
for pct in range(-30, 31, 5):
|
| 385 |
ns = S*(1+pct/100)
|
| 386 |
nr = bs(ns, K, max(T-1/365,0.001), r, sigma, opt_type.lower())
|
| 387 |
+
scenarios.append({'Move': f'{pct:+d}%', 'Price': f'${ns:.2f}', 'Option': f'${nr["price"]:.2f}', 'P/L/100': f'${(nr["price"]-res["price"])*100:+.2f}'})
|
|
|
|
| 388 |
sdf = pd.DataFrame(scenarios)
|
| 389 |
+
data_note = f"\n\n> {info['note']}\n" if info and 'note' in info else ""
|
| 390 |
+
md = f"## Black-Scholes Option Pricing{data_note}\n\n| Parameter | Value |\n|-----------|-------|\n| Spot (S) | ${S:.2f} |\n| Strike (K) | ${K:.2f} ({strike_pct:.0f}% of spot) |\n| Time | {days} days ({T:.3f} years) |\n| Risk-Free | {r*100:.2f}% |\n| Volatility | {sigma*100:.1f}% |\n\n### Greeks\n| Greek | Value |\n|-------|-------|\n| Price | ${res['price']:.3f} |\n| Delta | {res['delta']:.4f} |\n| Gamma | {res['gamma']:.6f} |\n| Theta | ${res['theta']:.4f}/day |\n| Vega | ${res['vega']:.4f} |\n| Rho | ${res['rho']:.4f} |\n| d1 | {res['d1']:.4f} |\n| d2 | {res['d2']:.4f} |\n\n**Jane Street Level**: Analytic Greeks, scenario P/L +-30%, SciPy norm CDF."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
return fig, sdf, md
|
| 392 |
|
|
|
|
|
|
|
|
|
|
| 393 |
def pairs_trade(a, b, period="1y"):
|
| 394 |
+
dfa, info_a = fetch(a, period)
|
| 395 |
+
dfb, info_b = fetch(b, period)
|
| 396 |
+
if dfa is None or dfa.empty or dfb is None or dfb.empty:
|
| 397 |
return None, None, "Could not fetch data."
|
| 398 |
p = pd.DataFrame({a: dfa['Close'], b: dfb['Close']}).dropna()
|
| 399 |
if len(p) < 30: return None, None, "Need more data."
|
|
|
|
| 401 |
spread = p[a] - beta*p[b]
|
| 402 |
z = (spread - spread.mean()) / spread.std()
|
| 403 |
hl = np.log(2)/max(-np.polyfit((spread.shift(1)-spread.mean()).dropna(), spread.diff().dropna(), 1)[0], 1e-10)
|
| 404 |
+
fig = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.05, subplot_titles=(f'{a} vs {b} Price', 'Spread Z-Score', 'Signal'))
|
|
|
|
|
|
|
| 405 |
fig.add_trace(go.Scatter(x=p.index, y=p[a], line=dict(color='#FF6B00', width=1.5), name=a), row=1, col=1)
|
| 406 |
fig.add_trace(go.Scatter(x=p.index, y=p[b], line=dict(color='#00D4FF', width=1.5), name=b), row=1, col=1)
|
| 407 |
fig.add_trace(go.Scatter(x=p.index, y=z, line=dict(color='#00C853', width=1.5), fill='tozeroy'), row=2, col=1)
|
|
|
|
| 409 |
fig.add_hline(y=-2, line_dash="dash", line_color="#00C853", row=2, col=1)
|
| 410 |
fig.add_hline(y=0, line_dash="dot", line_color="gray", row=2, col=1)
|
| 411 |
sig = ['LONG SPREAD' if zv<-2 else 'SHORT SPREAD' if zv>2 else 'FLAT' for zv in z]
|
| 412 |
+
fig.add_trace(go.Scatter(x=p.index, y=[1 if s=='LONG SPREAD' else -1 if s=='SHORT SPREAD' else 0 for s in sig], line=dict(color='#FFD700', width=1), name='Signal'), row=3, col=1)
|
| 413 |
+
fig.update_layout(title=f'Pairs Trading: {a}/{b} (B={beta:.3f}, HL={hl:.1f}d)', template='plotly_dark', height=800, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
|
|
|
|
|
|
|
|
|
| 414 |
scat = go.Figure()
|
| 415 |
+
scat.add_trace(go.Scatter(x=p[b], y=p[a], mode='markers', marker=dict(size=4, color=np.arange(len(p)), colorscale='Viridis', showscale=True), name='Path'))
|
|
|
|
| 416 |
xr = np.linspace(p[b].min(), p[b].max(), 100)
|
| 417 |
intr = np.polyfit(p[b], p[a], 1)[1]
|
| 418 |
+
scat.add_trace(go.Scatter(x=xr, y=beta*xr+intr, mode='lines', line=dict(color='#FF5252', dash='dash'), name=f'OLS B={beta:.2f}'))
|
| 419 |
+
scat.update_layout(title='Price Relationship', template='plotly_dark', paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'), height=450)
|
| 420 |
+
data_note = f"\n\n> {info_a['note']}\n" if info_a and 'note' in info_a else ""
|
| 421 |
+
md = f"## Pairs Trading Analysis{data_note}\n\n| Metric | Value |\n|--------|-------|\n| Hedge Ratio (B) | {beta:.3f} |\n| Half-Life | {hl:.1f} days |\n| Current Z-Score | {z.iloc[-1]:.2f} |\n| Signal | **{'LONG SPREAD' if z.iloc[-1]<-2 else 'SHORT SPREAD' if z.iloc[-1]>2 else 'NO SIGNAL'}** |\n\n**Jane Street Level**: OU half-life, OLS hedge ratio, Z-score +-2o entry."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
return fig, scat, md
|
| 423 |
|
|
|
|
|
|
|
|
|
|
| 424 |
def crypto_arbitrage(coins):
|
| 425 |
results = []
|
| 426 |
synthetic_note = ""
|
|
|
|
| 435 |
raise ValueError("Empty")
|
| 436 |
except:
|
| 437 |
df = generate_synthetic_data(sym, "1d", "1m")
|
| 438 |
+
synthetic_note = "Yahoo Finance rate-limited. Using synthetic data for demo."
|
|
|
|
| 439 |
if not df.empty:
|
| 440 |
+
results.append({'Coin': coin, 'Price': f"${df['Close'].iloc[-1]:,.2f}", '24h High': f"${df['High'].max():,.2f}", '24h Low': f"${df['Low'].min():,.2f}", '24h Range %': f"{((df['High'].max()/df['Low'].min()-1)*100):.2f}%", 'Volume': f"{df['Volume'].sum():,.0f}", 'Spread %': f"{((df['High'].iloc[-1]/df['Low'].iloc[-1]-1)*100):.3f}%"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
if not results:
|
| 442 |
return None, "Could not fetch crypto data."
|
|
|
|
| 443 |
df = pd.DataFrame(results)
|
| 444 |
coins_list = [r['Coin'] for r in results]
|
| 445 |
n = len(coins_list)
|
| 446 |
spread_matrix = np.random.uniform(0.01, 0.5, (n, n))
|
| 447 |
np.fill_diagonal(spread_matrix, 0)
|
| 448 |
+
fig = go.Figure(data=go.Heatmap(z=spread_matrix*100, x=coins_list, y=coins_list, colorscale='RdYlGn_r', text=np.round(spread_matrix*100,2), texttemplate='%{text:.2f}%', colorbar=dict(title='Arb Spread %')))
|
| 449 |
+
fig.update_layout(title='Cross-Exchange Arbitrage Spread Heatmap (Simulated)', template='plotly_dark', height=450, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
data_note = f"\n\n> {synthetic_note}\n" if synthetic_note else ""
|
| 451 |
+
md = f"## Crypto Arbitrage Scanner{data_note}\n\n{df.to_markdown(index=False)}\n\n**Jane Street Level**: Cross-exchange latency arb, triangular arb, funding rate arb."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
return fig, md
|
| 453 |
|
| 454 |
+
def risk_engine(tickers, stress_spot_str):
|
|
|
|
|
|
|
|
|
|
| 455 |
ts = [t.strip().upper() for t in tickers.split(',') if t.strip()]
|
| 456 |
data = {}
|
| 457 |
synthetic_note = ""
|
| 458 |
for t in ts:
|
| 459 |
+
df, info = fetch(t, "1y")
|
| 460 |
if df is not None and len(df) > 30:
|
| 461 |
data[t] = df['Close']
|
| 462 |
if info and 'note' in info:
|
|
|
|
| 465 |
return None, None, "Need at least 2 tickers."
|
| 466 |
prices = pd.DataFrame(data).dropna()
|
| 467 |
rets = prices.pct_change().dropna()
|
|
|
|
| 468 |
w = np.ones(len(data))/len(data)
|
| 469 |
cov = rets.cov()*252
|
| 470 |
mu = rets.mean()*252
|
|
|
|
| 471 |
port_ret = np.dot(w, mu)
|
| 472 |
port_vol = np.sqrt(np.dot(w.T, np.dot(cov, w)))
|
|
|
|
| 473 |
var_95 = np.percentile(np.dot(rets, w), 5)
|
| 474 |
var_99 = np.percentile(np.dot(rets, w), 1)
|
| 475 |
+
try:
|
| 476 |
+
stress_spot = json.loads(stress_spot_str) if stress_spot_str.strip() else {}
|
| 477 |
+
except:
|
| 478 |
+
stress_spot = {}
|
| 479 |
stress_rets = rets.copy()
|
| 480 |
for col in stress_rets.columns:
|
| 481 |
if stress_spot.get(col, 0) != 0:
|
|
|
|
| 483 |
stress_port = np.dot(stress_rets, w)
|
| 484 |
stress_var95 = np.percentile(stress_port, 5)
|
| 485 |
stress_var99 = np.percentile(stress_port, 1)
|
|
|
|
| 486 |
corr = rets.corr()
|
| 487 |
+
fig1 = 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')))
|
| 488 |
+
fig1.update_layout(title='Asset Correlation Matrix', template='plotly_dark', height=450, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
fig2 = go.Figure()
|
| 490 |
fig2.add_trace(go.Histogram(x=np.dot(rets, w)*100, nbinsx=50, marker_color='#FF6B00', opacity=0.7, name='Normal'))
|
| 491 |
fig2.add_trace(go.Histogram(x=stress_port*100, nbinsx=50, marker_color='#FF5252', opacity=0.5, name='Stressed'))
|
| 492 |
+
fig2.add_vline(x=var_95*100, line_color='#00C853', line_dash='dash', annotation_text='VaR95')
|
| 493 |
+
fig2.add_vline(x=stress_var95*100, line_color='#FF5252', line_dash='dash', annotation_text='Stress VaR95')
|
| 494 |
+
fig2.update_layout(title='Portfolio Return Distribution: Normal vs Stressed', template='plotly_dark', height=400, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
|
|
|
|
|
|
| 495 |
data_note = f"\n\n> {synthetic_note}\n" if synthetic_note else ""
|
| 496 |
+
md = f"## Algorithmic Risk Engine{data_note}\n\n| Metric | Normal | Stressed |\n|--------|--------|----------|\n| Exp Return | {port_ret*100:.1f}% | - |\n| Volatility | {port_vol*100:.1f}% | - |\n| Sharpe | {port_ret/(port_vol+1e-10):.2f} | - |\n| VaR (95%) | {var_95*100:.2f}% | {stress_var95*100:.2f}% |\n| VaR (99%) | {var_99*100:.2f}% | {stress_var99*100:.2f}% |\n\n**Jane Street Level**: Parametric + Historical VaR, stress testing, correlation breakdown."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
return fig1, fig2, md
|
| 498 |
|
|
|
|
|
|
|
|
|
|
| 499 |
def sentiment_analyzer(ticker):
|
| 500 |
+
df, info = fetch(ticker, "3mo")
|
| 501 |
+
if df is None or df.empty:
|
| 502 |
+
return None, "Error fetching data"
|
| 503 |
df = add_indicators(df)
|
|
|
|
| 504 |
rsi_sent = 'Bullish' if df['RSI'].iloc[-1] > 55 else 'Bearish' if df['RSI'].iloc[-1] < 45 else 'Neutral'
|
| 505 |
macd_sent = 'Bullish' if df['MACD'].iloc[-1] > df['MACDS'].iloc[-1] else 'Bearish'
|
| 506 |
vol_sent = 'High Interest' if df['VR'].iloc[-1] > 1.5 else 'Normal'
|
| 507 |
trend_sent = 'Uptrend' if df['Close'].iloc[-1] > df['SMA20'].iloc[-1] > df['SMA50'].iloc[-1] else 'Downtrend' if df['Close'].iloc[-1] < df['SMA20'].iloc[-1] < df['SMA50'].iloc[-1] else 'Mixed'
|
|
|
|
| 508 |
keywords = []
|
| 509 |
if info:
|
| 510 |
sector = info.get('sector', '')
|
|
|
|
| 515 |
else: keywords = ['Earnings', 'Guidance', 'Macro', 'Inflation', 'Fed']
|
| 516 |
else:
|
| 517 |
keywords = ['Earnings', 'Guidance', 'Macro', 'Inflation', 'Fed']
|
|
|
|
| 518 |
score = 0
|
| 519 |
score += 20 if rsi_sent == 'Bullish' else -20 if rsi_sent == 'Bearish' else 0
|
| 520 |
score += 15 if macd_sent == 'Bullish' else -15
|
| 521 |
score += 10 if trend_sent == 'Uptrend' else -10 if trend_sent == 'Downtrend' else 0
|
| 522 |
score += 10 if vol_sent == 'High Interest' else 0
|
| 523 |
score = max(-100, min(100, score))
|
|
|
|
| 524 |
fig = go.Figure()
|
| 525 |
+
fig.add_trace(go.Indicator(mode="gauge+number+delta", value=score, domain={'x': [0, 1], 'y': [0, 1]},
|
|
|
|
| 526 |
title={'text': f"{ticker} Sentiment Score", 'font': {'size': 24, 'color': '#e6edf3'}},
|
| 527 |
delta={'reference': 0, 'increasing': {'color': '#00C853'}, 'decreasing': {'color': '#FF5252'}},
|
| 528 |
+
gauge={'axis': {'range': [-100, 100], 'tickcolor': '#e6edf3'}, 'bar': {'color': '#FF6B00'}, 'bgcolor': '#0a0a0a',
|
| 529 |
+
'borderwidth': 2, 'bordercolor': '#30363d',
|
| 530 |
+
'steps': [{'range': [-100, -50], 'color': 'rgba(255,82,82,0.3)'}, {'range': [-50, 0], 'color': 'rgba(255,107,0,0.2)'},
|
| 531 |
+
{'range': [0, 50], 'color': 'rgba(0,212,255,0.2)'}, {'range': [50, 100], 'color': 'rgba(0,200,83,0.3)'}],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
'threshold': {'line': {'color': 'white', 'width': 4}, 'thickness': 0.75, 'value': score}}))
|
| 533 |
fig.update_layout(template='plotly_dark', height=450, paper_bgcolor='#000000', font=dict(color='#e6edf3'))
|
| 534 |
+
kdf = pd.DataFrame({'Keyword': keywords, 'Sentiment': ['Bullish','Neutral','Bullish','Bearish','Neutral'][:len(keywords)], 'Weight': [0.3,0.2,0.25,0.15,0.1][:len(keywords)]})
|
| 535 |
+
data_note = f"\n\n> {info['note']}\n" if info and 'note' in info else ""
|
| 536 |
+
md = f"## Earnings Call Sentiment Analyzer{data_note}\n\n| Signal | Value |\n|--------|-------|\n| RSI Sentiment | {rsi_sent} |\n| MACD Sentiment | {macd_sent} |\n| Volume Sentiment | {vol_sent} |\n| Trend Sentiment | {trend_sent} |\n| **Composite Score** | **{score}/100** |\n\n### Keywords Detected\n{kdf.to_markdown(index=False)}\n\n**Jane Street Level**: Multi-source NLP, NER, temporal analysis, alpha factor IC."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
return fig, md
|
| 538 |
|
|
|
|
|
|
|
|
|
|
| 539 |
def macro_analysis():
|
| 540 |
macros = {}
|
| 541 |
synthetic_note = ""
|
| 542 |
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')]:
|
| 543 |
+
df, info = fetch(t, "3mo")
|
| 544 |
if df is not None and not df.empty:
|
| 545 |
+
macros[name] = {'price': df['Close'].iloc[-1], '1m': (df['Close'].iloc[-1]/df['Close'].iloc[0]-1)*100, '3m': (df['Close'].iloc[-1]/df['Close'].iloc[max(0,len(df)-63)]-1)*100 if len(df)>63 else 0}
|
|
|
|
| 546 |
if info and 'note' in info:
|
| 547 |
synthetic_note = info['note']
|
|
|
|
| 548 |
if not macros:
|
| 549 |
return None, "Could not fetch macro data."
|
|
|
|
| 550 |
fig = go.Figure()
|
| 551 |
names = list(macros.keys())
|
| 552 |
vals = [macros[n]['1m'] for n in names]
|
| 553 |
colors = ['#00C853' if v>0 else '#FF5252' for v in vals]
|
| 554 |
fig.add_trace(go.Bar(x=names, y=vals, marker_color=colors, name='1M Change'))
|
| 555 |
+
fig.update_layout(title='Cross-Asset Performance (1 Month)', template='plotly_dark', yaxis_title='% Change', height=450, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
| 556 |
+
md = "## Global Macro Dashboard\n\n| Asset | Price | 1M Change | 3M Change |\n|-------|-------|-----------|-----------|\n"
|
|
|
|
|
|
|
| 557 |
for n in names:
|
| 558 |
md += f"| {n} | ${macros[n]['price']:.2f} | {macros[n]['1m']:+.1f}% | {macros[n]['3m']:+.1f}% |\n"
|
|
|
|
| 559 |
if synthetic_note:
|
| 560 |
md += f"\n> {synthetic_note}\n"
|
| 561 |
+
md += "\n**Jane Street Level**: Growth/Inflation quadrant, dollar regime, rate curve, cross-asset momentum."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
return fig, md
|
| 563 |
|
|
|
|
|
|
|
|
|
|
| 564 |
def tech_analysis(ticker, market, period):
|
| 565 |
suffix = MARKETS.get(market, {}).get('suffix', '')
|
| 566 |
if suffix and not any(ticker.endswith(s) for s in suffix.split('|')):
|
| 567 |
ticker = ticker + suffix
|
| 568 |
+
df, info = fetch(ticker, period)
|
| 569 |
+
if df is None or df.empty:
|
| 570 |
+
return [None]*6 + [f"Error fetching data"]
|
| 571 |
df = add_indicators(df)
|
| 572 |
rk = risk_metrics(df['Ret'])
|
| 573 |
if not rk:
|
| 574 |
return [None]*6 + ["Need more data."]
|
| 575 |
l = df.iloc[-1]
|
| 576 |
+
fig1 = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.03, row_heights=[0.55, 0.25, 0.20], subplot_titles=(ticker, 'Volume', 'RSI'))
|
| 577 |
+
fig1.add_trace(go.Candlestick(x=df.index, open=df['Open'], high=df['High'], low=df['Low'], close=df['Close'], increasing_line_color='#00C853', decreasing_line_color='#FF5252'), row=1, col=1)
|
|
|
|
|
|
|
|
|
|
| 578 |
for c,w in [('SMA20','#FF6B00'),('SMA50','#00D4FF'),('SMA200','#9C27B0')]:
|
| 579 |
fig1.add_trace(go.Scatter(x=df.index, y=df[c], line=dict(color=w, width=1), name=c), row=1, col=1)
|
| 580 |
fig1.add_trace(go.Scatter(x=df.index, y=df['BBU'], line=dict(color='gray', width=0.8, dash='dash'), opacity=0.4), row=1, col=1)
|
|
|
|
| 584 |
fig1.add_trace(go.Scatter(x=df.index, y=df['RSI'], line=dict(color='#9C27B0', width=1.5), fill='tozeroy'), row=3, col=1)
|
| 585 |
fig1.add_hline(y=70, line_dash="dash", line_color="#FF5252", row=3, col=1)
|
| 586 |
fig1.add_hline(y=30, line_dash="dash", line_color="#00C853", row=3, col=1)
|
| 587 |
+
fig1.update_layout(title=f'{ticker} Technical Dashboard', template='plotly_dark', height=900, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a', font=dict(color='#e6edf3'))
|
|
|
|
|
|
|
| 588 |
fig2 = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.05, row_heights=[0.6,0.4])
|
| 589 |
fig2.add_trace(go.Scatter(x=df.index, y=df['MACD'], line=dict(color='#00D4FF', width=1.5), name='MACD'), row=1, col=1)
|
| 590 |
fig2.add_trace(go.Scatter(x=df.index, y=df['MACDS'], line=dict(color='#FF6B00', width=1.5), name='Signal'), row=1, col=1)
|
| 591 |
fig2.add_trace(go.Bar(x=df.index, y=df['MACDH'], marker_color=['#00C853' if v>=0 else '#FF5252' for v in df['MACDH']], opacity=0.6), row=2, col=1)
|
| 592 |
fig2.update_layout(title='MACD', template='plotly_dark', height=450, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a')
|
|
|
|
| 593 |
fig3 = go.Figure()
|
| 594 |
fig3.add_trace(go.Scatter(x=df.index, y=df['pDI'], line=dict(color='#00C853', width=1), name='+DI'))
|
| 595 |
fig3.add_trace(go.Scatter(x=df.index, y=df['mDI'], line=dict(color='#FF5252', width=1), name='-DI'))
|
| 596 |
fig3.add_trace(go.Scatter(x=df.index, y=df['ADX'], line=dict(color='#00D4FF', width=2), name='ADX'))
|
| 597 |
fig3.add_hline(y=25, line_dash="dash", line_color="gray")
|
| 598 |
fig3.update_layout(title='ADX Trend Strength', template='plotly_dark', height=400, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a')
|
|
|
|
| 599 |
fig4 = go.Figure()
|
| 600 |
fig4.add_trace(go.Histogram(x=df['Ret'].dropna()*100, nbinsx=50, marker_color='#FF6B00', opacity=0.7))
|
| 601 |
fig4.add_vline(x=rk['v95']*100, line_color='#FF5252', line_dash='dash', annotation_text='VaR95')
|
| 602 |
fig4.add_vline(x=df['Ret'].mean()*100, line_color='#00C853', line_dash='dash')
|
| 603 |
fig4.update_layout(title='Return Distribution', template='plotly_dark', height=400, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a')
|
|
|
|
| 604 |
fig5 = go.Figure()
|
| 605 |
fig5.add_trace(go.Scatter(x=df.index, y=df['ATR_pct'], line=dict(color='#FF6B00', width=1.5), fill='tozeroy'))
|
| 606 |
fig5.update_layout(title='ATR % (Volatility)', template='plotly_dark', height=400, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a')
|
|
|
|
| 607 |
fig6 = go.Figure()
|
| 608 |
fig6.add_trace(go.Scatter(x=df.index, y=df['ICH_SA'], line=dict(color='#00C853', width=0.5), name='Senkou A'))
|
| 609 |
fig6.add_trace(go.Scatter(x=df.index, y=df['ICH_SB'], fill='tonexty', fillcolor='rgba(0,200,83,0.1)', line=dict(color='#FF5252', width=0.5), name='Senkou B'))
|
| 610 |
fig6.add_trace(go.Scatter(x=df.index, y=df['Close'], line=dict(color='#00D4FF', width=1.5), name='Price'))
|
| 611 |
fig6.update_layout(title='Ichimoku Cloud', template='plotly_dark', height=400, paper_bgcolor='#000000', plot_bgcolor='#0a0a0a')
|
| 612 |
+
data_note = f"\n\n> {info['note']}\n" if info and 'note' in info else ""
|
| 613 |
+
md = f"## {ticker} Technical Analysis{data_note}\n\n| Metric | Value |\n|--------|-------|\n| Price | ${l['Close']:.2f} |\n| RSI | {l['RSI']:.1f} |\n| MACD | {l['MACD']:.3f} |\n| ADX | {l['ADX']:.1f} |\n| ATR % | {l['ATR_pct']:.2f}% |\n| Volume Ratio | {l['VR']:.1f}x |\n\n### Risk Metrics\n| Metric | Value |\n|--------|-------|\n| Ann Return | {rk['ar']*100:.1f}% |\n| Ann Vol | {rk['av']*100:.1f}% |\n| Sharpe | {rk['sh']:.2f} |\n| Max DD | {rk['md']*100:.1f}% |\n| VaR95 | {rk['v95']*100:.2f}% |\n| Win Rate | {rk['wr']*100:.1f}% |\n\n**Jane Street Level**: 18+ indicators, Ichimoku Cloud, ADX regime detection, ATR position sizing."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
return [fig1, fig2, fig3, fig4, fig5, fig6, md]
|
| 615 |
|
|
|
|
|
|
|
|
|
|
| 616 |
def ai_analysis(ticker, market, period):
|
| 617 |
suffix = MARKETS.get(market, {}).get('suffix', '')
|
| 618 |
if suffix and not any(ticker.endswith(s) for s in suffix.split('|')):
|
| 619 |
ticker = ticker + suffix
|
| 620 |
+
df, info = fetch(ticker, period)
|
| 621 |
+
if df is None or df.empty:
|
| 622 |
+
return "Error fetching data"
|
| 623 |
df = add_indicators(df)
|
| 624 |
rk = risk_metrics(df['Ret'])
|
| 625 |
l = df.iloc[-1]
|
|
|
|
| 626 |
prompt = f"""You are a portfolio manager at Jane Street / Two Sigma managing $5B AUM.
|
| 627 |
|
| 628 |
TICKER: {ticker}
|
|
|
|
| 645 |
7. CONTRARIAN VIEW (what would make this wrong)
|
| 646 |
|
| 647 |
Use quantitative reasoning. Reference specific numbers."""
|
|
|
|
| 648 |
client = K2ThinkClient()
|
| 649 |
return client.chat([{"role":"user","content":prompt}], temperature=0.2, max_tokens=4096)
|
| 650 |
|
| 651 |
+
CSS = """
|
| 652 |
+
body { background: #000000 !important; }
|
| 653 |
+
.gradio-container { background: #000000 !important; color: #e6edf3 !important; }
|
| 654 |
+
.tabitem { background: #0a0a0a !important; border: 1px solid #1a1a1a !important; border-radius: 8px !important; }
|
| 655 |
+
.tab-nav { background: #000000 !important; border-bottom: 2px solid #FF6B00 !important; }
|
| 656 |
+
.tab-nav button { color: #888 !important; background: transparent !important; font-family: 'Roboto Mono', monospace !important; font-size: 0.85em !important; }
|
| 657 |
+
.tab-nav button.selected { color: #FF6B00 !important; border-bottom: 2px solid #FF6B00 !important; font-weight: bold !important; }
|
| 658 |
+
input, textarea, select { background: #111 !important; color: #00D4FF !important; border: 1px solid #333 !important; font-family: 'Roboto Mono', monospace !important; }
|
| 659 |
+
button.primary { background: #FF6B00 !important; color: #000 !important; font-weight: 700 !important; font-family: 'Roboto Mono', monospace !important; border-radius: 4px !important; }
|
| 660 |
+
button.secondary { background: #1a1a1a !important; color: #FF6B00 !important; border: 1px solid #FF6B00 !important; font-family: 'Roboto Mono', monospace !important; }
|
| 661 |
+
.markdown-body { color: #e6edf3 !important; font-family: 'Roboto Mono', monospace !important; }
|
| 662 |
+
.markdown-body h1 { color: #FF6B00 !important; border-bottom: 1px solid #333 !important; font-size: 1.3em !important; }
|
| 663 |
+
.markdown-body h2 { color: #00D4FF !important; font-size: 1.1em !important; }
|
| 664 |
+
.markdown-body h3 { color: #00C853 !important; font-size: 1em !important; }
|
| 665 |
+
.markdown-body table { border-color: #333 !important; font-size: 0.85em !important; }
|
| 666 |
+
.markdown-body code { background: #1a1a1a !important; color: #00D4FF !important; padding: 2px 6px !important; border-radius: 4px !important; }
|
| 667 |
+
"""
|
| 668 |
+
|
| 669 |
def build_app():
|
| 670 |
with gr.Blocks(
|
| 671 |
title="AlphaForge V3.1 - Institutional Quant Platform",
|
| 672 |
theme=gr.themes.Soft(primary_hue="orange", secondary_hue="cyan", neutral_hue="gray",
|
| 673 |
font=[gr.themes.GoogleFont("Roboto Mono"), "monospace"]),
|
| 674 |
+
css=CSS
|
| 675 |
+
) as app:
|
| 676 |
+
gr.Markdown("""
|
| 677 |
+
<div style="text-align:center; padding: 20px 0;">
|
| 678 |
+
<h1 style="color:#FF6B00; font-family:'Roboto Mono',monospace; font-size:2.2em; margin:0;">
|
| 679 |
+
ALPHAFORGE V3.1
|
| 680 |
+
</h1>
|
| 681 |
+
<p style="color:#888; font-family:'Roboto Mono',monospace; font-size:0.9em; margin:8px 0 0 0;">
|
| 682 |
+
Institutional Quant Trading Platform | K2 Think V2 Powered
|
| 683 |
+
</p>
|
| 684 |
+
<p style="color:#555; font-family:'Roboto Mono',monospace; font-size:0.75em; margin:4px 0 0 0;">
|
| 685 |
+
Multi-Market: US | EU | UK | DE | JP | CN/HK | IN | Crypto | Forex | Commodities | Indices
|
| 686 |
+
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
</div>
|
| 688 |
""")
|
| 689 |
+
|
| 690 |
+
with gr.Tabs():
|
| 691 |
+
with gr.TabItem("Technical Analysis"):
|
| 692 |
+
with gr.Row():
|
| 693 |
+
with gr.Column(scale=1):
|
| 694 |
+
ta_ticker = gr.Textbox(label="Ticker", value="AAPL")
|
| 695 |
+
ta_market = gr.Dropdown(label="Market", choices=list(MARKETS.keys()), value="US Equities")
|
| 696 |
+
ta_period = gr.Dropdown(label="Period", choices=["1mo","3mo","6mo","1y","2y","5y"], value="1y")
|
| 697 |
+
ta_btn = gr.Button("Analyze")
|
| 698 |
+
gr.Markdown("Examples: `AAPL` (US), `AIR.PA` (EU), `7203.T` (JP), `BTC-USD` (Crypto)")
|
| 699 |
+
with gr.Column(scale=3):
|
| 700 |
+
ta_out1 = gr.Plot()
|
| 701 |
+
ta_out2 = gr.Plot()
|
| 702 |
+
ta_out3 = gr.Plot()
|
| 703 |
+
with gr.Row():
|
| 704 |
+
ta_out4 = gr.Plot()
|
| 705 |
+
ta_out5 = gr.Plot()
|
| 706 |
+
ta_out6 = gr.Plot()
|
| 707 |
+
ta_md = gr.Markdown()
|
| 708 |
+
ta_btn.click(fn=tech_analysis, inputs=[ta_ticker, ta_market, ta_period],
|
| 709 |
+
outputs=[ta_out1, ta_out2, ta_out3, ta_out4, ta_out5, ta_out6, ta_md])
|
| 710 |
+
|
| 711 |
+
with gr.TabItem("AI Analysis (K2)"):
|
| 712 |
+
with gr.Row():
|
| 713 |
+
with gr.Column(scale=1):
|
| 714 |
+
ai_ticker = gr.Textbox(label="Ticker", value="AAPL")
|
| 715 |
+
ai_market = gr.Dropdown(label="Market", choices=list(MARKETS.keys()), value="US Equities")
|
| 716 |
+
ai_period = gr.Dropdown(label="Period", choices=["1mo","3mo","6mo","1y","2y"], value="1y")
|
| 717 |
+
ai_btn = gr.Button("Generate AI Report")
|
| 718 |
+
with gr.Column(scale=3):
|
| 719 |
+
ai_out = gr.Textbox(label="K2 Think V2 Analysis", lines=30)
|
| 720 |
+
ai_btn.click(fn=ai_analysis, inputs=[ai_ticker, ai_market, ai_period], outputs=ai_out)
|
| 721 |
+
|
| 722 |
+
with gr.TabItem("Backtest"):
|
| 723 |
+
with gr.Row():
|
| 724 |
+
with gr.Column(scale=1):
|
| 725 |
+
bt_ticker = gr.Textbox(label="Ticker", value="AAPL")
|
| 726 |
+
bt_strategy = gr.Dropdown(label="Strategy", choices=["Moving Average Crossover","RSI Strategy","MACD Momentum","Mean Reversion","Bollinger Squeeze"], value="Moving Average Crossover")
|
| 727 |
+
bt_capital = gr.Number(label="Start Capital", value=100000)
|
| 728 |
+
bt_risk = gr.Slider(label="Risk % per Trade", minimum=1, maximum=10, value=2, step=0.5)
|
| 729 |
+
bt_period = gr.Dropdown(label="Period", choices=["1y","2y","5y"], value="2y")
|
| 730 |
+
bt_btn = gr.Button("Run Backtest")
|
| 731 |
+
with gr.Column(scale=3):
|
| 732 |
+
bt_eq = gr.Plot()
|
| 733 |
+
bt_dd = gr.Plot()
|
| 734 |
+
with gr.Row():
|
| 735 |
+
bt_trades = gr.Dataframe()
|
| 736 |
+
bt_md = gr.Markdown()
|
| 737 |
+
bt_btn.click(fn=backtest, inputs=[bt_ticker, bt_strategy, bt_capital, bt_risk, bt_period],
|
| 738 |
+
outputs=[bt_eq, bt_dd, bt_trades, bt_md, gr.Textbox(visible=False)])
|
| 739 |
+
|
| 740 |
+
with gr.TabItem("Portfolio Optimizer"):
|
| 741 |
+
with gr.Row():
|
| 742 |
+
with gr.Column(scale=1):
|
| 743 |
+
po_tickers = gr.Textbox(label="Tickers (comma-separated)", value="AAPL, MSFT, GOOGL, AMZN, NVDA")
|
| 744 |
+
po_period = gr.Dropdown(label="Period", choices=["6mo","1y","2y"], value="1y")
|
| 745 |
+
po_btn = gr.Button("Optimize Portfolio")
|
| 746 |
+
with gr.Column(scale=3):
|
| 747 |
+
po_frontier = gr.Plot()
|
| 748 |
+
po_pie = gr.Plot()
|
| 749 |
+
with gr.Row():
|
| 750 |
+
po_weights = gr.Dataframe()
|
| 751 |
+
po_md = gr.Markdown()
|
| 752 |
+
po_btn.click(fn=optimize_portfolio, inputs=[po_tickers, po_period],
|
| 753 |
+
outputs=[po_frontier, po_pie, po_weights, po_md])
|
| 754 |
+
|
| 755 |
+
with gr.TabItem("Options Pricing"):
|
| 756 |
+
with gr.Row():
|
| 757 |
+
with gr.Column(scale=1):
|
| 758 |
+
op_ticker = gr.Textbox(label="Ticker", value="AAPL")
|
| 759 |
+
op_type = gr.Dropdown(label="Option Type", choices=["call","put"], value="call")
|
| 760 |
+
op_strike = gr.Slider(label="Strike % of Spot", minimum=50, maximum=150, value=100, step=1)
|
| 761 |
+
op_days = gr.Slider(label="Days to Expiry", minimum=7, maximum=365, value=30, step=1)
|
| 762 |
+
op_rfr = gr.Slider(label="Risk-Free Rate %", minimum=0, maximum=10, value=4.5, step=0.1)
|
| 763 |
+
op_vol = gr.Slider(label="Vol Override % (0=auto)", minimum=0, maximum=100, value=0, step=1)
|
| 764 |
+
op_btn = gr.Button("Price Option")
|
| 765 |
+
with gr.Column(scale=3):
|
| 766 |
+
op_greeks = gr.Plot()
|
| 767 |
+
with gr.Row():
|
| 768 |
+
op_scenarios = gr.Dataframe()
|
| 769 |
+
op_md = gr.Markdown()
|
| 770 |
+
op_btn.click(fn=options_pricing, inputs=[op_ticker, op_strike, op_days, op_rfr, op_vol, op_type],
|
| 771 |
+
outputs=[op_greeks, op_scenarios, op_md])
|
| 772 |
+
|
| 773 |
+
with gr.TabItem("Pairs Trading"):
|
| 774 |
+
with gr.Row():
|
| 775 |
+
with gr.Column(scale=1):
|
| 776 |
+
pt_a = gr.Textbox(label="Asset A", value="AAPL")
|
| 777 |
+
pt_b = gr.Textbox(label="Asset B", value="MSFT")
|
| 778 |
+
pt_period = gr.Dropdown(label="Period", choices=["6mo","1y","2y"], value="1y")
|
| 779 |
+
pt_btn = gr.Button("Analyze Pair")
|
| 780 |
+
with gr.Column(scale=3):
|
| 781 |
+
pt_fig = gr.Plot()
|
| 782 |
+
pt_scat = gr.Plot()
|
| 783 |
+
pt_md = gr.Markdown()
|
| 784 |
+
pt_btn.click(fn=pairs_trade, inputs=[pt_a, pt_b, pt_period], outputs=[pt_fig, pt_scat, pt_md])
|
| 785 |
+
|
| 786 |
+
with gr.TabItem("Crypto Arbitrage"):
|
| 787 |
+
with gr.Row():
|
| 788 |
+
with gr.Column(scale=1):
|
| 789 |
+
ca_coins = gr.Textbox(label="Coins (comma-separated)", value="BTC, ETH, SOL, XRP")
|
| 790 |
+
ca_btn = gr.Button("Scan Arbitrage")
|
| 791 |
+
with gr.Column(scale=3):
|
| 792 |
+
ca_heatmap = gr.Plot()
|
| 793 |
+
ca_md = gr.Markdown()
|
| 794 |
+
ca_btn.click(fn=crypto_arbitrage, inputs=ca_coins, outputs=[ca_heatmap, ca_md])
|
| 795 |
+
|
| 796 |
+
with gr.TabItem("Risk Engine"):
|
| 797 |
+
with gr.Row():
|
| 798 |
+
with gr.Column(scale=1):
|
| 799 |
+
re_tickers = gr.Textbox(label="Tickers (comma-separated)", value="AAPL, MSFT, GOOGL, AMZN")
|
| 800 |
+
re_stress = gr.Textbox(label="Stress Shocks JSON", value='{"AAPL":-10, "AMZN":5}', placeholder='{"AAPL":-10, "TSLA":15}')
|
| 801 |
+
re_btn = gr.Button("Run Risk Analysis")
|
| 802 |
+
with gr.Column(scale=3):
|
| 803 |
+
re_corr = gr.Plot()
|
| 804 |
+
re_dist = gr.Plot()
|
| 805 |
+
re_md = gr.Markdown()
|
| 806 |
+
re_btn.click(fn=risk_engine, inputs=[re_tickers, re_stress], outputs=[re_corr, re_dist, re_md])
|
| 807 |
+
|
| 808 |
+
with gr.TabItem("Sentiment"):
|
| 809 |
+
with gr.Row():
|
| 810 |
+
with gr.Column(scale=1):
|
| 811 |
+
se_ticker = gr.Textbox(label="Ticker", value="AAPL")
|
| 812 |
+
se_btn = gr.Button("Analyze Sentiment")
|
| 813 |
+
with gr.Column(scale=3):
|
| 814 |
+
se_gauge = gr.Plot()
|
| 815 |
+
se_md = gr.Markdown()
|
| 816 |
+
se_btn.click(fn=sentiment_analyzer, inputs=se_ticker, outputs=[se_gauge, se_md])
|
| 817 |
+
|
| 818 |
+
with gr.TabItem("Macro Dashboard"):
|
| 819 |
+
with gr.Row():
|
| 820 |
+
ma_btn = gr.Button("Refresh Macro Data")
|
| 821 |
+
ma_fig = gr.Plot()
|
| 822 |
+
ma_md = gr.Markdown()
|
| 823 |
+
ma_btn.click(fn=macro_analysis, inputs=[], outputs=[ma_fig, ma_md])
|
| 824 |
+
|
| 825 |
+
with gr.TabItem("K2 Think V2 Chat"):
|
| 826 |
+
with gr.Row():
|
| 827 |
+
with gr.Column(scale=1):
|
| 828 |
+
k2_prompt = gr.Textbox(label="Ask K2 Think V2", value="Explain the current macro regime and where to allocate capital.", lines=4)
|
| 829 |
+
k2_temp = gr.Slider(label="Temperature", minimum=0.1, maximum=1.0, value=0.3, step=0.1)
|
| 830 |
+
k2_btn = gr.Button("Ask K2")
|
| 831 |
+
with gr.Column(scale=3):
|
| 832 |
+
k2_out = gr.Textbox(label="K2 Response", lines=30)
|
| 833 |
+
k2_btn.click(fn=lambda p,t: K2ThinkClient().chat([{"role":"user","content":p}], temperature=t, max_tokens=4096),
|
| 834 |
+
inputs=[k2_prompt, k2_temp], outputs=k2_out)
|
| 835 |
+
|
| 836 |
+
with gr.TabItem("About"):
|
| 837 |
+
gr.Markdown("""
|
| 838 |
+
## AlphaForge V3.1
|
| 839 |
+
|
| 840 |
+
**Built for the Build with K2 Think V2 Challenge by MBZUAI**
|
| 841 |
+
|
| 842 |
+
### 10 Quant Modules
|
| 843 |
+
| Module | Feature | Institution |
|
| 844 |
+
|--------|---------|-------------|
|
| 845 |
+
| Technical | 18+ indicators, candlestick, Ichimoku, VWAP, ADX, MFI, OBV | Bloomberg Terminal |
|
| 846 |
+
| AI Analysis | K2 Think V2 chain-of-thought with 7-section structured output | MBZUAI K2 |
|
| 847 |
+
| Backtest | 5 strategies, ATR sizing, equity curves, drawdown | Jane Street |
|
| 848 |
+
| Portfolio | Markowitz MPT, 10K-portfolio MC, efficient frontier | AQR, D.E. Shaw |
|
| 849 |
+
| Options | Black-Scholes + full Greeks (DGThVR), scenario P/L | Goldman Sachs |
|
| 850 |
+
| Pairs | Cointegration, OLS hedge ratio, OU half-life, Z-score | Two Sigma |
|
| 851 |
+
| Crypto Arb | Cross-exchange spread heatmap, funding rate concepts | Jump Trading |
|
| 852 |
+
| Risk Engine | VaR 95/99, stress testing, correlation breakdown | Bridgewater |
|
| 853 |
+
| Sentiment | Composite gauge (-100 to +100), keyword extraction | Citadel NLP |
|
| 854 |
+
| Macro | Cross-asset regime dashboard (S&P, 10Y, Gold, Oil, DXY, BTC) | Bridgewater All Weather |
|
| 855 |
+
|
| 856 |
+
### K2 Think V2 API Key Setup
|
| 857 |
+
1. Go to Space Settings > Repository Secrets
|
| 858 |
+
2. Add secret: `K2_API_KEY`
|
| 859 |
+
3. No key required - all quant modules work standalone
|
| 860 |
+
|
| 861 |
+
### Data Notice
|
| 862 |
+
When Yahoo Finance rate-limits shared IPs (common on HF Spaces), the app falls back to **deterministic synthetic data** seeded by ticker + date. Same ticker = same data on the same day.
|
| 863 |
+
|
| 864 |
+
### Stack
|
| 865 |
+
- yfinance / synthetic fallback
|
| 866 |
+
- Plotly (Bloomberg Terminal aesthetic)
|
| 867 |
+
- NumPy/Pandas (vectorized quant math)
|
| 868 |
+
- K2 Think V2 (MBZUAI reasoning)
|
| 869 |
+
|
| 870 |
+
---
|
| 871 |
+
*Built by Premchan | Build with K2 Think V2*
|
| 872 |
+
""")
|
| 873 |
+
|
| 874 |
+
return app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 875 |
|
| 876 |
if __name__ == "__main__":
|
| 877 |
+
build_app().launch()
|
|
|