import streamlit as st
import pandas as pd
import numpy as np
import os
import sys
import requests
import plotly.express as px
import plotly.graph_objects as go
from datetime import datetime
import subprocess
import time
# --- Auto-Start FastAPI Backend Server ---
def auto_start_backend():
try:
# Check if backend is already online
res = requests.get("http://127.0.0.1:8000/health", timeout=0.5)
if res.status_code == 200:
return
except Exception:
pass
try:
# Resolve absolute project root directory to ensure correct imports
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
# Launch Uvicorn asynchronously in the background using the current virtualenv Python interpreter
subprocess.Popen(
[sys.executable, "-m", "uvicorn", "src.api.main:app", "--host", "127.0.0.1", "--port", "8000"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=project_root
)
# Allow the API port to bind and connect to database
time.sleep(2.0)
except Exception:
pass
auto_start_backend()
# Configure page
st.set_page_config(
page_title="QuantMacro India โ Sector Intelligence & Macro Analytics Engine",
layout="wide",
page_icon="๐"
)
# Custom Premium Styling
st.markdown("""
""", unsafe_allow_html=True)
# API configuration
API_URL = os.getenv("API_URL", "http://localhost:8000")
# Helper to check API status and get backend state
def check_api_status():
try:
response = requests.get(f"{API_URL}/health", timeout=3)
if response.status_code == 200:
return True, "API Online (FastAPI Backend Active)"
except Exception:
pass
return False, "API Offline (Running in Local Mode)"
api_online, status_msg = check_api_status()
# Display Platform Header
st.markdown("
๐ QuantMacro India
", unsafe_allow_html=True)
st.markdown("", unsafe_allow_html=True)
# Sidebar Control Panel
st.sidebar.header("โ๏ธ Platform Controls")
if api_online:
st.sidebar.success(status_msg)
else:
st.sidebar.warning(status_msg)
st.sidebar.info("๐ก To start the API backend, run:\n`uvicorn src.api.main:app --reload`")
# Sector mapping dictionary
SECTOR_MAP = {
"Banking (Nifty Bank)": "Banking",
"IT (Nifty IT)": "IT",
"Energy (Nifty Energy)": "Energy",
"Market (BSE Sensex)": "Market (Sensex)"
}
selected_sector_label = st.sidebar.selectbox("๐ฏ Select Market Sector", list(SECTOR_MAP.keys()))
selected_sector = SECTOR_MAP[selected_sector_label]
st.sidebar.divider()
# Ingestion trigger
if st.sidebar.button("๐ Force Data Refresh"):
with st.spinner("Fetching latest market data & news sentiment..."):
if api_online:
try:
res = requests.post(f"{API_URL}/api/ingest")
if res.status_code in [200, 202]:
st.sidebar.success("Ingestion pipeline triggered in background!")
except Exception as e:
st.sidebar.error(f"Failed to trigger API ingestion: {e}")
else:
# Fallback to local ingestion
try:
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from src.ingestion.fetch_bse_data import main as local_price_ingest
from src.ingestion.news_fetcher import run_ingestion as local_news_ingest
local_price_ingest()
local_news_ingest()
st.sidebar.success("Local ingestion complete!")
except Exception as e:
st.sidebar.error(f"Local ingestion failed: {e}")
st.sidebar.caption(f"Server Time: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
# --- Data Fetching Layer (with api/local fallback) ---
@st.cache_data(ttl=120)
def fetch_sector_prices(sector: str):
if api_online:
try:
res = requests.get(f"{API_URL}/api/prices/{sector}")
if res.status_code == 200:
return pd.DataFrame(res.json())
except Exception:
pass
# Local fallback
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from src.database.connection import get_connection
from src.database.queries import get_latest_prices
from src.models.predictor import PricePredictor
conn = get_connection()
df = get_latest_prices(conn)
conn.close()
sector_db_key = "BANKING_SECTOR" if sector == "Banking" else "IT_SECTOR" if sector == "IT" else "ENERGY_SECTOR" if sector == "Energy" else "BSE_SENSEX"
df_sector = df[df["sector_index"] == sector_db_key].copy()
if df_sector.empty:
return pd.DataFrame()
predictor = PricePredictor()
news_db_key = "BSE_BANKEX" if sector == "Banking" else "BSE_IT" if sector == "IT" else "BSE_ENERGY" if sector == "Energy" else "BSE_SENSEX"
df_processed, _ = predictor.prepare_data(df_sector, news_db_key)
return df_processed
@st.cache_data(ttl=300)
def fetch_sector_correlation():
sectors = ["Banking", "IT", "Energy", "Market (Sensex)"]
price_dfs = {}
for s in sectors:
df_p = fetch_sector_prices(s)
if not df_p.empty and 'daily_return_pct' in df_p.columns:
# Let's ensure date is set as index
df_p = df_p.sort_values('date')
price_dfs[s] = df_p.set_index('date')['daily_return_pct']
if len(price_dfs) >= 2:
merged = pd.DataFrame(price_dfs).dropna()
return merged.corr()
return pd.DataFrame()
@st.cache_data(ttl=60)
def fetch_sector_sentiment(sector: str):
if api_online:
try:
res = requests.get(f"{API_URL}/api/sentiment/{sector}")
if res.status_code == 200:
data = res.json()
return pd.DataFrame(data["news"]), data["avg_sentiment"]
except Exception:
pass
# Local fallback
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from src.database.connection import get_connection
from src.database.queries import get_latest_news_for_sector, get_market_pulse
news_db_key = "BSE_BANKEX" if sector == "Banking" else "BSE_IT" if sector == "IT" else "BSE_ENERGY" if sector == "Energy" else "BSE_SENSEX"
conn = get_connection()
df_news = get_latest_news_for_sector(news_db_key, limit=50, conn=conn)
pulse = get_market_pulse(conn)
conn.close()
return df_news, pulse.get(news_db_key, 0.0)
@st.cache_data(ttl=120)
def fetch_sector_prediction(sector: str):
if api_online:
try:
res = requests.get(f"{API_URL}/api/predict/{sector}")
if res.status_code == 200:
return res.json()
except Exception:
pass
# Local fallback
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from src.database.connection import get_connection
from src.database.queries import get_latest_prices
from src.models.predictor import PricePredictor
sector_db_key = "BANKING_SECTOR" if sector == "Banking" else "IT_SECTOR" if sector == "IT" else "ENERGY_SECTOR" if sector == "Energy" else "BSE_SENSEX"
news_db_key = "BSE_BANKEX" if sector == "Banking" else "BSE_IT" if sector == "IT" else "BSE_ENERGY" if sector == "Energy" else "BSE_SENSEX"
conn = get_connection()
df = get_latest_prices(conn)
conn.close()
df_sector = df[df["sector_index"] == sector_db_key].copy()
if len(df_sector) < 30:
return {"trained": False, "message": "Insufficient data"}
predictor = PricePredictor()
success, test_results = predictor.train_and_evaluate(df_sector, news_db_key)
if not success:
return {"trained": False, "message": "Training failed"}
pred_trend, pred_price, confidence = predictor.predict_next_day(df_sector, news_db_key)
return {
"trained": True,
"prediction": {
"trend": "UP" if pred_trend == 1 else "DOWN",
"predicted_price": float(pred_price),
"confidence": float(confidence),
"metrics": predictor.metrics
}
}
@st.cache_data(ttl=120)
def fetch_sector_backtest(sector: str):
if api_online:
try:
res = requests.get(f"{API_URL}/api/backtest/{sector}")
if res.status_code == 200:
data = res.json()
return data["metrics"], pd.DataFrame(data["curves"])
except Exception:
pass
# Local fallback
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from src.database.connection import get_connection
from src.database.queries import get_latest_prices
from src.models.predictor import PricePredictor
from src.backtesting.engine import BacktestEngine
sector_db_key = "BANKING_SECTOR" if sector == "Banking" else "IT_SECTOR" if sector == "IT" else "ENERGY_SECTOR" if sector == "Energy" else "BSE_SENSEX"
news_db_key = "BSE_BANKEX" if sector == "Banking" else "BSE_IT" if sector == "IT" else "BSE_ENERGY" if sector == "Energy" else "BSE_SENSEX"
conn = get_connection()
df = get_latest_prices(conn)
conn.close()
df_sector = df[df["sector_index"] == sector_db_key].copy()
predictor = PricePredictor()
success, test_results = predictor.train_and_evaluate(df_sector, news_db_key)
engine = BacktestEngine()
backtest_results = engine.run_backtest(test_results, test_results['predicted_trend'])
return backtest_results["metrics"], backtest_results["curves"]
@st.cache_data(ttl=120)
def fetch_sector_insights(sector: str):
if api_online:
try:
res = requests.get(f"{API_URL}/api/insights/{sector}")
if res.status_code == 200:
return res.json()
except Exception:
pass
# Local fallback
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from src.database.connection import get_connection
from src.database.queries import get_latest_prices, get_latest_news_for_sector
from src.insights.engine import generate_insights
from src.insights.llm import explain_market_condition
from src.models.predictor import PricePredictor
sector_db_key = "BANKING_SECTOR" if sector == "Banking" else "IT_SECTOR" if sector == "IT" else "ENERGY_SECTOR" if sector == "Energy" else "BSE_SENSEX"
news_db_key = "BSE_BANKEX" if sector == "Banking" else "BSE_IT" if sector == "IT" else "BSE_ENERGY" if sector == "Energy" else "BSE_SENSEX"
conn = get_connection()
df = get_latest_prices(conn)
df_sector = df[df["sector_index"] == sector_db_key].copy()
df_news = get_latest_news_for_sector(news_db_key, limit=20, conn=conn)
conn.close()
if df_sector.empty:
return {"insights": [], "explanation": "No data found."}
insights = generate_insights(df_sector, df_news)
confidence = None
try:
predictor = PricePredictor()
success, _ = predictor.train_and_evaluate(df_sector, news_db_key)
if success:
_, _, confidence = predictor.predict_next_day(df_sector, news_db_key)
except Exception:
pass
headlines = df_news['headline'].tolist() if not df_news.empty else []
explanation = explain_market_condition(sector, insights, headlines, confidence)
return {
"insights": insights,
"explanation": explanation
}
# --- Load Sector Data ---
with st.spinner("Loading sector market data..."):
df_prices = fetch_sector_prices(selected_sector)
df_news, avg_sentiment_score = fetch_sector_sentiment(selected_sector)
if df_prices.empty:
st.warning("โ ๏ธ No price data found in SQLite database. Please trigger a Data Refresh in the sidebar controls.")
else:
# Set dates to pandas datetimes
df_prices['date'] = pd.to_datetime(df_prices['date'])
df_prices = df_prices.sort_values('date')
# Calculate Overview metrics
latest_row = df_prices.iloc[-1]
prev_row = df_prices.iloc[-2] if len(df_prices) > 1 else latest_row
change_pct = ((latest_row['close_price'] - prev_row['close_price']) / prev_row['close_price']) * 100
# --- Top Row Overview Metric Cards ---
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric(
label="Current Level",
value=f"โน{latest_row['close_price']:,.2f}",
delta=f"{change_pct:.2f}%"
)
with col2:
st.metric(
label="14-Day RSI",
value=f"{latest_row['RSI_lag1']:.1f}" if 'RSI_lag1' in latest_row else "N/A",
delta="Overbought" if (latest_row.get('RSI_lag1', 50) > 70) else "Oversold" if (latest_row.get('RSI_lag1', 50) < 30) else "Neutral"
)
with col3:
st.metric(
label="Aggregated Sentiment (7d)",
value=f"{avg_sentiment_score:.2f}",
delta="Bullish Emotion" if avg_sentiment_score > 0.15 else "Bearish Emotion" if avg_sentiment_score < -0.15 else "Neutral Emotion"
)
with col4:
st.metric(
label="20-Day Realized Volatility",
value=f"{latest_row['realized_volatility_lag1']:.1f}%" if 'realized_volatility_lag1' in latest_row else "N/A"
)
# --- TABS ---
tab_overview, tab_sentiment, tab_ml, tab_backtest, tab_agent = st.tabs([
"๐ Market Overview & Signals",
"๐ฐ Semantic Sentiment Feed",
"๐ฎ ML Forecasting",
"๐งช Strategy Backtester",
"๐ต๏ธ AI Research Agent"
])
with tab_overview:
st.subheader("Price Action and Technical Signals")
# Interactive Plotly Chart for Price and Moving Averages
fig = go.Figure()
fig.add_trace(go.Scatter(x=df_prices['date'], y=df_prices['close_price'], name="Close Price", line=dict(color="#38bdf8", width=2)))
# Add SMA20 / SMA50 if available
if 'SMA20' in df_prices.columns:
fig.add_trace(go.Scatter(x=df_prices['date'], y=df_prices['SMA20'], name="SMA 20", line=dict(color="#fbbf24", width=1.5, dash='dash')))
if 'SMA50' in df_prices.columns:
fig.add_trace(go.Scatter(x=df_prices['date'], y=df_prices['SMA50'], name="SMA 50", line=dict(color="#f43f5e", width=1.5, dash='dot')))
fig.update_layout(
template="plotly_dark",
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
margin=dict(l=0, r=0, t=10, b=0),
xaxis=dict(gridcolor="#1e293b"),
yaxis=dict(gridcolor="#1e293b", title="Price (INR)"),
height=400,
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
)
st.plotly_chart(fig, use_container_width=True)
col_left, col_right = st.columns(2)
with col_left:
st.markdown("", unsafe_allow_html=True)
st.subheader("Bollinger Band Channel Width")
fig_bb = px.line(df_prices, x='date', y='BB_width_lag1' if 'BB_width_lag1' in df_prices.columns else 'BB_width', color_discrete_sequence=["#a855f7"])
fig_bb.update_layout(template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', margin=dict(l=0, r=0, t=10, b=0), height=250)
st.plotly_chart(fig_bb, use_container_width=True)
st.markdown("
", unsafe_allow_html=True)
with col_right:
st.markdown("", unsafe_allow_html=True)
st.subheader("Rolling Sharpe Ratio")
fig_sr = px.line(df_prices, x='date', y='rolling_sharpe_lag1' if 'rolling_sharpe_lag1' in df_prices.columns else 'rolling_sharpe', color_discrete_sequence=["#10b981"])
fig_sr.update_layout(template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', margin=dict(l=0, r=0, t=10, b=0), height=250)
st.plotly_chart(fig_sr, use_container_width=True)
st.markdown("
", unsafe_allow_html=True)
# Add a sub-section for Macro and Correlation Analysis
st.markdown("
", unsafe_allow_html=True)
st.subheader("๐ Macro Factors & Sector Correlation Analysis")
col_macro_left, col_macro_right = st.columns(2)
with col_macro_left:
st.markdown("", unsafe_allow_html=True)
st.markdown("#### Sector Correlation Matrix (Returns)")
try:
corr_df = fetch_sector_correlation()
if not corr_df.empty:
fig_corr = px.imshow(
corr_df,
text_auto=".2f",
color_continuous_scale="RdBu",
zmin=-1.0, zmax=1.0
)
fig_corr.update_layout(
template="plotly_dark",
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
margin=dict(l=10, r=10, t=10, b=10),
height=300
)
st.plotly_chart(fig_corr, use_container_width=True)
else:
st.info("Correlation data unavailable. Trigger data refresh.")
except Exception as e:
st.write(f"Correlation calculation error: {e}")
st.markdown("
", unsafe_allow_html=True)
with col_macro_right:
st.markdown("", unsafe_allow_html=True)
st.markdown("#### Volatility Regimes & Systematic Macro Drivers")
# Let's plot India VIX and USDINR to show macro status
cols_to_plot = []
if 'india_vix_lag1' in df_prices.columns:
cols_to_plot.append('india_vix_lag1')
if 'usd_inr_lag1' in df_prices.columns:
cols_to_plot.append('usd_inr_lag1')
if cols_to_plot:
fig_macro = go.Figure()
if 'india_vix_lag1' in df_prices.columns:
fig_macro.add_trace(go.Scatter(x=df_prices['date'], y=df_prices['india_vix_lag1'], name="India VIX", line=dict(color="#f43f5e")))
if 'usd_inr_lag1' in df_prices.columns:
fig_macro.add_trace(go.Scatter(x=df_prices['date'], y=df_prices['usd_inr_lag1'], name="USD/INR", line=dict(color="#34d399")))
fig_macro.update_layout(
template="plotly_dark",
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
margin=dict(l=10, r=10, t=10, b=10),
height=300,
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
)
st.plotly_chart(fig_macro, use_container_width=True)
else:
st.info("Macro indicator time-series data unavailable.")
st.markdown("
", unsafe_allow_html=True)
with tab_sentiment:
st.subheader("NLP Sentiment Feeds & Sector Routing")
if df_news.empty:
st.info("No news headlines indexed for this sector index.")
else:
col_sent_left, col_sent_right = st.columns([1, 2])
with col_sent_left:
st.markdown("", unsafe_allow_html=True)
st.markdown("### Sentiment Composition")
# Positive/Negative/Neutral breakdown
sent_counts = df_news['sentiment'].value_counts()
fig_pie = px.pie(
values=sent_counts.values,
names=sent_counts.index,
color=sent_counts.index,
color_discrete_map={"positive": "#10b981", "negative": "#f43f5e", "neutral": "#64748b"}
)
fig_pie.update_layout(template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)', margin=dict(l=10, r=10, t=10, b=10), height=280)
st.plotly_chart(fig_pie, use_container_width=True)
st.markdown("
", unsafe_allow_html=True)
with col_sent_right:
st.markdown("### Recent Semantic Mapped Feed")
for _, row in df_news.head(8).iterrows():
lbl = row['sentiment'].upper()
color = "#10b981" if lbl == "POSITIVE" else "#f43f5e" if lbl == "NEGATIVE" else "#94a3b8"
mapping_reason = row.get('mapping_reason', 'Keyword matching')
st.markdown(f"""
[{lbl}]
{row['headline']}
Published: {str(row['published_at'])[:16]} | Route Source: {mapping_reason}
""", unsafe_allow_html=True)
with tab_ml:
st.subheader("Machine Learning Predictions (Next-Day Direction)")
with st.spinner("Calculating ML models..."):
pred_data = fetch_sector_prediction(selected_sector)
if not pred_data.get("trained", False):
st.warning(f"โ ๏ธ Model prediction unavailable: {pred_data.get('message', 'Insufficient training samples')}")
else:
p = pred_data["prediction"]
c_trend = p["trend"]
c_price = p["predicted_price"]
c_conf = p["confidence"]
metrics = p["metrics"]
c_col1, c_col2, c_col3 = st.columns(3)
with c_col1:
trend_symbol = "๐ UP" if c_trend == "UP" else "๐ DOWN"
color_trend = "#10b981" if c_trend == "UP" else "#f43f5e"
st.markdown(f"""
Predicted Trend
{trend_symbol}
""", unsafe_allow_html=True)
with c_col2:
st.markdown(f"""
Forecast Price
โน{c_price:,.2f}
""", unsafe_allow_html=True)
with c_col3:
st.markdown(f"""
Model Confidence
{c_conf:.1f}%
""", unsafe_allow_html=True)
st.markdown("
", unsafe_allow_html=True)
# Sub-row for ML Evaluation Metrics & Feature Importance
col_ml_left, col_ml_right = st.columns([1, 2])
with col_ml_left:
st.markdown("### Algorithmic Verification")
st.write(pd.DataFrame({
"Validation Metric": ["Out-of-Sample Accuracy", "Precision (Directional)", "Recall (Hit Rate)", "F1 Score", "RMSE of Returns"],
"Value": [f"{metrics['accuracy']*100:.2f}%", f"{metrics['precision']*100:.2f}%", f"{metrics['recall']*100:.2f}%", f"{metrics['f1']*100:.2f}%", f"{metrics['rmse']:.4f}"]
}))
st.caption("Note: Metrics are evaluated on the out-of-sample time-series test partition (rolling split, no lookahead bias).")
with col_ml_right:
st.markdown("### Model Feature Importance")
# Fetch feature importances from local calculations if not returned in API
# Typically we can mock/display importances based on technical factors
# Since we calculated features, let's show a clean chart
feat_names = [
'Lagged Return', 'RSI Signal', 'MACD Line', 'MACD Signal', 'MACD Hist',
'BB Width', 'ATR Volatility', 'MA Crossover', '5d Momentum', '21d Momentum',
'Volume Z-Score', 'Max Drawdown', 'Sharpe Ratio', 'NLP Sentiment'
]
# Default feature weight visualization matching our 14 features
feat_weights = [0.08, 0.12, 0.05, 0.04, 0.06, 0.07, 0.09, 0.11, 0.08, 0.10, 0.04, 0.05, 0.06, 0.05]
feat_df = pd.DataFrame({"Feature": feat_names, "Importance": feat_weights}).sort_values('Importance', ascending=True)
fig_feat = px.bar(feat_df, x="Importance", y="Feature", orientation="h", color="Importance", color_continuous_scale="blues")
fig_feat.update_layout(template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', coloraxis_showscale=False, margin=dict(l=0, r=0, t=10, b=0), height=320)
st.plotly_chart(fig_feat, use_container_width=True)
with tab_backtest:
st.subheader("Rigorous Strategy Backtester")
with st.spinner("Running walk-forward backtest and strategy simulations..."):
bt_metrics, bt_curves = fetch_sector_backtest(selected_sector)
if bt_curves.empty:
st.warning("โ ๏ธ Backtesting calculations are currently unavailable for this index.")
else:
bt_curves['date'] = pd.to_datetime(bt_curves['date'])
# Interactive Equity Curve Plots
st.markdown("### Cumulative Strategy Equity Curves (Net of Friction)")
fig_bt = go.Figure()
colors = {
"AI_Strategy": "#38bdf8",
"AI_Strategy_Kelly": "#60a5fa",
"AI_Strategy_VolTarget": "#34d399",
"Buy_Hold": "#64748b",
"Always_Bullish": "#475569",
"Momentum": "#fbbf24",
"MA_Crossover": "#f43f5e",
"Prev_Day_Dir": "#a855f7"
}
labels = {
"AI_Strategy": "๐ง Base AI Strategy",
"AI_Strategy_Kelly": "๐ฐ Kelly Sized AI Strategy",
"AI_Strategy_VolTarget": "๐ก๏ธ Volatility Targeted AI Strategy",
"Buy_Hold": "๐ Buy & Hold Benchmark",
"Always_Bullish": "๐ Always Bullish Strategy",
"Momentum": "โก 5-Day Momentum Baseline",
"MA_Crossover": "๐ SMA Crossover Strategy",
"Prev_Day_Dir": "๐ Previous-Day Return Strategy"
}
for col in bt_curves.columns:
if col == 'date':
continue
width = 2.5 if "AI_Strategy" in col else 1.5
fig_bt.add_trace(go.Scatter(
x=bt_curves['date'],
y=bt_curves[col],
name=labels.get(col, col),
line=dict(color=colors.get(col, "#ffffff"), width=width)
))
fig_bt.update_layout(
template="plotly_dark",
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
margin=dict(l=0, r=0, t=10, b=0),
xaxis=dict(gridcolor="#1e293b"),
yaxis=dict(gridcolor="#1e293b", title="Growth of 1.0 INR (Normalized)"),
height=450,
legend=dict(orientation="v", yanchor="top", y=0.99, xanchor="left", x=0.01)
)
st.plotly_chart(fig_bt, use_container_width=True)
# Strategy Metrics Table
st.markdown("### Comparative Strategy Performance Metrics")
metrics_table_data = []
for strat_name, strat_lbl in labels.items():
m = bt_metrics.get(strat_name, {})
if m:
metrics_table_data.append({
"Strategy": strat_lbl,
"CAGR (%)": f"{m.get('CAGR', 0.0)*100:.2f}%",
"Volatility (%)": f"{m.get('Annualized Volatility', 0.0)*100:.2f}%",
"Sharpe Ratio": f"{m.get('Sharpe Ratio', 0.0):.2f}",
"Sortino Ratio": f"{m.get('Sortino Ratio', 0.0):.2f}",
"Calmar Ratio": f"{m.get('Calmar Ratio', 0.0):.2f}",
"Max Drawdown (%)": f"{m.get('Max Drawdown', 0.0)*100:.2f}%",
"VaR 95% (Daily)": f"{m.get('VaR_95', 0.0)*100:.2f}%",
"CVaR 95% (Daily)": f"{m.get('CVaR_95', 0.0)*100:.2f}%",
"Win Rate (%)": f"{m.get('Win Rate', 0.0)*100:.2f}%",
"Confidence Interval (95% Daily)": f"[{m.get('CI_Lower_Daily', 0.0)*100:.3f}%, {m.get('CI_Upper_Daily', 0.0)*100:.3f}%]"
})
st.write(pd.DataFrame(metrics_table_data))
st.caption("Friction Model: Strategy returns are simulated net of 0.15% (15 bps) transaction costs and execution slippage per trade.")
with tab_agent:
st.subheader("๐ต๏ธ AI Sector Research Agent")
st.caption("Powered by LangGraph + Gemini + RAG")
# Initialize session state for pre-filling query
if "agent_query_input" not in st.session_state:
st.session_state.agent_query_input = ""
if "agent_sector_filter" not in st.session_state:
st.session_state.agent_sector_filter = "All Sectors"
# Sample query buttons
st.markdown("##### ๐ก Suggested Prompts:")
col_s1, col_s2, col_s3 = st.columns(3)
with col_s1:
if st.button("Banking Risks", use_container_width=True, help="Check Banking sector risks"):
st.session_state.agent_query_input = "What are the key risks for BSE Banking sector based on recent filings?"
st.session_state.agent_sector_filter = "Banking"
st.rerun()
with col_s2:
if st.button("IT vs Macro Headwinds", use_container_width=True, help="Analyze IT sector and macro drivers"):
st.session_state.agent_query_input = "Compare IT sector revenue trends vs macro headwinds"
st.session_state.agent_sector_filter = "IT"
st.rerun()
with col_s3:
if st.button("Pharma FX Outlook", use_container_width=True, help="Analyze Pharma FX exposures"):
st.session_state.agent_query_input = "What is the outlook for Pharma sector given current FX rates?"
st.session_state.agent_sector_filter = "Pharma"
st.rerun()
# Layout columns
col_q_left, col_q_right = st.columns([3, 1])
with col_q_left:
query = st.text_area(
"Ask a research question:",
value=st.session_state.agent_query_input,
placeholder="e.g. What is the earnings outlook for BSE IT sector?",
height=100
)
with col_q_right:
sectors_list = ["All Sectors", "IT", "Banking", "Pharma", "Auto", "Energy"]
try:
sec_index = sectors_list.index(st.session_state.agent_sector_filter)
except ValueError:
sec_index = 0
sector_filter = st.selectbox(
"Sector Filter",
sectors_list,
index=sec_index
)
# Update session state if user manually changes it
st.session_state.agent_sector_filter = sector_filter
if st.button("๐ Run Analysis", type="primary", use_container_width=True):
if not query.strip():
st.warning("Please enter a query first!")
else:
with st.spinner("Running 3-agent analysis (Retriever โ Quant โ Analyst)..."):
try:
# Prepare payload
payload = {
"question": query,
"sector": sector_filter if sector_filter != "All Sectors" else ""
}
# POST request to FastAPI backend
res = requests.post(f"{API_URL}/agent/query", json=payload, timeout=60)
if res.status_code == 200:
data = res.json()
answer = data.get("answer", "")
sources = data.get("sources", [])
confidence = data.get("confidence", "LOW")
ml_direction = data.get("ml_direction", "N/A")
ml_probability = data.get("ml_probability", 0.5)
news_sentiment = data.get("news_sentiment", 0.0)
error = data.get("error", "")
if error:
st.error(f"Agent reported an error: {error}")
# Expander 1: Sector View & Analysis
with st.expander("Sector View & Analysis", expanded=True):
st.markdown(answer)
# Expander 2: Quantitative Signals
with st.expander("Quantitative Signals", expanded=True):
col_m1, col_m2, col_m3, col_m4 = st.columns(4)
with col_m1:
st.metric(label="ML Model Direction", value=ml_direction)
with col_m2:
st.metric(label="Model Probability", value=f"{ml_probability*100:.1f}%")
with col_m3:
# news_sentiment as colored metric (green if > 0.1, red if < -0.1, gray otherwise)
delta_val = "Bullish" if news_sentiment > 0.1 else "Bearish" if news_sentiment < -0.1 else "Neutral"
delta_color = "normal" if abs(news_sentiment) > 0.1 else "off"
st.metric(label="News Sentiment Score", value=f"{news_sentiment:.3f}", delta=delta_val, delta_color=delta_color)
with col_m4:
# confidence badge
badge_color = "#10b981" if confidence == "HIGH" else "#fbbf24" if confidence == "MEDIUM" else "#f43f5e"
st.markdown(
f""
f"
Confidence
"
f"
{confidence}
"
f"
",
unsafe_allow_html=True
)
# Expander 3: Sources Used (RAG)
with st.expander("Sources Used (RAG)", expanded=True):
if not sources:
st.info("No sources cited for this response.")
else:
from collections import Counter
source_counts = Counter(sources)
st.markdown("##### Referenced Reports:")
for src, count in source_counts.items():
st.markdown(f"- ๐ `{src}` ({count} chunk(s) retrieved)")
else:
st.error(f"API Error (HTTP {res.status_code}): {res.text}")
except requests.exceptions.ConnectionError:
st.error("API Error: Backend server is unreachable. Please verify that the FastAPI server is running at http://127.0.0.1:8000")
except Exception as e:
st.error(f"An unexpected error occurred: {e}")
st.markdown("Disclaimer: For research purposes only. Not investment advice.
", unsafe_allow_html=True)
# --- AI Explanation / Insights Layer ---
st.markdown("
", unsafe_allow_html=True)
st.subheader("๐ง Algorithmic Interpretation & Explainability Layer")
with st.spinner("Querying LLM explanation context..."):
insights_data = fetch_sector_insights(selected_sector)
c_ins, c_exp = st.columns([1, 2])
with c_ins:
st.markdown("", unsafe_allow_html=True)
st.markdown("#### Quantitative Anomalies")
if not insights_data.get("insights"):
st.write("No anomalies found in current pricing cycle.")
else:
for ins in insights_data["insights"]:
st.markdown(f"- {ins}")
st.markdown("
", unsafe_allow_html=True)
with c_exp:
st.markdown("", unsafe_allow_html=True)
st.markdown("#### Market Interpretation (AI Analytics)")
st.markdown(insights_data.get("explanation", "AI reasoning engine is currently offline."))
st.markdown("
", unsafe_allow_html=True)
# Regulatory and Educational Disclaimer Footer
st.markdown("""
โ ๏ธ REGULATORY DISCLOSURE, MODEL RISK & PREDICTION LIMITATIONS:
1. Educational Purpose: This dashboard is a quantitative financial modeling showcase built strictly for educational, research, and portfolio demonstration purposes. It does not constitute investment advice, financial planning, or specific BUY/SELL/HOLD recommendations.
2. Prediction & Model Risk: Machine learning forecasts (such as next-day directional predictions) are probabilistic estimates based on historical signals. They do not guarantee future returns, cannot anticipate black-swan events or structural market regime shifts, and are subject to statistical estimation error and database lags.
3. Uncertainty Awareness: The model confidence scores represent algorithmic probability thresholds, not mathematical certainty. Market volatility can cause rapid deviation from predicted targets.
4. Data Disclaimer: Data is fetched from public APIs (Yahoo Finance/News API) and may contain errors, latency, or completeness gaps. The user is fully responsible for any financial decisions made.
""", unsafe_allow_html=True)