Snapshot of accumulated work-in-progress on top of the baseline commit. This session — retire Tier B from the wallet shell (single-tier model, glossary "Tiers & trust"): - SendFlow.tsx: BCH-only; removed the non-functional Tier-B send path, the D-4/D-5 raw-BCH co-display, and the ADR-0013 friction ladder. - ArtemisionScreen.tsx: Create wizard collapsed to a single 5-step on-chain genesis flow; tier-selection step + Tier-B branch (prepareTierBImport) deleted. - QuickActions.tsx / SettingsScreen.tsx: receive note reworded; "Send friction thresholds" + "Tier explainer" rows removed; registry-trust note reworded. - Verified: tsc clean, vitest 74/74, full preview walkthrough, no console errors. Pre-existing uncommitted work also folded in: - USDm -> FIATm family rename/reorg (Parameters/FIATm/, design records) - ~130 chipnet + sim-fiatm scripts (catalytic-converter, hyperinflation backtests) - snapshots 0153-0168; wiki updates (glossary, decisions, index); ADR-0044/0045 - mockups + additional shell screens (Agora, Apps, MainScreen, AssetDetail, ...) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
232 lines
8.8 KiB
JavaScript
232 lines
8.8 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* sim-fiatm-adaptive-comparison.mjs — compares baseline §12 controller
|
||
* vs proposed adaptive overlay (price × flow 4-condition matrix) on a
|
||
* fresh-launched country currency.
|
||
*
|
||
* Two scenarios:
|
||
* A. Crash + active mints → adaptive slows down crash-side response
|
||
* (wider bargain window, more BCH absorbed)
|
||
* B. Rally + active redeems → adaptive speeds up rally-side response
|
||
* (premium window closes, no sustained extraction)
|
||
*
|
||
* Outputs JSON to stdout for the visualization.
|
||
*
|
||
* Run: node scripts/sim-fiatm-adaptive-comparison.mjs > adaptive-data.json
|
||
*/
|
||
|
||
// ── Controller constants (§12) ──────────────────────────────────────────
|
||
const BLOCK_MIN = 10;
|
||
const RETARGET_BLOCKS = 2016;
|
||
const SUPPLY_CATALYST = 210_000;
|
||
const N_FLOOR = 1, N_CEILING = 144;
|
||
const D_FLOOR = 0.05, D_CEILING = 0.20;
|
||
const N_STEP = 1, D_STEP = 0.005;
|
||
const DEADBAND_BPS = 50;
|
||
const CAP_BPS = 1000;
|
||
|
||
// ── Adaptive layer parameters ───────────────────────────────────────────
|
||
const EWMA_HALF_LIFE = 144; // 1 day
|
||
const EWMA_ALPHA = 1 - Math.pow(0.5, 1 / EWMA_HALF_LIFE);
|
||
const ADAPTIVE_FLOW_THRESHOLD = 1.5; // ratio mint/redeem to be "active"
|
||
const ADAPTIVE_DORMANT_THRESHOLD = 0.5; // total flow below this fraction of baseline = dormant
|
||
const ADAPTIVE_SLOWDOWN = 2.0; // 2x slower
|
||
const ADAPTIVE_SPEEDUP = 0.5; // 2x faster
|
||
|
||
class Controller {
|
||
constructor(adaptive = false, initialBchPrice = 200) {
|
||
// Fresh-launch state — say a new country currency, 3 catalysts in
|
||
this.N_up = 1;
|
||
this.N_down = 4; // already 3 catalysts past genesis
|
||
this.d_up = 0.115; // 10% + 3×0.5%
|
||
this.d_down = 0.085; // 10% - 3×0.5%
|
||
this.doubling_epoch = 0;
|
||
this.catalyst_count = 3;
|
||
this.cumulative_minted = SUPPLY_CATALYST * 3; // 630k
|
||
this.last_recal_block = 0;
|
||
this.last_retarget_block = 0;
|
||
this.window_mints = 0;
|
||
this.window_redeems = 0;
|
||
this.window_rate_moves = 0;
|
||
this.window_supply_start = this.cumulative_minted;
|
||
this.prev_window_growth = 0;
|
||
|
||
// EWMA for adaptive layer
|
||
this.mint_ewma = 0;
|
||
this.redeem_ewma = 0;
|
||
this.baseline_flow = 1.5; // baseline per-block flow
|
||
|
||
this.adaptive = adaptive;
|
||
// Start at oracle target for initial price (gap=0)
|
||
this.rate = Math.round(100_000_000 / initialBchPrice);
|
||
}
|
||
|
||
effectiveCadence(direction) {
|
||
// Existing §12 calc
|
||
const N = direction === 'down' ? this.N_down : this.N_up;
|
||
const baseCadence = N * Math.pow(2, this.doubling_epoch);
|
||
if (!this.adaptive) return baseCadence;
|
||
|
||
// Adaptive overlay
|
||
const totalFlowEwma = this.mint_ewma + this.redeem_ewma;
|
||
if (totalFlowEwma < this.baseline_flow * ADAPTIVE_DORMANT_THRESHOLD) return baseCadence;
|
||
|
||
const mintRatio = this.mint_ewma / Math.max(this.redeem_ewma, 0.01);
|
||
const isMintHeavy = mintRatio > ADAPTIVE_FLOW_THRESHOLD;
|
||
const isRedeemHeavy = mintRatio < 1 / ADAPTIVE_FLOW_THRESHOLD;
|
||
|
||
let mult = 1.0;
|
||
if (direction === 'down') {
|
||
// crash-side cadence
|
||
if (isMintHeavy) mult = ADAPTIVE_SLOWDOWN; // slow → wider window
|
||
else if (isRedeemHeavy) mult = ADAPTIVE_SPEEDUP; // speed → close window
|
||
} else {
|
||
// rally-side cadence
|
||
if (isRedeemHeavy) mult = ADAPTIVE_SPEEDUP; // speed → fair conversion
|
||
else if (isMintHeavy) mult = ADAPTIVE_SLOWDOWN; // slow → preserve premium
|
||
}
|
||
return Math.max(1, Math.round(baseCadence * mult));
|
||
}
|
||
|
||
step(block, bch_price, mints, redeems) {
|
||
this.cumulative_minted += mints;
|
||
this.window_mints += mints;
|
||
this.window_redeems += redeems;
|
||
|
||
// EWMA update (per-block)
|
||
this.mint_ewma += EWMA_ALPHA * (mints - this.mint_ewma);
|
||
this.redeem_ewma += EWMA_ALPHA * (redeems - this.redeem_ewma);
|
||
|
||
// Catalyst trigger (unchanged)
|
||
const expected_catalysts = Math.floor(this.cumulative_minted / SUPPLY_CATALYST);
|
||
while (expected_catalysts > this.catalyst_count) {
|
||
this.catalyst_count++;
|
||
this.N_down = Math.min(this.N_down + N_STEP, N_CEILING);
|
||
this.N_up = Math.max(this.N_up - N_STEP, N_FLOOR);
|
||
this.d_down = Math.max(this.d_down - D_STEP, D_FLOOR);
|
||
this.d_up = Math.min(this.d_up + D_STEP, D_CEILING);
|
||
}
|
||
|
||
// DART time trigger (unchanged)
|
||
if (block - this.last_retarget_block >= RETARGET_BLOCKS) {
|
||
this.last_retarget_block = block;
|
||
this.window_mints = 0; this.window_redeems = 0; this.window_rate_moves = 0;
|
||
this.prev_window_growth = this.cumulative_minted - this.window_supply_start;
|
||
this.window_supply_start = this.cumulative_minted;
|
||
}
|
||
|
||
// Rate adjustment with adaptive cadence
|
||
const target = Math.round(100_000_000 / bch_price);
|
||
const direction = target > this.rate ? 'down' : 'up';
|
||
const cadence = this.effectiveCadence(direction);
|
||
if (block - this.last_recal_block >= cadence) {
|
||
const gap = target - this.rate;
|
||
const gapBps = Math.abs((gap / this.rate) * 10000);
|
||
if (gapBps >= DEADBAND_BPS) {
|
||
const d = direction === 'down' ? this.d_down : this.d_up;
|
||
let delta = gap * d;
|
||
const max_delta = (this.rate * CAP_BPS) / 10000;
|
||
delta = Math.sign(delta) * Math.min(Math.abs(delta), max_delta);
|
||
this.rate = Math.round(this.rate + delta);
|
||
this.last_recal_block = block;
|
||
this.window_rate_moves += direction === 'down' ? 1 : -1;
|
||
}
|
||
}
|
||
}
|
||
|
||
gap(bch_price) {
|
||
const target = 100_000_000 / bch_price;
|
||
return ((this.rate - target) / target) * 100;
|
||
}
|
||
}
|
||
|
||
// ── Scenarios ────────────────────────────────────────────────────────────
|
||
const HOURS = 144; // blocks (one day)
|
||
const SIM_BLOCKS = 6 * HOURS; // 6 days = 864 blocks
|
||
|
||
function runScenario(scenarioName, priceFn, mintFn, redeemFn) {
|
||
const initialPrice = priceFn(0);
|
||
const baseline = new Controller(false, initialPrice);
|
||
const adaptive = new Controller(true, initialPrice);
|
||
|
||
const data = [];
|
||
for (let block = 0; block < SIM_BLOCKS; block++) {
|
||
const price = priceFn(block);
|
||
const mints = mintFn(block);
|
||
const redeems = redeemFn(block);
|
||
baseline.step(block, price, mints, redeems);
|
||
adaptive.step(block, price, mints, redeems);
|
||
|
||
// Sample every 12 blocks (2 hours) for chart clarity
|
||
if (block % 12 === 0) {
|
||
data.push({
|
||
hours: (block * BLOCK_MIN) / 60,
|
||
block,
|
||
bch_price: price,
|
||
oracle_target_sats_per_usd: Math.round(100_000_000 / price),
|
||
baseline_rate: baseline.rate,
|
||
adaptive_rate: adaptive.rate,
|
||
baseline_gap_pct: baseline.gap(price),
|
||
adaptive_gap_pct: adaptive.gap(price),
|
||
baseline_cadence_down: baseline.effectiveCadence('down'),
|
||
adaptive_cadence_down: adaptive.effectiveCadence('down'),
|
||
baseline_cadence_up: baseline.effectiveCadence('up'),
|
||
adaptive_cadence_up: adaptive.effectiveCadence('up'),
|
||
mint_ewma: adaptive.mint_ewma.toFixed(2),
|
||
redeem_ewma: adaptive.redeem_ewma.toFixed(2),
|
||
});
|
||
}
|
||
}
|
||
return { name: scenarioName, data };
|
||
}
|
||
|
||
// Scenario A: Crash + active mints
|
||
// BCH crashes from $200 to $150 (-25%) over 18 hours then stabilizes
|
||
// Mint activity ramps to 5x baseline during the dip
|
||
const scenarioA = runScenario(
|
||
'Crash + active mints',
|
||
(block) => {
|
||
const t = block / SIM_BLOCKS;
|
||
if (block < 18 * 6) return 200 - 50 * (block / (18 * 6));
|
||
return 150;
|
||
},
|
||
(block) => {
|
||
// Active counter-cyclical minting during the crash
|
||
if (block < 18 * 6) return 1.5 + 6 * (block / (18 * 6)); // ramp from 1.5 to 7.5
|
||
if (block < 36 * 6) return 7.5 - 4 * ((block - 18 * 6) / (18 * 6)); // peak then decline
|
||
return 3.5;
|
||
},
|
||
() => 1.0 // baseline redeem activity
|
||
);
|
||
|
||
// Scenario B: Rally + active redeems
|
||
// BCH rallies from $200 to $250 (+25%) over 18 hours then stabilizes
|
||
// Redeem activity ramps to 5x baseline as holders cash in premium
|
||
const scenarioB = runScenario(
|
||
'Rally + active redeems',
|
||
(block) => {
|
||
if (block < 18 * 6) return 200 + 50 * (block / (18 * 6));
|
||
return 250;
|
||
},
|
||
() => 1.0, // baseline mint activity
|
||
(block) => {
|
||
if (block < 18 * 6) return 1.5 + 6 * (block / (18 * 6));
|
||
if (block < 36 * 6) return 7.5 - 4 * ((block - 18 * 6) / (18 * 6));
|
||
return 3.5;
|
||
}
|
||
);
|
||
|
||
console.log(JSON.stringify({
|
||
meta: {
|
||
currency: 'A new country currency (fresh launch, 3 catalysts in)',
|
||
initial_state: { N_up: 1, N_down: 4, d_up: '11.5%', d_down: '8.5%', catalysts: 3 },
|
||
adaptive_params: {
|
||
ewma_half_life: '1 day',
|
||
flow_threshold: '1.5x mint/redeem ratio',
|
||
dormant_threshold: '50% of baseline flow',
|
||
slowdown_multiplier: '2.0x',
|
||
speedup_multiplier: '0.5x',
|
||
},
|
||
},
|
||
scenarios: [scenarioA, scenarioB],
|
||
}, null, 2));
|