From d9dec8ba5b285bf2bc7fc2fa418b536a05d8e641 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 1 Apr 2026 14:55:14 -0700 Subject: [PATCH] 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 --- .dockerignore | 9 + Dockerfile | 23 ++ dashboard/app.py | 13 +- dashboard/presets.py | 74 +++++++ dashboard/tabs/dca_explorer.py | 11 +- dashboard/tabs/signal_router.py | 17 +- dashboard/tabs/stress_tests.py | 6 + dashboard/tabs/system_config.py | 367 ++++++++++++++++++++++++++++++++ dashboard/tabs/token_launch.py | 13 +- docker-compose.yml | 24 +++ 10 files changed, 542 insertions(+), 15 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 dashboard/presets.py create mode 100644 dashboard/tabs/system_config.py create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f83a303 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +tests/ +notebooks/ +docs/ +reference/ +*.pyc +__pycache__ +.pytest_cache +.claude/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..26f0d89 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/dashboard/app.py b/dashboard/app.py index 46641ef..b5d6171 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -14,7 +14,13 @@ st.set_page_config( st.title("MYCO Bonding Surface Dashboard") 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", "DCA Explorer", "Signal Router", @@ -22,7 +28,10 @@ tab_launch, tab_dca, tab_signal, tab_stress, tab_crdt = st.tabs([ "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: token_launch.render() diff --git a/dashboard/presets.py b/dashboard/presets.py new file mode 100644 index 0000000..d463abc --- /dev/null +++ b/dashboard/presets.py @@ -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" diff --git a/dashboard/tabs/dca_explorer.py b/dashboard/tabs/dca_explorer.py index f078774..08edc08 100644 --- a/dashboard/tabs/dca_explorer.py +++ b/dashboard/tabs/dca_explorer.py @@ -28,6 +28,9 @@ def render(): 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) with col1: 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"): with st.spinner("Running DCA comparison..."): results = scenario_dca_comparison( + n_assets=n_assets, total_amount=float(total), n_chunks=chunks, interval=float(interval), @@ -60,6 +64,8 @@ def _render_comparison(): def _render_subscription(): + config = st.session_state.get("myco_config", MycoSystemConfig()) + col1, col2, col3 = st.columns(3) with col1: 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, ), } - config = SubscriptionDCAConfig(n_chunks=n_chunks, spread_fraction=0.8) - sys_config = MycoSystemConfig(n_reserve_assets=3) + dca_config = SubscriptionDCAConfig(n_chunks=n_chunks, spread_fraction=0.8) subscribers = [("subscriber_1", "custom", 0.0)] with st.spinner("Running 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, ) st.session_state["sub_dca_result"] = sim diff --git a/dashboard/tabs/signal_router.py b/dashboard/tabs/signal_router.py index d4abffc..22e878f 100644 --- a/dashboard/tabs/signal_router.py +++ b/dashboard/tabs/signal_router.py @@ -15,6 +15,8 @@ def render(): st.header("Signal Router") st.caption("Parameters update live — no button needed (sub-ms computation).") + config = st.session_state.get("myco_config") + col1, col2 = st.columns(2) with col1: trajectory = st.selectbox( @@ -39,20 +41,25 @@ def render(): } 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( - flow_threshold=0.1, - pamm_alpha_bar=10.0, - surge_fee_rate=0.05, + flow_threshold=base_flow, + pamm_alpha_bar=base_alpha, + surge_fee_rate=base_surge, oracle_multiplier_velocity=0.0, ) - config = SignalRouterConfig( + router_config = SignalRouterConfig( k_vol_flow=k_vol_flow, k_dev_alpha=k_dev_alpha, k_vol_fee=k_vol_fee, 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 st.plotly_chart(fig_signal_routing(result), use_container_width=True) diff --git a/dashboard/tabs/stress_tests.py b/dashboard/tabs/stress_tests.py index a4338bd..d8f7337 100644 --- a/dashboard/tabs/stress_tests.py +++ b/dashboard/tabs/stress_tests.py @@ -9,6 +9,9 @@ from dashboard.charts import fig_bank_run_sweep def render(): 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) with col1: fractions_str = st.text_input( @@ -20,6 +23,8 @@ def render(): "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"): fractions = [float(f.strip()) for f in fractions_str.split(",")] results = {} @@ -29,6 +34,7 @@ def render(): with st.spinner(f"Running {frac:.0%} redemption..."): results[frac] = scenario_bank_run( initial_reserve=float(initial_reserve), + n_assets=n_assets, redemption_fraction=frac, ) progress.progress((i + 1) / len(fractions)) diff --git a/dashboard/tabs/system_config.py b/dashboard/tabs/system_config.py new file mode 100644 index 0000000..7691eb4 --- /dev/null +++ b/dashboard/tabs/system_config.py @@ -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%}" + ) diff --git a/dashboard/tabs/token_launch.py b/dashboard/tabs/token_launch.py index cf6fbf0..6d21d16 100644 --- a/dashboard/tabs/token_launch.py +++ b/dashboard/tabs/token_launch.py @@ -9,16 +9,19 @@ from dashboard.charts import fig_simulation_overview def render(): 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: - n_assets = st.slider("Reserve assets", 2, 6, 3) - with col2: 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) - with col4: + with col3: 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"): with st.spinner("Running token launch simulation..."): result = scenario_token_launch( diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d3af12e --- /dev/null +++ b/docker-compose.yml @@ -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