// Volatility analysis for Washington's millionaires tax (ESSB 6346): an illustrative back-cast and crash scenarios. // OUR ILLUSTRATION, NOT A FORECAST. Run: node volatility.js // Inputs (same folder): // wa_irs_1m_plus_history.csv IRS SOI: WA Historic Table 2 (1997-2001, 2010-2023 have a $1M+ class; // 2002-2009 top out at $200k+), US Table 1.4 $1M+ (1998-2023) // wa_revenue_by_source_history.csv WA sales / B&O / property collections by fiscal year (optional) // Outputs: wa_millionaires_tax_backcast.csv, wa_crash_scenarios.csv, volatility_results.json // // Back-cast method: the tax is 9.9% of income above $1M, so revenue scales with the "excess base", // excess = AGI of $1M+ returns - (number of $1M+ returns x $1M). // Gross illustrative tax = 9.9% x excess. Ignores: the shared $1M deduction for couples filing separately, // the long-term capital gains swap and credit for WA capital gains tax (sec. 302), the 4.71% enforcement haircut, // 90-95% compliance, inflation indexing of the threshold, and any behavioral response. The $1M threshold is applied in // nominal dollars in every year, so early years overstate the threshold's real value. const fs = require("fs"), path = require("path"); const dir = __dirname; const RATE = 0.099; function readCsv(file) { const lines = fs.readFileSync(path.join(dir, file), "utf8").trim().split(/\r?\n/); const h = lines[0].split(","); return lines.slice(1).map(l => { const v = l.split(","), o = {}; h.forEach((k, i) => { o[k] = v[i] === "" || v[i] === undefined ? null : (isNaN(+v[i]) ? v[i] : +v[i]); }); return o; }); } const pct = (a, b) => (a == null || b == null || b === 0) ? null : +(100 * (a / b - 1)).toFixed(1); const r2 = x => x == null ? null : +x.toFixed(2); const r3 = x => x == null ? null : +x.toFixed(3); // ---- 1. Back-cast ---- const irs = readCsv("wa_irs_1m_plus_history.csv").filter(r => r.topClass === "1M+" && (r.geography === "WA" || r.geography === "US")); const backcast = []; for (const geo of ["WA", "US"]) { const rows = irs.filter(r => r.geography === geo).sort((a, b) => a.taxYear - b.taxYear); let prev = null; for (const r of rows) { const excess = r.agi1mPlusBillion - r.returns1mPlus / 1000; // $B: returns x $1M = returns/1000 $B const tax = RATE * excess; const exCg = r.netCapitalGain1mPlusBillion == null ? null : RATE * Math.max(0, excess - r.netCapitalGain1mPlusBillion); const consecutive = prev && prev.taxYear === r.taxYear - 1; backcast.push({ taxYear: r.taxYear, geography: geo, returns1mPlus: r.returns1mPlus, agi1mPlusBillion: r3(r.agi1mPlusBillion), netCapitalGain1mPlusBillion: r3(r.netCapitalGain1mPlusBillion), excessOver1mBillion: r3(excess), illustrativeTaxBillion: r3(tax), illustrativeTaxExNetCgBillion: r3(exCg), agiYoYPct: consecutive ? pct(r.agi1mPlusBillion, prev.agi1mPlusBillion) : null, netCgYoYPct: consecutive ? pct(r.netCapitalGain1mPlusBillion, prev.netCapitalGain1mPlusBillion) : null, illustrativeTaxYoYPct: consecutive ? pct(tax, prev.tax) : null, label: "OUR ILLUSTRATION: 9.9% x (AGI - returns x $1M), gross, static, nominal $1M threshold", }); prev = { taxYear: r.taxYear, agi1mPlusBillion: r.agi1mPlusBillion, netCapitalGain1mPlusBillion: r.netCapitalGain1mPlusBillion, tax }; } } const bc = (geo, y) => backcast.find(r => r.geography === geo && r.taxYear === y); const drop = (geo, y0, y1, k = "illustrativeTaxBillion") => +(1 - bc(geo, y1)[k] / bc(geo, y0)[k]).toFixed(3); // ---- 2. Crash scenarios against official baselines ---- // Baselines: ERFC Sept 25, 2026 forecast, 2029-31 biennium $9.435B (FY30-31) -> ~$4.72B/yr; // ESSB 6346 fiscal note (OFM FNS pkg 77653) FY30 $3.732B (first full year). const baselines = [ { name: "ERFC Sept 2026, FY30-31 average", annualBillion: 9.435 / 2 }, { name: "Fiscal note pkg 77653, FY30", annualBillion: 3.732 }, ]; const scenarios = [ { scenario: "2022-style (WA, TY2021 to TY2022)", geo: "WA", from: 2021, to: 2022, years: 1 }, { scenario: "2022-23 two-year (WA, TY2021 to TY2023)", geo: "WA", from: 2021, to: 2023, years: 2 }, { scenario: "Dot-com (WA, TY2000 to TY2001)", geo: "WA", from: 2000, to: 2001, years: 1 }, { scenario: "Dot-com peak to trough (US $1M+, TY2000 to TY2002)", geo: "US", from: 2000, to: 2002, years: 2 }, { scenario: "Great Recession (US $1M+, TY2007 to TY2009)", geo: "US", from: 2007, to: 2009, years: 2 }, { scenario: "Great Recession, first year (US $1M+, TY2007 to TY2008)", geo: "US", from: 2007, to: 2008, years: 1 }, ]; const crash = []; for (const s of scenarios) { const d = drop(s.geo, s.from, s.to), dx = drop(s.geo, s.from, s.to, "illustrativeTaxExNetCgBillion"); for (const b of baselines) { crash.push({ scenario: s.scenario, baseline: b.name, excessBaseDropPct: +(100 * d).toFixed(1), baselineAnnualBillion: r2(b.annualBillion), revenueAfterBillion: r2(b.annualBillion * (1 - d)), annualHitBillion: r2(b.annualBillion * d), // Variant: base excluding all net capital gains (closer to the income tax alone, since sec. 302/205 shift // long-term gains to the capital gains tax). SOI net CG includes short-term gains, so this understates the base. exNetCgBaseDropPct: +(100 * dx).toFixed(1), exNetCgAnnualHitBillion: r2(b.annualBillion * dx), label: "OUR ILLUSTRATION, NOT A FORECAST: baseline x historical % drop in the excess-over-$1M base", }); } } // ---- 3. Volatility comparison ---- function stats(series) { // series: array of YoY % changes const v = series.filter(x => x != null); const mean = v.reduce((a, b) => a + b, 0) / v.length; const sd = Math.sqrt(v.reduce((a, b) => a + (b - mean) ** 2, 0) / (v.length - 1)); return { n: v.length, meanYoYPct: r2(mean), sdYoYPct: r2(sd), minYoYPct: Math.min(...v), maxYoYPct: Math.max(...v), yearsDown: v.filter(x => x < 0).length }; } const vol = { waIllustrativeTax: stats(backcast.filter(r => r.geography === "WA").map(r => r.illustrativeTaxYoYPct)), waAgi1mPlus: stats(backcast.filter(r => r.geography === "WA").map(r => r.agiYoYPct)), waIllustrativeTaxExNetCg: stats((() => { const w = backcast.filter(r => r.geography === "WA"); return w.map((r, i) => i && w[i - 1].taxYear === r.taxYear - 1 ? pct(r.illustrativeTaxExNetCgBillion, w[i - 1].illustrativeTaxExNetCgBillion) : null); })()), usIllustrativeTax: stats(backcast.filter(r => r.geography === "US").map(r => r.illustrativeTaxYoYPct)), }; const capGains = [782.0, 394.7, 583.8, 1500]; // DOR, TY2022-25, $M (TY2025 rounded, includes 2.9% surcharge) vol.waCapitalGainsTax = stats(capGains.slice(1).map((x, i) => pct(x, capGains[i]))); const srcFile = path.join(dir, "wa_revenue_by_source_history.csv"); if (fs.existsSync(srcFile)) { const src = readCsv("wa_revenue_by_source_history.csv").sort((a, b) => a.fiscalYear - b.fiscalYear); for (const k of ["retailSalesMillion", "boMillion", "statePropertyMillion", "totalGfsMillion"]) { // Total GF-S switches from ERFC's older "GF-S plus related fund" series (FY1998-2004) to the current definition (FY2005+), so FY2005 change is left blank. const yoy = src.map((r, i) => i && src[i - 1].fiscalYear === r.fiscalYear - 1 && !(k === "totalGfsMillion" && r.fiscalYear === 2005) ? pct(r[k], src[i - 1][k]) : null); if (yoy.some(x => x != null)) vol[k.replace("Million", "")] = stats(yoy); } } // ---- Write ---- function writeCsv(file, rows) { const h = Object.keys(rows[0]); const esc = v => v == null ? "" : (/[",]/.test(String(v)) ? `"${String(v).replace(/"/g, '""')}"` : v); fs.writeFileSync(path.join(dir, file), [h.join(","), ...rows.map(r => h.map(k => esc(r[k])).join(","))].join("\n") + "\n"); } writeCsv("wa_millionaires_tax_backcast.csv", backcast); writeCsv("wa_crash_scenarios.csv", crash); fs.writeFileSync(path.join(dir, "volatility_results.json"), JSON.stringify({ volatility: vol, crash }, null, 2)); console.log(JSON.stringify(vol, null, 1)); for (const c of crash) console.log(c.scenario.padEnd(58), c.baseline.padEnd(34), c.excessBaseDropPct + "%", "(exCG " + c.exNetCgBaseDropPct + "% $" + c.exNetCgAnnualHitBillion + "B)", "$" + c.annualHitBillion + "B hit ->", "$" + c.revenueAfterBillion + "B"); for (const r of backcast.filter(r => r.geography === "WA")) console.log(r.taxYear, r.excessOver1mBillion, r.illustrativeTaxBillion, r.illustrativeTaxYoYPct, r.illustrativeTaxExNetCgBillion);