Files
FXandRatesDashboard/index.html
T
Tvishi Agarwal 0529392fe7 almost done?
2026-04-12 21:05:32 +00:00

616 lines
72 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<title>FX RADAR TEST v5</title>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.9/babel.min.js"></script>
<style>*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%}body{background:#0a0e17;font-family:'IBM Plex Sans',system-ui,sans-serif;color:#e2e8f0;font-size:13px}::-webkit-scrollbar{width:5px;height:5px}::-webkit-scrollbar-track{background:#0a0e17}::-webkit-scrollbar-thumb{background:#334155;border-radius:3px}button{font-family:inherit;cursor:pointer}table{border-spacing:0}@keyframes spin{to{transform:rotate(360deg)}}.sp{width:12px;height:12px;border:2px solid #334155;border-top-color:#6366f1;border-radius:50%;animation:spin .8s linear infinite;display:inline-block;vertical-align:middle}@media(max-width:900px){.g2{grid-template-columns:1fr!important}.g3{grid-template-columns:1fr!important}.hmob{display:none!important}}</style>
</head><body><div id="root"></div>
<script type="text/babel">
const {useState,useEffect,useRef,useCallback}=React;
const C={bg:"#0a0e17",bgC:"#111827",bd:"#1e293b",tx:"#e2e8f0",txD:"#94a3b8",txM:"#64748b",g:"#22c55e",r:"#ef4444",b:"#3b82f6",am:"#f59e0b",ac:"#6366f1",acD:"#4338ca",tl:"#14b8a6"};
const cc=v=>v>0?C.g:v<0?C.r:C.txM;const fc=(v,s="%")=>v!=null?(v>0?"+":"")+Number(v).toFixed(2)+s:"—";const fb=v=>v!=null?(v>0?"+":"")+Number(v).toFixed(1)+"bp":"—";const pc=(a,b)=>b?+((a/b-1)*100).toFixed(2):null;const Mn="'JetBrains Mono',monospace";
const _d=s=>{try{return atob(s)}catch{return""}};const _K={finnhub:_d("ZDc5ZGg1cHIwMXFxcG1oZ2QwYjBkNzlkaDVwcjAxcXFwbWhnZDBiZw==")};
const gk=n=>{try{return localStorage.getItem("fxr_"+n)||""}catch{return""}};const sk=(n,v)=>{try{localStorage.setItem("fxr_"+n,v)}catch{}};
const jf=async u=>{const r=await fetch(u);if(!r.ok)throw new Error(r.status);return r.json()};
async function fFr(d,sym){try{return await jf("https://api.frankfurter.dev/v1/"+d+"?base=USD&symbols="+sym)}catch{return null}}
async function fFD(){try{const r=await fetch("/.netlify/functions/fred-yields");if(!r.ok)throw new Error("fred-yields failed");return await r.json()}catch{return null}}
async function fECB(){try{const r=await fetch("/.netlify/functions/ecb-yields");if(!r.ok)throw new Error("ecb-yields failed");return await r.json()}catch(err){console.error("ECB yields fetch failed",err);return null}}
async function fFh(ep,p,k){if(!k)return null;try{const q=new URLSearchParams({...p,token:k}).toString();return await jf("https://finnhub.io/api/v1/"+ep+"?"+q)}catch{return null}}
async function fData(){const r=await fetch("/data.json?_="+Date.now());if(!r.ok)throw new Error("Failed to load data.json");return await r.json()}
const ago=n=>{const d=new Date();d.setDate(d.getDate()-n);return d.toISOString().slice(0,10)};const fwd=n=>{const d=new Date();d.setDate(d.getDate()+n);return d.toISOString().slice(0,10)};const jan2=()=>new Date().getFullYear()+"-01-02";
const G10=[{p:"EURUSD",q:"EUR",i:1},{p:"GBPUSD",q:"GBP",i:1},{p:"USDJPY",q:"JPY",i:0},{p:"USDCHF",q:"CHF",i:0},{p:"AUDUSD",q:"AUD",i:1},{p:"NZDUSD",q:"NZD",i:1},{p:"USDCAD",q:"CAD",i:0},{p:"USDSEK",q:"SEK",i:0},{p:"USDNOK",q:"NOK",i:0}];
const EMP=[{p:"USDCNY",q:"CNY"},{p:"USDMXN",q:"MXN"},{p:"USDBRL",q:"BRL"},{p:"USDZAR",q:"ZAR"},{p:"USDINR",q:"INR"},{p:"USDKRW",q:"KRW"},{p:"USDTRY",q:"TRY"},{p:"USDPLN",q:"PLN"},{p:"USDHUF",q:"HUF"},{p:"USDCZK",q:"CZK"},{p:"USDSGD",q:"SGD"},{p:"USDIDR",q:"IDR"},{p:"USDTHB",q:"THB"}].map(x=>({...x,i:0}));
const SYM=[...new Set([...G10,...EMP].map(x=>x.q))].join(",");
const RT=[{c:"US",f:"🇺🇸",cb:null,ccy:"USD"},{c:"Eurozone",f:"🇪🇺",cb:null,ccy:"EUR"},{c:"UK",f:"🇬🇧",cb:null,ccy:"GBP"},{c:"Japan",f:"🇯🇵",cb:null,ccy:"JPY"},{c:"Switzerland",f:"🇨🇭",cb:null,ccy:"CHF"},{c:"Australia",f:"🇦🇺",cb:null,ccy:"AUD"},{c:"New Zealand",f:"🇳🇿",cb:null,ccy:"NZD"},{c:"Canada",f:"🇨🇦",cb:null,ccy:"CAD"},{c:"Sweden",f:"🇸🇪",cb:null,ccy:"SEK"},{c:"Norway",f:"🇳🇴",cb:null,ccy:"NOK"},{c:"Brazil",f:"🇧🇷",cb:null,ccy:"BRL"},{c:"Mexico",f:"🇲🇽",cb:null,ccy:"MXN"},{c:"South Africa",f:"🇿🇦",cb:null,ccy:"ZAR"},{c:"India",f:"🇮🇳",cb:null,ccy:"INR"},{c:"South Korea",f:"🇰🇷",cb:null,ccy:"KRW"},{c:"Turkey",f:"🇹🇷",cb:null,ccy:"TRY"},{c:"Poland",f:"🇵🇱",cb:null,ccy:"PLN"}];
const cOf={};RT.forEach(r=>cOf[r.c]=r.ccy);
const tvF={US:"TVC:DXY",Eurozone:"FX:EURUSD",UK:"FX:GBPUSD",Japan:"FX:USDJPY",Switzerland:"FX:USDCHF",Australia:"FX:AUDUSD","New Zealand":"FX:NZDUSD",Canada:"FX:USDCAD",Sweden:"FX:USDSEK",Norway:"FX:USDNOK",Brazil:"FX:USDBRL",Mexico:"FX:USDMXN","South Africa":"FX:USDZAR",India:"FX:USDINR","South Korea":"FX:USDKRW",Turkey:"FX:USDTRY",Poland:"FX:USDPLN"};
const tvY={US:"TVC:US10Y",Eurozone:"TVC:DE10Y",UK:"TVC:GB10Y",Japan:"TVC:JP10Y",Canada:"TVC:CA10Y",Australia:"TVC:AU10Y"};
const pOf={US:"DXY",Eurozone:"EURUSD",UK:"GBPUSD",Japan:"USDJPY",Switzerland:"USDCHF",Australia:"AUDUSD","New Zealand":"NZDUSD",Canada:"USDCAD",Sweden:"USDSEK",Norway:"USDNOK",Brazil:"USDBRL",Mexico:"USDMXN","South Africa":"USDZAR",India:"USDINR","South Korea":"USDKRW",Turkey:"USDTRY",Poland:"USDPLN"};
const nKw={US:["fed","fomc","treasury","dollar","powell","nonfarm"],Eurozone:["ecb","lagarde","eurozone","bund"],UK:["boe","bailey","gilt","sterling","uk gdp","uk cpi"],Japan:["boj","ueda","yen","jgb","japan"],Switzerland:["snb","swiss","franc"],Australia:["rba","aussie","australia"],Canada:["boc","canada","loonie"],"New Zealand":["rbnz","kiwi","new zealand"],Sweden:["riksbank","sweden"],Norway:["norges","norway"],Brazil:["bcb","brazil","real"],Mexico:["banxico","mexico","peso"],"South Africa":["sarb","south africa","rand"],India:["rbi","india","rupee"],"South Korea":["bok","korea","won"],Turkey:["cbrt","turkey","lira"],Poland:["nbp","poland","zloty"]};
const MD={};
const bondPfx={"US":["us"],"UK":["uk"],"Eurozone":["de","it","fr","es"],"Japan":["jp"],"Canada":["ca"],"Australia":["au"],"New Zealand":["nz"],"Switzerland":["ch"],"Sweden":["se"],"Norway":["no"]};
/* ═══ UI ATOMS ════════════════════════════════════════════════════ */
const Dot=({on})=><span style={{display:"inline-block",width:6,height:6,borderRadius:"50%",background:on?C.g:C.am,marginRight:5}}/>;
function Tabs({tabs,act,set}){return <div style={{display:"flex",gap:2,borderBottom:"1px solid "+C.bd,marginBottom:12,overflowX:"auto"}}>{tabs.map(t=><button key={t} onClick={()=>set(t)} style={{padding:"8px 16px",fontSize:12,fontWeight:600,background:act===t?C.ac+"20":"transparent",color:act===t?C.ac:C.txD,border:"none",borderBottom:act===t?"2px solid "+C.ac:"2px solid transparent",whiteSpace:"nowrap"}}>{t}</button>)}</div>}
function Tbl({cols,rows,sm}){return <div style={{overflowX:"auto"}}><table style={{width:"100%",borderCollapse:"collapse",fontSize:sm?11:12}}><thead><tr>{cols.map(c=><th key={c.k} style={{padding:"6px 10px",textAlign:c.a||"right",color:C.txM,fontWeight:600,fontSize:10,letterSpacing:1,textTransform:"uppercase",borderBottom:"1px solid "+C.bd,whiteSpace:"nowrap",background:C.bgC}}>{c.l}</th>)}</tr></thead><tbody>{rows.map((r,i)=><tr key={i} style={{background:i%2?C.bg+"60":"transparent"}}>{cols.map(c=>{const v=c.rn?c.rn(r):r[c.k];return <td key={c.k} style={{padding:"5px 10px",textAlign:c.a||"right",fontFamily:c.nm?"inherit":Mn,fontSize:sm?11:12,color:c.cl?c.cl(r):C.tx,whiteSpace:"nowrap",borderBottom:"1px solid "+C.bd+"20"}}>{v!=null?v:"—"}</td>})}</tr>)}</tbody></table></div>}
function TV({sym,h=280}){const [shouldLoad,setShouldLoad]=React.useState(false);React.useEffect(()=>{setShouldLoad(false)},[sym]);if(!sym)return <div style={{height:h,borderRadius:6,border:"1px solid "+C.bd,display:"flex",alignItems:"center",justifyContent:"center",color:C.txM,fontSize:11}}>No chart</div>;if(!shouldLoad)return <div style={{height:h,borderRadius:6,border:"1px solid "+C.bd,background:C.bg,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:10,color:C.txM,fontSize:11}}><div style={{fontSize:12,color:C.txD}}>Chart ready to load</div><div style={{fontFamily:Mn,color:C.tx}}>{sym}</div><button onClick={()=>setShouldLoad(true)} style={{padding:"8px 14px",borderRadius:6,border:"1px solid "+C.ac+"50",background:C.ac+"18",color:C.ac,fontSize:12,fontWeight:700}}>Load Chart</button></div>;return <iframe loading="lazy" title={sym} src={"https://s.tradingview.com/widgetembed/?symbol="+encodeURIComponent(sym)+"&interval=D&range=3M&theme=dark&style=1&locale=en&hide_legend=1&hide_volume=1&hide_side_toolbar=1&allow_symbol_change=0&withdateranges=1&backgroundColor=%23111827&gridColor=%231e293b"} style={{width:"100%",height:h,borderRadius:6,border:"1px solid "+C.bd,background:C.bg}} frameBorder="0"/>}
function RiskBar({d,live}){const vd=(d.VIX?.chg||0)<0,dd=(d.DXY?.chg||0)<0,on=vd&&dd,off=!vd&&!dd,bc=on?C.g:off?C.r:C.am;const fmtLvl=(k,v)=>{if(v==null)return"—";if(k==="2s10s")return v.toFixed(1)+"bp";if(k.includes("Y"))return v.toFixed(2)+"%";return Number(v).toLocaleString(undefined,{maximumFractionDigits:2})};return <div style={{background:"linear-gradient(90deg,"+bc+"18 0%,"+C.bg+" 50%,"+bc+"18 100%)",borderBottom:"1px solid "+bc+"40",padding:"5px 16px",display:"flex",alignItems:"center",gap:16,overflowX:"auto",whiteSpace:"nowrap",fontSize:12}}><span style={{color:bc,fontWeight:700,fontSize:10,letterSpacing:2,padding:"2px 8px",border:"1px solid "+bc+"60",borderRadius:3}}>{on?"RISK ON":off?"RISK OFF":"MIXED"}</span><Dot on={live}/>{Object.entries(d).map(([k,v])=><span key={k} style={{display:"inline-flex",gap:4,alignItems:"center"}}><span style={{color:C.txM,fontWeight:600,fontSize:11}}>{k}</span><span style={{color:C.tx,fontFamily:Mn,fontWeight:600,fontSize:12}}>{fmtLvl(k,v.level)}</span>{v.chg!=null&&<span style={{color:cc(v.chg),fontFamily:Mn,fontSize:10}}>{k.includes("10Y")?fb(v.chg):fc(v.chg,k==="VIX"?"":"%")}</span>}</span>)}</div>}
/* ═══ PAGES ═══════════════════════════════════════════════════════ */
function FxTbl({data,live}){return <div><div style={{display:"flex",alignItems:"center",gap:8,marginBottom:8}}><Dot on={live}/><span style={{color:C.txM,fontSize:10}}>{live?"Live via Frankfurter":"Loading..."}</span></div><Tbl cols={[{k:"pair",l:"Pair",a:"left",nm:1,cl:()=>C.b},{k:"spot",l:"Spot",rn:r=>r.spot!=null?Number(r.spot).toFixed(r.spot>100?2:4):"—"},{k:"d1",l:"1D %",rn:r=>fc(r.d1),cl:r=>cc(r.d1)},{k:"w1",l:"1W %",rn:r=>fc(r.w1),cl:r=>cc(r.w1)},{k:"m1",l:"1M %",rn:r=>fc(r.m1),cl:r=>cc(r.m1)},{k:"ytd",l:"YTD %",rn:r=>fc(r.ytd),cl:r=>cc(r.ytd)}]} rows={data}/></div>}
function YldTbl({yld,live,policyRates}){return <div><div style={{display:"flex",alignItems:"center",gap:8,marginBottom:8}}><Dot on={live}/><span style={{color:C.txM,fontSize:10}}>{live?"Live rates loaded":"No rate data"}</span></div><Tbl cols={[{k:"c",l:"Country",a:"left",nm:1,rn:r=>r.f+" "+r.c},{k:"cb",l:"Policy",rn:r=>r.cb!=null?r.cb.toFixed(2)+"%":"\u2014",cl:r=>r.cb!=null?C.tx:C.txM},{k:"y2",l:"2Y",rn:r=>r.y2!=null?r.y2.toFixed(2)+"%":"\u2014"},{k:"y5",l:"5Y",rn:r=>r.y5!=null?r.y5.toFixed(2)+"%":"\u2014"},{k:"y10",l:"10Y",rn:r=>r.y10!=null?r.y10.toFixed(2)+"%":"\u2014"},{k:"y30",l:"30Y",rn:r=>r.y30!=null?r.y30.toFixed(2)+"%":"\u2014"},{k:"s2s10",l:"2s10s",rn:r=>(r.y2!=null&&r.y10!=null)?fb((r.y10-r.y2)*100):"\u2014",cl:r=>(r.y2!=null&&r.y10!=null)?cc(r.y10-r.y2):C.txM},{k:"s10s30",l:"10s30s",rn:r=>(r.y10!=null&&r.y30!=null)?fb((r.y30-r.y10)*100):"\u2014",cl:r=>(r.y10!=null&&r.y30!=null)?cc(r.y30-r.y10):C.txM},{k:"term",l:"10Y-Policy",rn:r=>(r.cb!=null&&r.y10!=null)?fb((r.y10-r.cb)*100):"\u2014",cl:r=>(r.cb!=null&&r.y10!=null)?cc(r.y10-r.cb):C.txM}]} rows={RT.map(r=>({...r,cb:policyRates?.[r.c]??null,...(yld[r.c]||{})}))} sm/></div>}
function Cmdt({d}){const items=[{k:"gold",l:"Gold XAU",s:"TVC:GOLD"},{k:"wti",l:"WTI Crude",s:"TVC:USOIL"},{k:"brent",l:"Brent",s:"TVC:UKOIL"}];return <div><div className="g3" style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:12,marginBottom:16}}>{items.map(it=>{const v=d[it.k];return <div key={it.k} style={{background:C.bg,border:"1px solid "+C.bd,borderRadius:6,padding:14}}><div style={{color:C.txM,fontSize:10,fontWeight:600,textTransform:"uppercase",letterSpacing:1,marginBottom:4}}>{it.l}</div><div style={{color:C.tx,fontSize:24,fontWeight:700,fontFamily:Mn}}>{v?.level!=null?"$"+Number(v.level).toLocaleString(undefined,{maximumFractionDigits:2}):"—"}</div>{v?.chg!=null&&<div style={{color:cc(v.chg),fontSize:12,fontFamily:Mn,marginTop:2}}>{fc(v.chg)}</div>}</div>})}</div><div className="g3" style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:12}}>{items.map(it=><div key={it.k}><TV sym={it.s} h={220}/></div>)}</div></div>}
function XCry({yld}){const g=RT.filter(r=>["US","Eurozone","UK","Japan","Switzerland","Australia","New Zealand","Canada","Sweden","Norway"].includes(r.c));const gY=c=>yld[c]?.y2??null;const pairs=[];for(let i=0;i<g.length;i++)for(let j=i+1;j<g.length;j++){const a=g[i],b=g[j],ya=gY(a.c),yb=gY(b.c);if(ya==null||yb==null)continue;const d=Math.abs(ya-yb),lc=ya>yb?a:b,sc=ya>yb?b:a;pairs.push({lc:lc.ccy,sc:sc.ccy,lf:lc.f,sf:sc.f,ly:Math.max(ya,yb),sy:Math.min(ya,yb),carry:d})}if(!pairs.length)return <div style={{padding:20,textAlign:"center",color:C.txM,fontSize:12}}>Carry data requires live yield feeds (FRED / ECB)</div>;return <Tbl cols={[{k:"rk",l:"#",a:"center",cl:r=>r.rk<=5?C.g:C.txD},{k:"t",l:"Trade",a:"left",nm:1,rn:r=>r.lf+" Long "+r.lc+" / "+r.sf+" Short "+r.sc},{k:"ly",l:"Long Yld",rn:r=>r.ly.toFixed(2)+"%"},{k:"sy",l:"Short Yld",rn:r=>r.sy.toFixed(2)+"%"},{k:"carry",l:"Carry",rn:r=>"+"+r.carry.toFixed(2)+"%",cl:r=>r.carry>3?C.g:r.carry>1?C.tl:C.txD},{k:"bp",l:"bps",rn:r=>"+"+(r.carry*100).toFixed(0),cl:r=>r.carry>3?C.g:r.carry>1?C.tl:C.txD}]} rows={pairs.sort((a,b)=>b.carry-a.carry).map((r,i)=>({...r,rk:i+1}))} sm/>}
function UCry({yld}){const uY=yld.US?.y2;if(uY==null)return <div style={{padding:20,textAlign:"center",color:C.txM,fontSize:12}}>Carry data requires live US yield feed (FRED)</div>;const rows=RT.filter(r=>r.c!=="US").map(r=>{const ry=yld[r.c]?.y2;return {...r,carry:ry!=null?+((ry-uY).toFixed(2)):null}}).filter(r=>r.carry!=null).sort((a,b)=>b.carry-a.carry).map((r,i)=>({...r,rk:i+1}));if(!rows.length)return <div style={{padding:20,textAlign:"center",color:C.txM,fontSize:12}}>No foreign yield data available yet</div>;return <Tbl cols={[{k:"rk",l:"#",a:"center",cl:r=>r.rk<=3?C.g:r.rk>=13?C.r:C.txD},{k:"c",l:"Currency",a:"left",nm:1,rn:r=>r.f+" "+r.ccy},{k:"y",l:"2Y Yield",rn:r=>(yld[r.c]?.y2!=null?yld[r.c].y2.toFixed(2)+"%":"—")},{k:"carry",l:"vs USD",rn:r=>fc(r.carry),cl:r=>cc(r.carry)},{k:"dir",l:"Direction",a:"left",nm:1,rn:r=>r.carry>0.3?"Long "+r.ccy:"Short "+r.ccy,cl:r=>r.carry>0.3?C.g:C.r}]} rows={rows} sm/>}
function CarryP({yld}){const [t,sT]=useState("Cross G10");return <div><Tabs tabs={["Cross G10","vs USD"]} act={t} set={sT}/>{t==="Cross G10"?<XCry yld={yld}/>:<UCry yld={yld}/>}</div>}
function EtfTbl({etfs}){const rows=Object.entries(etfs||{}).map(([name,v])=>({name,price:v?.price??null,d1:v?.d1_pct??null,w1:v?.w1_pct??null,m1:v?.m1_pct??null}));if(!rows.length)return <div style={{padding:20,textAlign:"center",color:C.txM,fontSize:12}}>No ETF data available</div>;return <Tbl cols={[{k:"name",l:"ETF",a:"left",nm:1,cl:()=>C.b},{k:"price",l:"Price",rn:r=>r.price!=null?Number(r.price).toFixed(2):"—"},{k:"d1",l:"1D %",rn:r=>fc(r.d1),cl:r=>cc(r.d1)},{k:"w1",l:"1W %",rn:r=>fc(r.w1),cl:r=>cc(r.w1)},{k:"m1",l:"1M %",rn:r=>fc(r.m1),cl:r=>cc(r.m1)}]} rows={rows}/>}
function VolTbl({vol}){const rows=Object.entries(vol||{}).map(([name,v])=>({name,price:v?.price??null,prev:v?.prev_close??null,chg:v?.change_pct??null}));if(!rows.length)return <div style={{padding:20,textAlign:"center",color:C.txM,fontSize:12}}>No volatility data available</div>;return <Tbl cols={[{k:"name",l:"Instrument",a:"left",nm:1,cl:()=>C.b},{k:"price",l:"Level",rn:r=>r.price!=null?Number(r.price).toFixed(2):"—"},{k:"prev",l:"Prev",rn:r=>r.prev!=null?Number(r.prev).toFixed(2):"—"},{k:"chg",l:"Change",rn:r=>r.chg!=null?fc(r.chg,""):"—",cl:r=>cc(r.chg)}]} rows={rows}/>}
function BondEtfTbl({bondEtfs}){const rows=Object.entries(bondEtfs||{}).map(([name,v])=>({name,country:v?.country??"",duration:v?.duration??"",price:v?.price??null,d1:v?.d1_pct??null,w1:v?.w1_pct??null,m1:v?.m1_pct??null}));if(!rows.length)return <div style={{padding:20,textAlign:"center",color:C.txM,fontSize:12}}>No bond ETF data available</div>;return <Tbl cols={[{k:"name",l:"Bond ETF",a:"left",nm:1,cl:()=>C.b},{k:"country",l:"Country",a:"left"},{k:"duration",l:"Duration",a:"left"},{k:"price",l:"Price",rn:r=>r.price!=null?Number(r.price).toFixed(2):"—"},{k:"d1",l:"1D %",rn:r=>fc(r.d1),cl:r=>cc(r.d1)},{k:"w1",l:"1W %",rn:r=>fc(r.w1),cl:r=>cc(r.w1)},{k:"m1",l:"1M %",rn:r=>fc(r.m1),cl:r=>cc(r.m1)}]} rows={rows} sm/>}
function Markets({g10,em,fxL,yld,yL,etfs,vol,bondEtfs,policyRates}){const [t,sT]=useState("G10 FX");return <div><Tabs tabs={["G10 FX","EM FX","Rates","Carry","ETFs","Volatility","Bond ETFs"]} act={t} set={sT}/>{t==="G10 FX"&&<FxTbl data={g10} live={fxL}/>}{t==="EM FX"&&<FxTbl data={em} live={fxL}/>}{t==="Rates"&&<YldTbl yld={yld} live={yL} policyRates={policyRates}/>}{t==="Carry"&&<CarryP yld={yld}/>}{t==="ETFs"&&<EtfTbl etfs={etfs}/>}{t==="Volatility"&&<VolTbl vol={vol}/>}{t==="Bond ETFs"&&<BondEtfTbl bondEtfs={bondEtfs}/>}</div>}
/* ═══ SENTIMENT GAUGE ═════════════════════════════════════════════ */
function calcSentiment(risk,g10,em,cmd){
// Sub-score: Volatility (VIX level + VIX change + MOVE)
let vol=50;
const vix=risk.VIX?.level,vChg=risk.VIX?.chg||0;
if(vix!=null){if(vix<15)vol+=20;else if(vix<20)vol+=10;else if(vix<25)vol-=5;else if(vix<30)vol-=15;else vol-=25}
if(vChg<-3)vol+=10;else if(vChg<-1)vol+=5;else if(vChg>3)vol-=10;else if(vChg>1)vol-=5;
const mv=risk.MOVE;if(mv?.level!=null){if(mv.level<90)vol+=8;else if(mv.level>130)vol-=10;else if(mv.level>115)vol-=5}
vol=Math.max(0,Math.min(100,Math.round(vol)));
// Sub-score: Currency Flow (DXY direction + G10 breadth)
let fx=50;const dChg=risk.DXY?.chg||0;
if(dChg<-0.5)fx+=15;else if(dChg<-0.2)fx+=8;else if(dChg>0.5)fx-=15;else if(dChg>0.2)fx-=8;
const g10M=[...g10].filter(x=>x.d1!=null&&x.pair!=="DXY").map(x=>x.d1);
if(g10M.length){const pos=g10M.filter(x=>x>0).length;fx+=Math.round((pos/g10M.length-0.5)*30)}
fx=Math.max(0,Math.min(100,Math.round(fx)));
// Sub-score: Safe Haven (Gold + JPY + CHF)
let hv=50;const gChg=cmd.gold?.chg||0;
if(gChg>2)hv-=18;else if(gChg>0.8)hv-=10;else if(gChg<-1)hv+=12;else if(gChg<-0.3)hv+=5;
const jpy=g10.find(x=>x.pair==="USDJPY");if(jpy?.d1!=null){if(jpy.d1<-0.5)hv-=10;else if(jpy.d1>0.5)hv+=10}
const chf=g10.find(x=>x.pair==="USDCHF");if(chf?.d1!=null){if(chf.d1<-0.3)hv-=6;else if(chf.d1>0.3)hv+=6}
hv=Math.max(0,Math.min(100,Math.round(hv)));
// Sub-score: EM Risk
let emR=50;const emM=[...em].filter(x=>x.d1!=null).map(x=>x.d1);
if(emM.length){const avg=emM.reduce((a,b)=>a+b,0)/emM.length;
if(avg<-0.8)emR+=18;else if(avg<-0.3)emR+=10;else if(avg>0.8)emR-=18;else if(avg>0.3)emR-=10;
const stressed=emM.filter(x=>x>1).length;if(stressed>=3)emR-=10}
emR=Math.max(0,Math.min(100,Math.round(emR)));
// Weighted composite
const score=Math.max(0,Math.min(100,Math.round(vol*0.30+fx*0.25+hv*0.20+emR*0.25)));
return {score,subs:{vol,fx,haven:hv,em:emR}};
}
function sentLabel(s){if(s>=75)return{t:"Extreme Greed",c:C.g};if(s>=60)return{t:"Greed",c:"#4ade80"};if(s>=45)return{t:"Neutral",c:C.am};if(s>=30)return{t:"Fear",c:"#f97316"};return{t:"Extreme Fear",c:C.r}}
function sentNarrative(score,subs,risk,cmd,g10,em){
const p=[];const{vol,fx,haven,em:emS}=subs;const vix=risk.VIX?.level;
if(vol>=60)p.push("Vol regime is calm"+(vix?" with VIX at "+vix.toFixed(1):"")+" — supportive for carry and risk.");
else if(vol<=35)p.push("Elevated vol"+(vix?" (VIX "+vix.toFixed(1)+")":"")+" signals caution — favour defensives and short gamma.");
else p.push("Vol in neutral range"+(vix?" at VIX "+vix.toFixed(1):"")+".");
if(fx>=60)p.push("USD weakening is a tailwind for risk FX.");
else if(fx<=35)p.push("Strong USD is pressuring risk FX crosses.");
if(haven<=35){const jpyD=g10.find(x=>x.pair==="USDJPY");p.push("Safe haven bid in gold"+(jpyD?.d1<-0.3?" and JPY":"")+" confirms defensive positioning.")}
else if(haven>=60)p.push("Safe havens seeing outflows — risk appetite intact.");
if(emS<=35){const worst=[...em].filter(x=>x.d1!=null).sort((a,b)=>b.d1-a.d1).slice(0,2);
p.push("EM stress"+(worst.length?" led by "+worst.map(x=>x.pair.replace("USD","")+" "+fc(x.d1)).join(", "):"")+". Watch for contagion.")}
else if(emS>=60)p.push("EM FX resilient — constructive for EM carry.");
return p.join(" ");
}
function sentDrivers(risk,cmd,g10,em){
const d=[];const vix=risk.VIX,dxy=risk.DXY;
if(vix?.level!=null)d.push({label:"VIX",val:vix.level.toFixed(1),chg:vix.chg,weight:vix.level<20?"Bullish":vix.level<25?"Neutral":"Bearish",cl:vix.level<20?C.g:vix.level<25?C.am:C.r});
if(dxy?.chg!=null)d.push({label:"DXY Direction",val:fc(dxy.chg),chg:dxy.chg,weight:dxy.chg<0?"USD Weak (Risk On)":"USD Bid (Defensive)",cl:dxy.chg<0?C.g:C.r});
const gChg=cmd.gold?.chg;if(gChg!=null)d.push({label:"Gold",val:fc(gChg),chg:gChg,weight:gChg>1?"Safe Haven Bid":gChg<-1?"Risk Appetite":"Flat",cl:gChg>1?C.r:gChg<-1?C.g:C.am});
const jpyP=g10.find(x=>x.pair==="USDJPY");if(jpyP?.d1!=null)d.push({label:"USDJPY",val:fc(jpyP.d1),chg:jpyP.d1,weight:jpyP.d1<-0.3?"JPY Bid (Risk Off)":jpyP.d1>0.3?"JPY Weak (Risk On)":"Sideways",cl:jpyP.d1<-0.3?C.r:jpyP.d1>0.3?C.g:C.am});
const emMv=[...em].filter(x=>x.d1!=null).map(x=>x.d1);if(emMv.length){const a=+(emMv.reduce((s,v)=>s+v,0)/emMv.length).toFixed(2);d.push({label:"EM FX Avg",val:fc(a),chg:a,weight:a<-0.3?"EM under pressure":a>0.3?"EM strength":"Mixed",cl:a<-0.3?C.r:a>0.3?C.g:C.am})}
const mv=risk.MOVE;if(mv?.level!=null)d.push({label:"MOVE (Bond Vol)",val:mv.level.toFixed(1),chg:mv.chg,weight:mv.level<100?"Calm":mv.level<120?"Elevated":"Stressed",cl:mv.level<100?C.g:mv.level<120?C.am:C.r});
const wChg=cmd.wti?.chg;if(wChg!=null)d.push({label:"WTI Crude",val:fc(wChg),chg:wChg,weight:wChg>2?"Supply Concern":wChg<-2?"Demand Worry":"Stable",cl:wChg>2?C.am:wChg<-2?C.r:C.g});
return d;
}
function SubBar({label,icon,value}){
const{t:lb,c:lc}=sentLabel(value);
return <div style={{display:"flex",alignItems:"center",gap:10,padding:"6px 0"}}>
<span style={{fontSize:14,width:22,textAlign:"center"}}>{icon}</span>
<span style={{color:C.txD,fontSize:11,fontWeight:600,width:100}}>{label}</span>
<div style={{flex:1,height:8,background:C.bd,borderRadius:4,position:"relative",overflow:"hidden"}}>
<div style={{position:"absolute",left:0,top:0,bottom:0,width:value+"%",background:"linear-gradient(90deg,"+C.r+","+C.am+","+C.g+")",borderRadius:4,transition:"width 0.5s ease"}}/>
</div>
<span style={{fontFamily:Mn,fontSize:12,fontWeight:700,color:lc,width:28,textAlign:"right"}}>{value}</span>
<span style={{fontSize:10,color:lc,width:90}}>{lb}</span>
</div>;
}
function Sentiment({risk,g10,em,cmd}){
const{score,subs}=calcSentiment(risk,g10,em,cmd);const{t:label,c:lc}=sentLabel(score);
const drivers=sentDrivers(risk,cmd,g10,em);
const narrative=sentNarrative(score,subs,risk,cmd,g10,em);
const ang=Math.PI-(score/100)*Math.PI;
return <div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:20}}>
<div style={{display:"flex",alignItems:"center",gap:10,marginBottom:16}}><span style={{fontSize:20}}>🧭</span><div><div style={{color:C.tx,fontSize:16,fontWeight:700}}>FX Sentiment Gauge</div><div style={{color:C.txM,fontSize:11}}>Composite score from volatility, currency flows, safe havens & EM risk</div></div></div>
<div style={{display:"flex",alignItems:"center",justifyContent:"center",gap:40,marginBottom:20,flexWrap:"wrap"}}>
<div style={{position:"relative",width:240,height:140}}>
<svg viewBox="0 0 240 140" style={{width:240,height:140}}>
<defs><linearGradient id="sg" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stopColor={C.r}/><stop offset="30%" stopColor="#f97316"/><stop offset="50%" stopColor={C.am}/><stop offset="70%" stopColor="#4ade80"/><stop offset="100%" stopColor={C.g}/></linearGradient></defs>
<path d="M20 120 A100 100 0 0 1 220 120" fill="none" stroke={C.bd} strokeWidth="18" strokeLinecap="round"/>
<path d="M20 120 A100 100 0 0 1 220 120" fill="none" stroke="url(#sg)" strokeWidth="18" strokeLinecap="round" strokeDasharray="314" strokeDashoffset={314-(score/100)*314}/>
<line x1="120" y1="120" x2={120+78*Math.cos(ang)} y2={120-78*Math.sin(ang)} stroke={lc} strokeWidth="3" strokeLinecap="round"/>
<circle cx="120" cy="120" r="6" fill={lc}/>
<text x="22" y="138" fill={C.txM} fontSize="9" fontFamily="JetBrains Mono,monospace">0</text>
<text x="120" y="14" fill={C.txM} fontSize="9" fontFamily="JetBrains Mono,monospace" textAnchor="middle">50</text>
<text x="218" y="138" fill={C.txM} fontSize="9" fontFamily="JetBrains Mono,monospace" textAnchor="end">100</text>
</svg>
<div style={{position:"absolute",bottom:0,left:0,right:0,textAlign:"center"}}>
<div style={{color:lc,fontSize:32,fontWeight:800,fontFamily:Mn}}>{score}</div>
</div>
</div>
<div style={{textAlign:"center"}}>
<div style={{color:lc,fontSize:24,fontWeight:800,letterSpacing:1}}>{label}</div>
<div style={{color:C.txM,fontSize:11,marginTop:4}}>0 = Extreme Fear · 100 = Extreme Greed</div>
</div>
</div>
{narrative&&<div style={{padding:14,background:C.bg,borderRadius:8,border:"1px solid "+lc+"25",marginBottom:20}}>
<div style={{color:C.tx,fontSize:12,lineHeight:1.7,fontStyle:"italic"}}>{narrative}</div>
</div>}
<div style={{marginBottom:20}}>
<div style={{color:C.txM,fontSize:10,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>Composition Breakdown</div>
<div style={{background:C.bg,borderRadius:8,border:"1px solid "+C.bd,padding:"8px 14px"}}>
<SubBar label="Volatility" icon="📉" value={subs.vol}/>
<SubBar label="Currency Flow" icon="💱" value={subs.fx}/>
<SubBar label="Safe Haven" icon="🛡" value={subs.haven}/>
<SubBar label="EM Risk" icon="🌍" value={subs.em}/>
</div>
</div>
<div style={{color:C.txM,fontSize:10,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>Signal Drivers</div>
<div style={{display:"grid",gap:6}}>{drivers.map((d,i)=><div key={i} style={{display:"flex",justifyContent:"space-between",alignItems:"center",padding:"8px 12px",background:C.bg,borderRadius:6,border:"1px solid "+C.bd}}>
<span style={{color:C.txD,fontSize:12,fontWeight:600}}>{d.label}</span>
<div style={{display:"flex",alignItems:"center",gap:12}}>
<span style={{color:C.tx,fontFamily:Mn,fontSize:12}}>{d.val}</span>
<span style={{color:d.cl,fontSize:11,fontWeight:600,minWidth:140,textAlign:"right"}}>{d.weight}</span>
</div>
</div>)}</div>
</div>;
}
/* ═══ SCENARIO ANALYSIS ══════════════════════════════════════════ */
function buildScenarios(risk,yld,g10,em,cmd){
const sc=[];const vix=risk.VIX?.level||20,dxy=risk.DXY?.level,dChg=risk.DXY?.chg||0,us10=yld.US?.y10;
const eur=g10.find(x=>x.pair==="EURUSD"),gbp=g10.find(x=>x.pair==="GBPUSD"),jpy=g10.find(x=>x.pair==="USDJPY");
sc.push({pair:"DXY",spot:dxy,bull:{target:dxy?+(dxy*1.03).toFixed(1):null,label:"USD Strength",drivers:["Hot CPI → Fed repricing hawkish","Risk-off episode drives safe-haven USD bid","US growth outperformance vs rest of G10"],prob:vix>25||dChg>0.3?"40%":"25%"},base:{target:dxy?+dxy.toFixed(1):null,label:"Range-bound",drivers:["Gradual disinflation keeps Fed on hold","Growth differential narrows modestly","Cross-currents offset — no dominant theme"],prob:"45%"},bear:{target:dxy?+(dxy*0.97).toFixed(1):null,label:"USD Weakness",drivers:["Dovish Fed pivot / rate cut acceleration","Global growth recovery reduces USD haven demand","Twin deficit concerns resurface"],prob:vix<18&&dChg<-0.3?"40%":"30%"}});
if(eur?.spot)sc.push({pair:"EURUSD",spot:eur.spot,bull:{target:+(eur.spot*1.04).toFixed(4),label:"EUR Rally",drivers:["ECB holds while Fed cuts → rate differential narrows","Eurozone PMIs inflect higher","Ukraine ceasefire catalyst"],prob:"30%"},base:{target:+eur.spot.toFixed(4),label:"Consolidation",drivers:["ECB & Fed both on hold — carry neutral","EZ growth stagnant but not contracting","Range trading 1.051.10"],prob:"45%"},bear:{target:+(eur.spot*0.96).toFixed(4),label:"EUR Sell-off",drivers:["Eurozone recession deepens","US exceptionalism trade resumes","Energy shock / geopolitical premium"],prob:"25%"}});
if(jpy?.spot)sc.push({pair:"USDJPY",spot:jpy.spot,bull:{target:+(jpy.spot*1.04).toFixed(2),label:"JPY Weakness",drivers:["BoJ stays ultra-dovish, US yields stay elevated","Carry trade revival on low vol","Japan CPI rolls over"],prob:"30%"},base:{target:+jpy.spot.toFixed(2),label:"Sideways",drivers:["BoJ gradual normalization priced in","US-JP yield gap stable","MoF intervention threat caps upside"],prob:"40%"},bear:{target:+(jpy.spot*0.96).toFixed(2),label:"JPY Strength",drivers:["BoJ rate hike surprise / YCC exit","Global risk-off → JPY safe haven","US recession fears → Treasury rally"],prob:"30%"}});
if(gbp?.spot)sc.push({pair:"GBPUSD",spot:gbp.spot,bull:{target:+(gbp.spot*1.03).toFixed(4),label:"GBP Rally",drivers:["BoE holds while Fed cuts","UK services inflation stays sticky","Fiscal outlook stabilises"],prob:"30%"},base:{target:+gbp.spot.toFixed(4),label:"Range-bound",drivers:["BoE cautious easing — 25bp per quarter","UK growth muddles through","Brexit frictions fade from focus"],prob:"45%"},bear:{target:+(gbp.spot*0.97).toFixed(4),label:"GBP Sell-off",drivers:["UK recession + rapid BoE cuts","Gilt market stress returns","Broad USD strength"],prob:"25%"}});
return sc;
}
function ScenPanel({s}){
const cols=[{k:"bull",label:"BULL",emoji:"🟢",c:C.g},{k:"base",label:"BASE",emoji:"🟡",c:C.am},{k:"bear",label:"BEAR",emoji:"🔴",c:C.r}];
return <div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16,marginBottom:12}}>
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:12}}>
<span style={{color:C.b,fontSize:14,fontWeight:800,fontFamily:Mn}}>{s.pair}</span>
<span style={{color:C.txD,fontSize:12}}>Spot: <span style={{color:C.tx,fontFamily:Mn}}>{s.spot!=null?s.spot:"—"}</span></span>
</div>
<div className="g3" style={{display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gap:10}}>
{cols.map(cl=>{
const sc=s[cl.k];if(!sc)return null;
return <div key={cl.k} style={{background:C.bg,borderRadius:6,border:"1px solid "+cl.c+"30",padding:12}}>
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:8}}>
<span style={{color:cl.c,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1}}>{cl.emoji} {cl.label}</span>
<span style={{color:cl.c,fontSize:11,fontWeight:700,fontFamily:Mn,background:cl.c+"18",padding:"2px 8px",borderRadius:3}}>{sc.prob}</span>
</div>
<div style={{color:C.tx,fontWeight:700,fontSize:13,marginBottom:4}}>{sc.label}</div>
{sc.target!=null&&<div style={{color:cl.c,fontFamily:Mn,fontSize:12,marginBottom:8}}>Target: {sc.target}</div>}
<div style={{display:"grid",gap:4}}>{sc.drivers.map((d,i)=><div key={i} style={{color:C.txD,fontSize:11,lineHeight:1.5}}> {d}</div>)}</div>
</div>;
})}
</div>
</div>;
}
function Scenarios({risk,yld,g10,em,cmd}){
const scenarios=buildScenarios(risk,yld,g10,em,cmd);
return <div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:20}}>
<div style={{display:"flex",alignItems:"center",gap:10,marginBottom:16}}><span style={{fontSize:20}}>🎯</span><div><div style={{color:C.tx,fontSize:16,fontWeight:700}}>Scenario Analysis</div><div style={{color:C.txM,fontSize:11}}>Bull / Base / Bear for key FX pairs — computed from live data</div></div></div>
{scenarios.map((s,i)=><ScenPanel key={i} s={s}/>)}
</div>;
}
/* ═══ CURVES ══════════════════════════════════════════════════════ */
function Curves({yld,policyRates}){const majors=["US","UK","Eurozone","Japan","Canada","Australia","New Zealand","Switzerland","Sweden","Norway"];const rows=majors.map(c=>{const y=yld[c]||{};const cb=policyRates?.[c]??null;return{c,f:RT.find(r=>r.c===c)?.f||"",cb,y2:y.y2??null,y5:y.y5??null,y10:y.y10??null,y30:y.y30??null,s2s10:(y.y2!=null&&y.y10!=null)?y.y10-y.y2:null,s10s30:(y.y10!=null&&y.y30!=null)?y.y30-y.y10:null,term:(cb!=null&&y.y10!=null)?y.y10-cb:null}});return <div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}><div style={{color:C.tx,fontSize:15,fontWeight:700,marginBottom:12}}>Curve Monitor</div><Tbl cols={[{k:"c",l:"Country",a:"left",nm:1,rn:r=>r.f+" "+r.c},{k:"cb",l:"Policy",rn:r=>r.cb!=null?r.cb.toFixed(2)+"%":"\u2014"},{k:"y2",l:"2Y",rn:r=>r.y2!=null?r.y2.toFixed(2)+"%":"\u2014"},{k:"y5",l:"5Y",rn:r=>r.y5!=null?r.y5.toFixed(2)+"%":"\u2014"},{k:"y10",l:"10Y",rn:r=>r.y10!=null?r.y10.toFixed(2)+"%":"\u2014"},{k:"y30",l:"30Y",rn:r=>r.y30!=null?r.y30.toFixed(2)+"%":"\u2014"},{k:"s2s10",l:"2s10s",rn:r=>r.s2s10!=null?fb(r.s2s10*100):"\u2014",cl:r=>r.s2s10!=null?cc(r.s2s10):C.txM},{k:"s10s30",l:"10s30s",rn:r=>r.s10s30!=null?fb(r.s10s30*100):"\u2014",cl:r=>r.s10s30!=null?cc(r.s10s30):C.txM},{k:"term",l:"10Y-Policy",rn:r=>r.term!=null?fb(r.term*100):"\u2014",cl:r=>r.term!=null?cc(r.term):C.txM}]} rows={rows} sm/></div>}
/* ═══ SPREADS ═════════════════════════════════════════════════════ */
function Spreads({yld}){const us10=yld.US?.y10??null;const de10=yld.Eurozone?.y10??yld.Germany?.y10??null;const jp10=yld.Japan?.y10??null;const uk10=yld.UK?.y10??null;const it10=yld.Italy?.y10??null;const fr10=yld.France?.y10??null;const es10=yld.Spain?.y10??null;const ca10=yld.Canada?.y10??null;const au10=yld.Australia?.y10??null;const rows=[{name:"UST 10Y \u2013 Bund 10Y",spread:(us10!=null&&de10!=null)?us10-de10:null},{name:"UST 10Y \u2013 JGB 10Y",spread:(us10!=null&&jp10!=null)?us10-jp10:null},{name:"Gilt 10Y \u2013 Bund 10Y",spread:(uk10!=null&&de10!=null)?uk10-de10:null},{name:"BTP 10Y \u2013 Bund 10Y",spread:(it10!=null&&de10!=null)?it10-de10:null},{name:"OAT 10Y \u2013 Bund 10Y",spread:(fr10!=null&&de10!=null)?fr10-de10:null},{name:"Spain 10Y \u2013 Bund 10Y",spread:(es10!=null&&de10!=null)?es10-de10:null},{name:"Canada 10Y \u2013 UST 10Y",spread:(ca10!=null&&us10!=null)?ca10-us10:null},{name:"Australia 10Y \u2013 UST 10Y",spread:(au10!=null&&us10!=null)?au10-us10:null}];return <div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}><div style={{color:C.tx,fontSize:15,fontWeight:700,marginBottom:12}}>Cross-Market Spreads</div><Tbl cols={[{k:"name",l:"Spread",a:"left",nm:1},{k:"spread",l:"Value",rn:r=>r.spread!=null?fb(r.spread*100):"\u2014",cl:r=>r.spread!=null?cc(r.spread):C.txM}]} rows={rows} sm/></div>}
/* ═══ DESK VIEW BUILDER ═══════════════════════════════════════════ */
function buildDeskView({ctry,ccy,fx,y,cbRate,m,risk,headlines,yld}){
const s2s10=(y.y2!=null&&y.y10!=null)?y.y10-y.y2:null;
const s10s30=(y.y10!=null&&y.y30!=null)?y.y30-y.y10:null;
const pGap=(cbRate!=null&&y.y10!=null)?y.y10-cbRate:null;
const frontGap=(cbRate!=null&&y.y2!=null)?y.y2-cbRate:null;
const us10=yld?.US?.y10??null;
const spreadVsUS=(us10!=null&&y.y10!=null&&ctry!=="US")?(y.y10-us10):null;
const vix=risk?.VIX,dxy=risk?.DXY,gold=risk?.Gold,wti=risk?.WTI;
// ── Trading Narrative ──
const nL=[];
nL.push(ctry+" ("+ccy+") is trading in a "+(fx?.d1>0.5?"constructive FX tone on the session":fx?.d1<-0.5?"softer FX tone on the session":"largely range-bound FX environment")+".");
if(s2s10!=null){
if(s2s10<-0.001)nL.push("The yield curve remains inverted at "+(s2s10*100).toFixed(0)+"bp (2s10s), pointing to restrictive policy and easing expectations priced further out.");
else if(s2s10<0.005)nL.push("The curve is essentially flat at "+(s2s10*100).toFixed(0)+"bp (2s10s), a transitional shape between restrictive and normal regimes.");
else nL.push("The curve is positively sloped at +"+(s2s10*100).toFixed(0)+"bp (2s10s), consistent with a more normal term structure.");
}
if(frontGap!=null){
if(frontGap<-0.25)nL.push("Front-end yields sit below the policy rate, suggesting the market is pricing a softer path for rates.");
else if(frontGap>0.25)nL.push("Front-end yields sit above the policy rate, suggesting higher-for-longer or tightening risk remains priced.");
else nL.push("Front-end yields are broadly aligned with the current policy rate.");
}
if(spreadVsUS!=null)nL.push("The 10Y spread vs UST is "+(spreadVsUS>0?"+":"")+(spreadVsUS*100).toFixed(0)+"bp"+(spreadVsUS>0.5?", indicating a significant local risk premium.":spreadVsUS<-0.5?", reflecting lower local yields versus the US benchmark.":"."));
if(m?.stance)nL.push("Central bank stance is currently assessed as "+m.stance.toLowerCase()+".");
// Cross-asset overlay
const vixLvl=vix?.level,dxyChg=dxy?.chg,goldChg=gold?.chg;
if(vixLvl!=null&&vixLvl>25&&goldChg>0&&dxyChg>0)nL.push("Cross-asset signals point to a defensive tone — elevated VIX, gold bid, and USD strength.");
else if(vixLvl!=null&&vixLvl<18&&dxyChg<0)nL.push("Cross-asset backdrop is supportive for carry and risk — VIX subdued, USD softer.");
// ── Macro Sections ──
const ratesBullets=[];
if(cbRate!=null)ratesBullets.push("Policy rate: "+cbRate.toFixed(2)+"%.");
if(y.y2!=null)ratesBullets.push("2Y yield: "+y.y2.toFixed(2)+"%.");
if(y.y5!=null)ratesBullets.push("5Y yield: "+y.y5.toFixed(2)+"%.");
if(y.y10!=null)ratesBullets.push("10Y yield: "+y.y10.toFixed(2)+"%.");
if(y.y30!=null)ratesBullets.push("30Y yield: "+y.y30.toFixed(2)+"%.");
if(s2s10!=null)ratesBullets.push("2s10s spread: "+(s2s10>0?"+":"")+(s2s10*100).toFixed(0)+"bp"+(s2s10<0?" (inverted)":s2s10<0.005?" (flat)":" (positive slope)")+".");
if(s10s30!=null)ratesBullets.push("10s30s spread: "+(s10s30>0?"+":"")+(s10s30*100).toFixed(0)+"bp.");
if(pGap!=null)ratesBullets.push("10Y minus policy rate: "+(pGap>0?"+":"")+(pGap*100).toFixed(0)+"bp"+(pGap<0?" — long end pricing below policy.":"."));
if(spreadVsUS!=null)ratesBullets.push("10Y vs UST: "+(spreadVsUS>0?"+":"")+(spreadVsUS*100).toFixed(0)+"bp.");
const macroBullets=[];
if(m.cpi)macroBullets.push("CPI YoY: "+m.cpi+(m.cpiP?" (prior "+m.cpiP+")":"")+".");
if(m.gdp)macroBullets.push("GDP: "+m.gdp+(m.gdpT?" ("+m.gdpT+")":"")+".");
if(m.unemp)macroBullets.push("Unemployment: "+m.unemp+(m.unempT?" ("+m.unempT+")":"")+".");
if(m.stance)macroBullets.push("CB stance: "+m.stance+".");
if(m.last)macroBullets.push("Last decision: "+m.last+".");
if(m.next)macroBullets.push("Next meeting: "+m.next+".");
if(m.pricing)macroBullets.push("Market pricing: "+m.pricing+".");
const causeEffect=[];
if(m.cpi){
const cpiNum=parseFloat(m.cpi);
if(cpiNum>3)causeEffect.push("Elevated CPI → cuts repriced out → front-end yields supported → "+ccy+" backstopped by rate differentials.");
else if(cpiNum<2)causeEffect.push("Low inflation → easing expectations firm → front-end yields drift lower → "+ccy+" under pressure from rate convergence.");
}
if(s2s10!=null&&s2s10<0)causeEffect.push("Inverted curve → recession signal / late-cycle dynamics → defensive positioning favoured.");
if(wti?.chg!=null&&wti.chg>2)causeEffect.push("Oil rally → inflation expectations rise → long-end yields pushed higher → commodity FX supported.");
if(goldChg!=null&&goldChg>1&&vixLvl!=null&&vixLvl>20)causeEffect.push("Gold bid + elevated VIX → haven demand → JPY/CHF supported, risk FX under pressure.");
// ── FX View ──
const fxDir=fx?.d1>0.5?"Constructive — "+ccy+" stronger on the day":fx?.d1<-0.5?"Defensive — "+ccy+" weaker on the day":"Neutral — "+ccy+" range-bound";
const fxRat=fx?.d1>0.5?"Spot performance is positive, suggesting supportive macro backdrop, yield backing, or favourable positioning flows.":fx?.d1<-0.5?"Spot performance is weaker, pointing to softer domestic drivers, external headwinds, or positioning unwind.":"No strong directional break. Watch for catalyst from rates or calendar events to establish direction.";
const fxLevels=[];
if(fx?.spot!=null)fxLevels.push("Spot: "+Number(fx.spot).toFixed(fx.spot>100?2:4));
if(fx?.d1!=null)fxLevels.push("1D move: "+fc(fx.d1));
if(fx?.w1!=null)fxLevels.push("1W move: "+fc(fx.w1));
if(y.y2!=null)fxLevels.push("2Y yield: "+y.y2.toFixed(2)+"%");
if(y.y10!=null)fxLevels.push("10Y yield: "+y.y10.toFixed(2)+"%");
if(s2s10!=null)fxLevels.push("2s10s: "+(s2s10*100).toFixed(0)+"bp");
// ── Rates View ──
let rBias,rRat;
if(s2s10==null){rBias="Insufficient curve data";rRat="Not enough tenor data to assess curve regime.";}
else if(s2s10<-0.001){rBias="Inverted curve — restrictive regime";rRat="Short-end yields remain above the long end, usually consistent with restrictive policy and easing expectations priced for later. Historically associated with late-cycle dynamics.";}
else if(s2s10<0.005){rBias="Flat curve — transitional regime";rRat="The curve is essentially flat, often a transition point between restrictive and easing cycles. Duration positioning becomes more tactical.";}
else if(s2s10<0.005){rBias="Mildly positive — early normalisation";rRat="Curve is gently positive, consistent with early-stage normalisation as the market prices a return to more conventional term premium.";}
else{rBias="Positive slope — normal term structure";rRat="Long-end yields sit above the front end, consistent with normal growth/inflation pricing and positive term premium.";}
// ── Risks ──
const risks=["Unexpected central bank rhetoric shift or inter-meeting action","Domestic inflation surprise — either direction","Global risk-off event lifting USD and safe havens"];
if(wti?.level!=null&&wti.level>85)risks.push("Elevated energy prices feeding through to inflation expectations");
if(vixLvl!=null&&vixLvl>25)risks.push("VIX remains elevated — volatility regime may persist");
if(s2s10!=null&&s2s10<-0.3)risks.push("Deeply inverted curve — historical recession signal");
if(m.next)risks.push("Upcoming CB meeting ("+m.next+") — potential for vol around decision");
// ── Headline Takeaways ──
const hlTakeaways=headlines.slice(0,4).map(h=>h.headline||"No headline available");
return{narrative:nL,macro_sections:[{title:"Rates Structure",bullets:ratesBullets.length?ratesBullets:["No rate data available."]},{title:"Macro Backdrop",bullets:macroBullets.length?macroBullets:["No macro data available."]},{title:"Cause → Effect",bullets:causeEffect.length?causeEffect:["No strong directional signals from current data."]}],fx_view:{direction:fxDir,rationale:fxRat,levels_to_watch:fxLevels.length?fxLevels:["Watch for rate/calendar catalysts"]},rates_view:{bias:rBias,rationale:rRat},key_risks:risks,headline_takeaways:hlTakeaways};
}
/* ═══ DEEP DIVE ═══════════════════════════════════════════════════ */
function Deep({ctry,g10,em,yld,news,nL,risk,macro,policyRates,govBonds}){
const ccy=cOf[ctry],fp=[...g10,...em].find(x=>x.pair===pOf[ctry]),y=yld[ctry]||{},tpl=RT.find(x=>x.c===ctry),m=macro?.[ctry]||{},cbRate=policyRates?.[ctry]??null;
const pfxList=bondPfx[ctry]||[];const cBonds=Object.entries(govBonds||{}).filter(([k])=>pfxList.some(p=>k.startsWith(p))).map(([k,v])=>({key:k,...v}));
const kws=nKw[ctry]||[ccy.toLowerCase()];const cN=news.filter(n=>kws.some(kw=>(n.headline||"").toLowerCase().includes(kw)));const dN=cN.length>=2?cN:news;
const ce=Object.entries(y).filter(([k,v])=>v!=null&&k.startsWith("y")).map(([k,v])=>[k.replace("y","").toUpperCase(),v]);
const curve2s10s=(y.y2!=null&&y.y10!=null)?y.y10-y.y2:null;
const curve10s30s=(y.y10!=null&&y.y30!=null)?y.y30-y.y10:null;
const policyGap=(cbRate!=null&&y.y10!=null)?y.y10-cbRate:null;
const us10=yld.US?.y10??null;
const localSpreadVsUS=(us10!=null&&y.y10!=null&&ctry!=="US")?y.y10-us10:null;
const [dv,setDv]=useState(null);
const gen=()=>{
const view=buildDeskView({ctry,ccy,fx:fp?{pair:fp.pair,spot:fp.spot,d1:fp.d1,w1:fp.w1,m1:fp.m1,ytd:fp.ytd}:null,y,cbRate,m,risk,headlines:dN.slice(0,8),yld});
setDv(view);
};
return <div>
<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:20,marginBottom:16}}>
<div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:12}}>
<div style={{color:C.tx,fontSize:15,fontWeight:700}}>📋 DESK NOTE {ctry} ({ccy})</div>
<button
onClick={gen}
style={{
padding:"8px 18px",
background:"linear-gradient(135deg,"+C.tl+",#38bdf8)",
color:"#fff",
border:"none",
borderRadius:6,
fontSize:12,
fontWeight:700
}}
>
{dv ? "↻ Refresh View" : "Generate Desk Note"}
</button>
</div>
{!dv && (
<div style={{padding:20,background:C.bg,borderRadius:6,border:"1px dashed "+C.bd,textAlign:"center",color:C.txM,fontSize:12}}>
Click "Generate Desk Note" for a signal-based macro view
</div>
)}
{dv && (
<>
<div style={{marginBottom:14}}>
<div style={{color:C.tl,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:6}}>
Trading Narrative
</div>
<div style={{padding:14,background:C.bg,borderRadius:6,border:"1px solid "+C.tl+"25"}}>
<div style={{display:"grid",gap:8}}>{dv.narrative.map((b,i)=><div key={i} style={{display:"flex",gap:8,alignItems:"flex-start"}}><span style={{color:C.tl,fontSize:13,flexShrink:0}}></span><span style={{color:C.tx,fontSize:13,lineHeight:1.7}}>{b}</span></div>)}</div>
</div>
</div>
<div style={{color:C.tl,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>
Macroeconomic Data & Trends
</div>
<div style={{display:"grid",gridTemplateColumns:"1fr",gap:10,marginBottom:14}}>
{dv.macro_sections.map((sec, i) => (
<div key={i} style={{background:C.bg,padding:14,borderRadius:6,border:"1px solid "+C.bd}}>
<div style={{color:C.b,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>
{sec.title}
</div>
<div style={{display:"grid",gap:6}}>
{sec.bullets.map((b, j) => (
<div key={j} style={{color:C.txD,fontSize:12,lineHeight:1.6}}>
{b}
</div>
))}
</div>
</div>
))}
</div>
<div className="g2" style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:12}}>
<div style={{background:C.bg,padding:14,borderRadius:6,border:"1px solid "+C.bd}}>
<div style={{color:C.g,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>
FX View
</div>
<div style={{color:C.tx,fontWeight:700,marginBottom:6}}>{dv.fx_view.direction}</div>
<div style={{color:C.txD,fontSize:12,lineHeight:1.6,marginBottom:8}}>{dv.fx_view.rationale}</div>
{dv.fx_view.levels_to_watch.map((x, i) => (
<div key={i} style={{color:C.am,fontSize:11,marginBottom:4}}> {x}</div>
))}
</div>
<div style={{background:C.bg,padding:14,borderRadius:6,border:"1px solid "+C.bd}}>
<div style={{color:C.r,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>
Rates View
</div>
<div style={{color:C.tx,fontWeight:700,marginBottom:6}}>{dv.rates_view.bias}</div>
<div style={{color:C.txD,fontSize:12,lineHeight:1.6}}>{dv.rates_view.rationale}</div>
</div>
</div>
<div className="g2" style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:12,marginTop:12}}>
<div style={{background:C.bg,padding:14,borderRadius:6,border:"1px solid "+C.bd}}>
<div style={{color:C.am,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>
Key Risks
</div>
{dv.key_risks.map((x, i) => (
<div key={i} style={{color:C.txD,fontSize:12,lineHeight:1.6,marginBottom:4}}> {x}</div>
))}
</div>
<div style={{background:C.bg,padding:14,borderRadius:6,border:"1px solid "+C.bd}}>
<div style={{color:C.tl,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:8}}>
Headline Takeaways
</div>
{dv.headline_takeaways.map((x, i) => (
<div key={i} style={{color:C.txD,fontSize:12,lineHeight:1.6,marginBottom:4}}> {x}</div>
))}
</div>
</div>
</>
)}
</div>
<div className="g2" style={{display:"grid",gridTemplateColumns:"1.1fr 0.9fr",gap:16}}>
<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}>
<div style={{display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:8}}>
<div>
<div style={{color:C.txM,fontSize:10,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:4}}>
FX {pOf[ctry]}
</div>
<div style={{color:C.tx,fontSize:34,fontWeight:800,fontFamily:Mn}}>
{fp?.spot!=null?Number(fp.spot).toFixed(fp.spot>100?2:4):"—"}
</div>
<div style={{display:"flex",gap:8,flexWrap:"wrap",marginTop:6}}>
{[["1D",fp?.d1],["1W",fp?.w1],["1M",fp?.m1],["YTD",fp?.ytd]].map(([t,v])=>(
<span key={t} style={{fontSize:11,fontFamily:Mn,color:v!=null?cc(v):C.txM}}>
{t}: {v!=null?fc(v):"—"}
</span>
))}
</div>
</div>
</div>
<TV sym={tvF[ctry]} h={290}/>
</div>
<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}>
<div style={{color:C.tx,fontSize:14,fontWeight:700,marginBottom:10}}>Yield Curve</div>
<div style={{display:"grid",gap:6,marginBottom:12}}>
{Object.entries(y)
.filter(([k,v]) => k.startsWith("y") && v != null)
.map(([k,v]) => (
<div key={k} style={{display:"flex",justifyContent:"space-between",padding:"4px 0",borderBottom:"1px solid "+C.bd+"30"}}>
<span style={{color:C.txD,fontSize:11}}>{k.replace("y","").toUpperCase()}</span>
<span style={{color:C.tx,fontFamily:Mn,fontSize:12}}>{v.toFixed(2)}%</span>
</div>
))}
</div>
<div style={{marginTop:12,paddingTop:10,borderTop:"1px solid "+C.bd}}>
<div style={{display:"flex",justifyContent:"space-between",padding:"4px 0"}}><span style={{color:C.txD,fontSize:11}}>2s10s</span><span style={{color:curve2s10s!=null?cc(curve2s10s):C.txM,fontFamily:Mn,fontSize:12}}>{curve2s10s!=null?fb(curve2s10s*100):"\u2014"}</span></div>
<div style={{display:"flex",justifyContent:"space-between",padding:"4px 0"}}><span style={{color:C.txD,fontSize:11}}>10s30s</span><span style={{color:curve10s30s!=null?cc(curve10s30s):C.txM,fontFamily:Mn,fontSize:12}}>{curve10s30s!=null?fb(curve10s30s*100):"\u2014"}</span></div>
<div style={{display:"flex",justifyContent:"space-between",padding:"4px 0"}}><span style={{color:C.txD,fontSize:11}}>10Y - Policy</span><span style={{color:policyGap!=null?cc(policyGap):C.txM,fontFamily:Mn,fontSize:12}}>{policyGap!=null?fb(policyGap*100):"\u2014"}</span></div>
{ctry!=="US"&&<div style={{display:"flex",justifyContent:"space-between",padding:"4px 0"}}><span style={{color:C.txD,fontSize:11}}>10Y vs US</span><span style={{color:localSpreadVsUS!=null?cc(localSpreadVsUS):C.txM,fontFamily:Mn,fontSize:12}}>{localSpreadVsUS!=null?fb(localSpreadVsUS*100):"\u2014"}</span></div>}
</div>
{tvY[ctry]
? <TV sym={tvY[ctry]} h={170}/>
: <div style={{height:170,borderRadius:6,border:"1px solid "+C.bd,display:"flex",alignItems:"center",justifyContent:"center",color:C.txM,fontSize:11}}>
Yield chart available for US, Eurozone, UK, Japan, Canada, Australia
</div>
}
</div>
</div>
{cBonds.length>0&&<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16,marginTop:16}}><div style={{color:C.tx,fontSize:14,fontWeight:700,marginBottom:10}}>🏦 Government Bonds</div><Tbl cols={[{k:"label",l:"Bond",a:"left",nm:1,cl:()=>C.b},{k:"value",l:"Yield %",rn:r=>r.value!=null?Number(r.value).toFixed(3):"\u2014"},{k:"change",l:"Chg (bp)",rn:r=>r.change!=null?fb(r.change*100):"\u2014",cl:r=>cc(r.change)},{k:"date",l:"Date",a:"left",cl:()=>C.txM}]} rows={cBonds} sm/></div>}
<div className="g3" style={{display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gap:16,marginTop:16}}>
<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}>
<div style={{color:C.tx,fontSize:13,fontWeight:700,marginBottom:10}}>Central Bank</div>
<div style={{display:"grid",gap:8}}>
<div style={{display:"flex",justifyContent:"space-between"}}><span style={{color:C.txM,fontSize:11}}>Policy Rate</span><span style={{color:C.tx,fontWeight:700,fontFamily:Mn}}>{cbRate!=null ? cbRate.toFixed(2)+"%" : "—"}</span></div>
<div style={{display:"flex",justifyContent:"space-between"}}><span style={{color:C.txM,fontSize:11}}>Stance</span><span style={{color:C.am,fontSize:11,fontWeight:700}}>{m.stance||"—"}</span></div>
<div style={{display:"flex",justifyContent:"space-between"}}><span style={{color:C.txM,fontSize:11}}>Last Decision</span><span style={{color:C.tx,fontSize:11}}>{m.last||"—"}</span></div>
<div style={{display:"flex",justifyContent:"space-between"}}><span style={{color:C.txM,fontSize:11}}>Next Meeting</span><span style={{color:C.g,fontSize:11}}>{m.next||"—"}</span></div>
<div style={{display:"flex",justifyContent:"space-between"}}><span style={{color:C.txM,fontSize:11}}>Pricing</span><span style={{color:C.tx,fontSize:11}}>{m.pricing||"—"}</span></div>
</div>
</div>
<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}>
<div style={{color:C.tx,fontSize:13,fontWeight:700,marginBottom:10}}>Macro Snapshot</div>
<div className="g3" style={{display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gap:8}}>
<div style={{background:C.bg,padding:10,borderRadius:6}}>
<div style={{color:C.txM,fontSize:10}}>CPI YoY</div>
<div style={{color:C.tx,fontSize:22,fontWeight:700,fontFamily:Mn}}>{m.cpi||"—"}</div>
<div style={{color:C.am,fontSize:10}}>Prior {m.cpiP||"—"}</div>
</div>
<div style={{background:C.bg,padding:10,borderRadius:6}}>
<div style={{color:C.txM,fontSize:10}}>GDP</div>
<div style={{color:C.tx,fontSize:22,fontWeight:700,fontFamily:Mn}}>{m.gdp||"—"}</div>
<div style={{color:C.am,fontSize:10}}>{m.gdpT||"—"}</div>
</div>
<div style={{background:C.bg,padding:10,borderRadius:6}}>
<div style={{color:C.txM,fontSize:10}}>Unemployment</div>
<div style={{color:C.tx,fontSize:22,fontWeight:700,fontFamily:Mn}}>{m.unemp||"—"}</div>
<div style={{color:C.am,fontSize:10}}>{m.unempT||"—"}</div>
</div>
</div>
</div>
<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}>
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:10}}>
<span style={{color:C.tx,fontSize:13,fontWeight:700}}>News</span>
<Dot on={nL}/>
</div>
<div style={{display:"grid",gap:8}}>
{dN.slice(0,4).map((n,i)=>(
<div key={i} style={{padding:"8px 10px",background:C.bg,borderRadius:6}}>
<a href={n.url||"#"} target="_blank" rel="noopener" style={{color:C.tx,fontSize:11,fontWeight:500,lineHeight:1.4,textDecoration:"none",display:"block"}}>
{n.headline}
</a>
<div style={{color:C.txM,fontSize:9,marginTop:3}}>
{n.source} · {n.datetime?new Date(n.datetime*1000).toLocaleString("en-GB",{day:"numeric",month:"short",hour:"2-digit",minute:"2-digit"}):"—"}
</div>
</div>
))}
</div>
</div>
</div>
</div>;
}
/* ═══ BRIEF ═══════════════════════════════════════════════════════ */
function genBr(g10,em,risk,yld,cal,cmd){const L=[];const vix=risk.VIX,dxy=risk.DXY;const vD=(vix?.chg||0)<0,dD=(dxy?.chg||0)<0;if(vD&&dD)L.push("Risk on — VIX down "+Math.abs(vix?.chg||0).toFixed(1)+" at "+(vix?.level?.toFixed(1)||"—")+", USD softer at "+(dxy?.level?.toFixed(1)||"—")+" ("+fc(dxy?.chg)+"). Supportive for carry.");else if(!vD&&!dD)L.push("Defensive tone — VIX up at "+(vix?.level?.toFixed(1)||"—")+", USD bid at "+(dxy?.level?.toFixed(1)||"—")+" ("+fc(dxy?.chg)+"). JPY/CHF finding support.");else L.push("Mixed — VIX "+(vix?.level?.toFixed(1)||"—")+" ("+fc(vix?.chg,"")+"), DXY "+(dxy?.level?.toFixed(1)||"—")+" ("+fc(dxy?.chg)+"). Positioning ahead of events.");const mv=[...g10,...em].filter(x=>x.d1!=null).sort((a,b)=>Math.abs(b.d1)-Math.abs(a.d1)).slice(0,4);if(mv.length)L.push("Movers: "+mv.map(x=>x.pair+" "+(x.d1>0?"▲":"▼")+Math.abs(x.d1).toFixed(2)+"%").join(", ")+".");if(cmd.gold?.level)L.push("Gold $"+cmd.gold.level.toFixed(0)+(cmd.gold.chg!=null?" ("+fc(cmd.gold.chg)+")":"")+"."+(cmd.wti?.level?" WTI $"+cmd.wti.level.toFixed(2)+(cmd.wti.chg!=null?" ("+fc(cmd.wti.chg)+")":"")+".":" "));const u2=yld.US?.y2;if(u2!=null){const tc=RT.filter(r=>r.c!=="US"&&yld[r.c]?.y2!=null).map(r=>({ccy:r.ccy,c:+((yld[r.c].y2)-u2).toFixed(2)})).filter(x=>x.c>1).sort((a,b)=>b.c-a.c).slice(0,3);L.push("US 2Y "+u2.toFixed(2)+"%."+(tc.length?" Top carry: "+tc.map(x=>x.ccy+" +"+x.c+"%").join(", ")+".":" "))}const us10=yld.US?.y10;if(u2!=null&&us10!=null){const curve=us10-u2;L.push("US curve at "+((curve*100)>0?"+":"")+(curve*100).toFixed(1)+"bp (2s10s).")}const de10=yld.Eurozone?.y10;if(us10!=null&&de10!=null){L.push("UST-Bund spread "+(((us10-de10)*100).toFixed(0))+"bp.")}const hi=cal.filter(e=>e.imp==="red").slice(0,4);if(hi.length)L.push("Watch: "+hi.map(e=>e.ctry+" "+e.ev+" ("+e.date+")").join(", ")+".");return L}
function Brief({g10,em,yld,risk,cal,cmd}){const [br,sBr]=useState(null);const [ts,sTs]=useState(null);const [ld,sLd]=useState(false);const gen=()=>{sLd(true);setTimeout(()=>{sBr(genBr(g10,em,risk,yld,cal,cmd));sTs(new Date().toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"}));sLd(false)},600)};return <div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:20}}><div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:16,flexWrap:"wrap",gap:8}}><div style={{display:"flex",alignItems:"center",gap:10}}><span style={{fontSize:20}}>📋</span><div><div style={{color:C.tx,fontSize:16,fontWeight:700}}>Signal-Based Morning Brief</div><div style={{color:C.txM,fontSize:11}}>Auto-generated from live FX, yields, carry, commodities & calendar</div></div></div><div style={{display:"flex",alignItems:"center",gap:8}}>{ts&&<span style={{color:C.txM,fontSize:10}}>Generated {ts}</span>}<button onClick={gen} disabled={ld} style={{padding:"10px 24px",background:ld?"transparent":"linear-gradient(135deg,"+C.ac+","+C.acD+")",color:ld?C.ac:"#fff",border:ld?"1px solid "+C.ac+"40":"none",borderRadius:6,fontSize:12,fontWeight:700}}>{ld?<><span className="sp" style={{marginRight:4}}/>Analysing</>:br?"↻ Regen":"⚡ Generate Brief"}</button></div></div>{br?<div style={{padding:16,background:C.bg,borderRadius:8,border:"1px solid "+C.ac+"25"}}><div style={{display:"grid",gap:8}}>{br.map((b,i)=><div key={i} style={{display:"flex",gap:8,alignItems:"flex-start"}}><span style={{color:C.ac,fontSize:13,flexShrink:0}}>•</span><span style={{color:C.tx,fontSize:13,lineHeight:1.7}}>{b}</span></div>)}</div></div>:<div style={{padding:24,background:C.bg,borderRadius:8,border:"1px dashed "+C.bd,textAlign:"center",color:C.txM,fontSize:12}}>Click "Generate Brief"</div>}</div>}
function Cal({evts,live}){const [fl,sF]=useState("All");const gF=["🇺🇸","🇪🇺","🇬🇧","🇯🇵","🇨🇭","🇦🇺","🇳🇿","🇨🇦","🇸🇪","🇳🇴"];return <div><div style={{display:"flex",gap:6,marginBottom:10,alignItems:"center"}}><Dot on={live}/>{["All","G10","EM","CB"].map(f=><button key={f} onClick={()=>sF(f)} style={{padding:"5px 14px",fontSize:11,fontWeight:600,borderRadius:4,background:fl===f?C.ac+"25":C.bg,color:fl===f?C.ac:C.txD,border:"1px solid "+(fl===f?C.ac+"50":C.bd)}}>{f}</button>)}</div><Tbl cols={[{k:"date",l:"Date",a:"left"},{k:"time",l:"Time",a:"left"},{k:"ctry",l:"",a:"center",nm:1},{k:"ev",l:"Event",a:"left",nm:1,cl:r=>r.imp==="red"?C.r:C.am},{k:"prev",l:"Prev"},{k:"fcast",l:"Fcst"},{k:"act",l:"Act",cl:r=>r.beat==="beat"?C.g:r.beat==="miss"?C.r:r.beat==="inline"?C.am:(r.act&&r.act!=="—")?C.g:C.txM},{k:"beat",l:"B/M",cl:r=>r.beat==="beat"?C.g:r.beat==="miss"?C.r:C.am,rn:r=>r.beat==="beat"?"▲ Beat":r.beat==="miss"?"▼ Miss":r.beat==="inline"?"● Inline":"—"}]} rows={evts.filter(e=>{if(fl==="G10")return gF.includes(e.ctry);if(fl==="EM")return!gF.includes(e.ctry);if(fl==="CB")return(e.ev||"").toLowerCase().match(/rate|fomc|boj|ecb|rba|rbnz|boc/);return true})}/></div>}
function Sett({open,close,keys,save}){const [lk,sL]=useState(keys);useEffect(()=>sL(keys),[keys,open]);if(!open)return null;return <div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.7)",zIndex:1000,display:"flex",alignItems:"center",justifyContent:"center"}} onClick={close}><div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:10,padding:24,width:520,maxWidth:"92vw"}} onClick={e=>e.stopPropagation()}><div style={{display:"flex",justifyContent:"space-between",marginBottom:16}}><span style={{color:C.tx,fontSize:16,fontWeight:700}}> API Keys</span><button onClick={close} style={{background:"none",border:"none",color:C.txM,fontSize:20}}>✕</button></div>{[{k:"finnhub",l:"Finnhub — VIX, commodities, news, calendar",u:"https://finnhub.io/register"}].map(a=><div key={a.k} style={{marginBottom:14,padding:12,background:C.bg,borderRadius:6,border:"1px solid "+C.bd}}><div style={{display:"flex",justifyContent:"space-between",marginBottom:4}}><span style={{color:C.tx,fontSize:12,fontWeight:600}}>{a.l}</span><a href={a.u} target="_blank" rel="noopener" style={{color:C.ac,fontSize:10,textDecoration:"none"}}>Get key →</a></div><input type="password" value={lk[a.k]||""} onChange={e=>sL({...lk,[a.k]:e.target.value})} style={{width:"100%",padding:"8px 12px",background:C.bgC,border:"1px solid "+C.bd,borderRadius:4,color:C.tx,fontSize:12,fontFamily:Mn,boxSizing:"border-box"}}/></div>)}<button onClick={()=>{save(lk);close()}} style={{width:"100%",padding:"12px",background:"linear-gradient(135deg,"+C.ac+","+C.acD+")",color:"#fff",border:"none",borderRadius:6,fontSize:13,fontWeight:700}}>Save</button></div></div>}
function Clock(){const [clk,setClk]=useState(new Date());useEffect(()=>{const t=setInterval(()=>setClk(new Date()),1000);return()=>clearInterval(t)},[]);const lt=clk.toLocaleString("en-GB",{timeZone:"Europe/London",hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:false});const nt=clk.toLocaleString("en-US",{timeZone:"America/New_York",hour:"2-digit",minute:"2-digit",hour12:false});const tt=clk.toLocaleString("en-US",{timeZone:"Asia/Tokyo",hour:"2-digit",minute:"2-digit",hour12:false});return <span className="hmob" style={{fontSize:10,color:C.txM}}>LON <span style={{color:C.tx,fontFamily:Mn}}>{lt}</span> NY <span style={{color:C.tx,fontFamily:Mn}}>{nt}</span> TKY <span style={{color:C.tx,fontFamily:Mn}}>{tt}</span></span>}
/* ═══ APP ══════════════════════════════════════════════════════════ */
function App(){
const [sec,setSec]=useState("brief");const [ctry,setCtry]=useState("US");const [sO,setSO]=useState(false);
const [keys,setKeys]=useState({finnhub:gk("finnhub")||_K.finnhub});
const [g10D,sG10]=useState([]);const [emD,sEM]=useState([]);const [fxL,sFxL]=useState(false);
const [yld,sYld]=useState({});const [yL,sYL]=useState(false);
const [risk,sRisk]=useState({DXY:{level:null,chg:null},VIX:{level:null,chg:null},Gold:{level:null,chg:null},WTI:{level:null,chg:null},Brent:{level:null,chg:null},"US 10Y":{level:null,chg:null}});
const [rL,sRL]=useState(false);const [cal,sCal]=useState([]);const [cL,sCL]=useState(false);const [news,sNews]=useState([]);const [nL,sNL]=useState(false);
const [cmd,sCmd]=useState({gold:{},wti:{},brent:{}});const [ld,sLd]=useState(false);const [lr,sLR]=useState(null);
const [etfs,setEtfs]=useState({});const [vol,setVol]=useState({});const [bondEtfs,setBondEtfs]=useState({});
const [macro,setMacro]=useState({});const [policyRates,setPolicyRates]=useState({});const [govBonds,setGovBonds]=useState({});
const svK=useCallback(nk=>{setKeys(nk);Object.entries(nk).forEach(([k,v])=>sk(k,v))},[]);
const load=useCallback(async()=>{
sLd(true);
try{
const data=await fData();
setEtfs(data.etfs||{});setVol(data.volatility||{});setBondEtfs(data.bond_etfs||{});
console.log("FULL DATA",data);console.log("POLICY_RATES",data.policy_rates);console.log("MACRO",data.macro);console.log("YIELDS",data.yields);
setMacro(data.macro||{});setPolicyRates(data.policy_rates||{});setGovBonds(data.gov_bonds||{});
const mapFx=defs=>defs.map(d=>{const x=data.fx?.[d.p];return{pair:d.p,spot:x?.price??null,d1:x?.d1_pct??null,w1:x?.w1_pct??null,m1:x?.m1_pct??null,ytd:null}});
sG10(mapFx(G10));sEM(mapFx(EMP));sFxL(true);
const mappedYields={};
if(data.yields){Object.entries(data.yields).forEach(([k,v])=>{if(k!=="US_yf")mappedYields[k]=v})}
sYld(mappedYields);sYL(true);
const dxy=data.fx?.DXY;const vix=data.volatility?.["VIX (S&P 500 Vol)"];const move=data.volatility?.["MOVE (Bond Vol)"];const usY=mappedYields.US||{};
sRisk({DXY:{level:dxy?.price??null,chg:dxy?.d1_pct??null},VIX:{level:vix?.price??null,chg:vix?.change_pct??null},Gold:{level:data.commodities?.gold?.price??null,chg:data.commodities?.gold?.d1_pct??null},WTI:{level:data.commodities?.wti?.price??null,chg:data.commodities?.wti?.d1_pct??null},Brent:{level:data.commodities?.brent?.price??null,chg:data.commodities?.brent?.d1_pct??null},"US 2Y":{level:usY.y2??null,chg:null},"US 10Y":{level:usY.y10??null,chg:null},"DE 10Y":{level:mappedYields.Eurozone?.y10??null,chg:null},"JP 10Y":{level:mappedYields.Japan?.y10??null,chg:null},"2s10s":{level:(usY.y2!=null&&usY.y10!=null)?+((usY.y10-usY.y2)*100).toFixed(1):null,chg:null},MOVE:{level:move?.price??null,chg:move?.change_pct??null}});
sRL(true);
sCmd({gold:{level:data.commodities?.gold?.price??null,chg:data.commodities?.gold?.d1_pct??null},wti:{level:data.commodities?.wti?.price??null,chg:data.commodities?.wti?.d1_pct??null},brent:{level:data.commodities?.brent?.price??null,chg:data.commodities?.brent?.d1_pct??null}});
const parsedNews=(data.news||[]).map(n=>{const dt=typeof n.datetime==="number"?n.datetime:Math.floor(Date.parse(n.datetime)/1000)||0;return{...n,datetime:dt}});sNews(parsedNews);sNL(true);
const parsedCal=(data.calendar||[]).map(e=>({date:e.date||"",time:e.time||"",ctry:e.ctry||"",ev:e.ev||"",prev:e.prev??"—",fcast:e.fcast??"—",act:e.act??"—",imp:e.imp||"orange",beat:e.beat||null}));sCal(parsedCal);sCL(true);
sLR(new Date());
}catch(err){console.error("Failed to load dashboard data",err)}finally{sLd(false)}
},[]);
useEffect(()=>{load()},[load]);
return <div style={{minHeight:"100vh",background:C.bg}}>
<RiskBar d={risk} live={rL||fxL}/>
<div style={{position:"sticky",top:0,zIndex:100,background:C.bgC,borderBottom:"1px solid "+C.bd,padding:"0 16px",display:"flex",alignItems:"center",justifyContent:"space-between",flexWrap:"wrap"}}>
<div style={{display:"flex",alignItems:"center",gap:6,overflowX:"auto"}}><span style={{fontWeight:800,fontSize:14,color:C.ac,padding:"12px 0"}}>FX</span><span style={{fontWeight:800,fontSize:14,color:C.tx}}>RADAR</span><div style={{width:1,height:20,background:C.bd,margin:"0 8px"}}/>{[{id:"brief",l:"📋 Brief"},{id:"sentiment",l:"🧭 Sentiment"},{id:"markets",l:"📊 Markets"},{id:"curves",l:"📈 Curves"},{id:"spreads",l:"↔ Spreads"},{id:"commod",l:"🛢 Commodities"},{id:"scenarios",l:"🎯 Scenarios"},{id:"deep",l:"🌍 Deep Dive"},{id:"cal",l:"📅 Calendar"}].map(s=><button key={s.id} onClick={()=>setSec(s.id)} style={{padding:"12px 10px",fontSize:12,fontWeight:600,background:"transparent",border:"none",color:sec===s.id?C.tx:C.txM,borderBottom:sec===s.id?"2px solid "+C.ac:"2px solid transparent",whiteSpace:"nowrap"}}>{s.l}</button>)}</div>
<div style={{display:"flex",alignItems:"center",gap:8,padding:"8px 0"}}><button onClick={load} disabled={ld} style={{background:C.tl+"18",border:"1px solid "+C.tl+"40",borderRadius:4,color:C.tl,padding:"4px 12px",fontSize:11,fontWeight:600}}>{ld?"...":"↻ Refresh"}</button>{lr&&<span style={{color:C.txM,fontSize:9}}>⏱{lr.toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit",second:"2-digit"})}</span>}<Clock/><button onClick={()=>setSO(true)} style={{background:C.g+"18",border:"1px solid "+C.g+"50",borderRadius:4,color:C.g,padding:"4px 8px",fontSize:11,fontWeight:600}}>✓</button></div>
</div>
<div style={{padding:"16px 20px",maxWidth:1600,margin:"0 auto"}}>
{sec==="brief"&&<Brief g10={g10D} em={emD} yld={yld} risk={risk} cal={cal} cmd={cmd}/>}
{sec==="sentiment"&&<Sentiment risk={risk} g10={g10D} em={emD} cmd={cmd}/>}
{sec==="markets"&&<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}><Markets g10={g10D} em={emD} fxL={fxL} yld={yld} yL={yL} etfs={etfs} vol={vol} bondEtfs={bondEtfs} policyRates={policyRates}/></div>}
{sec==="curves"&&<Curves yld={yld} policyRates={policyRates}/>}
{sec==="spreads"&&<Spreads yld={yld}/>}
{sec==="commod"&&<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}><div style={{color:C.tx,fontSize:15,fontWeight:700,marginBottom:12}}>Commodities</div><Cmdt d={cmd}/></div>}
{sec==="scenarios"&&<Scenarios risk={risk} yld={yld} g10={g10D} em={emD} cmd={cmd}/>}
{sec==="deep"&&<div><div style={{display:"flex",flexWrap:"wrap",gap:4,marginBottom:16}}>{RT.map(c=><button key={c.c} onClick={()=>setCtry(c.c)} style={{padding:"6px 12px",fontSize:11,fontWeight:600,borderRadius:4,background:ctry===c.c?C.ac+"25":C.bgC,color:ctry===c.c?C.ac:C.txD,border:"1px solid "+(ctry===c.c?C.ac+"50":C.bd)}}>{c.f} {c.c}</button>)}</div><Deep ctry={ctry} g10={g10D} em={emD} yld={yld} news={news} nL={nL} risk={risk} macro={macro} policyRates={policyRates} govBonds={govBonds}/></div>}
{sec==="cal"&&<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:16}}><div style={{marginBottom:12}}><span style={{color:C.tx,fontSize:15,fontWeight:700}}>Economic Calendar</span></div><Cal evts={cal} live={cL}/></div>}
</div>
<div style={{textAlign:"center",padding:"12px 0",borderTop:"1px solid "+C.bd+"20"}}><span style={{color:C.txM,fontSize:10}}>FX RADAR v4 · <Dot on={fxL}/>{fxL?"Live":"..."}</span></div>
<Sett open={sO} close={()=>setSO(false)} keys={keys} save={svK}/>
</div>;
}
ReactDOM.createRoot(document.getElementById("root")).render(<App/>);
</script></body></html>