188 lines
6.3 KiB
Python
188 lines
6.3 KiB
Python
"""Cross-chain collateral 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("Cross-Chain Collateral Map")
|
|
st.caption("Visualize collateral distribution across chains and staking assets.")
|
|
|
|
# Configuration
|
|
col1, col2 = st.columns(2)
|
|
with col1:
|
|
n_days = st.slider("Simulation days", 30, 365, 90, key="cc_days")
|
|
with col2:
|
|
eth_vol = st.select_slider(
|
|
"ETH Volatility",
|
|
options=[0.2, 0.4, 0.6, 0.8, 1.0],
|
|
value=0.6,
|
|
key="cc_vol",
|
|
)
|
|
|
|
if st.button("Run Cross-Chain Simulation", key="cc_run"):
|
|
with st.spinner("Simulating cross-chain system..."):
|
|
from src.crosschain.hub_spoke import (
|
|
create_default_system, simulate_deposit, tick,
|
|
apply_price_shock, get_crosschain_metrics,
|
|
)
|
|
|
|
system = create_default_system()
|
|
|
|
# Bootstrap with deposits
|
|
deposits = {
|
|
"ethereum": [("stETH", 100), ("rETH", 50), ("cbETH", 30)],
|
|
"arbitrum": [("wstETH", 80), ("rETH", 40)],
|
|
"optimism": [("wstETH", 60), ("sfrxETH", 40)],
|
|
"base": [("cbETH", 40), ("USDC", 100_000)],
|
|
"polygon": [("stMATIC", 200_000), ("USDC", 50_000)],
|
|
}
|
|
|
|
for chain, assets in deposits.items():
|
|
for symbol, qty in assets:
|
|
simulate_deposit(system, chain, symbol, qty, 0.0)
|
|
system.hub.process_messages(0.0)
|
|
|
|
# Simulate daily
|
|
dt = 1 / 365
|
|
history = []
|
|
for day in range(n_days):
|
|
# Random deposits
|
|
if np.random.random() < 0.3:
|
|
chains = list(system.hub.spokes.keys())
|
|
chain = np.random.choice(chains)
|
|
spoke = system.hub.spokes[chain]
|
|
if spoke.accepted_assets:
|
|
asset = np.random.choice(spoke.accepted_assets)
|
|
qty = np.random.lognormal(2, 1) / asset.price
|
|
simulate_deposit(system, chain, asset.symbol, qty, system.time)
|
|
|
|
# Price movements
|
|
eth_return = np.random.normal(0, eth_vol * np.sqrt(dt))
|
|
for chain, spoke in system.hub.spokes.items():
|
|
for asset in spoke.accepted_assets:
|
|
if asset.symbol != "USDC":
|
|
asset.price *= (1 + eth_return + np.random.normal(0, 0.01))
|
|
spoke._recalculate_value()
|
|
system.hub._recalculate_total()
|
|
|
|
metrics = tick(system, dt)
|
|
metrics["day"] = day
|
|
history.append(metrics)
|
|
|
|
# Visualization
|
|
_plot_chain_distribution(history, system)
|
|
_plot_collateral_timeline(history)
|
|
_plot_asset_breakdown(system)
|
|
|
|
|
|
def _plot_chain_distribution(history: list, system):
|
|
"""Stacked area chart of collateral per chain over time."""
|
|
st.subheader("Collateral by Chain")
|
|
|
|
fig = go.Figure()
|
|
chains = list(system.hub.spokes.keys())
|
|
colors = {
|
|
"ethereum": "#627EEA",
|
|
"arbitrum": "#28A0F0",
|
|
"optimism": "#FF0420",
|
|
"base": "#0052FF",
|
|
"polygon": "#8247E5",
|
|
}
|
|
|
|
days = [h["day"] for h in history]
|
|
for chain in chains:
|
|
values = [h["per_chain"].get(chain, 0) for h in history]
|
|
fig.add_trace(go.Scatter(
|
|
x=days, y=values,
|
|
name=chain.capitalize(),
|
|
stackgroup="one",
|
|
fillcolor=colors.get(chain, "#888888"),
|
|
line=dict(width=0.5, color=colors.get(chain, "#888888")),
|
|
))
|
|
|
|
fig.update_layout(
|
|
title="Cross-Chain Collateral Distribution",
|
|
xaxis_title="Day",
|
|
yaxis_title="USD Value",
|
|
template="plotly_dark",
|
|
height=500,
|
|
)
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
def _plot_collateral_timeline(history: list):
|
|
"""Total collateral and yield over time."""
|
|
st.subheader("Total Collateral & Yield")
|
|
|
|
fig = make_subplots(specs=[[{"secondary_y": True}]])
|
|
|
|
days = [h["day"] for h in history]
|
|
total_col = [h["total_collateral_usd"] for h in history]
|
|
cum_yield = [h["yield_this_tick"] for h in history]
|
|
cum_yield_sum = np.cumsum(cum_yield)
|
|
|
|
fig.add_trace(
|
|
go.Scatter(x=days, y=total_col, name="Total Collateral", line=dict(color="#2dd4bf", width=2)),
|
|
secondary_y=False,
|
|
)
|
|
fig.add_trace(
|
|
go.Scatter(x=days, y=cum_yield_sum, name="Cumulative Yield", line=dict(color="#fbbf24", width=2)),
|
|
secondary_y=True,
|
|
)
|
|
|
|
fig.update_layout(
|
|
title="System Collateral & Staking Yield",
|
|
template="plotly_dark",
|
|
height=400,
|
|
)
|
|
fig.update_yaxes(title_text="Collateral (USD)", secondary_y=False)
|
|
fig.update_yaxes(title_text="Cumulative Yield (USD)", secondary_y=True)
|
|
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
def _plot_asset_breakdown(system):
|
|
"""Treemap of current asset breakdown."""
|
|
st.subheader("Current Asset Breakdown")
|
|
|
|
labels, parents, values, colors = [], [], [], []
|
|
chain_colors = {
|
|
"ethereum": "#627EEA",
|
|
"arbitrum": "#28A0F0",
|
|
"optimism": "#FF0420",
|
|
"base": "#0052FF",
|
|
"polygon": "#8247E5",
|
|
}
|
|
|
|
for chain, spoke in system.hub.spokes.items():
|
|
labels.append(chain.capitalize())
|
|
parents.append("")
|
|
values.append(spoke.total_value_usd)
|
|
colors.append(chain_colors.get(chain, "#888"))
|
|
|
|
for asset in spoke.accepted_assets:
|
|
qty = spoke.balances.get(asset.symbol, 0)
|
|
val = qty * asset.price
|
|
if val > 0:
|
|
labels.append(f"{asset.symbol}")
|
|
parents.append(chain.capitalize())
|
|
values.append(val)
|
|
colors.append(chain_colors.get(chain, "#888"))
|
|
|
|
fig = go.Figure(go.Treemap(
|
|
labels=labels,
|
|
parents=parents,
|
|
values=values,
|
|
marker_colors=colors,
|
|
textinfo="label+value+percent root",
|
|
texttemplate="%{label}<br>$%{value:,.0f}<br>%{percentRoot:.1%}",
|
|
))
|
|
fig.update_layout(
|
|
title="Collateral Treemap",
|
|
template="plotly_dark",
|
|
height=500,
|
|
)
|
|
st.plotly_chart(fig, use_container_width=True)
|