Files
cross-chain-pmm-lps/scripts/size-inventory.cjs
2026-03-02 12:14:07 -08:00

107 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
/**
* Inventory sizing tool: reads simulation-params.json, optional per-chain V_epoch,
* outputs I_T^* (inventory target), suggested D_0, and assumptions per chain per cW token.
* Keeps configs honest and PRs reviewable.
*
* Usage:
* node scripts/size-inventory.cjs # use scenario defaults for V_epoch
* node scripts/size-inventory.cjs --sigma 1.8 # override sigma (default 1.5)
* node scripts/size-inventory.cjs --refill-ratio 0.33 # T_refill/T_epoch (default 0.33)
* node scripts/size-inventory.cjs --v-epoch '{"1":100000,"56":80000,"137":60000}' # per-chain V_epoch
* EUR tokens use eurDefaults from simulation-params (higher sigma, k).
*/
const fs = require('fs');
const path = require('path');
const CONFIG_DIR = path.join(__dirname, '..', 'config');
const PARAMS_PATH = path.join(CONFIG_DIR, 'simulation-params.json');
const TOKEN_MAP_PATH = path.join(CONFIG_DIR, 'token-map.json');
const CW_USD = ['cWUSDT', 'cWUSDC', 'cWAUSDT', 'cWUSDW'];
const CW_EUR = ['cWEURC', 'cWEURT'];
const CW_ALL = [...CW_USD, ...CW_EUR];
function loadJson(p) {
return JSON.parse(fs.readFileSync(p, 'utf8'));
}
function parseArg(name, def) {
const i = process.argv.indexOf(name);
if (i === -1) return def;
const v = process.argv[i + 1];
if (name === '--v-epoch') return v ? JSON.parse(v) : null;
if (name === '--sigma' || name === '--refill-ratio' || name === '--depth-mult') return v ? Number(v) : def;
return def;
}
function main() {
const params = loadJson(PARAMS_PATH);
const tokenMap = fs.existsSync(TOKEN_MAP_PATH) ? loadJson(TOKEN_MAP_PATH) : { bridgedSymbols: CW_ALL };
const cwTokens = tokenMap.bridgedSymbols || CW_ALL;
const chains = params.chains || {};
const eurDefaults = params.eurDefaults || { sigma: 2, k: 0.2, feeBps: 35 };
const sigmaUsd = parseArg('--sigma', 1.5);
const refillRatio = parseArg('--refill-ratio', 0.33);
const depthMult = parseArg('--depth-mult', 0.75); // D_0 = depthMult * I_T^*
const vEpochOverride = parseArg('--v-epoch', null);
const assumptions = {
sigma_usd: sigmaUsd,
sigma_eur: eurDefaults.sigma,
T_refill_T_epoch: refillRatio,
depth_multiplier: depthMult,
formula: 'I_T^* >= V_epoch * sigma * (1 + T_refill/T_epoch) / (1 - beta) + gamma_buffer',
depth_rule: `D_0 = ${depthMult} * I_T^*`,
};
const out = { assumptions, chains: {}, generatedAt: new Date().toISOString() };
for (const [chainId, chain] of Object.entries(chains)) {
const beta = chain.bridgeBeta ?? 0.001;
const gamma = Number(chain.bridgeGammaUnits || 0) || 0;
const gammaBuffer = gamma * 2; // optional: 2 refill batches
const currentTarget = Number(chain.inventoryTargetUnits || 0) || 0;
// V_epoch: override > env per chain > 10% of current target as scenario default
let vEpoch = vEpochOverride && vEpochOverride[chainId] != null
? Number(vEpochOverride[chainId])
: (process.env[`V_EPOCH_${chainId}`] ? Number(process.env[`V_EPOCH_${chainId}`]) : null);
if (vEpoch == null || isNaN(vEpoch)) {
vEpoch = currentTarget > 0 ? Math.round(currentTarget * 0.1) : 100000;
}
out.chains[chainId] = { name: chain.name, hubStable: chain.hubStable, tokens: {} };
for (const symbol of cwTokens) {
const isEur = CW_EUR.includes(symbol);
const sigma = isEur ? eurDefaults.sigma : sigmaUsd;
const denom = 1 - beta;
if (denom <= 0) throw new Error(`Chain ${chainId}: invalid beta ${beta}`);
const iTStar = Math.ceil((vEpoch * sigma * (1 + refillRatio)) / denom + gammaBuffer);
const d0 = Math.ceil(depthMult * iTStar);
const k = isEur ? (eurDefaults.k ?? 0.2) : (chain.k ?? 0.1);
const feeBps = isEur ? (eurDefaults.feeBps ?? 35) : (chain.feeBps ?? 25);
out.chains[chainId].tokens[symbol] = {
I_T_star: iTStar,
D_0: d0,
V_epoch: vEpoch,
sigma,
beta,
gamma,
gamma_buffer: gammaBuffer,
k,
feeBps,
stress_band_eur: isEur ? 'wider band; use higher sigma' : null,
};
}
}
console.log(JSON.stringify(out, null, 2));
return out;
}
main();