#!/usr/bin/env node /** * Routing simulator: graph, PMM state, path enumeration + waterfilling, arb step, * bot step, bridge shock (optional). Emits real scorecard. PR#2: deterministic, * arb + bot + intervention cost + peak deviation. * * Usage: node scripts/run-scenario.cjs hub_only_11 * node scripts/run-scenario.cjs --scenario full_quote_1_56_137 */ const fs = require('fs'); const path = require('path'); const CONFIG_DIR = path.join(__dirname, '..', 'config'); const SCENARIOS_DIR = path.join(CONFIG_DIR, 'scenarios'); const CW_EUR = ['cWEURC', 'cWEURT']; const PROBE_SIZE = 1000; const K_PATHS = 5; const CHUNK_FRACTION = 0.05; const AMM_DEPTH_UNITS = 10e6; const AMM_FEE_BPS = 5; const MAX_ITER_INVERSE = 50; const TOL_INVERSE = 1; // PR#2: Arb const DELTA_ARB_BPS = 40; const ARB_ALPHA = 1.0; const ARB_MAX_FRACTION_OF_TARGET = 0.1; // Marginal probe: must be small (x << D) so implied price ≈ (1-fee)*P, not curvature-dominated. 0.01 gives true marginal. const MARGINAL_EPS = 0.01; const ARB_PROBE = MARGINAL_EPS; const ARB_GAS_UNITS = 0; // PR#2: Bot corridor (tighter 0.75/1.25 so bot acts before depleted depth; faster 0.40) const LOW_WATER = 0.75; const HIGH_WATER = 1.25; const BOT_MAX_FRACTION_OF_TARGET = 0.4; // Seeded RNG (mulberry32) for deterministic runs let rngState = 0; function seedRng(seed) { rngState = (seed >>> 0) || 1; } function rng() { let t = (rngState += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); 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')); } 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 } }; let deploymentStatus = null; if (scenario.graphMode === 'deployed') { const p = path.join(CONFIG_DIR, 'deployment-status.json'); if (fs.existsSync(p)) deploymentStatus = loadJson(p); } const poolMatrix = fs.existsSync(path.join(CONFIG_DIR, 'pool-matrix.json')) ? loadJson(path.join(CONFIG_DIR, 'pool-matrix.json')) : { chains: {} }; return { simulationParams, tokenMap, capitalEfficiencyPolicy, routingControls, deploymentStatus, poolMatrix }; } function buildGraph(scenario, configs) { const { simulationParams, tokenMap, poolMatrix, deploymentStatus } = configs; const chains = simulationParams.chains || {}; const publicChains = tokenMap.publicChains || {}; 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 || (simulationParams.scenarioDefaults?.topologySensitivity?.fullQuoteModelChains) || []; const nodes = new Set(); const pmmEdges = []; const ammEdges = []; const bridgeEdges = []; 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 = 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 || []; const poolsOptional = pc.poolsOptional || []; const isFullQuote = topology === 'full_quote' || (topology === 'mixed' && fullQuoteChains.includes(chainId)); for (const cw of cwTokens) { const hubPool = `${cw}/${hubStable}`; const hasHub = poolsFirst.includes(hubPool) || true; if (hasHub) { pmmEdges.push({ type: 'pmm', chainId, base: cw, quote: hubStable, key: `${chainId}:${cw}:${hubStable}`, }); } if (isFullQuote) { for (const pool of poolsOptional) { const [base, quote] = pool.split('/'); if (base === cw && anchorStables.includes(quote)) { pmmEdges.push({ type: 'pmm', chainId, base, quote, key: `${chainId}:${base}:${quote}`, }); } } } } 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]}`, }); } } } for (let i = 0; i < chainIds.length; i++) { for (let j = 0; j < chainIds.length; j++) { if (i === j) continue; for (const cw of cwTokens) { bridgeEdges.push({ type: 'bridge', fromChain: chainIds[i], toChain: chainIds[j], token: cw, }); } } } return { nodes: Array.from(nodes), pmmEdges, ammEdges, bridgeEdges, chainIds, cwTokens, chains, publicChains, defaultPmm: simulationParams.defaultPmm || {}, }; } function buildPMMState(graph, configs) { const state = {}; const params = configs.simulationParams; const defaultPmm = params.defaultPmm || {}; const eurDefaults = params.eurDefaults || { k: 0.2, feeBps: 35 }; for (const e of graph.pmmEdges) { const chainConf = graph.chains[e.chainId] || {}; const isEur = CW_EUR.includes(e.base); 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(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; state[e.key] = { I_T: invTarget, I_T_star: invTarget, D_0: d0, k, fee, feeBps, P, }; } return state; } function getD(stateKey, state) { const s = state[stateKey]; if (!s) return 0; const ratio = s.I_T / s.I_T_star; return s.D_0 * Math.min(1, ratio); } function pmmSellT(stateKey, x, state) { const s = state[stateKey]; if (!s || x <= 0) return { outputQ: 0, newState: state }; const D = getD(stateKey, state); const term = D > 0 ? x - D * Math.log(1 + x / D) : x; const y = (1 - s.fee) * s.P * (x - s.k * (x - term)); const newState = { ...state, [stateKey]: { ...s, I_T: s.I_T + x } }; return { outputQ: Math.max(0, y), newState }; } function pmmBuyT(stateKey, Q, state) { const s = state[stateKey]; if (!s || Q <= 0) return { outputT: 0, newState: state }; let lo = 0; let hi = Q / ((1 - s.fee) * s.P) * 2; for (let iter = 0; iter < MAX_ITER_INVERSE; iter++) { const x = (lo + hi) / 2; const D = getD(stateKey, state); const term = D > 0 ? x - D * Math.log(1 + x / D) : x; const y = (1 - s.fee) * s.P * (x - s.k * (x - term)); if (Math.abs(y - Q) < TOL_INVERSE) { const newState = { ...state, [stateKey]: { ...s, I_T: Math.max(0, s.I_T - x) } }; return { outputT: x, newState }; } if (y < Q) lo = x; else hi = x; } const x = (lo + hi) / 2; const newState = { ...state, [stateKey]: { ...s, I_T: Math.max(0, s.I_T - x) } }; return { outputT: x, newState }; } // Implied price = analytical marginal at x=0: d/dx output = (1-fee)*P (invariant to D at limit). Avoids (1-k) bias from finite sell probe. function getImpliedPriceAndDeviation(stateKey, state) { const s = state[stateKey]; if (!s) return { pHat: 1, deviationBps: 0 }; const P = s.P; const pHat = (1 - s.fee) * P; const deviationBps = P > 0 ? ((pHat - P) / P) * 10000 : 0; return { pHat, deviationBps }; } function maxDeviationBpsOverPools(graph, state) { let maxBps = 0; for (const e of graph.pmmEdges) { const { deviationBps } = getImpliedPriceAndDeviation(e.key, state); if (Math.abs(deviationBps) > maxBps) maxBps = Math.abs(deviationBps); } return maxBps; } function getWorstPoolDiagnostic(graph, state) { let maxBps = 0; let worstKey = null; for (const e of graph.pmmEdges) { const { deviationBps } = getImpliedPriceAndDeviation(e.key, state); if (Math.abs(deviationBps) > maxBps) { maxBps = Math.abs(deviationBps); worstKey = e.key; } } if (!worstKey) return { key: null, deviation_bps: 0, I_T_ratio: null, D_effective: null, oracle_P: null, p_hat: null }; const s = state[worstKey]; const I_T_ratio = s && s.I_T_star > 0 ? s.I_T / s.I_T_star : null; const D_effective = s ? getD(worstKey, state) : null; const { pHat } = getImpliedPriceAndDeviation(worstKey, state); return { key: worstKey, deviation_bps: maxBps, I_T_ratio, D_effective, oracle_P: s ? s.P : null, p_hat: pHat }; } function arbProfitSellT(key, xArb, state) { const { outputQ } = pmmSellT(key, xArb, state); const s = state[key]; const P = s ? s.P : 1; return outputQ - P * xArb - ARB_GAS_UNITS; } function qRequiredForBuyT(key, xT, state, tol = 1) { const s = state[key]; if (!s) return 0; let qLo = 0; let qHi = xT * s.P * 2; for (let i = 0; i < MAX_ITER_INVERSE; i++) { const Q = (qLo + qHi) / 2; const { outputT } = pmmBuyT(key, Q, state); if (Math.abs(outputT - xT) < tol) return Q; if (outputT < xT) qLo = Q; else qHi = Q; } return (qLo + qHi) / 2; } function arbProfitBuyT(key, xArb, state) { const s = state[key]; const P = s ? s.P : 1; const Q = qRequiredForBuyT(key, xArb, state); return P * xArb - Q - ARB_GAS_UNITS; } function runArbStep(graph, state, configs) { let curState = state; let arbVolumeTotal = 0; let arbProfitTotal = 0; let peakDeviationBps = 0; for (const e of graph.pmmEdges) { const key = e.key; const s = curState[key]; if (!s || s.I_T_star == null) continue; const { deviationBps } = getImpliedPriceAndDeviation(key, curState); if (Math.abs(deviationBps) > peakDeviationBps) peakDeviationBps = Math.abs(deviationBps); if (Math.abs(deviationBps) <= DELTA_ARB_BPS) continue; const xMax = ARB_MAX_FRACTION_OF_TARGET * s.I_T_star; const xArb = Math.min(xMax, ARB_ALPHA * Math.abs(deviationBps) / 10000 * s.I_T_star); if (xArb < 1) continue; let profit = 0; if (deviationBps > 0) { profit = arbProfitSellT(key, xArb, curState); if (profit <= 0) continue; const { outputQ, newState } = pmmSellT(key, xArb, curState); curState = newState; arbVolumeTotal += xArb; arbProfitTotal += profit; } else { profit = arbProfitBuyT(key, xArb, curState); if (profit <= 0) continue; const Q = qRequiredForBuyT(key, xArb, curState); const { outputT, newState } = pmmBuyT(key, Q, curState); curState = newState; arbVolumeTotal += outputT; arbProfitTotal += profit; } } return { state: curState, arbVolumeTotal, arbProfitTotal, peakDeviationBps }; } function getBridgeRho(scenario, fromChain, toChain) { const lat = scenario.latencyModel || {}; const blocks = lat.finalityBlocksPerChainPair && lat.finalityBlocksPerChainPair[`${fromChain}-${toChain}`]; const rhoPerBlock = lat.rhoPerBlockBps ?? 0.5; return (blocks ?? 10) * rhoPerBlock / 10000; } function getMatchedQuoteForBase(base) { if (base === 'cWUSDT') return 'USDT'; if (base === 'cWUSDC') return 'USDC'; return null; } function getMicroTradeCandidates(graph, policy) { const tokenFilter = new Set(policy.tokens || []); const quoteFilter = new Set(policy.quoteTokens || []); let candidates = graph.pmmEdges.filter((e) => { if (tokenFilter.size > 0 && !tokenFilter.has(e.base)) return false; if (quoteFilter.size > 0 && !quoteFilter.has(e.quote)) return false; return true; }); if (policy.preferMatchedQuote) { const perChainBase = new Map(); for (const edge of candidates) { const key = `${edge.chainId}:${edge.base}`; const preferredQuote = getMatchedQuoteForBase(edge.base); const existing = perChainBase.get(key); if (!existing) { perChainBase.set(key, edge); continue; } const score = edge.quote === preferredQuote ? 0 : 1; const existingScore = existing.quote === preferredQuote ? 0 : 1; if ( score < existingScore || (score === existingScore && edge.quote.localeCompare(existing.quote) < 0) ) { perChainBase.set(key, edge); } } candidates = Array.from(perChainBase.values()); } candidates.sort((a, b) => ( a.chainId.localeCompare(b.chainId) || a.base.localeCompare(b.base) || a.quote.localeCompare(b.quote) )); return candidates; } function resolveMicroTradeDirection(policy, poolState, epochIndex, tradeIndex) { const mode = policy.mode || 'inventory_or_alternate'; const bandFrac = Number(policy.inventoryBandFraction ?? 0.05); const lower = (1 - bandFrac) * poolState.I_T_star; const upper = (1 + bandFrac) * poolState.I_T_star; if (mode === 'inventory' || mode === 'inventory_or_alternate') { if (poolState.I_T < lower) return 'sell_base'; if (poolState.I_T > upper) return 'buy_base'; if (mode === 'inventory') return null; } if (mode === 'alternate' || mode === 'inventory_or_alternate') { return ((epochIndex + tradeIndex) % 2 === 0) ? 'buy_base' : 'sell_base'; } if (mode === 'buy_base') return 'buy_base'; if (mode === 'sell_base') return 'sell_base'; return null; } function runMicroTradeSupportStep(graph, state, scenario, epochIndex) { const policy = scenario.microTradePolicy || {}; if (policy.enabled === false || Object.keys(policy).length === 0) { return { state, microTradeCount: 0, microTradeBuyCount: 0, microTradeSellCount: 0, microTradeVolumeTotal: 0, microTradeGasCostTotal: 0, }; } const requestedTrades = Math.max(0, parseInt(policy.tradesPerEpoch ?? 0, 10)); if (requestedTrades === 0) { return { state, microTradeCount: 0, microTradeBuyCount: 0, microTradeSellCount: 0, microTradeVolumeTotal: 0, microTradeGasCostTotal: 0, }; } const candidates = getMicroTradeCandidates(graph, policy); if (candidates.length === 0) { return { state, microTradeCount: 0, microTradeBuyCount: 0, microTradeSellCount: 0, microTradeVolumeTotal: 0, microTradeGasCostTotal: 0, }; } const gasCostPerTrade = Number(policy.gasCostPerTradeUnits ?? 0); const gasBudgetPerEpoch = Number(policy.gasBudgetPerEpochUnits ?? 0); let tradesAllowed = requestedTrades; if (gasCostPerTrade > 0 && gasBudgetPerEpoch > 0) { tradesAllowed = Math.min(tradesAllowed, Math.floor(gasBudgetPerEpoch / gasCostPerTrade)); } if (tradesAllowed <= 0) { return { state, microTradeCount: 0, microTradeBuyCount: 0, microTradeSellCount: 0, microTradeVolumeTotal: 0, microTradeGasCostTotal: 0, }; } let curState = state; let microTradeCount = 0; let microTradeBuyCount = 0; let microTradeSellCount = 0; let microTradeVolumeTotal = 0; let microTradeGasCostTotal = 0; for (let i = 0; i < tradesAllowed; i++) { const edge = candidates[(epochIndex * tradesAllowed + i) % candidates.length]; const poolState = curState[edge.key]; if (!poolState || poolState.I_T_star == null) continue; const rawTradeSize = Number(policy.tradeSizeUnits ?? 0); const maxFractionOfTarget = Number(policy.maxFractionOfTarget ?? 0.01); const maxTradeSize = maxFractionOfTarget > 0 ? poolState.I_T_star * maxFractionOfTarget : rawTradeSize; const tradeSize = Math.max(0, Math.min(rawTradeSize, maxTradeSize || rawTradeSize)); if (tradeSize <= 0) continue; const direction = resolveMicroTradeDirection(policy, poolState, epochIndex, i); if (!direction) continue; if (direction === 'sell_base') { const { newState } = pmmSellT(edge.key, tradeSize, curState); curState = newState; microTradeSellCount += 1; microTradeVolumeTotal += tradeSize; } else { const quoteSpend = tradeSize * (poolState.P || 1); const { outputT, newState } = pmmBuyT(edge.key, quoteSpend, curState); curState = newState; microTradeBuyCount += 1; microTradeVolumeTotal += outputT; } microTradeCount += 1; microTradeGasCostTotal += gasCostPerTrade; } return { state: curState, microTradeCount, microTradeBuyCount, microTradeSellCount, microTradeVolumeTotal, microTradeGasCostTotal, }; } function runBotStep(graph, state, scenario, configs) { const chains = graph.chains; let curState = state; let interventionCostInject = 0; let interventionCostWithdraw = 0; const byChain = {}; for (const e of graph.pmmEdges) { const key = e.key; const s = curState[key]; if (!s || s.I_T_star == null) continue; const I_T = s.I_T; const I_T_star = s.I_T_star; let u = 0; if (I_T < LOW_WATER * I_T_star) { u = Math.min(I_T_star - I_T, BOT_MAX_FRACTION_OF_TARGET * I_T_star); } else if (I_T > HIGH_WATER * I_T_star) { u = -Math.min(I_T - I_T_star, BOT_MAX_FRACTION_OF_TARGET * I_T_star); } if (u === 0) continue; const chainId = e.chainId; const chainConf = chains[chainId] || {}; const beta = Number(chainConf.bridgeBeta ?? 0.001); const gamma = Number(chainConf.bridgeGammaUnits ?? 10); const rho = getBridgeRho(scenario, chainId, '138'); const cost = Math.abs(u) * (beta + rho) + gamma; const c = Number(cost); if (u > 0) { interventionCostInject += c; } else { interventionCostWithdraw += c; } if (!byChain[chainId]) byChain[chainId] = { inject: 0, withdraw: 0 }; if (u > 0) byChain[chainId].inject += c; else byChain[chainId].withdraw += c; const newS = { ...s, I_T: Math.max(0, s.I_T + u) }; curState = { ...curState, [key]: newS }; } return { state: curState, interventionCostInject, interventionCostWithdraw, interventionCostByChain: byChain, }; } function ammOutput(inputUnits, feeBps = AMM_FEE_BPS) { return inputUnits * (1 - feeBps / 10000); } function pathCostSameChain(path, graph, state, fromToken, toToken, amount) { if (path.length === 1) { const e = path[0]; if (e.type === 'pmm') { const key = e.key; const s = state[key]; if (!s) return { cost: 1e9, output: 0 }; if (e.base === fromToken && e.quote === toToken) { const { outputQ } = pmmSellT(key, amount, state); return { cost: 1 - outputQ / amount, output: outputQ }; } if (e.base === toToken && e.quote === fromToken) { const { outputT } = pmmBuyT(key, amount, state); return { cost: 1 - outputT / amount, output: outputT }; } } if (e.type === 'amm') { const out = ammOutput(amount); return { cost: 1 - out / amount, output: out }; } } if (path.length === 2) { const [e1, e2] = path; let out1 = amount; let curState = state; if (e1.type === 'pmm') { const key = e1.key; if (e1.base === fromToken && e1.quote !== toToken) { const r = pmmSellT(key, out1, curState); out1 = r.outputQ; curState = r.newState; } else if (e1.quote === fromToken && e1.base !== toToken) { const r = pmmBuyT(key, out1, curState); out1 = r.outputT; curState = r.newState; } } else if (e1.type === 'amm') { out1 = ammOutput(out1); } const midToken = e1.type === 'pmm' ? (e1.base === fromToken ? e1.quote : e1.base) : (e1.tokenA === fromToken ? e1.tokenB : e1.tokenA); if (e2.type === 'pmm') { const key = e2.key; if (e2.base === midToken && e2.quote === toToken) { const r = pmmSellT(key, out1, curState); return { cost: 1 - r.outputQ / amount, output: r.outputQ }; } if (e2.quote === midToken && e2.base === toToken) { const r = pmmBuyT(key, out1, curState); return { cost: 1 - r.outputT / amount, output: r.outputT }; } } else if (e2.type === 'amm') { const out2 = ammOutput(out1); return { cost: 1 - out2 / amount, output: out2 }; } } return { cost: 1e9, output: 0 }; } function getNeighbors(chainId, token, graph) { const out = []; for (const e of graph.pmmEdges) { if (e.chainId !== chainId) continue; if (e.base === token) out.push({ token: e.quote, edge: e }); if (e.quote === token) out.push({ token: e.base, edge: e }); } for (const e of graph.ammEdges) { if (e.chainId !== chainId) continue; if (e.tokenA === token) out.push({ token: e.tokenB, edge: { ...e, type: 'amm' } }); if (e.tokenB === token) out.push({ token: e.tokenA, edge: { ...e, type: 'amm' } }); } return out; } function enumeratePaths(chainId, fromToken, toToken, graph, maxLen = 3) { const paths = []; function dfs(cur, path, visited) { if (cur === toToken) { paths.push([...path]); return; } if (path.length >= maxLen) return; const neighbors = getNeighbors(chainId, cur, graph); for (const { token, edge } of neighbors) { const key = `${chainId}:${token}`; if (visited.has(key)) continue; visited.add(key); path.push(edge); dfs(token, path, visited); path.pop(); visited.delete(key); } } dfs(fromToken, [], new Set([`${chainId}:${fromToken}`])); return paths; } function getRoutingControlsForChain(chainId, routingControls) { const defaults = routingControls.defaults || {}; const overrides = (routingControls.perChainOverrides || {})[String(chainId)] || {}; return { ...defaults, ...overrides }; } function getCandidatePaths(chainId, fromToken, toToken, graph, state, probeSize, k, routingControls) { const effective = getRoutingControlsForChain(chainId, routingControls); const publicEnabled = effective.publicRoutingEnabled !== false; const maxTrade = effective.maxTradeSizeUnits; let paths = enumeratePaths(chainId, fromToken, toToken, graph); 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; }); const withCost = paths.map((p) => { const { cost, output } = pathCostSameChain(p, graph, state, fromToken, toToken, probeSize); return { path: p, cost, output }; }); withCost.sort((a, b) => a.cost - b.cost); return withCost.slice(0, k).map((x) => x.path); } function waterfill(chainId, fromToken, toToken, amount, graph, state, routingControls) { const effective = getRoutingControlsForChain(chainId, routingControls); const maxTrade = effective.maxTradeSizeUnits ? parseInt(effective.maxTradeSizeUnits, 10) : null; const chunkSize = Math.max(1, Math.floor(amount * CHUNK_FRACTION)); let remaining = amount; let curState = state; const pathVolumes = {}; let totalOutput = 0; while (remaining > 0.5) { const chunk = Math.min(chunkSize, remaining); const capped = maxTrade != null ? Math.min(chunk, maxTrade) : chunk; const candidates = getCandidatePaths(chainId, fromToken, toToken, graph, curState, PROBE_SIZE, K_PATHS, routingControls); if (candidates.length === 0) break; let bestPath = null; let bestOutput = -1; for (const p of candidates) { const { output } = pathCostSameChain(p, graph, curState, fromToken, toToken, capped); if (output > bestOutput) { bestOutput = output; bestPath = p; } } if (!bestPath || bestOutput <= 0) break; const pathKey = bestPath.map((e) => (e.key || `${e.type}:${e.chainId}:${e.tokenA || e.base}:${e.tokenB || e.quote}`)).join('|'); pathVolumes[pathKey] = (pathVolumes[pathKey] || 0) + capped; totalOutput += bestOutput; let nextState = curState; let left = capped; let from = fromToken; for (let i = 0; i < bestPath.length && left > 0; i++) { const e = bestPath[i]; if (e.type === 'pmm') { const key = e.key; if (e.base === from) { const r = pmmSellT(key, left, nextState); left = r.outputQ; nextState = r.newState; from = e.quote; } else { const r = pmmBuyT(key, left, nextState); left = r.outputT; nextState = r.newState; from = e.base; } } else { left = ammOutput(left); from = e.tokenA === from ? e.tokenB : e.tokenA; } } curState = nextState; remaining -= capped; } return { state: curState, pathVolumes, totalOutput }; } function sampleTrade(scenario, chainIds, cwTokens, graph) { const orderflow = scenario.orderflowModel || {}; const volMin = orderflow.volumeMinUnits ?? 1000; const volMax = orderflow.volumeMaxUnits ?? 50000; const dist = orderflow.distribution || 'uniform'; let amount; if (dist === 'lognormal') { const median = (volMin + volMax) / 2; const sigma = 1.0; const z = Math.sqrt(-2 * Math.log(rng() || 1e-10)) * Math.cos(2 * Math.PI * rng()); amount = Math.max(volMin, Math.min(volMax, median * Math.exp(sigma * z))); } else if (dist === 'pareto') { const alpha = orderflow.paretoAlpha ?? 2.0; const xm = volMin; amount = Math.min(volMax, xm / Math.pow(rng() || 1e-10, 1 / alpha)); } else { amount = volMin + rng() * (volMax - volMin); } const chainId = chainIds[Math.floor(rng() * chainIds.length)]; const pub = graph.publicChains[chainId] || {}; const anchors = pub.anchorStables || ['USDC', 'USDT']; const fromToken = cwTokens[Math.floor(rng() * cwTokens.length)]; const toToken = anchors[Math.floor(rng() * anchors.length)]; if (fromToken === toToken) return null; return { chainId, fromToken, toToken, amount }; } function runEpoch(scenario, graph, state, configs, epochIndex) { const orderflow = scenario.orderflowModel || {}; const tradesPerEpoch = orderflow.tradesPerEpoch ?? 20; const pathShares = {}; let totalVolume = 0; let pmmVolume = 0; let churnSum = 0; const I_T_start = {}; let microTradeCount = 0; let microTradeBuyCount = 0; let microTradeSellCount = 0; let microTradeVolumeTotal = 0; let microTradeGasCostTotal = 0; for (const k of Object.keys(state)) { if (state[k].I_T_star != null) I_T_start[k] = state[k].I_T; } let curState = state; const shock = scenario.bridgeShock; if (shock && epochIndex < (shock.durationEpochs || 24)) { const fromChain = shock.fromChain; const toChain = shock.toChain; const frac = (shock.magnitudeFraction || 0.05) / (shock.durationEpochs || 24); let baseline = 0; for (const e of graph.pmmEdges) { if (e.chainId === fromChain || e.chainId === toChain) { const s = curState[e.key]; if (s && s.I_T_star != null) baseline += s.I_T_star; } } const shockAmount = Math.max(1000, frac * baseline); for (const cw of graph.cwTokens) { const fromPool = graph.pmmEdges.find((x) => x.chainId === fromChain && x.base === cw); const toPool = graph.pmmEdges.find((x) => x.chainId === toChain && x.base === cw); if (!fromPool || !toPool) continue; const sellAmount = Math.min(shockAmount / graph.cwTokens.length, curState[fromPool.key] ? curState[fromPool.key].I_T * 0.5 : shockAmount); if (sellAmount < 1) continue; const r1 = pmmSellT(fromPool.key, sellAmount, curState); curState = r1.newState; totalVolume += sellAmount; const buyAmount = Math.min(r1.outputQ, (curState[toPool.key] ? curState[toPool.key].I_T_star : 1e6) * 0.5); if (buyAmount > 1) { const r2 = pmmBuyT(toPool.key, buyAmount, curState); curState = r2.newState; totalVolume += buyAmount; } } } for (let t = 0; t < tradesPerEpoch; t++) { const trade = sampleTrade(scenario, graph.chainIds, graph.cwTokens, graph); if (!trade) continue; const { state: nextState, pathVolumes } = waterfill( trade.chainId, trade.fromToken, trade.toToken, trade.amount, graph, curState, configs.routingControls ); curState = nextState; totalVolume += trade.amount; for (const [pathKey, vol] of Object.entries(pathVolumes || {})) { pathShares[pathKey] = (pathShares[pathKey] || 0) + vol; } } const micro = runMicroTradeSupportStep(graph, curState, scenario, epochIndex); curState = micro.state; microTradeCount += micro.microTradeCount || 0; microTradeBuyCount += micro.microTradeBuyCount || 0; microTradeSellCount += micro.microTradeSellCount || 0; microTradeVolumeTotal += micro.microTradeVolumeTotal || 0; microTradeGasCostTotal += micro.microTradeGasCostTotal || 0; totalVolume += micro.microTradeVolumeTotal || 0; const peakDeviationBpsPreArb = maxDeviationBpsOverPools(graph, curState); const worstPreArb = getWorstPoolDiagnostic(graph, curState); const { state: afterArb, arbVolumeTotal, arbProfitTotal, peakDeviationBps: peakDeviationBpsPostArb } = runArbStep(graph, curState, configs); curState = afterArb; const worstPostArb = getWorstPoolDiagnostic(graph, curState); const { state: afterBot, interventionCostInject, interventionCostWithdraw, interventionCostByChain } = runBotStep(graph, curState, scenario, configs); curState = afterBot; const peakDeviationBpsPostBot = maxDeviationBpsOverPools(graph, curState); const worstPostBot = getWorstPoolDiagnostic(graph, curState); for (const k of Object.keys(curState)) { const s = curState[k]; if (s.I_T_star != null && I_T_start[k] != null) { churnSum += Math.abs(s.I_T - I_T_start[k]); } } const totalPathVol = Object.values(pathShares).reduce((a, b) => a + b, 0); if (totalPathVol > 0) { pmmVolume = Object.entries(pathShares) .filter(([pk]) => pk.split('|').some((part) => !part.includes(':amm:'))) .reduce((a, [, v]) => a + v, 0); } return { state: curState, pathShares, totalVolume, pmmVolume, churnSum, arbVolumeTotal: arbVolumeTotal || 0, arbProfitTotal: arbProfitTotal || 0, interventionCostInject: interventionCostInject || 0, interventionCostWithdraw: interventionCostWithdraw || 0, interventionCostByChain: interventionCostByChain || {}, microTradeCount, microTradeBuyCount, microTradeSellCount, microTradeVolumeTotal, microTradeGasCostTotal, peakDeviationBpsPreArb: peakDeviationBpsPreArb || 0, peakDeviationBpsPostArb: peakDeviationBpsPostArb || 0, peakDeviationBpsPostBot: peakDeviationBpsPostBot || 0, worst_pool_pre_arb: worstPreArb, worst_pool_post_arb: worstPostArb, worst_pool_post_bot: worstPostBot, I_T_by_key: Object.fromEntries(Object.entries(curState).filter(([, s]) => s.I_T_star != null).map(([k, s]) => [k, s.I_T])), }; } function computeScorecard(scenario, scenarioName, graph, initialState, epochResults) { const params = graph.chains; const defaultPmm = graph.defaultPmm || {}; const invTargetDefault = parseInt(defaultPmm.inventoryTargetUnits || '1000000', 10); let totalVolume = 0; let totalPmmVolume = 0; let churnSum = 0; let churnMax = 0; const pathShareTotals = {}; const I_T_over_epochs = {}; const I_T_star_by_key = {}; for (const e of graph.pmmEdges) { const chainConf = params[e.chainId] || {}; I_T_star_by_key[e.key] = parseInt(chainConf.inventoryTargetUnits || defaultPmm?.inventoryTargetUnits || '1000000', 10); } let arbVolumeTotal = 0; let arbProfitTotal = 0; let interventionCostInjectTotal = 0; let interventionCostWithdrawTotal = 0; const interventionCostByChain = {}; let peakDeviationBpsPreArb = 0; let peakDeviationBpsPostArb = 0; let peakDeviationBpsPostBot = 0; let microTradeCountTotal = 0; let microTradeBuyCountTotal = 0; let microTradeSellCountTotal = 0; let microTradeVolumeTotal = 0; let microTradeGasCostTotal = 0; for (const r of epochResults) { totalVolume += r.totalVolume; totalPmmVolume += r.pmmVolume; churnSum += r.churnSum; if (r.churnSum > churnMax) churnMax = r.churnSum; arbVolumeTotal += r.arbVolumeTotal || 0; arbProfitTotal += r.arbProfitTotal || 0; interventionCostInjectTotal += r.interventionCostInject || 0; interventionCostWithdrawTotal += r.interventionCostWithdraw || 0; microTradeCountTotal += r.microTradeCount || 0; microTradeBuyCountTotal += r.microTradeBuyCount || 0; microTradeSellCountTotal += r.microTradeSellCount || 0; microTradeVolumeTotal += r.microTradeVolumeTotal || 0; microTradeGasCostTotal += r.microTradeGasCostTotal || 0; for (const [chainId, v] of Object.entries(r.interventionCostByChain || {})) { if (!interventionCostByChain[chainId]) interventionCostByChain[chainId] = { inject: 0, withdraw: 0 }; interventionCostByChain[chainId].inject += v.inject || 0; interventionCostByChain[chainId].withdraw += v.withdraw || 0; } if ((r.peakDeviationBpsPreArb || 0) > peakDeviationBpsPreArb) peakDeviationBpsPreArb = r.peakDeviationBpsPreArb; if ((r.peakDeviationBpsPostArb || 0) > peakDeviationBpsPostArb) peakDeviationBpsPostArb = r.peakDeviationBpsPostArb; if ((r.peakDeviationBpsPostBot || 0) > peakDeviationBpsPostBot) peakDeviationBpsPostBot = r.peakDeviationBpsPostBot; for (const [pk, vol] of Object.entries(r.pathShares || {})) { pathShareTotals[pk] = (pathShareTotals[pk] || 0) + vol; } for (const [k, iT] of Object.entries(r.I_T_by_key || {})) { if (!I_T_over_epochs[k]) I_T_over_epochs[k] = []; I_T_over_epochs[k].push(iT); } } const interventionCostTotal = interventionCostInjectTotal + interventionCostWithdrawTotal; const drainHalfLife = {}; for (const [key, series] of Object.entries(I_T_over_epochs)) { const start = initialState[key] ? initialState[key].I_T : (series[0] || 0); const threshold = 0.5 * start; let epoch = -1; for (let i = 0; i < series.length; i++) { if (series[i] <= threshold) { epoch = i; break; } } if (epoch >= 0) drainHalfLife[key] = epoch; else drainHalfLife[key] = series.length; } const totalPathVol = Object.values(pathShareTotals).reduce((a, b) => a + b, 0); let hhi = 0; if (totalPathVol > 0) { for (const vol of Object.values(pathShareTotals)) { const s = vol / totalPathVol; hhi += s * s; } } const pathConcentrationIndex = hhi; const captureMean = totalVolume > 0 ? totalPmmVolume / totalVolume : 0; const numEpochs = epochResults.length; const churnMean = numEpochs > 0 ? churnSum / numEpochs : 0; const invTotal = Object.values(I_T_star_by_key).reduce((a, b) => a + b, 0); const churnMeanNorm = invTotal > 0 ? churnSum / numEpochs / invTotal : 0; const interventionNum = Number.isFinite(interventionCostTotal) ? interventionCostTotal : 0; const interventionPer1M = totalVolume > 0 ? (interventionNum / totalVolume) * 1e6 : 0; const lastEpoch = epochResults.length > 0 ? epochResults[epochResults.length - 1] : {}; const worst_pool_diagnostic = lastEpoch.worst_pool_pre_arb ? { pre_arb: lastEpoch.worst_pool_pre_arb, post_arb: lastEpoch.worst_pool_post_arb, post_bot: lastEpoch.worst_pool_post_bot, } : undefined; return { scenario: scenario.scenario || scenarioName, 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, churn_p95: churnMax / Math.max(1, invTotal), churn_max: churnMax, intervention_cost_total: Math.round(interventionNum), intervention_cost_inject_total: Math.round(interventionCostInjectTotal), intervention_cost_withdraw_total: Math.round(interventionCostWithdrawTotal), intervention_cost_by_chain: interventionCostByChain, intervention_cost_per_1M_volume: Math.round(interventionPer1M * 100) / 100, micro_trade_count: Math.round(microTradeCountTotal), micro_trade_buy_count: Math.round(microTradeBuyCountTotal), micro_trade_sell_count: Math.round(microTradeSellCountTotal), micro_trade_volume_total: Math.round(microTradeVolumeTotal), micro_trade_gas_cost_total: Math.round(microTradeGasCostTotal * 100) / 100, peak_deviation_bps: Math.round(Number.isFinite(peakDeviationBpsPostArb) ? peakDeviationBpsPostArb : 0), peak_deviation_bps_pre_arb: Math.round(peakDeviationBpsPreArb), peak_deviation_bps_post_arb: Math.round(peakDeviationBpsPostArb), peak_deviation_bps_post_bot: Math.round(peakDeviationBpsPostBot), reflexive_route_count: 0, drain_half_life_epochs: drainHalfLife, path_concentration_index: Math.min(1, Math.max(0, pathConcentrationIndex)), arb_volume_total: Math.round(arbVolumeTotal), arb_profit_total: Math.round(arbProfitTotal * 100) / 100, ...(worst_pool_diagnostic && { worst_pool_diagnostic }), }; } 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 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`); process.exit(1); } const scenario = loadJson(scenarioPath); const seed = scenario.seed != null ? Number(scenario.seed) : hashScenarioName(scenarioName); seedRng(seed); const configs = loadConfigs(scenario); const graph = buildGraph(scenario, configs); let state = buildPMMState(graph, configs); const epochs = scenario.epochs || 10; const initialState = JSON.parse(JSON.stringify(state)); const epochResults = []; for (let e = 0; e < epochs; e++) { const result = runEpoch(scenario, graph, state, configs, e); state = result.state; epochResults.push(result); } const scorecard = computeScorecard(scenario, scenarioName, graph, initialState, epochResults); 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();