131 lines
5.0 KiB
JavaScript
131 lines
5.0 KiB
JavaScript
#!/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',
|
|
];
|
|
|
|
const STRESS_SCENARIOS = [
|
|
'crash_40pct_external_asset',
|
|
'high_vol_sigma_spike',
|
|
'bank_run_redemption_spike',
|
|
];
|
|
|
|
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 gates = policy.gates || {};
|
|
const maxLiquidationProbability = Number(gates.maxLiquidationProbability ?? 0.02);
|
|
const maxDrawdownP95 = Number(gates.maxDrawdownP95 ?? 0.25);
|
|
const maxPegDeviationFrequency = Number(gates.maxPegDeviationFrequency ?? 0.05);
|
|
const maxExternalLiquidityFloorViolations = Number(gates.maxExternalLiquidityFloorViolations ?? 0);
|
|
const guard = policy.liveExecutionGuard || {};
|
|
|
|
assert(guard.status === 'simulation_only', 'liveExecutionGuard.status must remain simulation_only until external approvals exist');
|
|
assert(guard.riskDashboardEvidence, 'risk dashboard evidence path is required');
|
|
assert(guard.operatorRunbookEvidence, 'operator runbook evidence path is required');
|
|
assert(guard.treasuryLiquidityCommitmentEvidence, 'treasury liquidity commitment evidence path is required');
|
|
for (const evidencePath of [
|
|
guard.riskDashboardEvidence,
|
|
guard.operatorRunbookEvidence,
|
|
guard.treasuryLiquidityCommitmentEvidence,
|
|
]) {
|
|
assert(fs.existsSync(path.join(ROOT, evidencePath)), `evidence path missing: ${evidencePath}`);
|
|
}
|
|
|
|
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',
|
|
...STRESS_SCENARIOS,
|
|
]) {
|
|
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}`);
|
|
}
|
|
if (STRESS_SCENARIOS.includes(scenario)) {
|
|
assert(scorecard.liquidation_probability <= maxLiquidationProbability, `${scenario} liquidation probability exceeds gate`);
|
|
assert(scorecard.max_drawdown_p95 <= maxDrawdownP95, `${scenario} p95 drawdown exceeds gate`);
|
|
assert(scorecard.peg_deviation_frequency <= maxPegDeviationFrequency, `${scenario} peg deviation frequency exceeds gate`);
|
|
assert(scorecard.external_liquidity_floor_violations <= maxExternalLiquidityFloorViolations, `${scenario} external liquidity floor violations exceed gate`);
|
|
}
|
|
}
|
|
|
|
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();
|