|
|
|
|
|
|
|
|
import gradio as gr |
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
import plotly.graph_objects as go |
|
|
import plotly.express as px |
|
|
from datetime import datetime, timedelta |
|
|
import os |
|
|
import sys |
|
|
import json |
|
|
import torch |
|
|
from fetch_market_data import fetch_market_data, ASSETS, FRED_IDS |
|
|
from llm_analysis_rag import analyze_agent_decision, analyze_historical_segment |
|
|
from stable_baselines3 import SAC |
|
|
from environment import PortfolioEnv |
|
|
from evaluate_baselines import buy_and_hold, equally_weighted_rebalanced |
|
|
|
|
|
|
|
|
MODEL_PATH = os.path.join("checkpoints", "sac_portfolio_model.zip") |
|
|
WINDOW_SIZE = 30 |
|
|
MACRO_COLS = list(FRED_IDS.values()) |
|
|
DASHBOARD_DATA_PATH = os.path.join("data", "historical_dashboard_data.csv") |
|
|
|
|
|
|
|
|
TRAIN_START_DATE = "2015-01-01" |
|
|
TRAIN_END_DATE = "2020-12-31" |
|
|
|
|
|
|
|
|
DASHBOARD_DATA_DF = None |
|
|
|
|
|
|
|
|
TIME_PERIODS = { |
|
|
"6 Months": 180, |
|
|
"1 Year": 365, |
|
|
"2 Years": 730, |
|
|
"5 Years": 1825, |
|
|
"Max Available": 9999 |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def initialize_dashboard_data(): |
|
|
"""Fetches and loads historical data at startup for Tabs 3 & 4.""" |
|
|
global DASHBOARD_DATA_DF |
|
|
print("--- Initializing Historical Data for Analyst/Simulation Tabs ---") |
|
|
|
|
|
|
|
|
end_date = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') |
|
|
start_date = (datetime.now() - timedelta(days=365*6)).strftime('%Y-%m-%d') |
|
|
|
|
|
print(f"Fetching historical data from {start_date} to {end_date}...") |
|
|
|
|
|
fetch_market_data(start_date, end_date, DASHBOARD_DATA_PATH) |
|
|
|
|
|
if os.path.exists(DASHBOARD_DATA_PATH): |
|
|
DASHBOARD_DATA_DF = pd.read_csv(DASHBOARD_DATA_PATH, index_col=0, parse_dates=True) |
|
|
|
|
|
DASHBOARD_DATA_DF.dropna(how='all', inplace=True) |
|
|
|
|
|
asset_cols = [c for c in ASSETS if c in DASHBOARD_DATA_DF.columns] |
|
|
if asset_cols: |
|
|
DASHBOARD_DATA_DF['Daily_Ret_Eq'] = DASHBOARD_DATA_DF[asset_cols].pct_change().mean(axis=1) |
|
|
print(f"Data loaded successfully. Shape: {DASHBOARD_DATA_DF.shape}") |
|
|
print(f"Data range: {DASHBOARD_DATA_DF.index.min().date()} to {DASHBOARD_DATA_DF.index.max().date()}") |
|
|
else: |
|
|
print("โ Failed to initialize historical data.") |
|
|
|
|
|
|
|
|
try: |
|
|
initialize_dashboard_data() |
|
|
except Exception as e: |
|
|
print(f"Warning: Data initialization failed. Error: {e}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def evaluate_agent_pro(env, model): |
|
|
""" |
|
|
Runs the trained agent on the environment and returns portfolio values. |
|
|
""" |
|
|
obs, info = env.reset() |
|
|
terminated, truncated = False, False |
|
|
portfolio_values = [env.initial_balance] |
|
|
|
|
|
while not (terminated or truncated): |
|
|
action, _states = model.predict(obs, deterministic=True) |
|
|
obs, reward, terminated, truncated, info = env.step(action) |
|
|
portfolio_values.append(info['portfolio_value']) |
|
|
|
|
|
|
|
|
valid_dates = env.df.index[env.window_size-1:] |
|
|
return pd.Series(portfolio_values, index=valid_dates[:len(portfolio_values)]) |
|
|
|
|
|
def calculate_metrics_pro(portfolio_values, freq=252, rf=0.0): |
|
|
""" |
|
|
Calculates key professional performance metrics from a series of portfolio values. |
|
|
""" |
|
|
if len(portfolio_values) < 2: |
|
|
return {k: "N/A" for k in ["Total Return", "CAGR", "Sharpe Ratio", "Sortino Ratio", "Volatility", "Max Drawdown", "Calmar Ratio"]} |
|
|
|
|
|
returns = portfolio_values.pct_change().dropna() |
|
|
if returns.empty: |
|
|
return {k: "0.00%" if "%" in k else "0.00" for k in ["Total Return", "CAGR", "Sharpe Ratio", "Sortino Ratio", "Volatility", "Max Drawdown", "Calmar Ratio"]} |
|
|
|
|
|
total_return = (portfolio_values.iloc[-1] / portfolio_values.iloc[0]) - 1 |
|
|
num_years = (len(portfolio_values) - 1) / freq |
|
|
cagr = (portfolio_values.iloc[-1] / portfolio_values.iloc[0]) ** (1/num_years) - 1 if num_years > 0 else 0.0 |
|
|
|
|
|
sharpe_ratio = np.sqrt(freq) * (returns.mean() - rf) / returns.std() if returns.std() > 0 else np.nan |
|
|
|
|
|
downside_returns = returns[returns < 0] |
|
|
downside_std = downside_returns.std() |
|
|
sortino_ratio = np.sqrt(freq) * (returns.mean() - rf) / downside_std if downside_std > 0 else np.nan |
|
|
|
|
|
volatility = returns.std() * np.sqrt(freq) |
|
|
|
|
|
rolling_max = portfolio_values.cummax() |
|
|
drawdown = portfolio_values / rolling_max - 1.0 |
|
|
max_drawdown = drawdown.min() |
|
|
|
|
|
calmar_ratio = cagr / abs(max_drawdown) if max_drawdown != 0 and cagr != 0 else np.nan |
|
|
|
|
|
return { |
|
|
"Total Return": total_return, |
|
|
"CAGR": cagr, |
|
|
"Sharpe Ratio": sharpe_ratio, |
|
|
"Sortino Ratio": sortino_ratio, |
|
|
"Volatility": volatility, |
|
|
"Max Drawdown": max_drawdown, |
|
|
"Calmar Ratio": calmar_ratio |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_feature_importance(model, obs): |
|
|
obs_tensor = torch.as_tensor(obs, dtype=torch.float32, device=model.device) |
|
|
if obs_tensor.dim() == 1: obs_tensor = obs_tensor.unsqueeze(0) |
|
|
obs_tensor.requires_grad_() |
|
|
|
|
|
actor = model.policy.actor |
|
|
baseline = torch.zeros_like(obs_tensor) |
|
|
steps = 50 |
|
|
scaled_inputs = [baseline + (float(i) / steps) * (obs_tensor - baseline) for i in range(steps + 1)] |
|
|
|
|
|
grads = [] |
|
|
for scaled_input in scaled_inputs: |
|
|
action_mean = actor(scaled_input) |
|
|
target_output = action_mean.sum() |
|
|
grad = torch.autograd.grad(outputs=target_output, inputs=scaled_input)[0] |
|
|
grads.append(grad) |
|
|
|
|
|
|
|
|
stacked_grads = torch.stack(grads) |
|
|
avg_grads = (stacked_grads[:-1] + stacked_grads[1:]) / 2.0 |
|
|
avg_grads = avg_grads.mean(dim=0) |
|
|
|
|
|
|
|
|
integrated_grads = (obs_tensor - baseline) * avg_grads |
|
|
importance_scores = integrated_grads.detach().cpu().numpy().flatten() |
|
|
|
|
|
feature_names = [] |
|
|
for i in range(WINDOW_SIZE): |
|
|
for asset in ASSETS: feature_names.append(f"{asset}_t-{WINDOW_SIZE-1-i}") |
|
|
for i in range(WINDOW_SIZE): |
|
|
for macro in MACRO_COLS: feature_names.append(f"{macro}_t-{WINDOW_SIZE-1-i}") |
|
|
|
|
|
feature_importance_dict = dict(zip(feature_names, importance_scores)) |
|
|
aggregated_importance = {} |
|
|
for base_feature in ASSETS + MACRO_COLS: |
|
|
total_imp = sum(abs(val) for key, val in feature_importance_dict.items() if key.startswith(base_feature)) |
|
|
aggregated_importance[base_feature] = total_imp |
|
|
|
|
|
top_features = dict(sorted(aggregated_importance.items(), key=lambda item: item[1], reverse=True)[:8]) |
|
|
|
|
|
fig = px.bar(x=list(top_features.values()), y=list(top_features.keys()), orientation='h', |
|
|
title="Top Influential Features (XAI)", labels={'x': 'Importance', 'y': 'Feature'}, |
|
|
color=list(top_features.values()), color_continuous_scale=px.colors.sequential.Viridis) |
|
|
fig.update_layout(template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', |
|
|
yaxis={'categoryorder':'total ascending'}, coloraxis_showscale=False, margin=dict(l=10, r=10, t=40, b=10), height=300, |
|
|
hoverlabel=dict(bgcolor="white", font_size=14, font_family="Roboto", font_color="black")) |
|
|
return fig |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_historical_simulation(start_date_str, end_date_str): |
|
|
""" |
|
|
Runs the RL agent on historical data and compares to baselines using professional metrics. |
|
|
""" |
|
|
if DASHBOARD_DATA_DF is None: |
|
|
return go.Figure(), "Data not initialized. Please restart app.", gr.update(visible=False) |
|
|
|
|
|
status_msg = "Preparing simulation..." |
|
|
yield go.Figure(), status_msg, gr.update(visible=False) |
|
|
|
|
|
try: |
|
|
|
|
|
try: |
|
|
start_date = pd.to_datetime(start_date_str) |
|
|
end_date = pd.to_datetime(end_date_str) |
|
|
except ValueError: |
|
|
yield go.Figure(), "Error: Invalid date format. Use YYYY-MM-DD.", gr.update(visible=False) |
|
|
return |
|
|
|
|
|
if start_date < DASHBOARD_DATA_DF.index.min() or end_date > DASHBOARD_DATA_DF.index.max(): |
|
|
avail_start = DASHBOARD_DATA_DF.index.min().date() |
|
|
avail_end = DASHBOARD_DATA_DF.index.max().date() |
|
|
yield go.Figure(), f"Error: Selected dates outside available range ({avail_start} to {avail_end}).", gr.update(visible=False) |
|
|
return |
|
|
|
|
|
df_slice = DASHBOARD_DATA_DF.loc[start_date:end_date].copy() |
|
|
asset_cols_only = [c for c in ASSETS if c in df_slice.columns] |
|
|
|
|
|
if len(df_slice) < WINDOW_SIZE + 10: |
|
|
yield go.Figure(), "Error: Time period too short for simulation.", gr.update(visible=False) |
|
|
return |
|
|
|
|
|
|
|
|
status_msg = "Running RL Agent simulation..." |
|
|
yield go.Figure(), status_msg, gr.update(visible=False) |
|
|
|
|
|
env = PortfolioEnv(df_slice, WINDOW_SIZE, initial_balance=10000) |
|
|
|
|
|
if not os.path.exists(MODEL_PATH): |
|
|
raise FileNotFoundError(f"Model not found: {MODEL_PATH}") |
|
|
model = SAC.load(MODEL_PATH) |
|
|
|
|
|
|
|
|
rl_portfolio_series = evaluate_agent_pro(env, model) |
|
|
|
|
|
|
|
|
status_msg = "Calculating baselines and metrics..." |
|
|
yield go.Figure(), status_msg, gr.update(visible=False) |
|
|
|
|
|
|
|
|
bnh_portfolio_series = buy_and_hold(df_slice[asset_cols_only], initial_balance=10000) |
|
|
|
|
|
bnh_portfolio_series = bnh_portfolio_series.loc[rl_portfolio_series.index[0]:] |
|
|
|
|
|
bnh_portfolio_series = bnh_portfolio_series / bnh_portfolio_series.iloc[0] * 10000 |
|
|
|
|
|
eq_portfolio_series = equally_weighted_rebalanced(df_slice[asset_cols_only], initial_balance=10000) |
|
|
eq_portfolio_series = eq_portfolio_series.loc[rl_portfolio_series.index[0]:] |
|
|
eq_portfolio_series = eq_portfolio_series / eq_portfolio_series.iloc[0] * 10000 |
|
|
|
|
|
|
|
|
fig = go.Figure() |
|
|
fig.add_trace(go.Scatter(x=rl_portfolio_series.index, y=rl_portfolio_series, mode='lines', name='RL Agent (SAC)', line=dict(color='#10b981', width=3))) |
|
|
fig.add_trace(go.Scatter(x=bnh_portfolio_series.index, y=bnh_portfolio_series, mode='lines', name='Buy & Hold (SPY)', line=dict(color='#6b7280', dash='dash'))) |
|
|
fig.add_trace(go.Scatter(x=eq_portfolio_series.index, y=eq_portfolio_series, mode='lines', name='Equal Weighted', line=dict(color='#a855f7', dash='dot'))) |
|
|
|
|
|
fig.update_layout( |
|
|
title="Simulation: Strategy Performance Comparison ($10k Start)", |
|
|
xaxis_title="Date", |
|
|
yaxis_title="Portfolio Value ($)", |
|
|
template="plotly_dark", |
|
|
paper_bgcolor='rgba(0,0,0,0)', |
|
|
plot_bgcolor='rgba(0,0,0,0)', |
|
|
hovermode="x unified", |
|
|
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) |
|
|
) |
|
|
|
|
|
|
|
|
rl_m = calculate_metrics_pro(rl_portfolio_series) |
|
|
bnh_m = calculate_metrics_pro(bnh_portfolio_series) |
|
|
eq_m = calculate_metrics_pro(eq_portfolio_series) |
|
|
|
|
|
|
|
|
def fmt(val, is_pct=True): |
|
|
if pd.isna(val): return "N/A" |
|
|
return f"{val:.2%}" if is_pct else f"{val:.2f}" |
|
|
|
|
|
metrics_data = { |
|
|
"Metric": ["Total Return", "CAGR", "Sharpe Ratio", "Sortino Ratio", "Volatility (Ann.)", "Max Drawdown", "Calmar Ratio"], |
|
|
"RL Agent (SAC)": [fmt(rl_m["Total Return"]), fmt(rl_m["CAGR"]), fmt(rl_m["Sharpe Ratio"], False), fmt(rl_m["Sortino Ratio"], False), fmt(rl_m["Volatility"]), fmt(rl_m["Max Drawdown"]), fmt(rl_m["Calmar Ratio"], False)], |
|
|
"Buy & Hold (SPY)": [fmt(bnh_m["Total Return"]), fmt(bnh_m["CAGR"]), fmt(bnh_m["Sharpe Ratio"], False), fmt(bnh_m["Sortino Ratio"], False), fmt(bnh_m["Volatility"]), fmt(bnh_m["Max Drawdown"]), fmt(bnh_m["Calmar Ratio"], False)], |
|
|
"Equal Weighted": [fmt(eq_m["Total Return"]), fmt(eq_m["CAGR"]), fmt(eq_m["Sharpe Ratio"], False), fmt(eq_m["Sortino Ratio"], False), fmt(eq_m["Volatility"]), fmt(eq_m["Max Drawdown"]), fmt(eq_m["Calmar Ratio"], False)], |
|
|
} |
|
|
metrics_df = pd.DataFrame(metrics_data) |
|
|
|
|
|
|
|
|
metrics_md = metrics_df.to_markdown(index=False) |
|
|
final_metrics_display = f"### ๐ Professional Performance Metrics\n\n{metrics_md}" |
|
|
|
|
|
yield fig, "Simulation Complete.", final_metrics_display |
|
|
|
|
|
except Exception as e: |
|
|
import traceback |
|
|
traceback.print_exc() |
|
|
yield go.Figure(), f"Error during simulation: {str(e)}", gr.update(visible=False) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_historical_analysis(selected_assets, period_name): |
|
|
"""Backend for Tab 3.""" |
|
|
if DASHBOARD_DATA_DF is None or not selected_assets: |
|
|
return go.Figure(), "Please wait for data initialization or select assets." |
|
|
|
|
|
status_html = """<div style="color: #9ca3af;">๐ Processing data and running AI analysis...</div>""" |
|
|
yield go.Figure(), status_html |
|
|
|
|
|
try: |
|
|
|
|
|
days = TIME_PERIODS.get(period_name, 365) |
|
|
cutoff_date = datetime.now() - timedelta(days=days) |
|
|
valid_assets = [a for a in selected_assets if a in DASHBOARD_DATA_DF.columns] |
|
|
if not valid_assets: |
|
|
yield go.Figure(), "Error: Selected assets not found in available data." |
|
|
return |
|
|
df_filtered = DASHBOARD_DATA_DF.loc[cutoff_date:, valid_assets].copy() |
|
|
if df_filtered.empty: |
|
|
yield go.Figure(), f"No data found for the selected period: {period_name}" |
|
|
return |
|
|
|
|
|
|
|
|
df_normalized = df_filtered / df_filtered.iloc[0] * 100 |
|
|
fig = px.line(df_normalized, x=df_normalized.index, y=df_normalized.columns, |
|
|
title=f"Performance Comparison: {period_name} (Base=100)", |
|
|
color_discrete_sequence=px.colors.qualitative.Bold) |
|
|
fig.update_layout(template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', |
|
|
yaxis_title="Normalized Price", xaxis_title="Date", legend_title_text="", hovermode="x unified") |
|
|
|
|
|
|
|
|
analysis_text = analyze_historical_segment(df_filtered, valid_assets, period_name) |
|
|
formatted_analysis = f"### ๐ค AI Analyst Report: {period_name}\n\n{analysis_text}" |
|
|
yield fig, formatted_analysis |
|
|
|
|
|
except Exception as e: |
|
|
import traceback |
|
|
traceback.print_exc() |
|
|
yield go.Figure(), f"### Error during analysis\n\n{str(e)}" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_latest_data_window(window_size=30): |
|
|
"""Fetches latest data needed for prediction.""" |
|
|
print("Fetching prediction data...") |
|
|
lookback_days = window_size + 150 |
|
|
end_date = datetime.now().strftime('%Y-%m-%d') |
|
|
start_date = (datetime.now() - timedelta(days=lookback_days)).strftime('%Y-%m-%d') |
|
|
temp_filename = os.path.join("data", "temp_gradio_prediction_data.csv") |
|
|
fetch_market_data(start_date, end_date, temp_filename) |
|
|
if not os.path.exists(temp_filename): raise Exception("Failed to fetch market data file.") |
|
|
df = pd.read_csv(temp_filename, index_col=0, parse_dates=True) |
|
|
df.dropna(inplace=True) |
|
|
if len(df) < window_size: raise Exception(f"Not enough clean data fetched for prediction.") |
|
|
return df.iloc[-window_size:].copy() |
|
|
|
|
|
def prepare_observation(data_window): |
|
|
price_data = data_window[ASSETS].values |
|
|
macro_data = data_window[MACRO_COLS].values |
|
|
norm_prices = price_data / (price_data[0] + 1e-8) |
|
|
norm_macro = macro_data / (macro_data[0] + 1e-8) |
|
|
obs = np.concatenate([norm_prices, norm_macro], axis=1) |
|
|
|
|
|
return obs.flatten().astype(np.float32), obs.astype(np.float32), data_window |
|
|
|
|
|
def predict_and_analyze(): |
|
|
yield "Starting...", None, go.Figure(), "Loading..." |
|
|
try: |
|
|
data_window = get_latest_data_window(WINDOW_SIZE) |
|
|
flat_obs, raw_obs, df_window_for_analyst = prepare_observation(data_window) |
|
|
|
|
|
if not os.path.exists(MODEL_PATH): raise FileNotFoundError("Model not found.") |
|
|
model = SAC.load(MODEL_PATH) |
|
|
|
|
|
|
|
|
|
|
|
yield "XAI Calc...", None, go.Figure(), "Calculating XAI..." |
|
|
xai_plot = calculate_feature_importance(model, flat_obs) |
|
|
|
|
|
action, _ = model.predict(flat_obs, deterministic=True) |
|
|
exp_act = np.exp(np.asarray(action).flatten()) |
|
|
weights = exp_act / np.sum(exp_act) |
|
|
|
|
|
allocs = {ASSETS[i]: weights[i] for i in range(len(ASSETS))} |
|
|
allocs['Cash'] = weights[-1] |
|
|
alloc_df = pd.DataFrame(list(allocs.items()), columns=['Asset', 'Alloc']) |
|
|
alloc_df['Alloc'] = alloc_df['Alloc'].apply(lambda x: f"{x:.2%}") |
|
|
|
|
|
yield "AI Analysis...", alloc_df, xai_plot, "Running AI..." |
|
|
llm_allocs = {k: float(v) for k, v in allocs.items()} |
|
|
res = analyze_agent_decision(df_window_for_analyst, llm_allocs) |
|
|
|
|
|
if isinstance(res, dict): |
|
|
strat, risk, just, conf = res.get('strategy_summary','N/A'), res.get('risk_level','N/A').upper(), res.get('justification','N/A'), res.get('confidence_score','N/A') |
|
|
border_col = "#ef4444" if 'HIGH' in risk else "#10b981" |
|
|
bg_col = "#7f1d1d" if 'HIGH' in risk else "#064e3b" |
|
|
icon = "โ" if 'HIGH' in risk else "๐" |
|
|
status = "TRADE BLOCKED" if 'HIGH' in risk else "TRADE APPROVED" |
|
|
|
|
|
html = f"""<div style="background-color: #1f2937; padding: 20px; border-radius: 12px; border: 1px solid #374151;"> |
|
|
<h3 style="margin-top: 0; color: #e5e7eb;">๐ค AI Report</h3> |
|
|
<p><strong>Strategy:</strong> <span style="color:#d1d5db">{strat}</span></p> |
|
|
<p><strong>Risk:</strong> <span style="color:{border_col}; font-weight:bold">{risk}</span></p> |
|
|
<p><strong>Reason:</strong> <span style="color:#d1d5db">{just}</span></p> |
|
|
<p><strong>Conf:</strong> <span style="color:#d1d5db">{conf}/10</span></p></div> |
|
|
<div style="background-color:{bg_col}; color:white; padding:15px; margin-top:10px; border-radius:12px; text-align:center; font-weight:bold;">{icon} {status}</div>""" |
|
|
else: |
|
|
html = f"<div style='color:red'>{str(res)}</div>" |
|
|
|
|
|
yield "Done", alloc_df, xai_plot, html |
|
|
except Exception as e: |
|
|
import traceback |
|
|
traceback.print_exc() |
|
|
yield f"Error: {str(e)}", None, go.Figure(), f"Error: {str(e)}" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_dashboard_metrics(): |
|
|
return "$135,400", "+3.07%" |
|
|
|
|
|
def get_portfolio_history_plot(): |
|
|
dates = pd.date_range(start="2023-01-01", periods=100) |
|
|
np.random.seed(42) |
|
|
rl_returns = np.random.normal(0.001, 0.01, 100) |
|
|
bnh_returns = np.random.normal(0.0005, 0.012, 100) |
|
|
rl_value = 10000 * np.cumprod(1 + rl_returns) |
|
|
bnh_value = 10000 * np.cumprod(1 + bnh_returns) |
|
|
fig = go.Figure() |
|
|
fig.add_trace(go.Scatter(x=dates, y=rl_value, mode='lines', name='RL Agent (Live)', line=dict(color='#10b981', width=3))) |
|
|
fig.add_trace(go.Scatter(x=dates, y=bnh_value, mode='lines', name='Benchmark', line=dict(color='#6b7280', dash='dash'))) |
|
|
fig.update_layout(title="Portfolio Net Worth (Live Tracking)", xaxis_title="Date", yaxis_title="Net Worth ($)", template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)) |
|
|
return fig |
|
|
|
|
|
def get_current_allocation_plot(): |
|
|
labels = ASSETS + ['Cash'] |
|
|
values = [0.25, 0.10, 0.30, 0.15, 0.05, 0.15] |
|
|
fig = px.pie(values=values, names=labels, title="Current Holdings Breakdown", color_discrete_sequence=px.colors.qualitative.Bold) |
|
|
fig.update_traces(textposition='inside', textinfo='percent+label', hole=.4) |
|
|
fig.update_layout(template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)', legend=dict(orientation="h", yanchor="bottom", y=-0.1)) |
|
|
return fig |
|
|
|
|
|
def get_recent_transactions(): |
|
|
data = [["2025-11-24", "Rebalance", "MULTIPLE", "N/A"], ["2025-11-24", "SELL", "SPY", "$4,500"], ["2025-11-24", "BUY", "TLT", "$4,200"], ["2025-11-21", "BUY", "BTC-USD", "$1,000"]] |
|
|
return pd.DataFrame(data, columns=["Date", "Type", "Asset", "Approx. Value"]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_css = """ |
|
|
.metric-box { background-color: #1f2937; padding: 20px; border-radius: 12px; border: 1px solid #374151; text-align: center; } |
|
|
.metric-label { font-size: 1.1em; color: #9ca3af; margin-bottom: 5px; } |
|
|
.metric-value { font-size: 2.2em; font-weight: 700; color: #e5e7eb; } |
|
|
.disclaimer-box { background-color: #374151; padding: 15px; border-radius: 8px; border-left: 4px solid #f59e0b; color: #d1d5db; font-size: 0.9em; margin-bottom: 20px; } |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks( |
|
|
|
|
|
title="Deep RL Portfolio Manager") as demo: |
|
|
gr.HTML("""<script>function forceDark(){document.body.classList.add('dark');} forceDark(); setTimeout(forceDark, 500);</script>""") |
|
|
|
|
|
gr.Markdown("# ๐ง Deep RL & LLM Portfolio Manager") |
|
|
|
|
|
with gr.Tabs(): |
|
|
|
|
|
with gr.TabItem("๐ Live Dashboard"): |
|
|
|
|
|
with gr.Row(): |
|
|
nw_val, dc_val = get_dashboard_metrics() |
|
|
with gr.Column(elem_classes=["metric-box"]): |
|
|
gr.HTML(f"<div class='metric-label'>Current Net Worth</div><div class='metric-value'>{nw_val}</div>") |
|
|
with gr.Column(elem_classes=["metric-box"]): |
|
|
gr.HTML(f"<div class='metric-label'>24h Change</div><div class='metric-value' style='color: #10b981;'>{dc_val}</div>") |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=3): |
|
|
history_chart = gr.Plot(value=get_portfolio_history_plot(), label="Net Worth History") |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
allocation_chart = gr.Plot(value=get_current_allocation_plot(), label="Current Allocation") |
|
|
with gr.Column(scale=2): |
|
|
gr.Markdown("### Recent Transactions") |
|
|
transactions_table = gr.Dataframe(value=get_recent_transactions(), interactive=False, wrap=True) |
|
|
|
|
|
|
|
|
with gr.TabItem("๐ฎ Forecast & AI Analysis"): |
|
|
gr.Markdown("### Generate Tomorrow's Portfolio Strategy") |
|
|
run_btn = gr.Button("๐ Run Overnight Analysis", variant="primary", size="lg") |
|
|
status_output = gr.Textbox(label="System Status", placeholder="Ready...", interactive=False, lines=1) |
|
|
gr.Markdown("---") |
|
|
|
|
|
with gr.Row(): |
|
|
|
|
|
with gr.Column(scale=2): |
|
|
gr.Markdown("### ๐ Suggested Position") |
|
|
allocation_output = gr.Dataframe(headers=["Asset", "Allocation"], datatype=["str", "str"], interactive=False) |
|
|
|
|
|
|
|
|
gr.Markdown("### ๐ง Why did the agent choose this?") |
|
|
xai_output_plot = gr.Plot(label="Top Influential Factors (XAI)", show_label=False) |
|
|
|
|
|
|
|
|
with gr.Column(scale=3): |
|
|
analysis_report_html = gr.HTML(label="AI Risk Analysis Report") |
|
|
|
|
|
|
|
|
run_btn.click( |
|
|
fn=predict_and_analyze, |
|
|
inputs=None, |
|
|
outputs=[status_output, allocation_output, xai_output_plot, analysis_report_html] |
|
|
) |
|
|
|
|
|
|
|
|
with gr.TabItem("๐
Historical Data Analyst"): |
|
|
gr.Markdown("### Analyze Past Market Performance with AI") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
all_tickers_hist = ASSETS + list(FRED_IDS.values()) |
|
|
if DASHBOARD_DATA_DF is not None: |
|
|
available_tickers_hist = [t for t in all_tickers_hist if t in DASHBOARD_DATA_DF.columns] |
|
|
else: |
|
|
available_tickers_hist = [] |
|
|
default_tickers_hist = available_tickers_hist[:3] if available_tickers_hist else [] |
|
|
|
|
|
asset_selector = gr.Dropdown(choices=available_tickers_hist, value=default_tickers_hist, multiselect=True, label="1. Select Assets") |
|
|
period_selector = gr.Dropdown(choices=list(TIME_PERIODS.keys()), value="1 Year", label="2. Select Period") |
|
|
analyze_btn = gr.Button("๐ Run Analysis", variant="primary") |
|
|
|
|
|
with gr.Column(scale=3): |
|
|
historical_plot = gr.Plot(label="Performance Plot") |
|
|
|
|
|
gr.Markdown("---") |
|
|
historical_analysis_md = gr.Markdown("### ๐ค AI Analyst Report\n\n*Click 'Run Analysis' to generate.*") |
|
|
|
|
|
analyze_btn.click( |
|
|
fn=run_historical_analysis, |
|
|
inputs=[asset_selector, period_selector], |
|
|
outputs=[historical_plot, historical_analysis_md] |
|
|
) |
|
|
|
|
|
|
|
|
with gr.TabItem("๐ Historical Simulation"): |
|
|
gr.Markdown("### Backtest the RL Agent against Baselines") |
|
|
|
|
|
|
|
|
gr.HTML(f""" |
|
|
<div class='disclaimer-box'> |
|
|
<strong>โ ๏ธ IMPORTANT DISCLAIMER:</strong> The RL model was trained on data from approximately |
|
|
<strong>{TRAIN_START_DATE} to {TRAIN_END_DATE}</strong>. Running simulations outside or overlapping significantly |
|
|
with this period may not accurately reflect real-world performance (lookahead bias or out-of-distribution data). |
|
|
Use for educational purposes only. |
|
|
</div> |
|
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
start_date_input = gr.Textbox(label="Start Date (YYYY-MM-DD)", value=(datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')) |
|
|
end_date_input = gr.Textbox(label="End Date (YYYY-MM-DD)", value=(datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')) |
|
|
sim_btn = gr.Button("โถ๏ธ Run Simulation", variant="primary") |
|
|
sim_status = gr.Textbox(label="Status", interactive=False, lines=1) |
|
|
|
|
|
with gr.Column(scale=3): |
|
|
sim_plot = gr.Plot(label="Simulation Performance") |
|
|
|
|
|
gr.Markdown("---") |
|
|
|
|
|
sim_metrics_md = gr.Markdown("### ๐ Professional Performance Metrics\n\n*Run simulation to see metrics.*") |
|
|
|
|
|
sim_btn.click( |
|
|
fn=run_historical_simulation, |
|
|
inputs=[start_date_input, end_date_input], |
|
|
outputs=[sim_plot, sim_status, sim_metrics_md] |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.queue().launch(server_name="0.0.0.0", server_port=7860, debug=True, share=True) |