/* ═══════════════════════════════════════════════════════════════════ MARKET CLOCK · Shadow Terminal premium world clock widget Features: - 7 financial hubs with live session danger coloring - Analog/Digital toggle (persisted to localStorage) - Auto day/night face per city: white face = daytime, dark = night - Session color glow ring on each clock bezel - Dubai (owner's local) always accented in blue Usage: ═══════════════════════════════════════════════════════════════════ */ /* ── Inject keyframes once ─────────────────────────────────────────── */ (function injectClockCSS() { if (document.getElementById('st-clock-css')) return; const s = document.createElement('style'); s.id = 'st-clock-css'; s.textContent = ` @keyframes clock-digit-tick { 0% { opacity: 0.5; transform: translateY(-2px); } 100% { opacity: 1; transform: translateY(0); } } @keyframes session-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.68; } } @keyframes clock-fadein { 0% { opacity: 0; transform: translateY(6px); } 100% { opacity: 1; transform: translateY(0); } } @keyframes glow-pulse { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } } .clock-digit-tick { animation: clock-digit-tick 0.15s ease; } .session-pulse { animation: session-pulse 2s ease-in-out infinite; } .clock-card-enter { animation: clock-fadein 0.45s cubic-bezier(.2,.7,.3,1) both; } .st-clock-toggle { display: flex; gap: 2px; padding: 2px; background: #0B0D14; border: 1px solid #1E2130; border-radius: 5px; cursor: pointer; flex-shrink: 0; } .st-clock-toggle-btn { padding: 3px 7px; font-family: 'JetBrains Mono', monospace; font-size: 9px; font-weight: 600; letter-spacing: 0.1em; border: none; border-radius: 3px; cursor: pointer; transition: background 0.18s, color 0.18s, border-color 0.18s; } .st-clocks-row::-webkit-scrollbar { height: 3px; } .st-clocks-row::-webkit-scrollbar-track { background: transparent; } .st-clocks-row::-webkit-scrollbar-thumb { background: #1E2130; border-radius: 2px; } `; document.head.appendChild(s); })(); /* ── Design tokens ────────────────────────────────────────────────── */ const CK = { bg: '#0B0D14', panel: '#13161F', panel2: '#181C28', border: '#1E2130', ink: '#ECEFF6', muted: '#8B93A6', muted2: '#5A6276', amber: '#F7A600', green: '#2EE49B', red: '#FF5C77', blue: '#6EA8FF', gray: '#6B7280', }; /* ── Session logic ────────────────────────────────────────────────── */ function getSessionForHour(utcHour) { const h = ((utcHour % 24) + 24) % 24; if (h >= 13 && h < 17) return { key: 'OVERLAP', name: 'EU/US OVERLAP — DANGER ZONE', color: CK.red, threshold: '75%', cycleProb: '0.20', rec: 'Shadow threshold raised to 75% · Bot 2 conservative', dangerLevel: 'EXTREME', }; if (h >= 17 && h < 22) return { key: 'US_SESSION', name: 'US SESSION', color: CK.amber, threshold: '60%', cycleProb: '0.30', rec: 'Shadow threshold 60% · Bot 2 moderate', dangerLevel: 'HIGH', }; if (h >= 7 && h < 13) return { key: 'EU_SESSION', name: 'EU SESSION', color: CK.amber, threshold: '60%', cycleProb: '0.30', rec: 'Shadow threshold 60% · Bot 2 moderate', dangerLevel: 'MEDIUM', }; if (h >= 0 && h < 7) return { key: 'ASIAN_SESSION', name: 'ASIAN SESSION — CALM', color: CK.green, threshold: '55%', cycleProb: '0.45', rec: 'Shadow threshold 55% · Bot 2 active — grids optimal', dangerLevel: 'LOW', }; return { key: 'DEAD_ZONE', name: 'DEAD ZONE — LOW VOLUME', color: CK.gray, threshold: '65%', cycleProb: '0.10', rec: 'Hold positions · no new signals', dangerLevel: 'LOW', }; } /* ── DST helpers ─────────────────────────────────────────────────── */ function lastSunday(year, month) { const last = new Date(Date.UTC(year, month + 1, 0)); return new Date(Date.UTC(year, month + 1, 0 - last.getUTCDay())); } function nthSunday(year, month, n) { const d = new Date(Date.UTC(year, month, 1)); const off = (7 - d.getUTCDay()) % 7; return new Date(Date.UTC(year, month, 1 + off + (n - 1) * 7)); } function ukOffset(now) { const y = now.getUTCFullYear(); return (now >= lastSunday(y, 2) && now < lastSunday(y, 9)) ? 1 : 0; } function euOffset(now) { const y = now.getUTCFullYear(); return (now >= lastSunday(y, 2) && now < lastSunday(y, 9)) ? 2 : 1; } function usOffset(now) { const y = now.getUTCFullYear(); return (now >= nthSunday(y, 2, 2) && now < nthSunday(y, 10, 1)) ? -4 : -5; } /* ── Cities definition ───────────────────────────────────────────── */ function getCities(now) { return [ { id: 'tokyo', flag: '🇯🇵', city: 'TOKYO', offset: 9, sessionOpen: [0,6], sessionPeak: [1,4] }, { id: 'seoul', flag: '🇰🇷', city: 'SEOUL', offset: 9, sessionOpen: [0,6], sessionPeak: [1,5] }, { id: 'shanghai', flag: '🇨🇳', city: 'SHANGHAI', offset: 8, sessionOpen: [1,7], sessionPeak: [2,5] }, { id: 'dubai', flag: '🇦🇪', city: 'DUBAI', offset: 4, sessionOpen: [5,14], sessionPeak: [8,12], isLocal: true }, { id: 'frankfurt', flag: '🇩🇪', city: 'FRANKFURT', offset: euOffset(now), sessionOpen: [7,16], sessionPeak: [8,12] }, { id: 'london', flag: '🇬🇧', city: 'LONDON', offset: ukOffset(now), sessionOpen: [7,17], sessionPeak: [8,12] }, { id: 'newyork', flag: '🇺🇸', city: 'NEW YORK', offset: usOffset(now), sessionOpen: [13,22], sessionPeak: [14,20] }, ]; } /* ── Per-city status ─────────────────────────────────────────────── */ function getCityStatus(utcH, city) { if (city.isLocal) return 'LOCAL'; const [os, oe] = city.sessionOpen; const [ps, pe] = city.sessionPeak; if (utcH >= ps && utcH < pe) return 'PEAK'; if (utcH >= os && utcH < oe) return 'OPEN'; return 'CLOSED'; } /* ── Helpers ─────────────────────────────────────────────────────── */ function pad2(n) { return String(n).padStart(2, '0'); } function localTime(now, offsetHours) { const ms = now.getTime() + offsetHours * 3_600_000; const local = new Date(ms); return { hh: pad2(local.getUTCHours()), mm: pad2(local.getUTCMinutes()), ss: pad2(local.getUTCSeconds()), date: local, localHour: local.getUTCHours(), }; } function dayDiff(nowUtc, localDate) { const um = Date.UTC(nowUtc.getUTCFullYear(), nowUtc.getUTCMonth(), nowUtc.getUTCDate()); const lm = Date.UTC(localDate.getUTCFullYear(), localDate.getUTCMonth(), localDate.getUTCDate()); const d = (lm - um) / 86_400_000; if (d > 0) return 'TOMORROW'; if (d < 0) return 'YESTERDAY'; return null; } function isDay(localHour) { return localHour >= 6 && localHour < 20; } /* ══════════════════════════════════════════════════════════════════ ANALOG CLOCK — premium SVG bezel White face = daytime · Dark face = nighttime Session color ring around the bezel ══════════════════════════════════════════════════════════════════ */ function AnalogClock({ hh, mm, ss, localHour, ringColor, size = 80 }) { const day = isDay(localHour); const W = size; const CX = W / 2; const CY = W / 2; const R = W / 2 - 6; const secAngle = parseInt(ss) * 6; const minAngle = (parseInt(mm) + parseInt(ss) / 60) * 6; const hourAngle = ((parseInt(hh) % 12) + parseInt(mm) / 60 + parseInt(ss) / 3600) * 30; function pt(angleDeg, len) { const rad = (angleDeg - 90) * Math.PI / 180; return [CX + len * Math.cos(rad), CY + len * Math.sin(rad)]; } const [hx, hy] = pt(hourAngle, R * 0.50); const [mx, my] = pt(minAngle, R * 0.70); const [sx, sy] = pt(secAngle, R * 0.83); const [stx, sty] = pt(secAngle + 180, R * 0.20); const faceFill = day ? '#EEF1FA' : '#0C0E18'; const faceGrad1 = day ? '#F9FAFF' : '#13161F'; const tickMajor = day ? '#1E2130' : '#4A5068'; const tickMinor = day ? '#AAB0C0' : '#252840'; const hourHand = day ? '#0D0F18' : '#ECEFF6'; const minHand = day ? '#2A3050' : '#9AAABE'; const numeralC = day ? '#2A3050' : '#4A5068'; const bezelC = day ? '#C8D0E0' : '#1E2130'; const gradId = `fg-${day?'d':'n'}-${Math.round(R)}`; const shadowId = `hs-${Math.round(R)}`; const shadowC = day ? 'rgba(0,0,0,0.15)' : 'rgba(0,0,0,0.55)'; const ticks = Array.from({ length: 60 }, (_, i) => { const a = i * 6; const rad = (a - 90) * Math.PI / 180; const isHour = i % 5 === 0; const isQtr = i % 15 === 0; const outer = R - 1; const inner = isQtr ? R - 8 : isHour ? R - 5 : R - 3; return { ox: CX + outer*Math.cos(rad), oy: CY + outer*Math.sin(rad), ix: CX + inner*Math.cos(rad), iy: CY + inner*Math.sin(rad), isHour, isQtr }; }); const qtrLabels = [ { a: 0, label: '12' }, { a: 90, label: '3' }, { a: 180, label: '6' }, { a: 270, label: '9' }, ]; return ( {/* Ambient glow */} {/* Outer glow rings */} {/* Bezel ring */} {/* Face */} {/* Inner shadow ring */} {/* Tick marks */} {ticks.map((t, i) => ( ))} {/* Quarter numerals */} {qtrLabels.map(({ a, label }) => { const rad = (a - 90) * Math.PI / 180; const nr = R - 12; return ( {label} ); })} {/* Hour hand */} {/* Minute hand */} {/* Second hand */} {/* Center cap */} ); } /* ══════════════════════════════════════════════════════════════════ DAY/NIGHT BADGE — small sun/moon icon ══════════════════════════════════════════════════════════════════ */ function DayNightBadge({ localHour }) { const day = isDay(localHour); return (
{day ? {[0,45,90,135,180,225,270,315].map((a,i) => { const r = Math.PI * a / 180; return ; })} : } {day ? 'DAY' : 'NIGHT'}
); } /* ══════════════════════════════════════════════════════════════════ CLOCK CARD ══════════════════════════════════════════════════════════════════ */ function ClockCard({ city, now, utcHour, index, analogMode }) { const { hh, mm, ss, date, localHour } = localTime(now, city.offset); const status = getCityStatus(utcHour, city); const diff = dayDiff(now, date); const session = getSessionForHour(utcHour); let ringColor, pillColor, pillLabel; if (city.isLocal) { ringColor = CK.blue; pillColor = CK.blue; pillLabel = 'YOUR TIME'; } else { switch (status) { case 'PEAK': pillLabel = 'PEAK'; ringColor = session.key === 'OVERLAP' ? CK.red : CK.amber; pillColor = ringColor; break; case 'OPEN': pillLabel = 'OPEN'; ringColor = CK.green; pillColor = CK.green; break; default: pillLabel = 'CLOSED'; ringColor = CK.gray; pillColor = CK.gray; } } /* Compact sizes */ const minW = city.isLocal ? (analogMode ? 106 : 96) : (analogMode ? 96 : 86); return (
{/* Day/night badge — top-right */}
{/* Flag */} {city.flag} {/* Clock face */} {analogMode ? ( ) : (
{hh}:{mm} :{ss}
)} {/* Day diff */} {diff && (
{diff}
)} {/* City name */}
{city.city}
{/* Status pill */}
{pillLabel}
); } /* ══════════════════════════════════════════════════════════════════ SESSION BANNER + MODE TOGGLE ══════════════════════════════════════════════════════════════════ */ function SessionBanner({ session, analogMode, onToggle }) { const isRed = session.key === 'OVERLAP'; const c = session.color; const bgMap = { OVERLAP: 'rgba(255,92,119,0.09)', US_SESSION: 'rgba(247,166,0,0.05)', EU_SESSION: 'rgba(247,166,0,0.05)', ASIAN_SESSION: 'rgba(46,228,155,0.05)', DEAD_ZONE: 'rgba(107,114,128,0.04)', }; const borderMap = { OVERLAP: 'rgba(255,92,119,0.25)', US_SESSION: 'rgba(247,166,0,0.16)', EU_SESSION: 'rgba(247,166,0,0.16)', ASIAN_SESSION: 'rgba(46,228,155,0.16)', DEAD_ZONE: 'rgba(107,114,128,0.10)', }; return (
{/* Left: dot + name */}
{session.name}
{/* Right: rec + toggle */}
{session.rec}
{['ANA', 'DIG'].map(mode => { const active = (mode === 'ANA') === analogMode; return ( ); })}
); } /* ══════════════════════════════════════════════════════════════════ MARKET CLOCK — root component Props: overrideUtcHour {number|null} — force UTC hour (test only) ══════════════════════════════════════════════════════════════════ */ /* ══════════════════════════════════════════════════════════════════ NEXT FUNDING — countdown to Bybit's 8h funding (00 / 08 / 16 UTC). Funding flips move the whole perp market, so it's a real signal. ══════════════════════════════════════════════════════════════════ */ function FundingCell({ now }) { const secsNow = now.getUTCHours() * 3600 + now.getUTCMinutes() * 60 + now.getUTCSeconds(); const period = 8 * 3600; const diff = period - (secsNow % period); // seconds to next 8h boundary const hh = Math.floor(diff / 3600), mm = Math.floor((diff % 3600) / 60), ss = diff % 60; const soon = diff < 1800; // within 30 min const c = soon ? CK.red : CK.green; return (
NEXT FUNDING
{pad2(hh)}:{pad2(mm)}:{pad2(ss)}
8H CYCLE · 00/08/16 UTC
); } function MarketClock({ overrideUtcHour = null }) { const [now, setNow] = React.useState(() => new Date()); const [analogMode, setAnalogMode] = React.useState(() => { try { return localStorage.getItem('st_clock_mode') === 'analog'; } catch (_) { return false; } }); React.useEffect(() => { const id = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(id); }, []); const toggleMode = React.useCallback(() => { setAnalogMode(prev => { const next = !prev; try { localStorage.setItem('st_clock_mode', next ? 'analog' : 'digital'); } catch (_) {} return next; }); }, []); const displayNow = React.useMemo(() => { if (overrideUtcHour == null) return now; const d = new Date(now); d.setUTCHours(overrideUtcHour, d.getUTCMinutes(), d.getUTCSeconds(), 0); return d; }, [now, overrideUtcHour]); const utcHour = displayNow.getUTCHours(); const session = getSessionForHour(utcHour); const cities = getCities(displayNow); const utcStr = `${pad2(displayNow.getUTCHours())}:${pad2(displayNow.getUTCMinutes())}:${pad2(displayNow.getUTCSeconds())}`; return (
{/* Session banner */} {/* Clocks row */}
{cities.map((city, i) => ( ))} {/* NEXT FUNDING countdown — fills the gap before UTC */} {/* UTC reference — far right */}
UTC · LIVE
{utcStr}
{session.dangerLevel}
threshold {session.threshold}
); } window.MarketClock = MarketClock; window.getSessionForHour = getSessionForHour;