Spaces:
Sleeping
Sleeping
Full AlphaForge v2: 4 tabs, 14 indicators, K2 Think V2 integration, portfolio optimizer
Browse files
app.py
CHANGED
|
@@ -1,10 +1,1056 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|
|
|