File size: 46,612 Bytes
8570e58
a7aa576
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#101b2e"><meta name="description" content="FXShield is an offline-first educational foreign exchange exposure and hedging scenario simulator."><title>FXShield | Currency Risk Intelligence Studio</title>
<style>
:root{color-scheme:light;--bg:#f3f6fa;--paper:#fff;--soft:#f7f9fc;--ink:#172840;--secondary:#73829a;--border:#e5ebf2;--nav:#111f34;--navtype:#9eafc9;--aqua:#0a9e9d;--aquaLight:#e5f9f5;--blue:#557ce8;--pink:#df6e82;--amber:#dc9852;--shadow:0 10px 32px rgba(32,65,102,.045)}
body.night{color-scheme:dark;--bg:#0d1624;--paper:#19273b;--soft:#22334b;--ink:#edf6ff;--secondary:#a0b2c9;--border:#334459;--nav:#0a1220;--navtype:#a4b2c7;--aqua:#45d4c5;--aquaLight:#203b42;--blue:#8fb1ff;--pink:#ff9ea9;--amber:#f3bc77;--shadow:0 10px 28px rgba(0,0,0,.14)}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.55 system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}button,input,select,textarea{font:inherit}button{cursor:pointer}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid var(--aqua);outline-offset:2px}button:disabled{opacity:.5;cursor:not-allowed}a{color:inherit}.layout{display:grid;grid-template-columns:248px minmax(0,1fr);min-height:100vh}.sidebar{position:sticky;top:0;height:100vh;background:var(--nav);padding:25px 16px;display:flex;flex-direction:column;color:var(--navtype)}.brand{display:flex;align-items:center;gap:11px;padding:2px 9px 31px}.brand-icon{width:44px;height:44px;border-radius:14px;display:grid;place-items:center;background:linear-gradient(145deg,#45d6bd,#2579aa);color:white;font-size:24px;font-weight:900;box-shadow:0 10px 30px #20b6aa2c}.brand b{color:white;font-size:22px;letter-spacing:-.7px}.brand small{font-size:10px;display:block;letter-spacing:1.5px;text-transform:uppercase}.nlabel{font-size:10px;letter-spacing:1.6px;text-transform:uppercase;font-weight:800;color:#7a8da9;padding:8px 12px}.nav{display:grid;gap:5px}.nav button{padding:12px 14px;text-align:left;border:0;background:transparent;color:var(--navtype);border-radius:10px;font-size:12px;font-weight:750;display:flex;align-items:center;gap:12px;width:100%}.nav button:hover{background:#ffffff10;color:white}.nav button.selected{background:#ffffff16;color:white;box-shadow:inset 3px 0 0 #50d4b9}.nav .sym{width:19px;text-align:center;font-size:17px}.sidefoot{margin-top:auto;background:#ffffff0b;border:1px solid #ffffff15;border-radius:13px;padding:14px}.sidefoot b{color:white;font-size:12px}.sidefoot p{margin:4px 0 0;font-size:11px;line-height:1.65}.copyright{text-align:center;padding-top:16px;font-size:10px;color:#778cac}.main{min-width:0;max-width:1780px;width:100%;margin:0 auto;padding:23px clamp(18px,3.1vw,48px) 32px}.top{display:flex;justify-content:space-between;gap:14px;align-items:center;min-height:39px}.bread{font-size:11px;font-weight:800;color:var(--secondary)}.bread em{font-style:normal;color:var(--aqua)}.toptools{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.chip{font-size:10px;font-weight:800;padding:7px 10px;border:1px solid var(--border);border-radius:50px;background:var(--paper);color:var(--secondary)}.dot{width:7px;height:7px;background:var(--aqua);border-radius:50%;display:inline-block;margin-right:6px}.iconbutton{background:var(--paper);border:1px solid var(--border);color:var(--ink);width:36px;height:36px;border-radius:11px}.heading{margin:25px 0 24px;display:flex;justify-content:space-between;align-items:end;gap:12px;flex-wrap:wrap}.kicker{font-size:10px;color:var(--aqua);font-weight:900;letter-spacing:1.8px;text-transform:uppercase}.heading h1{font-size:clamp(28px,3vw,39px);line-height:1.16;margin:5px 0 6px;letter-spacing:-1.3px}.heading p{font-size:12px;color:var(--secondary);margin:0;max-width:720px}.actions{display:flex;gap:8px;flex-wrap:wrap}.btn{padding:9px 13px;background:var(--paper);border:1px solid var(--border);border-radius:9px;font-size:11px;font-weight:800;color:var(--ink)}.btn:hover{filter:brightness(.97)}.btn.primary{background:var(--aqua);border-color:var(--aqua);color:white}.btn.soft{background:var(--aquaLight);border-color:transparent;color:var(--aqua)}.btn.danger{color:var(--pink)}.btn.tiny{padding:5px 8px;font-size:10px}.mainsec{display:none}.mainsec.on{display:block}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-bottom:17px}.card{background:var(--paper);border:1px solid var(--border);border-radius:17px;padding:20px;box-shadow:var(--shadow);min-width:0}.metric{padding:20px;position:relative;overflow:hidden}.metric:after{content:'';position:absolute;right:0;top:0;bottom:0;width:4px;background:var(--aqua)}.metric:nth-child(2):after{background:var(--blue)}.metric:nth-child(3):after{background:var(--amber)}.metric:nth-child(4):after{background:var(--pink)}.metric label{display:block;color:var(--secondary);font-weight:800;font-size:11px}.metric strong{font-size:clamp(22px,2.4vw,32px);display:block;letter-spacing:-1px;margin:6px 0 5px;line-height:1.1}.metric small{font-size:10px;color:var(--secondary)}.grid2{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(300px,1fr);gap:16px;margin-bottom:16px}.halves{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.subheading{display:flex;gap:8px;justify-content:space-between;align-items:start;margin-bottom:17px;flex-wrap:wrap}.subheading h2{margin:0;font-size:16px;letter-spacing:-.4px}.subheading p{color:var(--secondary);font-size:11px;margin:3px 0 0}.label{font-weight:800;color:var(--secondary);font-size:10px;letter-spacing:.7px;text-transform:uppercase}.pill{display:inline-block;padding:5px 9px;border-radius:20px;font-size:10px;font-weight:850;background:var(--soft);border:1px solid var(--border)}.pill.teal{background:var(--aquaLight);border:0;color:var(--aqua)}.pill.red{background:#df6e8218;border:0;color:var(--pink)}.pill.gold{background:#dfaa601a;border:0;color:var(--amber)}.annotation{font-size:11px;line-height:1.7;color:var(--secondary)}.insight{background:linear-gradient(120deg,#147c87,#1b5c79);color:#fff;border:0;position:relative;overflow:hidden}.insight:after{content:'';position:absolute;border:25px solid #ffffff10;width:165px;height:165px;border-radius:50%;right:-55px;top:-75px}.insight .subheading p,.insight .annotation{color:#d3f6f3}.insight b.big{font-size:34px;line-height:1.2;display:block;margin:13px 0}.insight .btn{background:#fff;color:#125c67;border:0}.divider{height:1px;background:var(--border);margin:17px 0}.tablewrap{overflow-x:auto;width:100%}table{width:100%;border-collapse:collapse;min-width:620px}th{text-transform:uppercase;letter-spacing:.8px;font-size:10px;text-align:left;color:var(--secondary);padding:11px 10px;border-bottom:1px solid var(--border);white-space:nowrap}td{padding:12px 10px;font-size:12px;border-bottom:1px solid var(--border);vertical-align:middle}tbody tr:last-child td{border-bottom:0}tbody tr:hover{background:var(--soft)}.right{text-align:right}.navlinks{white-space:nowrap}.linkbutton{border:0;background:none;color:var(--aqua);font-size:11px;font-weight:800;padding:5px}.linkbutton.red{color:var(--pink)}.statrow{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:11px 0;border-bottom:1px solid var(--border);font-size:12px}.statrow:last-child{border:0}.statrow b{font-size:13px}.textmuted{color:var(--secondary)}.barline{display:grid;grid-template-columns:60px minmax(0,1fr) 70px;align-items:center;gap:9px;margin:14px 0;font-size:11px}.bartrack{height:10px;background:var(--soft);border-radius:9px;overflow:hidden}.barfill{height:100%;background:var(--aqua);border-radius:9px}.barline b{text-align:right}.formgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:13px}.field{display:flex;flex-direction:column;gap:6px;min-width:0}.field label{color:var(--secondary);font-size:11px;font-weight:800}.field input,.field select{width:100%;min-width:0;border:1px solid var(--border);border-radius:10px;padding:10px 11px;color:var(--ink);background:var(--soft)}.field .help{font-size:10px;color:var(--secondary)}.field.full{grid-column:1/-1}.formbuttons{display:flex;flex-wrap:wrap;gap:8px;margin-top:15px}.scenario-list{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:16px}.scenario{border:1px solid var(--border);background:var(--paper);color:var(--ink);padding:14px;border-radius:14px;text-align:left;min-height:119px}.scenario b{font-size:12px;display:block;margin:8px 0 2px}.scenario small{color:var(--secondary);font-size:10px}.scenario .symbol{font-size:21px;color:var(--aqua)}.scenario.active{border-color:var(--aqua);box-shadow:0 0 0 2px var(--aquaLight)}.two-stat{display:grid;grid-template-columns:1fr 1fr;gap:10px}.statbox{border:1px solid var(--border);background:var(--soft);border-radius:12px;padding:14px}.statbox span{font-size:10px;font-weight:800;color:var(--secondary)}.statbox b{display:block;font-size:22px;margin-top:4px}.note{padding:13px 15px;background:var(--aquaLight);color:var(--ink);font-size:11px;border-radius:11px;line-height:1.65}.warn{padding:13px 15px;background:#eaa85a12;color:var(--ink);border:1px solid #dea75140;font-size:11px;border-radius:11px;line-height:1.65}.method-row{display:flex;gap:12px;margin:16px 0}.method-row .num{background:var(--aquaLight);color:var(--aqua);font-weight:900;padding:7px;border-radius:9px;height:31px;min-width:31px;text-align:center}.method-row b{font-size:12px}.method-row p{font-size:11px;color:var(--secondary);line-height:1.7;margin:3px 0}.foot{padding:20px 0 4px;display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;color:var(--secondary);font-size:10px}.svgplot{width:100%;height:auto;display:block;overflow:visible}.legend{display:flex;gap:16px;flex-wrap:wrap;font-size:10px;color:var(--secondary);margin-top:9px}.legend i{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:6px;vertical-align:middle}.empty{font-size:12px;color:var(--secondary);padding:19px;text-align:center}.overlay{position:fixed;inset:0;background:#06122288;display:none;align-items:center;justify-content:center;padding:15px;z-index:20}.overlay.show{display:flex}.dialog{border-radius:19px;background:var(--paper);padding:24px;width:min(590px,100%);max-height:95vh;overflow:auto;box-shadow:0 20px 65px #06102045}.toast{position:fixed;bottom:22px;right:22px;z-index:40;padding:12px 17px;border-radius:12px;background:var(--nav);color:white;font-size:12px;opacity:0;transform:translateY(10px);pointer-events:none;transition:.2s}.toast.show{opacity:1;transform:translateY(0)}.errorline{font-size:11px;color:var(--pink);margin-top:8px;display:none}.helpblock{font-size:12px;color:var(--secondary);line-height:1.8}.scenarioTable{min-width:450px}
@media(max-width:1100px){.layout{grid-template-columns:205px minmax(0,1fr)}.sidebar{padding:20px 10px}.metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.grid2{grid-template-columns:1fr}.scenario-list{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:730px){.layout{display:block}.sidebar{position:static;height:auto;padding:13px 14px 10px}.brand{padding:0 4px 10px}.brand-icon{height:36px;width:36px;font-size:19px}.brand b{font-size:18px}.nlabel,.sidefoot,.copyright{display:none}.nav{display:flex;overflow-x:auto;gap:4px;padding:2px 0 3px;scrollbar-width:thin}.nav button{white-space:nowrap;width:auto;padding:9px 11px;font-size:11px}.nav .sym{font-size:13px;width:auto}.main{padding:16px 15px 26px}.heading{margin:21px 0 19px}.heading h1{font-size:29px}.top{align-items:start}.toptools .chip{display:none}.card{padding:15px}.metric strong{font-size:24px}.metrics{gap:9px}.metric{padding:15px}.scenario-list{gap:7px}.scenario{min-height:105px;padding:11px}.formgrid{grid-template-columns:1fr}.foot{line-height:1.6}.toast{left:15px;right:15px;bottom:15px}}
@media(max-width:390px){.metric strong{font-size:21px}.metric small{font-size:9px}.two-stat{grid-template-columns:1fr}.heading h1{font-size:25px}}
</style></head>
<body><div class="layout"><aside class="sidebar"><div class="brand"><div class="brand-icon">◇</div><div><b>FXShield</b><small>Currency intelligence</small></div></div><div class="nlabel">Workspace</div><nav class="nav" aria-label="Primary navigation"><button class="selected" data-page="overview"><span class="sym">◉</span> Command center</button><button data-page="stress"><span class="sym">⌁</span> Scenario lab</button><button data-page="ledger"><span class="sym">▤</span> Exposure ledger</button><button data-page="markets"><span class="sym">◎</span> FX assumptions</button><button data-page="method"><span class="sym">✧</span> Methodology</button></nav><div class="sidefoot"><b>Built for better decisions</b><p>Explore currency exposure, hedging trade-offs, and stress outcomes with transparent, editable assumptions.</p></div><div class="copyright">HUGDRIGO PORTFOLIO · #097</div></aside>
<main class="main"><div class="top"><div class="bread">HUGDRIGO / <em>FXSHIELD</em> / PROJECT 097</div><div class="toptools"><span class="chip"><span class="dot"></span>OFFLINE · DEMO DATA</span><button class="iconbutton" id="themeToggle" title="Toggle light/dark theme" aria-label="Toggle theme">☾</button></div></div><div class="heading"><div><div class="kicker">Treasury / Exposure intelligence</div><h1 id="pageTitle">Currency risk, in focus.</h1><p id="pageSubtitle">Quantify FX sensitivity, explore hedging, and see the assumptions behind every scenario.</p></div><div class="actions"><button class="btn" id="exportJSON">↓ Backup JSON</button><button class="btn primary" id="addExposure">+ New exposure</button></div></div>
<section class="mainsec on" id="overview"><div class="metrics" id="overviewMetrics"></div><div class="grid2"><div class="card"><div class="subheading"><div><h2>Portfolio settlement sensitivity</h2><p>USD value of net cash flows under uniform foreign-currency moves</p></div><span class="pill teal">HEDGED VS UNHEDGED</span></div><div id="sensitivityChart"></div><div class="legend"><span><i style="background:var(--aqua)"></i>Configured hedges</span><span><i style="background:var(--blue)"></i>No hedges</span></div><p class="annotation">These are modeled settlements, not changes in accounting value. A hedge fixes the protected share at its entered forward rate.</p></div><div class="card insight"><div class="subheading"><div><h2>FX protection snapshot</h2><p>10% adverse move, individually by exposure</p></div><span class="pill">ILLUSTRATIVE</span></div><span class="annotation">Gross downside sensitivity remaining</span><b class="big" id="downsideHeadline">$0</b><p class="annotation" id="protectionText"></p><div style="height:13px"></div><button class="btn" data-goto="stress">Explore scenarios →</button></div></div><div class="grid2"><div class="card"><div class="subheading"><div><h2>Exposure by currency</h2><p>Outstanding gross notional converted at editable spot rates</p></div><span class="pill">USD EQUIVALENT</span></div><div id="currencyBars"></div></div><div class="card"><div class="subheading"><div><h2>Risk and hedge summary</h2><p>Transparent calculations, not market forecasts</p></div></div><div id="riskStats"></div></div></div><div class="card"><div class="subheading"><div><h2>Largest open exposures</h2><p>Review the positions driving portfolio sensitivity</p></div><button class="btn tiny" data-goto="ledger">View all exposures →</button></div><div class="tablewrap"><table><thead><tr><th>Counterparty</th><th>Flow</th><th>Currency</th><th class="right">Notional (USD)</th><th class="right">Protected</th><th></th></tr></thead><tbody id="topExposures"></tbody></table></div></div></section>
<section class="mainsec" id="stress"><div class="scenario-list" id="scenarioButtons"></div><div class="grid2"><div class="card"><div class="subheading"><div><h2>Scenario controls</h2><p>Apply a uniform FX shock or a direction-specific adverse move</p></div><span class="pill teal">SIMULATOR</span></div><div class="formgrid"><div class="field"><label for="shockSize">FX move magnitude (%)</label><input type="number" id="shockSize" min="0" max="30" step="0.5"><span class="help">All demo rates are USD per unit of foreign currency</span></div><div class="field"><label for="shockDirection">Shock pattern</label><select id="shockDirection"><option value="down">All foreign currencies weaken</option><option value="up">All foreign currencies strengthen</option><option value="adverse">Adverse for each position</option><option value="none">No movement</option></select><span class="help">Per-position adverse is a conservative bound, not a coherent macro scenario</span></div></div><div class="divider"></div><div class="two-stat" id="scenarioTotals"></div><p class="annotation">Hedges fix the protected share at your forward assumption; the uncovered portion settles at the stressed spot rate.</p></div><div class="card"><div class="subheading"><div><h2>Stress breakdown</h2><p>Settlement change against your modeled base case</p></div></div><div id="scenarioDetails"></div></div></div><div class="grid2"><div class="card"><div class="subheading"><div><h2>Scenario impact by currency</h2><p>Difference in modeled settlement from base case, USD</p></div></div><div id="scenarioPlot"></div></div><div class="card"><div class="subheading"><div><h2>Illustrative uncertainty</h2><p>500 seeded scenarios with linked and idiosyncratic FX shocks</p></div><span class="pill">NOT A FORECAST</span></div><div id="monteStats"></div><p class="annotation">Random stress scenarios assume synthetic volatility and correlation. Quantiles are sensitivities, not calibrated probabilities or Value at Risk.</p></div></div></section>
<section class="mainsec" id="ledger"><div class="card"><div class="subheading"><div><h2>Open currency exposures</h2><p>Amounts are in original currencies; exposure prices come from the FX assumptions tab</p></div><div class="actions"><button class="btn" id="exportCSV">↓ Export CSV</button><button class="btn soft" id="showArchived">Show archived: off</button></div></div><div class="tablewrap"><table><thead><tr><th>Counterparty</th><th>Flow</th><th>CCY</th><th class="right">Original amount</th><th class="right">Due (days)</th><th class="right">Hedge</th><th class="right">Spot USD equiv.</th><th></th></tr></thead><tbody id="ledgerRows"></tbody></table></div><div id="ledgerFoot" class="annotation" style="padding-top:12px"></div></div></section>
<section class="mainsec" id="markets"><div class="grid2"><div class="card"><div class="subheading"><div><h2>Editable FX assumptions</h2><p>Spot and contracted forward quotes expressed in USD per 1 unit of foreign currency</p></div><span class="pill teal">MANUAL INPUTS</span></div><form id="ratesForm"><div class="tablewrap"><table><thead><tr><th>Currency</th><th>Spot USD / unit</th><th>Forward USD / unit</th></tr></thead><tbody id="ratesRows"></tbody></table></div><div class="formbuttons"><button type="submit" class="btn primary">Save assumptions</button><button type="button" class="btn" id="resetRates">Reset demo rates</button></div></form></div><div class="card"><div class="subheading"><div><h2>Interpret rates correctly</h2><p>A deliberately simple, single reporting-currency model</p></div></div><div class="method-row"><span class="num">1</span><div><b>Quote direction</b><p>EUR 1.08 means one euro is worth US$1.08 in this example.</p></div></div><div class="method-row"><span class="num">2</span><div><b>Forward rate</b><p>For the hedged share, the forward quote is a user-supplied fixed settlement rate, not a price fetched from a trading venue.</p></div></div><div class="method-row"><span class="num">3</span><div><b>Known limitations</b><p>No live feeds, carry curves, basis risk, collateral, bid/ask spreads, transaction costs, taxes, or counterparty credit models.</p></div></div><div class="warn">Manual or synthetic rates may not reflect current markets. Do not use demo assumptions to price an actual foreign exchange transaction.</div></div></div></section>
<section class="mainsec" id="method"><div class="grid2"><div class="card"><div class="subheading"><div><h2>How the engine works</h2><p>Simple mathematics you can audit and explain</p></div></div><div class="method-row"><span class="num">1</span><div><b>Signed exposure</b><p>Receivables have a positive USD cash-flow sign and payables a negative one. All displayed positions are converted at the selected spot assumption.</p></div></div><div class="method-row"><span class="num">2</span><div><b>Hedged settlement</b><p>USD settlement = sign × foreign amount × (hedge fraction × fixed forward rate + unhedged fraction × scenario spot).</p></div></div><div class="method-row"><span class="num">3</span><div><b>Market sensitivity</b><p>A uniform foreign exchange shock changes each spot quote by the same percentage. Direction-specific adverse shocks, by contrast, lower receivable rates and raise payable rates individually.</p></div></div><div class="method-row"><span class="num">4</span><div><b>Gross sensitivity bound</b><p>At an illustrative 10% unfavorable move applied to each position, remaining sensitivity = 10% × Σ(amount × spot × unhedged fraction). This ignores offsets and is not a market risk probability.</p></div></div><div class="method-row"><span class="num">5</span><div><b>Uncertainty sampler</b><p>A deterministic seeded generator creates 500 synthetic outcomes from a common normal shock plus currency-specific normal shocks. Results are sorted to show 5th, 50th, and 95th percentile settlements.</p></div></div></div><div class="card"><div class="subheading"><div><h2>Data & responsible use</h2><p>Designed for demonstration and educational exploration</p></div></div><div class="note">FXShield is an offline educational simulator, not a hedging recommendation, execution platform, trading system, or a financial forecast. All opening data and rates are synthetic.</div><div class="divider"></div><p class="helpblock">No network requests, external libraries, analytics or accounts are required. Your edits are stored in this browser when local storage is available. JSON backups and CSV exports stay on your device until you decide to share them. Importing a backup replaces the current workspace only after your confirmation.</p><div class="divider"></div><p class="helpblock">Hedge accounting, actual forward points, portfolio netting agreements, settlement timing, interest rates, option strategies, cash conversion cycles, FX fixing conventions, liquidity needs, concentration limits and actual correlation modeling are outside this simplified model.</p><div class="formbuttons"><button class="btn primary" data-goto="stress">Open scenario lab</button><button class="btn" id="loadDemo">Restore full demo workspace</button></div></div></div></section>
<footer class="foot"><span>FXShield v1.0 · Project 097 · Hugdrigo portfolio</span><span>Offline-first · Synthetic data · MIT licensed</span></footer></main></div>
<div class="overlay" id="editOverlay" role="dialog" aria-modal="true" aria-labelledby="dialogTitle"><div class="dialog"><div class="subheading"><div><h2 id="dialogTitle">New currency exposure</h2><p>Manage illustrative international receivables and payables</p></div><button type="button" class="iconbutton" id="closeDialog" aria-label="Close dialog">×</button></div><form id="exposureForm"><input id="editingId" type="hidden"><div class="formgrid"><div class="field full"><label for="entityInput">Counterparty or description</label><input id="entityInput" maxlength="80" required placeholder="e.g. European distribution partner"></div><div class="field"><label for="typeInput">Cash-flow direction</label><select id="typeInput"><option value="receivable">Receivable (incoming)</option><option value="payable">Payable (outgoing)</option></select></div><div class="field"><label for="ccyInput">Original currency</label><select id="ccyInput"><option>EUR</option><option>GBP</option><option>JPY</option><option>BRL</option><option>CAD</option><option>MXN</option></select></div><div class="field"><label for="amountInput">Original-currency amount</label><input id="amountInput" type="number" min="1" max="10000000000" step="any" required></div><div class="field"><label for="dueInput">Due in days</label><input id="dueInput" type="number" min="0" max="3650" step="1" required></div><div class="field full"><label for="hedgeInput">Forward-hedged share (%)</label><input id="hedgeInput" type="number" min="0" max="100" step="1" required></div></div><div id="formError" class="errorline" role="alert"></div><div class="formbuttons"><button type="submit" class="btn primary">Save exposure</button><button type="button" class="btn" id="cancelDialog">Cancel</button></div></form></div></div>
<input hidden id="fileImport" type="file" accept=".json,application/json"><div class="toast" id="toast" role="status" aria-live="polite"></div>
<script>
'use strict';
(()=>{
const STORAGE='hugdrigo_fxshield_97_v1';
const CURRENCIES=['EUR','GBP','JPY','BRL','CAD','MXN'];
const DEFAULT_RATES={EUR:{spot:1.0800,forward:1.0750},GBP:{spot:1.2900,forward:1.2840},JPY:{spot:.00670,forward:.00676},BRL:{spot:.1800,forward:.1815},CAD:{spot:.7400,forward:.7420},MXN:{spot:.0550,forward:.0547}};
const EXAMPLES=[
{id:'x1',entity:'European Distribution',kind:'receivable',ccy:'EUR',amount:540000,due:45,hedge:65,active:true},
{id:'x2',entity:'UK Cloud Services',kind:'payable',ccy:'GBP',amount:245000,due:75,hedge:45,active:true},
{id:'x3',entity:'Tokyo Components',kind:'payable',ccy:'JPY',amount:48000000,due:110,hedge:70,active:true},
{id:'x4',entity:'Brazil Wholesale',kind:'receivable',ccy:'BRL',amount:1150000,due:60,hedge:25,active:true},
{id:'x5',entity:'Canadian Licensing',kind:'receivable',ccy:'CAD',amount:280000,due:120,hedge:40,active:true},
{id:'x6',entity:'Mexico Logistics',kind:'payable',ccy:'MXN',amount:2700000,due:30,hedge:20,active:true},
{id:'x7',entity:'EU Machinery Order',kind:'payable',ccy:'EUR',amount:125000,due:180,hedge:80,active:true},
{id:'x8',entity:'London Consultancy',kind:'receivable',ccy:'GBP',amount:135000,due:95,hedge:55,active:true}
];
const PRESETS=[{id:'base',title:'Base case',description:'No spot-rate changes',direction:'none',shock:0,icon:'◎'}, {id:'usd-up',title:'USD strengthens',description:'Foreign currency quotes fall',direction:'down',shock:10,icon:'↘'}, {id:'usd-down',title:'USD weakens',description:'Foreign currency quotes rise',direction:'up',shock:10,icon:'↗'}, {id:'adverse',title:'Adverse by flow',description:'Worst direction per exposure',direction:'adverse',shock:12,icon:'⚑'}];
const $=id=>document.getElementById(id), copy=x=>JSON.parse(JSON.stringify(x));
const usd=n=>!Number.isFinite(n)?'—':(n<0?'−':'')+'$'+new Intl.NumberFormat('en-US',{maximumFractionDigits:0}).format(Math.abs(n));
const shortUSD=n=>!Number.isFinite(n)?'—':(n<0?'−':'')+'$'+(Math.abs(n)>=1e6?(Math.abs(n)/1e6).toFixed(2)+'M':Math.abs(n)>=1e3?(Math.abs(n)/1e3).toFixed(1)+'K':Math.abs(n).toFixed(0));
const num=n=>new Intl.NumberFormat('en-US',{maximumFractionDigits:2}).format(n);
const signed=n=>(n>0?'+':n<0?'−':'')+shortUSD(Math.abs(n));
const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const safeCSV=s=>{let t=String(s??'');if(/^[\s\t\r\n]*[=+@-]/.test(t))t="'"+t;return '"'+t.replace(/"/g,'""')+'"'};
let state={rates:copy(DEFAULT_RATES),rows:copy(EXAMPLES),scenario:{direction:'none',shock:0},dark:false};let page='overview',showArchived=false,toastTimer=null;
function validateRate(r){if(!r||typeof r!=='object'||Array.isArray(r))throw Error('Invalid rate object');const spot=Number(r.spot),forward=Number(r.forward);if(!Number.isFinite(spot)||!Number.isFinite(forward)||spot<=0||forward<=0||spot>10000||forward>10000||spot<1e-7||forward<1e-7)throw Error('A spot or forward rate is out of range');return {spot,forward}}
function validateRates(r){if(!r||typeof r!=='object'||Array.isArray(r))throw Error('Missing currency rates');const out={};for(const c of CURRENCIES)out[c]=validateRate(r[c]);return out}
function validateRow(v){if(!v||typeof v!=='object'||Array.isArray(v))throw Error('Invalid exposure');const entity=String(v.entity??'').trim(),id=String(v.id??'');const amount=Number(v.amount),due=Number(v.due),hedge=Number(v.hedge);if(!id||id.length>85||!entity||entity.length>80||!['receivable','payable'].includes(v.kind)||!CURRENCIES.includes(v.ccy)||!Number.isFinite(amount)||amount<1||amount>1e10||!Number.isSafeInteger(due)||due<0||due>3650||!Number.isSafeInteger(hedge)||hedge<0||hedge>100||typeof v.active!=='boolean')throw Error('Invalid exposure fields');return {id,entity,kind:v.kind,ccy:v.ccy,amount,due,hedge,active:v.active}}
function validateRows(arr){if(!Array.isArray(arr)||arr.length>300)throw Error('Backup exceeds 300 exposure records');const rows=arr.map(validateRow);if(new Set(rows.map(x=>x.id)).size!==rows.length)throw Error('Duplicate exposure IDs');return rows}
function validateScenario(s){if(!s||typeof s!=='object'||!['none','down','up','adverse'].includes(s.direction))throw Error('Invalid scenario');const shock=Number(s.shock);if(!Number.isFinite(shock)||shock<0||shock>30)throw Error('Shock must be 0–30%');return {direction:s.direction,shock}}
function validateBackup(b){if(!b||b.app!=='FXShield'||b.version!==1)throw Error('Not an FXShield v1 backup');return {rates:validateRates(b.rates),rows:validateRows(b.rows),scenario:validateScenario(b.scenario),dark:!!b.dark}}
function persist(){try{localStorage.setItem(STORAGE,JSON.stringify(state));return true}catch(e){return false}}
function restore(){try{const old=localStorage.getItem(STORAGE);if(old){const s=JSON.parse(old);state={rates:validateRates(s.rates),rows:validateRows(s.rows),scenario:validateScenario(s.scenario),dark:!!s.dark}}}catch(e){state={rates:copy(DEFAULT_RATES),rows:copy(EXAMPLES),scenario:{direction:'none',shock:0},dark:false}}}
function toast(msg){clearTimeout(toastTimer);$('toast').textContent=msg;$('toast').classList.add('show');toastTimer=setTimeout(()=>$('toast').classList.remove('show'),3100)}
function activeRows(){return state.rows.filter(x=>x.active)}
const direction=e=>e.kind==='receivable'?1:-1;
function spotFor(e,s=state.scenario){const spot=state.rates[e.ccy].spot,p=s.shock/100;return s.direction==='none'?spot:s.direction==='up'?spot*(1+p):s.direction==='down'?spot*(1-p):spot*(1+(e.kind==='receivable'?-p:p))}
function settlement(e,s=state.scenario,hedged=true){const h=hedged?e.hedge/100:0,forward=state.rates[e.ccy].forward;return direction(e)*e.amount*(h*forward+(1-h)*spotFor(e,s))}
function spotValue(e){return direction(e)*e.amount*state.rates[e.ccy].spot}
function portfolio(s=state.scenario,hedged=true){return activeRows().reduce((total,e)=>total+settlement(e,s,hedged),0)}
function grossExposure(){return activeRows().reduce((t,e)=>t+e.amount*state.rates[e.ccy].spot,0)}
function unhedgedExposure(){return activeRows().reduce((t,e)=>t+e.amount*state.rates[e.ccy].spot*(1-e.hedge/100),0)}
function activeCurrencyStats(){return CURRENCIES.map(ccy=>{const rows=activeRows().filter(x=>x.ccy===ccy);return {ccy,count:rows.length,gross:rows.reduce((a,e)=>a+Math.abs(spotValue(e)),0),net:rows.reduce((a,e)=>a+spotValue(e),0),unhedged:rows.reduce((a,e)=>a+Math.abs(spotValue(e))*(1-e.hedge/100),0)}}).filter(x=>x.count)}
function lineChart(series,xs){const W=710,H=260,L=78,R=24,T=15,B=39,values=series.flatMap(s=>s.values).filter(Number.isFinite),min=Math.min(...values),max=Math.max(...values),pad=Math.max((max-min)*.2,1),lo=min-pad,hi=max+pad;const x=i=>L+(W-L-R)*i/(xs.length-1),y=v=>T+(H-T-B)*(1-(v-lo)/(hi-lo));let svg=`<svg class="svgplot" viewBox="0 0 ${W} ${H}" role="img" aria-label="FX scenario comparison line chart">`;
for(let j=0;j<5;j++){const v=lo+(hi-lo)*j/4,yy=y(v);svg+=`<line x1="${L}" x2="${W-R}" y1="${yy}" y2="${yy}" stroke="var(--border)" stroke-dasharray="4 5"/><text x="${L-11}" y="${yy+4}" text-anchor="end" fill="var(--secondary)" font-size="11">${shortUSD(v)}</text>`}
xs.forEach((v,i)=>{svg+=`<text x="${x(i)}" y="${H-12}" fill="var(--secondary)" text-anchor="middle" font-size="11">${v>0?'+':''}${v}%</text>`});for(const [j,s]of series.entries()){const col=j===0?'var(--aqua)':'var(--blue)',dash=j===0?'':'stroke-dasharray="6 5"';svg+=`<polyline fill="none" stroke="${col}" stroke-width="3" ${dash} stroke-linecap="round" stroke-linejoin="round" points="${s.values.map((v,i)=>`${x(i)},${y(v)}`).join(' ')}"/>`;for(let i=0;i<s.values.length;i++)svg+=`<circle cx="${x(i)}" cy="${y(s.values[i])}" r="3.1" fill="${col}"/>`;}return svg+'</svg>'}
function renderOverview(){const rows=activeRows(),gross=grossExposure(),unhedged=unhedgedExposure(),hedgedPct=gross?100*(1-unhedged/gross):0,receivable=rows.filter(e=>e.kind==='receivable').length,payable=rows.length-receivable,base=portfolio({direction:'none',shock:0}),risk=unhedged*.1;
const stats=[['Gross FX exposure',shortUSD(gross),'Sum of absolute amounts in USD'],['Hedge coverage',hedgedPct.toFixed(1)+'%','Weighted by spot-equivalent notional'],['Open currency positions',String(rows.length),'Across '+activeCurrencyStats().length+' currencies'],['10% adverse sensitivity',shortUSD(risk),'Per-position stress bound']];$('overviewMetrics').innerHTML=stats.map(x=>`<div class="card metric"><label>${x[0]}</label><strong>${x[1]}</strong><small>${x[2]}</small></div>`).join('');
const xs=[-20,-15,-10,-5,0,5,10,15,20];$('sensitivityChart').innerHTML=lineChart([{values:xs.map(x=>portfolio({direction:x<0?'down':'up',shock:Math.abs(x)},true))},{values:xs.map(x=>portfolio({direction:x<0?'down':'up',shock:Math.abs(x)},false))}],xs);$('downsideHeadline').textContent=shortUSD(risk);$('protectionText').textContent=`${hedgedPct.toFixed(1)}% of gross exposure is notionally hedged. Without those hedges, the same gross 10% adverse sensitivity would be ${shortUSD(gross*.1)}. This is a simplified, conservative exposure bound.`;
const c=activeCurrencyStats().sort((a,b)=>b.gross-a.gross),max=Math.max(...c.map(x=>x.gross),1);$('currencyBars').innerHTML=c.length?c.map(x=>`<div class="barline"><b>${x.ccy}</b><div class="bartrack"><div class="barfill" style="width:${100*x.gross/max}%;background:${x.net<0?'var(--blue)':'var(--aqua)'}"></div></div><b>${shortUSD(x.gross)}</b></div>`).join(''):'<div class="empty">Add an exposure to see the currency mix.</div>';
$('riskStats').innerHTML=[['Projected base cash settlement',usd(base)],['Open receivables / payables',`${receivable} / ${payable}`],['Unhedged gross notional',usd(unhedged)],['10% protection vs fully open',usd((gross-unhedged)*.1)]].map(([label,val])=>`<div class="statrow"><span class="textmuted">${label}</span><b>${val}</b></div>`).join('');$('topExposures').innerHTML=rows.slice().sort((a,b)=>Math.abs(spotValue(b))-Math.abs(spotValue(a))).slice(0,5).map(e=>`<tr><td><b>${esc(e.entity)}</b></td><td><span class="pill ${e.kind==='receivable'?'teal':'gold'}">${e.kind==='receivable'?'Receive':'Pay'}</span></td><td>${e.ccy}</td><td class="right">${usd(Math.abs(spotValue(e)))}</td><td class="right">${e.hedge}%</td><td><button class="linkbutton" data-edit="${esc(e.id)}">Edit</button></td></tr>`).join('')||'<tr><td colspan="6" class="empty">No active positions yet</td></tr>'}
function renderStress(){let sc=state.scenario;$('scenarioButtons').innerHTML=PRESETS.map(p=>`<button class="scenario ${p.direction===sc.direction&&p.shock===sc.shock?'active':''}" data-scenario="${p.id}"><span class="symbol">${p.icon}</span><b>${p.title}</b><small>${p.description}</small></button>`).join('');$('shockSize').value=sc.shock;$('shockDirection').value=sc.direction;const base=portfolio({direction:'none',shock:0}),hedged=portfolio(),open=portfolio(sc,false),nH=portfolio({direction:'none',shock:0},false),difference=hedged-base;
$('scenarioTotals').innerHTML=`<div class="statbox"><span>Hedged settlement</span><b>${shortUSD(hedged)}</b></div><div class="statbox"><span>Change vs hedged base</span><b style="color:${difference<0?'var(--pink)':'var(--aqua)'}">${signed(difference)}</b></div><div class="statbox"><span>Unhedged settlement</span><b>${shortUSD(open)}</b></div><div class="statbox"><span>Unhedged change vs base</span><b>${signed(open-nH)}</b></div>`;
const cats=activeCurrencyStats();$('scenarioDetails').innerHTML=[['Scenario shock',sc.direction==='none'?'0%':sc.shock+'%'],['Active original-currency legs',String(activeRows().length)],['Configured hedged share',grossExposure()?(100*(1-unhedgedExposure()/grossExposure())).toFixed(1)+'%':'0%'],['Forward / spot carry in base',signed(base-nH)]].map(([k,v])=>`<div class="statrow"><span class="textmuted">${k}</span><b>${v}</b></div>`).join('');$('scenarioPlot').innerHTML=cats.length?`<div class="tablewrap"><table class="scenarioTable"><thead><tr><th>Currency</th><th class="right">Base cash flow</th><th class="right">Stress change</th></tr></thead><tbody>${cats.map(x=>{const rows=activeRows().filter(e=>e.ccy===x.ccy),b=rows.reduce((a,e)=>a+settlement(e,{direction:'none',shock:0}),0),v=rows.reduce((a,e)=>a+settlement(e),0);return `<tr><td><b>${x.ccy}</b></td><td class="right">${usd(b)}</td><td class="right" style="color:${v-b<0?'var(--pink)':'var(--aqua)'}">${signed(v-b)}</td></tr>`}).join('')}</tbody></table></div>`:'<div class="empty">No active currency positions.</div>';renderMonte()}
function random(seed){let a=seed>>>0;return ()=>{a+=0x6D2B79F5;let t=a;t=Math.imul(t^t>>>15,t|1);t^=t+Math.imul(t^t>>>7,t|61);return((t^t>>>14)>>>0)/4294967296}}
function gaussian(rng){return Math.sqrt(-2*Math.log(Math.max(1e-12,rng())))*Math.cos(2*Math.PI*rng())}
function monte(){const rng=random(970097),out=[],rows=activeRows();for(let run=0;run<500;run++){const market=gaussian(rng)*.075,shocks={};for(const c of CURRENCIES)shocks[c]=Math.max(-.40,Math.min(.40,market+gaussian(rng)*.05));const total=rows.reduce((t,e)=>{const h=e.hedge/100,r=state.rates[e.ccy];return t+direction(e)*e.amount*(h*r.forward+(1-h)*r.spot*(1+shocks[e.ccy]))},0);out.push(total)}return out.sort((a,b)=>a-b)}
function renderMonte(){const samples=monte(),q=p=>samples[Math.floor(p*(samples.length-1))],base=portfolio({direction:'none',shock:0});$('monteStats').innerHTML=`<div class="two-stat"><div class="statbox"><span>5th-percentile settlement</span><b>${shortUSD(q(.05))}</b></div><div class="statbox"><span>Median settlement</span><b>${shortUSD(q(.50))}</b></div><div class="statbox"><span>95th-percentile settlement</span><b>${shortUSD(q(.95))}</b></div><div class="statbox"><span>5th-percentile vs base</span><b>${signed(q(.05)-base)}</b></div></div>`}
function renderLedger(){const rows=state.rows.filter(x=>showArchived||x.active);$('showArchived').textContent='Show archived: '+(showArchived?'on':'off');$('ledgerRows').innerHTML=rows.map(e=>`<tr><td><b>${esc(e.entity)}</b>${e.active?'':' <span class="pill red">Archived</span>'}</td><td>${e.kind==='receivable'?'Receivable':'Payable'}</td><td><b>${e.ccy}</b></td><td class="right">${num(e.amount)}</td><td class="right">${e.due}</td><td class="right">${e.hedge}%</td><td class="right">${usd(Math.abs(spotValue(e)))}</td><td class="navlinks"><button class="linkbutton" data-edit="${esc(e.id)}">Edit</button><button class="linkbutton ${e.active?'red':''}" data-archive="${esc(e.id)}">${e.active?'Archive':'Restore'}</button></td></tr>`).join('')||'<tr><td colspan="8" class="empty">No matching exposures</td></tr>';$('ledgerFoot').textContent=`${activeRows().length} active positions · ${state.rows.length-activeRows().length} archived · Maximum 300 records. Archiving preserves all prior data.`}
function renderMarkets(){ $('ratesRows').innerHTML=CURRENCIES.map(c=>`<tr><td><b>${c}</b></td><td><input required aria-label="${c} spot rate" type="number" min="0.0000001" max="10000" step="any" id="spot${c}" value="${state.rates[c].spot}"></td><td><input required aria-label="${c} forward rate" type="number" min="0.0000001" max="10000" step="any" id="forward${c}" value="${state.rates[c].forward}"></td></tr>`).join('')}
function render(){document.body.classList.toggle('night',state.dark);$('themeToggle').textContent=state.dark?'☀':'☾';renderOverview();renderStress();renderLedger();renderMarkets()}
const HEADINGS={overview:['Currency risk, in focus.','Quantify FX sensitivity, explore hedging, and see the assumptions behind every scenario.'],stress:['Explore the what-ifs.','Compare modeled settlement outcomes as currency rates move and hedges remain fixed.'],ledger:['Manage your exposures.','Keep track of open currency receivables, payables, and simulated hedge coverage.'],markets:['Make assumptions explicit.','Change synthetic FX spot and forward inputs; no external quotes are fetched.'],method:['Transparent by design.','Understand the equations, uncertainty assumptions, and limitations before interpreting results.']};
function goto(p){if(!HEADINGS[p])return;page=p;for(const s of document.querySelectorAll('.mainsec'))s.classList.toggle('on',s.id===p);for(const b of document.querySelectorAll('[data-page]')){b.classList.toggle('selected',b.dataset.page===p);b.setAttribute('aria-current',b.dataset.page===p?'page':'false')}[$('pageTitle').textContent,$('pageSubtitle').textContent]=HEADINGS[p];window.scrollTo(0,0)}
function openEdit(id){$('formError').style.display='none';const e=id?state.rows.find(x=>x.id===id):null;if(id&&!e)return;$('dialogTitle').textContent=e?'Edit currency exposure':'New currency exposure';$('editingId').value=e?.id||'';$('entityInput').value=e?.entity||'';$('typeInput').value=e?.kind||'receivable';$('ccyInput').value=e?.ccy||'EUR';$('amountInput').value=e?.amount??100000;$('dueInput').value=e?.due??60;$('hedgeInput').value=e?.hedge??50;$('editOverlay').classList.add('show');$('entityInput').focus()}
function closeEdit(){$('editOverlay').classList.remove('show')}
function saveEdit(ev){ev.preventDefault();try{const id=$('editingId').value||'pos-'+Date.now()+'-'+Math.floor(Math.random()*1000000);if(!state.rows.some(x=>x.id===id)&&state.rows.length>=300)throw Error('Maximum 300 exposures reached');const old=state.rows.find(x=>x.id===id),obj=validateRow({id,entity:$('entityInput').value,kind:$('typeInput').value,ccy:$('ccyInput').value,amount:$('amountInput').value,due:$('dueInput').value,hedge:$('hedgeInput').value,active:old?old.active:true});if(old)Object.assign(old,obj);else state.rows.push(obj);persist();closeEdit();render();toast('Exposure saved successfully')}catch(e){$('formError').textContent=e.message;$('formError').style.display='block'}}
function download(filename,contents,type){const url=URL.createObjectURL(new Blob([contents],{type})),a=document.createElement('a');a.href=url;a.download=filename;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),3000)}
function exportJSON(){const o={app:'FXShield',version:1,exportedAt:new Date().toISOString(),...copy(state)};download('fxshield97-backup.json',JSON.stringify(o,null,2),'application/json')}
function exportCSV(){const heads=['counterparty','flow','currency','foreign_amount','due_days','hedge_percent','active','spot_usd_per_unit','forward_usd_per_unit','spot_value_usd','base_settlement_usd'];const rows=state.rows.map(e=>[e.entity,e.kind,e.ccy,e.amount,e.due,e.hedge,e.active?'yes':'no',state.rates[e.ccy].spot,state.rates[e.ccy].forward,spotValue(e),settlement(e,{direction:'none',shock:0})]);download('fxshield97-exposures.csv',[heads.join(','),...rows.map(r=>r.map(safeCSV).join(','))].join('\r\n')+'\r\n','text/csv;charset=utf-8')}
async function importJSON(ev){const f=ev.target.files?.[0];ev.target.value='';if(!f)return;try{if(f.size>250000)throw Error('Backup exceeds 250 KB');const parsed=validateBackup(JSON.parse(await f.text()));if(!confirm('Replace the current FXShield workspace with this backup?'))return;state=parsed;persist();render();toast('Backup successfully restored')}catch(e){toast('Import rejected: '+e.message)}}
function applyScenario(direction,shock){try{state.scenario=validateScenario({direction,shock});persist();renderStress()}catch(e){toast(e.message)}}
function archive(id){const e=state.rows.find(x=>x.id===id);if(!e)return;e.active=!e.active;persist();render();toast(e.active?'Exposure restored':'Exposure archived, not deleted')}
document.addEventListener('click',ev=>{const node=ev.target.closest('[data-goto],[data-page],[data-scenario],[data-edit],[data-archive]');if(!node)return;if(node.dataset.goto)goto(node.dataset.goto);if(node.dataset.page)goto(node.dataset.page);if(node.dataset.scenario){const s=PRESETS.find(p=>p.id===node.dataset.scenario);if(s)applyScenario(s.direction,s.shock)}if(node.dataset.edit)openEdit(node.dataset.edit);if(node.dataset.archive)archive(node.dataset.archive)});
$('themeToggle').onclick=()=>{state.dark=!state.dark;persist();render()};$('addExposure').onclick=()=>openEdit();$('closeDialog').onclick=closeEdit;$('cancelDialog').onclick=closeEdit;$('editOverlay').addEventListener('click',e=>{if(e.target===$('editOverlay'))closeEdit()});document.addEventListener('keydown',e=>{if(e.key==='Escape')closeEdit()});$('exposureForm').addEventListener('submit',saveEdit);$('showArchived').onclick=()=>{showArchived=!showArchived;renderLedger()};$('exportCSV').onclick=exportCSV;$('exportJSON').onclick=exportJSON;$('fileImport').addEventListener('change',importJSON);$('shockSize').addEventListener('change',()=>applyScenario($('shockDirection').value,Number($('shockSize').value)));$('shockDirection').addEventListener('change',()=>applyScenario($('shockDirection').value,Number($('shockSize').value)));
$('ratesForm').addEventListener('submit',e=>{e.preventDefault();try{const o={};for(const c of CURRENCIES)o[c]={spot:$('spot'+c).value,forward:$('forward'+c).value};state.rates=validateRates(o);persist();render();toast('FX assumptions saved')}catch(err){toast(err.message)}});
$('resetRates').onclick=()=>{if(!confirm('Restore the six illustrative spot and forward rate pairs? Exposures are preserved.'))return;state.rates=copy(DEFAULT_RATES);persist();render();toast('Demo rate assumptions restored')};$('loadDemo').onclick=()=>{if(!confirm('Replace your workspace with the full demonstration dataset?'))return;state={rates:copy(DEFAULT_RATES),rows:copy(EXAMPLES),scenario:{direction:'none',shock:0},dark:state.dark};persist();render();goto('overview');toast('Demo workspace restored')};
const btn=document.createElement('button');btn.className='btn';btn.textContent='↑ Import JSON';btn.id='importJSON';$('exportJSON').before(btn);btn.onclick=()=>$('fileImport').click();
restore();render();goto('overview');
})();
</script></body></html>