Add capital efficiency risk simulation

This commit is contained in:
defiQUG
2026-04-27 11:26:55 -07:00
parent 1cf845cb3a
commit 818e864d2b
15 changed files with 1111 additions and 13 deletions

View File

@@ -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

View 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
}
}

View File

@@ -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" }
}
}
}
}
}
}

View 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
}
}
}

View 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
}
}

View 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
}
}
}

View 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
}
}
}

View 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]
}
}
}
}

View File

@@ -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 }
}
}

View File

@@ -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)

View 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.

View 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.

View File

@@ -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
```

View File

@@ -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();

View 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();