chore(sim): refresh deployment status, pool matrix, schemas, and scenario scripts

- deployment-status and pool-matrix snapshots aligned with validate-deployment-status.cjs.
- Micro-trade / scorecard docs and run-scenario wiring.

Made-with: Cursor
This commit is contained in:
defiQUG
2026-04-07 22:56:16 -07:00
parent f7f3e3b020
commit 168dba25d9
13 changed files with 2051 additions and 84 deletions

View File

@@ -30,7 +30,7 @@ Validates `config/deployment-status.json` for minimum viable deployed graph. Use
**Rules:**
- If `bridgeAvailable === true` on a chain, `cwTokens` must include at least **cWUSDT** and **cWUSDC** (phase 1).
- For each `pmmPool`: `role` ∈ {defense, public_routing}; `feeBps` and `k` present; `base`/`quote` (or `tokenIn`/`tokenOut`) exist in `cwTokens` or `anchorAddresses`.
- For each `pmmPool` and each `pmmPoolsVolatile[]` entry: `role` ∈ {defense, public_routing, truu_routing}; `feeBps` and `k` present; `base`/`quote` (or `tokenIn`/`tokenOut`) exist in `cwTokens` or `anchorAddresses` (e.g. mainnet **TRUU** under `anchorAddresses.TRUU`). Non-zero `poolAddress` must not be the zero address.
**Run:**
@@ -40,12 +40,16 @@ node scripts/validate-deployment-status.cjs
**Exit code:** 0 if valid, 1 if invalid (errors to stderr).
**Parent proxmox repo:** live Mainnet cW/TRUU pool deploy and ratio-matched top-up scripts live under `scripts/deployment/` (`deploy-mainnet-pmm-cw-truu-pool.sh`, `add-mainnet-truu-pmm-topup.sh`); see `docs/03-deployment/MAINNET_PMM_TRUU_CWUSD_PEG_AND_BOT_RUNBOOK.md` §11.
---
## run-scenario.cjs
Builds the **real** routing graph from configs, runs epochs with PMM state updates, path enumeration + waterfilling, **arb step** (implied-price deviation, capped corrective trades, profit gate), **bot step** (inject/withdraw at 0.5×/1.5× I_T^* with intervention cost β/γ/ρ), and optional **bridge shock** trades. Emits a **real scorecard** (PR#2). Runs are **deterministic** when `scenario.seed` is set or derived from scenario name.
Scenarios may also include an optional **microTradePolicy** for gas-budgeted support trades in selected USD wrapper rails. This is meant to model tiny, controlled `cWUSDC` / `cWUSDT` turnover-seeding or inventory-corrective activity, not to turn the PMM into a venue.
**Configs used:** `simulation-params.json`, `token-map.json`, `routing-controls.json`; `deployment-status.json` only when `graphMode = deployed`. Pool topology from `pool-matrix.json` and scenario `topology` / `fullQuoteChains`.
**Tuning constants (in script):**
@@ -64,8 +68,9 @@ Builds the **real** routing graph from configs, runs epochs with PMM state updat
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
```
**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`; `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).
**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).
**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.

View File

@@ -355,6 +355,173 @@ function getBridgeRho(scenario, fromChain, toChain) {
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;
@@ -625,6 +792,11 @@ function runEpoch(scenario, graph, state, configs, epochIndex) {
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;
@@ -682,6 +854,15 @@ function runEpoch(scenario, graph, state, configs, epochIndex) {
}
}
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);
@@ -717,6 +898,11 @@ function runEpoch(scenario, graph, state, configs, epochIndex) {
interventionCostInject: interventionCostInject || 0,
interventionCostWithdraw: interventionCostWithdraw || 0,
interventionCostByChain: interventionCostByChain || {},
microTradeCount,
microTradeBuyCount,
microTradeSellCount,
microTradeVolumeTotal,
microTradeGasCostTotal,
peakDeviationBpsPreArb: peakDeviationBpsPreArb || 0,
peakDeviationBpsPostArb: peakDeviationBpsPostArb || 0,
peakDeviationBpsPostBot: peakDeviationBpsPostBot || 0,
@@ -753,6 +939,11 @@ function computeScorecard(scenario, scenarioName, graph, initialState, epochResu
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;
@@ -763,6 +954,11 @@ function computeScorecard(scenario, scenarioName, graph, initialState, epochResu
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;
@@ -838,6 +1034,11 @@ function computeScorecard(scenario, scenarioName, graph, initialState, epochResu
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),

View File

@@ -5,8 +5,9 @@
*
* Rules:
* - If bridgeAvailable === true on a chain, cwTokens must include at least cWUSDT and cWUSDC (phase 1).
* - For each pmmPool: role in {defense, public_routing}, feeBps and k present,
* base/quote (or tokenIn/tokenOut) exist in cwTokens or anchorAddresses.
* - For each pmmPool / pmmPoolsVolatile[]: role in {defense, public_routing, truu_routing},
* feeBps and k present, base/quote (or tokenIn/tokenOut) exist in cwTokens or anchorAddresses.
* TRUU must be listed under anchorAddresses when used as quote (e.g. mainnet chain 1).
*
* Exit code: 0 if valid, 1 if invalid (and prints errors to stderr).
*/
@@ -18,12 +19,41 @@ const CONFIG_DIR = path.join(__dirname, '..', 'config');
const DEPLOYMENT_STATUS_PATH = path.join(CONFIG_DIR, 'deployment-status.json');
const PHASE1_CW = ['cWUSDT', 'cWUSDC'];
const VALID_ROLES = ['defense', 'public_routing'];
const VALID_ROLES = ['defense', 'public_routing', 'truu_routing'];
const VALID_REFERENCE_PROTOCOLS = ['uniswap_v3', 'balancer', 'curve', '1inch'];
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, 'utf8'));
}
function validatePoolEntries(chainId, pools, listLabel, knownTokens, errors) {
for (let i = 0; i < pools.length; i++) {
const pool = pools[i];
const base = pool.base ?? pool.tokenIn;
const quote = pool.quote ?? pool.tokenOut;
if (!VALID_ROLES.includes(pool.role)) {
errors.push(`Chain ${chainId} ${listLabel}[${i}]: role must be one of ${VALID_ROLES.join(', ')}`);
}
if (pool.feeBps == null || pool.k == null) {
errors.push(`Chain ${chainId} ${listLabel}[${i}]: feeBps and k required`);
}
if (base && !knownTokens.has(base)) {
errors.push(`Chain ${chainId} ${listLabel}[${i}]: base/tokenIn "${base}" not in cwTokens or anchorAddresses`);
}
if (quote && !knownTokens.has(quote)) {
errors.push(`Chain ${chainId} ${listLabel}[${i}]: quote/tokenOut "${quote}" not in cwTokens or anchorAddresses`);
}
const addr = pool.poolAddress;
if (addr != null && addr !== '') {
const z = String(addr).toLowerCase();
if (z === '0x0000000000000000000000000000000000000000') {
errors.push(`Chain ${chainId} ${listLabel}[${i}]: poolAddress must not be zero when set`);
}
}
}
}
function main() {
const status = loadJson(DEPLOYMENT_STATUS_PATH);
const chains = status.chains || {};
@@ -31,8 +61,13 @@ function main() {
for (const [chainId, chain] of Object.entries(chains)) {
const cwTokens = chain.cwTokens || {};
const gasMirrors = chain.gasMirrors || {};
const anchorAddresses = chain.anchorAddresses || {};
const gasQuoteAddresses = chain.gasQuoteAddresses || {};
const pmmPools = chain.pmmPools || [];
const pmmPoolsVolatile = chain.pmmPoolsVolatile || [];
const gasPmmPools = chain.gasPmmPools || [];
const gasReferenceVenues = chain.gasReferenceVenues || [];
const bridgeAvailable = chain.bridgeAvailable;
if (bridgeAvailable === true) {
@@ -43,24 +78,63 @@ function main() {
}
}
const knownTokens = new Set([...Object.keys(cwTokens), ...Object.keys(anchorAddresses)]);
const knownTokens = new Set([
...Object.keys(cwTokens),
...Object.keys(gasMirrors),
...Object.keys(anchorAddresses),
...Object.keys(gasQuoteAddresses),
]);
for (let i = 0; i < pmmPools.length; i++) {
const pool = pmmPools[i];
const base = pool.base ?? pool.tokenIn;
const quote = pool.quote ?? pool.tokenOut;
validatePoolEntries(chainId, pmmPools, 'pmmPools', knownTokens, errors);
validatePoolEntries(chainId, pmmPoolsVolatile, 'pmmPoolsVolatile', knownTokens, errors);
validatePoolEntries(chainId, gasPmmPools, 'gasPmmPools', knownTokens, errors);
if (!VALID_ROLES.includes(pool.role)) {
errors.push(`Chain ${chainId} pmmPools[${i}]: role must be one of ${VALID_ROLES.join(', ')}`);
const gasPoolsByFamily = new Map();
for (const pool of gasPmmPools) {
if (!pool.familyKey || typeof pool.familyKey !== 'string') {
errors.push(`Chain ${chainId} gasPmmPools entry is missing familyKey`);
continue;
}
if (pool.feeBps == null || pool.k == null) {
errors.push(`Chain ${chainId} pmmPools[${i}]: feeBps and k required`);
if (!gasPoolsByFamily.has(pool.familyKey)) gasPoolsByFamily.set(pool.familyKey, []);
gasPoolsByFamily.get(pool.familyKey).push(pool);
}
for (const [familyKey, pools] of gasPoolsByFamily.entries()) {
const poolTypes = new Set(pools.map((pool) => pool.poolType));
if (!poolTypes.has('wrapped_native')) {
errors.push(`Chain ${chainId} gas family ${familyKey}: missing wrapped_native DODO pool`);
}
if (base && !knownTokens.has(base)) {
errors.push(`Chain ${chainId} pmmPools[${i}]: base/tokenIn "${base}" not in cwTokens or anchorAddresses`);
if (!poolTypes.has('stable_quote')) {
errors.push(`Chain ${chainId} gas family ${familyKey}: missing stable_quote DODO pool`);
}
if (quote && !knownTokens.has(quote)) {
errors.push(`Chain ${chainId} pmmPools[${i}]: quote/tokenOut "${quote}" not in cwTokens or anchorAddresses`);
}
const referenceVenuesByFamily = new Map();
for (let i = 0; i < gasReferenceVenues.length; i++) {
const venue = gasReferenceVenues[i];
if (!VALID_REFERENCE_PROTOCOLS.includes(venue.protocol)) {
errors.push(`Chain ${chainId} gasReferenceVenues[${i}]: protocol must be one of ${VALID_REFERENCE_PROTOCOLS.join(', ')}`);
}
if (!venue.familyKey || typeof venue.familyKey !== 'string') {
errors.push(`Chain ${chainId} gasReferenceVenues[${i}]: familyKey required`);
continue;
}
if (!referenceVenuesByFamily.has(venue.familyKey)) referenceVenuesByFamily.set(venue.familyKey, []);
referenceVenuesByFamily.get(venue.familyKey).push(venue);
}
for (const [familyKey, venues] of referenceVenuesByFamily.entries()) {
const protocols = new Set(venues.map((venue) => venue.protocol));
if (!protocols.has('uniswap_v3')) {
errors.push(`Chain ${chainId} gas family ${familyKey}: missing uniswap_v3 reference venue`);
}
const oneInch = venues.find((venue) => venue.protocol === '1inch');
if (oneInch?.routingVisible === true || oneInch?.live === true) {
const hasUniswap = venues.some((venue) => venue.protocol === 'uniswap_v3' && venue.live === true);
const hasDodo = (gasPoolsByFamily.get(familyKey) || []).some((pool) => pool.publicRoutingEnabled === true);
if (!hasUniswap || !hasDodo) {
errors.push(`Chain ${chainId} gas family ${familyKey}: 1inch cannot be live/routingVisible before DODO and Uniswap venues are live`);
}
}
}
}