Restore app.py + fix share=True for HF Spaces
Browse files
app.py
CHANGED
|
@@ -10,7 +10,7 @@ Features:
|
|
| 10 |
- Alpha signal generation
|
| 11 |
- AI-powered market analysis with chain-of-thought reasoning
|
| 12 |
|
| 13 |
-
API Key:
|
| 14 |
"""
|
| 15 |
import os
|
| 16 |
import json
|
|
@@ -23,8 +23,6 @@ import numpy as np
|
|
| 23 |
from datetime import datetime, timedelta
|
| 24 |
import plotly.graph_objects as go
|
| 25 |
from plotly.subplots import make_subplots
|
| 26 |
-
from scipy import stats
|
| 27 |
-
from scipy.optimize import minimize
|
| 28 |
import warnings
|
| 29 |
warnings.filterwarnings('ignore')
|
| 30 |
|
|
@@ -32,7 +30,7 @@ warnings.filterwarnings('ignore')
|
|
| 32 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 33 |
# K2 Think V2 API Configuration
|
| 34 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 35 |
-
K2_API_KEY = os.environ.get("K2_API_KEY")
|
| 36 |
K2_BASE_URL = "https://api.k2think.ai/v1/chat/completions"
|
| 37 |
K2_MODEL = "MBZUAI-IFM/K2-Think-v2"
|
| 38 |
|
|
@@ -178,22 +176,31 @@ def compute_technical_indicators(df: pd.DataFrame) -> pd.DataFrame:
|
|
| 178 |
"""Compute technical indicators"""
|
| 179 |
df = df.copy()
|
| 180 |
|
|
|
|
| 181 |
df['Returns'] = df['Close'].pct_change()
|
|
|
|
|
|
|
| 182 |
df['SMA_20'] = df['Close'].rolling(20).mean()
|
| 183 |
df['SMA_50'] = df['Close'].rolling(50).mean()
|
| 184 |
df['SMA_200'] = df['Close'].rolling(200).mean()
|
|
|
|
|
|
|
| 185 |
df['EMA_12'] = df['Close'].ewm(span=12, adjust=False).mean()
|
| 186 |
df['EMA_26'] = df['Close'].ewm(span=26, adjust=False).mean()
|
|
|
|
|
|
|
| 187 |
df['MACD'] = df['EMA_12'] - df['EMA_26']
|
| 188 |
df['MACD_Signal'] = df['MACD'].ewm(span=9, adjust=False).mean()
|
| 189 |
df['MACD_Histogram'] = df['MACD'] - df['MACD_Signal']
|
| 190 |
|
|
|
|
| 191 |
delta = df['Close'].diff()
|
| 192 |
gain = (delta.where(delta > 0, 0)).rolling(14).mean()
|
| 193 |
loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
|
| 194 |
rs = gain / (loss + 1e-10)
|
| 195 |
df['RSI'] = 100 - (100 / (1 + rs))
|
| 196 |
|
|
|
|
| 197 |
df['BB_Middle'] = df['Close'].rolling(20).mean()
|
| 198 |
bb_std = df['Close'].rolling(20).std()
|
| 199 |
df['BB_Upper'] = df['BB_Middle'] + 2 * bb_std
|
|
@@ -201,20 +208,24 @@ def compute_technical_indicators(df: pd.DataFrame) -> pd.DataFrame:
|
|
| 201 |
df['BB_Width'] = (df['BB_Upper'] - df['BB_Lower']) / df['BB_Middle']
|
| 202 |
df['BB_Position'] = (df['Close'] - df['BB_Lower']) / (df['BB_Upper'] - df['BB_Lower'] + 1e-10)
|
| 203 |
|
|
|
|
| 204 |
typical_price = (df['High'] + df['Low'] + df['Close']) / 3
|
| 205 |
df['VWAP'] = (typical_price * df['Volume']).cumsum() / (df['Volume'].cumsum() + 1e-10)
|
| 206 |
|
|
|
|
| 207 |
high_low = df['High'] - df['Low']
|
| 208 |
high_close = np.abs(df['High'] - df['Close'].shift())
|
| 209 |
low_close = np.abs(df['Low'] - df['Close'].shift())
|
| 210 |
tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
|
| 211 |
df['ATR'] = tr.rolling(14).mean()
|
| 212 |
|
|
|
|
| 213 |
low_14 = df['Low'].rolling(14).min()
|
| 214 |
high_14 = df['High'].rolling(14).max()
|
| 215 |
df['Stoch_K'] = 100 * (df['Close'] - low_14) / (high_14 - low_14 + 1e-10)
|
| 216 |
df['Stoch_D'] = df['Stoch_K'].rolling(3).mean()
|
| 217 |
|
|
|
|
| 218 |
df['Volume_MA'] = df['Volume'].rolling(20).mean()
|
| 219 |
df['Volume_Ratio'] = df['Volume'] / (df['Volume_MA'] + 1e-10)
|
| 220 |
|
|
@@ -235,11 +246,13 @@ def generate_signals(df: pd.DataFrame) -> dict:
|
|
| 235 |
'confidence': 50
|
| 236 |
}
|
| 237 |
|
|
|
|
| 238 |
if latest['Close'] > latest['SMA_20'] > latest['SMA_50']:
|
| 239 |
signals['trend'] = 'bullish'
|
| 240 |
elif latest['Close'] < latest['SMA_20'] < latest['SMA_50']:
|
| 241 |
signals['trend'] = 'bearish'
|
| 242 |
|
|
|
|
| 243 |
if latest['RSI'] < 30:
|
| 244 |
signals['momentum'] = 'oversold (bullish bounce potential)'
|
| 245 |
elif latest['RSI'] > 70:
|
|
@@ -249,15 +262,18 @@ def generate_signals(df: pd.DataFrame) -> dict:
|
|
| 249 |
elif latest['MACD'] < latest['MACD_Signal'] and prev['MACD'] >= prev['MACD_Signal']:
|
| 250 |
signals['momentum'] = 'MACD bearish crossover'
|
| 251 |
|
|
|
|
| 252 |
bb_width = latest['BB_Width']
|
| 253 |
if bb_width > df['BB_Width'].quantile(0.9):
|
| 254 |
signals['volatility'] = 'expanding (breakout likely)'
|
| 255 |
elif bb_width < df['BB_Width'].quantile(0.1):
|
| 256 |
signals['volatility'] = 'contracting (squeeze setup)'
|
| 257 |
|
|
|
|
| 258 |
if latest['Volume_Ratio'] > 2.0:
|
| 259 |
signals['volume'] = 'heavy (institutional interest)'
|
| 260 |
|
|
|
|
| 261 |
score = 50
|
| 262 |
if signals['trend'] == 'bullish': score += 15
|
| 263 |
if signals['trend'] == 'bearish': score -= 15
|
|
@@ -270,6 +286,7 @@ def generate_signals(df: pd.DataFrame) -> dict:
|
|
| 270 |
|
| 271 |
signals['composite_score'] = max(0, min(100, score))
|
| 272 |
|
|
|
|
| 273 |
bullish_count = sum([
|
| 274 |
signals['trend'] == 'bullish',
|
| 275 |
'oversold' in signals['momentum'] or 'bullish crossover' in signals['momentum'],
|
|
@@ -294,24 +311,31 @@ def compute_risk_metrics(df: pd.DataFrame) -> dict:
|
|
| 294 |
if len(returns) < 30:
|
| 295 |
return {}
|
| 296 |
|
|
|
|
| 297 |
annual_return = returns.mean() * 252
|
| 298 |
annual_vol = returns.std() * np.sqrt(252)
|
| 299 |
sharpe = annual_return / (annual_vol + 1e-10)
|
| 300 |
|
|
|
|
| 301 |
downside = returns[returns < 0]
|
| 302 |
downside_dev = downside.std() * np.sqrt(252) if len(downside) > 0 else 1e-10
|
| 303 |
sortino = annual_return / (downside_dev + 1e-10)
|
| 304 |
|
|
|
|
| 305 |
cumulative = (1 + returns).cumprod()
|
| 306 |
running_max = cumulative.expanding().max()
|
| 307 |
drawdown = (cumulative - running_max) / running_max
|
| 308 |
max_dd = drawdown.min()
|
| 309 |
|
|
|
|
| 310 |
var_95 = np.percentile(returns, 5)
|
| 311 |
var_99 = np.percentile(returns, 1)
|
| 312 |
cvar_95 = returns[returns <= var_95].mean() if len(returns[returns <= var_95]) > 0 else var_95
|
| 313 |
|
|
|
|
| 314 |
calmar = annual_return / (abs(max_dd) + 1e-10)
|
|
|
|
|
|
|
| 315 |
skew = returns.skew()
|
| 316 |
kurt = returns.kurtosis()
|
| 317 |
|
|
@@ -347,6 +371,7 @@ def create_candlestick_chart(df: pd.DataFrame, ticker: str):
|
|
| 347 |
subplot_titles=(f'{ticker} Price', 'Volume', 'RSI')
|
| 348 |
)
|
| 349 |
|
|
|
|
| 350 |
fig.add_trace(go.Candlestick(
|
| 351 |
x=df.index,
|
| 352 |
open=df['Open'],
|
|
@@ -356,11 +381,13 @@ def create_candlestick_chart(df: pd.DataFrame, ticker: str):
|
|
| 356 |
name='Price'
|
| 357 |
), row=1, col=1)
|
| 358 |
|
|
|
|
| 359 |
fig.add_trace(go.Scatter(x=df.index, y=df['SMA_20'],
|
| 360 |
line=dict(color='orange', width=1), name='SMA 20'), row=1, col=1)
|
| 361 |
fig.add_trace(go.Scatter(x=df.index, y=df['SMA_50'],
|
| 362 |
line=dict(color='blue', width=1), name='SMA 50'), row=1, col=1)
|
| 363 |
|
|
|
|
| 364 |
fig.add_trace(go.Scatter(x=df.index, y=df['BB_Upper'],
|
| 365 |
line=dict(color='gray', width=1, dash='dash'),
|
| 366 |
name='BB Upper', opacity=0.5), row=1, col=1)
|
|
@@ -368,11 +395,13 @@ def create_candlestick_chart(df: pd.DataFrame, ticker: str):
|
|
| 368 |
line=dict(color='gray', width=1, dash='dash'),
|
| 369 |
name='BB Lower', opacity=0.5), row=1, col=1)
|
| 370 |
|
|
|
|
| 371 |
colors = ['green' if df['Close'].iloc[i] >= df['Open'].iloc[i] else 'red'
|
| 372 |
for i in range(len(df))]
|
| 373 |
fig.add_trace(go.Bar(x=df.index, y=df['Volume'],
|
| 374 |
marker_color=colors, name='Volume', opacity=0.7), row=2, col=1)
|
| 375 |
|
|
|
|
| 376 |
fig.add_trace(go.Scatter(x=df.index, y=df['RSI'],
|
| 377 |
line=dict(color='purple', width=1.5), name='RSI'), row=3, col=1)
|
| 378 |
fig.add_hline(y=70, line_dash="dash", line_color="red", row=3, col=1)
|
|
@@ -430,6 +459,7 @@ def create_return_distribution(returns: pd.Series, ticker: str):
|
|
| 430 |
opacity=0.7
|
| 431 |
))
|
| 432 |
|
|
|
|
| 433 |
x_range = np.linspace(returns.min(), returns.max(), 100)
|
| 434 |
mu, sigma = returns.mean(), returns.std()
|
| 435 |
normal_pdf = len(returns) * (x_range[1] - x_range[0]) * stats.norm.pdf(x_range, mu, sigma)
|
|
@@ -455,6 +485,7 @@ def create_return_distribution(returns: pd.Series, ticker: str):
|
|
| 455 |
def optimize_portfolio(tickers: list, period: str = "1y"):
|
| 456 |
"""Mean-variance optimization for a portfolio"""
|
| 457 |
try:
|
|
|
|
| 458 |
data = {}
|
| 459 |
for t in tickers:
|
| 460 |
t = t.strip().upper()
|
|
@@ -467,6 +498,7 @@ def optimize_portfolio(tickers: list, period: str = "1y"):
|
|
| 467 |
if len(data) < 2:
|
| 468 |
return None, "Need at least 2 valid tickers for portfolio optimization."
|
| 469 |
|
|
|
|
| 470 |
prices = pd.DataFrame(data)
|
| 471 |
prices = prices.dropna()
|
| 472 |
returns = prices.pct_change().dropna()
|
|
@@ -474,8 +506,11 @@ def optimize_portfolio(tickers: list, period: str = "1y"):
|
|
| 474 |
if len(returns) < 30:
|
| 475 |
return None, "Insufficient data after alignment."
|
| 476 |
|
|
|
|
| 477 |
mu = returns.mean() * 252
|
| 478 |
sigma = returns.cov() * 252
|
|
|
|
|
|
|
| 479 |
n = len(mu)
|
| 480 |
|
| 481 |
def neg_sharpe(weights):
|
|
@@ -485,16 +520,19 @@ def optimize_portfolio(tickers: list, period: str = "1y"):
|
|
| 485 |
return -(port_return / (port_vol + 1e-10))
|
| 486 |
|
| 487 |
constraints = {'type': 'eq', 'fun': lambda w: np.sum(w) - 1}
|
| 488 |
-
bounds = tuple((0, 0.5) for _ in range(n))
|
| 489 |
x0 = np.ones(n) / n
|
| 490 |
|
| 491 |
result = minimize(neg_sharpe, x0, method='SLSQP', bounds=bounds, constraints=constraints)
|
|
|
|
| 492 |
optimal_weights = result.x
|
| 493 |
|
|
|
|
| 494 |
port_return = np.dot(optimal_weights, mu)
|
| 495 |
port_vol = np.sqrt(np.dot(optimal_weights.T, np.dot(sigma, optimal_weights)))
|
| 496 |
port_sharpe = port_return / (port_vol + 1e-10)
|
| 497 |
|
|
|
|
| 498 |
eq_weights = np.ones(n) / n
|
| 499 |
eq_return = np.dot(eq_weights, mu)
|
| 500 |
eq_vol = np.sqrt(np.dot(eq_weights.T, np.dot(sigma, eq_weights)))
|
|
@@ -524,6 +562,7 @@ def create_efficient_frontier(opt_result: dict):
|
|
| 524 |
sigma = opt_result['covariance']
|
| 525 |
n = len(mu)
|
| 526 |
|
|
|
|
| 527 |
n_portfolios = 5000
|
| 528 |
weights = np.random.dirichlet(np.ones(n), n_portfolios)
|
| 529 |
|
|
@@ -533,6 +572,7 @@ def create_efficient_frontier(opt_result: dict):
|
|
| 533 |
|
| 534 |
fig = go.Figure()
|
| 535 |
|
|
|
|
| 536 |
fig.add_trace(go.Scatter(
|
| 537 |
x=port_vols, y=port_returns,
|
| 538 |
mode='markers',
|
|
@@ -546,6 +586,7 @@ def create_efficient_frontier(opt_result: dict):
|
|
| 546 |
name='Random Portfolios'
|
| 547 |
))
|
| 548 |
|
|
|
|
| 549 |
fig.add_trace(go.Scatter(
|
| 550 |
x=[opt_result['optimal_volatility']],
|
| 551 |
y=[opt_result['optimal_return']],
|
|
@@ -556,6 +597,7 @@ def create_efficient_frontier(opt_result: dict):
|
|
| 556 |
name='Optimal Portfolio'
|
| 557 |
))
|
| 558 |
|
|
|
|
| 559 |
fig.add_trace(go.Scatter(
|
| 560 |
x=[opt_result['equal_volatility']],
|
| 561 |
y=[opt_result['equal_return']],
|
|
@@ -587,17 +629,21 @@ def analyze_ticker(ticker: str, period: str = "6mo"):
|
|
| 587 |
return None, None, None, None, "Please enter a ticker symbol."
|
| 588 |
|
| 589 |
df, info = fetch_stock_data(ticker, period=period)
|
|
|
|
| 590 |
if df is None:
|
| 591 |
return None, None, None, None, info
|
| 592 |
|
|
|
|
| 593 |
df = compute_technical_indicators(df)
|
| 594 |
signals = generate_signals(df)
|
| 595 |
risk = compute_risk_metrics(df)
|
| 596 |
|
|
|
|
| 597 |
candlestick = create_candlestick_chart(df, ticker)
|
| 598 |
macd_chart = create_macd_chart(df, ticker)
|
| 599 |
returns_chart = create_return_distribution(df['Returns'].dropna(), ticker)
|
| 600 |
|
|
|
|
| 601 |
latest = df.iloc[-1]
|
| 602 |
prev = df.iloc[-2] if len(df) > 1 else latest
|
| 603 |
|
|
@@ -652,6 +698,7 @@ def ai_analyze(ticker: str, period: str = "6mo"):
|
|
| 652 |
risk = compute_risk_metrics(df)
|
| 653 |
latest = df.iloc[-1]
|
| 654 |
|
|
|
|
| 655 |
data_summary = f"""
|
| 656 |
Ticker: {ticker}
|
| 657 |
Current Price: ${latest['Close']:.2f}
|
|
@@ -685,6 +732,7 @@ Max Drawdown: {risk.get('max_drawdown', 0)*100:.1f}%
|
|
| 685 |
VaR 95%: {risk.get('var_95_daily', 0)*100:.2f}%
|
| 686 |
"""
|
| 687 |
|
|
|
|
| 688 |
client = K2ThinkClient()
|
| 689 |
analysis = client.analyze_market(ticker, data_summary, technical_summary)
|
| 690 |
|
|
@@ -698,19 +746,23 @@ def analyze_portfolio(tickers_str: str, period: str = "1y"):
|
|
| 698 |
if len(tickers) < 2:
|
| 699 |
return None, None, "Please enter at least 2 tickers separated by commas."
|
| 700 |
|
|
|
|
| 701 |
result, error = optimize_portfolio(tickers, period)
|
| 702 |
|
| 703 |
if error:
|
| 704 |
return None, None, error
|
| 705 |
|
|
|
|
| 706 |
frontier = create_efficient_frontier(result)
|
| 707 |
|
|
|
|
| 708 |
weights_df = pd.DataFrame({
|
| 709 |
'Ticker': result['tickers'],
|
| 710 |
'Optimal Weight (%)': result['optimal_weights'] * 100,
|
| 711 |
'Equal Weight (%)': result['equal_weights'] * 100
|
| 712 |
})
|
| 713 |
|
|
|
|
| 714 |
summary = f"""## Portfolio Optimization Results
|
| 715 |
|
| 716 |
**Tickers:** {', '.join(result['tickers'])}
|
|
@@ -754,6 +806,7 @@ def ai_portfolio_advice(tickers_str: str, period: str = "1y"):
|
|
| 754 |
if error:
|
| 755 |
return error
|
| 756 |
|
|
|
|
| 757 |
portfolio_data = f"""
|
| 758 |
Holdings:
|
| 759 |
{chr(10).join([f"- {t}: {w*100:.1f}%" for t, w in zip(result['tickers'], result['optimal_weights'])])}
|
|
@@ -766,9 +819,10 @@ Individual Asset Returns (annual):
|
|
| 766 |
{chr(10).join([f"- {t}: {r*100:.1f}%" for t, r in zip(result['tickers'], result['annual_returns'])])}
|
| 767 |
"""
|
| 768 |
|
|
|
|
| 769 |
corr = result['covariance']
|
| 770 |
for i in range(len(corr)):
|
| 771 |
-
corr.iloc[i, i] = np.nan
|
| 772 |
|
| 773 |
risk_metrics = f"""
|
| 774 |
Correlation Matrix (off-diagonal):
|
|
@@ -802,6 +856,7 @@ def build_app():
|
|
| 802 |
"""
|
| 803 |
) as demo:
|
| 804 |
|
|
|
|
| 805 |
gr.HTML("""
|
| 806 |
<div class="main-title">π₯ AlphaForge x K2 Think V2</div>
|
| 807 |
<div class="subtitle">Elite Quantitative Trading Platform powered by MBZUAI's State-of-the-Art Reasoning Model</div>
|
|
@@ -847,6 +902,7 @@ def build_app():
|
|
| 847 |
|
| 848 |
error_output = gr.Textbox(label="Status", visible=False)
|
| 849 |
|
|
|
|
| 850 |
analyze_btn.click(
|
| 851 |
fn=analyze_ticker,
|
| 852 |
inputs=[ticker_input, period_input],
|
|
@@ -985,6 +1041,7 @@ def build_app():
|
|
| 985 |
if __name__ == "__main__":
|
| 986 |
demo = build_app()
|
| 987 |
|
|
|
|
| 988 |
demo.queue().launch(
|
| 989 |
server_name="0.0.0.0",
|
| 990 |
server_port=7860,
|
|
|
|
| 10 |
- Alpha signal generation
|
| 11 |
- AI-powered market analysis with chain-of-thought reasoning
|
| 12 |
|
| 13 |
+
API Key: IFM-4SpQ0qEg0Wlsw04O
|
| 14 |
"""
|
| 15 |
import os
|
| 16 |
import json
|
|
|
|
| 23 |
from datetime import datetime, timedelta
|
| 24 |
import plotly.graph_objects as go
|
| 25 |
from plotly.subplots import make_subplots
|
|
|
|
|
|
|
| 26 |
import warnings
|
| 27 |
warnings.filterwarnings('ignore')
|
| 28 |
|
|
|
|
| 30 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
# K2 Think V2 API Configuration
|
| 32 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 33 |
+
K2_API_KEY = os.environ.get("K2_API_KEY", "IFM-4SpQ0qEg0Wlsw04O")
|
| 34 |
K2_BASE_URL = "https://api.k2think.ai/v1/chat/completions"
|
| 35 |
K2_MODEL = "MBZUAI-IFM/K2-Think-v2"
|
| 36 |
|
|
|
|
| 176 |
"""Compute technical indicators"""
|
| 177 |
df = df.copy()
|
| 178 |
|
| 179 |
+
# Returns
|
| 180 |
df['Returns'] = df['Close'].pct_change()
|
| 181 |
+
|
| 182 |
+
# Simple Moving Averages
|
| 183 |
df['SMA_20'] = df['Close'].rolling(20).mean()
|
| 184 |
df['SMA_50'] = df['Close'].rolling(50).mean()
|
| 185 |
df['SMA_200'] = df['Close'].rolling(200).mean()
|
| 186 |
+
|
| 187 |
+
# EMA
|
| 188 |
df['EMA_12'] = df['Close'].ewm(span=12, adjust=False).mean()
|
| 189 |
df['EMA_26'] = df['Close'].ewm(span=26, adjust=False).mean()
|
| 190 |
+
|
| 191 |
+
# MACD
|
| 192 |
df['MACD'] = df['EMA_12'] - df['EMA_26']
|
| 193 |
df['MACD_Signal'] = df['MACD'].ewm(span=9, adjust=False).mean()
|
| 194 |
df['MACD_Histogram'] = df['MACD'] - df['MACD_Signal']
|
| 195 |
|
| 196 |
+
# RSI
|
| 197 |
delta = df['Close'].diff()
|
| 198 |
gain = (delta.where(delta > 0, 0)).rolling(14).mean()
|
| 199 |
loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
|
| 200 |
rs = gain / (loss + 1e-10)
|
| 201 |
df['RSI'] = 100 - (100 / (1 + rs))
|
| 202 |
|
| 203 |
+
# Bollinger Bands
|
| 204 |
df['BB_Middle'] = df['Close'].rolling(20).mean()
|
| 205 |
bb_std = df['Close'].rolling(20).std()
|
| 206 |
df['BB_Upper'] = df['BB_Middle'] + 2 * bb_std
|
|
|
|
| 208 |
df['BB_Width'] = (df['BB_Upper'] - df['BB_Lower']) / df['BB_Middle']
|
| 209 |
df['BB_Position'] = (df['Close'] - df['BB_Lower']) / (df['BB_Upper'] - df['BB_Lower'] + 1e-10)
|
| 210 |
|
| 211 |
+
# VWAP
|
| 212 |
typical_price = (df['High'] + df['Low'] + df['Close']) / 3
|
| 213 |
df['VWAP'] = (typical_price * df['Volume']).cumsum() / (df['Volume'].cumsum() + 1e-10)
|
| 214 |
|
| 215 |
+
# ATR (Average True Range)
|
| 216 |
high_low = df['High'] - df['Low']
|
| 217 |
high_close = np.abs(df['High'] - df['Close'].shift())
|
| 218 |
low_close = np.abs(df['Low'] - df['Close'].shift())
|
| 219 |
tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
|
| 220 |
df['ATR'] = tr.rolling(14).mean()
|
| 221 |
|
| 222 |
+
# Stochastic Oscillator
|
| 223 |
low_14 = df['Low'].rolling(14).min()
|
| 224 |
high_14 = df['High'].rolling(14).max()
|
| 225 |
df['Stoch_K'] = 100 * (df['Close'] - low_14) / (high_14 - low_14 + 1e-10)
|
| 226 |
df['Stoch_D'] = df['Stoch_K'].rolling(3).mean()
|
| 227 |
|
| 228 |
+
# Volume indicators
|
| 229 |
df['Volume_MA'] = df['Volume'].rolling(20).mean()
|
| 230 |
df['Volume_Ratio'] = df['Volume'] / (df['Volume_MA'] + 1e-10)
|
| 231 |
|
|
|
|
| 246 |
'confidence': 50
|
| 247 |
}
|
| 248 |
|
| 249 |
+
# Trend signal
|
| 250 |
if latest['Close'] > latest['SMA_20'] > latest['SMA_50']:
|
| 251 |
signals['trend'] = 'bullish'
|
| 252 |
elif latest['Close'] < latest['SMA_20'] < latest['SMA_50']:
|
| 253 |
signals['trend'] = 'bearish'
|
| 254 |
|
| 255 |
+
# Momentum signal
|
| 256 |
if latest['RSI'] < 30:
|
| 257 |
signals['momentum'] = 'oversold (bullish bounce potential)'
|
| 258 |
elif latest['RSI'] > 70:
|
|
|
|
| 262 |
elif latest['MACD'] < latest['MACD_Signal'] and prev['MACD'] >= prev['MACD_Signal']:
|
| 263 |
signals['momentum'] = 'MACD bearish crossover'
|
| 264 |
|
| 265 |
+
# Volatility signal
|
| 266 |
bb_width = latest['BB_Width']
|
| 267 |
if bb_width > df['BB_Width'].quantile(0.9):
|
| 268 |
signals['volatility'] = 'expanding (breakout likely)'
|
| 269 |
elif bb_width < df['BB_Width'].quantile(0.1):
|
| 270 |
signals['volatility'] = 'contracting (squeeze setup)'
|
| 271 |
|
| 272 |
+
# Volume signal
|
| 273 |
if latest['Volume_Ratio'] > 2.0:
|
| 274 |
signals['volume'] = 'heavy (institutional interest)'
|
| 275 |
|
| 276 |
+
# Composite score (0-100, >60 bullish, <40 bearish)
|
| 277 |
score = 50
|
| 278 |
if signals['trend'] == 'bullish': score += 15
|
| 279 |
if signals['trend'] == 'bearish': score -= 15
|
|
|
|
| 286 |
|
| 287 |
signals['composite_score'] = max(0, min(100, score))
|
| 288 |
|
| 289 |
+
# Confidence based on indicator agreement
|
| 290 |
bullish_count = sum([
|
| 291 |
signals['trend'] == 'bullish',
|
| 292 |
'oversold' in signals['momentum'] or 'bullish crossover' in signals['momentum'],
|
|
|
|
| 311 |
if len(returns) < 30:
|
| 312 |
return {}
|
| 313 |
|
| 314 |
+
# Basic stats
|
| 315 |
annual_return = returns.mean() * 252
|
| 316 |
annual_vol = returns.std() * np.sqrt(252)
|
| 317 |
sharpe = annual_return / (annual_vol + 1e-10)
|
| 318 |
|
| 319 |
+
# Sortino (downside deviation)
|
| 320 |
downside = returns[returns < 0]
|
| 321 |
downside_dev = downside.std() * np.sqrt(252) if len(downside) > 0 else 1e-10
|
| 322 |
sortino = annual_return / (downside_dev + 1e-10)
|
| 323 |
|
| 324 |
+
# Max drawdown
|
| 325 |
cumulative = (1 + returns).cumprod()
|
| 326 |
running_max = cumulative.expanding().max()
|
| 327 |
drawdown = (cumulative - running_max) / running_max
|
| 328 |
max_dd = drawdown.min()
|
| 329 |
|
| 330 |
+
# VaR / CVaR
|
| 331 |
var_95 = np.percentile(returns, 5)
|
| 332 |
var_99 = np.percentile(returns, 1)
|
| 333 |
cvar_95 = returns[returns <= var_95].mean() if len(returns[returns <= var_95]) > 0 else var_95
|
| 334 |
|
| 335 |
+
# Calmar
|
| 336 |
calmar = annual_return / (abs(max_dd) + 1e-10)
|
| 337 |
+
|
| 338 |
+
# Skewness and kurtosis
|
| 339 |
skew = returns.skew()
|
| 340 |
kurt = returns.kurtosis()
|
| 341 |
|
|
|
|
| 371 |
subplot_titles=(f'{ticker} Price', 'Volume', 'RSI')
|
| 372 |
)
|
| 373 |
|
| 374 |
+
# Candlestick
|
| 375 |
fig.add_trace(go.Candlestick(
|
| 376 |
x=df.index,
|
| 377 |
open=df['Open'],
|
|
|
|
| 381 |
name='Price'
|
| 382 |
), row=1, col=1)
|
| 383 |
|
| 384 |
+
# SMAs
|
| 385 |
fig.add_trace(go.Scatter(x=df.index, y=df['SMA_20'],
|
| 386 |
line=dict(color='orange', width=1), name='SMA 20'), row=1, col=1)
|
| 387 |
fig.add_trace(go.Scatter(x=df.index, y=df['SMA_50'],
|
| 388 |
line=dict(color='blue', width=1), name='SMA 50'), row=1, col=1)
|
| 389 |
|
| 390 |
+
# Bollinger Bands
|
| 391 |
fig.add_trace(go.Scatter(x=df.index, y=df['BB_Upper'],
|
| 392 |
line=dict(color='gray', width=1, dash='dash'),
|
| 393 |
name='BB Upper', opacity=0.5), row=1, col=1)
|
|
|
|
| 395 |
line=dict(color='gray', width=1, dash='dash'),
|
| 396 |
name='BB Lower', opacity=0.5), row=1, col=1)
|
| 397 |
|
| 398 |
+
# Volume
|
| 399 |
colors = ['green' if df['Close'].iloc[i] >= df['Open'].iloc[i] else 'red'
|
| 400 |
for i in range(len(df))]
|
| 401 |
fig.add_trace(go.Bar(x=df.index, y=df['Volume'],
|
| 402 |
marker_color=colors, name='Volume', opacity=0.7), row=2, col=1)
|
| 403 |
|
| 404 |
+
# RSI
|
| 405 |
fig.add_trace(go.Scatter(x=df.index, y=df['RSI'],
|
| 406 |
line=dict(color='purple', width=1.5), name='RSI'), row=3, col=1)
|
| 407 |
fig.add_hline(y=70, line_dash="dash", line_color="red", row=3, col=1)
|
|
|
|
| 459 |
opacity=0.7
|
| 460 |
))
|
| 461 |
|
| 462 |
+
# Add normal overlay
|
| 463 |
x_range = np.linspace(returns.min(), returns.max(), 100)
|
| 464 |
mu, sigma = returns.mean(), returns.std()
|
| 465 |
normal_pdf = len(returns) * (x_range[1] - x_range[0]) * stats.norm.pdf(x_range, mu, sigma)
|
|
|
|
| 485 |
def optimize_portfolio(tickers: list, period: str = "1y"):
|
| 486 |
"""Mean-variance optimization for a portfolio"""
|
| 487 |
try:
|
| 488 |
+
# Fetch data
|
| 489 |
data = {}
|
| 490 |
for t in tickers:
|
| 491 |
t = t.strip().upper()
|
|
|
|
| 498 |
if len(data) < 2:
|
| 499 |
return None, "Need at least 2 valid tickers for portfolio optimization."
|
| 500 |
|
| 501 |
+
# Align and compute returns
|
| 502 |
prices = pd.DataFrame(data)
|
| 503 |
prices = prices.dropna()
|
| 504 |
returns = prices.pct_change().dropna()
|
|
|
|
| 506 |
if len(returns) < 30:
|
| 507 |
return None, "Insufficient data after alignment."
|
| 508 |
|
| 509 |
+
# Expected returns and covariance
|
| 510 |
mu = returns.mean() * 252
|
| 511 |
sigma = returns.cov() * 252
|
| 512 |
+
|
| 513 |
+
# Optimize: max Sharpe
|
| 514 |
n = len(mu)
|
| 515 |
|
| 516 |
def neg_sharpe(weights):
|
|
|
|
| 520 |
return -(port_return / (port_vol + 1e-10))
|
| 521 |
|
| 522 |
constraints = {'type': 'eq', 'fun': lambda w: np.sum(w) - 1}
|
| 523 |
+
bounds = tuple((0, 0.5) for _ in range(n)) # Max 50% per asset
|
| 524 |
x0 = np.ones(n) / n
|
| 525 |
|
| 526 |
result = minimize(neg_sharpe, x0, method='SLSQP', bounds=bounds, constraints=constraints)
|
| 527 |
+
|
| 528 |
optimal_weights = result.x
|
| 529 |
|
| 530 |
+
# Portfolio metrics
|
| 531 |
port_return = np.dot(optimal_weights, mu)
|
| 532 |
port_vol = np.sqrt(np.dot(optimal_weights.T, np.dot(sigma, optimal_weights)))
|
| 533 |
port_sharpe = port_return / (port_vol + 1e-10)
|
| 534 |
|
| 535 |
+
# Equal weight for comparison
|
| 536 |
eq_weights = np.ones(n) / n
|
| 537 |
eq_return = np.dot(eq_weights, mu)
|
| 538 |
eq_vol = np.sqrt(np.dot(eq_weights.T, np.dot(sigma, eq_weights)))
|
|
|
|
| 562 |
sigma = opt_result['covariance']
|
| 563 |
n = len(mu)
|
| 564 |
|
| 565 |
+
# Generate random portfolios
|
| 566 |
n_portfolios = 5000
|
| 567 |
weights = np.random.dirichlet(np.ones(n), n_portfolios)
|
| 568 |
|
|
|
|
| 572 |
|
| 573 |
fig = go.Figure()
|
| 574 |
|
| 575 |
+
# Scatter of random portfolios
|
| 576 |
fig.add_trace(go.Scatter(
|
| 577 |
x=port_vols, y=port_returns,
|
| 578 |
mode='markers',
|
|
|
|
| 586 |
name='Random Portfolios'
|
| 587 |
))
|
| 588 |
|
| 589 |
+
# Optimal portfolio
|
| 590 |
fig.add_trace(go.Scatter(
|
| 591 |
x=[opt_result['optimal_volatility']],
|
| 592 |
y=[opt_result['optimal_return']],
|
|
|
|
| 597 |
name='Optimal Portfolio'
|
| 598 |
))
|
| 599 |
|
| 600 |
+
# Equal weight
|
| 601 |
fig.add_trace(go.Scatter(
|
| 602 |
x=[opt_result['equal_volatility']],
|
| 603 |
y=[opt_result['equal_return']],
|
|
|
|
| 629 |
return None, None, None, None, "Please enter a ticker symbol."
|
| 630 |
|
| 631 |
df, info = fetch_stock_data(ticker, period=period)
|
| 632 |
+
|
| 633 |
if df is None:
|
| 634 |
return None, None, None, None, info
|
| 635 |
|
| 636 |
+
# Compute indicators
|
| 637 |
df = compute_technical_indicators(df)
|
| 638 |
signals = generate_signals(df)
|
| 639 |
risk = compute_risk_metrics(df)
|
| 640 |
|
| 641 |
+
# Create charts
|
| 642 |
candlestick = create_candlestick_chart(df, ticker)
|
| 643 |
macd_chart = create_macd_chart(df, ticker)
|
| 644 |
returns_chart = create_return_distribution(df['Returns'].dropna(), ticker)
|
| 645 |
|
| 646 |
+
# Summary text
|
| 647 |
latest = df.iloc[-1]
|
| 648 |
prev = df.iloc[-2] if len(df) > 1 else latest
|
| 649 |
|
|
|
|
| 698 |
risk = compute_risk_metrics(df)
|
| 699 |
latest = df.iloc[-1]
|
| 700 |
|
| 701 |
+
# Build data summary
|
| 702 |
data_summary = f"""
|
| 703 |
Ticker: {ticker}
|
| 704 |
Current Price: ${latest['Close']:.2f}
|
|
|
|
| 732 |
VaR 95%: {risk.get('var_95_daily', 0)*100:.2f}%
|
| 733 |
"""
|
| 734 |
|
| 735 |
+
# Call K2 Think V2
|
| 736 |
client = K2ThinkClient()
|
| 737 |
analysis = client.analyze_market(ticker, data_summary, technical_summary)
|
| 738 |
|
|
|
|
| 746 |
if len(tickers) < 2:
|
| 747 |
return None, None, "Please enter at least 2 tickers separated by commas."
|
| 748 |
|
| 749 |
+
# Optimize
|
| 750 |
result, error = optimize_portfolio(tickers, period)
|
| 751 |
|
| 752 |
if error:
|
| 753 |
return None, None, error
|
| 754 |
|
| 755 |
+
# Create efficient frontier
|
| 756 |
frontier = create_efficient_frontier(result)
|
| 757 |
|
| 758 |
+
# Weights comparison
|
| 759 |
weights_df = pd.DataFrame({
|
| 760 |
'Ticker': result['tickers'],
|
| 761 |
'Optimal Weight (%)': result['optimal_weights'] * 100,
|
| 762 |
'Equal Weight (%)': result['equal_weights'] * 100
|
| 763 |
})
|
| 764 |
|
| 765 |
+
# Summary
|
| 766 |
summary = f"""## Portfolio Optimization Results
|
| 767 |
|
| 768 |
**Tickers:** {', '.join(result['tickers'])}
|
|
|
|
| 806 |
if error:
|
| 807 |
return error
|
| 808 |
|
| 809 |
+
# Build portfolio data
|
| 810 |
portfolio_data = f"""
|
| 811 |
Holdings:
|
| 812 |
{chr(10).join([f"- {t}: {w*100:.1f}%" for t, w in zip(result['tickers'], result['optimal_weights'])])}
|
|
|
|
| 819 |
{chr(10).join([f"- {t}: {r*100:.1f}%" for t, r in zip(result['tickers'], result['annual_returns'])])}
|
| 820 |
"""
|
| 821 |
|
| 822 |
+
# Correlation matrix
|
| 823 |
corr = result['covariance']
|
| 824 |
for i in range(len(corr)):
|
| 825 |
+
corr.iloc[i, i] = np.nan # Hide diagonal
|
| 826 |
|
| 827 |
risk_metrics = f"""
|
| 828 |
Correlation Matrix (off-diagonal):
|
|
|
|
| 856 |
"""
|
| 857 |
) as demo:
|
| 858 |
|
| 859 |
+
# Header
|
| 860 |
gr.HTML("""
|
| 861 |
<div class="main-title">π₯ AlphaForge x K2 Think V2</div>
|
| 862 |
<div class="subtitle">Elite Quantitative Trading Platform powered by MBZUAI's State-of-the-Art Reasoning Model</div>
|
|
|
|
| 902 |
|
| 903 |
error_output = gr.Textbox(label="Status", visible=False)
|
| 904 |
|
| 905 |
+
# Bind buttons
|
| 906 |
analyze_btn.click(
|
| 907 |
fn=analyze_ticker,
|
| 908 |
inputs=[ticker_input, period_input],
|
|
|
|
| 1041 |
if __name__ == "__main__":
|
| 1042 |
demo = build_app()
|
| 1043 |
|
| 1044 |
+
# For HuggingFace Spaces
|
| 1045 |
demo.queue().launch(
|
| 1046 |
server_name="0.0.0.0",
|
| 1047 |
server_port=7860,
|