From 818e864d2bf6f3aac2d2b80b36633557d2d39be0 Mon Sep 17 00:00:00 2001 From: defiQUG Date: Mon, 27 Apr 2026 11:26:55 -0700 Subject: [PATCH] Add capital efficiency risk simulation --- README.md | 1 + config/capital-efficiency-policy.json | 92 ++++ config/scenario-schema.json | 63 +++ .../scenarios/bank_run_redemption_spike.json | 38 ++ .../chain138_deployed_capital_efficiency.json | 31 ++ .../scenarios/crash_40pct_external_asset.json | 35 ++ config/scenarios/high_vol_sigma_spike.json | 37 ++ config/scenarios/leverage_sweep_1x_to_4x.json | 43 ++ config/scorecard-schema.json | 32 +- docs/12-sim-scorecard.md | 20 + docs/16-capital-efficiency-risk-simulation.md | 144 ++++++ ...ital-efficiency-contract-blueprint-gate.md | 28 ++ scripts/README.md | 24 + scripts/run-scenario.cjs | 434 +++++++++++++++++- scripts/validate-capital-efficiency.cjs | 102 ++++ 15 files changed, 1111 insertions(+), 13 deletions(-) create mode 100644 config/capital-efficiency-policy.json create mode 100644 config/scenarios/bank_run_redemption_spike.json create mode 100644 config/scenarios/chain138_deployed_capital_efficiency.json create mode 100644 config/scenarios/crash_40pct_external_asset.json create mode 100644 config/scenarios/high_vol_sigma_spike.json create mode 100644 config/scenarios/leverage_sweep_1x_to_4x.json create mode 100644 docs/16-capital-efficiency-risk-simulation.md create mode 100644 docs/17-capital-efficiency-contract-blueprint-gate.md create mode 100644 scripts/validate-capital-efficiency.cjs diff --git a/README.md b/README.md index fd15022..581adfa 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Implementation-grade blueprint for the **home-minted M1 suite on ChainID 138**, 8. **Sim scorecard:** Simulator output contract and pass/fail gates: [docs/12-sim-scorecard.md](docs/12-sim-scorecard.md), [config/scorecard-schema.json](config/scorecard-schema.json). Bridge edge with optional **latency risk** ρ(Δt): [docs/08-simulation-model.md](docs/08-simulation-model.md) §2. 9. **Scenario contract:** Formal scenario input schema and three Phase 0 scenarios: [config/scenario-schema.json](config/scenario-schema.json), [config/scenarios/](config/scenarios/). **Routing exposure controls:** [config/routing-controls.json](config/routing-controls.json) (maxTradeSize, cooldown, minImprovementBps, publicRoutingEnabled). **Real sim (PR#1):** `node scripts/run-scenario.cjs hub_only_11` — graph from configs, PMM state, path enumeration + waterfilling, real scorecard (drain half-life, path concentration, capture, churn); see [spec/13-minimal-router-sim.md](spec/13-minimal-router-sim.md). 10. **USD-wrapper support lane:** Gas-budgeted micro-trade policy for `cWUSDC` / `cWUSDT` and a runnable scenario: [docs/15-gas-budgeted-micro-trade-support.md](docs/15-gas-budgeted-micro-trade-support.md), [config/scenarios/micro_support_usd_wrappers_1_56_137.json](config/scenarios/micro_support_usd_wrappers_1_56_137.json). +11. **Capital efficiency risk simulation:** Simulation-only Monte Carlo overlay for treasury allocation, leverage, peg pressure, volatility throttles, liquidation probability, and parameter optimization: [docs/16-capital-efficiency-risk-simulation.md](docs/16-capital-efficiency-risk-simulation.md), [docs/17-capital-efficiency-contract-blueprint-gate.md](docs/17-capital-efficiency-contract-blueprint-gate.md), [config/capital-efficiency-policy.json](config/capital-efficiency-policy.json), stress scenarios under [config/scenarios/](config/scenarios/), and CI-style validation via `node scripts/validate-capital-efficiency.cjs`. ## Parent repo diff --git a/config/capital-efficiency-policy.json b/config/capital-efficiency-policy.json new file mode 100644 index 0000000..b9a6e44 --- /dev/null +++ b/config/capital-efficiency-policy.json @@ -0,0 +1,92 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Simulation-only capital efficiency policy for Chain 138/cW PMM treasury risk modeling. This is not a live leverage or mint/redemption contract configuration.", + "version": "1.0.0", + "updated": "2026-04-27", + "defaults": { + "paths": 1000, + "epochs": 365, + "seed": 138001, + "initialCapital": 1000000, + "alpha": 0.7, + "leverage": 2, + "spreadBps": 35, + "volumeEfficiency": 2, + "pmmK": 0.1, + "liquidityTargetUnits": 1000000 + }, + "parameterBands": { + "alphaBalanced": [0.65, 0.75], + "alphaYieldDominant": [0.75, 0.9], + "leverageSafe": [1, 1], + "leverageTarget": [2, 3], + "leverageUnstableAbove": 4, + "spreadCompetitiveBps": 10, + "spreadTargetBps": [30, 50], + "spreadVolumeRiskCeilingBps": 100, + "volumeEfficiencyPassive": 1, + "volumeEfficiencyActive": [2, 4], + "volumeEfficiencyHighlyOptimizedAbove": 5 + }, + "treasury": { + "yieldRatePerEpoch": 0.00035, + "volatilityDragLambda": 0.35, + "marketMakingScale": 0.04 + }, + "volatilityProcess": { + "sigma0": 0.03, + "sigmaBar": 0.03, + "kappa": 0.08, + "eta": 0.01 + }, + "pegDynamics": { + "p0": 1, + "beta": 0.25, + "imbalanceStd": 0.01, + "arbLiquidityCoefficient": 0.65 + }, + "risk": { + "minExternalLiquidityPct": 0.2, + "maxLtvBps": 6500, + "hardMaxLtvBps": 7500, + "liquidationLtvBps": 8000, + "hardMaxLeverage": 4, + "liquidationLossFraction": 0.08, + "sigmaCrit": 0.08, + "throttleLeverageMultiplier": 0.7, + "throttleAlphaMultiplier": 0.9, + "throttleSpreadBps": 50, + "maxSpreadBps": 100, + "circuitBreakerSpreadBps": 100, + "pegCircuitBreakerBps": 200, + "collateralVolatilityHaircut": 0.5, + "bankRunRedemptionFeeBps": 100 + }, + "gates": { + "maxDeployableLeverage": 3, + "maxLiquidationProbability": 0.02, + "maxDrawdownP95": 0.25, + "maxPegDeviationFrequency": 0.05, + "maxExternalLiquidityFloorViolations": 0 + }, + "optimizer": { + "paths": 250, + "maxCandidates": 250, + "topN": 10 + }, + "liveExecutionGuard": { + "status": "simulation_only", + "requiresBeforeContractWork": [ + "smart_contract_audit_engagement", + "governance_approval", + "risk_dashboard", + "operator_runbook", + "treasury_liquidity_commitments" + ], + "auditEngagementEvidence": null, + "governanceApprovalEvidence": null, + "riskDashboardEvidence": null, + "operatorRunbookEvidence": null, + "treasuryLiquidityCommitmentEvidence": null + } +} diff --git a/config/scenario-schema.json b/config/scenario-schema.json index 0205410..f172099 100644 --- a/config/scenario-schema.json +++ b/config/scenario-schema.json @@ -213,6 +213,69 @@ "seed": { "type": "integer", "description": "Optional RNG seed for deterministic runs; if omitted, derived from scenario name" + }, + "capitalEfficiency": { + "type": "object", + "description": "Optional simulation-only treasury/risk Monte Carlo overlay. Does not configure live contracts.", + "properties": { + "enabled": { "type": "boolean", "default": false }, + "paths": { "type": "integer", "minimum": 1 }, + "epochs": { "type": "integer", "minimum": 1 }, + "seed": { "type": "integer" }, + "initialCapital": { "type": "number", "minimum": 0 }, + "alpha": { "type": "number", "minimum": 0, "maximum": 1 }, + "leverage": { "type": "number", "minimum": 1 }, + "spreadBps": { "type": "number", "minimum": 0 }, + "volumeEfficiency": { "type": "number", "minimum": 0 }, + "pmmK": { "type": "number", "minimum": 0 }, + "liquidityTargetUnits": { "type": "number", "minimum": 0 }, + "treasury": { "type": "object" }, + "volatilityProcess": { + "type": "object", + "properties": { + "sigma0": { "type": "number", "minimum": 0 }, + "sigmaBar": { "type": "number", "minimum": 0 }, + "kappa": { "type": "number", "minimum": 0 }, + "eta": { "type": "number", "minimum": 0 } + } + }, + "pegDynamics": { + "type": "object", + "properties": { + "p0": { "type": "number" }, + "beta": { "type": "number" }, + "imbalanceStd": { "type": "number", "minimum": 0 }, + "arbLiquidityCoefficient": { "type": "number", "minimum": 0 } + } + }, + "risk": { "type": "object" }, + "stress": { + "type": "object", + "properties": { + "preset": { + "type": "string", + "enum": ["crash_40pct_external_asset", "high_vol_sigma_spike", "bank_run_redemption_spike", "bridge_shock"] + }, + "epoch": { "type": "integer", "minimum": 0 }, + "durationEpochs": { "type": "integer", "minimum": 1 }, + "externalAssetReturn": { "type": "number" }, + "sigmaAdd": { "type": "number" }, + "redemptionFraction": { "type": "number", "minimum": 0 }, + "imbalanceAdd": { "type": "number" }, + "events": { "type": "array", "items": { "type": "object" } } + } + }, + "optimizer": { + "type": "object", + "properties": { + "enabled": { "type": "boolean", "default": false }, + "paths": { "type": "integer", "minimum": 1 }, + "topN": { "type": "integer", "minimum": 1 }, + "maxCandidates": { "type": "integer", "minimum": 1 }, + "grid": { "type": "object" } + } + } + } } } } diff --git a/config/scenarios/bank_run_redemption_spike.json b/config/scenarios/bank_run_redemption_spike.json new file mode 100644 index 0000000..4f1ed16 --- /dev/null +++ b/config/scenarios/bank_run_redemption_spike.json @@ -0,0 +1,38 @@ +{ + "scenario": "bank_run_redemption_spike", + "graphMode": "design", + "topology": "hub", + "chainsIncluded": ["1", "56", "137"], + "tokensIncluded": ["cWUSDT", "cWUSDC"], + "epochBlocks": 300, + "epochs": 180, + "orderflowModel": { + "distribution": "pareto", + "volumeMinUnits": 2500, + "volumeMaxUnits": 80000, + "tradesPerEpoch": 25, + "paretoAlpha": 1.8 + }, + "oracleModel": "static", + "latencyModel": { + "finalityBlocksPerChainPair": {}, + "rhoPerBlockBps": 0.5 + }, + "capitalEfficiency": { + "enabled": true, + "paths": 1000, + "epochs": 180, + "seed": 138403, + "initialCapital": 1000000, + "alpha": 0.68, + "leverage": 2, + "spreadBps": 45, + "volumeEfficiency": 2.5, + "stress": { + "preset": "bank_run_redemption_spike", + "epoch": 30, + "durationEpochs": 12, + "redemptionFraction": 0.25 + } + } +} diff --git a/config/scenarios/chain138_deployed_capital_efficiency.json b/config/scenarios/chain138_deployed_capital_efficiency.json new file mode 100644 index 0000000..c42f3da --- /dev/null +++ b/config/scenarios/chain138_deployed_capital_efficiency.json @@ -0,0 +1,31 @@ +{ + "scenario": "chain138_deployed_capital_efficiency", + "graphMode": "deployed", + "topology": "hub", + "chainsIncluded": ["138"], + "tokensIncluded": ["cUSDT", "cUSDC", "cEURC", "cEURT", "cXAUC", "cXAUT"], + "epochBlocks": 300, + "epochs": 120, + "orderflowModel": { + "distribution": "uniform", + "volumeMinUnits": 1000, + "volumeMaxUnits": 25000, + "tradesPerEpoch": 12 + }, + "oracleModel": "static", + "latencyModel": { + "finalityBlocksPerChainPair": {}, + "rhoPerBlockBps": 0.5 + }, + "capitalEfficiency": { + "enabled": true, + "paths": 500, + "epochs": 120, + "seed": 138405, + "initialCapital": 1000000, + "alpha": 0.7, + "leverage": 2, + "spreadBps": 35, + "volumeEfficiency": 2 + } +} diff --git a/config/scenarios/crash_40pct_external_asset.json b/config/scenarios/crash_40pct_external_asset.json new file mode 100644 index 0000000..011d3bd --- /dev/null +++ b/config/scenarios/crash_40pct_external_asset.json @@ -0,0 +1,35 @@ +{ + "scenario": "crash_40pct_external_asset", + "graphMode": "design", + "topology": "hub", + "chainsIncluded": ["1", "56", "137"], + "tokensIncluded": ["cWUSDT", "cWUSDC"], + "epochBlocks": 300, + "epochs": 180, + "orderflowModel": { + "distribution": "uniform", + "volumeMinUnits": 5000, + "volumeMaxUnits": 30000, + "tradesPerEpoch": 15 + }, + "oracleModel": "static", + "latencyModel": { + "finalityBlocksPerChainPair": {}, + "rhoPerBlockBps": 0.5 + }, + "capitalEfficiency": { + "enabled": true, + "paths": 1000, + "epochs": 180, + "seed": 138401, + "initialCapital": 1000000, + "alpha": 0.7, + "leverage": 2.5, + "spreadBps": 40, + "volumeEfficiency": 2.5, + "stress": { + "preset": "crash_40pct_external_asset", + "epoch": 30 + } + } +} diff --git a/config/scenarios/high_vol_sigma_spike.json b/config/scenarios/high_vol_sigma_spike.json new file mode 100644 index 0000000..83e7611 --- /dev/null +++ b/config/scenarios/high_vol_sigma_spike.json @@ -0,0 +1,37 @@ +{ + "scenario": "high_vol_sigma_spike", + "graphMode": "design", + "topology": "hub", + "chainsIncluded": ["1", "56", "137"], + "tokensIncluded": ["cWUSDT", "cWUSDC"], + "epochBlocks": 300, + "epochs": 180, + "orderflowModel": { + "distribution": "lognormal", + "volumeMinUnits": 1000, + "volumeMaxUnits": 60000, + "tradesPerEpoch": 20 + }, + "oracleModel": "static", + "latencyModel": { + "finalityBlocksPerChainPair": {}, + "rhoPerBlockBps": 0.5 + }, + "capitalEfficiency": { + "enabled": true, + "paths": 1000, + "epochs": 180, + "seed": 138402, + "initialCapital": 1000000, + "alpha": 0.72, + "leverage": 2.5, + "spreadBps": 40, + "volumeEfficiency": 3, + "stress": { + "preset": "high_vol_sigma_spike", + "epoch": 30, + "durationEpochs": 36, + "sigmaAdd": 0.08 + } + } +} diff --git a/config/scenarios/leverage_sweep_1x_to_4x.json b/config/scenarios/leverage_sweep_1x_to_4x.json new file mode 100644 index 0000000..2c92a6a --- /dev/null +++ b/config/scenarios/leverage_sweep_1x_to_4x.json @@ -0,0 +1,43 @@ +{ + "scenario": "leverage_sweep_1x_to_4x", + "graphMode": "design", + "topology": "hub", + "chainsIncluded": ["1", "56", "137"], + "tokensIncluded": ["cWUSDT", "cWUSDC"], + "epochBlocks": 300, + "epochs": 180, + "orderflowModel": { + "distribution": "uniform", + "volumeMinUnits": 5000, + "volumeMaxUnits": 30000, + "tradesPerEpoch": 15 + }, + "oracleModel": "static", + "latencyModel": { + "finalityBlocksPerChainPair": {}, + "rhoPerBlockBps": 0.5 + }, + "capitalEfficiency": { + "enabled": true, + "paths": 1000, + "epochs": 180, + "seed": 138404, + "initialCapital": 1000000, + "alpha": 0.7, + "leverage": 2, + "spreadBps": 40, + "volumeEfficiency": 2.5, + "optimizer": { + "enabled": true, + "paths": 250, + "topN": 12, + "grid": { + "alpha": [0.65, 0.7, 0.75, 0.8], + "leverage": [1, 2, 2.5, 3, 3.5, 4], + "spreadBps": [30, 40, 50], + "pmmK": [0.1, 0.15], + "liquidityTargetUnits": [500000, 1000000] + } + } + } +} diff --git a/config/scorecard-schema.json b/config/scorecard-schema.json index 504cdc0..571e5c7 100644 --- a/config/scorecard-schema.json +++ b/config/scorecard-schema.json @@ -169,6 +169,36 @@ } } } - } + }, + "capital_efficiency_enabled": { + "type": "boolean", + "description": "True when the simulation-only capital efficiency Monte Carlo overlay was run" + }, + "capital_efficiency_paths": { "type": "integer", "minimum": 1 }, + "capital_efficiency_epochs": { "type": "integer", "minimum": 1 }, + "initial_capital": { "type": "number", "minimum": 0 }, + "alpha": { "type": "number", "minimum": 0, "maximum": 1 }, + "leverage": { "type": "number", "minimum": 1 }, + "spread_bps": { "type": "number", "minimum": 0 }, + "volume_efficiency": { "type": "number", "minimum": 0 }, + "pmm_k": { "type": "number", "minimum": 0 }, + "liquidity_target_units": { "type": "number", "minimum": 0 }, + "roi_mean": { "type": "number" }, + "roi_p05": { "type": "number" }, + "roi_p95": { "type": "number" }, + "pnl_distribution": { + "type": "object", + "properties": { + "p05": { "type": "number" }, + "p50": { "type": "number" }, + "p95": { "type": "number" } + } + }, + "max_drawdown_p95": { "type": "number", "minimum": 0 }, + "liquidation_probability": { "type": "number", "minimum": 0, "maximum": 1 }, + "peg_deviation_frequency": { "type": "number", "minimum": 0, "maximum": 1 }, + "external_liquidity_floor_violations": { "type": "integer", "minimum": 0 }, + "volatility_throttle_events": { "type": "integer", "minimum": 0 }, + "spread_adjustment_events": { "type": "integer", "minimum": 0 } } } diff --git a/docs/12-sim-scorecard.md b/docs/12-sim-scorecard.md index 834817a..f403c2f 100644 --- a/docs/12-sim-scorecard.md +++ b/docs/12-sim-scorecard.md @@ -36,6 +36,15 @@ Every run (hub-only, full-quote, bridge shock) should produce a scorecard with a | `micro_trade_gas_cost_total` | number | Abstract gas budget consumed by support trades | | `scenario` | string | e.g. `hub_only_11`, `full_quote_1_56_137`, `bridge_shock_137_56` | | `runId` | string | Optional run identifier | +| `roi_mean` | number | Mean capital-efficiency ROI when Monte Carlo is enabled | +| `roi_p05` / `roi_p95` | number | 5th/95th percentile ROI band | +| `pnl_distribution` | object | PnL percentiles `{p05,p50,p95}` | +| `max_drawdown_p95` | number | 95th percentile max drawdown | +| `liquidation_probability` | number | Fraction of Monte Carlo paths that liquidated | +| `peg_deviation_frequency` | number | Fraction of path-epochs above peg circuit-break threshold | +| `external_liquidity_floor_violations` | integer | Count of path-epochs below minimum external liquidity before clamp | +| `volatility_throttle_events` | integer | Count of sigma-triggered deleverage/allocation throttle events | +| `spread_adjustment_events` | integer | Count of volatility/liquidity/peg-driven spread adjustments | **Example (minimal):** @@ -79,6 +88,17 @@ From [10-behavioral-stability-analysis.md](10-behavioral-stability-analysis.md): **Pass:** All gates satisfied for the scenario. **Fail:** Any gate violated; do not treat scenario as deployable without parameter change or topology reduction. +Capital-efficiency scenarios also use `config/capital-efficiency-policy.json` gates: + +| Gate | Default | +|------|---------| +| Liquidation probability | `<= 0.02` | +| p95 max drawdown | `<= 0.25` | +| Peg deviation frequency | `<= 0.05` | +| External liquidity floor violations | `0` | +| Deployable leverage | `<= 3x` | +| Hard leverage ceiling | `<= 4x` | + --- ## 3. Phase 0 comparison (three scenarios) diff --git a/docs/16-capital-efficiency-risk-simulation.md b/docs/16-capital-efficiency-risk-simulation.md new file mode 100644 index 0000000..0455f26 --- /dev/null +++ b/docs/16-capital-efficiency-risk-simulation.md @@ -0,0 +1,144 @@ +# Capital Efficiency Risk Simulation + +This module extends the existing PMM routing simulator with a simulation-only treasury/risk overlay. It evaluates capital allocation, leverage, spread policy, peg pressure, volatility throttles, external liquidity floors, and liquidation probability before any live contract work. + +The model is intentionally wired to the current ecosystem: + +- PMM routing, capture, churn, intervention cost, and peg deviation still come from `scripts/run-scenario.cjs`. +- Risk defaults live in `config/capital-efficiency-policy.json`. +- Scenario-specific capital assumptions live under `capitalEfficiency` in `config/scenarios/*.json`. +- `deployment-status.json` remains the deployed-graph source when `graphMode = deployed`. + +This is not a live leverage configuration. Contract work remains gated by audit engagement evidence, governance approval, operational dashboards, and runbooks. + +## Model + +Each Monte Carlo path tracks: + +- `T`: total capital +- `alpha`: treasury/yield allocation +- `L`: leverage +- `sigma`: mean-reverting volatility +- `P`: peg price around 1.0 +- external liquidity floor +- collateral/debt liquidation state + +Per epoch, capital updates with: + +```text +T_next = T + T * (yield + market_making - volatility_drag - intervention_drag - redemption_drag) +``` + +Volatility follows: + +```text +sigma_next = sigma + kappa * (sigma_bar - sigma) + eta * N(0, 1) +``` + +Peg dynamics follow a lightweight imbalance/arb model: + +```text +P_next = P + beta * imbalance - arb_liquidity_coefficient * (P - 1) +``` + +If volatility exceeds `sigmaCrit`, the simulator reduces effective allocation/leverage and widens spread up to the configured ceiling. If external liquidity drops below the policy floor, the path records a violation and clamps allocation. + +## Run + +Baseline routing scenarios are unchanged: + +```bash +node scripts/run-scenario.cjs hub_only_11 +node scripts/run-scenario.cjs bridge_shock_137_56 +``` + +Capital stress scenarios: + +```bash +node scripts/run-scenario.cjs crash_40pct_external_asset +node scripts/run-scenario.cjs high_vol_sigma_spike +node scripts/run-scenario.cjs bank_run_redemption_spike +``` + +Optimizer sweep: + +```bash +node scripts/run-scenario.cjs --optimizer leverage_sweep_1x_to_4x +``` + +`leverage_sweep_1x_to_4x` also enables optimizer mode by default. + +CI-style validation: + +```bash +node scripts/validate-capital-efficiency.cjs +``` + +## Scorecard Additions + +Capital-enabled scenarios emit: + +- `roi_mean`, `roi_p05`, `roi_p95` +- `pnl_distribution` +- `max_drawdown_p95` +- `liquidation_probability` +- `peg_deviation_frequency` +- `external_liquidity_floor_violations` +- `volatility_throttle_events` +- `spread_adjustment_events` + +Optimizer output ranks parameter candidates by ROI penalized for liquidation, drawdown, and peg frequency. A candidate is deployable only if it passes the policy gates in `capital-efficiency-policy.json`. + +## Institutional Defaults + +The default posture is conservative: + +- external liquidity floor: 20% of capital +- target leverage: 2-3x +- deployable optimizer candidates capped at 3x +- hard leverage rejection above 4x +- default max LTV: 65% +- hard LTV ceiling: 75% +- target spread: 30-50 bps +- public PMM remains peg support, not the primary profit engine + +Any later Solidity blueprint must consume the simulator outputs as evidence, not as authority to deploy leverage automatically. + +## Latest Local Run + +Generated on 2026-04-27 from the current configs: + +| Scenario | ROI mean | Liquidation probability | p95 drawdown | Notes | +|---|---:|---:|---:|---| +| `chain138_deployed_capital_efficiency` | `0.0542` | `0` | `0` | Base deployed Chain 138 graph survives under defaults. | +| `crash_40pct_external_asset` | `-0.0646` | `1` | `0.08` | Crash scenario liquidates at the tested 2.5x leverage. | +| `high_vol_sigma_spike` | `-0.0665` | `1` | `0.0841` | Volatility spike liquidates at the tested 2.5x leverage. | +| `bank_run_redemption_spike` | `-0.1586` | `0` | `0.2177` | Redemption stress survives but consumes most drawdown budget. | +| `leverage_sweep_1x_to_4x` | `0.1359` top deployable | `0` | `0` | Top deployable candidate is capped at 3x by policy. | + +Interpretation: + +- Crash/high-volatility profiles are not deployable at 2.5x without lower allocation, lower leverage, stronger collateral haircuts, or faster deleveraging assumptions. +- Bank-run defense survives locally but should be treated as near-limit because p95 drawdown is close to the 25% default gate. +- Optimizer may still simulate 3.5x/4x candidates, but policy prevents them from being marked deployable. + +## Dashboard And Runbook Requirements + +Before any live leverage contract work, operations must expose: + +- ROI band: `roi_mean`, `roi_p05`, `roi_p95` +- Drawdown: `max_drawdown_p95` +- Liquidation: `liquidation_probability` +- Liquidity floor: `external_liquidity_floor_violations` +- Peg defense: `peg_deviation_frequency` +- Throttles: `volatility_throttle_events` +- Spread changes: `spread_adjustment_events` +- Existing PMM health: capture, churn, intervention cost, and worst-pool diagnostics + +Deployment remains blocked until: + +- Smart contract audit engagement evidence exists. +- Governance approval is recorded. +- Risk dashboard and alerting are live. +- Operator runbook covers deleverage, circuit breaker, redemption throttle, and treasury liquidity deployment. +- Treasury/liquidity commitments are documented. diff --git a/docs/17-capital-efficiency-contract-blueprint-gate.md b/docs/17-capital-efficiency-contract-blueprint-gate.md new file mode 100644 index 0000000..c4fe36f --- /dev/null +++ b/docs/17-capital-efficiency-contract-blueprint-gate.md @@ -0,0 +1,28 @@ +# Capital Efficiency Contract Blueprint Gate + +Status: blocked by simulator gates and external institutional prerequisites. + +This document is the handoff point for the requested contract blueprint. It intentionally does not define deployable Solidity yet because current local stress results show liquidation under the `crash_40pct_external_asset` and `high_vol_sigma_spike` scenarios at the tested 2.5x leverage profile. + +## Required Before Blueprint + +- `node scripts/validate-capital-efficiency.cjs` passes. +- Stress scenarios pass the policy gates in `config/capital-efficiency-policy.json`. +- Crash and high-volatility scenarios no longer liquidate under the candidate profile. +- Audit engagement evidence is recorded. +- Governance approval evidence is recorded. +- Risk dashboard and alerting are operational. +- Operator runbook covers deleverage, circuit breaker, redemption throttle, and treasury liquidity deployment. +- Treasury/liquidity commitments are documented. + +## Blueprint Scope Once Unblocked + +- Treasury engine: allocation caps, yield strategy adapter interface, idle/external liquidity floor. +- Liquidity engine: PMM/DODO provider hooks, pool role separation, route exposure controls. +- Leverage engine: LP-token collateral accounting, borrow/redeploy loop, max LTV checks. +- Risk engine: volatility throttle, spread adjustment, peg circuit breaker, liquidation guard. +- Keeper flow: rebalance, auto-deleverage, and dashboard event emission. + +## Current Decision + +Do not implement live leverage contracts. Continue tuning simulation policy and scenarios until deployable candidates pass crash, high-volatility, bank-run, and deployed Chain 138 graph checks. diff --git a/scripts/README.md b/scripts/README.md index 9c46f80..8c0e69f 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -80,8 +80,32 @@ node scripts/run-scenario.cjs hub_only_11 node scripts/run-scenario.cjs --scenario full_quote_1_56_137 node scripts/run-scenario.cjs bridge_shock_137_56 node scripts/run-scenario.cjs micro_support_usd_wrappers_1_56_137 +node scripts/run-scenario.cjs crash_40pct_external_asset +node scripts/run-scenario.cjs high_vol_sigma_spike +node scripts/run-scenario.cjs bank_run_redemption_spike +node scripts/run-scenario.cjs --optimizer leverage_sweep_1x_to_4x ``` **Output:** JSON scorecard including: `capture_mean`, `churn_mean`, `drain_half_life_epochs`, `path_concentration_index`; `intervention_cost_total` / `intervention_cost_inject_total` / `intervention_cost_withdraw_total` / `intervention_cost_by_chain` / `intervention_cost_per_1M_volume`; `micro_trade_count` / `micro_trade_volume_total` / `micro_trade_gas_cost_total`; `peak_deviation_bps` (post-arb), `peak_deviation_bps_pre_arb`, `peak_deviation_bps_post_arb`, `peak_deviation_bps_post_bot`; `arb_volume_total`, `arb_profit_total` (execution-based, not mid). See [docs/12-sim-scorecard.md](../docs/12-sim-scorecard.md) and [config/scorecard-schema.json](../config/scorecard-schema.json). +When `capitalEfficiency.enabled = true`, output also includes Monte Carlo capital-risk metrics: `roi_mean`, `roi_p05`, `roi_p95`, `pnl_distribution`, `max_drawdown_p95`, `liquidation_probability`, `peg_deviation_frequency`, `external_liquidity_floor_violations`, `volatility_throttle_events`, and `spread_adjustment_events`. Optimizer mode emits ranked parameter candidates and never treats leverage above the configured hard ceiling as deployable. + **Orderflow:** Trade sizes use `distribution: "uniform"` by default. Scenario schema supports `lognormal` / `pareto` for skewed (many small + occasional whale) flows; implement in `sampleTrade()` when needed. + +--- + +## validate-capital-efficiency.cjs + +Runs CI-style checks for the simulation-only capital efficiency overlay: + +- JSON parse for policy, schemas, and scenarios +- Baseline scenario remains capital-overlay free +- Stress scenarios emit capital risk fields +- Deterministic repeat check +- Optimizer deployable candidates respect `maxDeployableLeverage` + +**Run:** + +```bash +node scripts/validate-capital-efficiency.cjs +``` diff --git a/scripts/run-scenario.cjs b/scripts/run-scenario.cjs index 4e02b35..e8e29b1 100644 --- a/scripts/run-scenario.cjs +++ b/scripts/run-scenario.cjs @@ -48,12 +48,32 @@ function rng() { t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; } +function normalSample() { + const u1 = Math.max(rng(), 1e-12); + const u2 = Math.max(rng(), 1e-12); + return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2); +} function hashScenarioName(name) { let h = 0; for (let i = 0; i < name.length; i++) h = (Math.imul(31, h) + name.charCodeAt(i)) >>> 0; return h; } +function percentile(values, p) { + if (!values.length) return 0; + const sorted = [...values].sort((a, b) => a - b); + const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1)); + return sorted[idx]; +} + +function round2(n) { + return Math.round((Number(n) || 0) * 100) / 100; +} + +function round4(n) { + return Math.round((Number(n) || 0) * 10000) / 10000; +} + function loadJson(p) { return JSON.parse(fs.readFileSync(p, 'utf8')); } @@ -61,6 +81,9 @@ function loadJson(p) { function loadConfigs(scenario) { const simulationParams = loadJson(path.join(CONFIG_DIR, 'simulation-params.json')); const tokenMap = loadJson(path.join(CONFIG_DIR, 'token-map.json')); + const capitalEfficiencyPolicy = fs.existsSync(path.join(CONFIG_DIR, 'capital-efficiency-policy.json')) + ? loadJson(path.join(CONFIG_DIR, 'capital-efficiency-policy.json')) + : {}; const routingControls = fs.existsSync(path.join(CONFIG_DIR, 'routing-controls.json')) ? loadJson(path.join(CONFIG_DIR, 'routing-controls.json')) : { defaults: { publicRoutingEnabled: true, maxTradeSizeUnits: null } }; @@ -72,14 +95,25 @@ function loadConfigs(scenario) { const poolMatrix = fs.existsSync(path.join(CONFIG_DIR, 'pool-matrix.json')) ? loadJson(path.join(CONFIG_DIR, 'pool-matrix.json')) : { chains: {} }; - return { simulationParams, tokenMap, routingControls, deploymentStatus, poolMatrix }; + return { simulationParams, tokenMap, capitalEfficiencyPolicy, routingControls, deploymentStatus, poolMatrix }; } function buildGraph(scenario, configs) { - const { simulationParams, tokenMap, poolMatrix } = configs; + const { simulationParams, tokenMap, poolMatrix, deploymentStatus } = configs; const chains = simulationParams.chains || {}; const publicChains = tokenMap.publicChains || {}; - const cwTokens = scenario.tokensIncluded || tokenMap.bridgedSymbols || ['cWUSDT', 'cWUSDC', 'cWAUSDT', 'cWEURC', 'cWEURT', 'cWUSDW']; + const deployedChains = deploymentStatus?.chains || {}; + const deployedTokenSet = new Set(); + if (scenario.graphMode === 'deployed') { + for (const chain of Object.values(deployedChains)) { + for (const t of Object.keys(chain.cwTokens || {})) deployedTokenSet.add(t); + for (const p of [...(chain.pmmPools || []), ...(chain.pmmPoolsVolatile || []), ...(chain.gasPmmPools || [])]) { + if (p.base) deployedTokenSet.add(p.base); + if (p.tokenIn) deployedTokenSet.add(p.tokenIn); + } + } + } + const cwTokens = scenario.tokensIncluded || (deployedTokenSet.size ? Array.from(deployedTokenSet) : tokenMap.bridgedSymbols) || ['cWUSDT', 'cWUSDC', 'cWAUSDT', 'cWEURC', 'cWEURT', 'cWUSDW']; const chainIds = scenario.chainsIncluded || Object.keys(chains); const topology = scenario.topology || 'hub'; const fullQuoteChains = scenario.fullQuoteChains @@ -92,13 +126,57 @@ function buildGraph(scenario, configs) { for (const chainId of chainIds) { const chainConf = chains[chainId] || {}; + const deployedChain = scenario.graphMode === 'deployed' ? deployedChains[chainId] : null; const pub = publicChains[chainId] || {}; const hubStable = chainConf.hubStable || pub.hubStable || 'USDC'; - const anchorStables = pub.anchorStables || [hubStable]; + const anchorStables = deployedChain + ? Object.keys(deployedChain.anchorAddresses || {}) + : (pub.anchorStables || [hubStable]); for (const t of cwTokens) nodes.add(`${chainId}:${t}`); for (const a of anchorStables) nodes.add(`${chainId}:${a}`); + if (deployedChain) { + const deployedPools = [ + ...(deployedChain.pmmPools || []), + ...(deployedChain.pmmPoolsVolatile || []), + ...(deployedChain.gasPmmPools || []), + ]; + for (const pool of deployedPools) { + const base = pool.base || pool.tokenIn; + const quote = pool.quote || pool.tokenOut; + if (!base || !quote) continue; + nodes.add(`${chainId}:${base}`); + nodes.add(`${chainId}:${quote}`); + pmmEdges.push({ + type: 'pmm', + chainId, + base, + quote, + key: `${chainId}:${base}:${quote}`, + k: pool.k, + feeBps: pool.feeBps, + inventoryTargetUnits: pool.inventoryTargetUnits, + depthD0Units: pool.depthD0Units, + role: pool.role || 'public_routing', + publicRoutingEnabled: pool.publicRoutingEnabled !== false, + poolAddress: pool.poolAddress, + }); + } + for (let i = 0; i < anchorStables.length; i++) { + for (let j = i + 1; j < anchorStables.length; j++) { + ammEdges.push({ + type: 'amm', + chainId, + tokenA: anchorStables[i], + tokenB: anchorStables[j], + key: `${chainId}:amm:${anchorStables[i]}:${anchorStables[j]}`, + }); + } + } + continue; + } + const poolChains = poolMatrix.chains || {}; const pc = poolChains[chainId] || {}; const poolsFirst = pc.poolsFirst || []; @@ -182,11 +260,11 @@ function buildPMMState(graph, configs) { for (const e of graph.pmmEdges) { const chainConf = graph.chains[e.chainId] || {}; const isEur = CW_EUR.includes(e.base); - const k = isEur ? (eurDefaults.k ?? 0.2) : (chainConf.k ?? defaultPmm.k ?? 0.1); - const feeBps = isEur ? (eurDefaults.feeBps ?? 35) : (chainConf.feeBps ?? defaultPmm.feeBps ?? 25); + const k = e.k != null ? Number(e.k) : (isEur ? (eurDefaults.k ?? 0.2) : (chainConf.k ?? defaultPmm.k ?? 0.1)); + const feeBps = e.feeBps != null ? Number(e.feeBps) : (isEur ? (eurDefaults.feeBps ?? 35) : (chainConf.feeBps ?? defaultPmm.feeBps ?? 25)); const fee = feeBps / 10000; - const invTarget = parseInt(chainConf.inventoryTargetUnits || defaultPmm.inventoryTargetUnits || '1000000', 10); - const d0 = parseInt(chainConf.depthD0Units || defaultPmm.depthD0Units || '500000', 10); + const invTarget = parseInt(e.inventoryTargetUnits || chainConf.inventoryTargetUnits || defaultPmm.inventoryTargetUnits || '1000000', 10); + const d0 = parseInt(e.depthD0Units || chainConf.depthD0Units || defaultPmm.depthD0Units || '500000', 10); const eurP = params.eurUsd != null ? Number(params.eurUsd) : (params.eurPegMultiplier != null && params.eurPegMultiplier !== 1 ? Number(params.eurPegMultiplier) : 1.1); const P = isEur ? eurP : 1; @@ -686,6 +764,7 @@ function getCandidatePaths(chainId, fromToken, toToken, graph, state, probeSize, paths = paths.filter((p) => { const first = p[0]; if (first.type === 'pmm' && publicEnabled === false) return false; + if (first.type === 'pmm' && first.publicRoutingEnabled === false) return false; return true; }); @@ -883,7 +962,7 @@ function runEpoch(scenario, graph, state, configs, epochIndex) { const totalPathVol = Object.values(pathShares).reduce((a, b) => a + b, 0); if (totalPathVol > 0) { pmmVolume = Object.entries(pathShares) - .filter(([pk]) => pk.includes(':cW')) + .filter(([pk]) => pk.split('|').some((part) => !part.includes(':amm:'))) .reduce((a, [, v]) => a + v, 0); } @@ -1023,7 +1102,7 @@ function computeScorecard(scenario, scenarioName, graph, initialState, epochResu return { scenario: scenario.scenario || scenarioName, - runId: `run-${Date.now()}`, + runId: `run-${scenario.seed != null ? Number(scenario.seed) : hashScenarioName(scenarioName)}`, capture_mean: Math.min(1, Math.max(0, captureMean)), capture_p95: Math.min(1, captureMean * 1.2), churn_mean: churnMeanNorm, @@ -1052,9 +1131,334 @@ function computeScorecard(scenario, scenarioName, graph, initialState, epochResu }; } +function mergeCapitalConfig(policy, scenarioConfig = {}, overrides = {}) { + const defaults = policy.defaults || {}; + const risk = { ...(policy.risk || {}), ...(scenarioConfig.risk || {}), ...(overrides.risk || {}) }; + const treasury = { ...(policy.treasury || {}), ...(scenarioConfig.treasury || {}), ...(overrides.treasury || {}) }; + const volatility = { ...(policy.volatilityProcess || {}), ...(scenarioConfig.volatilityProcess || {}), ...(overrides.volatilityProcess || {}) }; + const peg = { ...(policy.pegDynamics || {}), ...(scenarioConfig.pegDynamics || {}), ...(overrides.pegDynamics || {}) }; + const stress = { ...(scenarioConfig.stress || {}), ...(overrides.stress || {}) }; + return { + enabled: scenarioConfig.enabled === true || overrides.enabled === true, + paths: Number(overrides.paths ?? scenarioConfig.paths ?? defaults.paths ?? 1000), + epochs: Number(overrides.epochs ?? scenarioConfig.epochs ?? defaults.epochs ?? 365), + seed: Number(overrides.seed ?? scenarioConfig.seed ?? defaults.seed ?? 138001), + initialCapital: Number(overrides.initialCapital ?? scenarioConfig.initialCapital ?? defaults.initialCapital ?? 1000000), + alpha: Number(overrides.alpha ?? scenarioConfig.alpha ?? defaults.alpha ?? 0.7), + leverage: Number(overrides.leverage ?? scenarioConfig.leverage ?? defaults.leverage ?? 2), + spreadBps: Number(overrides.spreadBps ?? scenarioConfig.spreadBps ?? defaults.spreadBps ?? 35), + volumeEfficiency: Number(overrides.volumeEfficiency ?? scenarioConfig.volumeEfficiency ?? defaults.volumeEfficiency ?? 2), + pmmK: Number(overrides.pmmK ?? scenarioConfig.pmmK ?? defaults.pmmK ?? 0.1), + liquidityTargetUnits: Number(overrides.liquidityTargetUnits ?? scenarioConfig.liquidityTargetUnits ?? defaults.liquidityTargetUnits ?? 1000000), + treasury, + volatility, + peg, + risk, + stress, + }; +} + +function getStressForEpoch(stress, epoch, initialCapital) { + const out = { + externalAssetReturn: 0, + volatilityAdd: 0, + redemptionFraction: 0, + imbalanceAdd: 0, + liquidationLossFraction: 0, + }; + const events = Array.isArray(stress.events) ? stress.events : []; + for (const ev of events) { + const at = Number(ev.epoch ?? 0); + const duration = Math.max(1, Number(ev.durationEpochs ?? 1)); + if (epoch < at || epoch >= at + duration) continue; + if (ev.type === 'crash') out.externalAssetReturn += Number(ev.externalAssetReturn ?? -0.4) / duration; + if (ev.type === 'high_volatility') out.volatilityAdd += Number(ev.sigmaAdd ?? 0.05); + if (ev.type === 'bank_run') out.redemptionFraction += Number(ev.redemptionFraction ?? 0.1) / duration; + if (ev.type === 'bridge_shock') out.imbalanceAdd += Number(ev.imbalanceAdd ?? 0.05); + if (ev.type === 'liquidation_loss') out.liquidationLossFraction += Number(ev.lossFraction ?? 0.05); + } + if (stress.preset === 'crash_40pct_external_asset' && epoch === Number(stress.epoch ?? 24)) { + out.externalAssetReturn -= 0.4; + } + if (stress.preset === 'high_vol_sigma_spike') { + const at = Number(stress.epoch ?? 24); + const duration = Math.max(1, Number(stress.durationEpochs ?? 24)); + if (epoch >= at && epoch < at + duration) out.volatilityAdd += Number(stress.sigmaAdd ?? 0.08); + } + if (stress.preset === 'bank_run_redemption_spike') { + const at = Number(stress.epoch ?? 24); + const duration = Math.max(1, Number(stress.durationEpochs ?? 12)); + if (epoch >= at && epoch < at + duration) out.redemptionFraction += Number(stress.redemptionFraction ?? 0.25) / duration; + } + if (stress.preset === 'bridge_shock') { + const at = Number(stress.epoch ?? 24); + const duration = Math.max(1, Number(stress.durationEpochs ?? 24)); + if (epoch >= at && epoch < at + duration) out.imbalanceAdd += Number(stress.imbalanceAdd ?? 0.05); + } + if (initialCapital <= 0) out.redemptionFraction = 0; + return out; +} + +function simulateCapitalPath(config, baselineScorecard) { + const risk = config.risk || {}; + const treasury = config.treasury || {}; + const vol = config.volatility || {}; + const peg = config.peg || {}; + const spreadRate = config.spreadBps / 10000; + const minExternalLiquidityPct = Number(risk.minExternalLiquidityPct ?? 0.2); + const maxLtv = Number(risk.maxLtvBps ?? 6500) / 10000; + const hardMaxLtv = Number(risk.hardMaxLtvBps ?? 7500) / 10000; + const hardMaxLeverage = Number(risk.hardMaxLeverage ?? 4); + const liquidationLtv = Math.min(hardMaxLtv, Number(risk.liquidationLtvBps ?? 8000) / 10000); + const sigmaCrit = Number(risk.sigmaCrit ?? 0.08); + const throttleLeverageMultiplier = Number(risk.throttleLeverageMultiplier ?? 0.7); + const throttleAlphaMultiplier = Number(risk.throttleAlphaMultiplier ?? 0.9); + const pegCircuitBreakerBps = Number(risk.pegCircuitBreakerBps ?? 200); + const volDrag = Number(treasury.volatilityDragLambda ?? 0.35); + const yieldRate = Number(treasury.yieldRatePerEpoch ?? 0.00035); + const mmScale = Number(treasury.marketMakingScale ?? 0.04); + const liquidationLossFraction = Number(risk.liquidationLossFraction ?? 0.08); + const redemptionFeeBps = Number(risk.bankRunRedemptionFeeBps ?? 100); + + let T = config.initialCapital; + let highWater = T; + let maxDrawdown = 0; + let sigma = Number(vol.sigma0 ?? vol.sigmaBar ?? 0.03); + let P = Number(peg.p0 ?? 1); + let liquidated = false; + let pegDeviationEpochs = 0; + let externalLiquidityFloorViolations = 0; + let volatilityThrottleEvents = 0; + let spreadAdjustmentEvents = 0; + let liquidationEvents = 0; + let totalPnl = 0; + const baselineInterventionDrag = Math.min(0.002, Number(baselineScorecard.intervention_cost_per_1M_volume || 0) / 1e9); + const baselinePegPressure = Math.min(0.1, Number(baselineScorecard.peak_deviation_bps || 0) / 10000); + + for (let e = 0; e < config.epochs; e++) { + if (liquidated || T <= 0) break; + const stress = getStressForEpoch(config.stress || {}, e, T); + const z = normalSample(); + sigma = Math.max(0, sigma + Number(vol.kappa ?? 0.08) * (Number(vol.sigmaBar ?? 0.03) - sigma) + Number(vol.eta ?? 0.01) * z + stress.volatilityAdd); + + let effectiveAlpha = config.alpha; + let effectiveLeverage = config.leverage; + let effectiveSpreadBps = config.spreadBps; + if (sigma > sigmaCrit) { + effectiveAlpha *= throttleAlphaMultiplier; + effectiveLeverage = Math.max(1, effectiveLeverage * throttleLeverageMultiplier); + effectiveSpreadBps = Math.min(Number(risk.maxSpreadBps ?? 100), Math.max(effectiveSpreadBps, Number(risk.throttleSpreadBps ?? 50))); + volatilityThrottleEvents += 1; + spreadAdjustmentEvents += 1; + } + if (effectiveLeverage > hardMaxLeverage) { + liquidated = true; + liquidationEvents += 1; + T = Math.max(0, T * (1 - liquidationLossFraction)); + maxDrawdown = Math.max(maxDrawdown, highWater > 0 ? (highWater - T) / highWater : 0); + break; + } + + const externalLiquidity = (1 - effectiveAlpha) * T; + if (externalLiquidity < minExternalLiquidityPct * T) { + externalLiquidityFloorViolations += 1; + effectiveAlpha = Math.min(effectiveAlpha, 1 - minExternalLiquidityPct); + spreadAdjustmentEvents += 1; + } + + const grossLeveredExposure = effectiveLeverage * effectiveAlpha * T; + const debt = Math.max(0, (effectiveLeverage - 1) * effectiveAlpha * T); + const collateralShock = 1 + stress.externalAssetReturn - sigma * Number(risk.collateralVolatilityHaircut ?? 0.5); + const collateral = Math.max(0, grossLeveredExposure * collateralShock); + const ltv = collateral > 0 ? debt / collateral : 1; + if (ltv > liquidationLtv || ltv > maxLtv * Number(risk.maxLtvBreachMultiplier ?? 1.2)) { + liquidated = true; + liquidationEvents += 1; + T = Math.max(0, T * (1 - liquidationLossFraction - stress.liquidationLossFraction)); + maxDrawdown = Math.max(maxDrawdown, highWater > 0 ? (highWater - T) / highWater : 0); + break; + } + + const imbalance = Number(peg.imbalanceStd ?? 0.01) * normalSample() + stress.imbalanceAdd + baselinePegPressure; + const arbFlow = Number(peg.arbLiquidityCoefficient ?? 0.65) * (P - 1); + P = P + Number(peg.beta ?? 0.25) * imbalance - arbFlow; + const pegDeviationBps = Math.abs(P - 1) * 10000; + if (pegDeviationBps > pegCircuitBreakerBps) { + pegDeviationEpochs += 1; + effectiveSpreadBps = Math.min(Number(risk.maxSpreadBps ?? 100), Math.max(effectiveSpreadBps, Number(risk.circuitBreakerSpreadBps ?? 100))); + spreadAdjustmentEvents += 1; + } + + const sqrtAllocation = Math.sqrt(Math.max(0, effectiveAlpha * (1 - effectiveAlpha))); + const yieldComponent = yieldRate * effectiveAlpha * effectiveLeverage; + const mmComponent = (effectiveSpreadBps / 10000) * config.volumeEfficiency * sqrtAllocation * sigma * mmScale; + const volatilityDrag = volDrag * sigma * Math.max(0.25, effectiveLeverage - 0.5) / 365; + const redemptionDrag = stress.redemptionFraction * (1 - redemptionFeeBps / 10000); + const pnlRate = yieldComponent + mmComponent - volatilityDrag - baselineInterventionDrag - redemptionDrag; + const pnl = T * pnlRate; + totalPnl += pnl; + T = Math.max(0, T + pnl); + highWater = Math.max(highWater, T); + maxDrawdown = Math.max(maxDrawdown, highWater > 0 ? (highWater - T) / highWater : 0); + } + + return { + endingCapital: T, + roi: config.initialCapital > 0 ? (T - config.initialCapital) / config.initialCapital : 0, + totalPnl, + maxDrawdown, + liquidated, + liquidationEvents, + pegDeviationEpochs, + externalLiquidityFloorViolations, + volatilityThrottleEvents, + spreadAdjustmentEvents, + }; +} + +function runCapitalMonteCarlo(scenario, scenarioName, configs, baselineScorecard, overrides = {}) { + const capitalConfig = mergeCapitalConfig(configs.capitalEfficiencyPolicy || {}, scenario.capitalEfficiency || {}, overrides); + if (!capitalConfig.enabled) return null; + const baseSeed = capitalConfig.seed != null ? Number(capitalConfig.seed) : hashScenarioName(`${scenarioName}:capital`); + const rois = []; + const pnls = []; + const drawdowns = []; + let liquidationCount = 0; + let pegDeviationEpochs = 0; + let externalLiquidityFloorViolations = 0; + let volatilityThrottleEvents = 0; + let spreadAdjustmentEvents = 0; + + for (let p = 0; p < capitalConfig.paths; p++) { + seedRng((baseSeed + Math.imul(p + 1, 2654435761)) >>> 0); + const result = simulateCapitalPath(capitalConfig, baselineScorecard); + rois.push(result.roi); + pnls.push(result.totalPnl); + drawdowns.push(result.maxDrawdown); + if (result.liquidated) liquidationCount += 1; + pegDeviationEpochs += result.pegDeviationEpochs; + externalLiquidityFloorViolations += result.externalLiquidityFloorViolations; + volatilityThrottleEvents += result.volatilityThrottleEvents; + spreadAdjustmentEvents += result.spreadAdjustmentEvents; + } + + const totalEpochs = Math.max(1, capitalConfig.paths * capitalConfig.epochs); + return { + capital_efficiency_enabled: true, + capital_efficiency_paths: capitalConfig.paths, + capital_efficiency_epochs: capitalConfig.epochs, + initial_capital: Math.round(capitalConfig.initialCapital), + alpha: round4(capitalConfig.alpha), + leverage: round4(capitalConfig.leverage), + spread_bps: round2(capitalConfig.spreadBps), + volume_efficiency: round4(capitalConfig.volumeEfficiency), + pmm_k: round4(capitalConfig.pmmK), + liquidity_target_units: Math.round(capitalConfig.liquidityTargetUnits), + roi_mean: round4(rois.reduce((a, b) => a + b, 0) / Math.max(1, rois.length)), + roi_p05: round4(percentile(rois, 5)), + roi_p95: round4(percentile(rois, 95)), + pnl_distribution: { + p05: round2(percentile(pnls, 5)), + p50: round2(percentile(pnls, 50)), + p95: round2(percentile(pnls, 95)), + }, + max_drawdown_p95: round4(percentile(drawdowns, 95)), + liquidation_probability: round4(liquidationCount / Math.max(1, capitalConfig.paths)), + peg_deviation_frequency: round4(pegDeviationEpochs / totalEpochs), + external_liquidity_floor_violations: externalLiquidityFloorViolations, + volatility_throttle_events: volatilityThrottleEvents, + spread_adjustment_events: spreadAdjustmentEvents, + }; +} + +function passesCapitalGates(metrics, policy, overrides = {}) { + const gates = { ...(policy.gates || {}), ...(overrides.gates || {}) }; + return ( + metrics.leverage <= Number((policy.risk || {}).hardMaxLeverage ?? 4) + && metrics.leverage <= Number(gates.maxDeployableLeverage ?? (policy.risk || {}).hardMaxLeverage ?? 4) + && metrics.liquidation_probability <= Number(gates.maxLiquidationProbability ?? 0.02) + && metrics.max_drawdown_p95 <= Number(gates.maxDrawdownP95 ?? 0.25) + && metrics.peg_deviation_frequency <= Number(gates.maxPegDeviationFrequency ?? 0.05) + && metrics.external_liquidity_floor_violations <= Number(gates.maxExternalLiquidityFloorViolations ?? 0) + ); +} + +function buildOptimizerGrid(policy, scenarioConfig) { + const opt = scenarioConfig.optimizer || {}; + const grid = opt.grid || {}; + const alphas = grid.alpha || [0.65, 0.7, 0.75, 0.8, 0.85]; + const leverages = grid.leverage || [1, 2, 2.5, 3]; + const spreads = grid.spreadBps || [30, 40, 50]; + const pmmKs = grid.pmmK || [0.1, 0.15, 0.2]; + const liquidityTargets = grid.liquidityTargetUnits || [500000, 1000000, 1500000]; + const out = []; + for (const alpha of alphas) { + for (const leverage of leverages) { + for (const spreadBps of spreads) { + for (const pmmK of pmmKs) { + for (const liquidityTargetUnits of liquidityTargets) { + out.push({ alpha, leverage, spreadBps, pmmK, liquidityTargetUnits }); + } + } + } + } + } + const limit = Number(opt.maxCandidates ?? (policy.optimizer || {}).maxCandidates ?? 250); + return out.slice(0, limit); +} + +function capitalBand(initialCapital) { + if (initialCapital < 500000) return "sub_500k"; + if (initialCapital < 2000000) return "500k_2m"; + if (initialCapital < 10000000) return "2m_10m"; + return "10m_plus"; +} + +function runOptimizer(scenario, scenarioName, configs, baselineScorecard) { + const policy = configs.capitalEfficiencyPolicy || {}; + const scenarioConfig = scenario.capitalEfficiency || {}; + const grid = buildOptimizerGrid(policy, scenarioConfig); + const candidates = []; + for (let i = 0; i < grid.length; i++) { + const overrides = { + enabled: true, + ...grid[i], + paths: Number((scenarioConfig.optimizer || {}).paths ?? (policy.optimizer || {}).paths ?? 250), + seed: (Number(scenarioConfig.seed ?? (policy.defaults || {}).seed ?? 138001) + i * 9973) >>> 0, + }; + const metrics = runCapitalMonteCarlo(scenario, scenarioName, configs, baselineScorecard, overrides); + const deployable = passesCapitalGates(metrics, policy, scenarioConfig.optimizer || {}); + candidates.push({ + rank_score: round4(metrics.roi_mean - metrics.liquidation_probability * 2 - metrics.max_drawdown_p95 * 0.5 - metrics.peg_deviation_frequency), + deployable, + ...metrics, + }); + } + candidates.sort((a, b) => { + if (a.deployable !== b.deployable) return a.deployable ? -1 : 1; + return b.rank_score - a.rank_score; + }); + const top = candidates.slice(0, Number((scenarioConfig.optimizer || {}).topN ?? 10)); + return { + scenario: `${scenario.scenario || scenarioName}:optimizer`, + runId: `optimizer-${scenarioConfig.seed != null ? Number(scenarioConfig.seed) : hashScenarioName(`${scenarioName}:optimizer`)}`, + optimizer_enabled: true, + capital_band: capitalBand(Number((scenarioConfig.initialCapital ?? (policy.defaults || {}).initialCapital) || 0)), + candidates_evaluated: candidates.length, + deployable_candidates: candidates.filter((c) => c.deployable).length, + top_candidates: top, + }; +} + function main() { const idx = process.argv.indexOf('--scenario'); - const scenarioName = idx >= 0 && process.argv[idx + 1] ? process.argv[idx + 1] : process.argv[2] || 'hub_only_11'; + const optimizerMode = process.argv.includes('--optimizer') || process.argv.includes('--optimize'); + const positional = process.argv.slice(2).filter((arg, i, args) => { + if (arg.startsWith('--')) return false; + const prev = args[i - 1]; + return prev !== '--scenario'; + }); + const scenarioName = idx >= 0 && process.argv[idx + 1] ? process.argv[idx + 1] : positional[0] || 'hub_only_11'; const scenarioPath = path.join(SCENARIOS_DIR, `${scenarioName}.json`); if (!fs.existsSync(scenarioPath)) { process.stderr.write(`Scenario not found: ${scenarioPath}\n`); @@ -1078,7 +1482,13 @@ function main() { } const scorecard = computeScorecard(scenario, scenarioName, graph, initialState, epochResults); - console.log(JSON.stringify(scorecard, null, 2)); + const capitalMetrics = runCapitalMonteCarlo(scenario, scenarioName, configs, scorecard); + const fullScorecard = capitalMetrics ? { ...scorecard, ...capitalMetrics } : scorecard; + if (optimizerMode || scenario.capitalEfficiency?.optimizer?.enabled === true) { + console.log(JSON.stringify(runOptimizer(scenario, scenarioName, configs, fullScorecard), null, 2)); + return; + } + console.log(JSON.stringify(fullScorecard, null, 2)); } main(); diff --git a/scripts/validate-capital-efficiency.cjs b/scripts/validate-capital-efficiency.cjs new file mode 100644 index 0000000..bb347a7 --- /dev/null +++ b/scripts/validate-capital-efficiency.cjs @@ -0,0 +1,102 @@ +#!/usr/bin/env node +/** + * CI-style validation for the simulation-only capital efficiency overlay. + * + * Checks: + * - JSON configs and scenario files parse. + * - Baseline scenario stays capital-overlay free. + * - Capital stress scenarios emit required risk metrics. + * - Deterministic seeds produce byte-identical scorecards. + * - Optimizer never marks leverage above policy gates as deployable. + */ + +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); + +const ROOT = path.join(__dirname, '..'); +const CONFIG_DIR = path.join(ROOT, 'config'); +const SCENARIOS_DIR = path.join(CONFIG_DIR, 'scenarios'); +const RUNNER = path.join(ROOT, 'scripts', 'run-scenario.cjs'); + +const REQUIRED_CAPITAL_FIELDS = [ + 'roi_mean', + 'roi_p05', + 'roi_p95', + 'pnl_distribution', + 'max_drawdown_p95', + 'liquidation_probability', + 'peg_deviation_frequency', + 'external_liquidity_floor_violations', + 'volatility_throttle_events', + 'spread_adjustment_events', +]; + +function readJson(file) { + return JSON.parse(fs.readFileSync(file, 'utf8')); +} + +function runScenario(name, extraArgs = []) { + const out = execFileSync(process.execPath, [RUNNER, ...extraArgs, name], { + cwd: ROOT, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + return { raw: out, json: JSON.parse(out) }; +} + +function fail(msg) { + process.stderr.write(`[capital-efficiency] ${msg}\n`); + process.exit(1); +} + +function assert(cond, msg) { + if (!cond) fail(msg); +} + +function main() { + const files = [ + path.join(CONFIG_DIR, 'scenario-schema.json'), + path.join(CONFIG_DIR, 'scorecard-schema.json'), + path.join(CONFIG_DIR, 'capital-efficiency-policy.json'), + ...fs.readdirSync(SCENARIOS_DIR) + .filter((f) => f.endsWith('.json')) + .map((f) => path.join(SCENARIOS_DIR, f)), + ]; + for (const file of files) readJson(file); + + const policy = readJson(path.join(CONFIG_DIR, 'capital-efficiency-policy.json')); + const maxDeployableLeverage = Number(policy.gates?.maxDeployableLeverage ?? policy.risk?.hardMaxLeverage ?? 4); + + const baseline = runScenario('hub_only_11').json; + assert(baseline.capital_efficiency_enabled !== true, 'hub_only_11 unexpectedly enabled capital overlay'); + + for (const scenario of [ + 'chain138_deployed_capital_efficiency', + 'crash_40pct_external_asset', + 'high_vol_sigma_spike', + 'bank_run_redemption_spike', + ]) { + const scorecard = runScenario(scenario).json; + assert(scorecard.capital_efficiency_enabled === true, `${scenario} did not emit capital overlay`); + for (const field of REQUIRED_CAPITAL_FIELDS) { + assert(Object.prototype.hasOwnProperty.call(scorecard, field), `${scenario} missing ${field}`); + } + } + + const a = runScenario('crash_40pct_external_asset').raw; + const b = runScenario('crash_40pct_external_asset').raw; + assert(a === b, 'deterministic seed repeat check failed for crash_40pct_external_asset'); + + const optimizer = runScenario('leverage_sweep_1x_to_4x', ['--optimizer']).json; + assert(optimizer.optimizer_enabled === true, 'optimizer did not emit optimizer payload'); + for (const candidate of optimizer.top_candidates || []) { + if (candidate.deployable) { + assert(candidate.leverage <= maxDeployableLeverage, `deployable candidate exceeds maxDeployableLeverage: ${candidate.leverage}`); + } + } + + process.stdout.write('capital efficiency validation OK\n'); +} + +main();