368 lines
15 KiB
Python
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%}"
|
|
)
|