172 lines
6.0 KiB
Python
172 lines
6.0 KiB
Python
"""cadCAD Monte Carlo simulation dashboard tab."""
|
||
|
||
import streamlit as st
|
||
import plotly.graph_objects as go
|
||
from plotly.subplots import make_subplots
|
||
import numpy as np
|
||
|
||
|
||
def render():
|
||
st.header("cadCAD Monte Carlo Simulation")
|
||
st.caption("Full system simulation with stochastic processes, parameter sweeps, and stress testing.")
|
||
|
||
scenario = st.selectbox("Scenario", [
|
||
"Normal Growth",
|
||
"ETH Crash Stress Test",
|
||
"Parameter Sweep",
|
||
])
|
||
|
||
col1, col2, col3 = st.columns(3)
|
||
with col1:
|
||
timesteps = st.slider("Days", 30, 730, 180, key="cd_days")
|
||
with col2:
|
||
runs = st.slider("Monte Carlo Runs", 1, 10, 3, key="cd_runs")
|
||
with col3:
|
||
if scenario == "ETH Crash Stress Test":
|
||
crash_mag = st.slider("Crash Magnitude %", 20, 80, 50, 5, key="cd_crash")
|
||
elif scenario == "Parameter Sweep":
|
||
st.info("Sweeps: volatility × growth × redemption")
|
||
|
||
if st.button("Run cadCAD Simulation", key="cd_run"):
|
||
with st.spinner(f"Running {scenario} ({runs} runs, {timesteps} days)..."):
|
||
try:
|
||
from src.cadcad.config import (
|
||
scenario_normal_growth,
|
||
scenario_stress_test,
|
||
scenario_parameter_sweep,
|
||
)
|
||
|
||
if scenario == "Normal Growth":
|
||
df = scenario_normal_growth(timesteps=timesteps, runs=runs)
|
||
elif scenario == "ETH Crash Stress Test":
|
||
df = scenario_stress_test(timesteps=timesteps, runs=runs)
|
||
else:
|
||
df = scenario_parameter_sweep(timesteps=timesteps, runs=1)
|
||
|
||
st.session_state["cadcad_results"] = df
|
||
st.success(f"Simulation complete: {len(df)} data points across {runs} run(s)")
|
||
|
||
except Exception as e:
|
||
st.error(f"Simulation error: {e}")
|
||
st.info("Make sure cadCAD is installed: `pip install cadCAD`")
|
||
return
|
||
|
||
# Display results
|
||
if "cadcad_results" in st.session_state:
|
||
df = st.session_state["cadcad_results"]
|
||
_plot_overview(df)
|
||
_plot_tranches(df)
|
||
_plot_crosschain(df)
|
||
|
||
|
||
def _plot_overview(df):
|
||
"""System overview: supply, collateral, price, CR."""
|
||
st.subheader("System Overview")
|
||
|
||
fig = make_subplots(
|
||
rows=2, cols=2,
|
||
subplot_titles=("Total Supply", "Total Collateral", "MYCO Price", "System CR"),
|
||
)
|
||
|
||
runs = df["run"].unique()
|
||
colors = ["#2dd4bf", "#f59e0b", "#ef4444", "#a78bfa", "#60a5fa"]
|
||
|
||
for i, run in enumerate(runs):
|
||
rd = df[df["run"] == run]
|
||
color = colors[i % len(colors)]
|
||
opacity = 0.8 if len(runs) == 1 else 0.4
|
||
|
||
fig.add_trace(go.Scatter(
|
||
x=rd["timestep"], y=rd["total_supply"],
|
||
name=f"Run {run}" if i == 0 else None, showlegend=(i == 0),
|
||
line=dict(color=color, width=1), opacity=opacity,
|
||
), row=1, col=1)
|
||
|
||
fig.add_trace(go.Scatter(
|
||
x=rd["timestep"], y=rd["total_collateral_usd"],
|
||
showlegend=False, line=dict(color=color, width=1), opacity=opacity,
|
||
), row=1, col=2)
|
||
|
||
fig.add_trace(go.Scatter(
|
||
x=rd["timestep"], y=rd["myco_price"],
|
||
showlegend=False, line=dict(color=color, width=1), opacity=opacity,
|
||
), row=2, col=1)
|
||
|
||
fig.add_trace(go.Scatter(
|
||
x=rd["timestep"], y=rd["system_cr"].clip(upper=5),
|
||
showlegend=False, line=dict(color=color, width=1), opacity=opacity,
|
||
), row=2, col=2)
|
||
|
||
fig.update_layout(template="plotly_dark", height=600, showlegend=True)
|
||
st.plotly_chart(fig, use_container_width=True)
|
||
|
||
|
||
def _plot_tranches(df):
|
||
"""Tranche metrics across runs."""
|
||
st.subheader("Tranche Dynamics")
|
||
|
||
fig = make_subplots(
|
||
rows=1, cols=3,
|
||
subplot_titles=("Senior CR", "Mezzanine CR", "Junior CR"),
|
||
)
|
||
|
||
runs = df["run"].unique()
|
||
for i, run in enumerate(runs):
|
||
rd = df[df["run"] == run]
|
||
opacity = 0.6
|
||
|
||
for j, (col, color) in enumerate([
|
||
("senior_cr", "#10B981"),
|
||
("mezzanine_cr", "#F59E0B"),
|
||
("junior_cr", "#EF4444"),
|
||
]):
|
||
fig.add_trace(go.Scatter(
|
||
x=rd["timestep"], y=rd[col].clip(upper=5),
|
||
showlegend=False, line=dict(color=color, width=1), opacity=opacity,
|
||
), row=1, col=j + 1)
|
||
|
||
# Add threshold lines
|
||
fig.add_hline(y=1.5, line_dash="dash", line_color="#10B981", row=1, col=1)
|
||
fig.add_hline(y=1.2, line_dash="dash", line_color="#F59E0B", row=1, col=2)
|
||
fig.add_hline(y=1.0, line_dash="dash", line_color="#EF4444", row=1, col=3)
|
||
|
||
fig.update_layout(template="plotly_dark", height=350)
|
||
st.plotly_chart(fig, use_container_width=True)
|
||
|
||
|
||
def _plot_crosschain(df):
|
||
"""Cross-chain metrics."""
|
||
st.subheader("Cross-Chain Activity")
|
||
|
||
# Check for per-chain columns
|
||
chain_cols = [c for c in df.columns if c.startswith("collateral_")]
|
||
if not chain_cols:
|
||
st.info("No per-chain data in this simulation.")
|
||
return
|
||
|
||
fig = go.Figure()
|
||
rd = df[df["run"] == df["run"].iloc[0]] # First run
|
||
|
||
for col in chain_cols:
|
||
chain_name = col.replace("collateral_", "").capitalize()
|
||
fig.add_trace(go.Scatter(
|
||
x=rd["timestep"], y=rd[col],
|
||
name=chain_name, stackgroup="one",
|
||
))
|
||
|
||
fig.update_layout(
|
||
title="Collateral by Chain (Run 1)",
|
||
xaxis_title="Timestep", yaxis_title="USD Value",
|
||
template="plotly_dark", height=400,
|
||
)
|
||
st.plotly_chart(fig, use_container_width=True)
|
||
|
||
# Summary stats
|
||
st.subheader("Summary Statistics")
|
||
last_step = df[df["timestep"] == df["timestep"].max()]
|
||
cols = st.columns(4)
|
||
cols[0].metric("Avg Final Supply", f"{last_step['total_supply'].mean():,.0f}")
|
||
cols[1].metric("Avg Final Collateral", f"${last_step['total_collateral_usd'].mean():,.0f}")
|
||
cols[2].metric("Avg System CR", f"{last_step['system_cr'].mean():.2f}")
|
||
cols[3].metric("Total CCIP Messages", f"{last_step['ccip_messages'].mean():,.0f}")
|