Files
FXandRatesDashboard/index.html
T
Tvishi Agarwal b3d70960d1 pleasepleasework
2026-04-12 20:48:09 +00:00

582 lines
66 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>;
}
function CorrMx(){return <div><div style={{marginBottom:12}}><span style={{color:C.ac,fontSize:13,fontWeight:700}}>30-Day Correlation Matrix</span></div><div style={{padding:40,textAlign:"center",color:C.txM,fontSize:12,border:"1px dashed "+C.bd,borderRadius:6}}>—<br/><span style={{fontSize:11,marginTop:8,display:"inline-block"}}>Correlation data not available — requires a live feed</span></div></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>}
/* ═══ 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 [ai,setAi]=useState(null);
const [aiLd,setAiLd]=useState(false);
const [aiErr,setAiErr]=useState("");
const gen = async () => {
setAiLd(true);
setAiErr("");
try {
const body = {
country: ctry,
currency: ccy,
fx: fp ? {
pair: fp.pair,
spot: fp.spot,
d1: fp.d1,
w1: fp.w1,
m1: fp.m1,
ytd: fp.ytd
} : null,
yields: y,
centralBank: tpl ? {
country: tpl.c,
policyRate: cbRate
} : null,
macro: m,
riskBar: risk,
govBonds: cBonds,
headlines: dN.slice(0, 8).map(n => ({
headline: n.headline,
source: n.source,
datetime: n.datetime
}))
};
const res = await fetch("/.netlify/functions/deep-dive-summary", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Failed to generate AI summary");
}
setAi(data);
} catch (err) {
setAiErr(err.message || "AI request failed");
} finally {
setAiLd(false);
}
};
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}}>🧠 AI MACRO SUMMARY {ctry} ({ccy})</div>
<button
onClick={gen}
disabled={aiLd}
style={{
padding:"8px 18px",
background:"linear-gradient(135deg,"+C.tl+",#38bdf8)",
color:"#fff",
border:"none",
borderRadius:6,
fontSize:12,
fontWeight:700
}}
>
{aiLd ? "Generating..." : ai ? "↻ Regenerate View" : "Generate View"}
</button>
</div>
{aiErr && (
<div style={{padding:12,background:"#2a1111",border:"1px solid #7f1d1d",borderRadius:6,color:"#fca5a5",marginBottom:12}}>
{aiErr}
</div>
)}
{!ai && !aiLd && !aiErr && (
<div style={{padding:20,background:C.bg,borderRadius:6,border:"1px dashed "+C.bd,textAlign:"center",color:C.txM,fontSize:12}}>
Click "Generate View" for a live AI deep dive
</div>
)}
{ai && (
<>
<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",color:C.tx,fontSize:13,lineHeight:1.75}}>
{ai.narrative}
</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}}>
{ai.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}}>{ai.fx_view.direction}</div>
<div style={{color:C.txD,fontSize:12,lineHeight:1.6,marginBottom:8}}>{ai.fx_view.rationale}</div>
{ai.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}}>{ai.rates_view.bias}</div>
<div style={{color:C.txD,fontSize:12,lineHeight:1.6}}>{ai.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>
{ai.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>
{ai.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.join(" ")}
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}}>AI Morning Brief</div><div style={{color:C.txM,fontSize:11}}>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={{color:C.tx,fontSize:13,fontStyle:"italic",lineHeight:1.7}}>{br}</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:"corr",l:"🔗 Correlations"},{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==="corr"&&<div style={{background:C.bgC,border:"1px solid "+C.bd,borderRadius:8,padding:20}}><CorrMx/></div>}
{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>