myco-bonding-curve/dashboard/tabs/tranches.py

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.")