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

368 lines
15 KiB
Python

"""System Config tab — parameter editor with presets and JSON import/export."""
import json
import numpy as np
import streamlit as st
from src.composed.myco_surface import MycoSystemConfig
from src.primitives.redemption_curve import PAMMParams
from dashboard.presets import PRESETS, DEFAULT_PRESET
def _apply_preset(name: str) -> None:
"""Bulk-set session_state values from a preset."""
preset = PRESETS[name]
for key, value in preset.items():
st.session_state[f"cfg_{key}"] = value
st.session_state["cfg_active_preset"] = name
def _build_config() -> MycoSystemConfig:
"""Build a MycoSystemConfig from current session_state."""
n = st.session_state.get("cfg_n_reserve_assets", 3)
lambdas = st.session_state.get("cfg_surface_lambdas", [1.0] * n)
weights = st.session_state.get("cfg_initial_target_weights", [1.0 / n] * n)
vault_names = st.session_state.get("cfg_vault_names", ["USDC", "ETH", "DAI"][:n])
return MycoSystemConfig(
n_reserve_assets=n,
surface_lambdas=np.array(lambdas[:n]),
vault_names=vault_names[:n],
initial_target_weights=np.array(weights[:n]),
pamm_params=PAMMParams(
alpha_bar=st.session_state.get("cfg_pamm_alpha_bar", 10.0),
xu_bar=st.session_state.get("cfg_pamm_xu_bar", 0.8),
theta_bar=st.session_state.get("cfg_pamm_theta_bar", 0.5),
),
flow_memory=st.session_state.get("cfg_flow_memory", 0.999),
flow_threshold=st.session_state.get("cfg_flow_threshold", 0.1),
static_fee=st.session_state.get("cfg_static_fee", 0.003),
surge_fee_rate=st.session_state.get("cfg_surge_fee_rate", 0.05),
imbalance_threshold=st.session_state.get("cfg_imbalance_threshold", 0.2),
max_labor_mint_fraction=st.session_state.get("cfg_max_labor_mint_fraction", 0.1),
max_subscription_mint_fraction=st.session_state.get("cfg_max_subscription_mint_fraction", 0.1),
max_staking_bonus_fraction=st.session_state.get("cfg_max_staking_bonus_fraction", 0.05),
)
def config_to_dict(config: MycoSystemConfig) -> dict:
"""Serialize a MycoSystemConfig to a JSON-safe dict."""
return {
"n_reserve_assets": config.n_reserve_assets,
"vault_names": config.vault_names,
"surface_lambdas": config.surface_lambdas.tolist() if config.surface_lambdas is not None else None,
"initial_target_weights": config.initial_target_weights.tolist() if config.initial_target_weights is not None else None,
"pamm_alpha_bar": config.pamm_params.alpha_bar,
"pamm_xu_bar": config.pamm_params.xu_bar,
"pamm_theta_bar": config.pamm_params.theta_bar,
"static_fee": config.static_fee,
"surge_fee_rate": config.surge_fee_rate,
"imbalance_threshold": config.imbalance_threshold,
"flow_memory": config.flow_memory,
"flow_threshold": config.flow_threshold,
"max_labor_mint_fraction": config.max_labor_mint_fraction,
"max_subscription_mint_fraction": config.max_subscription_mint_fraction,
"max_staking_bonus_fraction": config.max_staking_bonus_fraction,
}
def dict_to_config(d: dict) -> MycoSystemConfig:
"""Deserialize a dict into a MycoSystemConfig."""
n = d.get("n_reserve_assets", 3)
return MycoSystemConfig(
n_reserve_assets=n,
vault_names=d.get("vault_names", ["USDC", "ETH", "DAI"][:n]),
surface_lambdas=np.array(d["surface_lambdas"]) if d.get("surface_lambdas") else None,
initial_target_weights=np.array(d["initial_target_weights"]) if d.get("initial_target_weights") else None,
pamm_params=PAMMParams(
alpha_bar=d.get("pamm_alpha_bar", 10.0),
xu_bar=d.get("pamm_xu_bar", 0.8),
theta_bar=d.get("pamm_theta_bar", 0.5),
),
static_fee=d.get("static_fee", 0.003),
surge_fee_rate=d.get("surge_fee_rate", 0.05),
imbalance_threshold=d.get("imbalance_threshold", 0.2),
flow_memory=d.get("flow_memory", 0.999),
flow_threshold=d.get("flow_threshold", 0.1),
max_labor_mint_fraction=d.get("max_labor_mint_fraction", 0.1),
max_subscription_mint_fraction=d.get("max_subscription_mint_fraction", 0.1),
max_staking_bonus_fraction=d.get("max_staking_bonus_fraction", 0.05),
)
def _load_from_dict(d: dict) -> None:
"""Set session_state values from a config dict."""
n = d.get("n_reserve_assets", 3)
st.session_state["cfg_n_reserve_assets"] = n
st.session_state["cfg_vault_names"] = d.get("vault_names", ["USDC", "ETH", "DAI"][:n])
st.session_state["cfg_surface_lambdas"] = d.get("surface_lambdas", [1.0] * n)
st.session_state["cfg_initial_target_weights"] = d.get("initial_target_weights", [1.0 / n] * n)
st.session_state["cfg_pamm_alpha_bar"] = d.get("pamm_alpha_bar", 10.0)
st.session_state["cfg_pamm_xu_bar"] = d.get("pamm_xu_bar", 0.8)
st.session_state["cfg_pamm_theta_bar"] = d.get("pamm_theta_bar", 0.5)
st.session_state["cfg_static_fee"] = d.get("static_fee", 0.003)
st.session_state["cfg_surge_fee_rate"] = d.get("surge_fee_rate", 0.05)
st.session_state["cfg_imbalance_threshold"] = d.get("imbalance_threshold", 0.2)
st.session_state["cfg_flow_memory"] = d.get("flow_memory", 0.999)
st.session_state["cfg_flow_threshold"] = d.get("flow_threshold", 0.1)
st.session_state["cfg_max_labor_mint_fraction"] = d.get("max_labor_mint_fraction", 0.1)
st.session_state["cfg_max_subscription_mint_fraction"] = d.get("max_subscription_mint_fraction", 0.1)
st.session_state["cfg_max_staking_bonus_fraction"] = d.get("max_staking_bonus_fraction", 0.05)
st.session_state["cfg_active_preset"] = "Custom"
def _ensure_defaults():
"""Initialize session_state with Balanced preset if not yet set."""
if "cfg_n_reserve_assets" not in st.session_state:
_apply_preset(DEFAULT_PRESET)
def render():
st.header("System Configuration")
_ensure_defaults()
# --- Preset buttons ---
st.subheader("Presets")
cols = st.columns(len(PRESETS))
for i, name in enumerate(PRESETS):
with cols[i]:
if st.button(name, key=f"preset_{name}", use_container_width=True):
_apply_preset(name)
st.rerun()
active = st.session_state.get("cfg_active_preset", DEFAULT_PRESET)
st.caption(f"Active preset: **{active}**")
# --- Bonding Surface Geometry ---
with st.expander("Bonding Surface Geometry", expanded=True):
n = st.slider(
"Reserve assets (n)",
2, 6,
value=st.session_state.get("cfg_n_reserve_assets", 3),
key="w_n_assets",
)
# If n changed, reset per-asset lists
prev_n = st.session_state.get("cfg_n_reserve_assets", 3)
if n != prev_n:
st.session_state["cfg_n_reserve_assets"] = n
default_names = ["USDC", "ETH", "DAI", "WBTC", "SOL", "AVAX"]
st.session_state["cfg_vault_names"] = default_names[:n]
st.session_state["cfg_surface_lambdas"] = [1.0] * n
st.session_state["cfg_initial_target_weights"] = [1.0 / n] * n
st.session_state["cfg_active_preset"] = "Custom"
st.rerun()
st.session_state["cfg_n_reserve_assets"] = n
# Vault names
st.markdown("**Vault names**")
vault_names = st.session_state.get("cfg_vault_names", ["USDC", "ETH", "DAI"][:n])
name_cols = st.columns(n)
new_names = []
for i in range(n):
with name_cols[i]:
val = st.text_input(
f"Asset {i+1}",
value=vault_names[i] if i < len(vault_names) else f"Asset{i+1}",
key=f"w_vault_{i}",
)
new_names.append(val)
st.session_state["cfg_vault_names"] = new_names
# Surface lambdas
st.markdown("**Surface lambdas**")
lambdas = st.session_state.get("cfg_surface_lambdas", [1.0] * n)
lam_cols = st.columns(n)
new_lams = []
for i in range(n):
with lam_cols[i]:
val = st.slider(
f"\u03bb_{new_names[i]}",
1.0, 10.0,
value=float(lambdas[i]) if i < len(lambdas) else 1.0,
step=0.5,
key=f"w_lambda_{i}",
)
new_lams.append(val)
st.session_state["cfg_surface_lambdas"] = new_lams
# Target weights
st.markdown("**Initial target weights** (auto-normalized)")
weights = st.session_state.get("cfg_initial_target_weights", [1.0 / n] * n)
wt_cols = st.columns(n)
raw_weights = []
for i in range(n):
with wt_cols[i]:
val = st.slider(
f"w_{new_names[i]}",
0.01, 1.0,
value=float(weights[i]) if i < len(weights) else 1.0 / n,
step=0.01,
key=f"w_weight_{i}",
)
raw_weights.append(val)
total_w = sum(raw_weights)
normalized = [w / total_w for w in raw_weights]
st.session_state["cfg_initial_target_weights"] = normalized
st.caption(f"Normalized: {', '.join(f'{w:.2%}' for w in normalized)}")
# --- P-AMM Redemption Curve ---
with st.expander("P-AMM Redemption Curve"):
c1, c2, c3 = st.columns(3)
with c1:
alpha = st.slider(
"\u0101\u0304 (alpha_bar) — discount curvature",
1.0, 100.0,
value=st.session_state.get("cfg_pamm_alpha_bar", 10.0),
step=1.0,
key="w_alpha_bar",
)
st.session_state["cfg_pamm_alpha_bar"] = alpha
with c2:
xu = st.slider(
"x\u0304_U (xu_bar) — no-discount threshold",
0.0, 1.0,
value=st.session_state.get("cfg_pamm_xu_bar", 0.8),
step=0.05,
key="w_xu_bar",
)
st.session_state["cfg_pamm_xu_bar"] = xu
with c3:
theta = st.slider(
"\u03b8\u0304 (theta_bar) — floor redemption rate",
0.0, 1.0,
value=st.session_state.get("cfg_pamm_theta_bar", 0.5),
step=0.05,
key="w_theta_bar",
)
st.session_state["cfg_pamm_theta_bar"] = theta
# --- Fees & Flow Dampening ---
with st.expander("Fees & Flow Dampening"):
c1, c2 = st.columns(2)
with c1:
sf = st.slider(
"static_fee",
0.0, 0.05,
value=st.session_state.get("cfg_static_fee", 0.003),
step=0.001,
format="%.3f",
key="w_static_fee",
)
st.session_state["cfg_static_fee"] = sf
sr = st.slider(
"surge_fee_rate",
0.0, 0.5,
value=st.session_state.get("cfg_surge_fee_rate", 0.05),
step=0.01,
key="w_surge_fee",
)
st.session_state["cfg_surge_fee_rate"] = sr
it = st.slider(
"imbalance_threshold",
0.0, 0.5,
value=st.session_state.get("cfg_imbalance_threshold", 0.2),
step=0.05,
key="w_imbalance_thresh",
)
st.session_state["cfg_imbalance_threshold"] = it
with c2:
fm = st.slider(
"flow_memory",
0.9, 0.9999,
value=st.session_state.get("cfg_flow_memory", 0.999),
step=0.0001,
format="%.4f",
key="w_flow_memory",
)
st.session_state["cfg_flow_memory"] = fm
ft = st.slider(
"flow_threshold",
0.01, 0.5,
value=st.session_state.get("cfg_flow_threshold", 0.1),
step=0.01,
key="w_flow_threshold",
)
st.session_state["cfg_flow_threshold"] = ft
# --- Commitment Caps ---
with st.expander("Commitment Caps"):
c1, c2, c3 = st.columns(3)
with c1:
lm = st.slider(
"max_labor_mint_fraction",
0.0, 0.3,
value=st.session_state.get("cfg_max_labor_mint_fraction", 0.1),
step=0.01,
key="w_labor_cap",
)
st.session_state["cfg_max_labor_mint_fraction"] = lm
with c2:
sm = st.slider(
"max_subscription_mint_fraction",
0.0, 0.3,
value=st.session_state.get("cfg_max_subscription_mint_fraction", 0.1),
step=0.01,
key="w_sub_cap",
)
st.session_state["cfg_max_subscription_mint_fraction"] = sm
with c3:
sb = st.slider(
"max_staking_bonus_fraction",
0.0, 0.15,
value=st.session_state.get("cfg_max_staking_bonus_fraction", 0.05),
step=0.01,
key="w_staking_cap",
)
st.session_state["cfg_max_staking_bonus_fraction"] = sb
# --- Build and store config ---
config = _build_config()
st.session_state["myco_config"] = config
# --- JSON Export / Import ---
st.divider()
exp_col, imp_col = st.columns(2)
with exp_col:
st.subheader("Export Config")
config_json = json.dumps(config_to_dict(config), indent=2)
st.download_button(
"Download JSON",
data=config_json,
file_name="myco_config.json",
mime="application/json",
)
with imp_col:
st.subheader("Import Config")
uploaded = st.file_uploader("Upload JSON", type=["json"], key="cfg_upload")
if uploaded is not None:
try:
d = json.loads(uploaded.read())
_load_from_dict(d)
st.success("Config loaded successfully!")
st.rerun()
except (json.JSONDecodeError, KeyError) as e:
st.error(f"Invalid config file: {e}")
# --- Config Summary ---
st.divider()
st.subheader("Active Configuration Summary")
n = config.n_reserve_assets
names = ", ".join(config.vault_names[:n])
st.markdown(
f"**{n} assets** ({names}) | "
f"P-AMM: \u0101={config.pamm_params.alpha_bar:.1f}, "
f"x\u0304_U={config.pamm_params.xu_bar:.2f}, "
f"\u03b8\u0304={config.pamm_params.theta_bar:.2f} | "
f"Fees: static={config.static_fee:.3f}, surge={config.surge_fee_rate:.2f} | "
f"Flow: mem={config.flow_memory:.4f}, thresh={config.flow_threshold:.2f} | "
f"Caps: labor={config.max_labor_mint_fraction:.0%}, "
f"sub={config.max_subscription_mint_fraction:.0%}, "
f"stake={config.max_staking_bonus_fraction:.0%}"
)