Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>β’ POLYMARKET CONTROL DECK</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"/> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/> | |
| <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&family=VT323&display=swap" rel="stylesheet"/> | |
| <style> | |
| :root{ | |
| --void:#050810; --panel:#0a1020; --panel-raised:#0f1828; | |
| --rim:#1a2a44; --rim-bright:#2a4a74; | |
| --phosphor:#00ff9f; --amber:#ffb347; --cyan:#4dd8ff; | |
| --warn:#ff3b5c; --hot:#ff7020; --dim:#4a6080; | |
| --text:#c5d8e8; --text-bright:#e8f4ff; | |
| } | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| html,body{background:var(--void);color:var(--text);font-family:'Share Tech Mono',monospace;font-size:14px;min-height:100%;overflow-x:hidden} | |
| body{background:radial-gradient(ellipse at 50% 30%,#0a1530 0%,#050810 60%,#000 100%);position:relative;min-height:100vh} | |
| body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:9999; | |
| background:repeating-linear-gradient(0deg,rgba(0,255,159,.025) 0px,rgba(0,255,159,.025) 1px,transparent 1px,transparent 3px)} | |
| body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:9998; | |
| background:radial-gradient(ellipse at 50% 50%,transparent 55%,rgba(0,0,0,.75) 100%)} | |
| .app{max-width:1600px;margin:0 auto;padding:16px;position:relative;z-index:1} | |
| a{color:var(--cyan)} | |
| select::-ms-expand{display:none} | |
| .header{ | |
| display:flex;align-items:center;justify-content:space-between; | |
| background:linear-gradient(180deg,#0f1828 0%,#0a1220 100%); | |
| border:1px solid var(--rim);border-bottom:2px solid var(--phosphor); | |
| padding:14px 24px;margin-bottom:16px; | |
| clip-path:polygon(0 0,calc(100% - 22px) 0,100% 22px,100% 100%,22px 100%,0 calc(100% - 22px)); | |
| box-shadow:0 0 30px rgba(0,255,159,.12),inset 0 0 20px rgba(0,0,0,.5); | |
| } | |
| .header h1{font-family:'Orbitron',sans-serif;font-weight:900;font-size:22px;letter-spacing:4px;color:var(--phosphor); | |
| text-shadow:0 0 10px rgba(0,255,159,.8),0 0 20px rgba(0,255,159,.4)} | |
| .header .sub{color:var(--dim);font-size:11px;letter-spacing:2px;margin-top:4px} | |
| .clock{font-family:'VT323',monospace;font-size:26px;color:var(--amber);text-shadow:0 0 8px rgba(255,179,71,.6)} | |
| .led{display:inline-block;width:10px;height:10px;border-radius:50%;margin-left:10px;vertical-align:middle; | |
| background:radial-gradient(circle,var(--phosphor),#003322);box-shadow:0 0 10px var(--phosphor); | |
| animation:pulse 1.5s ease-in-out infinite} | |
| @keyframes pulse{0%,100%{opacity:1;box-shadow:0 0 12px var(--phosphor)}50%{opacity:.45;box-shadow:0 0 4px var(--phosphor)}} | |
| .panel{ | |
| background:linear-gradient(180deg,var(--panel-raised) 0%,var(--panel) 100%); | |
| border:1px solid var(--rim);padding:18px;position:relative; | |
| box-shadow:inset 0 0 40px rgba(0,0,0,.6),0 2px 12px rgba(0,0,0,.8); | |
| clip-path:polygon(0 0,calc(100% - 14px) 0,100% 14px,100% 100%,14px 100%,0 calc(100% - 14px)) | |
| } | |
| .panel .label{position:absolute;top:-1px;left:16px;background:var(--panel); | |
| padding:3px 10px;font-size:10px;letter-spacing:3px;color:var(--cyan); | |
| text-shadow:0 0 4px var(--cyan);border:1px solid var(--rim);border-bottom:none;z-index:2} | |
| .panel.reactor::before{ | |
| content:'';position:absolute;top:8px;right:14px;width:8px;height:8px;border-radius:50%; | |
| background:var(--phosphor);box-shadow:0 0 8px var(--phosphor);animation:pulse 1s infinite | |
| } | |
| .grid{display:grid;gap:16px} | |
| .row2{grid-template-columns:3fr 2fr} | |
| .control-row{display:grid;grid-template-columns:1.2fr 2.2fr auto auto;gap:18px;align-items:flex-end;margin-top:8px} | |
| .ctrl{display:flex;flex-direction:column;gap:6px;min-width:0} | |
| .ctrl label{font-size:10px;letter-spacing:2px;color:var(--dim)} | |
| select,button{ | |
| background:linear-gradient(180deg,#0f2038 0%,#0a1a30 100%); | |
| color:var(--text-bright);border:1px solid var(--rim-bright); | |
| padding:10px 14px;font-family:inherit;font-size:13px;cursor:pointer;outline:none; | |
| transition:all .15s;letter-spacing:1px | |
| } | |
| select:hover,button:hover:not(:disabled){border-color:var(--phosphor);box-shadow:0 0 10px rgba(0,255,159,.35)} | |
| button:disabled{opacity:.35;cursor:not-allowed} | |
| select{min-width:0;width:100%;appearance:none;-webkit-appearance:none; | |
| background-image:linear-gradient(45deg,transparent 50%,var(--cyan) 50%),linear-gradient(135deg,var(--cyan) 50%,transparent 50%); | |
| background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat; | |
| padding-right:30px} | |
| button.primary{ | |
| background:linear-gradient(180deg,#1a4030 0%,#0a2018 100%); | |
| color:var(--phosphor);text-shadow:0 0 6px var(--phosphor); | |
| letter-spacing:3px;font-weight:bold;text-transform:uppercase; | |
| border-color:var(--phosphor);min-width:130px | |
| } | |
| button.primary:hover:not(:disabled){ | |
| background:linear-gradient(180deg,#2a6045 0%,#1a3020 100%); | |
| box-shadow:0 0 20px rgba(0,255,159,.6),inset 0 0 10px rgba(0,255,159,.15) | |
| } | |
| button.primary:disabled{color:var(--dim);text-shadow:none;border-color:var(--rim)} | |
| .interval-group{display:flex;gap:3px} | |
| .interval-group button{padding:8px 12px;font-size:11px;background:#0a1a30;color:var(--dim);border:1px solid var(--rim);flex:1;text-align:center;letter-spacing:1px} | |
| .interval-group button.active{ | |
| background:linear-gradient(180deg,#ffb347 0%,#c27200 100%); | |
| color:#100800;border-color:var(--amber);text-shadow:none; | |
| box-shadow:0 0 12px rgba(255,179,71,.55),inset 0 0 6px rgba(255,255,255,.2)} | |
| .target-event{font-size:11px;color:var(--cyan);letter-spacing:2px;margin-bottom:6px;text-transform:uppercase;text-shadow:0 0 4px rgba(77,216,255,.5)} | |
| .target-question{font-size:15px;color:var(--text-bright);min-height:42px;line-height:1.35;margin-bottom:10px} | |
| .price-big{ | |
| font-family:'VT323',monospace;font-size:110px;line-height:.9; | |
| color:var(--phosphor);text-align:center;margin:4px 0; | |
| text-shadow:0 0 18px rgba(0,255,159,.85),0 0 36px rgba(0,255,159,.4),0 0 60px rgba(0,255,159,.2); | |
| letter-spacing:-2px | |
| } | |
| .price-change{text-align:center;font-size:18px;letter-spacing:3px;margin-bottom:14px} | |
| .price-change.up{color:var(--phosphor);text-shadow:0 0 6px rgba(0,255,159,.7)} | |
| .price-change.down{color:var(--warn);text-shadow:0 0 6px rgba(255,59,92,.7)} | |
| .price-change.flat{color:var(--dim)} | |
| .chart-container{width:100%;height:280px;display:block} | |
| .meta-row{display:grid;grid-template-columns:repeat(5,1fr);gap:4px;padding:12px 0 2px;border-top:1px dashed var(--rim);margin-top:12px} | |
| .meta-row .item{text-align:center} | |
| .meta-row .item .key{color:var(--dim);font-size:9px;letter-spacing:2px} | |
| .meta-row .item .val{color:var(--amber);font-size:16px;text-shadow:0 0 6px rgba(255,179,71,.5);margin-top:2px;font-family:'VT323',monospace} | |
| .factor-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:10px} | |
| .factor-tile{ | |
| background:linear-gradient(180deg,#0a1828 0%,#050f20 100%); | |
| border:1px solid var(--rim);padding:14px;position:relative; | |
| clip-path:polygon(0 0,calc(100% - 12px) 0,100% 12px,100% 100%,12px 100%,0 calc(100% - 12px)); | |
| transition:all .3s | |
| } | |
| .factor-tile .name{font-size:10px;color:var(--cyan);letter-spacing:3px;text-shadow:0 0 4px rgba(77,216,255,.5)} | |
| .factor-tile .status{font-family:'Orbitron',sans-serif;font-weight:700;font-size:15px;letter-spacing:2px;margin-top:10px;color:var(--amber);text-shadow:0 0 6px rgba(255,179,71,.5)} | |
| .factor-tile .metric{font-size:11px;color:var(--dim);margin-top:6px;font-family:'Share Tech Mono',monospace} | |
| .factor-tile.alert{border-color:var(--warn);animation:alert-pulse 1.4s infinite} | |
| .factor-tile.alert .status{color:var(--warn);text-shadow:0 0 8px rgba(255,59,92,.7)} | |
| .factor-tile.hot{border-color:var(--phosphor);box-shadow:0 0 15px rgba(0,255,159,.3)} | |
| .factor-tile.hot .status{color:var(--phosphor);text-shadow:0 0 8px rgba(0,255,159,.7)} | |
| @keyframes alert-pulse{ | |
| 0%,100%{box-shadow:0 0 15px rgba(255,59,92,.3),inset 0 0 20px rgba(255,59,92,.05)} | |
| 50%{box-shadow:0 0 30px rgba(255,59,92,.7),inset 0 0 30px rgba(255,59,92,.15)} | |
| } | |
| .gauge-wrap{grid-column:span 2;padding-top:6px} | |
| .gauge-wrap .title{font-size:10px;color:var(--cyan);letter-spacing:3px;margin-bottom:2px;text-shadow:0 0 4px rgba(77,216,255,.5)} | |
| .gauge{width:100%;height:120px} | |
| .log-panel{margin-top:16px} | |
| .log{background:#030610;border:1px solid var(--rim);padding:14px 16px; | |
| font-family:'VT323',monospace;font-size:16px;color:var(--phosphor); | |
| max-height:170px;min-height:120px;overflow-y:auto; | |
| box-shadow:inset 0 0 24px rgba(0,0,0,.9);margin-top:6px} | |
| .log::-webkit-scrollbar{width:8px} | |
| .log::-webkit-scrollbar-track{background:#030610} | |
| .log::-webkit-scrollbar-thumb{background:var(--rim)} | |
| .log .entry{opacity:.85;line-height:1.35;animation:logline .4s} | |
| .log .entry.new{color:var(--text-bright);text-shadow:0 0 8px var(--phosphor);opacity:1} | |
| .log .entry.err{color:var(--warn);text-shadow:0 0 6px rgba(255,59,92,.6)} | |
| .log .ts{color:var(--dim);margin-right:10px} | |
| @keyframes logline{from{opacity:0;transform:translateX(-4px)}to{opacity:.85}} | |
| .spinner{display:inline-block;width:14px;height:14px;border:2px solid var(--dim); | |
| border-top-color:var(--phosphor);border-radius:50%;animation:spin .7s linear infinite;vertical-align:middle;margin-left:6px} | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| .footer{text-align:center;padding:20px 0 10px;font-size:10px;color:var(--dim);letter-spacing:3px} | |
| .standby{padding:80px 0;text-align:center;color:var(--dim);font-size:14px;letter-spacing:4px;animation:pulse 2s infinite} | |
| @media(max-width:1000px){ | |
| .row2{grid-template-columns:1fr} | |
| .control-row{grid-template-columns:1fr 1fr} | |
| .price-big{font-size:72px} | |
| .meta-row{grid-template-columns:repeat(3,1fr)} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app"></div> | |
| <script type="module"> | |
| import { h, render } from 'https://esm.sh/preact@10.22.0'; | |
| import { useState, useEffect, useRef } from 'https://esm.sh/preact@10.22.0/hooks'; | |
| import htm from 'https://esm.sh/htm@3.1.1'; | |
| const html = htm.bind(h); | |
| // Same-origin proxy (FastAPI server.py) β avoids Gamma's CORS whitelist. | |
| const GAMMA = '/api'; | |
| const CLOB = '/api'; | |
| const CATEGORIES = { | |
| 'TOP 24H VOLUME': { order: 'volume24hr', ascending: false }, | |
| 'TOP TOTAL VOL': { order: 'volume', ascending: false }, | |
| 'TOP LIQUIDITY': { order: 'liquidity', ascending: false }, | |
| 'MOST COMPETITIVE':{ order: 'competitive', ascending: false }, | |
| 'BREAKING HOT': { order: 'volume24hr', ascending: false, hot: true }, | |
| 'ENDING SOON': { order: 'end_date', ascending: true }, | |
| }; | |
| // ===== Math ===== | |
| const mean = a => a.length ? a.reduce((s,x)=>s+x,0)/a.length : 0; | |
| const std = a => { if (a.length<2) return 0; const m=mean(a); return Math.sqrt(a.reduce((s,x)=>s+(x-m)**2,0)/a.length); }; | |
| const logit = p => { const c=Math.min(Math.max(p,1e-6),1-1e-6); return Math.log(c/(1-c)); }; | |
| const diff = a => { const r=[]; for(let i=1;i<a.length;i++) r.push(a[i]-a[i-1]); return r; }; | |
| function autocorr(a, lag){ | |
| if (a.length<=lag+1) return NaN; | |
| const m = mean(a); let num=0, den=0; | |
| for(let i=0;i<a.length;i++) den += (a[i]-m)**2; | |
| for(let i=lag;i<a.length;i++) num += (a[i]-m)*(a[i-lag]-m); | |
| return den>0 ? num/den : 0; | |
| } | |
| function hurst(series){ | |
| const n = series.length; if (n<30) return NaN; | |
| const lags = []; for (let l=4; l<n/2; l=Math.floor(l*1.5)) lags.push(l); | |
| if (lags.length<4) return NaN; | |
| const xs=[], ys=[]; | |
| for (const lag of lags){ | |
| const chunks = Math.floor(n/lag); if (chunks<1) continue; | |
| const rs = []; | |
| for (let i=0;i<chunks;i++){ | |
| const seg = series.slice(i*lag,(i+1)*lag); | |
| const m = mean(seg); const dev = seg.map(x=>x-m); | |
| let cum=0; const cs = dev.map(d=>(cum+=d)); | |
| const R = Math.max(...cs)-Math.min(...cs); const S = std(seg); | |
| if (S>0) rs.push(R/S); | |
| } | |
| if (rs.length){ xs.push(Math.log(lag)); ys.push(Math.log(mean(rs))); } | |
| } | |
| if (xs.length<4) return NaN; | |
| const mx=mean(xs), my=mean(ys); let num=0, den=0; | |
| for (let i=0;i<xs.length;i++){ num+=(xs[i]-mx)*(ys[i]-my); den+=(xs[i]-mx)**2; } | |
| return den>0 ? num/den : NaN; | |
| } | |
| function pearson(arr){ | |
| const xs = arr.map(p=>p[0]), ys = arr.map(p=>p[1]); | |
| const mx = mean(xs), my = mean(ys); | |
| let num=0, dx=0, dy=0; | |
| for (let i=0;i<arr.length;i++){ | |
| num += (xs[i]-mx)*(ys[i]-my); | |
| dx += (xs[i]-mx)**2; | |
| dy += (ys[i]-my)**2; | |
| } | |
| return (dx*dy)>0 ? num/Math.sqrt(dx*dy) : 0; | |
| } | |
| function computeFactors(history, siblingHistories){ | |
| if (!history || history.length<20) return { error:'INSUFFICIENT HISTORY' }; | |
| const p = history.map(d=>d.p); | |
| const t = history.map(d=>d.t); // seconds | |
| const lg = p.map(logit); | |
| const ret = diff(lg); | |
| const nowSec = t[t.length-1]; | |
| const windowDLogit = hours => { | |
| const cutoff = nowSec - hours*3600; | |
| const idx = history.findIndex(d=>d.t >= cutoff); | |
| if (idx<0 || idx>=history.length-1) return NaN; | |
| return lg[lg.length-1] - lg[idx]; | |
| }; | |
| const vAll = std(ret); | |
| const m24 = windowDLogit(24); | |
| const m7 = windowDLogit(24*7); | |
| const z24 = vAll>0 ? m24/(vAll*Math.sqrt(24)) : NaN; | |
| const z7 = vAll>0 ? m7 /(vAll*Math.sqrt(24*7)) : NaN; | |
| const momLabel = (isNaN(z24)||isNaN(z7)) ? 'INSUFFICIENT' | |
| : (z24> 1.5 && z7> 0.5) ? 'STRONG UP' | |
| : (z24<-1.5 && z7<-0.5) ? 'STRONG DOWN' | |
| : z24> 0.7 ? 'UP' | |
| : z24<-0.7 ? 'DOWN' : 'FLAT'; | |
| const H = hurst(p); | |
| const a1 = autocorr(ret, 1); | |
| const mrLabel = isNaN(H) ? 'INSUFFICIENT' | |
| : (H<0.4 || (!isNaN(a1)&&a1<-0.15)) ? 'MEAN REVERTING' | |
| : H>0.6 ? 'TRENDING' : 'RANDOM WALK'; | |
| const retT = t.slice(1); | |
| const cut24 = nowSec - 24*3600; | |
| const cut7d = nowSec - 7*24*3600; | |
| const r24 = ret.filter((_,i)=>retT[i]>=cut24); | |
| const r7 = ret.filter((_,i)=>retT[i]>=cut7d); | |
| const v24 = std(r24), v7 = std(r7); | |
| const ratio = v7>0 ? v24/v7 : NaN; | |
| const volLabel = isNaN(ratio) ? 'INSUFFICIENT' | |
| : ratio>1.6 ? 'HIGH VOL' | |
| : ratio<0.6 ? 'LOW VOL' : 'NORMAL'; | |
| let corr = { label:'NO SIBLINGS', baseline:null, recent:null, delta:null }; | |
| if (siblingHistories && siblingHistories.length){ | |
| const resample = src => { | |
| const out = []; let j=0; | |
| for (const tt of t){ | |
| while (j<src.length-1 && src[j+1].t<=tt) j++; | |
| out.push(src[j]?.p ?? NaN); | |
| } | |
| return out; | |
| }; | |
| const sibs = siblingHistories.map(resample); | |
| const composite = t.map((_,i)=>{ | |
| const vals = sibs.map(s=>s[i]).filter(v=>!isNaN(v)); | |
| return vals.length ? mean(vals) : NaN; | |
| }); | |
| const pairs = p.map((x,i)=>[x,composite[i]]).filter(([a,b])=>!isNaN(a)&&!isNaN(b)); | |
| if (pairs.length>=48){ | |
| const cutIdx = pairs.length-24; | |
| const cb = pearson(pairs.slice(0,cutIdx)); | |
| const cr = pearson(pairs.slice(cutIdx)); | |
| const d = cr-cb; | |
| corr = { label: Math.abs(d)>0.35 ? 'CORR BREAK' : 'STABLE', baseline:cb, recent:cr, delta:d }; | |
| } | |
| } | |
| return { | |
| momentum: { z24, z7, label: momLabel }, | |
| meanRev: { hurst: H, a1, label: mrLabel }, | |
| volRegime:{ v24, v7, ratio, label: volLabel }, | |
| corr, | |
| }; | |
| } | |
| // ===== API ===== | |
| async function fetchEvents(cfg, limit=40){ | |
| const params = new URLSearchParams({ | |
| active:'true', closed:'false', order: cfg.order, | |
| ascending: String(cfg.ascending), limit: String(limit) | |
| }); | |
| const r = await fetch(`${GAMMA}/events?${params}`); | |
| if (!r.ok) throw new Error(`EVENTS ${r.status}`); | |
| return r.json(); | |
| } | |
| function flattenMarkets(events, hot=false){ | |
| const rows = []; | |
| for (const evt of events){ | |
| for (const m of (evt.markets||[])){ | |
| let tokens=m.clobTokenIds, outs=m.outcomes, prices=m.outcomePrices; | |
| try{ if (typeof tokens==='string') tokens=JSON.parse(tokens);}catch{} | |
| try{ if (typeof outs==='string') outs =JSON.parse(outs); }catch{} | |
| try{ if (typeof prices==='string') prices=JSON.parse(prices);}catch{} | |
| if (!tokens || !tokens.length || !outs) continue; | |
| const vol = parseFloat(m.volumeNum || m.volume || 0); | |
| const vol24 = parseFloat(m.volume24hr || 0); | |
| const liq = parseFloat(m.liquidityNum || 0); | |
| rows.push({ | |
| event: evt.title, question: m.question, slug: m.slug, | |
| vol_total: vol, vol_24h: vol24, liquidity: liq, | |
| hot_ratio: vol>0 ? vol24/vol : 0, | |
| token_yes: tokens[0], token_no: tokens[1], | |
| outcomes: outs, prices: (prices||[]).map(parseFloat), | |
| end_date: m.endDate, | |
| }); | |
| } | |
| } | |
| if (hot) return rows.filter(r=>r.vol_total>50000).sort((a,b)=>b.hot_ratio-a.hot_ratio); | |
| return rows; | |
| } | |
| async function fetchHistory(tokenId, interval='1w', fidelity=60){ | |
| const params = new URLSearchParams({ market: tokenId, interval, fidelity: String(fidelity) }); | |
| const r = await fetch(`${CLOB}/prices-history?${params}`); | |
| if (!r.ok) throw new Error(`HISTORY ${r.status}`); | |
| const data = await r.json(); | |
| return data.history || []; | |
| } | |
| // ===== Components ===== | |
| function Clock(){ | |
| const [t, setT] = useState(new Date()); | |
| useEffect(()=>{ const id=setInterval(()=>setT(new Date()),1000); return ()=>clearInterval(id); },[]); | |
| const h = String(t.getUTCHours()).padStart(2,'0'); | |
| const m = String(t.getUTCMinutes()).padStart(2,'0'); | |
| const s = String(t.getUTCSeconds()).padStart(2,'0'); | |
| return html`<span class="clock">${h}:${m}:${s} UTC</span><span class="led"></span>`; | |
| } | |
| function Sparkline({ data, width=820, height=280 }){ | |
| if (!data || data.length<2) { | |
| return html`<svg viewBox="0 0 ${width} ${height}" class="chart-container"> | |
| <rect x="0" y="0" width="${width}" height="${height}" fill="#030810" stroke="#1a2a44"/> | |
| <text x="${width/2}" y="${height/2}" fill="#4a6080" text-anchor="middle" font-family="Share Tech Mono" font-size="14" letter-spacing="4">β NO SIGNAL β</text> | |
| </svg>`; | |
| } | |
| const xs = data.map(d=>d.t), ys = data.map(d=>d.p); | |
| const xmin = Math.min(...xs), xmax = Math.max(...xs); | |
| const pad = 38; | |
| const W = width - pad*2, H = height - pad*2; | |
| const sx = x => pad + ((x-xmin)/(xmax-xmin || 1))*W; | |
| const sy = y => pad + (1 - y)*H; | |
| const path = data.map((d,i)=>`${i===0?'M':'L'}${sx(d.t).toFixed(1)},${sy(d.p).toFixed(1)}`).join(' '); | |
| const areaPath = `${path} L${sx(xmax).toFixed(1)},${sy(0).toFixed(1)} L${sx(xmin).toFixed(1)},${sy(0).toFixed(1)} Z`; | |
| const gridVals = [0, 0.25, 0.5, 0.75, 1]; | |
| const tickXs = []; | |
| const nTicks = 6; | |
| for (let i=0;i<nTicks;i++){ | |
| const t = xmin + (xmax-xmin)*(i/(nTicks-1)); | |
| const d = new Date(t*1000); | |
| tickXs.push({ x: sx(t), label: `${String(d.getUTCMonth()+1).padStart(2,'0')}-${String(d.getUTCDate()).padStart(2,'0')}` }); | |
| } | |
| const lastX = sx(xs[xs.length-1]); | |
| const lastY = sy(ys[ys.length-1]); | |
| return html` | |
| <svg viewBox="0 0 ${width} ${height}" class="chart-container" xmlns="http://www.w3.org/2000/svg"> | |
| <defs> | |
| <linearGradient id="fillg" x1="0" x2="0" y1="0" y2="1"> | |
| <stop offset="0%" stop-color="#00ff9f" stop-opacity="0.45"/> | |
| <stop offset="100%" stop-color="#00ff9f" stop-opacity="0"/> | |
| </linearGradient> | |
| <filter id="glow" x="-50%" y="-50%" width="200%" height="200%"> | |
| <feGaussianBlur stdDeviation="2.8" result="blur"/> | |
| <feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge> | |
| </filter> | |
| </defs> | |
| <rect x="${pad}" y="${pad}" width="${W}" height="${H}" fill="#030810" stroke="#0a3050"/> | |
| ${gridVals.map(v=>html` | |
| <line x1="${pad}" x2="${pad+W}" y1="${sy(v)}" y2="${sy(v)}" stroke="#0a3050" stroke-dasharray="2 4"/> | |
| <text x="${pad-6}" y="${sy(v)+4}" fill="#4a6080" font-size="10" text-anchor="end" font-family="Share Tech Mono">${(v*100).toFixed(0)}</text> | |
| `)} | |
| ${tickXs.map(t=>html` | |
| <line x1="${t.x}" x2="${t.x}" y1="${pad}" y2="${pad+H}" stroke="#0a3050" stroke-dasharray="2 4"/> | |
| <text x="${t.x}" y="${pad+H+16}" fill="#4a6080" font-size="10" text-anchor="middle" font-family="Share Tech Mono">${t.label}</text> | |
| `)} | |
| <path d="${areaPath}" fill="url(#fillg)"/> | |
| <path d="${path}" fill="none" stroke="#00ff9f" stroke-width="2" filter="url(#glow)"/> | |
| <circle cx="${lastX}" cy="${lastY}" r="4" fill="#00ff9f" filter="url(#glow)"> | |
| <animate attributeName="r" values="4;8;4" dur="1.6s" repeatCount="indefinite"/> | |
| <animate attributeName="opacity" values="1;.4;1" dur="1.6s" repeatCount="indefinite"/> | |
| </circle> | |
| </svg> | |
| `; | |
| } | |
| function Gauge({ value }){ | |
| const min=-3, max=3; | |
| const clamped = Math.max(min, Math.min(max, value||0)); | |
| const pct = (clamped-min)/(max-min); | |
| const angle = Math.PI - pct*Math.PI; | |
| const cx=150, cy=100, r=82; | |
| const nx = cx + (r-10)*Math.cos(angle); | |
| const ny = cy - (r-10)*Math.sin(angle); | |
| const arc = `M ${cx-r} ${cy} A ${r} ${r} 0 0 1 ${cx+r} ${cy}`; | |
| const ticks = [-3,-2,-1,0,1,2,3]; | |
| return html` | |
| <svg viewBox="0 0 300 120" class="gauge" xmlns="http://www.w3.org/2000/svg"> | |
| <defs> | |
| <linearGradient id="arcg" x1="0" x2="1"><stop offset="0%" stop-color="#ff3b5c"/><stop offset="50%" stop-color="#ffb347"/><stop offset="100%" stop-color="#00ff9f"/></linearGradient> | |
| <filter id="gauge-glow"><feGaussianBlur stdDeviation="2"/></filter> | |
| </defs> | |
| <path d="${arc}" fill="none" stroke="#0a1828" stroke-width="16"/> | |
| <path d="${arc}" fill="none" stroke="url(#arcg)" stroke-width="8" filter="url(#gauge-glow)"/> | |
| ${ticks.map(tv=>{ | |
| const tpct = (tv-min)/(max-min); | |
| const a = Math.PI - tpct*Math.PI; | |
| const x1 = cx + (r-4)*Math.cos(a), y1 = cy - (r-4)*Math.sin(a); | |
| const x2 = cx + (r+8)*Math.cos(a), y2 = cy - (r+8)*Math.sin(a); | |
| const lx = cx + (r+18)*Math.cos(a), ly = cy - (r+18)*Math.sin(a) + 4; | |
| return html` | |
| <line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#4a6080" stroke-width="1.5"/> | |
| <text x="${lx}" y="${ly}" fill="#4a6080" font-size="10" text-anchor="middle" font-family="Share Tech Mono">${tv>0?'+'+tv:tv}</text> | |
| `; | |
| })} | |
| <line x1="${cx}" y1="${cy}" x2="${nx}" y2="${ny}" stroke="#e8f4ff" stroke-width="3" stroke-linecap="round"/> | |
| <circle cx="${cx}" cy="${cy}" r="6" fill="#ffb347" stroke="#4a6080" stroke-width="1"/> | |
| <text x="${cx}" y="${cy+30}" fill="#ffb347" font-size="18" text-anchor="middle" font-family="VT323" style="text-shadow:0 0 6px rgba(255,179,71,.6)"> | |
| z = ${clamped.toFixed(2)} | |
| </text> | |
| </svg> | |
| `; | |
| } | |
| function FactorTile({ name, status, metric, alert, hot }){ | |
| const cls = `factor-tile${alert?' alert':''}${hot?' hot':''}`; | |
| return html` | |
| <div class=${cls}> | |
| <div class="name">${name}</div> | |
| <div class="status">${status}</div> | |
| <div class="metric">${metric}</div> | |
| </div> | |
| `; | |
| } | |
| // ===== Main ===== | |
| function App(){ | |
| const [category, setCategory] = useState('TOP 24H VOLUME'); | |
| const [markets, setMarkets] = useState([]); | |
| const [marketIdx, setMarketIdx] = useState(0); | |
| const [intv, setIntv] = useState('1w'); | |
| const [fidelity] = useState(60); | |
| const [history, setHistory] = useState([]); | |
| const [factors, setFactors] = useState(null); | |
| const [loading, setLoading] = useState(false); | |
| const [analyzing, setAnalyzing] = useState(false); | |
| const [log, setLog] = useState([]); | |
| const [booted, setBooted] = useState(false); | |
| const logRef = useRef(null); | |
| const addLog = (msg, type='') => { | |
| const now = new Date(); | |
| const ts = `${String(now.getUTCHours()).padStart(2,'0')}:${String(now.getUTCMinutes()).padStart(2,'0')}:${String(now.getUTCSeconds()).padStart(2,'0')}`; | |
| setLog(l => [...l.slice(-50), { ts, msg, type, id: Date.now()+Math.random() }]); | |
| }; | |
| useEffect(()=>{ | |
| if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; | |
| }, [log]); | |
| useEffect(()=>{ | |
| const seq = [ | |
| 'INIT POLYMARKET CONTROL DECK v1.0', | |
| '> ESTABLISHING LINK TO polymarket telemetry bus', | |
| '> AUTH PROXY ONLINE (gamma + clob)', | |
| '> ENGAGING FACTOR ENGINE', | |
| '> CALIBRATING TELEMETRY BUS', | |
| '> SYSTEM NOMINAL', | |
| ]; | |
| let i = 0; | |
| const id = setInterval(()=>{ | |
| if (i<seq.length){ addLog(seq[i++]); } | |
| else { clearInterval(id); setBooted(true); } | |
| }, 220); | |
| return ()=>clearInterval(id); | |
| }, []); | |
| const loadMarkets = async (cat) => { | |
| setLoading(true); | |
| addLog(`QUERY :: ${cat}`); | |
| try{ | |
| const cfg = CATEGORIES[cat]; | |
| const events = await fetchEvents(cfg, 40); | |
| let rows = flattenMarkets(events, cfg.hot).slice(0, 30); | |
| setMarkets(rows); | |
| setMarketIdx(0); | |
| addLog(`RECEIVED ${events.length} EVENTS :: ${rows.length} MARKETS`); | |
| } catch(e){ addLog(`ERR: ${e.message}`, 'err'); } | |
| setLoading(false); | |
| }; | |
| useEffect(()=>{ if (booted) loadMarkets(category); }, [category, booted]); | |
| const analyze = async () => { | |
| const m = markets[marketIdx]; | |
| if (!m) return; | |
| setAnalyzing(true); | |
| addLog(`TARGET LOCK :: ${m.question.slice(0,54).toUpperCase()}`); | |
| try{ | |
| let h = await fetchHistory(m.token_yes, intv, fidelity); | |
| if (h.length < 20 && intv !== 'max') { | |
| addLog(`SPARSE HISTORY @ ${intv}, EXPANDING TO MAX`); | |
| h = await fetchHistory(m.token_yes, 'max', 720); | |
| } | |
| setHistory(h); | |
| addLog(`PULLED ${h.length} SAMPLES`); | |
| const sibs = markets.filter(x => x.event === m.event && x.question !== m.question).slice(0, 3); | |
| const sibHist = []; | |
| for (const s of sibs){ | |
| try { | |
| let sh = await fetchHistory(s.token_yes, intv, fidelity); | |
| if (sh.length < 20 && intv !== 'max') sh = await fetchHistory(s.token_yes, 'max', 720); | |
| if (sh.length) sibHist.push(sh); | |
| } catch {} | |
| } | |
| addLog(`SIBLINGS RESOLVED :: ${sibHist.length}/${sibs.length}`); | |
| const f = computeFactors(h, sibHist); | |
| setFactors(f); | |
| if (f.error) addLog(`FACTOR ENGINE :: ${f.error}`, 'err'); | |
| else { | |
| addLog(`β MOMENTUM :: ${f.momentum.label}`); | |
| addLog(`β MEAN REV :: ${f.meanRev.label}`); | |
| addLog(`β VOL REGIME :: ${f.volRegime.label}`); | |
| addLog(`β CORR :: ${f.corr.label}`); | |
| } | |
| } catch(e){ addLog(`ERR: ${e.message}`, 'err'); } | |
| setAnalyzing(false); | |
| }; | |
| useEffect(()=>{ if (markets.length && booted) analyze(); }, [markets, marketIdx, intv]); | |
| const current = markets[marketIdx]; | |
| const lastP = history.length ? history[history.length-1].p : null; | |
| const firstP = history.length ? history[0].p : null; | |
| const changePp = (lastP!==null && firstP!==null) ? (lastP-firstP)*100 : 0; | |
| const changeCls = Math.abs(changePp)<0.5 ? 'flat' : (changePp>=0?'up':'down'); | |
| return html` | |
| <div class="app"> | |
| <div class="header"> | |
| <div> | |
| <h1>β’ POLYMARKET CONTROL DECK β£</h1> | |
| <div class="sub">FACTOR ENGINE // TELEMETRY LINK ${loading||analyzing?'// BUSY':'// STANDBY'} // UPLINK NOMINAL // NES PROJECT ML IN B DENIS POKROVSKY</div> | |
| </div> | |
| <div><${Clock}/></div> | |
| </div> | |
| <div class="panel" style=${{marginBottom:'16px'}}> | |
| <div class="label">TARGET SELECTION</div> | |
| <div class="control-row"> | |
| <div class="ctrl"> | |
| <label>CATEGORY</label> | |
| <select value=${category} onChange=${e=>setCategory(e.currentTarget.value)}> | |
| ${Object.keys(CATEGORIES).map(k=>html`<option value=${k}>${k}</option>`)} | |
| </select> | |
| </div> | |
| <div class="ctrl" style=${{minWidth:0}}> | |
| <label>MARKET ${loading?html`<span class="spinner"/>`:''}</label> | |
| <select value=${marketIdx} onChange=${e=>setMarketIdx(parseInt(e.currentTarget.value))} disabled=${loading}> | |
| ${markets.length===0 ? html`<option>-- LOADING --</option>` : | |
| markets.map((m,i)=>html`<option value=${i}>${m.question.slice(0,90)} :: $${(m.vol_24h/1000).toFixed(0)}K/24H</option>`)} | |
| </select> | |
| </div> | |
| <div class="ctrl"> | |
| <label>INTERVAL</label> | |
| <div class="interval-group"> | |
| ${['1h','6h','1d','1w','1m','max'].map(iv=>html` | |
| <button class=${intv===iv?'active':''} onClick=${()=>setIntv(iv)}>${iv.toUpperCase()}</button> | |
| `)} | |
| </div> | |
| </div> | |
| <div class="ctrl"> | |
| <label> </label> | |
| <button class="primary" onClick=${analyze} disabled=${analyzing||!current}> | |
| ${analyzing?'SCANNING':'ACQUIRE'} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid row2"> | |
| <div class="panel reactor"> | |
| <div class="label">PRIMARY TELEMETRY</div> | |
| ${current ? html` | |
| <div style=${{padding:'8px 4px 0'}}> | |
| <div class="target-event">βΈ ${current.event}</div> | |
| <div class="target-question">${current.question}</div> | |
| <div class="price-big">${lastP!==null ? (lastP*100).toFixed(1)+'%' : '--.--'}</div> | |
| <div class="price-change ${changeCls}"> | |
| ${changePp>=0?'β²':'βΌ'} ${Math.abs(changePp).toFixed(2)} PP :: PERIOD ${intv.toUpperCase()} | |
| </div> | |
| <${Sparkline} data=${history}/> | |
| <div class="meta-row"> | |
| <div class="item"><div class="key">24H VOL</div><div class="val">$${(current.vol_24h/1000).toFixed(0)}K</div></div> | |
| <div class="item"><div class="key">TOTAL VOL</div><div class="val">$${(current.vol_total/1e6).toFixed(2)}M</div></div> | |
| <div class="item"><div class="key">LIQUIDITY</div><div class="val">$${(current.liquidity/1000).toFixed(0)}K</div></div> | |
| <div class="item"><div class="key">SAMPLES</div><div class="val">${history.length}</div></div> | |
| <div class="item"><div class="key">ENDS</div><div class="val">${current.end_date?current.end_date.slice(0,10):'--'}</div></div> | |
| </div> | |
| </div> | |
| ` : html`<div class="standby">β STANDBY β</div>`} | |
| </div> | |
| <div class="panel"> | |
| <div class="label">FACTOR ANALYSIS</div> | |
| ${factors && !factors.error ? html` | |
| <div class="factor-grid"> | |
| <${FactorTile} | |
| name="β MOMENTUM" | |
| status=${factors.momentum.label} | |
| metric=${`24H z=${(factors.momentum.z24??NaN).toFixed(2)} Β· 7D z=${(factors.momentum.z7??NaN).toFixed(2)}`} | |
| hot=${['STRONG UP','UP'].includes(factors.momentum.label)} | |
| alert=${['STRONG DOWN','DOWN'].includes(factors.momentum.label)}/> | |
| <${FactorTile} | |
| name="β MEAN REVERSION" | |
| status=${factors.meanRev.label} | |
| metric=${`H=${(factors.meanRev.hurst??NaN).toFixed(2)} Β· Οβ=${(factors.meanRev.a1??NaN).toFixed(2)}`} | |
| hot=${factors.meanRev.label==='MEAN REVERTING'}/> | |
| <${FactorTile} | |
| name="β VOL REGIME" | |
| status=${factors.volRegime.label} | |
| metric=${`Ο24h/Ο7d = ${(factors.volRegime.ratio??NaN).toFixed(2)}`} | |
| alert=${factors.volRegime.label==='HIGH VOL'}/> | |
| <${FactorTile} | |
| name="β CORR BREAK" | |
| status=${factors.corr.label} | |
| metric=${factors.corr.delta!==null && factors.corr.delta!==undefined | |
| ? `base=${factors.corr.baseline.toFixed(2)} now=${factors.corr.recent.toFixed(2)} Ξ=${factors.corr.delta.toFixed(2)}` | |
| : 'NEEDS SIBLINGS'} | |
| alert=${factors.corr.label==='CORR BREAK'}/> | |
| <div class="gauge-wrap"> | |
| <div class="title">β MOMENTUM GAUGE // 24H Z-SCORE</div> | |
| <${Gauge} value=${factors.momentum.z24}/> | |
| </div> | |
| </div> | |
| ` : html`<div class="standby">${factors?.error || 'β AWAITING TELEMETRY β'}</div>`} | |
| </div> | |
| </div> | |
| <div class="panel log-panel"> | |
| <div class="label">SYSTEM LOG</div> | |
| <div class="log" ref=${logRef}> | |
| ${log.slice(-20).map((e,i,arr)=>html` | |
| <div class="entry ${i===arr.length-1?'new':''} ${e.type}" key=${e.id}> | |
| <span class="ts">[${e.ts}]</span>${e.msg} | |
| </div> | |
| `)} | |
| </div> | |
| </div> | |
| <div class="footer"> | |
| β’ POLYMARKET CONTROL DECK β£ // DATA FROM gamma-api.polymarket.com & clob.polymarket.com // READ-ONLY // NO TRADING | |
| </div> | |
| </div> | |
| `; | |
| } | |
| render(html`<${App}/>`, document.getElementById('app')); | |
| </script> | |
| </body> | |
| </html> | |