Premchan369 commited on
Commit
b7a60d3
Β·
verified Β·
1 Parent(s): 9e18f4b

Full AlphaForge v2: 4 tabs, 14 indicators, K2 Think V2 integration, portfolio optimizer

Browse files
Files changed (1) hide show
  1. app.py +1053 -7
app.py CHANGED
@@ -1,10 +1,1056 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
 
3
- with gr.Blocks(title="Hello Test") as demo:
4
- gr.Markdown("# Hello World")
5
- name = gr.Textbox(label="Your name")
6
- output = gr.Textbox(label="Greeting")
7
- button = gr.Button("Greet")
8
- button.click(lambda x: f"Hello, {x}!", inputs=name, outputs=output)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
1
+ """
2
+ AlphaForge x K2 Think V2 β€” Institutional-Grade Quantitative Analysis Platform
3
+ Built for the "Build with K2 Think V2" Challenge by MBZUAI.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import time
9
+ import traceback
10
+ from datetime import datetime, timedelta
11
+ from typing import Optional, Dict, List, Tuple
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+ import plotly.graph_objects as go
16
+ import plotly.express as px
17
+ from plotly.subplots import make_subplots
18
+ import yfinance as yf
19
+ import requests
20
  import gradio as gr
21
 
22
+ # ─── Configuration ───────────────────────────────────────────────────────────
23
+
24
+ K2_API_URL = "https://api.k2think.ai/v1/chat/completions"
25
+ K2_API_KEY = os.environ.get("K2_API_KEY", "")
26
+ K2_MODEL = "MBZUAI-IFM/K2-Think-v2"
27
+
28
+ APP_TITLE = "AlphaForge x K2 Think V2"
29
+ APP_DESC = "Institutional-Grade Quantitative Analysis Platform"
30
+
31
+ # ─── K2 Think V2 Client ─────────────────────────────────────────────────────
32
+
33
+ def call_k2(messages: list, temperature: float = 0.3, max_tokens: int = 2048) -> str:
34
+ """Call the K2 Think V2 API with proper error handling."""
35
+ if not K2_API_KEY:
36
+ return "⚠️ K2_API_KEY not set. Add it as a Space secret in Settings."
37
+
38
+ headers = {
39
+ "Authorization": f"Bearer {K2_API_KEY}",
40
+ "Content-Type": "application/json",
41
+ "Accept": "application/json"
42
+ }
43
+
44
+ payload = {
45
+ "model": K2_MODEL,
46
+ "messages": messages,
47
+ "temperature": temperature,
48
+ "max_tokens": max_tokens
49
+ }
50
+
51
+ try:
52
+ response = requests.post(K2_API_URL, headers=headers, json=payload, timeout=120)
53
+
54
+ if response.status_code != 200:
55
+ return f"⚠️ API Error {response.status_code}: {response.text[:500]}"
56
+
57
+ # Handle streaming response
58
+ full_content = ""
59
+ for line in response.text.split('\n'):
60
+ line = line.strip()
61
+ if not line or line == 'data: [DONE]':
62
+ continue
63
+ if line.startswith('data: '):
64
+ line = line[6:]
65
+ try:
66
+ chunk = json.loads(line)
67
+ delta = chunk.get('choices', [{}])[0].get('delta', {})
68
+ if 'content' in delta:
69
+ full_content += delta['content']
70
+ except (json.JSONDecodeError, KeyError, IndexError):
71
+ pass
72
+
73
+ # Try non-streaming fallback
74
+ if not full_content:
75
+ try:
76
+ data = response.json()
77
+ full_content = data.get('choices', [{}])[0].get('message', {}).get('content', '')
78
+ except (json.JSONDecodeError, KeyError):
79
+ pass
80
+
81
+ if not full_content:
82
+ return f"⚠️ Empty response from K2 Think V2. Raw: {response.text[:300]}"
83
+
84
+ return full_content.strip()
85
+
86
+ except requests.exceptions.Timeout:
87
+ return "⚠️ K2 Think V2 request timed out (120s). Try again or use a shorter question."
88
+ except Exception as e:
89
+ return f"⚠️ API Error: {str(e)}"
90
+
91
+
92
+ def call_k2_non_streaming(messages: list, temperature: float = 0.3, max_tokens: int = 2048) -> str:
93
+ """Non-streaming fallback for K2 Think V2."""
94
+ if not K2_API_KEY:
95
+ return "⚠️ K2_API_KEY not set."
96
+
97
+ headers = {
98
+ "Authorization": f"Bearer {K2_API_KEY}",
99
+ "Content-Type": "application/json"
100
+ }
101
+
102
+ payload = {
103
+ "model": K2_MODEL,
104
+ "messages": messages,
105
+ "temperature": temperature,
106
+ "max_tokens": max_tokens,
107
+ "stream": False
108
+ }
109
+
110
+ try:
111
+ response = requests.post(K2_API_URL, headers=headers, json=payload, timeout=120)
112
+ if response.status_code != 200:
113
+ return f"⚠️ API Error {response.status_code}: {response.text[:500]}"
114
+ data = response.json()
115
+ content = data.get('choices', [{}])[0].get('message', {}).get('content', '')
116
+ return content.strip() if content else "⚠️ Empty response."
117
+ except Exception as e:
118
+ return f"⚠️ Error: {str(e)}"
119
+
120
+
121
+ # ─── Data Layer ──────────────────────────────────────────────────────────────
122
+
123
+ def fetch_stock_data(ticker: str, period: str = "1y") -> Tuple[Optional[pd.DataFrame], Optional[dict], str]:
124
+ """Fetch and compute all technical indicators for a single stock."""
125
+ info = {}
126
+ try:
127
+ stock = yf.Ticker(ticker)
128
+ df = stock.history(period=period)
129
+
130
+ if df.empty:
131
+ return None, None, f"No data found for {ticker}."
132
+
133
+ # Get stock info
134
+ try:
135
+ info_obj = stock.info
136
+ info = {
137
+ "name": info_obj.get("longName", ticker),
138
+ "sector": info_obj.get("sector", "N/A"),
139
+ "industry": info_obj.get("industry", "N/A"),
140
+ "market_cap": info_obj.get("marketCap", None),
141
+ "pe_ratio": info_obj.get("trailingPE", None),
142
+ "forward_pe": info_obj.get("forwardPE", None),
143
+ "dividend_yield": info_obj.get("dividendYield", None),
144
+ "beta": info_obj.get("beta", None),
145
+ "52w_high": info_obj.get("fiftyTwoWeekHigh", None),
146
+ "52w_low": info_obj.get("fiftyTwoWeekLow", None),
147
+ "avg_volume": info_obj.get("averageVolume", None),
148
+ "short_ratio": info_obj.get("shortRatio", None),
149
+ }
150
+ except Exception:
151
+ pass
152
+
153
+ # === Technical Indicators ===
154
+ close = df['Close'].astype(float)
155
+ high = df['High'].astype(float)
156
+ low = df['Low'].astype(float)
157
+ volume = df['Volume'].astype(float)
158
+
159
+ # SMA
160
+ df['SMA_20'] = close.rolling(20).mean()
161
+ df['SMA_50'] = close.rolling(50).mean()
162
+ df['SMA_200'] = close.rolling(200).mean()
163
+
164
+ # EMA
165
+ df['EMA_12'] = close.ewm(span=12, adjust=False).mean()
166
+ df['EMA_26'] = close.ewm(span=26, adjust=False).mean()
167
+
168
+ # MACD
169
+ df['MACD'] = df['EMA_12'] - df['EMA_26']
170
+ df['MACD_Signal'] = df['MACD'].ewm(span=9, adjust=False).mean()
171
+ df['MACD_Hist'] = df['MACD'] - df['MACD_Signal']
172
+
173
+ # RSI
174
+ delta = close.diff()
175
+ gain = delta.where(delta > 0, 0.0)
176
+ loss = -delta.where(delta < 0, 0.0)
177
+ avg_gain = gain.rolling(14).mean()
178
+ avg_loss = loss.rolling(14).mean()
179
+ rs = avg_gain / avg_loss
180
+ df['RSI'] = 100 - (100 / (1 + rs))
181
+
182
+ # Bollinger Bands
183
+ df['BB_Mid'] = close.rolling(20).mean()
184
+ bb_std = close.rolling(20).std()
185
+ df['BB_Upper'] = df['BB_Mid'] + 2 * bb_std
186
+ df['BB_Lower'] = df['BB_Mid'] - 2 * bb_std
187
+
188
+ # ATR
189
+ high_low = high - low
190
+ high_close = (high - close.shift()).abs()
191
+ low_close = (low - close.shift()).abs()
192
+ tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
193
+ df['ATR'] = tr.rolling(14).mean()
194
+
195
+ # Stochastic
196
+ low_14 = low.rolling(14).min()
197
+ high_14 = high.rolling(14).max()
198
+ df['Stoch_K'] = 100 * (close - low_14) / (high_14 - low_14)
199
+ df['Stoch_D'] = df['Stoch_K'].rolling(3).mean()
200
+
201
+ # VWAP
202
+ df['VWAP'] = (df['Volume'] * (high + low + close) / 3).cumsum() / df['Volume'].cumsum()
203
+
204
+ # Volume Ratio
205
+ df['Vol_Ratio'] = volume / volume.rolling(20).mean()
206
+
207
+ # Daily returns
208
+ df['Returns'] = close.pct_change()
209
+ df['Log_Returns'] = np.log(close / close.shift(1))
210
+
211
+ # === Composite Alpha Signal (5 factors) ===
212
+ trend_score = ((df['SMA_20'] > df['SMA_50']).astype(float) * 0.5 +
213
+ (df['SMA_50'] > df['SMA_200']).astype(float) * 0.3 +
214
+ (close > df['SMA_20']).astype(float) * 0.2)
215
+
216
+ macd_score = ((df['MACD'] > df['MACD_Signal']).astype(float) * 0.5 +
217
+ (df['MACD_Hist'] > 0).astype(float) * 0.5)
218
+
219
+ rsi_score = ((df['RSI'] < 30).astype(float) * 1.0 +
220
+ ((df['RSI'] > 50) & (df['RSI'] < 70)).astype(float) * 0.5 +
221
+ (df['RSI'] > 70).astype(float) * (-1.0))
222
+
223
+ vol_score = (df['Vol_Ratio'] > 1.5).astype(float)
224
+
225
+ price_score = ((close > df['VWAP']).astype(float) * 0.5 +
226
+ (close > df['BB_Mid']).astype(float) * 0.3 +
227
+ (close < df['BB_Lower']).astype(float) * 0.2)
228
+
229
+ df['Alpha_Score'] = (0.30 * trend_score + 0.20 * macd_score +
230
+ 0.20 * rsi_score + 0.15 * vol_score + 0.15 * price_score)
231
+
232
+ # Risk metrics
233
+ returns = df['Returns'].dropna()
234
+ rf = 0.05 / 252 # daily risk-free rate
235
+
236
+ sharpe = np.sqrt(252) * (returns.mean() - rf) / returns.std() if returns.std() > 0 else 0
237
+ downside = returns[returns < 0]
238
+ sortino = np.sqrt(252) * (returns.mean() - rf) / downside.std() if len(downside) > 0 and downside.std() > 0 else 0
239
+ var_95 = np.percentile(returns, 5)
240
+ cvar_95 = returns[returns <= var_95].mean() if len(returns[returns <= var_95]) > 0 else var_95
241
+ max_dd = (close / close.cummax() - 1).min()
242
+ calmar = (returns.mean() * 252) / abs(max_dd) if max_dd != 0 else 0
243
+ skewness = returns.skew()
244
+ kurtosis = returns.kurtosis()
245
+
246
+ risk_metrics = {
247
+ "sharpe": round(sharpe, 3),
248
+ "sortino": round(sortino, 3),
249
+ "var_95": f"{var_95:.4f}",
250
+ "cvar_95": f"{cvar_95:.4f}",
251
+ "max_drawdown": f"{max_dd:.4f}",
252
+ "calmar": round(calmar, 3),
253
+ "skewness": round(skewness, 3),
254
+ "kurtosis": round(kurtosis, 3),
255
+ }
256
+
257
+ # Current signals
258
+ latest = {
259
+ "price": f"${close.iloc[-1]:.2f}",
260
+ "change": f"{close.iloc[-1] - close.iloc[-2]:.2f}",
261
+ "change_pct": f"{(close.iloc[-1] / close.iloc[-2] - 1) * 100:.2f}%",
262
+ "rsi": round(df['RSI'].iloc[-1], 1) if not pd.isna(df['RSI'].iloc[-1]) else "N/A",
263
+ "macd": round(df['MACD'].iloc[-1], 3) if not pd.isna(df['MACD'].iloc[-1]) else "N/A",
264
+ "macd_signal": round(df['MACD_Signal'].iloc[-1], 3) if not pd.isna(df['MACD_Signal'].iloc[-1]) else "N/A",
265
+ "alpha_score": round(df['Alpha_Score'].iloc[-1], 3) if not pd.isna(df['Alpha_Score'].iloc[-1]) else "N/A",
266
+ "vol_ratio": round(df['Vol_Ratio'].iloc[-1], 2) if not pd.isna(df['Vol_Ratio'].iloc[-1]) else "N/A",
267
+ "atr": round(df['ATR'].iloc[-1], 2) if not pd.isna(df['ATR'].iloc[-1]) else "N/A",
268
+ "vwap": f"${df['VWAP'].iloc[-1]:.2f}" if not pd.isna(df['VWAP'].iloc[-1]) else "N/A",
269
+ "sma_20": f"${df['SMA_20'].iloc[-1]:.2f}" if not pd.isna(df['SMA_20'].iloc[-1]) else "N/A",
270
+ "sma_50": f"${df['SMA_50'].iloc[-1]:.2f}" if not pd.isna(df['SMA_50'].iloc[-1]) else "N/A",
271
+ "sma_200": f"${df['SMA_200'].iloc[-1]:.2f}" if not pd.isna(df['SMA_200'].iloc[-1]) else "N/A",
272
+ "stoch_k": round(df['Stoch_K'].iloc[-1], 1) if not pd.isna(df['Stoch_K'].iloc[-1]) else "N/A",
273
+ }
274
+
275
+ return df, {"info": info, "latest": latest, "risk": risk_metrics}, ""
276
+
277
+ except Exception as e:
278
+ return None, None, f"Error fetching {ticker}: {str(e)}"
279
+
280
+
281
+ # ─── Visualization ───────────────────────────────────────────────────────────
282
+
283
+ def make_candlestick_chart(df: pd.DataFrame, ticker: str) -> go.Figure:
284
+ """Create interactive candlestick chart with SMA and Bollinger Bands."""
285
+ fig = make_subplots(
286
+ rows=3, cols=1,
287
+ shared_xaxes=True,
288
+ vertical_spacing=0.03,
289
+ row_heights=[0.5, 0.25, 0.25],
290
+ subplot_titles=(f"{ticker} β€” Price & Bollinger Bands", "MACD", "RSI")
291
+ )
292
+
293
+ # Candlestick
294
+ fig.add_trace(
295
+ go.Candlestick(
296
+ x=df.index, open=df['Open'], high=df['High'],
297
+ low=df['Low'], close=df['Close'],
298
+ name="Price", showlegend=True
299
+ ), row=1, col=1
300
+ )
301
+
302
+ # Moving Averages
303
+ for sma, name, color in [('SMA_20', 'SMA 20', 'blue'), ('SMA_50', 'SMA 50', 'orange'), ('SMA_200', 'SMA 200', 'red')]:
304
+ if sma in df.columns and df[sma].notna().any():
305
+ fig.add_trace(go.Scatter(x=df.index, y=df[sma], mode='lines',
306
+ name=name, line=dict(color=color, width=1)),
307
+ row=1, col=1)
308
+
309
+ # Bollinger Bands
310
+ if 'BB_Upper' in df.columns:
311
+ fig.add_trace(go.Scatter(x=df.index, y=df['BB_Upper'], mode='lines',
312
+ name='BB Upper', line=dict(color='gray', width=0.5, dash='dash'),
313
+ showlegend=False), row=1, col=1)
314
+ fig.add_trace(go.Scatter(x=df.index, y=df['BB_Lower'], mode='lines',
315
+ name='BB Lower', line=dict(color='gray', width=0.5, dash='dash'),
316
+ fill='tonexty', fillcolor='rgba(128,128,128,0.1)',
317
+ showlegend=False), row=1, col=1)
318
+
319
+ # MACD
320
+ if 'MACD' in df.columns:
321
+ fig.add_trace(go.Scatter(x=df.index, y=df['MACD'], mode='lines',
322
+ name='MACD', line=dict(color='blue', width=1)),
323
+ row=2, col=1)
324
+ fig.add_trace(go.Scatter(x=df.index, y=df['MACD_Signal'], mode='lines',
325
+ name='Signal', line=dict(color='red', width=1)),
326
+ row=2, col=1)
327
+ colors = ['green' if v >= 0 else 'red' for v in df['MACD_Hist'].fillna(0)]
328
+ fig.add_trace(go.Bar(x=df.index, y=df['MACD_Hist'], name='Histogram',
329
+ marker_color=colors, showlegend=False),
330
+ row=2, col=1)
331
+
332
+ # RSI
333
+ if 'RSI' in df.columns:
334
+ fig.add_trace(go.Scatter(x=df.index, y=df['RSI'], mode='lines',
335
+ name='RSI', line=dict(color='purple', width=1)),
336
+ row=3, col=1)
337
+ fig.add_hline(y=70, line_dash="dash", line_color="red", opacity=0.5, row=3, col=1)
338
+ fig.add_hline(y=30, line_dash="dash", line_color="green", opacity=0.5, row=3, col=1)
339
+
340
+ fig.update_layout(
341
+ template='plotly_dark',
342
+ height=800,
343
+ hovermode='x unified',
344
+ showlegend=True,
345
+ margin=dict(l=40, r=40, t=60, b=40),
346
+ xaxis_rangeslider_visible=False,
347
+ )
348
+ fig.update_xaxes(title_text="Date", row=3, col=1)
349
+ fig.update_yaxes(title_text="Price ($)", row=1, col=1)
350
+ fig.update_yaxes(title_text="MACD", row=2, col=1)
351
+ fig.update_yaxes(title_text="RSI", row=3, col=1)
352
+
353
+ return fig
354
+
355
+
356
+ def make_returns_chart(df: pd.DataFrame) -> go.Figure:
357
+ """Create returns distribution histogram."""
358
+ returns = df['Returns'].dropna()
359
+
360
+ fig = go.Figure()
361
+ fig.add_trace(go.Histogram(
362
+ x=returns, nbinsx=50, name='Daily Returns',
363
+ histnorm='probability density',
364
+ marker_color='rgba(100, 149, 237, 0.7)',
365
+ showlegend=True
366
+ ))
367
+
368
+ # Normal distribution overlay
369
+ x = np.linspace(returns.min(), returns.max(), 200)
370
+ y = (1 / (returns.std() * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((x - returns.mean()) / returns.std()) ** 2)
371
+ fig.add_trace(go.Scatter(x=x, y=y, mode='lines',
372
+ name='Normal Distribution',
373
+ line=dict(color='red', width=2)))
374
+
375
+ fig.update_layout(
376
+ template='plotly_dark',
377
+ title="Daily Returns Distribution vs Normal",
378
+ height=400,
379
+ margin=dict(l=40, r=40, t=60, b=40),
380
+ )
381
+ fig.update_xaxes(title_text="Daily Return")
382
+ fig.update_yaxes(title_text="Density")
383
+
384
+ return fig
385
+
386
+
387
+ # ─── Portfolio Optimization ──────────────────────────────────────────────────
388
+
389
+ def optimize_portfolio(tickers_str: str, period: str = "1y") -> Tuple:
390
+ """Mean-variance portfolio optimization with efficient frontier."""
391
+ tickers = [t.strip().upper() for t in tickers_str.split(',') if t.strip()]
392
+
393
+ if len(tickers) < 2:
394
+ return None, None, None, "Enter at least 2 tickers (comma-separated)."
395
+
396
+ try:
397
+ data = yf.download(tickers, period=period)['Close']
398
+ if isinstance(data, pd.Series):
399
+ data = data.to_frame()
400
+
401
+ data = data.dropna()
402
+
403
+ if data.empty:
404
+ return None, None, None, "No overlapping data for these tickers."
405
+
406
+ returns = data.pct_change().dropna()
407
+ mean_returns = returns.mean() * 252
408
+ cov_matrix = returns.cov() * 252
409
+
410
+ n_assets = len(tickers)
411
+
412
+ # === Efficient frontier via random portfolios ===
413
+ n_portfolios = 5000
414
+ results = np.zeros((3, n_portfolios))
415
+ weights_record = []
416
+
417
+ np.random.seed(42)
418
+ for i in range(n_portfolios):
419
+ w = np.random.random(n_assets)
420
+ w /= w.sum()
421
+ weights_record.append(w)
422
+ portfolio_return = np.sum(w * mean_returns)
423
+ portfolio_std = np.sqrt(w @ cov_matrix @ w)
424
+ results[0, i] = portfolio_std
425
+ results[1, i] = portfolio_return
426
+ results[2, i] = portfolio_return / portfolio_std # Sharpe
427
+
428
+ max_sharpe_idx = np.argmax(results[2])
429
+ max_sharpe_weights = weights_record[max_sharpe_idx]
430
+
431
+ # === Optimization: maximize Sharpe ===
432
+ from scipy.optimize import minimize
433
+
434
+ def neg_sharpe(w):
435
+ port_return = np.sum(w * mean_returns)
436
+ port_std = np.sqrt(w @ cov_matrix @ w)
437
+ return -port_return / port_std if port_std > 0 else 0
438
+
439
+ constraints = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1})
440
+ bounds = tuple((0, 0.5) for _ in range(n_assets))
441
+ init_guess = np.ones(n_assets) / n_assets
442
+
443
+ opt_result = minimize(neg_sharpe, init_guess, method='SLSQP',
444
+ bounds=bounds, constraints=constraints)
445
+
446
+ opt_weights = opt_result.x if opt_result.success else max_sharpe_weights
447
+ opt_return = np.sum(opt_weights * mean_returns)
448
+ opt_std = np.sqrt(opt_weights @ cov_matrix @ opt_weights)
449
+ opt_sharpe = opt_return / opt_std if opt_std > 0 else 0
450
+
451
+ # === Efficient frontier chart ===
452
+ fig = go.Figure()
453
+ fig.add_trace(go.Scatter(
454
+ x=results[0], y=results[1],
455
+ mode='markers',
456
+ marker=dict(
457
+ size=5,
458
+ color=results[2],
459
+ colorscale='Viridis',
460
+ colorbar=dict(title='Sharpe Ratio'),
461
+ showscale=True
462
+ ),
463
+ name='Portfolios',
464
+ text=[f"Sharpe: {s:.3f}" for s in results[2]],
465
+ hovertemplate='Risk: %{x:.3f}<br>Return: %{y:.3f}<extra></extra>'
466
+ ))
467
+
468
+ fig.add_trace(go.Scatter(
469
+ x=[opt_std], y=[opt_return],
470
+ mode='markers',
471
+ marker=dict(size=15, color='red', symbol='star'),
472
+ name=f'Optimal (Sharpe: {opt_sharpe:.3f})'
473
+ ))
474
+
475
+ equal_weights = np.ones(n_assets) / n_assets
476
+ eq_return = np.sum(equal_weights * mean_returns)
477
+ eq_std = np.sqrt(equal_weights @ cov_matrix @ equal_weights)
478
+ fig.add_trace(go.Scatter(
479
+ x=[eq_std], y=[eq_return],
480
+ mode='markers',
481
+ marker=dict(size=12, color='orange', symbol='diamond'),
482
+ name=f'Equal Weight'
483
+ ))
484
+
485
+ fig.update_layout(
486
+ template='plotly_dark',
487
+ title=f"Efficient Frontier β€” {len(tickers)} Assets",
488
+ height=600,
489
+ xaxis_title="Risk (Annualized Std Dev)",
490
+ yaxis_title="Return (Annualized)",
491
+ hovermode='closest',
492
+ margin=dict(l=40, r=40, t=60, b=40),
493
+ )
494
+
495
+ # Weights table
496
+ weights_df = pd.DataFrame({
497
+ 'Ticker': tickers,
498
+ 'Optimal Weight': [f"{w:.2%}" for w in opt_weights],
499
+ 'Equal Weight': [f"{1/n_assets:.2%}"] * n_assets,
500
+ 'Difference': [f"{(w - 1/n_assets):.2%}" for w in opt_weights]
501
+ })
502
+
503
+ # Portfolio risk metrics
504
+ opt_returns_series = returns @ opt_weights
505
+ rf = 0.05 / 252
506
+ port_sharpe = np.sqrt(252) * (opt_returns_series.mean() - rf) / opt_returns_series.std()
507
+ port_max_dd = (1 + opt_returns_series).cumprod().div((1 + opt_returns_series).cumprod().cummax()) - 1
508
+ max_drawdown = port_max_dd.min()
509
+
510
+ port_metrics = {
511
+ "Expected Return": f"{opt_return:.2%}",
512
+ "Risk (Std Dev)": f"{opt_std:.2%}",
513
+ "Sharpe Ratio": f"{port_sharpe:.3f}",
514
+ "Max Drawdown": f"{max_drawdown:.2%}",
515
+ "Correlation Matrix": cov_matrix.corr().round(3).to_html(),
516
+ }
517
+
518
+ return fig, weights_df, port_metrics, ""
519
+
520
+ except Exception as e:
521
+ return None, None, None, f"Optimization error: {str(e)}"
522
+
523
+
524
+ # ─── K2 Think Analysis ───────────────────────────────────────────────────────
525
+
526
+ def analyze_stock_with_ai(ticker: str) -> str:
527
+ """K2 Think V2 deep analysis of a single stock."""
528
+ df, data, error = fetch_stock_data(ticker)
529
+ if error:
530
+ return f"❌ {error}"
531
+
532
+ if data is None:
533
+ return "❌ Could not retrieve data."
534
+
535
+ info = data.get("info", {})
536
+ latest = data.get("latest", {})
537
+ risk = data.get("risk", {})
538
+
539
+ data_summary = f"""
540
+ Ticker: {ticker}
541
+ Company: {info.get('name', ticker)}
542
+ Sector: {info.get('sector', 'N/A')} | Industry: {info.get('industry', 'N/A')}
543
+ Market Cap: {_fmt_billions(info.get('market_cap'))}
544
+ P/E: {info.get('pe_ratio', 'N/A')} | Forward P/E: {info.get('forward_pe', 'N/A')}
545
+ Beta: {info.get('beta', 'N/A')}
546
+ 52-Week Range: ${info.get('52w_low', 'N/A')} β€” ${info.get('52w_high', 'N/A')}
547
+ """
548
+
549
+ technical_summary = f"""
550
+ Current Price: {latest.get('price', 'N/A')} ({latest.get('change_pct', 'N/A')})
551
+ RSI (14): {latest.get('rsi', 'N/A')}
552
+ MACD: {latest.get('macd', 'N/A')} vs Signal {latest.get('macd_signal', 'N/A')}
553
+ Stochastic K: {latest.get('stoch_k', 'N/A')}
554
+ VWAP: {latest.get('vwap', 'N/A')}
555
+ SMA 20/50/200: {latest.get('sma_20', 'N/A')} / {latest.get('sma_50', 'N/A')} / {latest.get('sma_200', 'N/A')}
556
+ Volume Ratio: {latest.get('vol_ratio', 'N/A')} (vs 20-day avg)
557
+ ATR: ${latest.get('atr', 'N/A')}
558
+ Alpha Score: {latest.get('alpha_score', 'N/A')}
559
+
560
+ RISK METRICS:
561
+ Sharpe: {risk.get('sharpe', 'N/A')} | Sortino: {risk.get('sortino', 'N/A')}
562
+ VaR (95%): {risk.get('var_95', 'N/A')} | CVaR (95%): {risk.get('cvar_95', 'N/A')}
563
+ Max Drawdown: {risk.get('max_drawdown', 'N/A')} | Calmar: {risk.get('calmar', 'N/A')}
564
+ Skewness: {risk.get('skewness', 'N/A')} | Kurtosis: {risk.get('kurtosis', 'N/A')}
565
+ """
566
+
567
+ prompt = f"""You are an elite quantitative analyst at a top-tier hedge fund managing $5B AUM.
568
+
569
+ Analyze {ticker} with deep, step-by-step reasoning.
570
+
571
+ {data_summary}
572
+
573
+ {technical_summary}
574
+
575
+ Provide a comprehensive analysis following this structure:
576
+
577
+ ## 1. Executive Summary (3 bullets)
578
+ - One sentence on the overall technical picture
579
+ - Key risk factor to watch
580
+ - Most important catalyst
581
+
582
+ ## 2. Technical Analysis Deep Dive
583
+ - Interpret RSI in context (oversold/overbought/neutral and what it means)
584
+ - MACD signal analysis (crossover, divergence, histogram momentum)
585
+ - Moving average relationship (golden cross, death cross, or trending)
586
+ - Volume analysis (accumulation vs distribution)
587
+ - Bollinger Band position and squeeze/expansion
588
+ - Alpha Score interpretation
589
+
590
+ ## 3. Risk Assessment
591
+ - Current volatility regime (low/medium/high)
592
+ - VaR interpretation and downside scenarios
593
+ - Drawdown risk and recovery potential
594
+ - Tail risk assessment (skewness/kurtosis)
595
+
596
+ ## 4. Alpha Signal
597
+ - Direction: BULLISH / NEUTRAL / BEARISH
598
+ - Confidence: low / medium / high
599
+ - Time horizon: 1-week / 1-month / 3-month
600
+
601
+ ## 5. Actionable Trade Idea
602
+ - Entry: specific price level with reasoning
603
+ - Stop-loss: specific price level with reasoning
604
+ - Target: specific price level with reasoning
605
+ - Position sizing recommendation (% of portfolio)
606
+ - Risk-reward ratio
607
+
608
+ ## 6. Catalyst Watch
609
+ - Upcoming events that could move the stock (earnings, economic reports, sector events)
610
+ - Bull case catalyst
611
+ - Bear case catalyst
612
+
613
+ Format your response with clear markdown headers. Use specific numbers, not vague statements. Think step-by-step."""
614
+
615
+ messages = [
616
+ {"role": "system", "content": "You are an elite quantitative analyst. Always provide specific numbers, clear reasoning, and actionable insights. Use markdown formatting."},
617
+ {"role": "user", "content": prompt}
618
+ ]
619
+
620
+ try:
621
+ result = call_k2(messages, temperature=0.3, max_tokens=3072)
622
+ return result
623
+ except Exception as e:
624
+ # Try non-streaming fallback
625
+ try:
626
+ return call_k2_non_streaming(messages, temperature=0.3, max_tokens=3072)
627
+ except:
628
+ return f"❌ AI Analysis failed: {str(e)}"
629
+
630
+
631
+ def analyze_portfolio_with_ai(tickers_str: str) -> str:
632
+ """K2 Think V2 portfolio analysis."""
633
+ tickers = [t.strip().upper() for t in tickers_str.split(',') if t.strip()]
634
+
635
+ if len(tickers) < 2:
636
+ return "Enter at least 2 tickers for portfolio analysis."
637
+
638
+ try:
639
+ data = yf.download(tickers, period="1y")['Close']
640
+ if isinstance(data, pd.Series):
641
+ data = data.to_frame()
642
+ data = data.dropna()
643
+ returns = data.pct_change().dropna()
644
+
645
+ # Portfolio metrics
646
+ equal_weights = np.ones(len(tickers)) / len(tickers)
647
+ port_returns = returns @ equal_weights
648
+ port_cum = (1 + port_returns).cumprod()
649
+
650
+ annual_return = port_returns.mean() * 252
651
+ annual_vol = port_returns.std() * np.sqrt(252)
652
+ sharpe = (annual_return - 0.05) / annual_vol if annual_vol > 0 else 0
653
+ max_dd = ((1 + port_returns).cumprod().div((1 + port_returns).cumprod().cummax()) - 1).min()
654
+
655
+ # Individual stock stats
656
+ stock_stats = []
657
+ for t in tickers:
658
+ r = returns[t]
659
+ stock_stats.append({
660
+ "ticker": t,
661
+ "ann_return": f"{r.mean() * 252:.2%}",
662
+ "ann_vol": f"{r.std() * np.sqrt(252):.2%}",
663
+ "sharpe": f"{(r.mean() * 252 - 0.05) / (r.std() * np.sqrt(252)):.2f}" if r.std() > 0 else "N/A",
664
+ "max_dd": f"{(1 + r).cumprod().div((1 + r).cumprod().cummax()).min():.2%}",
665
+ })
666
+
667
+ corr_matrix = returns.corr().round(3)
668
+
669
+ # Current prices
670
+ prices = {t: f"${data[t].iloc[-1]:.2f}" for t in tickers}
671
+
672
+ summary = f"""
673
+ PORTFOLIO: {', '.join(tickers)}
674
+ Period: 1 year
675
+
676
+ EQUAL-WEIGHT METRICS:
677
+ Annualized Return: {annual_return:.2%}
678
+ Annualized Volatility: {annual_vol:.2%}
679
+ Sharpe Ratio: {sharpe:.3f}
680
+ Max Drawdown: {max_dd:.2%}
681
+
682
+ STOCK STATS:
683
+ {json.dumps(stock_stats, indent=2)}
684
+
685
+ CORRELATION MATRIX:
686
+ {corr_matrix.to_json(indent=2)}
687
+
688
+ CURRENT PRICES:
689
+ {json.dumps(prices, indent=2)}
690
+ """
691
+
692
+ prompt = f"""You are a portfolio manager at a multi-strategy hedge fund overseeing $500M AUM.
693
+
694
+ Analyze this portfolio with institutional-grade rigor.
695
+
696
+ {summary}
697
+
698
+ Provide a comprehensive analysis:
699
+
700
+ ## 1. Portfolio Health Score (0-100)
701
+ - Quantitative rating with letter grade (A+ to F)
702
+ - Key drivers of the score
703
+ - What would improve the score
704
+
705
+ ## 2. Concentration Risk Analysis
706
+ - Sector/style concentration if evident
707
+ - Single-name risk assessment
708
+ - Diversification benefit analysis
709
+
710
+ ## 3. Correlation Matrix Insights
711
+ - Most correlated pair and implications
712
+ - Least correlated / negative correlated pair
713
+ - Hedging opportunities from correlation structure
714
+
715
+ ## 4. Risk Budget Analysis
716
+ - Contribution to portfolio risk by each position
717
+ - Which stock drives most of the volatility
718
+ - Tail risk concentration
719
+
720
+ ## 5. Rebalancing Recommendations
721
+ - Specific % adjustments for each ticker
722
+ - Reasoning for each adjustment
723
+ - Expected impact on Sharpe
724
+
725
+ ## 6. Hedging Strategy
726
+ - Specific options or ETFs to hedge tail risk
727
+ - Cost of hedging vs benefit
728
+ - When to implement the hedge
729
+
730
+ ## 7. Forward-Looking Assessment
731
+ - Expected 6-month return and volatility
732
+ - Key risks to the outlook
733
+ - Scenario analysis (bull/base/bear)
734
+
735
+ Use specific numbers and clear markdown formatting."""
736
+
737
+ messages = [
738
+ {"role": "system", "content": "You are an elite portfolio manager. Provide specific, quantitative, actionable advice with clear reasoning."},
739
+ {"role": "user", "content": prompt}
740
+ ]
741
+
742
+ try:
743
+ return call_k2(messages, temperature=0.3, max_tokens=3072)
744
+ except:
745
+ return call_k2_non_streaming(messages, temperature=0.3, max_tokens=3072)
746
+
747
+ except Exception as e:
748
+ return f"❌ Portfolio analysis error: {str(e)}"
749
+
750
+
751
+ def chat_with_k2(message: str, temperature: float = 0.5) -> str:
752
+ """Direct chat with K2 Think V2."""
753
+ if not message.strip():
754
+ return "Please enter a question."
755
+
756
+ messages = [
757
+ {"role": "system", "content": "You are a helpful AI assistant specializing in quantitative finance, trading strategies, risk management, and financial markets. Provide detailed, accurate, and educational responses."},
758
+ {"role": "user", "content": message}
759
+ ]
760
+
761
+ try:
762
+ result = call_k2(messages, temperature=temperature, max_tokens=2048)
763
+ return result
764
+ except:
765
+ return call_k2_non_streaming(messages, temperature=temperature, max_tokens=2048)
766
+
767
+
768
+ # ─── Helpers ─────────────────────────────────────────────────────────────────
769
+
770
+ def _fmt_billions(val) -> str:
771
+ if val is None:
772
+ return "N/A"
773
+ if val >= 1e12:
774
+ return f"${val/1e12:.2f}T"
775
+ elif val >= 1e9:
776
+ return f"${val/1e9:.2f}B"
777
+ elif val >= 1e6:
778
+ return f"${val/1e6:.2f}M"
779
+ return f"${val:,.0f}"
780
+
781
+
782
+ # ─── Gradio UI Handlers ──────────────────────────────────────────────────────
783
+
784
+ def on_analyze(ticker: str):
785
+ """Handle single stock analysis."""
786
+ if not ticker.strip():
787
+ return None, None, None, "Please enter a ticker symbol."
788
+
789
+ ticker = ticker.strip().upper()
790
+ df, data, error = fetch_stock_data(ticker)
791
+
792
+ if error:
793
+ return None, None, None, f"❌ {error}"
794
+
795
+ chart = make_candlestick_chart(df, ticker)
796
+ returns_chart = make_returns_chart(df)
797
+
798
+ # Summary table
799
+ info = data.get("info", {})
800
+ latest = data.get("latest", {})
801
+ risk = data.get("risk", {})
802
+
803
+ summary = f"""
804
+ ### {info.get('name', ticker)} ({ticker})
805
+
806
+ | Metric | Value |
807
+ |--------|-------|
808
+ | **Price** | {latest.get('price', 'N/A')} |
809
+ | **Change** | {latest.get('change_pct', 'N/A')} |
810
+ | **RSI** | {latest.get('rsi', 'N/A')} |
811
+ | **MACD/Signal** | {latest.get('macd', 'N/A')} / {latest.get('macd_signal', 'N/A')} |
812
+ | **Alpha Score** | {latest.get('alpha_score', 'N/A')} |
813
+ | **SMA 20/50/200** | {latest.get('sma_20', 'N/A')} / {latest.get('sma_50', 'N/A')} / {latest.get('sma_200', 'N/A')} |
814
+ | **VWAP** | {latest.get('vwap', 'N/A')} |
815
+ | **Volume Ratio** | {latest.get('vol_ratio', 'N/A')} |
816
+ | **ATR** | ${latest.get('atr', 'N/A')} |
817
+
818
+ ### Risk Metrics
819
+ | Metric | Value |
820
+ |--------|-------|
821
+ | **Sharpe** | {risk.get('sharpe', 'N/A')} |
822
+ | **Sortino** | {risk.get('sortino', 'N/A')} |
823
+ | **VaR (95%)** | {risk.get('var_95', 'N/A')} |
824
+ | **CVaR (95%)** | {risk.get('cvar_95', 'N/A')} |
825
+ | **Max Drawdown** | {risk.get('max_drawdown', 'N/A')} |
826
+ | **Calmar** | {risk.get('calmar', 'N/A')} |
827
+ """
828
+
829
+ return chart, returns_chart, summary, ""
830
+
831
+
832
+ def on_ai_analysis(ticker: str):
833
+ """Run K2 Think V2 analysis."""
834
+ if not ticker.strip():
835
+ return "Please enter a ticker symbol."
836
+ result = analyze_stock_with_ai(ticker.strip().upper())
837
+ return result
838
+
839
+
840
+ def on_portfolio_optimize(tickers_str: str):
841
+ """Handle portfolio optimization."""
842
+ fig, weights_df, metrics, error = optimize_portfolio(tickers_str)
843
+ if error:
844
+ return None, None, f"❌ {error}"
845
+ return fig, weights_df, ""
846
+
847
+
848
+ def on_portfolio_ai(tickers_str: str):
849
+ """Run K2 Think V2 portfolio analysis."""
850
+ if not tickers_str.strip():
851
+ return "Please enter tickers."
852
+ result = analyze_portfolio_with_ai(tickers_str)
853
+ return result
854
+
855
+
856
+ # ─── Gradio UI ───────────────────────────────────────────────────────────────
857
+
858
+ def build_ui():
859
+ custom_css = """
860
+ .gradio-container { max-width: 1200px !important; margin: 0 auto !important; }
861
+ .k2-badge { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 4px 12px; border-radius: 12px; font-size: 0.8em; }
862
+ .metric-positive { color: #00ff88; font-weight: bold; }
863
+ .metric-negative { color: #ff4444; font-weight: bold; }
864
+ footer { visibility: hidden; }
865
+ """
866
+
867
+ with gr.Blocks(
868
+ title=APP_TITLE,
869
+ theme=gr.themes.Soft(primary_hue="blue", secondary_hue="purple"),
870
+ css=custom_css
871
+ ) as demo:
872
+ gr.Markdown(f"""
873
+ # πŸ”₯ {APP_TITLE}
874
+ ### {APP_DESC}
875
+ > Powered by **K2 Think V2** (MBZUAI) β€” State-of-the-art reasoning for financial markets.
876
+ > Model compression powered by **Q-TensorFormer v3**.
877
+ """)
878
+
879
+ with gr.Tabs():
880
+ # ─── TAB 1: Single Stock Analysis ───
881
+ with gr.Tab("πŸ“ˆ Single Stock Analysis", id="stock"):
882
+ with gr.Row():
883
+ ticker_input = gr.Textbox(
884
+ label="Ticker Symbol",
885
+ placeholder="Enter ticker (e.g., AAPL, NVDA, MSFT)",
886
+ value="AAPL",
887
+ scale=3
888
+ )
889
+ analyze_btn = gr.Button("πŸ” Analyze", variant="primary", scale=1)
890
+ ai_btn = gr.Button("πŸ€– AI Deep Analysis (K2 Think)", variant="secondary", scale=1)
891
+
892
+ with gr.Row():
893
+ status = gr.Markdown("", visible=True)
894
+
895
+ with gr.Row():
896
+ chart = gr.Plot(label="Price & Technicals", scale=2)
897
+ with gr.Column(scale=1):
898
+ summary = gr.Markdown(label="Summary", value="Enter a ticker to begin.")
899
+
900
+ with gr.Row():
901
+ returns_chart = gr.Plot(label="Return Distribution", scale=1)
902
+
903
+ with gr.Row():
904
+ ai_output = gr.Markdown(
905
+ label="K2 Think V2 Deep Analysis",
906
+ value="*Click 'AI Deep Analysis' for institutional-grade analysis powered by K2 Think V2.*",
907
+ elem_classes=["ai-output"]
908
+ )
909
+
910
+ analyze_btn.click(
911
+ fn=on_analyze,
912
+ inputs=[ticker_input],
913
+ outputs=[chart, returns_chart, summary, status]
914
+ )
915
+ ai_btn.click(
916
+ fn=on_ai_analysis,
917
+ inputs=[ticker_input],
918
+ outputs=[ai_output]
919
+ )
920
+
921
+ # ─── TAB 2: Portfolio Optimizer ───
922
+ with gr.Tab("πŸ’Ό Portfolio Optimizer", id="portfolio"):
923
+ gr.Markdown("### Markowitz Mean-Variance Optimization")
924
+ with gr.Row():
925
+ pf_input = gr.Textbox(
926
+ label="Tickers (comma-separated)",
927
+ placeholder="AAPL, MSFT, NVDA, GOOGL, AMZN",
928
+ value="AAPL, MSFT, GOOGL, AMZN, NVDA",
929
+ scale=4
930
+ )
931
+ pf_opt_btn = gr.Button("⚑ Optimize", variant="primary", scale=1)
932
+ pf_ai_btn = gr.Button("πŸ€– AI Portfolio Advice (K2 Think)", variant="secondary", scale=1)
933
+
934
+ with gr.Row():
935
+ pf_status = gr.Markdown("")
936
+
937
+ with gr.Row():
938
+ ef_chart = gr.Plot(label="Efficient Frontier", scale=2)
939
+ with gr.Column(scale=1):
940
+ weights_table = gr.DataFrame(
941
+ label="Portfolio Weights",
942
+ headers=["Ticker", "Optimal Weight", "Equal Weight", "Difference"]
943
+ )
944
+
945
+ with gr.Row():
946
+ pf_ai_output = gr.Markdown(
947
+ label="K2 Think V2 Portfolio Analysis",
948
+ value="*Click 'AI Portfolio Advice' for institutional portfolio analysis.*"
949
+ )
950
+
951
+ pf_opt_btn.click(
952
+ fn=on_portfolio_optimize,
953
+ inputs=[pf_input],
954
+ outputs=[ef_chart, weights_table, pf_status]
955
+ )
956
+ pf_ai_btn.click(
957
+ fn=on_portfolio_ai,
958
+ inputs=[pf_input],
959
+ outputs=[pf_ai_output]
960
+ )
961
+
962
+ # ─── TAB 3: Direct K2 Think Chat ───
963
+ with gr.Tab("πŸ’¬ K2 Think V2 Chat", id="chat"):
964
+ gr.Markdown("### πŸ’¬ Ask K2 Think V2 Anything About Finance")
965
+ gr.Markdown("""
966
+ Ask about:
967
+ - **Trading strategies** (momentum, mean-reversion, pairs)
968
+ - **Portfolio theory** (Black-Litterman, risk parity)
969
+ - **Derivatives pricing** (Greeks, volatility surface)
970
+ - **Market microstructure** (order flow, liquidity)
971
+ - **Quant interview prep** (brain teasers, math, coding)
972
+ """)
973
+
974
+ with gr.Row():
975
+ chat_input = gr.Textbox(
976
+ label="Your Question",
977
+ placeholder="Explain the Black-Scholes model and its limitations...",
978
+ lines=3,
979
+ scale=4
980
+ )
981
+ with gr.Row():
982
+ temp_slider = gr.Slider(
983
+ label="Temperature", minimum=0.0, maximum=1.0, value=0.3, step=0.1,
984
+ scale=1
985
+ )
986
+ chat_btn = gr.Button("πŸš€ Ask K2 Think", variant="primary", scale=1)
987
+
988
+ chat_output = gr.Markdown(
989
+ label="K2 Think V2 Response",
990
+ value="*Ask a question to get started.*"
991
+ )
992
+
993
+ chat_btn.click(
994
+ fn=chat_with_k2,
995
+ inputs=[chat_input, temp_slider],
996
+ outputs=[chat_output]
997
+ )
998
+
999
+ # ─── TAB 4: About ───
1000
+ with gr.Tab("ℹ️ About", id="about"):
1001
+ gr.Markdown("""
1002
+ ## πŸ”₯ AlphaForge x K2 Think V2
1003
+
1004
+ **Institutional-Grade Quantitative Analysis Platform**
1005
+
1006
+ ### Powered By
1007
+ - **K2 Think V2** β€” MBZUAI's state-of-the-art reasoning model for deep financial analysis
1008
+ - **Q-TensorFormer v3** β€” Quantum-enhanced tensor network compression for efficient model deployment
1009
+ - **yfinance** β€” Real-time market data from Yahoo Finance
1010
+ - **Plotly** β€” Interactive financial visualizations
1011
+ - **SciPy** β€” Portfolio optimization engine
1012
+
1013
+ ### Features
1014
+ 1. **Single Stock Analysis** β€” 14+ technical indicators, composite alpha signal, risk metrics
1015
+ 2. **AI Deep Analysis** β€” Institutional-grade analysis via K2 Think V2 chain-of-thought reasoning
1016
+ 3. **Portfolio Optimizer** β€” Markowitz mean-variance optimization with 5000-portfolio efficient frontier
1017
+ 4. **AI Portfolio Advice** β€” K2 Think V2 portfolio health scoring, rebalancing, and hedging strategies
1018
+ 5. **Direct K2 Chat** β€” Any financial question answered by SOTA reasoning model
1019
+
1020
+ ### Related Projects
1021
+ - [Q-TensorFormer v3](https://huggingface.co/Premchan369/q-tensorformer) β€” Quantum+Tensor LLM compression
1022
+ - [AlphaForge Platform](https://huggingface.co/Premchan369/alphaforge-quant-system) β€” 25 open-source quant modules
1023
+ - [Build with K2 Think V2](https://build.k2think.ai/) β€” MBZUAI Challenge
1024
+
1025
+ ### Setup
1026
+ 1. Set `K2_API_KEY` as a Space secret in Settings
1027
+ 2. Factory Rebuild
1028
+ 3. Start analyzing!
1029
+
1030
+ ### License
1031
+ Apache 2.0
1032
+ """)
1033
+
1034
+ # ─── Footer ───
1035
+ gr.Markdown("""
1036
+ ---
1037
+ <div align="center">
1038
+ <small>AlphaForge x K2 Think V2 Β· Built for the Build with K2 Think V2 Challenge Β· MBZUAI</small><br>
1039
+ <small>"From 'Can I see it?' β†’ 'Can AI explain it?' β†’ 'Can it run everywhere?'"</small>
1040
+ </div>
1041
+ """)
1042
+
1043
+ return demo
1044
+
1045
+
1046
+ # ─── Main ────────────────────────────────────────────────────────────────────
1047
+
1048
+ if __name__ == "__main__":
1049
+ demo = build_ui()
1050
+ demo.queue(max_size=10, default_concurrency_limit=3)
1051
+ demo.launch(
1052
+ server_name="0.0.0.0",
1053
+ server_port=7860,
1054
+ share=True,
1055
+ )
1056