2026-04-06 22:21:36 +01:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" />< meta name = "viewport" content = "width=device-width,initial-scale=1.0" />
2026-04-12 20:27:36 +00:00
< title > FX RADAR TEST v5</ title >
2026-04-06 22:21:36 +01:00
< 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 : 13 px }:: -webkit-scrollbar { width : 5 px ; height : 5 px }:: -webkit-scrollbar-track { background : #0a0e17 }:: -webkit-scrollbar-thumb { background : #334155 ; border-radius : 3 px } button { font-family : inherit ; cursor : pointer } table { border-spacing : 0 }@ keyframes spin { to { transform : rotate ( 360 deg )}}. sp { width : 12 px ; height : 12 px ; border : 2 px solid #334155 ; border-top-color : #6366f1 ; border-radius : 50 % ; animation : spin .8 s linear infinite ; display : inline-block ; vertical-align : middle }@ media ( max-width : 900px ) {. g2 { grid-template-columns : 1 fr !important }. g3 { grid-template-columns : 1 fr !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";
2026-04-06 23:19:31 +00:00
const _d=s=>{try{return atob(s)}catch{return""}};const _K={finnhub:_d("ZDc5ZGg1cHIwMXFxcG1oZ2QwYjBkNzlkaDVwcjAxcXFwbWhnZDBiZw==")};
2026-04-06 22:21:36 +01:00
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}}
2026-04-06 23:19:31 +00:00
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}}
2026-04-06 23:31:58 +00:00
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}}
2026-04-06 22:21:36 +01:00
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}}
2026-04-09 20:15:17 +00:00
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()}
2026-04-06 22:21:36 +01:00
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(",");
2026-04-06 23:19:31 +00:00
2026-04-06 23:46:40 +00:00
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"}];
2026-04-06 22:21:36 +01:00
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"]};
2026-04-06 23:46:40 +00:00
const MD={};
2026-04-12 20:13:22 +00:00
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"]};
2026-04-06 23:46:40 +00:00
2026-04-06 22:21:36 +01:00
/* ═══ 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 > }
2026-04-06 23:19:31 +00:00
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" /> }
2026-04-12 20:35:22 +00:00
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 > }
2026-04-06 22:21:36 +01:00
/* ═══ 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 > }
2026-04-12 20:35:22 +00:00
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 > }
2026-04-06 22:21:36 +01:00
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 > }
2026-04-06 23:46:40 +00:00
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/>}
2026-04-06 22:21:36 +01:00
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 > }
2026-04-09 20:39:42 +00:00
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/>}
2026-04-12 20:27:36 +00:00
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 > }
2026-04-09 20:58:07 +00:00
/* ═══ 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 . 05 – 1 . 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 > ;
}
2026-04-06 22:21:36 +01:00
2026-04-12 20:35:22 +00:00
/* ═══ 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 > }
2026-04-12 21:05:32 +00:00
/* ═══ 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};
}
2026-04-06 22:21:36 +01:00
/* ═══ DEEP DIVE ═══════════════════════════════════════════════════ */
2026-04-12 20:13:22 +00:00
function Deep({ctry,g10,em,yld,news,nL,risk,macro,policyRates,govBonds}){
2026-04-09 20:39:42 +00:00
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;
2026-04-12 20:13:22 +00:00
const pfxList=bondPfx[ctry]||[];const cBonds=Object.entries(govBonds||{}).filter(([k])=>pfxList.some(p=>k.startsWith(p))).map(([k,v])=>({key:k,...v}));
2026-04-06 22:21:36 +01:00
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]);
2026-04-12 20:35:22 +00:00
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;
2026-04-12 21:05:32 +00:00
const [dv,setDv]=useState(null);
2026-04-06 22:29:52 +00:00
2026-04-12 21:05:32 +00:00
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);
2026-04-06 22:29:52 +00:00
};
2026-04-06 22:21:36 +01:00
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}} >
2026-04-12 21:05:32 +00:00
< div style = {{color:C.tx,fontSize:15,fontWeight:700}} > 📋 DESK NOTE — {ctry} ({ccy})</ div >
2026-04-06 22:29:52 +00:00
< button
onClick = {gen}
style = {{
padding: " 8px 18px ",
background: " linear-gradient ( 135deg ,"+ C . tl +",# 38bdf8 )",
color: "# fff ",
border: " none ",
borderRadius:6 ,
fontSize:12 ,
fontWeight:700
}}
>
2026-04-12 21:05:32 +00:00
{dv ? "↻ Refresh View" : "Generate Desk Note"}
2026-04-06 22:29:52 +00:00
</ button >
2026-04-06 22:21:36 +01:00
</ div >
2026-04-06 22:29:52 +00:00
2026-04-12 21:05:32 +00:00
{!dv && (
2026-04-06 22:29:52 +00:00
< div style = {{padding:20,background:C.bg,borderRadius:6,border:"1px dashed "+ C . bd , textAlign: " center ", color:C . txM , fontSize:12 }} >
2026-04-12 21:05:32 +00:00
Click "Generate Desk Note" for a signal-based macro view
2026-04-06 22:21:36 +01:00
</ div >
2026-04-06 22:29:52 +00:00
)}
2026-04-12 21:05:32 +00:00
{dv && (
2026-04-06 22:29:52 +00:00
< >
< div style = {{marginBottom:14}} >
< div style = {{color:C.tl,fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:1,marginBottom:6}} >
Trading Narrative
</ div >
2026-04-12 21:05:32 +00:00
< 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 >
2026-04-06 22:29:52 +00:00
</ 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}} >
2026-04-12 21:05:32 +00:00
{dv.macro_sections.map((sec, i) => (
2026-04-06 22:29:52 +00:00
< 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 >
2026-04-12 21:05:32 +00:00
< 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) => (
2026-04-06 22:29:52 +00:00
< 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 >
2026-04-12 21:05:32 +00:00
< 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 >
2026-04-06 22:29:52 +00:00
</ 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 >
2026-04-12 21:05:32 +00:00
{dv.key_risks.map((x, i) => (
2026-04-06 22:29:52 +00:00
< 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 >
2026-04-12 21:05:32 +00:00
{dv.headline_takeaways.map((x, i) => (
2026-04-06 22:29:52 +00:00
< div key = {i} style = {{color:C.txD,fontSize:12,lineHeight:1.6,marginBottom:4}} > • {x}</ div >
))}
</ div >
</ div >
< />
)}
2026-04-06 22:21:36 +01:00
</ div >
2026-04-06 22:29:52 +00:00
< 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 >
2026-04-12 20:35:22 +00:00
< 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 >
2026-04-06 22:29:52 +00:00
{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 >
2026-04-12 20:13:22 +00:00
{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 > }
2026-04-06 22:29:52 +00:00
< 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}} >
2026-04-09 20:39:42 +00:00
< 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 >
2026-04-06 22:29:52 +00:00
< 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 >
2026-04-06 22:21:36 +01:00
</ div >
</ div > ;
}
/* ═══ BRIEF ═══════════════════════════════════════════════════════ */
2026-04-12 21:05:32 +00:00
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 > }
2026-04-09 20:58:07 +00:00
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 > }
2026-04-06 23:19:31 +00:00
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 > }
2026-04-06 22:21:36 +01:00
2026-04-06 22:29:52 +00:00
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 > }
2026-04-06 22:21:36 +01:00
/* ═══ APP ══════════════════════════════════════════════════════════ */
function App(){
2026-04-06 22:29:52 +00:00
const [sec,setSec]=useState("brief");const [ctry,setCtry]=useState("US");const [sO,setSO]=useState(false);
2026-04-06 23:19:31 +00:00
const [keys,setKeys]=useState({finnhub:gk("finnhub")||_K.finnhub});
2026-04-06 22:21:36 +01:00
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}});
2026-04-06 23:46:40 +00:00
const [rL,sRL]=useState(false);const [cal,sCal]=useState([]);const [cL,sCL]=useState(false);const [news,sNews]=useState([]);const [nL,sNL]=useState(false);
2026-04-06 22:21:36 +01:00
const [cmd,sCmd]=useState({gold:{},wti:{},brent:{}});const [ld,sLd]=useState(false);const [lr,sLR]=useState(null);
2026-04-09 20:39:42 +00:00
const [etfs,setEtfs]=useState({});const [vol,setVol]=useState({});const [bondEtfs,setBondEtfs]=useState({});
2026-04-12 20:13:22 +00:00
const [macro,setMacro]=useState({});const [policyRates,setPolicyRates]=useState({});const [govBonds,setGovBonds]=useState({});
2026-04-06 22:21:36 +01:00
const svK=useCallback(nk=>{setKeys(nk);Object.entries(nk).forEach(([k,v])=>sk(k,v))},[]);
const load=useCallback(async()=>{
sLd(true);
2026-04-09 20:15:17 +00:00
try{
const data=await fData();
2026-04-09 20:39:42 +00:00
setEtfs(data.etfs||{});setVol(data.volatility||{});setBondEtfs(data.bond_etfs||{});
2026-04-12 20:48:09 +00:00
console.log("FULL DATA",data);console.log("POLICY_RATES",data.policy_rates);console.log("MACRO",data.macro);console.log("YIELDS",data.yields);
2026-04-12 20:13:22 +00:00
setMacro(data.macro||{});setPolicyRates(data.policy_rates||{});setGovBonds(data.gov_bonds||{});
2026-04-09 20:15:17 +00:00
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||{};
2026-04-12 20:35:22 +00:00
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}});
2026-04-09 20:15:17 +00:00
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);
2026-04-09 20:58:07 +00:00
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);
2026-04-09 20:15:17 +00:00
sLR(new Date());
}catch(err){console.error("Failed to load dashboard data",err)}finally{sLd(false)}
},[]);
2026-04-06 22:21:36 +01:00
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 "}} >
2026-04-12 21:05:32 +00:00
< 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 >
2026-04-06 22:29:52 +00:00
< 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 >
2026-04-06 22:21:36 +01:00
</ 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}/ > }
2026-04-09 20:58:07 +00:00
{sec==="sentiment"&& < Sentiment risk = {risk} g10 = {g10D} em = {emD} cmd = {cmd}/ > }
2026-04-12 20:27:36 +00:00
{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 > }
2026-04-12 20:35:22 +00:00
{sec==="curves"&& < Curves yld = {yld} policyRates = {policyRates}/ > }
{sec==="spreads"&& < Spreads yld = {yld}/ > }
2026-04-06 22:21:36 +01:00
{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 > }
2026-04-09 20:58:07 +00:00
{sec==="scenarios"&& < Scenarios risk = {risk} yld = {yld} g10 = {g10D} em = {emD} cmd = {cmd}/ > }
2026-04-12 20:13:22 +00:00
{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 > }
2026-04-06 22:21:36 +01:00
{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 >