""" QuantScale AI - Streamlit Frontend (Main App) Directly imports QuantScaleSystem - no HTTP dependency needed. """ import re import pandas as pd import streamlit as st from core.schema import OptimizationRequest # --- Page Config --- st.set_page_config( page_title="QuantScale AI", page_icon="📈", layout="wide", initial_sidebar_state="collapsed" ) st.markdown(""" """, unsafe_allow_html=True) # --- Parsers --- def parse_investment_amount(text: str) -> float: text = text.replace(",", "") match = re.search(r'\$?([\d.]+)\s*([kKmM]?)', text) if match: amount = float(match.group(1)) suffix = match.group(2).lower() if suffix == 'k': amount *= 1_000 elif suffix == 'm': amount *= 1_000_000 return amount return 100_000.0 def parse_strategy(text: str): lower = text.lower() strategy, top_n = None, None if "smallest" in lower: strategy = "smallest_market_cap" elif "largest" in lower: strategy = "largest_market_cap" if strategy: match = re.search(r'(\d+)\s*(?:smallest|largest|companies|stocks)', lower) top_n = int(match.group(1)) if match else 50 return strategy, top_n def build_portfolio_df(allocations: dict, investment: float) -> pd.DataFrame: rows = [] for ticker, weight in sorted(allocations.items(), key=lambda x: x[1], reverse=True): rows.append({ "Ticker": ticker, "Allocation (%)": f"{weight * 100:.2f}%", "Investment ($)": f"${weight * investment:,.2f}" }) return pd.DataFrame(rows) # --- Lazy-load system to avoid import overhead on every rerender --- @st.cache_resource(show_spinner="Loading QuantScale Engine...") def get_system(): from main import QuantScaleSystem return QuantScaleSystem() import plotly.graph_objects as go import plotly.express as px # --- UI --- st.markdown('
Market Context & Portfolio EDA
', unsafe_allow_html=True) eda_col1, eda_col2 = st.columns([2, 1]) with eda_col1: # 1. Performance Comparison (Trailing 30 Days) last_30_returns = market_data.iloc[-21:] # Portfolio vs Benchmark Cumulative Returns bench_daily = (last_30_returns * benchmark_weights).sum(axis=1) port_daily = (last_30_returns * pd.Series(opt.weights)).sum(axis=1) cum_bench = (1 + bench_daily).cumprod() * 100 cum_port = (1 + port_daily).cumprod() * 100 fig_perf = go.Figure() fig_perf.add_trace(go.Scatter(x=cum_bench.index, y=cum_bench, name="Benchmark (S&P 500 Proxy)", line=dict(color="#94a3b8", width=2, dash='dot'))) fig_perf.add_trace(go.Scatter(x=cum_port.index, y=cum_port, name="Optimized Portfolio", line=dict(color="#60a5fa", width=3))) fig_perf.update_layout( title="Growth of $100 (Trailing 30 Days)", template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', margin=dict(l=20, r=20, t=40, b=20), legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) ) st.plotly_chart(fig_perf, use_container_width=True) with eda_col2: # 2. Sector Weight Comparison (Bar) # Aggregating sector weights port_sector_weights = {} bench_sector_weights = {} for ticker, weight in opt.weights.items(): s = sector_map.get(ticker, "Unknown") port_sector_weights[s] = port_sector_weights.get(s, 0) + weight for ticker, weight in benchmark_weights.items(): s = sector_map.get(ticker, "Unknown") bench_sector_weights[s] = bench_sector_weights.get(s, 0) + weight all_sectors = sorted(list(set(list(port_sector_weights.keys()) + list(bench_sector_weights.keys())))) # 3. Portfolio Composition Pie Chart (Holding Level) # Using top 15 holdings for better readability in the pie sorted_weights = sorted(opt.weights.items(), key=lambda x: x[1], reverse=True) top_holdings = sorted_weights[:15] other_weight = sum(w for t, w in sorted_weights[15:]) labels = [t for t, w in top_holdings] values = [w for t, w in top_holdings] if other_weight > 0: labels.append("Others") values.append(other_weight) fig_pie = go.Figure(data=[go.Pie( labels=labels, values=values, hole=.4, textinfo='label', marker=dict(colors=px.colors.qualitative.Prism) )]) fig_pie.update_layout( title="Top Holdings Allocation", template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', margin=dict(l=10, r=10, t=40, b=10), showlegend=False ) st.plotly_chart(fig_pie, use_container_width=True) # Bar chart for sector comparison fig_sector = go.Figure(data=[ go.Bar(name='Bench', x=all_sectors, y=[bench_sector_weights.get(s, 0)*100 for s in all_sectors], marker_color="#94a3b8"), go.Bar(name='Port', x=all_sectors, y=[port_sector_weights.get(s, 0)*100 for s in all_sectors], marker_color="#34d399") ]) fig_sector.update_layout( title="Sector Exposure Match (%)", template="plotly_dark", barmode='group', height=250, paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', margin=dict(l=10, r=10, t=30, b=10), showlegend=False ) st.plotly_chart(fig_sector, use_container_width=True) st.divider() # --- AI Commentary --- st.markdown('AI Performance Attribution
', unsafe_allow_html=True) st.markdown(f'Full Portfolio Allocation (100%) — {total} Holdings
', unsafe_allow_html=True ) c1, c2, c3 = st.columns(3) c1.metric("Total Holdings", total) c2.metric("Largest Position", df["Ticker"].iloc[0]) c3.metric("Smallest Position", df["Ticker"].iloc[-1]) st.dataframe( df, use_container_width=True, hide_index=True, height=min(500, 36 * total + 40), column_config={ "Ticker": st.column_config.TextColumn("Ticker", width="small"), "Allocation (%)": st.column_config.TextColumn("Allocation (%)", width="small"), "Investment ($)": st.column_config.TextColumn( f"Investment (of ${investment_amount:,.0f})", width="medium" ), } ) # Metadata: Update trigger for build system