2026-06-21 16:19:34 -04:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" />
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" />
2026-06-27 15:29:34 -04:00
< title > Polymarket Analyst — AI Market Intelligence & Paper Trader</ title >
< meta name = "description" content = "Daily AI analysis of Polymarket prediction markets with an autonomous paper-trading agent. Runs entirely in your browser." />
< 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=Inter:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&display=swap" rel = "stylesheet" >
< link rel = "icon" href = "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📊</text></svg>" >
2026-06-21 16:19:34 -04:00
< style >
: root {
2026-06-27 15:29:34 -04:00
--bg : #070b14 ; --bg2 : #0b1120 ; --panel : rgba ( 22 , 29 , 46 , .66 ); --panel-solid : #161d2e ;
--panel-2 : rgba ( 36 , 45 , 68 , .6 ); --border : rgba ( 255 , 255 , 255 , .09 ); --border-strong : rgba ( 255 , 255 , 255 , .16 );
--text : #eef2f9 ; --muted : #94a1bb ; --green : #34d399 ; --red : #fb7185 ; --yellow : #fbbf24 ;
--accent : #7c8cff ; --accent2 : #a878ff ; --cyan : #38d2e6 ;
--shadow : 0 18 px 50 px -20 px rgba ( 0 , 0 , 0 , .7 );
2026-06-21 16:19:34 -04:00
}
* { box-sizing : border-box }
2026-06-27 15:29:34 -04:00
html { scroll-behavior : smooth }
body { margin : 0 ; font-family : 'Inter' , - apple-system , BlinkMacSystemFont , "Segoe UI" , Roboto , sans-serif ;
background : var ( -- bg ); color : var ( -- text ); line-height : 1.5 ; -webkit- font-smoothing : antialiased ;
min-height : 100 vh ; overflow-x : hidden }
h1 , h2 , h3 , . brand-name { font-family : 'Space Grotesk' , 'Inter' , sans-serif }
/* ---------- layered background imagery ---------- */
. bg { position : fixed ; inset : 0 ; z-index : -2 ; overflow : hidden ;
background :
radial-gradient ( 1200 px 700 px at 80 % -10 % , rgba ( 124 , 140 , 255 , .18 ), transparent 60 % ),
radial-gradient ( 900 px 600 px at 5 % 10 % , rgba ( 168 , 120 , 255 , .16 ), transparent 55 % ),
radial-gradient ( 1000 px 800 px at 50 % 110 % , rgba ( 56 , 210 , 230 , .12 ), transparent 60 % ),
linear-gradient ( 180 deg , #070b14 , #0a1020 40 % , #070b14 )}
. bg :: before { content : "" ; position : absolute ; inset : 0 ; opacity : .5 ;
background-image : url ( "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M44 0H0V44' fill='none' stroke='%23ffffff' stroke-opacity='0.035' stroke-width='1'/%3E%3C/svg%3E" )}
. bg . orb { position : absolute ; border-radius : 50 % ; filter : blur ( 70 px ); opacity : .55 ; animation : float 18 s ease-in-out infinite }
. orb . a { width : 480 px ; height : 480 px ; left : -120 px ; top : -80 px ; background : radial-gradient ( circle , #5b6cff , transparent 70 % )}
. orb . b { width : 520 px ; height : 520 px ; right : -140 px ; top : 140 px ; background : radial-gradient ( circle , #a878ff , transparent 70 % ); animation-delay : -6 s }
. orb . c { width : 440 px ; height : 440 px ; left : 35 % ; bottom : -180 px ; background : radial-gradient ( circle , #21b6c9 , transparent 70 % ); animation-delay : -12 s }
@ keyframes float { 0 %, 100 % { transform : translate ( 0 , 0 )} 50 % { transform : translate ( 40 px , -30 px )}}
/* decorative skyline/chart silhouette pinned to bottom */
. bg . chartline { position : absolute ; left : 0 ; right : 0 ; bottom : 0 ; width : 100 % ; height : 240 px ; opacity : .5 }
/* ---------- app shell ---------- */
. app { max-width : 1240 px ; margin : 0 auto ; padding : 0 22 px 60 px }
. topnav { position : sticky ; top : 0 ; z-index : 50 ; display : flex ; align-items : center ; gap : 18 px ; flex-wrap : wrap ;
padding : 14 px 18 px ; margin : 14 px 0 8 px ; border : 1 px solid var ( -- border ); border-radius : 18 px ;
background : rgba ( 10 , 15 , 28 , .72 ); backdrop-filter : blur ( 16 px ); box-shadow : var ( -- shadow )}
. brand { display : flex ; align-items : center ; gap : 11 px }
. logo { width : 40 px ; height : 40 px ; border-radius : 12 px ; display : grid ; place-items : center ; font-size : 20 px ;
background : linear-gradient ( 135 deg , var ( -- accent ), var ( -- accent2 )); box-shadow : 0 6 px 20 px -6 px var ( -- accent )}
. brand-name { font-size : 18 px ; font-weight : 700 ; letter-spacing : -.2 px ; line-height : 1 }
. brand-sub { font-size : 11 px ; color : var ( -- muted ); margin-top : 2 px }
. tabs { display : flex ; gap : 4 px ; background : rgba ( 255 , 255 , 255 , .04 ); padding : 4 px ; border-radius : 12 px ;
border : 1 px solid var ( -- border )}
. tab { appearance : none ; border : 0 ; background : transparent ; color : var ( -- muted ); font-family : inherit ;
font-size : 13.5 px ; font-weight : 600 ; padding : 8 px 15 px ; border-radius : 9 px ; cursor : pointer ; transition : .18 s ; white-space : nowrap }
. tab : hover { color : var ( -- text )}
. tab . active { color : #fff ; background : linear-gradient ( 135 deg , var ( -- accent ), var ( -- accent2 ));
box-shadow : 0 6 px 18 px -8 px var ( -- accent )}
. nav-right { margin-left : auto ; display : flex ; align-items : center ; gap : 10 px }
. status { display : flex ; align-items : center ; gap : 7 px ; font-size : 12.5 px ; color : var ( -- muted );
padding : 6 px 11 px ; border-radius : 9 px ; background : rgba ( 255 , 255 , 255 , .04 ); border : 1 px solid var ( -- border )}
. dot { width : 8 px ; height : 8 px ; border-radius : 50 % ; background : var ( -- muted ); box-shadow : 0 0 0 0 rgba ( 52 , 211 , 153 , .5 )}
. dot . live { background : var ( -- green ); animation : pulse 1.6 s infinite }
@ keyframes pulse { 0 % { box-shadow : 0 0 0 0 rgba ( 52 , 211 , 153 , .5 )} 70 % { box-shadow : 0 0 0 7 px rgba ( 52 , 211 , 153 , 0 )} 100 % { box-shadow : 0 0 0 0 rgba ( 52 , 211 , 153 , 0 )}}
. btn { border : 1 px solid var ( -- border - strong ); border-radius : 10 px ; padding : 9 px 15 px ; font-size : 13.5 px ;
font-weight : 600 ; font-family : inherit ; cursor : pointer ; background : rgba ( 255 , 255 , 255 , .05 ); color : var ( -- text ); transition : .16 s }
. btn : hover { border-color : var ( -- accent ); transform : translateY ( -1 px )}
. btn . primary { background : linear-gradient ( 135 deg , var ( -- accent ), var ( -- accent2 )); border-color : transparent ; color : #fff ;
box-shadow : 0 8 px 22 px -10 px var ( -- accent )}
. btn . primary : hover { filter : brightness ( 1.08 )}
. btn : disabled { opacity : .55 ; cursor : wait ; transform : none }
2026-06-21 16:19:34 -04:00
. btn . ghost { background : transparent }
2026-06-27 15:29:34 -04:00
/* ---------- hero ---------- */
. hero { position : relative ; border : 1 px solid var ( -- border ); border-radius : 22 px ; padding : 34 px 32 px ; margin : 14 px 0 22 px ;
overflow : hidden ; background : linear-gradient ( 120 deg , rgba ( 124 , 140 , 255 , .16 ), rgba ( 168 , 120 , 255 , .08 ) 50 % , rgba ( 56 , 210 , 230 , .10 ));
box-shadow : var ( -- shadow )}
. hero-art { position : absolute ; right : -10 px ; top : -10 px ; bottom : -10 px ; width : 46 % ; opacity : .85 ; pointer-events : none }
. hero h1 { margin : 0 0 10 px ; font-size : 34 px ; letter-spacing : -.8 px ; line-height : 1.08 ; max-width : 640 px }
. hero h1 . grad { background : linear-gradient ( 100 deg , #a9b6ff , #d9b4ff 50 % , #8fe8f3 ); -webkit- background-clip : text ; background-clip : text ; color : transparent }
. hero p { margin : 0 ; color : #c3cce0 ; font-size : 15.5 px ; max-width : 560 px }
. hero . badges { display : flex ; gap : 9 px ; flex-wrap : wrap ; margin-top : 18 px }
. hbadge { font-size : 12 px ; font-weight : 600 ; color : #dfe6f5 ; padding : 6 px 12 px ; border-radius : 999 px ;
background : rgba ( 255 , 255 , 255 , .07 ); border : 1 px solid var ( -- border )}
/* ---------- panels & cards ---------- */
. tabpanel { display : none ; animation : fade .35 s ease }
. tabpanel . active { display : block }
@ keyframes fade { from { opacity : 0 ; transform : translateY ( 8 px )} to { opacity : 1 ; transform : none }}
. grid2 { display : grid ; grid-template-columns : 1.05 fr .95 fr ; gap : 20 px }
@ media ( max-width : 920px ) {. grid2 { grid-template-columns : 1 fr }}
. card { background : var ( -- panel ); border : 1 px solid var ( -- border ); border-radius : 18 px ; padding : 20 px 22 px ;
margin-bottom : 20 px ; backdrop-filter : blur ( 14 px ); box-shadow : var ( -- shadow )}
. card . hoverable { transition : .2 s }
. card . hoverable : hover { border-color : var ( -- border - strong ); transform : translateY ( -2 px )}
. card-h { display : flex ; align-items : center ; justify-content : space-between ; margin : 0 0 16 px }
. card-h h3 { margin : 0 ; font-size : 13 px ; color : var ( -- muted ); text-transform : uppercase ; letter-spacing : 1 px ; font-weight : 600 }
. section-title { display : flex ; align-items : center ; gap : 11 px ; font-size : 20 px ; margin : 6 px 0 16 px ; font-weight : 700 }
. section-title :: before { content : "" ; width : 5 px ; height : 22 px ; border-radius : 3 px ; background : linear-gradient ( var ( -- accent ), var ( -- accent2 ))}
. muted { color : var ( -- muted )}. small { font-size : 12.5 px }
/* ---------- stat cards ---------- */
. stats { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 165 px , 1 fr )); gap : 14 px ; margin-bottom : 20 px }
. stat { position : relative ; background : var ( -- panel ); border : 1 px solid var ( -- border ); border-radius : 16 px ;
padding : 18 px ; overflow : hidden ; backdrop-filter : blur ( 14 px ); transition : .2 s }
. stat : hover { transform : translateY ( -2 px ); border-color : var ( -- border - strong )}
. stat :: after { content : "" ; position : absolute ; left : 0 ; top : 0 ; height : 3 px ; width : 100 % ;
background : linear-gradient ( 90 deg , var ( -- accent ), var ( -- accent2 ))}
. stat . ic { font-size : 18 px ; opacity : .9 }
. stat . label { color : var ( -- muted ); font-size : 11.5 px ; text-transform : uppercase ; letter-spacing : .6 px ; margin-top : 8 px }
. stat . value { font-size : 27 px ; font-weight : 700 ; margin-top : 4 px ; font-family : 'Space Grotesk' , sans-serif ; letter-spacing : -.5 px }
. pos-val { color : var ( -- green )}. neg-val { color : var ( -- red )}
/* ---------- equity chart ---------- */
# chart { width : 100 % ; height : 230 px ; display : block }
. chart-meta { display : flex ; justify-content : space-between ; font-size : 12 px ; color : var ( -- muted ); margin-top : 10 px }
/* ---------- suggestions ---------- */
. filterbar { display : flex ; gap : 8 px ; flex-wrap : wrap ; margin-bottom : 8 px }
. chip { font-size : 13 px ; font-weight : 600 ; padding : 7 px 14 px ; border-radius : 999 px ; cursor : pointer ;
background : rgba ( 255 , 255 , 255 , .05 ); border : 1 px solid var ( -- border ); color : var ( -- muted ); transition : .16 s ; user-select : none }
2026-06-21 16:19:34 -04:00
. chip : hover { border-color : var ( -- accent ); color : var ( -- text )}
2026-06-27 15:29:34 -04:00
. chip . active { background : linear-gradient ( 135 deg , var ( -- accent ), var ( -- accent2 )); border-color : transparent ; color : #fff }
. chip . ct { opacity : .75 ; font-size : 11 px ; margin-left : 5 px }
. sug-grid { display : grid ; grid-template-columns : 1 fr 1 fr ; gap : 16 px }
@ media ( max-width : 760px ) {. sug-grid { grid-template-columns : 1 fr }}
. sug { position : relative ; background : var ( -- panel ); border : 1 px solid var ( -- border ); border-radius : 16 px ; padding : 18 px ;
border-left : 4 px solid var ( -- accent ); backdrop-filter : blur ( 14 px ); transition : .2 s }
. sug : hover { transform : translateY ( -3 px ); border-color : var ( -- border - strong ); box-shadow : var ( -- shadow )}
. sug . q { font-weight : 600 ; font-size : 15 px ; line-height : 1.35 ; margin-top : 6 px }
. sug . event { color : var ( -- muted ); font-size : 12 px ; margin : 3 px 0 12 px }
. sug-row { display : flex ; gap : 12 px ; flex-wrap : wrap ; align-items : center ; margin-bottom : 9 px }
. cat-badge { font-size : 10 px ; font-weight : 700 ; text-transform : uppercase ; letter-spacing : .5 px ; padding : 3 px 9 px ; border-radius : 7 px }
. pill { font-size : 12 px ; font-weight : 700 ; padding : 4 px 11 px ; border-radius : 999 px }
. pill . YES { background : rgba ( 52 , 211 , 153 , .16 ); color : var ( -- green )}
. pill . NO { background : rgba ( 251 , 113 , 133 , .16 ); color : var ( -- red )}
. conv-bar { flex : 1 ; min-width : 90 px ; height : 7 px ; background : rgba ( 255 , 255 , 255 , .07 ); border-radius : 999 px ; overflow : hidden }
. conv-bar > span { display : block ; height : 100 % ; background : linear-gradient ( 90 deg , var ( -- accent ), var ( -- green ))}
. rationale { font-size : 13 px ; color : #cdd6e8 ; margin : 10 px 0 }
. drivers { display : flex ; gap : 6 px ; flex-wrap : wrap }
. driver { font-size : 11 px ; background : rgba ( 255 , 255 , 255 , .05 ); border : 1 px solid var ( -- border ); padding : 3 px 9 px ; border-radius : 7 px ; color : var ( -- muted )}
. metrics { display : flex ; gap : 16 px ; font-size : 12 px ; color : var ( -- muted ); margin-top : 12 px ; flex-wrap : wrap }
. metrics b { color : var ( -- text )}
a . market-link { color : var ( -- accent ); text-decoration : none ; font-size : 12 px ; font-weight : 600 }
a . market-link : hover { text-decoration : underline }
/* ---------- positions / log ---------- */
. pos { display : flex ; justify-content : space-between ; align-items : center ; padding : 13 px 0 ; border-bottom : 1 px solid var ( -- border )}
. pos : last-child { border-bottom : none }
. pos . pq { font-size : 13.5 px ; max-width : 62 % ; font-weight : 500 }
. pos . sub { color : var ( -- muted ); font-size : 11.5 px ; margin-top : 3 px }
. right { text-align : right }
. log-item { font-size : 12.5 px ; padding : 9 px 0 ; border-bottom : 1 px solid var ( -- border ); color : #cdd6e8 }
. log-item : last-child { border-bottom : none }
. badge { font-weight : 700 ; font-size : 10 px ; padding : 2 px 7 px ; border-radius : 5 px ; margin-right : 7 px }
. badge . OPEN { background : rgba ( 124 , 140 , 255 , .2 ); color : var ( -- accent )}
. badge . CLOSE { background : rgba ( 251 , 191 , 36 , .2 ); color : var ( -- yellow )}
. log-date { color : var ( -- muted )}
. empty { color : var ( -- muted ); font-size : 13.5 px ; padding : 14 px 0 ; text-align : center }
/* ---------- about ---------- */
. about-grid { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 230 px , 1 fr )); gap : 16 px ; margin-bottom : 8 px }
. feature { background : var ( -- panel ); border : 1 px solid var ( -- border ); border-radius : 16 px ; padding : 20 px ; backdrop-filter : blur ( 14 px )}
. feature . fic { width : 42 px ; height : 42 px ; border-radius : 12 px ; display : grid ; place-items : center ; font-size : 20 px ; margin-bottom : 12 px ;
background : linear-gradient ( 135 deg , rgba ( 124 , 140 , 255 , .25 ), rgba ( 168 , 120 , 255 , .18 )); border : 1 px solid var ( -- border )}
. feature h4 { margin : 0 0 7 px ; font-size : 15 px }
. feature p { margin : 0 ; font-size : 13 px ; color : var ( -- muted ); line-height : 1.55 }
. steps { counter-reset : s ; list-style : none ; padding : 0 ; margin : 0 }
. steps li { position : relative ; padding : 0 0 16 px 44 px ; font-size : 14 px ; color : #cdd6e8 }
. steps li :: before { counter-increment : s ; content : counter ( s ); position : absolute ; left : 0 ; top : -2 px ; width : 30 px ; height : 30 px ;
border-radius : 9 px ; display : grid ; place-items : center ; font-weight : 700 ; font-size : 13 px ; color : #fff ;
background : linear-gradient ( 135 deg , var ( -- accent ), var ( -- accent2 ))}
. disclaimer { border : 1 px solid rgba ( 251 , 191 , 36 , .3 ); background : rgba ( 251 , 191 , 36 , .07 ); border-radius : 14 px ; padding : 16 px 18 px ;
font-size : 13 px ; color : #e6dcc2 ; margin-top : 8 px }
/* ---------- misc ---------- */
. toast { position : fixed ; bottom : 24 px ; right : 24 px ; z-index : 100 ; background : rgba ( 18 , 24 , 40 , .92 ); border : 1 px solid var ( -- accent );
padding : 13 px 19 px ; border-radius : 13 px ; font-size : 14 px ; opacity : 0 ; transform : translateY ( 12 px ); transition : .28 s ;
pointer-events : none ; max-width : 360 px ; backdrop-filter : blur ( 12 px ); box-shadow : var ( -- shadow )}
. toast . show { opacity : 1 ; transform : translateY ( 0 )}
footer { margin-top : 34 px ; padding-top : 22 px ; border-top : 1 px solid var ( -- border ); color : var ( -- muted ); font-size : 12.5 px ; text-align : center }
:: -webkit-scrollbar { width : 11 px ; height : 11 px }
:: -webkit-scrollbar-thumb { background : rgba ( 255 , 255 , 255 , .12 ); border-radius : 6 px ; border : 3 px solid transparent ; background-clip : padding-box }
:: -webkit-scrollbar-thumb : hover { background : rgba ( 255 , 255 , 255 , .2 ); background-clip : padding-box }
2026-06-21 16:19:34 -04:00
</ style >
</ head >
< body >
2026-06-27 15:29:34 -04:00
< div class = "bg" aria-hidden = "true" >
< div class = "orb a" ></ div >< div class = "orb b" ></ div >< div class = "orb c" ></ div >
< svg class = "chartline" viewBox = "0 0 1440 240" preserveAspectRatio = "none" xmlns = "http://www.w3.org/2000/svg" >
< defs >< linearGradient id = "cl" x1 = "0" y1 = "0" x2 = "0" y2 = "1" >
< stop offset = "0%" stop-color = "#7c8cff" stop-opacity = "0.28" />< stop offset = "100%" stop-color = "#7c8cff" stop-opacity = "0" />
</ linearGradient ></ defs >
< path d = "M0,170 C120,150 180,110 300,120 C420,130 470,70 600,90 C720,108 800,40 940,70 C1080,100 1160,150 1280,120 C1360,100 1410,130 1440,120 L1440,240 L0,240 Z" fill = "url(#cl)" />
< path d = "M0,170 C120,150 180,110 300,120 C420,130 470,70 600,90 C720,108 800,40 940,70 C1080,100 1160,150 1280,120 C1360,100 1410,130 1440,120" fill = "none" stroke = "#7c8cff" stroke-opacity = "0.5" stroke-width = "2" />
</ svg >
</ div >
2026-06-21 16:19:34 -04:00
2026-06-27 15:29:34 -04:00
< div class = "app" >
< nav class = "topnav" >
< div class = "brand" >
< div class = "logo" > 📊</ div >
< div >< div class = "brand-name" > Polymarket Analyst</ div >< div class = "brand-sub" > AI market intelligence · paper trader</ div ></ div >
2026-06-21 16:19:34 -04:00
</ div >
2026-06-27 15:29:34 -04:00
< div class = "tabs" id = "tabs" >
< button class = "tab" data-tab = "overview" > Overview</ button >
< button class = "tab" data-tab = "suggestions" > Suggestions</ button >
< button class = "tab" data-tab = "portfolio" > Portfolio</ button >
< button class = "tab" data-tab = "about" > About</ button >
</ div >
< div class = "nav-right" >
< span class = "status" >< span class = "dot" id = "statusDot" ></ span >< span id = "statusText" > idle</ span ></ span >
< button id = "runBtn" class = "btn primary" > Run cycle</ button >
< button id = "resetBtn" class = "btn ghost" > Reset</ button >
</ div >
</ nav >
2026-06-21 16:19:34 -04:00
2026-06-27 15:29:34 -04:00
<!-- ============ OVERVIEW ============ -->
< section class = "tabpanel" data-tab = "overview" >
< div class = "hero" >
< svg class = "hero-art" viewBox = "0 0 400 300" xmlns = "http://www.w3.org/2000/svg" aria-hidden = "true" >
< defs >< linearGradient id = "ha" x1 = "0" y1 = "1" x2 = "1" y2 = "0" >
< stop offset = "0" stop-color = "#7c8cff" stop-opacity = ".0" />< stop offset = "1" stop-color = "#a878ff" stop-opacity = ".5" /></ linearGradient ></ defs >
< g fill = "none" stroke = "#a9b6ff" stroke-opacity = ".5" stroke-width = "2" >
< path d = "M20,250 L120,170 L200,210 L290,90 L380,40" />
</ g >
< path d = "M20,250 L120,170 L200,210 L290,90 L380,40 L380,290 L20,290 Z" fill = "url(#ha)" opacity = ".5" />
< g fill = "#d9b4ff" >< circle cx = "120" cy = "170" r = "5" />< circle cx = "200" cy = "210" r = "5" />< circle cx = "290" cy = "90" r = "5" />< circle cx = "380" cy = "40" r = "6" /></ g >
< g opacity = ".6" >
< rect x = "60" y = "200" width = "14" height = "50" rx = "3" fill = "#34d399" />
< rect x = "160" y = "220" width = "14" height = "40" rx = "3" fill = "#fb7185" />
< rect x = "250" y = "150" width = "14" height = "80" rx = "3" fill = "#34d399" />
< rect x = "330" y = "120" width = "14" height = "60" rx = "3" fill = "#34d399" />
</ g >
</ svg >
< h1 > Trade smarter on < span class = "grad" > prediction markets</ span ></ h1 >
< p > Every day, an AI scores live Polymarket markets for mispricing, momentum and liquidity — then an autonomous agent paper-trades the best ideas and tracks its own P& L.</ p >
< div class = "badges" >
< span class = "hbadge" > 🔴 Live Polymarket data</ span >
< span class = "hbadge" > 🧠 Rule-based AI scoring</ span >
< span class = "hbadge" > 🤖 Autonomous paper agent</ span >
< span class = "hbadge" > 🗂️ Category filters</ span >
</ div >
</ div >
< div class = "stats" id = "stats" ></ div >
< div class = "grid2" >
< div class = "card hoverable" >
< div class = "card-h" >< h3 > Equity Curve</ h3 >< span class = "small muted" id = "lastRun" ></ span ></ div >
< svg id = "chart" viewBox = "0 0 600 230" preserveAspectRatio = "none" ></ svg >
< div class = "chart-meta" id = "chartMeta" ></ div >
</ div >
< div class = "card hoverable" >
< div class = "card-h" >< h3 > Top Picks Today</ h3 >< span class = "small muted" > highest conviction</ span ></ div >
< div id = "topPicks" ></ div >
</ div >
</ div >
</ section >
<!-- ============ SUGGESTIONS ============ -->
< section class = "tabpanel" data-tab = "suggestions" >
< div class = "section-title" > 💡 Daily Suggestions < span class = "small muted" id = "sugDate" style = "font-weight:400" ></ span ></ div >
< div class = "card" >
< div class = "filterbar" id = "filterbar" ></ div >
< div class = "small muted" id = "focusNote" style = "margin-top:6px" ></ div >
</ div >
< div class = "sug-grid" id = "suggestions" ></ div >
</ section >
<!-- ============ PORTFOLIO ============ -->
< section class = "tabpanel" data-tab = "portfolio" >
< div class = "section-title" > 🤖 Agent Portfolio</ div >
< div class = "stats" id = "statsPf" ></ div >
< div class = "grid2" >
< div class = "card" >< div class = "card-h" >< h3 > Open Positions</ h3 ></ div >< div id = "positions" ></ div ></ div >
< div class = "card" >< div class = "card-h" >< h3 > Activity Log</ h3 ></ div >< div id = "history" ></ div ></ div >
</ div >
</ section >
<!-- ============ ABOUT ============ -->
< section class = "tabpanel" data-tab = "about" >
< div class = "section-title" > ℹ ️ How it works</ div >
< div class = "about-grid" >
< div class = "feature" >< div class = "fic" > 🔴</ div >< h4 > Live market data</ h4 >< p > Pulls the most active open binary markets straight from Polymarket's public Gamma API — no keys, no backend.</ p ></ div >
< div class = "feature" >< div class = "fic" > 🧮</ div >< h4 > Four-signal model</ h4 >< p > Each market is scored on liquidity, 24h momentum, a fair-value mispricing estimate, and time-to-resolution.</ p ></ div >
< div class = "feature" >< div class = "fic" > 🤖</ div >< h4 > Autonomous agent</ h4 >< p > Sizes positions with a capped fractional-Kelly rule, marks them to market daily, and settles resolved ones.</ p ></ div >
< div class = "feature" >< div class = "fic" > 🗂️</ div >< h4 > Category focus</ h4 >< p > Filter Politics, Sports, Crypto, Economy & more — and steer the agent to trade only your chosen category.</ p ></ div >
</ div >
< div class = "card" style = "margin-top:16px" >
< div class = "card-h" >< h3 > The daily cycle</ h3 ></ div >
< ol class = "steps" >
< li >< b > Fetch</ b > — page through hundreds of live markets and tag each by category.</ li >
< li >< b > Score</ b > — rank every market 0– 100 by conviction and estimate its edge vs. a fair-value model.</ li >
< li >< b > Suggest</ b > — surface the strongest picks per category with a plain-English rationale.</ li >
< li >< b > Trade</ b > — the agent opens sized paper positions, marks existing ones to market, and snapshots equity.</ li >
</ ol >
</ div >
< div class = "disclaimer" > ⚠️ < b > Paper trading only — not financial advice.</ b > Nothing here places real orders or moves money. The analysis is a transparent heuristic. Your portfolio lives in this browser's local storage (each device keeps its own).</ div >
</ section >
< footer >
Paper trading only · Live prices from Polymarket's public Gamma API · Not financial advice ·
< a class = "market-link" href = "https://github.com/theodore-song/polymarket-analyst" target = "_blank" rel = "noopener" > Source on GitHub</ a >
</ footer >
</ div >
2026-06-21 16:19:34 -04:00
< script >
"use strict" ;
/* ============================================================
Polymarket Analyst — fully client-side.
Fetches live markets, scores them, and runs a paper-trading
agent. State persists in localStorage. No backend needed.
============================================================ */
const GAMMA = "https://gamma-api.polymarket.com" ;
const STARTING_BALANCE = 10000 ;
const STORE_KEY = "pma_portfolio_v1" ;
const SUG_KEY = "pma_suggestions_v1" ;
const FOCUS_KEY = "pma_focus_v1" ;
const CATEGORIES = [ "All" , "Politics" , "Sports" , "Crypto" , "Economy" , "Pop Culture" , "Other" ];
2026-06-27 15:29:34 -04:00
const CAT_COLORS = { Politics : "#fb7185" , Sports : "#34d399" , Crypto : "#fbbf24" , Economy : "#7c8cff" ,
"Pop Culture" : "#c77dff" , Other : "#94a1bb" , All : "#7c8cff" };
const catColor = ( c ) => CAT_COLORS [ c ] || CAT_COLORS . Other ;
2026-06-21 16:19:34 -04:00
const getFocus = () => localStorage . getItem ( FOCUS_KEY ) || "All" ;
const setFocus = ( c ) => localStorage . setItem ( FOCUS_KEY , c );
/* ---------- small utils ---------- */
const $ = ( id ) => document . getElementById ( id );
const fmtUSD = ( n ) => "$" + Number ( n ). toLocaleString ( "en-US" ,{ minimumFractionDigits : 2 , maximumFractionDigits : 2 });
const fmtPct = ( n ) => ( n >= 0 ? "+" : "" ) + Number ( n ). toFixed ( 2 ) + "%" ;
const signClass = ( n ) => ( n > 0 ? "pos-val" : n < 0 ? "neg-val" : "" );
const todayStr = () => new Date (). toISOString (). slice ( 0 , 10 );
const nowIso = () => new Date (). toISOString ();
function toast ( msg ){
let t = document . querySelector ( ".toast" );
if ( ! t ){ t = document . createElement ( "div" ); t . className = "toast" ; document . body . appendChild ( t );}
t . textContent = msg ; t . classList . add ( "show" );
clearTimeout ( t . _h ); t . _h = setTimeout (()=> t . classList . remove ( "show" ), 3600 );
}
function setStatus ( text , live ){
$ ( "statusText" ). textContent = text ;
$ ( "statusDot" ). classList . toggle ( "live" , !! live );
}
/* ---------- Polymarket client ---------- */
function parseJsonField ( v ){
if ( v == null ) return [];
if ( Array . isArray ( v )) return v ;
try { return JSON . parse ( v );} catch ( e ){ return [];}
}
function toNum ( v , d = 0 ){ const n = Number ( v ); return Number . isFinite ( n ) ? n : d ;}
function daysUntil ( iso ){
if ( ! iso ) return null ;
const dt = new Date ( iso );
if ( isNaN ( dt )) return null ;
return ( dt - new Date ()) / 86400000 ;
}
2026-06-27 15:29:34 -04:00
/* Map Polymarket tag slugs/labels to a top-level category. */
2026-06-21 16:19:34 -04:00
const CATEGORY_RULES = [
[ "Politics" ,[ "politics" , "election" , "elections" , "us-politics" , "geopolitics" , "trump" ,
"world-leaders" , "government" , "congress" , "policy" , "biden" , "2026-election" , "democrats" , "republicans" ]],
[ "Crypto" ,[ "crypto" , "bitcoin" , "ethereum" , "btc" , "eth" , "solana" , "memecoins" , "defi" , "stablecoin" , "xrp" ]],
[ "Sports" ,[ "sports" , "soccer" , "football" , "nba" , "nfl" , "mlb" , "nhl" , "tennis" , "basketball" , "baseball" ,
"ufc" , "boxing" , "cricket" , "golf" , "f1" , "fifa-world-cup" , "games" , "tournament-futures" , "epl" ]],
[ "Economy" ,[ "economy" , "business" , "fed" , "inflation" , "interest-rates" , "gdp" , "jobs" , "recession" ,
"stocks" , "earnings" , "financials" , "tariffs" ]],
[ "Pop Culture" ,[ "pop-culture" , "entertainment" , "movies" , "music" , "tv" , "awards" , "celebrity" , "gaming" , "ai" ]],
];
function classifyCategory ( tags ){
const slugs = tags . map ( t =>( t . slug || t . label || "" ). toLowerCase ());
for ( const [ cat , keys ] of CATEGORY_RULES ){
if ( slugs . some ( s => keys . includes ( s ))) return cat ;
}
return "Other" ;
}
function normalizeMarket ( raw ){
const outcomes = parseJsonField ( raw . outcomes );
const prices = parseJsonField ( raw . outcomePrices ). map ( p => toNum ( p ));
if ( outcomes . length !== 2 || prices . length !== 2 ) return null ;
if ( raw . acceptingOrders === false || raw . closed ) return null ;
const yes = prices [ 0 ];
if ( yes <= 0 || yes >= 1 ) return null ;
const events = raw . events || [];
const ev = events . length ? events [ 0 ] : {};
const tags = Array . isArray ( raw . tags ) ? raw . tags : [];
return {
category : classifyCategory ( tags ),
tags : tags . map ( t => t . label ). filter ( Boolean ). slice ( 0 , 4 ),
id : String ( raw . id ),
question : ( raw . question || "" ). trim (),
event : ( ev . title || "" ). trim (),
yes_price :+ yes . toFixed ( 4 ),
no_price :+ prices [ 1 ]. toFixed ( 4 ),
volume : toNum ( raw . volumeNum || raw . volume ),
volume_24hr : toNum ( raw . volume24hr ),
volume_1wk : toNum ( raw . volume1wk ),
liquidity : toNum ( raw . liquidityNum || raw . liquidity ),
end_date : raw . endDateIso || raw . endDate ,
days_to_resolution : daysUntil ( raw . endDate ),
image : raw . image || "" ,
url : ev . slug ? `https://polymarket.com/event/ ${ ev . slug } ` : "" ,
};
}
async function fetchMarkets ( pages = 4 , perPage = 100 ){
const all = [];
for ( let i = 0 ; i < pages ; i ++ ){
const params = new URLSearchParams ({ closed : "false" , active : "true" , archived : "false" ,
include_tag : "true" , limit : String ( perPage ), offset : String ( i * perPage ),
order : "volume24hr" , ascending : "false" });
const r = await fetch ( ` ${ GAMMA } /markets? ${ params } ` );
if ( ! r . ok ){ if ( i === 0 ) throw new Error ( "Polymarket API " + r . status ); break ;}
const raw = await r . json ();
if ( ! raw . length ) break ;
all . push (... raw );
}
return all . map ( normalizeMarket ). filter ( m => m && m . question );
}
async function fetchMarketPrice ( id ){
try {
const r = await fetch ( ` ${ GAMMA } /markets/ ${ id } ` );
if ( ! r . ok ) return null ;
return normalizeMarket ( await r . json ());
} catch ( e ){ return null ;}
}
2026-06-27 15:29:34 -04:00
/* ---------- Analysis engine ---------- */
2026-06-21 16:19:34 -04:00
const W = { LIQ : 0.25 , MOM : 0.25 , MIS : 0.30 , TIME : 0.20 };
const MIN_VOLUME = 20000 ;
const clamp = ( x , a , b )=> Math . max ( a , Math . min ( b , x ));
function liquiditySignal ( m ){
const vol = Math . min ( 1 , Math . log10 ( m . volume + 1 ) / 6.0 );
const liq = Math . min ( 1 , Math . log10 ( m . liquidity + 1 ) / 5.3 );
return 0.6 * vol + 0.4 * liq ;
}
function momentumSignal ( m ){
const dailyAvg = m . volume_1wk ? m . volume_1wk / 7 : 0 ;
if ( dailyAvg <= 0 ) return m . volume_24hr > 0 ? 0.3 : 0 ;
const ratio = m . volume_24hr / dailyAvg ;
return clamp (( ratio - 0.5 ) / 2.0 , 0 , 1 );
}
function fairValue ( m ){
const p = m . yes_price , mom = momentumSignal ( m ), liq = liquiditySignal ( m );
if ( p > 0.92 ) return Math . min ( 0.995 , p + 0.04 * liq * ( 1 - p ) * 4 );
if ( p < 0.08 ) return Math . max ( 0.005 , p - 0.20 * p );
const drift = ( mom - 0.5 ) * 0.06 ;
return clamp ( p + drift , 0.02 , 0.98 );
}
function timingSignal ( d ){
if ( d == null ) return 0.4 ;
if ( d < 1 ) return 0.1 ;
if ( d <= 3 ) return 0.5 ;
if ( d <= 90 ) return 1.0 - ( d - 3 ) / 87.0 * 0.4 ;
return 0.35 ;
}
function analyzeMarket ( m ){
if ( m . volume < MIN_VOLUME ) return null ;
const p = m . yes_price , fair = fairValue ( m ), edgeYes = fair - p , edge = Math . abs ( edgeYes );
const liq = liquiditySignal ( m ), mom = momentumSignal ( m ), timing = timingSignal ( m . days_to_resolution );
const mis = Math . min ( 1 , edge / 0.10 );
const score = W . LIQ * liq + W . MOM * mom + W . MIS * mis + W . TIME * timing ;
const conviction =+ ( score * 100 ). toFixed ( 1 );
let side , entry , rationale ;
if ( edgeYes > 0.015 ){ side = "YES" ; entry = p ;
rationale = `Model fair value ${ pct ( fair ) } vs market ${ pct ( p ) } — YES looks underpriced by ~ ${ ( edgeYes * 100 ). toFixed ( 1 ) } c.` ;}
else if ( edgeYes <- 0.015 ){ side = "NO" ; entry = m . no_price ;
rationale = `Model fair value ${ pct ( fair ) } vs market ${ pct ( p ) } — YES looks overpriced, so NO at ${ pct ( m . no_price ) } has the edge.` ;}
else { side = "HOLD" ; entry = p ;
rationale = `Price ${ pct ( p ) } is close to model fair value ${ pct ( fair ) } ; no clear edge — watch only.` ;}
const drivers = [];
if ( liq > 0.6 ) drivers . push ( "deep/liquid market" );
if ( mom > 0.6 ) drivers . push ( "strong fresh volume (24h surge)" );
if ( timing > 0.8 ) drivers . push ( "resolves in a good window" );
if ( mis > 0.4 ) drivers . push ( "notable price/value gap" );
if ( ! drivers . length ) drivers . push ( "thin signal" );
return { market_id : m . id , question : m . question , event : m . event , url : m . url ,
category : m . category , tags : m . tags ,
yes_price : p , no_price : m . no_price , fair_value :+ fair . toFixed ( 4 ), edge :+ edgeYes . toFixed ( 4 ),
side , entry_price :+ entry . toFixed ( 4 ), conviction , volume : m . volume , volume_24hr : m . volume_24hr ,
days_to_resolution : m . days_to_resolution != null ?+ m . days_to_resolution . toFixed ( 1 ) : null ,
drivers , rationale };
}
const pct = ( x )=> Math . round ( x * 100 ) + "%" ;
function generateSuggestions ( markets , perCategory = 10 ){
const actionable = markets . map ( analyzeMarket ). filter ( Boolean ). filter ( a => a . side !== "HOLD" )
. sort (( a , b )=> b . conviction - a . conviction );
const byCat = {}; const out = [];
for ( const a of actionable ){
const c = a . category || "Other" ;
byCat [ c ] = ( byCat [ c ] || 0 ) + 1 ;
if ( byCat [ c ] <= perCategory ) out . push ( a );
}
return out . sort (( a , b )=> b . conviction - a . conviction );
}
2026-06-27 15:29:34 -04:00
/* ---------- Portfolio store ---------- */
2026-06-21 16:19:34 -04:00
function defaultPortfolio (){
return { cash : STARTING_BALANCE , starting_balance : STARTING_BALANCE ,
positions : [], closed : [], history : [], snapshots : [], last_run : null };
}
function loadPortfolio (){
try { const s = localStorage . getItem ( STORE_KEY ); return s ? JSON . parse ( s ) : defaultPortfolio ();}
catch ( e ){ return defaultPortfolio ();}
}
function savePortfolio ( p ){ localStorage . setItem ( STORE_KEY , JSON . stringify ( p ));}
function loadSuggestions (){
try { const s = localStorage . getItem ( SUG_KEY ); return s ? JSON . parse ( s ) : { date : null , suggestions : []};}
catch ( e ){ return { date : null , suggestions : []};}
}
function saveSuggestions ( sugs ){
const payload = { date : todayStr (), generated_at : nowIso (), suggestions : sugs };
localStorage . setItem ( SUG_KEY , JSON . stringify ( payload )); return payload ;
}
/* ---------- Agent ---------- */
const MAX_POS_FRAC = 0.10 , MAX_NEW_PER_DAY = 4 , MIN_CONV = 45 , CASH_FLOOR = 0.05 ;
const equity = ( p )=> p . cash + p . positions . reduce (( s , x )=> s + x . shares * x . current_price , 0 );
function kellyFraction ( conv , edge ){
const base = ( conv / 100 ) * Math . min ( 1 , Math . abs ( edge ) / 0.10 );
return Math . min ( MAX_POS_FRAC , 0.25 * base );
}
const hasPosition = ( p , id )=> p . positions . some ( x => x . market_id === id );
async function markToMarket ( p ){
const notes = [], stillOpen = [];
for ( const pos of p . positions ){
const fresh = await fetchMarketPrice ( pos . market_id );
if ( fresh == null ){
const proceeds = pos . shares * pos . current_price ;
p . cash += proceeds ; pos . exit_price = pos . current_price ; pos . closed_at = nowIso ();
pos . realized_pnl =+ ( proceeds - pos . cost ). toFixed ( 2 ); pos . reason_closed = "market resolved/closed" ;
p . closed . push ( pos );
p . history . push ({ date : todayStr (), action : "CLOSE" , market_id : pos . market_id , question : pos . question ,
side : pos . side , detail : `Settled for ${ fmtUSD ( proceeds ) } (P&L ${ fmtUSD ( pos . realized_pnl ) } )` });
notes . push ( `Closed ' ${ pos . question . slice ( 0 , 50 ) } ' (resolved)` ); continue ;
}
const price = pos . side === "YES" ? fresh . yes_price : fresh . no_price ;
pos . current_price =+ price . toFixed ( 4 );
pos . value =+ ( pos . shares * price ). toFixed ( 2 );
pos . unrealized_pnl =+ ( pos . value - pos . cost ). toFixed ( 2 );
stillOpen . push ( pos );
}
p . positions = stillOpen ; return notes ;
}
function openPositions ( p , sugs , focus ){
const notes = [];
const cands = sugs . filter ( s =>( s . side === "YES" || s . side === "NO" ) && s . conviction >= MIN_CONV
&&! hasPosition ( p , s . market_id ) && ( focus === "All" ||! focus || s . category === focus ))
. sort (( a , b )=> b . conviction - a . conviction );
let opened = 0 ;
for ( const s of cands ){
if ( opened >= MAX_NEW_PER_DAY ) break ;
const eq = equity ( p ), investable = p . cash - eq * CASH_FLOOR ;
if ( investable <= 50 ) break ;
const frac = kellyFraction ( s . conviction , s . edge );
let stake = Math . min ( eq * frac , investable );
if ( stake < 50 ) continue ;
const entry = s . entry_price ;
if ( entry <= 0 || entry >= 1 ) continue ;
const shares =+ ( stake / entry ). toFixed ( 2 ), cost =+ ( shares * entry ). toFixed ( 2 );
if ( cost > p . cash ) continue ;
p . cash =+ ( p . cash - cost ). toFixed ( 2 );
p . positions . push ({ market_id : s . market_id , question : s . question , side : s . side , shares ,
entry_price :+ entry . toFixed ( 4 ), current_price :+ entry . toFixed ( 4 ), cost , value : cost ,
2026-06-27 15:29:34 -04:00
unrealized_pnl : 0 , conviction : s . conviction , rationale : s . rationale , opened_at : nowIso (), url : s . url || "" , category : s . category });
2026-06-21 16:19:34 -04:00
p . history . push ({ date : todayStr (), action : "OPEN" , market_id : s . market_id , question : s . question , side : s . side ,
detail : `Bought ${ shares } ${ s . side } @ ${ pct ( entry ) } for ${ fmtUSD ( cost ) } (conviction ${ s . conviction } )` });
notes . push ( `Opened ${ s . side } on ' ${ s . question . slice ( 0 , 50 ) } ' ( ${ fmtUSD ( cost ) } )` );
opened ++ ;
}
return notes ;
}
function recordSnapshot ( p ){
const eq = equity ( p ), posVal =+ ( eq - p . cash ). toFixed ( 2 );
const snap = { date : todayStr (), timestamp : nowIso (), cash :+ p . cash . toFixed ( 2 ), positions_value : posVal ,
equity :+ eq . toFixed ( 2 ), pnl :+ ( eq - p . starting_balance ). toFixed ( 2 ),
return_pct :+ (( eq / p . starting_balance - 1 ) * 100 ). toFixed ( 2 ), open_positions : p . positions . length };
p . snapshots = p . snapshots . filter ( s => s . date !== snap . date );
p . snapshots . push ( snap ); p . snapshots . sort (( a , b )=> a . date < b . date ?- 1 : 1 );
}
async function runDailyCycle (){
setStatus ( "fetching markets…" , true );
const markets = await fetchMarkets ( 4 , 100 );
setStatus ( "analyzing…" , true );
const sugs = generateSuggestions ( markets , 12 );
saveSuggestions ( sugs );
const p = loadPortfolio ();
setStatus ( "agent trading…" , true );
const mtm = await markToMarket ( p );
const trades = openPositions ( p , sugs , getFocus ());
recordSnapshot ( p );
p . last_run = nowIso ();
savePortfolio ( p );
setStatus ( "up to date" , false );
return { suggestions : sugs . length , actions : mtm . concat ( trades ), equity :+ equity ( p ). toFixed ( 2 )};
}
/* ---------- Rendering ---------- */
2026-06-27 15:29:34 -04:00
function renderAll (){ renderStats (); renderChart ( loadPortfolio (). snapshots ); renderTopPicks ();
renderSuggestions (); renderPositions ( loadPortfolio (). positions );
renderHistory ( loadPortfolio (). history . slice ( - 50 ). reverse ());}
function statCards (){
const p = loadPortfolio (), eq = equity ( p ), pnl = eq - p . starting_balance ;
return [
{ ic : "💰" , label : "Equity" , value : fmtUSD ( eq )},
{ ic : "💵" , label : "Cash" , value : fmtUSD ( p . cash )},
{ ic : "📈" , label : "Total P&L" , value : fmtUSD ( pnl ), cls : signClass ( pnl )},
{ ic : "🎯" , label : "Return" , value : fmtPct (( eq / p . starting_balance - 1 ) * 100 ), cls : signClass ( pnl )},
{ ic : "📂" , label : "Open positions" , value : p . positions . length },
];
}
function renderStats (){
const html = statCards (). map ( s =>
`<div class="stat"><div class="ic"> ${ s . ic } </div><div class="label"> ${ s . label } </div>
<div class="value ${ s . cls || "" } "> ${ s . value } </div></div>` ). join ( "" );
$ ( "stats" ). innerHTML = html ;
const pf = $ ( "statsPf" ); if ( pf ) pf . innerHTML = html ;
const p = loadPortfolio ();
if ( p . last_run ){ const d = new Date ( p . last_run ); $ ( "lastRun" ). textContent = "updated " + d . toLocaleString ();}
}
function renderTopPicks (){
const root = $ ( "topPicks" ); if ( ! root ) return ;
const all = ( loadSuggestions (). suggestions ) || [];
const top = all . slice ( 0 , 4 );
if ( ! top . length ){ root . innerHTML = `<div class="empty">Run a cycle to see top picks.</div>` ; return ;}
root . innerHTML = top . map ( s => `
<div class="pos">
<div>
<div class="pq"> ${ esc ( s . question . slice ( 0 , 60 )) }${ s . question . length > 60 ? "…" : "" } </div>
<div class="sub"><span class="cat-badge" style="background: ${ catColor ( s . category ) } 22;color: ${ catColor ( s . category ) } "> ${ esc ( s . category ) } </span>
BUY ${ s . side } @ ${ Math . round ( s . entry_price * 100 ) } ¢</div>
</div>
<div class="right"><div style="font-family:'Space Grotesk';font-weight:700"> ${ Math . round ( s . conviction ) } </div>
<div class="sub">conviction</div></div>
</div>` ). join ( "" );
}
2026-06-21 16:19:34 -04:00
function renderFilterBar ( all ){
const focus = getFocus ();
const counts = {}; CATEGORIES . forEach ( c => counts [ c ] = 0 );
all . forEach ( s =>{ const c = counts [ s . category ] !== undefined ? s . category : "Other" ; counts [ c ] ++ ;});
counts [ "All" ] = all . length ;
$ ( "filterbar" ). innerHTML = CATEGORIES . map ( c =>{
const n = counts [ c ] || 0 ;
const dim = ( c !== "All" && n === 0 ) ? "opacity:.4;" : "" ;
return `<span class="chip ${ c === focus ? "active" : "" } " data-cat=" ${ c } " style=" ${ dim } "> ${ c } <span class="ct"> ${ n } </span></span>` ;
}). join ( "" );
document . querySelectorAll ( "#filterbar .chip" ). forEach ( el =>{
el . addEventListener ( "click" ,()=>{ setFocus ( el . dataset . cat ); renderSuggestions ();});
});
}
function renderSuggestions (){
const data = loadSuggestions ();
$ ( "sugDate" ). textContent = data . date ? `· ${ data . date } ` : "" ;
const all = data . suggestions || [];
const root = $ ( "suggestions" );
renderFilterBar ( all );
if ( ! all . length ){
2026-06-27 15:29:34 -04:00
root . innerHTML = `<div class="empty" style="grid-column:1/-1">No suggestions yet. Click "Run cycle".</div>` ;
2026-06-21 16:19:34 -04:00
$ ( "focusNote" ). textContent = "" ; return ;}
const focus = getFocus ();
const filtered = ( focus === "All" ? all : all . filter ( s => s . category === focus )). slice ( 0 , 12 );
$ ( "focusNote" ). textContent = focus === "All"
2026-06-27 15:29:34 -04:00
? "The agent trades the strongest picks across all categories."
2026-06-21 16:19:34 -04:00
: `Filtered to ${ focus } — the agent will only open new ${ focus } positions while this is selected.` ;
if ( ! filtered . length ){
2026-06-27 15:29:34 -04:00
root . innerHTML = `<div class="empty" style="grid-column:1/-1">No ${ esc ( focus ) } suggestions in today's batch.</div>` ; return ;}
2026-06-21 16:19:34 -04:00
root . innerHTML = filtered . map ( s =>{
2026-06-27 15:29:34 -04:00
const col = catColor ( s . category );
2026-06-21 16:19:34 -04:00
const drivers = s . drivers . map ( d => `<span class="driver"> ${ d } </span>` ). join ( "" );
const days = s . days_to_resolution != null ? ` ${ s . days_to_resolution } d` : "—" ;
2026-06-27 15:29:34 -04:00
return `<div class="sug" style="border-left-color: ${ col } ">
<div class="sug-row" style="margin-bottom:2px">
<span class="cat-badge" style="background: ${ col } 22;color: ${ col } "> ${ esc ( s . category || "Other" ) } </span>
2026-06-21 16:19:34 -04:00
</div>
<div class="q"> ${ esc ( s . question ) } </div>
<div class="event"> ${ esc ( s . event || "" ) } </div>
<div class="sug-row">
<span class="pill ${ s . side } ">BUY ${ s . side } </span>
2026-06-27 15:29:34 -04:00
<span class="muted small">@ ${ Math . round ( s . entry_price * 100 ) } ¢</span>
2026-06-21 16:19:34 -04:00
<div class="conv-bar"><span style="width: ${ s . conviction } %"></span></div>
2026-06-27 15:29:34 -04:00
<span class="muted small"> ${ Math . round ( s . conviction ) } </span>
2026-06-21 16:19:34 -04:00
</div>
<div class="rationale"> ${ esc ( s . rationale ) } </div>
<div class="drivers"> ${ drivers } </div>
<div class="metrics">
<span>Vol <b> ${ fmtUSD ( s . volume ) } </b></span>
<span>24h <b> ${ fmtUSD ( s . volume_24hr ) } </b></span>
<span>Resolves <b> ${ days } </b></span>
${ s . url ? `<a class="market-link" href=" ${ s . url } " target="_blank" rel="noopener">View ↗</a>` : "" }
</div></div>` ;
}). join ( "" );
}
function renderChart ( snaps ){
const svg = $ ( "chart" );
2026-06-27 15:29:34 -04:00
if ( ! snaps ||! snaps . length ){ svg . innerHTML = "" ; $ ( "chartMeta" ). textContent = "No history yet — run a cycle." ; return ;}
2026-06-21 16:19:34 -04:00
const pts = snaps . map ( s => s . equity );
2026-06-27 15:29:34 -04:00
const W = 600 , H = 230 , pad = 8 ;
2026-06-21 16:19:34 -04:00
const min = Math . min (... pts , snaps [ 0 ]. equity ) * 0.999 , max = Math . max (... pts ) * 1.001 , span = ( max - min ) || 1 ;
const x = ( i )=> snaps . length === 1 ? W / 2 : pad + ( i / ( snaps . length - 1 )) * ( W - 2 * pad );
const y = ( v )=> H - pad - (( v - min ) / span ) * ( H - 2 * pad );
const line = snaps . map (( s , i )=> ` ${ i === 0 ? "M" : "L" }${ x ( i ). toFixed ( 1 ) } , ${ y ( s . equity ). toFixed ( 1 ) } ` ). join ( " " );
const area = ` ${ line } L ${ x ( snaps . length - 1 ). toFixed ( 1 ) } , ${ H - pad } L ${ x ( 0 ). toFixed ( 1 ) } , ${ H - pad } Z` ;
2026-06-27 15:29:34 -04:00
const up = pts [ pts . length - 1 ] >= snaps [ 0 ]. equity , color = up ? "#34d399" : "#fb7185" ;
2026-06-21 16:19:34 -04:00
svg . innerHTML = `<defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color=" ${ color } " stop-opacity="0.35"/>
<stop offset="100%" stop-color=" ${ color } " stop-opacity="0"/></linearGradient></defs>
2026-06-27 15:29:34 -04:00
<path d=" ${ area } " fill="url(#g)"/><path d=" ${ line } " fill="none" stroke=" ${ color } " stroke-width="2.5"/>` ;
2026-06-21 16:19:34 -04:00
const last = snaps [ snaps . length - 1 ];
$ ( "chartMeta" ). innerHTML = `<span> ${ snaps [ 0 ]. date } </span><span> ${ snaps . length } day(s) · ${ fmtPct ( last . return_pct ) } </span><span> ${ last . date } </span>` ;
}
function renderPositions ( positions ){
2026-06-27 15:29:34 -04:00
const root = $ ( "positions" ); if ( ! root ) return ;
2026-06-21 16:19:34 -04:00
if ( ! positions . length ){ root . innerHTML = `<div class="empty">No open positions yet.</div>` ; return ;}
root . innerHTML = positions . map ( p =>{
2026-06-27 15:29:34 -04:00
const pnl = p . unrealized_pnl || 0 , col = catColor ( p . category );
2026-06-21 16:19:34 -04:00
return `<div class="pos"><div>
<div class="pq"> ${ esc ( p . question ) } </div>
2026-06-27 15:29:34 -04:00
<div class="sub"> ${ p . category ? `<span class="cat-badge" style="background: ${ col } 22;color: ${ col } "> ${ esc ( p . category ) } </span> ` : "" }${ p . shares } ${ p . side } @ ${ Math . round ( p . entry_price * 100 ) } ¢ → ${ Math . round ( p . current_price * 100 ) } ¢ · conv ${ p . conviction } </div>
2026-06-21 16:19:34 -04:00
</div><div class="right">
<div> ${ fmtUSD ( p . value ) } </div>
2026-06-27 15:29:34 -04:00
<div class=" ${ signClass ( pnl ) } "> ${ pnl >= 0 ? "+" : "" }${ fmtUSD ( pnl ) } </div>
2026-06-21 16:19:34 -04:00
</div></div>` ;
}). join ( "" );
}
function renderHistory ( history ){
2026-06-27 15:29:34 -04:00
const root = $ ( "history" ); if ( ! root ) return ;
2026-06-21 16:19:34 -04:00
if ( ! history . length ){ root . innerHTML = `<div class="empty">No activity yet.</div>` ; return ;}
root . innerHTML = history . map ( h =>
`<div class="log-item"><span class="badge ${ h . action } "> ${ h . action } </span><span class="log-date"> ${ h . date } </span> — ${ esc ( h . detail ) } </div>` ). join ( "" );
}
function esc ( s ){ return String ( s ). replace ( /[&<>"]/g , c =>({ "&" : "&" , "<" : "<" , ">" : ">" , '"' : """ }[ c ]));}
2026-06-27 15:29:34 -04:00
/* ---------- Tab routing (deep-linkable via #hash) ---------- */
const TABS = [ "overview" , "suggestions" , "portfolio" , "about" ];
function showTab ( name ){
if ( ! TABS . includes ( name )) name = "overview" ;
document . querySelectorAll ( ".tabpanel" ). forEach ( p => p . classList . toggle ( "active" , p . dataset . tab === name ));
document . querySelectorAll ( ".tab" ). forEach ( t => t . classList . toggle ( "active" , t . dataset . tab === name ));
if ( location . hash . slice ( 1 ) !== name ) history . replaceState ( null , "" , "#" + name );
window . scrollTo ({ top : 0 , behavior : "smooth" });
}
document . querySelectorAll ( ".tab" ). forEach ( t => t . addEventListener ( "click" ,()=> showTab ( t . dataset . tab )));
window . addEventListener ( "hashchange" ,()=> showTab ( location . hash . slice ( 1 )));
2026-06-21 16:19:34 -04:00
/* ---------- Wire up ---------- */
$ ( "runBtn" ). addEventListener ( "click" , async ()=>{
const btn = $ ( "runBtn" ); btn . disabled = true ; btn . textContent = "Running…" ;
try {
const r = await runDailyCycle ();
toast ( `Cycle done · ${ r . suggestions } suggestions · equity ${ fmtUSD ( r . equity ) } ` );
renderAll ();
} catch ( e ){ toast ( "Run failed: " + e . message ); setStatus ( "error" , false );}
2026-06-27 15:29:34 -04:00
btn . disabled = false ; btn . textContent = "Run cycle" ;
2026-06-21 16:19:34 -04:00
});
$ ( "resetBtn" ). addEventListener ( "click" ,()=>{
if ( ! confirm ( "Reset the paper portfolio to its starting balance?" )) return ;
savePortfolio ( defaultPortfolio ()); localStorage . removeItem ( SUG_KEY );
toast ( "Portfolio reset." ); renderAll ();
});
2026-06-27 15:29:34 -04:00
/* On load: route to tab, render, then auto-run once per day. */
2026-06-21 16:19:34 -04:00
( async function init (){
2026-06-27 15:29:34 -04:00
showTab ( location . hash . slice ( 1 ) || "overview" );
2026-06-21 16:19:34 -04:00
renderAll ();
const sug = loadSuggestions ();
if ( sug . date !== todayStr ()){
const btn = $ ( "runBtn" ); btn . disabled = true ; btn . textContent = "Running…" ;
try { await runDailyCycle (); renderAll (); toast ( "Fresh daily analysis ready." );}
catch ( e ){ setStatus ( "error — click Run to retry" , false );}
2026-06-27 15:29:34 -04:00
btn . disabled = false ; btn . textContent = "Run cycle" ;
2026-06-21 16:19:34 -04:00
} else { setStatus ( "up to date" , false );}
})();
</ script >
</ body >
</ html >