feat: add System Config tab, presets, JSON export/import, and Dockerize for simulate.rspace.online

- Add System Config tab with ~15 parameter sliders grouped in accordions
- Add 4 named presets (Conservative, Balanced, Aggressive, High-Risk)
- Add JSON config export/import for sharing configurations
- All tabs now read config from session_state instead of hardcoding
- Signal Router uses config's fee/flow params as base AdaptiveParams
- Multi-stage Dockerfile (Python 3.12-slim, non-root, healthcheck)
- docker-compose.yml with Traefik labels for simulate.rspace.online

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-01 14:55:14 -07:00
parent 1cb3701769
commit d9dec8ba5b
10 changed files with 542 additions and 15 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
.git
tests/
notebooks/
docs/
reference/
*.pyc
__pycache__
.pytest_cache
.claude/

23
Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM python:3.12-slim AS builder
WORKDIR /build
COPY pyproject.toml .
COPY src/ src/
COPY dashboard/ dashboard/
RUN pip install --no-cache-dir ".[dashboard]"
FROM python:3.12-slim
WORKDIR /app
RUN useradd --create-home appuser
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY src/ src/
COPY dashboard/ dashboard/
COPY pyproject.toml .
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8501
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8501/_stcore/health')"
ENTRYPOINT ["streamlit", "run", "dashboard/app.py", \
"--server.port=8501", "--server.address=0.0.0.0", \
"--server.headless=true", "--browser.gatherUsageStats=false"]

View File

@ -14,7 +14,13 @@ st.set_page_config(
st.title("MYCO Bonding Surface Dashboard") st.title("MYCO Bonding Surface Dashboard")
st.caption("Interactive simulations for the multi-asset bonding curve with CRDT-native primitives.") st.caption("Interactive simulations for the multi-asset bonding curve with CRDT-native primitives.")
tab_launch, tab_dca, tab_signal, tab_stress, tab_crdt = st.tabs([ # Initialize default config in session_state if not present
if "myco_config" not in st.session_state:
from src.composed.myco_surface import MycoSystemConfig
st.session_state["myco_config"] = MycoSystemConfig()
tab_config, tab_launch, tab_dca, tab_signal, tab_stress, tab_crdt = st.tabs([
"System Config",
"Token Launch", "Token Launch",
"DCA Explorer", "DCA Explorer",
"Signal Router", "Signal Router",
@ -22,7 +28,10 @@ tab_launch, tab_dca, tab_signal, tab_stress, tab_crdt = st.tabs([
"CRDT Flow", "CRDT Flow",
]) ])
from dashboard.tabs import token_launch, dca_explorer, signal_router, stress_tests, crdt_flow from dashboard.tabs import system_config, token_launch, dca_explorer, signal_router, stress_tests, crdt_flow
with tab_config:
system_config.render()
with tab_launch: with tab_launch:
token_launch.render() token_launch.render()

74
dashboard/presets.py Normal file
View File

@ -0,0 +1,74 @@
"""Named preset configurations for the MYCO system."""
PRESETS: dict[str, dict] = {
"Conservative": {
"n_reserve_assets": 3,
"vault_names": ["USDC", "ETH", "DAI"],
"surface_lambdas": [1.0, 1.0, 1.0],
"initial_target_weights": [0.5, 0.25, 0.25],
"pamm_alpha_bar": 5.0,
"pamm_xu_bar": 0.9,
"pamm_theta_bar": 0.7,
"static_fee": 0.005,
"surge_fee_rate": 0.02,
"imbalance_threshold": 0.2,
"flow_memory": 0.999,
"flow_threshold": 0.05,
"max_labor_mint_fraction": 0.05,
"max_subscription_mint_fraction": 0.05,
"max_staking_bonus_fraction": 0.03,
},
"Balanced": {
"n_reserve_assets": 3,
"vault_names": ["USDC", "ETH", "DAI"],
"surface_lambdas": [1.0, 1.0, 1.0],
"initial_target_weights": [0.34, 0.33, 0.33],
"pamm_alpha_bar": 10.0,
"pamm_xu_bar": 0.8,
"pamm_theta_bar": 0.5,
"static_fee": 0.003,
"surge_fee_rate": 0.05,
"imbalance_threshold": 0.2,
"flow_memory": 0.999,
"flow_threshold": 0.10,
"max_labor_mint_fraction": 0.10,
"max_subscription_mint_fraction": 0.10,
"max_staking_bonus_fraction": 0.05,
},
"Aggressive": {
"n_reserve_assets": 3,
"vault_names": ["USDC", "ETH", "DAI"],
"surface_lambdas": [1.0, 1.0, 1.0],
"initial_target_weights": [0.34, 0.33, 0.33],
"pamm_alpha_bar": 20.0,
"pamm_xu_bar": 0.6,
"pamm_theta_bar": 0.3,
"static_fee": 0.001,
"surge_fee_rate": 0.10,
"imbalance_threshold": 0.2,
"flow_memory": 0.999,
"flow_threshold": 0.20,
"max_labor_mint_fraction": 0.15,
"max_subscription_mint_fraction": 0.15,
"max_staking_bonus_fraction": 0.08,
},
"High-Risk": {
"n_reserve_assets": 3,
"vault_names": ["USDC", "ETH", "DAI"],
"surface_lambdas": [1.0, 1.0, 1.0],
"initial_target_weights": [0.34, 0.33, 0.33],
"pamm_alpha_bar": 50.0,
"pamm_xu_bar": 0.4,
"pamm_theta_bar": 0.1,
"static_fee": 0.0,
"surge_fee_rate": 0.20,
"imbalance_threshold": 0.2,
"flow_memory": 0.999,
"flow_threshold": 0.30,
"max_labor_mint_fraction": 0.20,
"max_subscription_mint_fraction": 0.20,
"max_staking_bonus_fraction": 0.10,
},
}
DEFAULT_PRESET = "Balanced"

View File

@ -28,6 +28,9 @@ def render():
def _render_comparison(): def _render_comparison():
config = st.session_state.get("myco_config")
n_assets = config.n_reserve_assets if config else 3
col1, col2, col3 = st.columns(3) col1, col2, col3 = st.columns(3)
with col1: with col1:
total = st.number_input("Total amount ($)", 1000, 100_000, 10_000, step=1000, key="dca_total") total = st.number_input("Total amount ($)", 1000, 100_000, 10_000, step=1000, key="dca_total")
@ -39,6 +42,7 @@ def _render_comparison():
if st.button("Compare Strategies", key="dca_compare_run"): if st.button("Compare Strategies", key="dca_compare_run"):
with st.spinner("Running DCA comparison..."): with st.spinner("Running DCA comparison..."):
results = scenario_dca_comparison( results = scenario_dca_comparison(
n_assets=n_assets,
total_amount=float(total), total_amount=float(total),
n_chunks=chunks, n_chunks=chunks,
interval=float(interval), interval=float(interval),
@ -60,6 +64,8 @@ def _render_comparison():
def _render_subscription(): def _render_subscription():
config = st.session_state.get("myco_config", MycoSystemConfig())
col1, col2, col3 = st.columns(3) col1, col2, col3 = st.columns(3)
with col1: with col1:
tier_payment = st.number_input("Payment/period ($)", 50, 5000, 100, step=50, key="sub_payment") tier_payment = st.number_input("Payment/period ($)", 50, 5000, 100, step=50, key="sub_payment")
@ -78,13 +84,12 @@ def _render_subscription():
loyalty_halflife=90.0, loyalty_halflife=90.0,
), ),
} }
config = SubscriptionDCAConfig(n_chunks=n_chunks, spread_fraction=0.8) dca_config = SubscriptionDCAConfig(n_chunks=n_chunks, spread_fraction=0.8)
sys_config = MycoSystemConfig(n_reserve_assets=3)
subscribers = [("subscriber_1", "custom", 0.0)] subscribers = [("subscriber_1", "custom", 0.0)]
with st.spinner("Running subscription DCA..."): with st.spinner("Running subscription DCA..."):
sim = simulate_subscription_dca( sim = simulate_subscription_dca(
tiers=tiers, config=config, system_config=sys_config, tiers=tiers, config=dca_config, system_config=config,
subscribers=subscribers, duration=float(duration), dt=1.0, subscribers=subscribers, duration=float(duration), dt=1.0,
) )
st.session_state["sub_dca_result"] = sim st.session_state["sub_dca_result"] = sim

View File

@ -15,6 +15,8 @@ def render():
st.header("Signal Router") st.header("Signal Router")
st.caption("Parameters update live — no button needed (sub-ms computation).") st.caption("Parameters update live — no button needed (sub-ms computation).")
config = st.session_state.get("myco_config")
col1, col2 = st.columns(2) col1, col2 = st.columns(2)
with col1: with col1:
trajectory = st.selectbox( trajectory = st.selectbox(
@ -39,20 +41,25 @@ def render():
} }
prices = traj_map[trajectory]() prices = traj_map[trajectory]()
# Use config's fee/flow params as base adaptive params
base_flow = config.flow_threshold if config else 0.1
base_alpha = config.pamm_params.alpha_bar if config else 10.0
base_surge = config.surge_fee_rate if config else 0.05
base = AdaptiveParams( base = AdaptiveParams(
flow_threshold=0.1, flow_threshold=base_flow,
pamm_alpha_bar=10.0, pamm_alpha_bar=base_alpha,
surge_fee_rate=0.05, surge_fee_rate=base_surge,
oracle_multiplier_velocity=0.0, oracle_multiplier_velocity=0.0,
) )
config = SignalRouterConfig( router_config = SignalRouterConfig(
k_vol_flow=k_vol_flow, k_vol_flow=k_vol_flow,
k_dev_alpha=k_dev_alpha, k_dev_alpha=k_dev_alpha,
k_vol_fee=k_vol_fee, k_vol_fee=k_vol_fee,
k_oracle_vel=k_oracle_vel, k_oracle_vel=k_oracle_vel,
) )
result = simulate_signal_routing(base, config, prices) result = simulate_signal_routing(base, router_config, prices)
result["prices"] = prices # Add for chart result["prices"] = prices # Add for chart
st.plotly_chart(fig_signal_routing(result), use_container_width=True) st.plotly_chart(fig_signal_routing(result), use_container_width=True)

View File

@ -9,6 +9,9 @@ from dashboard.charts import fig_bank_run_sweep
def render(): def render():
st.header("Stress Tests") st.header("Stress Tests")
config = st.session_state.get("myco_config")
n_assets = config.n_reserve_assets if config else 3
col1, col2 = st.columns(2) col1, col2 = st.columns(2)
with col1: with col1:
fractions_str = st.text_input( fractions_str = st.text_input(
@ -20,6 +23,8 @@ def render():
"Initial reserve ($)", 10_000, 1_000_000, 100_000, step=10_000, "Initial reserve ($)", 10_000, 1_000_000, 100_000, step=10_000,
) )
st.caption(f"Using **{n_assets}** reserve assets from System Config.")
if st.button("Run Stress Test", key="stress_run"): if st.button("Run Stress Test", key="stress_run"):
fractions = [float(f.strip()) for f in fractions_str.split(",")] fractions = [float(f.strip()) for f in fractions_str.split(",")]
results = {} results = {}
@ -29,6 +34,7 @@ def render():
with st.spinner(f"Running {frac:.0%} redemption..."): with st.spinner(f"Running {frac:.0%} redemption..."):
results[frac] = scenario_bank_run( results[frac] = scenario_bank_run(
initial_reserve=float(initial_reserve), initial_reserve=float(initial_reserve),
n_assets=n_assets,
redemption_fraction=frac, redemption_fraction=frac,
) )
progress.progress((i + 1) / len(fractions)) progress.progress((i + 1) / len(fractions))

View File

@ -0,0 +1,367 @@
"""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%}"
)

View File

@ -9,16 +9,19 @@ from dashboard.charts import fig_simulation_overview
def render(): def render():
st.header("Token Launch Simulation") st.header("Token Launch Simulation")
col1, col2, col3, col4 = st.columns(4) config = st.session_state.get("myco_config")
n_assets = config.n_reserve_assets if config else 3
col1, col2, col3 = st.columns(3)
with col1: with col1:
n_assets = st.slider("Reserve assets", 2, 6, 3)
with col2:
n_depositors = st.slider("Depositors", 10, 200, 50) n_depositors = st.slider("Depositors", 10, 200, 50)
with col3: with col2:
total_raise = st.number_input("Total raise ($)", 10_000, 1_000_000, 100_000, step=10_000) total_raise = st.number_input("Total raise ($)", 10_000, 1_000_000, 100_000, step=10_000)
with col4: with col3:
duration = st.slider("Duration (days)", 30, 365, 90) duration = st.slider("Duration (days)", 30, 365, 90)
st.caption(f"Using **{n_assets}** reserve assets from System Config.")
if st.button("Run Simulation", key="token_launch_run"): if st.button("Run Simulation", key="token_launch_run"):
with st.spinner("Running token launch simulation..."): with st.spinner("Running token launch simulation..."):
result = scenario_token_launch( result = scenario_token_launch(

24
docker-compose.yml Normal file
View File

@ -0,0 +1,24 @@
services:
myco-dashboard:
build: .
container_name: myco-dashboard
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.myco-dashboard.rule=Host(`simulate.rspace.online`)"
- "traefik.http.routers.myco-dashboard.entrypoints=web"
- "traefik.http.services.myco-dashboard.loadbalancer.server.port=8501"
- "traefik.http.services.myco-dashboard.loadbalancer.healthcheck.path=/_stcore/health"
- "traefik.http.services.myco-dashboard.loadbalancer.healthcheck.interval=30s"
networks:
- traefik-public
deploy:
resources:
limits:
memory: 2G
reservations:
memory: 512M
networks:
traefik-public:
external: true