Add capital efficiency risk simulation
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
92
config/capital-efficiency-policy.json
Normal file
92
config/capital-efficiency-policy.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
config/scenarios/bank_run_redemption_spike.json
Normal file
38
config/scenarios/bank_run_redemption_spike.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
31
config/scenarios/chain138_deployed_capital_efficiency.json
Normal file
31
config/scenarios/chain138_deployed_capital_efficiency.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
35
config/scenarios/crash_40pct_external_asset.json
Normal file
35
config/scenarios/crash_40pct_external_asset.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
37
config/scenarios/high_vol_sigma_spike.json
Normal file
37
config/scenarios/high_vol_sigma_spike.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
43
config/scenarios/leverage_sweep_1x_to_4x.json
Normal file
43
config/scenarios/leverage_sweep_1x_to_4x.json
Normal file
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
144
docs/16-capital-efficiency-risk-simulation.md
Normal file
144
docs/16-capital-efficiency-risk-simulation.md
Normal file
@@ -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.
|
||||
28
docs/17-capital-efficiency-contract-blueprint-gate.md
Normal file
28
docs/17-capital-efficiency-contract-blueprint-gate.md
Normal file
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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();
|
||||
|
||||
102
scripts/validate-capital-efficiency.cjs
Normal file
102
scripts/validate-capital-efficiency.cjs
Normal file
@@ -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();
|
||||
Reference in New Issue
Block a user