191 lines
7.0 KiB
Python
191 lines
7.0 KiB
Python
"""Conviction voting governance 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("Conviction Voting Governance")
|
||
st.caption("Explore conviction voting dynamics for parameter governance.")
|
||
|
||
# Parameters
|
||
col1, col2, col3, col4 = st.columns(4)
|
||
with col1:
|
||
half_life = st.slider("Half-life (epochs)", 2, 30, 7, key="cv_hl")
|
||
with col2:
|
||
beta = st.slider("Max fund share (β)", 0.05, 0.5, 0.2, 0.05, key="cv_beta")
|
||
with col3:
|
||
rho = st.slider("Scale factor (ρ) ×1000", 0.5, 10.0, 2.5, 0.5, key="cv_rho")
|
||
with col4:
|
||
n_voters = st.slider("Voters", 5, 50, 20, key="cv_voters")
|
||
|
||
rho_actual = rho / 1000
|
||
|
||
tab_curves, tab_sim, tab_trigger = st.tabs(["Conviction Curves", "Governance Sim", "Trigger Map"])
|
||
|
||
with tab_curves:
|
||
_render_conviction_curves(half_life)
|
||
|
||
with tab_sim:
|
||
_render_governance_sim(half_life, beta, rho_actual, n_voters)
|
||
|
||
with tab_trigger:
|
||
_render_trigger_map(beta, rho_actual)
|
||
|
||
|
||
def _render_conviction_curves(half_life: float):
|
||
"""Visualize conviction charging and discharging."""
|
||
from src.primitives.conviction import ConvictionParams, generate_conviction_curves
|
||
|
||
params = ConvictionParams.from_half_life(half_life)
|
||
|
||
st.markdown(f"**α = {params.alpha:.4f}** (half-life = {half_life} epochs)")
|
||
|
||
curves = generate_conviction_curves(1000, params.alpha, epochs=100)
|
||
|
||
fig = make_subplots(rows=1, cols=2, subplot_titles=("Charging", "Discharging"))
|
||
|
||
fig.add_trace(go.Scatter(
|
||
x=curves["time"], y=curves["charge"],
|
||
name="Conviction", line=dict(color="#2dd4bf", width=2),
|
||
), row=1, col=1)
|
||
fig.add_trace(go.Scatter(
|
||
x=curves["time"], y=curves["max"],
|
||
name="Max", line=dict(color="#fbbf24", dash="dash"),
|
||
), row=1, col=1)
|
||
|
||
fig.add_trace(go.Scatter(
|
||
x=curves["time"], y=curves["discharge"],
|
||
name="Decay", line=dict(color="#ef4444", width=2),
|
||
), row=1, col=2)
|
||
|
||
fig.update_layout(template="plotly_dark", height=400, showlegend=True)
|
||
fig.update_xaxes(title_text="Epochs")
|
||
fig.update_yaxes(title_text="Conviction")
|
||
st.plotly_chart(fig, use_container_width=True)
|
||
|
||
# Key metrics
|
||
from src.primitives.conviction import epochs_to_fraction, max_conviction
|
||
col1, col2, col3 = st.columns(3)
|
||
col1.metric("50% of max", f"{epochs_to_fraction(0.5, params.alpha):.1f} epochs")
|
||
col2.metric("90% of max", f"{epochs_to_fraction(0.9, params.alpha):.1f} epochs")
|
||
col3.metric("Max conviction (1000 tokens)", f"{max_conviction(1000, params.alpha):,.0f}")
|
||
|
||
|
||
def _render_governance_sim(half_life: float, beta: float, rho: float, n_voters: int):
|
||
"""Run a governance simulation."""
|
||
if st.button("Run Governance Simulation", key="cv_run"):
|
||
with st.spinner("Simulating conviction voting..."):
|
||
from src.primitives.conviction import (
|
||
ConvictionParams, ConvictionSystem, Proposal, Voter,
|
||
stake, tick, get_governance_metrics,
|
||
)
|
||
|
||
params = ConvictionParams.from_half_life(half_life, beta=beta, rho=rho)
|
||
system = ConvictionSystem(params=params)
|
||
|
||
# Create voters
|
||
for i in range(n_voters):
|
||
holdings = np.random.lognormal(mean=np.log(5000), sigma=1.0)
|
||
system.voters[f"v{i}"] = Voter(id=f"v{i}", holdings=holdings)
|
||
system.total_supply = sum(v.holdings for v in system.voters.values())
|
||
|
||
# Create proposals
|
||
proposals = [
|
||
("Lower senior CR to 1.3", 0.05),
|
||
("Add Scroll chain", 0.08),
|
||
("Increase mez yield to 10%", 0.03),
|
||
("Add sfrxETH to Arbitrum", 0.02),
|
||
]
|
||
for i, (title, funds) in enumerate(proposals):
|
||
system.proposals[f"p{i}"] = Proposal(
|
||
id=f"p{i}", title=title, funds_requested=funds,
|
||
)
|
||
|
||
# Simulate staking & ticking
|
||
history = []
|
||
for epoch in range(100):
|
||
# Some voters stake/unstake
|
||
for vid, voter in system.voters.items():
|
||
if np.random.random() < 0.3:
|
||
candidates = [p for p in system.proposals.values() if p.status == "candidate"]
|
||
if candidates:
|
||
prop = np.random.choice(candidates)
|
||
amt = voter.holdings * np.random.uniform(0.05, 0.3)
|
||
stake(system, vid, prop.id, amt)
|
||
|
||
tick(system)
|
||
metrics = get_governance_metrics(system)
|
||
metrics["epoch"] = epoch
|
||
history.append(metrics)
|
||
|
||
_plot_conviction_progress(history, system)
|
||
|
||
|
||
def _plot_conviction_progress(history: list, system):
|
||
"""Plot conviction progress toward triggers for each proposal."""
|
||
fig = go.Figure()
|
||
|
||
for prop_id, prop in system.proposals.items():
|
||
progress = [
|
||
h["proposals"].get(prop_id, {}).get("progress", 0)
|
||
for h in history
|
||
]
|
||
epochs = [h["epoch"] for h in history]
|
||
fig.add_trace(go.Scatter(
|
||
x=epochs, y=progress,
|
||
name=f"{prop.title} ({'PASSED' if prop.status == 'passed' else prop.status})",
|
||
line=dict(width=2),
|
||
))
|
||
|
||
fig.add_hline(y=1.0, line_dash="dash", line_color="white",
|
||
annotation_text="Trigger threshold")
|
||
|
||
fig.update_layout(
|
||
title="Proposal Conviction Progress",
|
||
xaxis_title="Epoch",
|
||
yaxis_title="Progress (conviction / trigger)",
|
||
template="plotly_dark",
|
||
height=500,
|
||
)
|
||
st.plotly_chart(fig, use_container_width=True)
|
||
|
||
# Summary
|
||
for prop in system.proposals.values():
|
||
status_emoji = {"passed": "✅", "candidate": "⏳", "failed": "❌"}.get(prop.status, "❓")
|
||
st.write(f"{status_emoji} **{prop.title}** — {prop.status} (age: {prop.age})")
|
||
|
||
|
||
def _render_trigger_map(beta: float, rho: float):
|
||
"""2D contour map of trigger function."""
|
||
from src.primitives.conviction import trigger_threshold, ConvictionParams
|
||
|
||
params = ConvictionParams(beta=beta, rho=rho)
|
||
supply_range = np.linspace(10_000, 1_000_000, 100)
|
||
share_range = np.linspace(0.001, beta - 0.001, 100)
|
||
|
||
Z = np.zeros((len(share_range), len(supply_range)))
|
||
for i, share in enumerate(share_range):
|
||
for j, supply in enumerate(supply_range):
|
||
Z[i, j] = np.log10(trigger_threshold(share, supply, params))
|
||
|
||
fig = go.Figure(go.Contour(
|
||
z=Z,
|
||
x=supply_range,
|
||
y=share_range,
|
||
colorscale="Viridis",
|
||
colorbar=dict(title="log₁₀(trigger)"),
|
||
))
|
||
|
||
fig.update_layout(
|
||
title="Trigger Function Map",
|
||
xaxis_title="Total Supply",
|
||
yaxis_title="Share of Funds Requested",
|
||
template="plotly_dark",
|
||
height=500,
|
||
)
|
||
st.plotly_chart(fig, use_container_width=True)
|
||
st.caption("Higher trigger = more conviction needed. Requesting closer to β makes it exponentially harder.")
|