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-06 23:31:58 +00:00
< title > FX RADAR TEST v4</ 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 }}
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 22:21:36 +01:00
const RT = [{ c : "US" , f : "🇺🇸" , cb : 5.50 , ccy : "USD" },{ c : "Eurozone" , f : "🇪🇺" , cb : 4.50 , ccy : "EUR" },{ c : "UK" , f : "🇬🇧" , cb : 5.25 , ccy : "GBP" },{ c : "Japan" , f : "🇯🇵" , cb : 0.10 , ccy : "JPY" },{ c : "Switzerland" , f : "🇨🇭" , cb : 1.75 , ccy : "CHF" },{ c : "Australia" , f : "🇦🇺" , cb : 4.35 , ccy : "AUD" },{ c : "New Zealand" , f : "🇳🇿" , cb : 5.50 , ccy : "NZD" },{ c : "Canada" , f : "🇨🇦" , cb : 5.00 , ccy : "CAD" },{ c : "Sweden" , f : "🇸🇪" , cb : 4.00 , ccy : "SEK" },{ c : "Norway" , f : "🇳🇴" , cb : 4.50 , ccy : "NOK" },{ c : "Brazil" , f : "🇧🇷" , cb : 10.75 , ccy : "BRL" },{ c : "Mexico" , f : "🇲🇽" , cb : 11.00 , ccy : "MXN" },{ c : "South Africa" , f : "🇿🇦" , cb : 8.25 , ccy : "ZAR" },{ c : "India" , f : "🇮🇳" , cb : 6.50 , ccy : "INR" },{ c : "South Korea" , f : "🇰🇷" , cb : 3.50 , ccy : "KRW" },{ c : "Turkey" , f : "🇹🇷" , cb : 50.00 , ccy : "TRY" },{ c : "Poland" , f : "🇵🇱" , cb : 5.75 , ccy : "PLN" }];
const cOf = {}; RT . forEach ( r => cOf [ r . c ] = r . ccy );
const tvF = { US : "TVC:DXY" , Eurozone : "FX:EURUSD" , UK : "FX:GBPUSD" , Japan : "FX:USDJPY" , Switzerland : "FX:USDCHF" , Australia : "FX:AUDUSD" , "New Zealand" : "FX:NZDUSD" , Canada : "FX:USDCAD" , Sweden : "FX:USDSEK" , Norway : "FX:USDNOK" , Brazil : "FX:USDBRL" , Mexico : "FX:USDMXN" , "South Africa" : "FX:USDZAR" , India : "FX:USDINR" , "South Korea" : "FX:USDKRW" , Turkey : "FX:USDTRY" , Poland : "FX:USDPLN" };
const tvY = { US : "TVC:US10Y" , Eurozone : "TVC:DE10Y" , UK : "TVC:GB10Y" , Japan : "TVC:JP10Y" , Canada : "TVC:CA10Y" , Australia : "TVC:AU10Y" };
const pOf = { US : "DXY" , Eurozone : "EURUSD" , UK : "GBPUSD" , Japan : "USDJPY" , Switzerland : "USDCHF" , Australia : "AUDUSD" , "New Zealand" : "NZDUSD" , Canada : "USDCAD" , Sweden : "USDSEK" , Norway : "USDNOK" , Brazil : "USDBRL" , Mexico : "USDMXN" , "South Africa" : "USDZAR" , India : "USDINR" , "South Korea" : "USDKRW" , Turkey : "USDTRY" , Poland : "USDPLN" };
const nKw = { US : [ "fed" , "fomc" , "treasury" , "dollar" , "powell" , "nonfarm" ], Eurozone : [ "ecb" , "lagarde" , "eurozone" , "bund" ], UK : [ "boe" , "bailey" , "gilt" , "sterling" , "uk gdp" , "uk cpi" ], Japan : [ "boj" , "ueda" , "yen" , "jgb" , "japan" ], Switzerland : [ "snb" , "swiss" , "franc" ], Australia : [ "rba" , "aussie" , "australia" ], Canada : [ "boc" , "canada" , "loonie" ], "New Zealand" : [ "rbnz" , "kiwi" , "new zealand" ], Sweden : [ "riksbank" , "sweden" ], Norway : [ "norges" , "norway" ], Brazil : [ "bcb" , "brazil" , "real" ], Mexico : [ "banxico" , "mexico" , "peso" ], "South Africa" : [ "sarb" , "south africa" , "rand" ], India : [ "rbi" , "india" , "rupee" ], "South Korea" : [ "bok" , "korea" , "won" ], Turkey : [ "cbrt" , "turkey" , "lira" ], Poland : [ "nbp" , "poland" , "zloty" ]};
// ═══ FULL MACRO DATABASE ═════════════════════════════════════════
const MD = {
2026-04-06 22:29:52 +00:00
US : { cpi : "3.2%" , cpiP : "3.1%" , cpiT : "re-accelerating" , core : "3.8%" , gdp : "2.5%" , gdpT : "resilient" , unemp : "3.7%" , unempT : "near cycle lows" , wages : "4.1% YoY" , pmi : "52.6" , tb : "-$68.5B" , stance : "Hawkish hold" , next : "2026-05-01" , last : "Hold 5.50%" , pricing : "2 cuts in 12m" },
Eurozone : { cpi : "2.6%" , cpiP : "2.8%" , cpiT : "falling toward target" , core : "3.1%" , gdp : "0.3%" , gdpT : "anaemic" , unemp : "6.4%" , unempT : "stable" , wages : "3.5% YoY" , pmi : "49.2" , tb : "+€28B" , stance : "Easing bias" , next : "2026-04-10" , last : "Hold 4.50%" , pricing : "3 cuts in 12m" },
UK : { cpi : "3.4%" , cpiP : "4.0%" , cpiT : "falling but sticky services" , core : "4.5%" , gdp : "0.2%" , gdpT : "tepid" , unemp : "4.2%" , unempT : "rising" , wages : "5.6% YoY" , pmi : "53.8" , tb : "-£4.8B" , stance : "Cautious hold" , next : "2026-05-09" , last : "Hold 5.25%" , pricing : "2 cuts in 12m" },
Japan : { cpi : "2.8%" , cpiP : "2.5%" , cpiT : "rising and broadening" , core : "2.5%" , gdp : "1.2%" , gdpT : "recovering" , unemp : "2.5%" , unempT : "tight" , wages : "2.1% (Shunto: 5.3%)" , pmi : "51.4" , tb : "-¥580B" , stance : "Cautious normalisation" , next : "2026-04-26" , last : "Hold 0.10%" , pricing : "1 hike in 12m" },
Switzerland : { cpi : "1.4%" , cpiP : "1.2%" , cpiT : "low" , core : "1.2%" , gdp : "0.6%" , gdpT : "modest" , unemp : "2.3%" , unempT : "stable" , wages : "1.8% YoY" , pmi : "48.5" , tb : "+CHF5.2B" , stance : "Easing" , next : "2026-06-19" , last : "Cut to 1.75%" , pricing : "1-2 more cuts" },
Australia : { cpi : "3.6%" , cpiP : "3.4%" , cpiT : "sticky" , core : "3.9%" , gdp : "1.5%" , gdpT : "slowing" , unemp : "3.9%" , unempT : "stable" , wages : "4.2% YoY" , pmi : "50.1" , tb : "+A$8.5B" , stance : "On hold" , next : "2026-05-20" , last : "Hold 4.35%" , pricing : "2 cuts in 12m" },
"New Zealand" : { cpi : "4.7%" , cpiP : "4.7%" , cpiT : "sticky" , core : "4.3%" , gdp : "-0.3%" , gdpT : "recession" , unemp : "4.3%" , unempT : "rising" , wages : "3.8% YoY" , pmi : "47.1" , tb : "-NZ$1.2B" , stance : "Easing" , next : "2026-05-28" , last : "Cut to 5.50%" , pricing : "3-4 cuts" },
Canada : { cpi : "2.9%" , cpiP : "2.8%" , cpiT : "near target" , core : "3.2%" , gdp : "1.0%" , gdpT : "slowing" , unemp : "5.8%" , unempT : "rising" , wages : "3.9% YoY" , pmi : "49.8" , tb : "-C$1.5B" , stance : "Easing" , next : "2026-04-16" , last : "Cut to 5.00%" , pricing : "2-3 more cuts" },
Sweden : { cpi : "2.2%" , cpiP : "2.5%" , cpiT : "at target" , core : "2.5%" , gdp : "0.1%" , gdpT : "stagnant" , unemp : "8.2%" , unempT : "high" , wages : "3.5% YoY" , pmi : "48.2" , tb : "+SEK12B" , stance : "Easing" , next : "2026-05-07" , last : "Cut to 4.00%" , pricing : "2-3 more cuts" },
Norway : { cpi : "3.6%" , cpiP : "3.9%" , cpiT : "sticky" , core : "4.1%" , gdp : "0.8%" , gdpT : "stable" , unemp : "3.7%" , unempT : "low" , wages : "5.2% YoY" , pmi : "51.8" , tb : "+NOK80B" , stance : "Hawkish hold" , next : "2026-05-08" , last : "Hold 4.50%" , pricing : "1-2 cuts" },
Brazil : { cpi : "4.5%" , cpiP : "4.2%" , cpiT : "rising" , core : "4.0%" , gdp : "2.1%" , gdpT : "strong" , unemp : "7.8%" , unempT : "falling" , wages : "N/A" , pmi : "52.1" , tb : "+$7.2B" , stance : "Tightening" , next : "2026-05-07" , last : "Hike to 10.75%" , pricing : "Terminal ~12%" },
Mexico : { cpi : "4.2%" , cpiP : "4.4%" , cpiT : "slowly falling" , core : "4.6%" , gdp : "2.5%" , gdpT : "resilient" , unemp : "2.8%" , unempT : "tight" , wages : "N/A" , pmi : "51.5" , tb : "-$5.8B" , stance : "Cautious hold" , next : "2026-05-15" , last : "Hold 11.00%" , pricing : "2-3 cuts" },
"South Africa" : { cpi : "5.3%" , cpiP : "5.1%" , cpiT : "elevated" , core : "4.6%" , gdp : "0.7%" , gdpT : "weak" , unemp : "32.1%" , unempT : "structural" , wages : "N/A" , pmi : "49.5" , tb : "-ZAR15B" , stance : "Hold" , next : "2026-05-29" , last : "Hold 8.25%" , pricing : "1-2 cuts" },
India : { cpi : "5.1%" , cpiP : "5.3%" , cpiT : "moderating" , core : "4.2%" , gdp : "6.5%" , gdpT : "strong" , unemp : "7.1%" , unempT : "stable" , wages : "N/A" , pmi : "58.3" , tb : "-$20B" , stance : "Easing bias" , next : "2026-04-09" , last : "Cut to 6.50%" , pricing : "2 more cuts" },
"South Korea" : { cpi : "2.1%" , cpiP : "2.3%" , cpiT : "at target" , core : "2.0%" , gdp : "2.0%" , gdpT : "recovering" , unemp : "2.7%" , unempT : "stable" , wages : "N/A" , pmi : "50.8" , tb : "+$4.5B" , stance : "Easing" , next : "2026-05-29" , last : "Cut to 3.50%" , pricing : "1-2 cuts" },
Turkey : { cpi : "65%" , cpiP : "67%" , cpiT : "falling from peak" , core : "70%" , gdp : "3.2%" , gdpT : "slowing" , unemp : "9.4%" , unempT : "stable" , wages : "N/A" , pmi : "49.1" , tb : "-$7.5B" , stance : "Hawkish hold" , next : "2026-04-17" , last : "Hold 50.00%" , pricing : "Cuts H2 2026" },
Poland : { cpi : "3.9%" , cpiP : "3.7%" , cpiT : "rising" , core : "4.1%" , gdp : "2.8%" , gdpT : "solid" , unemp : "5.1%" , unempT : "stable" , wages : "12% YoY" , pmi : "50.2" , tb : "-PLN5B" , stance : "On hold" , next : "2026-05-07" , last : "Hold 5.75%" , pricing : "Cuts unlikely H1" },
2026-04-06 22:21:36 +01:00
};
const DCAL = [{ date : "2026-04-08" , time : "12:30" , ctry : "🇺🇸" , ev : "FOMC Minutes" , prev : "—" , fcast : "—" , act : "—" , imp : "red" },{ date : "2026-04-09" , time : "12:30" , ctry : "🇺🇸" , ev : "US CPI YoY" , prev : "3.2%" , fcast : "3.1%" , act : "—" , imp : "red" },{ date : "2026-04-10" , time : "11:00" , ctry : "🇪🇺" , ev : "ECB Rate Decision" , prev : "4.50%" , fcast : "4.25%" , act : "—" , imp : "red" },{ date : "2026-04-10" , time : "07:00" , ctry : "🇬🇧" , ev : "UK GDP MoM" , prev : "-0.1%" , fcast : "0.2%" , act : "—" , imp : "red" }];
const CP = [ "EURUSD" , "GBPUSD" , "USDJPY" , "AUDUSD" , "USDCAD" , "NZDUSD" , "Gold" , "US10Y" ]; const CM = [[ 1 ,. 82 , - . 31 ,. 65 , - . 72 ,. 58 ,. 42 , - . 35 ],[. 82 , 1 , - . 22 ,. 55 , - . 61 ,. 48 ,. 35 , - . 28 ],[ - . 31 , - . 22 , 1 , - . 45 ,. 38 , - . 42 , - . 18 ,. 72 ],[. 65 ,. 55 , - . 45 , 1 , - . 58 ,. 85 ,. 52 , - . 42 ],[ - . 72 , - . 61 ,. 38 , - . 58 , 1 , - . 52 , - . 38 ,. 32 ],[. 58 ,. 48 , - . 42 ,. 85 , - . 52 , 1 ,. 45 , - . 38 ],[. 42 ,. 35 , - . 18 ,. 52 , - . 38 ,. 45 , 1 , - . 55 ],[ - . 35 , - . 28 ,. 72 , - . 42 ,. 32 , - . 38 , - . 55 , 1 ]];
/* ═══ 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-06 22:21:36 +01: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 ; 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}}>{v.level!=null?Number(v.level).toLocaleString(undefined,{maximumFractionDigits:2}):"—"}</span>{v.chg!=null&&<span style={{color:cc(v.chg),fontFamily:Mn,fontSize:10}}>{k.includes("10Y")?fb(v.chg):fc(v.chg,k==="VIX"?"":"%")}</span>}</span>)}</div>}
/* ═══ PAGES ═══════════════════════════════════════════════════════ */
function FxTbl ({ data , live }){ return < div >< div style = {{ display : "flex" , alignItems : "center" , gap : 8 , marginBottom : 8 }} >< Dot on = { live } />< span style = {{ color : C . txM , fontSize : 10 }} > { live ? "Live via Frankfurter" : "Loading..." } < /span></div><Tbl cols={[{k:"pair",l:"Pair",a:"left",nm:1,cl:()=>C.b},{k:"spot",l:"Spot",rn:r=>r.spot!=null?Number(r.spot).toFixed(r.spot>100?2:4):"—"},{k:"d1",l:"1D %",rn:r=>fc(r.d1),cl:r=>cc(r.d1)},{k:"w1",l:"1W %",rn:r=>fc(r.w1),cl:r=>cc(r.w1)},{k:"m1",l:"1M %",rn:r=>fc(r.m1),cl:r=>cc(r.m1)},{k:"ytd",l:"YTD %",rn:r=>fc(r.ytd),cl:r=>cc(r.ytd)}]} rows={data}/></div>}
function YldTbl ({ yld , live }){ return < div >< div style = {{ display : "flex" , alignItems : "center" , gap : 8 , marginBottom : 8 }} >< Dot on = { live } />< span style = {{ color : C . txM , fontSize : 10 }} > { live ? "US via FRED · Others: CB rate only" : "No yield data" } < /span></div><Tbl cols={[{k:"c",l:"Country",a:"left",nm:1,rn:r=>r.f+" "+r.c},{k:"cb",l:"CB Rate",rn:r=>r.cb.toFixed(2)+"%"},{k:"y2",l:"2Y",rn:r=>r.y2!=null?r.y2.toFixed(2):"—",cl:r=>r.y2?C.tx:C.txM},{k:"y10",l:"10Y",rn:r=>r.y10!=null?r.y10.toFixed(2):"—",cl:r=>r.y10?C.tx:C.txM},{k:"s",l:"2s10s",rn:r=>r.y2&&r.y10?fb((r.y10-r.y2)*100):"—",cl:r=>r.y2&&r.y10?cc(r.y10-r.y2):C.txM}]} rows={RT.map(r=>({...r,...(yld[r.c]||{})}))} sm/></div>}
function Cmdt ({ d }){ const items = [{ k : "gold" , l : "Gold XAU" , s : "TVC:GOLD" },{ k : "wti" , l : "WTI Crude" , s : "TVC:USOIL" },{ k : "brent" , l : "Brent" , s : "TVC:UKOIL" }]; return < div >< div className = "g3" style = {{ display : "grid" , gridTemplateColumns : "repeat(3,1fr)" , gap : 12 , marginBottom : 16 }} > { items . map ( it =>{ const v = d [ it . k ]; return < div key = { it . k } style = {{ background : C . bg , border : "1px solid " + C . bd , borderRadius : 6 , padding : 14 }} >< div style = {{ color : C . txM , fontSize : 10 , fontWeight : 600 , textTransform : "uppercase" , letterSpacing : 1 , marginBottom : 4 }} > { it . l } < /div><div style={{color:C.tx,fontSize:24,fontWeight:700,fontFamily:Mn}}>{v?.level!=null?"$"+Number(v.level).toLocaleString(undefined,{maximumFractionDigits:2}):"—"}</div>{v?.chg!=null&&<div style={{color:cc(v.chg),fontSize:12,fontFamily:Mn,marginTop:2}}>{fc(v.chg)}</div>}</div>})}</div><div className="g3" style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:12}}>{items.map(it=><div key={it.k}><TV sym={it.s} h={220}/></div>)}</div></div>}
function XCry ({ yld }){ const g = RT . filter ( r =>[ "US" , "Eurozone" , "UK" , "Japan" , "Switzerland" , "Australia" , "New Zealand" , "Canada" , "Sweden" , "Norway" ]. includes ( r . c )); const gY = c => yld [ c ] ? . y2 ?? null ; const pairs = []; for ( let i = 0 ; i < g . length ; i ++ ) for ( let j = i + 1 ; j < g . length ; j ++ ){ const a = g [ i ], b = g [ j ], ya = gY ( a . c ) ?? a . cb , yb = gY ( b . c ) ?? b . cb , 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 })} 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 ?? 5.5 ; 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 : "cb" , l : "CB Rate" , rn : r => r . cb . 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 = { RT . filter ( r => r . c !== "US" ). map ( r =>({... r , carry :+ (( yld [ r . c ] ? . y2 ?? r . cb ) - uY ). toFixed ( 2 )})). sort (( a , b )=> b . carry - a . carry ). map (( r , i )=>({... r , rk : i + 1 }))} sm /> }
function CarryP ({ yld }){ const [ t , sT ] = useState ( "Cross G10" ); return < div >< Tabs tabs = {[ "Cross G10" , "vs USD" ]} act = { t } set = { sT } /> { t === "Cross G10" ?< XCry yld = { yld } />:< UCry yld = { yld } /> } < /div>}
function Markets ({ g10 , em , fxL , yld , yL }){ const [ t , sT ] = useState ( "G10 FX" ); return < div >< Tabs tabs = {[ "G10 FX" , "EM FX" , "Rates" , "Carry" ]} 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 } /> }{ t === "Carry" &&< CarryP yld = { yld } /> } < /div>}
function CorrMx (){ const cC = v => v >= . 7 ? "rgba(34,197,94," + v * . 7 + ")" : v >= . 3 ? "rgba(34,197,94," + v * . 4 + ")" : v >=- . 3 ? "rgba(148,163,184,0.1)" : v >=- . 7 ? "rgba(239,68,68," + Math . abs ( v ) * . 4 + ")" : "rgba(239,68,68," + Math . abs ( v ) * . 7 + ")" ; return < div >< div style = {{ marginBottom : 12 }} >< span style = {{ color : C . ac , fontSize : 13 , fontWeight : 700 }} > 30 - Day Correlation Matrix < /span></div><div style={{overflowX:"auto"}}><div style={{display:"inline-block"}}><div style={{display:"flex"}}><div style={{width:70}}/>{CP.map(p=><div key={p} style={{width:40,textAlign:"center",fontSize:8,color:C.txM,fontWeight:600,transform:"rotate(-45deg)",height:40,display:"flex",alignItems:"flex-end",justifyContent:"center",paddingBottom:4}}>{p}</div>)}</div>{CP.map((p,i)=><div key={p} style={{display:"flex"}}><div style={{width:70,fontSize:10,color:C.txD,fontWeight:600,textAlign:"right",paddingRight:8,display:"flex",alignItems:"center",justifyContent:"flex-end"}}>{p}</div>{CM[i].map((v,j)=><div key={j} title={v.toFixed(2)} style={{width:40,height:40,display:"flex",alignItems:"center",justifyContent:"center",background:i===j?"rgba(99,102,241,0.3)":cC(v),border:"1px solid "+C.bd+"40",fontSize:9,fontFamily:Mn,fontWeight:600,color:Math.abs(v)>.5?C.tx:C.txM}}>{i===j?"1":v.toFixed(1)}</div>)}</div>)}</div></div></div>}
/* ═══ DEEP DIVE ═══════════════════════════════════════════════════ */
2026-04-06 22:29:52 +00:00
function Deep ({ ctry , g10 , em , yld , news , nL , risk }){
2026-04-06 22:21:36 +01: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 = MD [ ctry ] || {};
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-06 22:29:52 +00:00
const [ ai , setAi ] = useState ( null );
const [ aiLd , setAiLd ] = useState ( false );
const [ aiErr , setAiErr ] = useState ( "" );
const gen = async () => {
setAiLd ( true );
setAiErr ( "" );
try {
const body = {
country : ctry ,
currency : ccy ,
fx : fp ? {
pair : fp . pair ,
spot : fp . spot ,
d1 : fp . d1 ,
w1 : fp . w1 ,
m1 : fp . m1 ,
ytd : fp . ytd
} : null ,
yields : y ,
centralBank : tpl ? {
country : tpl . c ,
policyRate : tpl . cb
} : null ,
macro : m ,
riskBar : risk ,
headlines : dN . slice ( 0 , 8 ). map ( n => ({
headline : n . headline ,
source : n . source ,
datetime : n . datetime
}))
};
const res = await fetch ( "/.netlify/functions/deep-dive-summary" , {
method : "POST" ,
headers : {
"Content-Type" : "application/json"
},
body : JSON . stringify ( body )
});
const data = await res . json ();
if ( ! res . ok ) {
throw new Error ( data . error || "Failed to generate AI summary" );
}
setAi ( data );
} catch ( err ) {
setAiErr ( err . message || "AI request failed" );
} finally {
setAiLd ( false );
}
};
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-06 22:29:52 +00:00
< div style = {{ color : C . tx , fontSize : 15 , fontWeight : 700 }} > 🧠 AI MACRO SUMMARY — { ctry } ({ ccy }) < /div>
< button
onClick = { gen }
disabled = { aiLd }
style = {{
padding : "8px 18px" ,
background : "linear-gradient(135deg," + C . tl + ",#38bdf8)" ,
color : "#fff" ,
border : "none" ,
borderRadius : 6 ,
fontSize : 12 ,
fontWeight : 700
}}
>
{ aiLd ? "Generating..." : ai ? "↻ Regenerate View" : "Generate View" }
< /button>
2026-04-06 22:21:36 +01:00
< /div>
2026-04-06 22:29:52 +00:00
{ aiErr && (
< div style = {{ padding : 12 , background : "#2a1111" , border : "1px solid #7f1d1d" , borderRadius : 6 , color : "#fca5a5" , marginBottom : 12 }} >
{ aiErr }
2026-04-06 22:21:36 +01:00
< /div>
2026-04-06 22:29:52 +00:00
)}
{ ! ai && ! aiLd && ! aiErr && (
< div style = {{ padding : 20 , background : C . bg , borderRadius : 6 , border : "1px dashed " + C . bd , textAlign : "center" , color : C . txM , fontSize : 12 }} >
Click "Generate View" for a live AI deep dive
2026-04-06 22:21:36 +01:00
< /div>
2026-04-06 22:29:52 +00:00
)}
{ ai && (
<>
< div style = {{ marginBottom : 14 }} >
< div style = {{ color : C . tl , fontSize : 11 , fontWeight : 700 , textTransform : "uppercase" , letterSpacing : 1 , marginBottom : 6 }} >
Trading Narrative
< /div>
< div style = {{ padding : 14 , background : C . bg , borderRadius : 6 , border : "1px solid " + C . tl + "25" , color : C . tx , fontSize : 13 , lineHeight : 1.75 }} >
{ ai . narrative }
< /div>
< /div>
< div style = {{ color : C . tl , fontSize : 11 , fontWeight : 700 , textTransform : "uppercase" , letterSpacing : 1 , marginBottom : 8 }} >
Macroeconomic Data & Trends
< /div>
< div style = {{ display : "grid" , gridTemplateColumns : "1fr" , gap : 10 , marginBottom : 14 }} >
{ ai . macro_sections . map (( sec , i ) => (
< div key = { i } style = {{ background : C . bg , padding : 14 , borderRadius : 6 , border : "1px solid " + C . bd }} >
< div style = {{ color : C . b , fontSize : 11 , fontWeight : 700 , textTransform : "uppercase" , letterSpacing : 1 , marginBottom : 8 }} >
{ sec . title }
< /div>
< div style = {{ display : "grid" , gap : 6 }} >
{ sec . bullets . map (( b , j ) => (
< div key = { j } style = {{ color : C . txD , fontSize : 12 , lineHeight : 1.6 }} >
• { b }
< /div>
))}
< /div>
< /div>
))}
< /div>
< div className = "g2" style = {{ display : "grid" , gridTemplateColumns : "1fr 1fr" , gap : 12 }} >
< div style = {{ background : C . bg , padding : 14 , borderRadius : 6 , border : "1px solid " + C . bd }} >
< div style = {{ color : C . g , fontSize : 11 , fontWeight : 700 , textTransform : "uppercase" , letterSpacing : 1 , marginBottom : 8 }} >
FX View
< /div>
< div style = {{ color : C . tx , fontWeight : 700 , marginBottom : 6 }} > { ai . fx_view . direction } < /div>
< div style = {{ color : C . txD , fontSize : 12 , lineHeight : 1.6 , marginBottom : 8 }} > { ai . fx_view . rationale } < /div>
{ ai . fx_view . levels_to_watch . map (( x , i ) => (
< div key = { i } style = {{ color : C . am , fontSize : 11 , marginBottom : 4 }} > • { x } < /div>
))}
< /div>
< div style = {{ background : C . bg , padding : 14 , borderRadius : 6 , border : "1px solid " + C . bd }} >
< div style = {{ color : C . r , fontSize : 11 , fontWeight : 700 , textTransform : "uppercase" , letterSpacing : 1 , marginBottom : 8 }} >
Rates View
< /div>
< div style = {{ color : C . tx , fontWeight : 700 , marginBottom : 6 }} > { ai . rates_view . bias } < /div>
< div style = {{ color : C . txD , fontSize : 12 , lineHeight : 1.6 }} > { ai . rates_view . rationale } < /div>
< /div>
< /div>
< div className = "g2" style = {{ display : "grid" , gridTemplateColumns : "1fr 1fr" , gap : 12 , marginTop : 12 }} >
< div style = {{ background : C . bg , padding : 14 , borderRadius : 6 , border : "1px solid " + C . bd }} >
< div style = {{ color : C . am , fontSize : 11 , fontWeight : 700 , textTransform : "uppercase" , letterSpacing : 1 , marginBottom : 8 }} >
Key Risks
< /div>
{ ai . key_risks . map (( x , i ) => (
< div key = { i } style = {{ color : C . txD , fontSize : 12 , lineHeight : 1.6 , marginBottom : 4 }} > • { x } < /div>
))}
< /div>
< div style = {{ background : C . bg , padding : 14 , borderRadius : 6 , border : "1px solid " + C . bd }} >
< div style = {{ color : C . tl , fontSize : 11 , fontWeight : 700 , textTransform : "uppercase" , letterSpacing : 1 , marginBottom : 8 }} >
Headline Takeaways
< /div>
{ ai . headline_takeaways . map (( x , i ) => (
< div key = { i } style = {{ color : C . txD , fontSize : 12 , lineHeight : 1.6 , marginBottom : 4 }} > • { x } < /div>
))}
< /div>
< /div>
< />
)}
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>
{ 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>
< div className = "g3" style = {{ display : "grid" , gridTemplateColumns : "1fr 1fr 1fr" , gap : 16 , marginTop : 16 }} >
< div style = {{ background : C . bgC , border : "1px solid " + C . bd , borderRadius : 8 , padding : 16 }} >
< div style = {{ color : C . tx , fontSize : 13 , fontWeight : 700 , marginBottom : 10 }} > Central Bank < /div>
< div style = {{ display : "grid" , gap : 8 }} >
< div style = {{ display : "flex" , justifyContent : "space-between" }} >< span style = {{ color : C . txM , fontSize : 11 }} > Policy Rate < /span><span style={{color:C.tx,fontWeight:700,fontFamily:Mn}}>{tpl?.cb!=null ? tpl.cb.toFixed(2)+"%" : "—"}</span></div>
< div style = {{ display : "flex" , justifyContent : "space-between" }} >< span style = {{ color : C . txM , fontSize : 11 }} > Stance < /span><span style={{color:C.am,fontSize:11,fontWeight:700}}>{m.stance||"—"}</span></div>
< div style = {{ display : "flex" , justifyContent : "space-between" }} >< span style = {{ color : C . txM , fontSize : 11 }} > Last Decision < /span><span style={{color:C.tx,fontSize:11}}>{m.last||"—"}</span></div>
< div style = {{ display : "flex" , justifyContent : "space-between" }} >< span style = {{ color : C . txM , fontSize : 11 }} > Next Meeting < /span><span style={{color:C.g,fontSize:11}}>{m.next||"—"}</span></div>
< div style = {{ display : "flex" , justifyContent : "space-between" }} >< span style = {{ color : C . txM , fontSize : 11 }} > Pricing < /span><span style={{color:C.tx,fontSize:11}}>{m.pricing||"—"}</span></div>
< /div>
< /div>
< div style = {{ background : C . bgC , border : "1px solid " + C . bd , borderRadius : 8 , padding : 16 }} >
< div style = {{ color : C . tx , fontSize : 13 , fontWeight : 700 , marginBottom : 10 }} > Macro Snapshot < /div>
< div className = "g3" style = {{ display : "grid" , gridTemplateColumns : "1fr 1fr 1fr" , gap : 8 }} >
< div style = {{ background : C . bg , padding : 10 , borderRadius : 6 }} >
< div style = {{ color : C . txM , fontSize : 10 }} > CPI YoY < /div>
< div style = {{ color : C . tx , fontSize : 22 , fontWeight : 700 , fontFamily : Mn }} > { m . cpi || "—" } < /div>
< div style = {{ color : C . am , fontSize : 10 }} > Prior { m . cpiP || "—" } < /div>
< /div>
< div style = {{ background : C . bg , padding : 10 , borderRadius : 6 }} >
< div style = {{ color : C . txM , fontSize : 10 }} > GDP < /div>
< div style = {{ color : C . tx , fontSize : 22 , fontWeight : 700 , fontFamily : Mn }} > { m . gdp || "—" } < /div>
< div style = {{ color : C . am , fontSize : 10 }} > { m . gdpT || "—" } < /div>
< /div>
< div style = {{ background : C . bg , padding : 10 , borderRadius : 6 }} >
< div style = {{ color : C . txM , fontSize : 10 }} > Unemployment < /div>
< div style = {{ color : C . tx , fontSize : 22 , fontWeight : 700 , fontFamily : Mn }} > { m . unemp || "—" } < /div>
< div style = {{ color : C . am , fontSize : 10 }} > { m . unempT || "—" } < /div>
< /div>
< /div>
< /div>
< div style = {{ background : C . bgC , border : "1px solid " + C . bd , borderRadius : 8 , padding : 16 }} >
< div style = {{ display : "flex" , alignItems : "center" , gap : 8 , marginBottom : 10 }} >
< span style = {{ color : C . tx , fontSize : 13 , fontWeight : 700 }} > News < /span>
< Dot on = { nL } />
< /div>
< div style = {{ display : "grid" , gap : 8 }} >
{ dN . slice ( 0 , 4 ). map (( n , i )=>(
< div key = { i } style = {{ padding : "8px 10px" , background : C . bg , borderRadius : 6 }} >
< a href = { n . url || "#" } target = "_blank" rel = "noopener" style = {{ color : C . tx , fontSize : 11 , fontWeight : 500 , lineHeight : 1.4 , textDecoration : "none" , display : "block" }} >
{ n . headline }
< /a>
< div style = {{ color : C . txM , fontSize : 9 , marginTop : 3 }} >
{ n . source } · { n . datetime ? new Date ( n . datetime * 1000 ). toLocaleString ( "en-GB" ,{ day : "numeric" , month : "short" , hour : "2-digit" , minute : "2-digit" }) : "—" }
< /div>
< /div>
))}
< /div>
< /div>
2026-04-06 22:21:36 +01:00
< /div>
< /div>;
}
/* ═══ BRIEF ═══════════════════════════════════════════════════════ */
function genBr ( g10 , em , risk , yld , cal , cmd ){ const L = []; const vix = risk . VIX , dxy = risk . DXY ; const vD = ( vix ? . chg || 0 ) < 0 , dD = ( dxy ? . chg || 0 ) < 0 ; if ( vD && dD ) L . push ( "Risk on — VIX down " + Math . abs ( vix ? . chg || 0 ). toFixed ( 1 ) + " at " + ( vix ? . level ? . toFixed ( 1 ) || "—" ) + ", USD softer at " + ( dxy ? . level ? . toFixed ( 1 ) || "—" ) + " (" + fc ( dxy ? . chg ) + "). Supportive for carry." ); else if ( ! vD &&! dD ) L . push ( "Defensive tone — VIX up at " + ( vix ? . level ? . toFixed ( 1 ) || "—" ) + ", USD bid at " + ( dxy ? . level ? . toFixed ( 1 ) || "—" ) + " (" + fc ( dxy ? . chg ) + "). JPY/CHF finding support." ); else L . push ( "Mixed — VIX " + ( vix ? . level ? . toFixed ( 1 ) || "—" ) + " (" + fc ( vix ? . chg , "" ) + "), DXY " + ( dxy ? . level ? . toFixed ( 1 ) || "—" ) + " (" + fc ( dxy ? . chg ) + "). Positioning ahead of events." ); const mv = [... g10 ,... em ]. filter ( x => x . d1 != null ). sort (( a , b )=> Math . abs ( b . d1 ) - Math . abs ( a . d1 )). slice ( 0 , 4 ); if ( mv . length ) L . push ( "Movers: " + mv . map ( x => x . pair + " " + ( x . d1 > 0 ? "▲" : "▼" ) + Math . abs ( x . d1 ). toFixed ( 2 ) + "%" ). join ( ", " ) + "." ); if ( cmd . gold ? . level ) L . push ( "Gold $" + cmd . gold . level . toFixed ( 0 ) + ( cmd . gold . chg != null ? " (" + fc ( cmd . gold . chg ) + ")" : "" ) + "." + ( cmd . wti ? . level ? " WTI $" + cmd . wti . level . toFixed ( 2 ) + ( cmd . wti . chg != null ? " (" + fc ( cmd . wti . chg ) + ")" : "" ) + "." : " " )); const u2 = yld . US ? . y2 ; if ( u2 != null ){ const tc = RT . filter ( r => r . c !== "US" ). map ( r =>({ ccy : r . ccy , c :+ (( yld [ r . c ] ? . y2 ?? r . cb ) - 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 hi = cal . filter ( e => e . imp === "red" ). slice ( 0 , 4 ); if ( hi . length ) L . push ( "Watch: " + hi . map ( e => e . ctry + " " + e . ev + " (" + e . date + ")" ). join ( ", " ) + "." ); return L . join ( " " )}
function Brief ({ g10 , em , yld , risk , cal , cmd }){ const [ br , sBr ] = useState ( null ); const [ ts , sTs ] = useState ( null ); const [ ld , sLd ] = useState ( false ); const gen = ()=>{ sLd ( true ); setTimeout (()=>{ sBr ( genBr ( g10 , em , risk , yld , cal , cmd )); sTs ( new Date (). toLocaleTimeString ( "en-GB" ,{ hour : "2-digit" , minute : "2-digit" })); sLd ( false )}, 600 )}; return < div style = {{ background : C . bgC , border : "1px solid " + C . bd , borderRadius : 8 , padding : 20 }} >< div style = {{ display : "flex" , alignItems : "center" , justifyContent : "space-between" , marginBottom : 16 , flexWrap : "wrap" , gap : 8 }} >< div style = {{ display : "flex" , alignItems : "center" , gap : 10 }} >< span style = {{ fontSize : 20 }} > 🧠 < /span><div><div style={{color:C.tx,fontSize:16,fontWeight:700}}>AI Morning Brief</div><div style={{color:C.txM,fontSize:11}}>Live FX, yields, carry, commodities & calendar</div></div></div><div style={{display:"flex",alignItems:"center",gap:8}}>{ts&&<span style={{color:C.txM,fontSize:10}}>Generated {ts}</span>}<button onClick={gen} disabled={ld} style={{padding:"10px 24px",background:ld?"transparent":"linear-gradient(135deg,"+C.ac+","+C.acD+")",color:ld?C.ac:"#fff",border:ld?"1px solid "+C.ac+"40":"none",borderRadius:6,fontSize:12,fontWeight:700}}>{ld?<><span className="sp" style={{marginRight:4}}/>Analysing</>:br?"↻ Regen":"⚡ Generate Brief"}</button></div></div>{br?<div style={{padding:16,background:C.bg,borderRadius:8,border:"1px solid "+C.ac+"25"}}><div style={{color:C.tx,fontSize:13,fontStyle:"italic",lineHeight:1.7}}>{br}</div></div>:<div style={{padding:24,background:C.bg,borderRadius:8,border:"1px dashed "+C.bd,textAlign:"center",color:C.txM,fontSize:12}}>Click "Generate Brief"</div>}</div>}
function Cal ({ evts , live }){ const [ fl , sF ] = useState ( "All" ); const gF = [ "🇺🇸" , "🇪🇺" , "🇬🇧" , "🇯🇵" , "🇨🇭" , "🇦🇺" , "🇳🇿" , "🇨🇦" , "🇸🇪" , "🇳🇴" ]; return < div >< div style = {{ display : "flex" , gap : 6 , marginBottom : 10 , alignItems : "center" }} >< Dot on = { live } /> {[ "All" , "G10" , "EM" , "CB" ]. map ( f => < button key = { f } onClick = {()=> sF ( f )} style = {{ padding : "5px 14px" , fontSize : 11 , fontWeight : 600 , borderRadius : 4 , background : fl === f ? C . ac + "25" : C . bg , color : fl === f ? C . ac : C . txD , border : "1px solid " + ( fl === f ? C . ac + "50" : C . bd )}} > { f } < /button>)}</div><Tbl cols={[{k:"date",l:"Date",a:"left"},{k:"time",l:"Time",a:"left"},{k:"ctry",l:"",a:"center",nm:1},{k:"ev",l:"Event",a:"left",nm:1,cl:r=>r.imp==="red"?C.r:C.am},{k:"prev",l:"Prev"},{k:"fcast",l:"Fcst"},{k:"act",l:"Act",cl:r=>(r.act&&r.act!=="—")?C.g:C.txM}]} 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 }});
const [ rL , sRL ] = useState ( false ); const [ cal , sCal ] = useState ( DCAL ); const [ cL , sCL ] = useState ( false ); const [ news , sNews ] = useState ([]); const [ nL , sNL ] = useState ( false );
const [ cmd , sCmd ] = useState ({ gold : {}, wti : {}, brent : {}}); const [ ld , sLd ] = useState ( false ); const [ lr , sLR ] = useState ( null );
const svK = useCallback ( nk =>{ setKeys ( nk ); Object . entries ( nk ). forEach (([ k , v ])=> sk ( k , v ))},[]);
const load = useCallback ( async ()=>{
sLd ( true );
try { const [ now , d1 , w1 , m1 , ytd ] = await Promise . all ([ fFr ( "latest" , SYM ), fFr ( ago ( 1 ), SYM ), fFr ( ago ( 7 ), SYM ), fFr ( ago ( 30 ), SYM ), fFr ( jan2 (), SYM )]); if ( now ? . rates ){ const bld = defs => defs . map ( d =>{ const s = now . rates [ d . q ]; if ( ! s ) return { pair : d . p , spot : null , d1 : null , w1 : null , m1 : null , ytd : null }; const sp = d . i ? 1 / s : s ; const ch = h =>{ const v = h ? . rates ? .[ d . q ]; if ( ! v ) return null ; return d . i ? pc ( 1 / s , 1 / v ) : pc ( s , v )}; return { pair : d . p , spot : sp , d1 : ch ( d1 ), w1 : ch ( w1 ), m1 : ch ( m1 ), ytd : ch ( ytd )}}); sG10 ( bld ( G10 )); sEM ( bld ( EMP )); sFxL ( true ); const eur = now . rates . EUR , eurY = d1 ? . rates ? . EUR ; if ( eur ) sRisk ( p =>({... p , DXY : { level :+ ( 1 / eur * 104.5 ). toFixed ( 2 ), chg : eurY ?+ pc ( 1 / eur , 1 / eurY ) : null }}))}} catch {}
2026-04-06 23:19:31 +00:00
try { const usYields = await fFD (); if ( usYields ){ sYld ( p =>({... p , US : usYields })); sYL ( true ); if ( usYields . y10 != null ) sRisk ( p =>({... p , "US 10Y" : { level : usYields . y10 , chg : null }}))}} catch {}
2026-04-06 23:31:58 +00:00
try { const euYields = await fECB (); if ( euYields ){ sYld ( p =>({... p , Eurozone : euYields }))}} catch ( err ){ console . error ( "Eurozone yields load failed" , err )}
2026-04-06 22:21:36 +01:00
if ( keys . finnhub ){
2026-04-06 22:29:52 +00:00
const [ vR , gR , wR , bR ] = await Promise . allSettled ([ fFh ( "quote" ,{ symbol : "CBOE:VIX" }, keys . finnhub ), fFh ( "forex/rates" ,{ base : "USD" }, keys . finnhub ), fFh ( "quote" ,{ symbol : "NYMEX:CL1!" }, keys . finnhub ), fFh ( "quote" ,{ symbol : "NYMEX:BZ1!" }, keys . finnhub )]);
const v = vR . status === "fulfilled" ? vR . value : null ; if ( v ? . c ) sRisk ( p =>({... p , VIX : { level : v . c , chg :+ ( v . c - v . pc ). toFixed ( 2 )}}));
const g = gR . status === "fulfilled" ? gR . value : null ; if ( g ? . quote ? . XAU ){ const gp = 1 / g . quote . XAU ; sCmd ( p =>({... p , gold : { level : gp , chg : null }})); sRisk ( p =>({... p , Gold : { level : gp , chg : null }}));}
const w = wR . status === "fulfilled" ? wR . value : null ; if ( w ? . c ){ sCmd ( p =>({... p , wti : { level : w . c , chg : w . pc ?+ pc ( w . c , w . pc ) : null }})); sRisk ( p =>({... p , WTI : { level : w . c , chg : w . pc ?+ pc ( w . c , w . pc ) : null }}));}
const b = bR . status === "fulfilled" ? bR . value : null ; if ( b ? . c ){ sCmd ( p =>({... p , brent : { level : b . c , chg : b . pc ?+ pc ( b . c , b . pc ) : null }})); sRisk ( p =>({... p , Brent : { level : b . c , chg : b . pc ?+ pc ( b . c , b . pc ) : null }}));}
2026-04-06 22:21:36 +01:00
sRL ( true )}
sLd ( false ); sLR ( new Date ());
},[ keys ]);
useEffect (()=>{ load ()},[ load ]);
2026-04-06 22:29:52 +00:00
useEffect (()=>{ if (( sec === "deep" || sec === "brief" ) && keys . finnhub &&! nL ){( async ()=>{ try { const nd = await fFh ( "news" ,{ category : "forex" }, keys . finnhub ); if ( Array . isArray ( nd )){ sNews ( nd . slice ( 0 , 30 )); sNL ( true )}} catch {}})()}},[ sec , keys . finnhub , nL ]);
useEffect (()=>{ if ( sec === "cal" && keys . finnhub &&! cL ){( async ()=>{ try { const cd = await fFh ( "calendar/economic" ,{ from : ago ( 0 ), to : fwd ( 7 )}, keys . finnhub ); if ( cd ? . economicCalendar ? . length ){ const fl = { "US" : "🇺🇸" , "EU" : "🇪🇺" , "GB" : "🇬🇧" , "JP" : "🇯🇵" , "CH" : "🇨🇭" , "AU" : "🇦🇺" , "NZ" : "🇳🇿" , "CA" : "🇨🇦" , "SE" : "🇸🇪" , "NO" : "🇳🇴" , "BR" : "🇧🇷" , "MX" : "🇲🇽" , "ZA" : "🇿🇦" , "IN" : "🇮🇳" , "KR" : "🇰🇷" , "TR" : "🇹🇷" , "PL" : "🇵🇱" , "CN" : "🇨🇳" , "DE" : "🇩🇪" }; sCal ( cd . economicCalendar . slice ( 0 , 30 ). map ( e =>({ date : e . time ? . slice ( 0 , 10 ) || "" , time : e . time ? . slice ( 11 , 16 ) || "" , ctry : fl [ e . country ] || e . country || "" , ev : e . event || "" , prev : e . prev != null ? String ( e . prev ) : "—" , fcast : e . estimate != null ? String ( e . estimate ) : "—" , act : e . actual != null ? String ( e . actual ) : "—" , imp : e . impact === "high" ? "red" : "orange" }))); sCL ( true )}} catch {}})()}},[ sec , keys . finnhub , cL ]);
2026-04-06 22:21:36 +01:00
return < div style = {{ minHeight : "100vh" , background : C . bg }} >
< RiskBar d = { risk } live = { rL || fxL } />
< div style = {{ position : "sticky" , top : 0 , zIndex : 100 , background : C . bgC , borderBottom : "1px solid " + C . bd , padding : "0 16px" , display : "flex" , alignItems : "center" , justifyContent : "space-between" , flexWrap : "wrap" }} >
< div style = {{ display : "flex" , alignItems : "center" , gap : 6 , overflowX : "auto" }} >< span style = {{ fontWeight : 800 , fontSize : 14 , color : C . ac , padding : "12px 0" }} > FX < /span><span style={{fontWeight:800,fontSize:14,color:C.tx}}>RADAR</span><div style={{width:1,height:20,background:C.bd,margin:"0 8px"}}/>{[{id:"brief",l:"🧠 Brief"},{id:"markets",l:"📊 Markets"},{id:"commod",l:"🛢 Commodities"},{id:"corr",l:"🔗 Correlations"},{id:"deep",l:"🌍 Deep Dive"},{id:"cal",l:"📅 Calendar"}].map(s=><button key={s.id} onClick={()=>setSec(s.id)} style={{padding:"12px 10px",fontSize:12,fontWeight:600,background:"transparent",border:"none",color:sec===s.id?C.tx:C.txM,borderBottom:sec===s.id?"2px solid "+C.ac:"2px solid transparent",whiteSpace:"nowrap"}}>{s.l}</button>)}</div>
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 } /> }
{ 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 } />< /div>}
{ sec === "commod" &&< div style = {{ background : C . bgC , border : "1px solid " + C . bd , borderRadius : 8 , padding : 16 }} >< div style = {{ color : C . tx , fontSize : 15 , fontWeight : 700 , marginBottom : 12 }} > Commodities < /div><Cmdt d={cmd}/ >< /div>}
{ sec === "corr" &&< div style = {{ background : C . bgC , border : "1px solid " + C . bd , borderRadius : 8 , padding : 20 }} >< CorrMx />< /div>}
2026-04-06 22:29:52 +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}/></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 >