300 lines
9.0 KiB
Python
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)
|