Four moves (git mv, history preserved on all 39 files): - ASM_Potidaea_OrbitalSwap -> ASM_Native/Potidaea_OrbitalSwap (renamed, dropped the ASM_ prefix; now nested under ASM_Native) - MBA_Shares -> ASM_Native/MBA_Shares - FIATm -> ASM_Native/FIATm - Pari_P2P -> Betting/Pari_P2P ASM_Native now groups the native-BCH swap (Potidaea) with the native asset families it swaps (FIATμ/BCHm, MBA/shares). Rewrote ASM_Native/README.md from a pointer stub into a real folder index describing the contained subfolders + how they fit (mechanism vs assets). Rewrote every reference: Potidaea internal links (depth +1; self-folder refs collapsed), external links from DEX_Anthemus/wiki/ASM_CrossChain, prose/path refs across Parameters + wiki + CLAUDE.md + docs + code-comment doc-pointers (scripts/src/contracts), memory, and .gitignore (transcripts ignore rule). Co-moved siblings (FIATm<->MBA_Shares) kept their ../sibling links untouched since they stayed siblings under ASM_Native. Verified with a link-resolver: all 453 relative links across 165 files resolve. Snapshots keep old paths (historical changelog). Parallel FX-desk/uniform-reserve track left untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
468 lines
23 KiB
JavaScript
468 lines
23 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* sim-usdm-historical-coingecko.mjs — historical USDm controller back-test using
|
||
* REAL BCH/USD price data fetched from CoinGecko (public free API).
|
||
*
|
||
* Variant of `sim-usdm-historical-bch.mjs` (snapshot 0148). That script uses
|
||
* hardcoded approximated monthly closes; this one fetches actual daily prices
|
||
* for the full BCH history (Aug 2017 → now) and runs the §12 controller against
|
||
* the real series.
|
||
*
|
||
* Outputs:
|
||
* 1. Terminal summary (early-adopter outcome, controller end-state, etc.).
|
||
* 2. HTML chart (`.usdm-historical-chart.html`) with three series:
|
||
* BCH/USD spot · oracle target rate (sats/$1) · controller rate (sats/$1).
|
||
*
|
||
* Run: node scripts/sim-usdm-historical-coingecko.mjs
|
||
* open .usdm-historical-chart.html in a browser
|
||
*/
|
||
import { writeFileSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
|
||
// ── §12 controller constants (mirror sim-usdm-controller.mjs / -historical-bch.mjs) ──
|
||
const RETARGET_BLOCKS = 2016;
|
||
const TIME_DOUBLING_BLOCKS = 210_000;
|
||
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 W_PRICE = 1.0, W_FLOW = 0.5, W_SUPPLY = 0.5;
|
||
const K_MAX = 5;
|
||
const DEADBAND_BPS = 50, CAP_BPS = 1000;
|
||
const BLOCKS_PER_DAY = 144;
|
||
const DAYS_PER_MONTH = 30;
|
||
const BLOCKS_PER_MONTH = BLOCKS_PER_DAY * DAYS_PER_MONTH;
|
||
|
||
// ── BCH/USD daily fetch ───────────────────────────────────────────────────
|
||
// CoinGecko, CryptoCompare, and CoinPaprika all moved historical data behind
|
||
// API keys on their free tiers. Binance's public klines endpoint still serves
|
||
// full daily OHLCV without authentication. BCH/USDT was listed on Binance
|
||
// 2018-09 (we paginate back to listing date). For data before that, we layer
|
||
// CoinGecko's last-365-days endpoint on top for the most recent year — the
|
||
// "use updated CoinGecko price feed" requirement.
|
||
async function fetchBchUsd() {
|
||
console.log('Fetching BCH/USD daily history (Binance klines, paginated)...');
|
||
const headers = { Accept: 'application/json', 'User-Agent': 'Deviant-FIATm-sim/1.0' };
|
||
const ALL = new Map(); // ts_ms -> close
|
||
let endTime = Date.now();
|
||
let page = 0;
|
||
const STOP = new Date('2018-09-01').getTime(); // before BCH/USDT listing
|
||
while (endTime > STOP && page < 6) {
|
||
const url = `https://api.binance.com/api/v3/klines?symbol=BCHUSDT&interval=1d&limit=1000&endTime=${endTime}`;
|
||
const res = await fetch(url, { headers });
|
||
if (!res.ok) throw new Error(`Binance ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
||
const data = await res.json();
|
||
if (!Array.isArray(data) || data.length === 0) break;
|
||
// Each kline: [openTime, open, high, low, close, volume, closeTime, ...]
|
||
let oldest = Infinity;
|
||
for (const k of data) {
|
||
const ts = k[0];
|
||
const close = parseFloat(k[4]);
|
||
if (close > 0) ALL.set(ts, close);
|
||
if (ts < oldest) oldest = ts;
|
||
}
|
||
page++;
|
||
console.log(` page ${page}: ${data.length} candles · oldest ${new Date(oldest).toISOString().slice(0, 10)}`);
|
||
if (oldest >= endTime) break;
|
||
endTime = oldest - 1;
|
||
}
|
||
if (ALL.size === 0) throw new Error('Binance returned no usable data');
|
||
|
||
// Layer the last-365-days from CoinGecko on top (the "updated CoinGecko feed").
|
||
try {
|
||
console.log('Refreshing the last 365 days from CoinGecko (free tier)...');
|
||
const cgUrl = 'https://api.coingecko.com/api/v3/coins/bitcoin-cash/market_chart?vs_currency=usd&days=365';
|
||
const cgRes = await fetch(cgUrl, { headers });
|
||
if (cgRes.ok) {
|
||
const cg = await cgRes.json();
|
||
if (Array.isArray(cg.prices)) {
|
||
let overlaid = 0;
|
||
// CoinGecko returns multiple data points per day; bucket by UTC date and take last.
|
||
const byDay = new Map();
|
||
for (const [ts, p] of cg.prices) {
|
||
const day = new Date(ts).toISOString().slice(0, 10);
|
||
byDay.set(day, [ts, p]);
|
||
}
|
||
for (const [, [ts, p]] of byDay) {
|
||
ALL.set(ts, p);
|
||
overlaid++;
|
||
}
|
||
console.log(` CoinGecko overlay: ${overlaid} fresh days merged in`);
|
||
}
|
||
} else {
|
||
console.log(` (CoinGecko overlay skipped: ${cgRes.status})`);
|
||
}
|
||
} catch (e) {
|
||
console.log(` (CoinGecko overlay skipped: ${e.message})`);
|
||
}
|
||
|
||
return [...ALL.entries()].sort((a, b) => a[0] - b[0]);
|
||
}
|
||
|
||
function monthlyCloses(daily) {
|
||
const byMonth = new Map();
|
||
for (const [ts, price] of daily) {
|
||
const d = new Date(ts);
|
||
const key = `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`;
|
||
byMonth.set(key, { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, price, ts });
|
||
}
|
||
return [...byMonth.values()].sort((a, b) => a.ts - b.ts);
|
||
}
|
||
|
||
function buildPriceArray(monthly) {
|
||
const prices = [];
|
||
for (let i = 0; i < monthly.length - 1; i++) {
|
||
const p0 = monthly[i].price, p1 = monthly[i + 1].price;
|
||
for (let b = 0; b < BLOCKS_PER_MONTH; b++) {
|
||
const t = b / BLOCKS_PER_MONTH;
|
||
prices.push(p0 + (p1 - p0) * t);
|
||
}
|
||
}
|
||
for (let b = 0; b < BLOCKS_PER_MONTH; b++) prices.push(monthly[monthly.length - 1].price);
|
||
return prices;
|
||
}
|
||
|
||
// Phase-based adoption curve (same as sim-usdm-historical-bch.mjs).
|
||
function mintsAtBlock(block) {
|
||
const me = Math.floor(block / BLOCKS_PER_MONTH);
|
||
if (me < 16) return 5;
|
||
if (me < 40) return 15;
|
||
if (me < 53) return 80;
|
||
if (me < 66) return 25;
|
||
if (me < 90) return 60;
|
||
return 100;
|
||
}
|
||
function redeemsAtBlock(block) {
|
||
const mints = mintsAtBlock(block);
|
||
const me = Math.floor(block / BLOCKS_PER_MONTH);
|
||
if (me >= 53 && me < 66) return Math.floor(mints * 0.4);
|
||
if (me >= 16 && me < 24) return Math.floor(mints * 0.3);
|
||
return Math.floor(mints * 0.15);
|
||
}
|
||
|
||
// ── §12 Controller (mirror) ───────────────────────────────────────────────
|
||
class Controller {
|
||
constructor(initialRate) {
|
||
this.rate = initialRate;
|
||
this.N_up = 1; this.N_down = 1;
|
||
this.d_up = 0.10; this.d_down = 0.10;
|
||
this.doubling_epoch = 0;
|
||
this.catalyst_count = 0;
|
||
this.last_recal_block = 0;
|
||
this.last_retarget_block = 0;
|
||
this.cumulative_minted = 0;
|
||
this.window_mints = 0; this.window_redeems = 0;
|
||
this.window_rate_moves = 0;
|
||
this.window_supply_start = 0;
|
||
this.prev_window_growth = 0;
|
||
}
|
||
cadenceBlocks(direction) {
|
||
const N = direction === 'down' ? this.N_down : this.N_up;
|
||
return N * Math.pow(2, this.doubling_epoch);
|
||
}
|
||
step(block, bch_price, mints, redeems) {
|
||
this.cumulative_minted += mints;
|
||
this.window_mints += mints;
|
||
this.window_redeems += redeems;
|
||
const expected_epoch = Math.floor(block / TIME_DOUBLING_BLOCKS);
|
||
if (expected_epoch > this.doubling_epoch) this.doubling_epoch = expected_epoch;
|
||
const expected_catalysts = Math.floor(this.cumulative_minted / SUPPLY_CATALYST);
|
||
while (expected_catalysts > this.catalyst_count) { this.catalyst_count += 1; this.applyCatalyst(); }
|
||
if (block - this.last_retarget_block >= RETARGET_BLOCKS) {
|
||
this.applyRetarget();
|
||
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;
|
||
}
|
||
const target = Math.round(100_000_000 / bch_price);
|
||
const direction = target > this.rate ? 'down' : 'up';
|
||
const cadence = this.cadenceBlocks(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;
|
||
}
|
||
}
|
||
}
|
||
applyCatalyst() {
|
||
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);
|
||
}
|
||
applyRetarget() {
|
||
const net_dir = Math.sign(this.window_rate_moves);
|
||
const flow_ratio = this.window_mints / Math.max(this.window_redeems, 1);
|
||
const window_growth = this.cumulative_minted - this.window_supply_start;
|
||
const growth_accel = this.prev_window_growth > 0 ? window_growth / this.prev_window_growth : 1.0;
|
||
const ds = (raw, t1, t2, t3) => raw === 0 ? 0 : Math.sign(raw) * (Math.abs(raw) >= t3 ? 3 : Math.abs(raw) >= t2 ? 2 : Math.abs(raw) >= t1 ? 1 : 0);
|
||
const dprice = ds(net_dir * 3, 1, 2, 3);
|
||
const dflow = ds(flow_ratio - 1, 0.2, 1.0, 5.0) * (flow_ratio > 1 ? 1 : -1);
|
||
const dsupply = ds(growth_accel - 1, 0.1, 1.0, 4.0) * (growth_accel > 1 ? 1 : -1);
|
||
const sign_up_price = dprice < 0 ? dprice : 0;
|
||
const sign_down_price = dprice > 0 ? dprice : 0;
|
||
const sum_up = W_PRICE * sign_up_price + W_FLOW * dflow + W_SUPPLY * (-dsupply);
|
||
const sum_down = W_PRICE * sign_down_price + W_FLOW * dflow + W_SUPPLY * (-Math.abs(dsupply));
|
||
const delta_up = Math.max(-K_MAX, Math.min(K_MAX, Math.round(sum_up)));
|
||
const delta_down = Math.max(-K_MAX, Math.min(K_MAX, Math.round(sum_down)));
|
||
this.N_up = Math.max(N_FLOOR, Math.min(N_CEILING, this.N_up + delta_up));
|
||
this.N_down = Math.max(N_FLOOR, Math.min(N_CEILING, this.N_down + delta_down));
|
||
}
|
||
}
|
||
|
||
// ── Main ──────────────────────────────────────────────────────────────────
|
||
async function main() {
|
||
const t0 = Date.now();
|
||
const daily = await fetchBchUsd();
|
||
console.log(`Got ${daily.length} daily price points in ${Date.now() - t0} ms`);
|
||
|
||
const monthly = monthlyCloses(daily);
|
||
const lo = Math.min(...monthly.map(m => m.price));
|
||
const hi = Math.max(...monthly.map(m => m.price));
|
||
console.log(`Compressed to ${monthly.length} monthly closes (${monthly[0].year}-${String(monthly[0].month).padStart(2,'0')} → ${monthly[monthly.length-1].year}-${String(monthly[monthly.length-1].month).padStart(2,'0')})`);
|
||
console.log(`BCH/USD range: $${lo.toFixed(2)} → $${hi.toFixed(2)}`);
|
||
|
||
const priceArray = buildPriceArray(monthly);
|
||
const totalBlocks = priceArray.length;
|
||
const initialRate = Math.round(100_000_000 / priceArray[0]);
|
||
const ctrl = new Controller(initialRate);
|
||
|
||
// Sample monthly for the chart.
|
||
const samples = [];
|
||
for (let block = 0; block < totalBlocks; block++) {
|
||
ctrl.step(block, priceArray[block], mintsAtBlock(block), redeemsAtBlock(block));
|
||
if (block % BLOCKS_PER_MONTH === 0 || block === totalBlocks - 1) {
|
||
const target = Math.round(100_000_000 / priceArray[block]);
|
||
const gap = (ctrl.rate - target) / target;
|
||
const monthIdx = Math.min(Math.floor(block / BLOCKS_PER_MONTH), monthly.length - 1);
|
||
const m = monthly[monthIdx];
|
||
samples.push({
|
||
date: `${m.year}-${String(m.month).padStart(2, '0')}`,
|
||
bch_price: +priceArray[block].toFixed(2),
|
||
oracle_target: target,
|
||
controller_rate: ctrl.rate,
|
||
gap_pct: +(gap * 100).toFixed(2),
|
||
N_up: ctrl.N_up, N_down: ctrl.N_down,
|
||
d_up: +(ctrl.d_up * 100).toFixed(1), d_down: +(ctrl.d_down * 100).toFixed(1),
|
||
catalysts: ctrl.catalyst_count,
|
||
cumulative_minted: ctrl.cumulative_minted,
|
||
});
|
||
}
|
||
}
|
||
|
||
// Holder outcomes.
|
||
const finalBchUsd = priceArray[totalBlocks - 1];
|
||
const earlyLockedSats = 100 * initialRate; // $100 USDm at the very first month
|
||
const earlyRealUsdNow = (earlyLockedSats / 1e8) * finalBchUsd;
|
||
// Bargain-hunter (lowest BCH price month):
|
||
const bargainMonthIdx = monthly.findIndex(m => m.price === lo);
|
||
const bargainMintBlock = bargainMonthIdx * BLOCKS_PER_MONTH;
|
||
const bargainSampleRate = samples.find(s => s.date === `${monthly[bargainMonthIdx].year}-${String(monthly[bargainMonthIdx].month).padStart(2,'0')}`)?.controller_rate ?? Math.round(100_000_000 / lo);
|
||
const bargainLockedSats = 100 * bargainSampleRate;
|
||
const bargainRealUsdNow = (bargainLockedSats / 1e8) * finalBchUsd;
|
||
|
||
// ── Terminal summary ─────────────────────────────────────────────────
|
||
console.log('');
|
||
console.log('═'.repeat(80));
|
||
console.log(' USDm historical back-test (CoinGecko data) — summary');
|
||
console.log('═'.repeat(80));
|
||
console.log(` Window: ${monthly[0].year}-${String(monthly[0].month).padStart(2,'0')} → ${monthly[monthly.length-1].year}-${String(monthly[monthly.length-1].month).padStart(2,'0')} (${monthly.length} months, ${totalBlocks.toLocaleString()} simulated blocks)`);
|
||
console.log(` Final BCH/USD: $${finalBchUsd.toFixed(2)}`);
|
||
console.log('');
|
||
console.log(` Early adopter — minted $100 USDm in ${monthly[0].year}-${String(monthly[0].month).padStart(2,'0')} at BCH=$${priceArray[0].toFixed(0)}:`);
|
||
console.log(` Locked sats: ${earlyLockedSats.toLocaleString()}`);
|
||
console.log(` Real USD value now: $${earlyRealUsdNow.toFixed(2)} (${earlyRealUsdNow >= 100 ? '+' : ''}$${(earlyRealUsdNow - 100).toFixed(2)} = ${((earlyRealUsdNow / 100 - 1) * 100).toFixed(1)}%)`);
|
||
console.log('');
|
||
console.log(` Bargain-hunter — minted $100 USDm at BCH cycle low (${monthly[bargainMonthIdx].year}-${String(monthly[bargainMonthIdx].month).padStart(2,'0')}, BCH=$${lo.toFixed(0)}):`);
|
||
console.log(` Locked sats: ${bargainLockedSats.toLocaleString()}`);
|
||
console.log(` Real USD value now: $${bargainRealUsdNow.toFixed(2)} (${((bargainRealUsdNow / 100 - 1) * 100).toFixed(1)}%)`);
|
||
console.log('');
|
||
console.log(` Controller end-state:`);
|
||
console.log(` N_up = ${ctrl.N_up}, N_down = ${ctrl.N_down}`);
|
||
console.log(` d_up = ${(ctrl.d_up * 100).toFixed(1)}%, d_down = ${(ctrl.d_down * 100).toFixed(1)}%`);
|
||
console.log(` Catalysts fired: ${ctrl.catalyst_count}`);
|
||
console.log(` Cumulative minted: ${ctrl.cumulative_minted.toLocaleString()} units`);
|
||
console.log(` Doubling regime: ${ctrl.doubling_epoch}`);
|
||
console.log('');
|
||
|
||
// ── HTML chart ───────────────────────────────────────────────────────
|
||
const html = renderChart(samples, {
|
||
initialBchUsd: priceArray[0],
|
||
finalBchUsd,
|
||
earlyLockedSats,
|
||
earlyRealUsdNow,
|
||
bargainLockedSats,
|
||
bargainRealUsdNow,
|
||
bargainDate: `${monthly[bargainMonthIdx].year}-${String(monthly[bargainMonthIdx].month).padStart(2,'0')}`,
|
||
bargainBchUsd: lo,
|
||
ctrl,
|
||
months: monthly.length,
|
||
dailyPoints: daily.length,
|
||
windowStart: `${monthly[0].year}-${String(monthly[0].month).padStart(2,'0')}`,
|
||
windowEnd: `${monthly[monthly.length-1].year}-${String(monthly[monthly.length-1].month).padStart(2,'0')}`,
|
||
});
|
||
const out = join(import.meta.dirname, '..', '.usdm-historical-chart.html');
|
||
writeFileSync(out, html);
|
||
console.log(`Chart written to: ${out}`);
|
||
console.log(`Open it in a browser to view (file:// path works in Chrome/Firefox).`);
|
||
}
|
||
|
||
function renderChart(samples, s) {
|
||
return `<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>USDm Historical Backtest — CoinGecko</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||
<style>
|
||
:root {
|
||
--bg: #0a0a0a; --panel: #141414; --border: #222; --fg: #e8e8e8; --muted: #888;
|
||
--gold: #fbbf24; --cyan: #22d3ee; --green: #4ade80; --rose: #fb7185;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body { margin: 0; padding: 24px; background: var(--bg); color: var(--fg); font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; }
|
||
h1 { margin: 0 0 6px; font-size: 22px; font-weight: 600; }
|
||
.sub { color: var(--muted); margin-bottom: 24px; font-size: 13px; }
|
||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 16px; }
|
||
.card .lbl { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.6px; margin-bottom: 6px; }
|
||
.card .val { font-size: 22px; font-weight: 600; }
|
||
.card .sub2 { color: var(--muted); font-size: 12px; margin-top: 4px; }
|
||
.v-gold { color: var(--gold); } .v-cyan { color: var(--cyan); } .v-green { color: var(--green); } .v-rose { color: var(--rose); }
|
||
.chart-wrap { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 20px; margin-bottom: 16px; }
|
||
.chart-wrap h2 { margin: 0 0 12px; font-size: 14px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.6px; }
|
||
canvas { display: block; max-width: 100%; }
|
||
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||
th, td { padding: 6px 10px; text-align: right; border-bottom: 1px solid var(--border); }
|
||
th { color: var(--muted); font-weight: 500; text-align: left; }
|
||
th:first-child, td:first-child { text-align: left; }
|
||
.footer { color: var(--muted); font-size: 12px; margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>USDm Historical Back-test — Real BCH/USD from CoinGecko</h1>
|
||
<div class="sub">${s.windowStart} → ${s.windowEnd} · ${s.months} months · ${s.dailyPoints.toLocaleString()} daily price points · §12 controller @ snapshot 0148 defaults</div>
|
||
|
||
<div class="grid">
|
||
<div class="card">
|
||
<div class="lbl">BCH/USD now</div>
|
||
<div class="val v-gold">$${s.finalBchUsd.toFixed(2)}</div>
|
||
<div class="sub2">from $${s.initialBchUsd.toFixed(2)} at start</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="lbl">Early adopter $100 USDm</div>
|
||
<div class="val v-green">$${s.earlyRealUsdNow.toFixed(2)}</div>
|
||
<div class="sub2">${((s.earlyRealUsdNow / 100 - 1) * 100).toFixed(1)}% · ${s.earlyLockedSats.toLocaleString()} sats locked</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="lbl">Bargain hunter $100 USDm</div>
|
||
<div class="val v-green">$${s.bargainRealUsdNow.toFixed(2)}</div>
|
||
<div class="sub2">${((s.bargainRealUsdNow / 100 - 1) * 100).toFixed(1)}% · minted ${s.bargainDate} at BCH=$${s.bargainBchUsd.toFixed(0)}</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="lbl">Controller end-state</div>
|
||
<div class="val v-cyan">N↑=${s.ctrl.N_up} · N↓=${s.ctrl.N_down}</div>
|
||
<div class="sub2">d↑=${(s.ctrl.d_up * 100).toFixed(1)}% · d↓=${(s.ctrl.d_down * 100).toFixed(1)}% · ${s.ctrl.catalyst_count} catalysts</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="chart-wrap">
|
||
<h2>BCH/USD spot vs §12 controller rate (log scale)</h2>
|
||
<canvas id="chart-rate" height="120"></canvas>
|
||
</div>
|
||
|
||
<div class="chart-wrap">
|
||
<h2>Controller-vs-oracle gap (% · positive = controller above oracle target; negative = bargain window)</h2>
|
||
<canvas id="chart-gap" height="80"></canvas>
|
||
</div>
|
||
|
||
<div class="chart-wrap">
|
||
<h2>DART asymmetric bias accumulation (cadence + damping per direction)</h2>
|
||
<canvas id="chart-bias" height="80"></canvas>
|
||
</div>
|
||
|
||
<div class="chart-wrap">
|
||
<h2>Last 12 monthly samples</h2>
|
||
<table>
|
||
<thead><tr>
|
||
<th>date</th><th>BCH/USD</th><th>oracle (sats/$)</th><th>controller (sats/$)</th>
|
||
<th>gap%</th><th>N↑</th><th>N↓</th><th>d↑</th><th>d↓</th><th>catalysts</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
${samples.slice(-12).map(r => ` <tr><td>${r.date}</td><td>$${r.bch_price.toFixed(2)}</td><td>${r.oracle_target.toLocaleString()}</td><td>${r.controller_rate.toLocaleString()}</td><td>${(r.gap_pct >= 0 ? '+' : '') + r.gap_pct.toFixed(2)}%</td><td>${r.N_up}</td><td>${r.N_down}</td><td>${r.d_up}%</td><td>${r.d_down}%</td><td>${r.catalysts}</td></tr>`).join('\n')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="footer">Data: CoinGecko public API · Controller: §12 of <code>Parameters/ASM_Native/FIATm/fiatm_complete-design_FOR-design-record.md</code> · Generated by <code>scripts/sim-usdm-historical-coingecko.mjs</code></div>
|
||
|
||
<script>
|
||
const samples = ${JSON.stringify(samples)};
|
||
const labels = samples.map(s => s.date);
|
||
const gridCol = '#1a1a1a', axisCol = '#888';
|
||
Chart.defaults.color = '#bbb';
|
||
Chart.defaults.borderColor = gridCol;
|
||
const commonScales = { x: { grid: { color: gridCol }, ticks: { color: axisCol, maxRotation: 0, autoSkipPadding: 24 } } };
|
||
|
||
new Chart(document.getElementById('chart-rate'), {
|
||
type: 'line',
|
||
data: { labels, datasets: [
|
||
{ label: 'BCH/USD spot', data: samples.map(s => s.bch_price), borderColor: '#fbbf24', backgroundColor: 'rgba(251,191,36,0.08)', yAxisID: 'price', pointRadius: 0, borderWidth: 2, tension: 0.2, fill: false },
|
||
{ label: 'Oracle target (sats/$1)', data: samples.map(s => s.oracle_target), borderColor: '#22d3ee', yAxisID: 'rate', pointRadius: 0, borderWidth: 1.5, borderDash: [4, 3], tension: 0.2, fill: false },
|
||
{ label: 'Controller rate (sats/$1)', data: samples.map(s => s.controller_rate), borderColor: '#4ade80', backgroundColor: 'rgba(74,222,128,0.06)', yAxisID: 'rate', pointRadius: 0, borderWidth: 2, tension: 0.2, fill: false },
|
||
]},
|
||
options: {
|
||
responsive: true, maintainAspectRatio: false,
|
||
interaction: { mode: 'index', intersect: false },
|
||
plugins: { legend: { position: 'top', labels: { boxWidth: 14 } } },
|
||
scales: {
|
||
...commonScales,
|
||
price: { type: 'logarithmic', position: 'left', title: { display: true, text: 'BCH/USD ($, log)', color: '#fbbf24' }, ticks: { color: '#fbbf24' }, grid: { color: gridCol } },
|
||
rate: { type: 'logarithmic', position: 'right', title: { display: true, text: 'sats per $1 (log)', color: '#4ade80' }, ticks: { color: '#4ade80' }, grid: { drawOnChartArea: false } },
|
||
},
|
||
},
|
||
});
|
||
|
||
new Chart(document.getElementById('chart-gap'), {
|
||
type: 'line',
|
||
data: { labels, datasets: [
|
||
{ label: 'controller − oracle (%)', data: samples.map(s => s.gap_pct), borderColor: '#fb7185', backgroundColor: ctx => { const v = ctx.parsed?.y ?? 0; return v < 0 ? 'rgba(251,113,133,0.12)' : 'rgba(74,222,128,0.08)'; }, pointRadius: 0, borderWidth: 1.5, tension: 0.2, fill: true },
|
||
]},
|
||
options: {
|
||
responsive: true, maintainAspectRatio: false,
|
||
plugins: { legend: { display: false } },
|
||
scales: { ...commonScales, y: { title: { display: true, text: 'gap %' }, grid: { color: gridCol } } },
|
||
},
|
||
});
|
||
|
||
new Chart(document.getElementById('chart-bias'), {
|
||
type: 'line',
|
||
data: { labels, datasets: [
|
||
{ label: 'N↑ (rally cadence)', data: samples.map(s => s.N_up), borderColor: '#22d3ee', pointRadius: 0, borderWidth: 1.5, tension: 0.2, fill: false },
|
||
{ label: 'N↓ (crash cadence)', data: samples.map(s => s.N_down), borderColor: '#fb7185', pointRadius: 0, borderWidth: 2, tension: 0.2, fill: false },
|
||
{ label: 'catalysts fired', data: samples.map(s => s.catalysts), borderColor: '#fbbf24', pointRadius: 0, borderWidth: 1.5, borderDash: [3, 3], tension: 0.2, fill: false, yAxisID: 'cat' },
|
||
]},
|
||
options: {
|
||
responsive: true, maintainAspectRatio: false,
|
||
plugins: { legend: { position: 'top', labels: { boxWidth: 14 } } },
|
||
scales: {
|
||
...commonScales,
|
||
y: { type: 'linear', position: 'left', title: { display: true, text: 'cadence (blocks)' }, grid: { color: gridCol } },
|
||
cat: { type: 'linear', position: 'right', title: { display: true, text: 'catalysts', color: '#fbbf24' }, ticks: { color: '#fbbf24' }, grid: { drawOnChartArea: false } },
|
||
},
|
||
},
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
main().catch((e) => { console.error('FAILED:', e.message); console.error(e.stack); process.exit(1); });
|