myco-bonding-curve/dashboard/charts_config.py

300 lines
9.0 KiB
Python

"""Plotly figure factories for the System Config parameter visualizations.
Pure-math charts that show how parameters affect curve shapes — no simulation needed.
"""
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
def fig_pamm_redemption_curve(
alpha_bar: float = 10.0,
xu_bar: float = 0.8,
theta_bar: float = 0.5,
) -> go.Figure:
"""P-AMM redemption rate vs fraction of supply redeemed, at several backing ratios.
Shows three regions: no-discount, parabolic discount, and floor.
"""
fig = go.Figure()
x = np.linspace(0, 1, 300)
backing_levels = [0.95, 0.85, 0.70, 0.50]
colors = ["#4CAF50", "#2196F3", "#FF9800", "#E91E63"]
for ba, color in zip(backing_levels, colors):
rates = _pamm_rate_curve(x, ba, alpha_bar, xu_bar, theta_bar)
fig.add_trace(go.Scatter(
x=x, y=rates,
name=f"ba = {ba:.0%}",
line=dict(color=color, width=2),
))
# Theta floor line
fig.add_hline(
y=theta_bar, line_dash="dot", line_color="gray",
annotation_text=f"floor = {theta_bar:.0%}",
annotation_position="bottom right",
)
# xu_bar threshold line
fig.add_vline(
x=xu_bar, line_dash="dot", line_color="gray",
annotation_text=f"x̄_U = {xu_bar:.0%}",
annotation_position="top left",
)
fig.update_layout(
title="P-AMM Redemption Curve",
xaxis_title="Fraction of Supply Redeemed (x)",
yaxis_title="Redemption Rate (USD per MYCO)",
yaxis_range=[0, 1.05],
height=350,
template="plotly_white",
margin=dict(t=40, b=40),
legend=dict(orientation="h", yanchor="bottom", y=1.02),
)
return fig
def fig_fee_structure(
static_fee: float = 0.003,
surge_fee_rate: float = 0.05,
threshold: float = 0.2,
) -> go.Figure:
"""Fee rate vs pool imbalance level."""
fig = go.Figure()
imbalance = np.linspace(0, 1, 200)
fees = np.array([
_surge_fee_at_imbalance(i, static_fee, surge_fee_rate, threshold)
for i in imbalance
])
fig.add_trace(go.Scatter(
x=imbalance, y=fees * 100,
name="Effective Fee",
fill="tozeroy",
line=dict(color="#FF5722", width=2),
fillcolor="rgba(255,87,34,0.15)",
))
# Threshold annotation
fig.add_vline(
x=threshold, line_dash="dot", line_color="gray",
annotation_text=f"threshold = {threshold:.0%}",
)
fig.update_layout(
title="Imbalance Fee Structure",
xaxis_title="Pool Imbalance",
yaxis_title="Fee Rate (%)",
height=350,
template="plotly_white",
margin=dict(t=40, b=40),
)
return fig
def fig_flow_dampening(threshold: float = 0.1) -> go.Figure:
"""Flow penalty multiplier vs flow ratio."""
fig = go.Figure()
ratio = np.linspace(0, threshold * 3, 200)
half_t = threshold / 2
penalty = np.ones_like(ratio)
for i, r in enumerate(ratio):
if r <= half_t:
penalty[i] = 1.0
elif r >= threshold * 2:
penalty[i] = 0.1
else:
norm = (r - half_t) / (threshold * 1.5)
penalty[i] = max(0.1, 1.0 - norm ** 2)
fig.add_trace(go.Scatter(
x=ratio, y=penalty,
name="Penalty Multiplier",
fill="tozeroy",
line=dict(color="#9C27B0", width=2),
fillcolor="rgba(156,39,176,0.15)",
))
fig.add_vline(
x=threshold, line_dash="dot", line_color="gray",
annotation_text=f"threshold = {threshold:.2f}",
)
fig.update_layout(
title="Flow Dampening (Anti-Bank-Run)",
xaxis_title="Flow Ratio (recent outflow / reserve)",
yaxis_title="Redemption Multiplier",
yaxis_range=[0, 1.1],
height=350,
template="plotly_white",
margin=dict(t=40, b=40),
)
return fig
def fig_stakeholder_scenarios(
alpha_bar: float = 10.0,
xu_bar: float = 0.8,
theta_bar: float = 0.5,
static_fee: float = 0.003,
surge_fee_rate: float = 0.05,
max_labor_frac: float = 0.1,
max_sub_frac: float = 0.1,
max_stake_frac: float = 0.05,
) -> go.Figure:
"""Stakeholder outcome comparison across backing ratio scenarios.
Shows what $100 worth of MYCO gets you when redeeming at different
backing ratios, plus commitment minting capacity.
"""
fig = make_subplots(
rows=1, cols=2,
subplot_titles=("Redeemer Outcomes ($100 MYCO)", "Commitment Minting Capacity"),
horizontal_spacing=0.12,
)
# --- Left panel: redeemer outcomes ---
ba_values = np.linspace(0.3, 1.2, 50)
redeemed_early = [] # x = 0.1 (first 10%)
redeemed_mid = [] # x = 0.5 (middle)
redeemed_late = [] # x = 0.9 (last redeemers)
for ba in ba_values:
redeemed_early.append(100 * _pamm_rate_scalar(0.1, ba, alpha_bar, xu_bar, theta_bar))
redeemed_mid.append(100 * _pamm_rate_scalar(0.5, ba, alpha_bar, xu_bar, theta_bar))
redeemed_late.append(100 * _pamm_rate_scalar(0.9, ba, alpha_bar, xu_bar, theta_bar))
for vals, name, color, dash in [
(redeemed_early, "Early redeemer (x=10%)", "#4CAF50", "solid"),
(redeemed_mid, "Mid redeemer (x=50%)", "#FF9800", "solid"),
(redeemed_late, "Late redeemer (x=90%)", "#E91E63", "solid"),
]:
fig.add_trace(go.Scatter(
x=ba_values, y=vals, name=name,
line=dict(color=color, width=2, dash=dash),
), row=1, col=1)
fig.add_hline(y=100, line_dash="dot", line_color="gray", row=1, col=1)
fig.add_vline(x=1.0, line_dash="dot", line_color="gray", row=1, col=1)
# --- Right panel: commitment minting capacity ---
supply_levels = ["1,000", "10,000", "100,000"]
supply_nums = [1_000, 10_000, 100_000]
labor_caps = [s * max_labor_frac for s in supply_nums]
sub_caps = [s * max_sub_frac for s in supply_nums]
stake_caps = [s * max_stake_frac for s in supply_nums]
fig.add_trace(go.Bar(
x=supply_levels, y=labor_caps, name="Labor Mint Cap",
marker_color="#2196F3",
), row=1, col=2)
fig.add_trace(go.Bar(
x=supply_levels, y=sub_caps, name="Subscription Cap",
marker_color="#FF9800",
), row=1, col=2)
fig.add_trace(go.Bar(
x=supply_levels, y=stake_caps, name="Staking Bonus Cap",
marker_color="#4CAF50",
), row=1, col=2)
fig.update_layout(
height=400,
template="plotly_white",
margin=dict(t=40, b=40),
legend=dict(orientation="h", yanchor="bottom", y=1.02),
barmode="group",
)
fig.update_xaxes(title_text="Backing Ratio", row=1, col=1)
fig.update_yaxes(title_text="USD Returned", row=1, col=1)
fig.update_xaxes(title_text="Supply Level", row=1, col=2)
fig.update_yaxes(title_text="Max Tokens Mintable", row=1, col=2)
return fig
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _pamm_rate_curve(
x: np.ndarray, ba: float,
alpha_bar: float, xu_bar: float, theta_bar: float,
) -> np.ndarray:
"""Vectorized P-AMM rate computation for a fixed backing ratio."""
rates = np.full_like(x, ba)
if ba >= 1.0:
return np.minimum(rates, 1.0)
delta = 1.0 - ba
ya = 1.0 # Normalized supply
# Dynamic alpha
alpha = max(alpha_bar / ya, 2.0 * delta)
# xu
xu = xu_bar
if alpha > 0 and delta > 0:
xu_from_delta = 1.0 - np.sqrt(2.0 * delta / alpha)
xu = min(xu, max(0, xu_from_delta))
# xl
inner = (1.0 - xu) ** 2 - 2.0 * (1.0 - theta_bar) / alpha
xl = 1.0 - np.sqrt(inner) if inner >= 0 else None
for i, xi in enumerate(x):
if xi <= xu:
rates[i] = ba
elif xl is not None and xi <= xl:
rates[i] = max(ba - xi + alpha * (xi - xu) ** 2 / 2, theta_bar)
else:
rates[i] = theta_bar
return np.minimum(rates, 1.0)
def _pamm_rate_scalar(
x: float, ba: float,
alpha_bar: float, xu_bar: float, theta_bar: float,
) -> float:
"""Single-point P-AMM rate."""
if ba >= 1.0:
return min(ba, 1.0)
if ba <= 0:
return theta_bar
delta = 1.0 - ba
alpha = max(alpha_bar, 2.0 * delta)
xu = xu_bar
if alpha > 0 and delta > 0:
xu_from_delta = 1.0 - np.sqrt(2.0 * delta / alpha)
xu = min(xu, max(0, xu_from_delta))
inner = (1.0 - xu) ** 2 - 2.0 * (1.0 - theta_bar) / alpha
xl = 1.0 - np.sqrt(inner) if inner >= 0 else None
if x <= xu:
return ba
elif xl is not None and x <= xl:
return max(ba - x + alpha * (x - xu) ** 2 / 2, theta_bar)
else:
return theta_bar
def _surge_fee_at_imbalance(
imbalance: float,
static_fee: float,
surge_fee_rate: float,
threshold: float,
) -> float:
"""Fee rate at a given imbalance level (assuming swap worsens imbalance)."""
if imbalance <= threshold:
return static_fee
excess = (imbalance - threshold) / (1.0 - threshold)
return static_fee + (surge_fee_rate - static_fee) * min(excess, 1.0)