217 lines
7.7 KiB
Python
217 lines
7.7 KiB
Python
"""Risk tranche visualization 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("Risk Tranches")
|
|
st.caption("Senior / Mezzanine / Junior tranche dynamics with yield waterfall and stress testing.")
|
|
|
|
# Tranche parameters
|
|
st.subheader("Tranche Configuration")
|
|
col1, col2, col3 = st.columns(3)
|
|
with col1:
|
|
st.markdown("**Senior (myUSD-S)**")
|
|
senior_cr = st.slider("Min Collateral Ratio", 1.1, 2.0, 1.5, 0.05, key="sr_cr")
|
|
senior_yield = st.slider("Target Yield %", 1.0, 6.0, 3.0, 0.5, key="sr_y")
|
|
with col2:
|
|
st.markdown("**Mezzanine (myUSD-M)**")
|
|
mez_cr = st.slider("Min Collateral Ratio", 1.0, 1.5, 1.2, 0.05, key="mz_cr")
|
|
mez_yield = st.slider("Target Yield %", 4.0, 15.0, 8.0, 0.5, key="mz_y")
|
|
with col3:
|
|
st.markdown("**Junior ($MYCO)**")
|
|
st.info("First-loss position. Gets residual yield after Senior & Mez are paid.")
|
|
|
|
# Simulation controls
|
|
st.subheader("Simulation")
|
|
col1, col2, col3 = st.columns(3)
|
|
with col1:
|
|
initial_collateral = st.number_input("Initial Collateral ($)", 100_000, 10_000_000, 1_000_000, 100_000)
|
|
with col2:
|
|
staking_apy = st.slider("Avg Staking APY %", 2.0, 8.0, 4.0, 0.5)
|
|
with col3:
|
|
n_days = st.slider("Days", 30, 730, 365, key="tr_days")
|
|
|
|
# Stress scenario
|
|
stress = st.selectbox("Stress Scenario", [
|
|
"None",
|
|
"30% ETH crash at day 60",
|
|
"50% ETH crash at day 60",
|
|
"Slow bleed (1% daily for 30 days)",
|
|
])
|
|
|
|
if st.button("Run Tranche Simulation", key="tr_run"):
|
|
with st.spinner("Simulating tranches..."):
|
|
from src.primitives.risk_tranching import (
|
|
RiskTrancheSystem, TrancheParams,
|
|
deposit_collateral, mint_tranche,
|
|
distribute_yield, apply_loss,
|
|
get_tranche_metrics,
|
|
)
|
|
|
|
params = TrancheParams(
|
|
senior_collateral_ratio=senior_cr,
|
|
mezzanine_collateral_ratio=mez_cr,
|
|
senior_yield_target=senior_yield / 100,
|
|
mezzanine_yield_target=mez_yield / 100,
|
|
)
|
|
system = RiskTrancheSystem(params=params)
|
|
|
|
# Bootstrap
|
|
deposit_collateral(system, initial_collateral)
|
|
mint_tranche(system, "senior", initial_collateral * 0.4 / senior_cr)
|
|
mint_tranche(system, "mezzanine", initial_collateral * 0.25 / mez_cr)
|
|
mint_tranche(system, "junior", initial_collateral * 0.15)
|
|
|
|
# Simulate
|
|
dt = 1 / 365
|
|
history = []
|
|
collateral_value = initial_collateral
|
|
|
|
for day in range(n_days):
|
|
# Staking yield
|
|
daily_yield = collateral_value * (staking_apy / 100) * dt
|
|
distribute_yield(system, daily_yield, dt)
|
|
collateral_value += daily_yield
|
|
|
|
# Price movement
|
|
price_change = np.random.normal(0, 0.02) # 2% daily vol
|
|
|
|
# Apply stress
|
|
if stress == "30% ETH crash at day 60" and day == 60:
|
|
price_change = -0.30
|
|
elif stress == "50% ETH crash at day 60" and day == 60:
|
|
price_change = -0.50
|
|
elif stress == "Slow bleed (1% daily for 30 days)" and 60 <= day < 90:
|
|
price_change = -0.01
|
|
|
|
if price_change < 0:
|
|
loss = abs(price_change) * collateral_value
|
|
apply_loss(system, loss)
|
|
collateral_value -= loss
|
|
else:
|
|
gain = price_change * collateral_value
|
|
collateral_value += gain
|
|
system.total_collateral = collateral_value
|
|
|
|
metrics = get_tranche_metrics(system)
|
|
metrics["day"] = day
|
|
metrics["collateral_value"] = collateral_value
|
|
history.append(metrics)
|
|
|
|
_plot_tranche_supplies(history)
|
|
_plot_collateral_ratios(history)
|
|
_plot_yield_waterfall(history)
|
|
_plot_loss_absorption(history)
|
|
|
|
|
|
def _plot_tranche_supplies(history: list):
|
|
"""Stacked area of tranche supplies."""
|
|
st.subheader("Tranche Supplies")
|
|
|
|
fig = go.Figure()
|
|
days = [h["day"] for h in history]
|
|
|
|
for name, color in [("senior", "#10B981"), ("mezzanine", "#F59E0B"), ("junior", "#EF4444")]:
|
|
values = [h[name]["supply"] for h in history]
|
|
fig.add_trace(go.Scatter(
|
|
x=days, y=values, name=name.capitalize(),
|
|
stackgroup="one", line=dict(width=0.5, color=color),
|
|
))
|
|
|
|
fig.update_layout(
|
|
title="Tranche Token Supply",
|
|
xaxis_title="Day", yaxis_title="Supply",
|
|
template="plotly_dark", height=400,
|
|
)
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
def _plot_collateral_ratios(history: list):
|
|
"""Collateral ratios per tranche over time."""
|
|
st.subheader("Collateral Ratios")
|
|
|
|
fig = go.Figure()
|
|
days = [h["day"] for h in history]
|
|
|
|
for name, color, threshold in [
|
|
("senior", "#10B981", 1.5),
|
|
("mezzanine", "#F59E0B", 1.2),
|
|
("junior", "#EF4444", 1.0),
|
|
]:
|
|
values = [min(h[name]["cr"], 5.0) for h in history] # Cap for display
|
|
fig.add_trace(go.Scatter(
|
|
x=days, y=values, name=name.capitalize(),
|
|
line=dict(color=color, width=2),
|
|
))
|
|
# Threshold line
|
|
fig.add_hline(
|
|
y=threshold, line_dash="dash", line_color=color,
|
|
annotation_text=f"{name} min", opacity=0.5,
|
|
)
|
|
|
|
fig.add_hline(y=1.0, line_dash="dot", line_color="red", annotation_text="Underwater")
|
|
|
|
fig.update_layout(
|
|
title="Collateral Ratios (higher = safer)",
|
|
xaxis_title="Day", yaxis_title="Ratio",
|
|
template="plotly_dark", height=400,
|
|
)
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
def _plot_yield_waterfall(history: list):
|
|
"""Cumulative yield per tranche (waterfall)."""
|
|
st.subheader("Yield Waterfall")
|
|
|
|
fig = go.Figure()
|
|
days = [h["day"] for h in history]
|
|
|
|
for name, color in [("senior", "#10B981"), ("mezzanine", "#F59E0B"), ("junior", "#EF4444")]:
|
|
values = [h[name]["yield"] for h in history]
|
|
fig.add_trace(go.Scatter(
|
|
x=days, y=values, name=f"{name.capitalize()} yield",
|
|
line=dict(color=color, width=2),
|
|
fill="tozeroy" if name == "senior" else "tonexty",
|
|
))
|
|
|
|
fig.update_layout(
|
|
title="Cumulative Yield Distribution (Senior → Mez → Junior)",
|
|
xaxis_title="Day", yaxis_title="Cumulative Yield ($)",
|
|
template="plotly_dark", height=400,
|
|
)
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
def _plot_loss_absorption(history: list):
|
|
"""Cumulative losses absorbed per tranche."""
|
|
st.subheader("Loss Absorption")
|
|
|
|
total_losses = sum(h["junior"]["losses"] + h["mezzanine"]["losses"] + h["senior"]["losses"]
|
|
for h in history[-1:])
|
|
|
|
if total_losses > 0:
|
|
fig = go.Figure()
|
|
days = [h["day"] for h in history]
|
|
|
|
for name, color in [("junior", "#EF4444"), ("mezzanine", "#F59E0B"), ("senior", "#10B981")]:
|
|
values = [h[name]["losses"] for h in history]
|
|
fig.add_trace(go.Bar(
|
|
x=[name.capitalize()],
|
|
y=[values[-1]],
|
|
name=name.capitalize(),
|
|
marker_color=color,
|
|
))
|
|
|
|
fig.update_layout(
|
|
title="Total Losses Absorbed by Tranche",
|
|
yaxis_title="Losses ($)",
|
|
template="plotly_dark", height=300,
|
|
)
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
else:
|
|
st.success("No losses absorbed — all tranches healthy.")
|