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 );
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 }} > 2 s10s < /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 }} > 10 s30s < /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 }} > 10 Y - 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 }} > 10 Y 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 >