polyserve / index.html
pentarosarium's picture
Upload index.html
6155b47 verified
<!DOCTYPE html>
<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>&nbsp;</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 &amp; clob.polymarket.com // READ-ONLY // NO TRADING
</div>
</div>
`;
}
render(html`<${App}/>`, document.getElementById('app'));
</script>
</body>
</html>