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-07-05 14:58:21 -04:00
< title > Polymarket Analyst — 5 AI Agents Compete to Trade Prediction Markets</ title >
< meta name = "description" content = "Daily AI analysis of Polymarket with five competing autonomous paper-trading agents and a live leaderboard. Runs entirely in your browser." />
2026-06-27 15:29:34 -04:00
< 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" >
2026-07-05 14:58:21 -04:00
< 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 )}}
. 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 }
2026-07-05 14:58:21 -04:00
. 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 )}
2026-06-27 15:29:34 -04:00
. tab { appearance : none ; border : 0 ; background : transparent ; color : var ( -- muted ); font-family : inherit ;
2026-07-05 14:58:21 -04:00
font-size : 13.5 px ; font-weight : 600 ; padding : 8 px 14 px ; border-radius : 9 px ; cursor : pointer ; transition : .18 s ; white-space : nowrap }
2026-06-27 15:29:34 -04:00
. tab : hover { color : var ( -- text )}
2026-07-05 14:58:21 -04:00
. tab . active { color : #fff ; background : linear-gradient ( 135 deg , var ( -- accent ), var ( -- accent2 )); box-shadow : 0 6 px 18 px -8 px var ( -- accent )}
2026-06-27 15:29:34 -04:00
. 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 )}
2026-07-05 14:58:21 -04:00
. dot { width : 8 px ; height : 8 px ; border-radius : 50 % ; background : var ( -- muted )}
2026-06-27 15:29:34 -04:00
. 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 )}
2026-07-05 14:58:21 -04:00
. 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 )}
2026-06-27 15:29:34 -04:00
. 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 ;
2026-07-05 14:58:21 -04:00
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 )}
2026-06-27 15:29:34 -04:00
. hero-art { position : absolute ; right : -10 px ; top : -10 px ; bottom : -10 px ; width : 46 % ; opacity : .85 ; pointer-events : none }
2026-07-05 14:58:21 -04:00
. hero h1 { margin : 0 0 10 px ; font-size : 34 px ; letter-spacing : -.8 px ; line-height : 1.08 ; max-width : 660 px }
2026-06-27 15:29:34 -04:00
. hero h1 . grad { background : linear-gradient ( 100 deg , #a9b6ff , #d9b4ff 50 % , #8fe8f3 ); -webkit- background-clip : text ; background-clip : text ; color : transparent }
2026-07-05 14:58:21 -04:00
. hero p { margin : 0 ; color : #c3cce0 ; font-size : 15.5 px ; max-width : 580 px }
2026-06-27 15:29:34 -04:00
. hero . badges { display : flex ; gap : 9 px ; flex-wrap : wrap ; margin-top : 18 px }
2026-07-05 14:58:21 -04:00
. 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 )}
2026-06-27 15:29:34 -04:00
/* ---------- 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 }
2026-07-05 14:58:21 -04:00
. 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 }
2026-06-27 15:29:34 -04:00
. stat : hover { transform : translateY ( -2 px ); border-color : var ( -- border - strong )}
2026-07-05 14:58:21 -04:00
. stat :: after { content : "" ; position : absolute ; left : 0 ; top : 0 ; height : 3 px ; width : 100 % ; background : linear-gradient ( 90 deg , var ( -- accent ), var ( -- accent2 ))}
2026-06-27 15:29:34 -04:00
. 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 }
2026-07-05 14:58:21 -04:00
. stat . value { font-size : 25 px ; font-weight : 700 ; margin-top : 4 px ; font-family : 'Space Grotesk' , sans-serif ; letter-spacing : -.5 px }
2026-06-27 15:29:34 -04:00
. pos-val { color : var ( -- green )}. neg-val { color : var ( -- red )}
2026-07-05 14:58:21 -04:00
/* ---------- charts ---------- */
# chart , # chart2 { width : 100 % ; height : 240 px ; display : block }
. legend { display : flex ; flex-wrap : wrap ; gap : 8 px 16 px ; margin-top : 14 px }
. leg { display : flex ; align-items : center ; gap : 7 px ; font-size : 12.5 px ; color : #cdd6e8 }
. legdot { width : 10 px ; height : 10 px ; border-radius : 3 px ; display : inline-block }
/* ---------- leaderboard ---------- */
. lb-grid { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 225 px , 1 fr )); gap : 16 px ; margin-bottom : 20 px }
. lb-card { position : relative ; background : var ( -- panel ); border : 1 px solid var ( -- border ); border-radius : 18 px ; padding : 18 px ;
backdrop-filter : blur ( 14 px ); cursor : pointer ; transition : .2 s ; overflow : hidden }
. lb-card : hover { transform : translateY ( -3 px ); border-color : var ( -- border - strong ); box-shadow : var ( -- shadow )}
. lb-top { display : flex ; align-items : center ; gap : 10 px ; margin-bottom : 14 px }
. medal { font-size : 20 px ; min-width : 26 px }
. lb-emoji { font-size : 24 px }
. lb-name { font-weight : 700 ; font-size : 15 px ; font-family : 'Space Grotesk' }
. lb-blurb { font-size : 11.5 px ; color : var ( -- muted ); line-height : 1.4 ; margin-top : 2 px }
. lb-mid { display : flex ; align-items : baseline ; justify-content : space-between ; margin-bottom : 12 px }
. lb-eq { font-size : 26 px ; font-weight : 700 ; font-family : 'Space Grotesk' ; letter-spacing : -.5 px }
. lb-ret { font-size : 15 px ; font-weight : 700 ; font-family : 'Space Grotesk' }
. lb-foot { display : flex ; align-items : center ; justify-content : space-between ; gap : 8 px }
. rankpill { position : absolute ; top : 14 px ; right : 14 px ; font-size : 11 px ; font-weight : 700 ; padding : 3 px 9 px ; border-radius : 999 px ; background : rgba ( 255 , 255 , 255 , .07 ); border : 1 px solid var ( -- border ); color : var ( -- muted )}
/* ---------- segmented agent selector ---------- */
. seg { display : flex ; gap : 6 px ; flex-wrap : wrap ; margin-bottom : 18 px }
. segbtn { display : flex ; align-items : center ; gap : 7 px ; font-size : 13 px ; font-weight : 600 ; padding : 8 px 13 px ; border-radius : 11 px ; cursor : pointer ;
background : rgba ( 255 , 255 , 255 , .05 ); border : 1 px solid var ( -- border ); color : var ( -- muted ); transition : .16 s ; user-select : none }
. segbtn : hover { color : var ( -- text ); border-color : var ( -- border - strong )}
. segbtn . active { color : #fff ; border-color : transparent }
2026-06-27 15:29:34 -04:00
/* ---------- suggestions ---------- */
. filterbar { display : flex ; gap : 8 px ; flex-wrap : wrap ; margin-bottom : 8 px }
2026-07-05 14:58:21 -04:00
. 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-27 15:29:34 -04:00
. chip : hover { border-color : var ( -- accent ); color : var ( -- text )}
. 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 }}
2026-07-05 14:58:21 -04:00
. 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 }
2026-06-27 15:29:34 -04:00
. 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 }
2026-06-21 16:19:34 -04:00
. conv-bar > span { display : block ; height : 100 % ; background : linear-gradient ( 90 deg , var ( -- accent ), var ( -- green ))}
2026-06-27 15:29:34 -04:00
. rationale { font-size : 13 px ; color : #cdd6e8 ; margin : 10 px 0 }
2026-06-21 16:19:34 -04:00
. drivers { display : flex ; gap : 6 px ; flex-wrap : wrap }
2026-06-27 15:29:34 -04:00
. 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 }
2026-06-21 16:19:34 -04:00
. metrics b { color : var ( -- text )}
2026-06-27 15:29:34 -04:00
a . market-link { color : var ( -- accent ); text-decoration : none ; font-size : 12 px ; font-weight : 600 }
2026-06-21 16:19:34 -04:00
a . market-link : hover { text-decoration : underline }
2026-06-27 15:29:34 -04:00
/* ---------- positions / log ---------- */
. pos { display : flex ; justify-content : space-between ; align-items : center ; padding : 13 px 0 ; border-bottom : 1 px solid var ( -- border )}
2026-06-21 16:19:34 -04:00
. pos : last-child { border-bottom : none }
2026-06-27 15:29:34 -04:00
. 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 }
2026-06-21 16:19:34 -04:00
. right { text-align : right }
2026-06-27 15:29:34 -04:00
. log-item { font-size : 12.5 px ; padding : 9 px 0 ; border-bottom : 1 px solid var ( -- border ); color : #cdd6e8 }
2026-06-21 16:19:34 -04:00
. log-item : last-child { border-bottom : none }
2026-06-27 15:29:34 -04:00
. 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 )}
2026-06-21 16:19:34 -04:00
. log-date { color : var ( -- muted )}
2026-06-27 15:29:34 -04:00
. 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 )}
2026-07-05 14:58:21 -04:00
. 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 )}
2026-06-27 15:29:34 -04:00
. 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 }
2026-07-05 14:58:21 -04:00
. 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 }
2026-06-27 15:29:34 -04:00
/* ---------- misc ---------- */
2026-07-05 14:58:21 -04:00
. 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 )}
2026-06-21 16:19:34 -04:00
. toast . show { opacity : 1 ; transform : translateY ( 0 )}
2026-06-27 15:29:34 -04:00
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 >
< div class = "app" >
< nav class = "topnav" >
< div class = "brand" >
2026-07-05 14:58:21 -04:00
< div class = "logo" > 🏆</ div >
< div >< div class = "brand-name" > Polymarket Arena</ div >< div class = "brand-sub" > 10 agents · strategies, copycats & whales</ div ></ div >
2026-06-27 15:29:34 -04:00
</ div >
< div class = "tabs" id = "tabs" >
< button class = "tab" data-tab = "overview" > Overview</ button >
2026-07-05 14:58:21 -04:00
< button class = "tab" data-tab = "leaderboard" > Leaderboard</ button >
2026-06-27 15:29:34 -04:00
< button class = "tab" data-tab = "suggestions" > Suggestions</ button >
2026-07-05 14:58:21 -04:00
< button class = "tab" data-tab = "portfolio" > Portfolios</ button >
2026-06-27 15:29:34 -04:00
< 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 >
<!-- ============ 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" >
2026-07-05 14:58:21 -04:00
< 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-width = "2.5" >
< path d = "M20,250 L120,160 L200,200 L290,80 L380,40" stroke = "#34d399" />
< path d = "M20,255 L120,210 L200,150 L290,170 L380,120" stroke = "#fbbf24" opacity = ".8" />
< path d = "M20,248 L120,230 L200,235 L290,210 L380,200" stroke = "#fb7185" opacity = ".7" />
2026-06-27 15:29:34 -04:00
</ g >
2026-07-05 14:58:21 -04:00
< g fill = "#d9b4ff" >< circle cx = "380" cy = "40" r = "6" /></ g >
2026-06-27 15:29:34 -04:00
</ svg >
2026-07-05 14:58:21 -04:00
< h1 > Ten AI agents race to < span class = "grad" > beat the market</ span ></ h1 >
< p > Five in-house strategies, a copycat that shadows the leader, and four agents that mirror the live positions of the top traders on Polymarket — all paper-trading for the top of the leaderboard.</ p >
2026-06-27 15:29:34 -04:00
< div class = "badges" >
2026-07-05 14:58:21 -04:00
< span class = "hbadge" > 🏆 10 competing agents</ span >
< span class = "hbadge" > 🐋 Copies real Polymarket whales</ span >
< span class = "hbadge" > 🐒 Copycat meta-agent</ span >
< span class = "hbadge" > 🔴 Live data</ span >
2026-06-27 15:29:34 -04:00
</ div >
</ div >
< div class = "stats" id = "stats" ></ div >
< div class = "grid2" >
< div class = "card hoverable" >
2026-07-05 14:58:21 -04:00
< div class = "card-h" >< h3 > The Equity Race</ h3 >< span class = "small muted" id = "lastRun" ></ span ></ div >
< svg id = "chart" viewBox = "0 0 600 240" preserveAspectRatio = "none" ></ svg >
< div class = "legend" id = "comboLegend" ></ div >
2026-06-27 15:29:34 -04:00
</ 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 >
2026-07-05 14:58:21 -04:00
<!-- ============ LEADERBOARD ============ -->
< section class = "tabpanel" data-tab = "leaderboard" >
< div class = "section-title" > 🏆 Leaderboard < span class = "small muted" id = "lbSub" style = "font-weight:400" ></ span ></ div >
< div class = "lb-grid" id = "leaderboard" ></ div >
< div class = "card" >
< div class = "card-h" >< h3 > Equity Race — all agents</ h3 >< span class = "small muted" > $10,000 start each</ span ></ div >
< svg id = "chart2" viewBox = "0 0 600 240" preserveAspectRatio = "none" ></ svg >
< div class = "legend" id = "comboLegend2" ></ div >
</ div >
</ section >
2026-06-27 15:29:34 -04:00
<!-- ============ 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 >
2026-07-05 14:58:21 -04:00
<!-- ============ PORTFOLIOS ============ -->
2026-06-27 15:29:34 -04:00
< section class = "tabpanel" data-tab = "portfolio" >
2026-07-05 14:58:21 -04:00
< div class = "section-title" > 🤖 Agent Portfolios</ div >
< div class = "seg" id = "agentSel" ></ div >
2026-06-27 15:29:34 -04:00
< 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" >
2026-07-05 14:58:21 -04:00
< div class = "section-title" > ℹ ️ The competition</ div >
< p class = "muted" style = "margin-top:-6px;max-width:820px" > Every agent starts with < b > $10,000</ b > in paper money. The five in-house strategies trade the same daily suggestions their own way; the < b > Copycat</ b > shadows whichever strategy is winning; and the four < b > Whale Shadow</ b > agents mirror the live positions of the highest-profit real traders on Polymarket (auto-discovered from its public leaderboard). All mark to market daily and climb — or sink — the board.</ p >
< div class = "about-grid" id = "aboutAgents" ></ div >
2026-06-27 15:29:34 -04:00
< 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 >
2026-07-05 14:58:21 -04:00
< li >< b > Compete</ b > — all five agents trade the same suggestions their own way, then snapshot equity for the leaderboard.</ li >
2026-06-27 15:29:34 -04:00
</ ol >
2026-06-21 16:19:34 -04:00
</ div >
2026-07-05 14:58:21 -04:00
< 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. All portfolios live in this browser's local storage (each device keeps its own).</ div >
2026-06-27 15:29:34 -04:00
</ 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" ;
/* ============================================================
2026-07-05 14:58:21 -04:00
Polymarket Arena — fully client-side.
Fetches live markets, scores them, and runs FIVE competing
paper-trading agents. State persists in localStorage.
2026-06-21 16:19:34 -04:00
============================================================ */
const GAMMA = "https://gamma-api.polymarket.com" ;
const STARTING_BALANCE = 10000 ;
2026-07-05 14:58:21 -04:00
const AGENTS_KEY = "pma_agents_v2" ;
const SUG_KEY = "pma_suggestions_v2" ;
2026-06-21 16:19:34 -04:00
const FOCUS_KEY = "pma_focus_v1" ;
2026-07-05 14:58:21 -04:00
const VIEW_KEY = "pma_view_v1" ;
2026-06-21 16:19:34 -04:00
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" ,
2026-07-05 14:58:21 -04:00
"Pop Culture" : "#c77dff" , Other : "#94a1bb" , All : "#7c8cff" , Copy : "#22d3ee" };
const DATA_API = "https://data-api.polymarket.com" ;
const LB_API = "https://lb-api.polymarket.com" ;
const CLOB = "https://clob.polymarket.com" ;
2026-06-27 15:29:34 -04:00
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 );
2026-07-05 14:58:21 -04:00
/* ---------- the five competing agents ----------
Each shares the same daily suggestions but ranks/sizes them differently. */
const AGENTS = [
{ id : "value" , name : "Value Hunter" , emoji : "🎯" , color : "#7c8cff" , kind : "strategy" ,
blurb : "Backs the biggest model edge — the most mispriced markets." ,
rank : ( s )=>[... s ]. sort (( a , b )=> b . conviction - a . conviction ),
maxNew : 4 , maxFrac : 0.10 , minConv : 45 , kelly : 0.25 },
{ id : "momentum" , name : "Momentum Chaser" , emoji : "🚀" , color : "#34d399" , kind : "strategy" ,
blurb : "Follows the fresh money — favors 24h volume surges." ,
rank : ( s )=>[... s ]. sort (( a , b )=>( b . volume_24hr * ( b . conviction + 20 )) - ( a . volume_24hr * ( a . conviction + 20 ))),
maxNew : 5 , maxFrac : 0.12 , minConv : 40 , kelly : 0.30 },
{ id : "favorite" , name : "Favorite Backer" , emoji : "🛡️" , color : "#38d2e6" , kind : "strategy" ,
blurb : "Low-variance grinder — backs strong favorites likely to hit." ,
rank : ( s )=>[... s ]. filter ( x => x . entry_price >= 0.6 ). sort (( a , b )=> b . entry_price - a . entry_price || b . conviction - a . conviction ),
maxNew : 4 , maxFrac : 0.14 , minConv : 38 , kelly : 0.40 },
{ id : "longshot" , name : "Longshot Hunter" , emoji : "🎰" , color : "#fbbf24" , kind : "strategy" ,
blurb : "High-variance contrarian — hunts cheap, underpriced longshots." ,
rank : ( s )=>[... s ]. filter ( x => x . entry_price <= 0.4 ). sort (( a , b )=> a . entry_price - b . entry_price || b . conviction - a . conviction ),
maxNew : 5 , maxFrac : 0.08 , minConv : 38 , kelly : 0.20 },
{ id : "diversifier" , name : "The Diversifier" , emoji : "🌐" , color : "#c77dff" , kind : "strategy" ,
blurb : "Spreads many small equal-weight bets across the whole board." ,
rank : ( s )=>[... s ]. sort (( a , b )=> b . conviction - a . conviction ),
maxNew : 8 , maxFrac : 0.05 , minConv : 35 , kelly : 0.15 , flat : true },
{ id : "copycat" , name : "The Copycat" , emoji : "🐒" , color : "#ec4899" , kind : "copycat" ,
blurb : "Shadows whichever in-house strategy is currently winning." },
{ id : "whale1" , name : "Whale Shadow α " , emoji : "🐋" , color : "#2dd4bf" , kind : "whale" , slot : 0 ,
blurb : "Mirrors the live positions of a top Polymarket trader." },
{ id : "whale2" , name : "Whale Shadow β" , emoji : "🐳" , color : "#fb923c" , kind : "whale" , slot : 1 ,
blurb : "Mirrors the live positions of a top Polymarket trader." },
{ id : "whale3" , name : "Whale Shadow γ " , emoji : "🦈" , color : "#a3e635" , kind : "whale" , slot : 2 ,
blurb : "Mirrors the live positions of a top Polymarket trader." },
{ id : "whale4" , name : "Whale Shadow δ" , emoji : "🐡" , color : "#60a5fa" , kind : "whale" , slot : 3 ,
blurb : "Mirrors the live positions of a top Polymarket trader." },
];
const agentById = ( id ) => AGENTS . find ( a => a . id === id ) || AGENTS [ 0 ];
2026-06-21 16:19:34 -04:00
/* ---------- 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 );
}
2026-07-05 14:58:21 -04:00
function setStatus ( text , live ){ $ ( "statusText" ). textContent = text ; $ ( "statusDot" ). classList . toggle ( "live" , !! live );}
2026-06-21 16:19:34 -04:00
/* ---------- Polymarket client ---------- */
2026-07-05 14:58:21 -04:00
function parseJsonField ( v ){ if ( v == null ) return []; if ( Array . isArray ( v )) return v ; try { return JSON . parse ( v );} catch ( e ){ return [];}}
2026-06-21 16:19:34 -04:00
function toNum ( v , d = 0 ){ const n = Number ( v ); return Number . isFinite ( n ) ? n : d ;}
2026-07-05 14:58:21 -04:00
function daysUntil ( iso ){ if ( ! iso ) return null ; const dt = new Date ( iso ); if ( isNaN ( dt )) return null ; return ( dt - new Date ()) / 86400000 ;}
2026-06-21 16:19:34 -04:00
const CATEGORY_RULES = [
2026-07-05 14:58:21 -04:00
[ "Politics" ,[ "politics" , "election" , "elections" , "us-politics" , "geopolitics" , "trump" , "world-leaders" , "government" , "congress" , "policy" , "biden" , "2026-election" , "democrats" , "republicans" ]],
2026-06-21 16:19:34 -04:00
[ "Crypto" ,[ "crypto" , "bitcoin" , "ethereum" , "btc" , "eth" , "solana" , "memecoins" , "defi" , "stablecoin" , "xrp" ]],
2026-07-05 14:58:21 -04:00
[ "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" ]],
2026-06-21 16:19:34 -04:00
[ "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 ());
2026-07-05 14:58:21 -04:00
for ( const [ cat , keys ] of CATEGORY_RULES ){ if ( slugs . some ( s => keys . includes ( s ))) return cat ;}
2026-06-21 16:19:34 -04:00
return "Other" ;
}
function normalizeMarket ( raw ){
const outcomes = parseJsonField ( raw . outcomes );
const prices = parseJsonField ( raw . outcomePrices ). map ( p => toNum ( p ));
2026-07-05 14:58:21 -04:00
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 ] : {};
2026-06-21 16:19:34 -04:00
const tags = Array . isArray ( raw . tags ) ? raw . tags : [];
2026-07-05 14:58:21 -04:00
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 } ` : "" };
2026-06-21 16:19:34 -04:00
}
async function fetchMarkets ( pages = 4 , perPage = 100 ){
const all = [];
for ( let i = 0 ; i < pages ; i ++ ){
2026-07-05 14:58:21 -04:00
const params = new URLSearchParams ({ closed : "false" , active : "true" , archived : "false" , include_tag : "true" ,
limit : String ( perPage ), offset : String ( i * perPage ), order : "volume24hr" , ascending : "false" });
2026-06-21 16:19:34 -04:00
const r = await fetch ( ` ${ GAMMA } /markets? ${ params } ` );
2026-07-05 14:58:21 -04:00
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 );
2026-06-21 16:19:34 -04:00
}
return all . map ( normalizeMarket ). filter ( m => m && m . question );
}
async function fetchMarketPrice ( id ){
2026-07-05 14:58:21 -04:00
try { const r = await fetch ( ` ${ GAMMA } /markets/ ${ id } ` ); if ( ! r . ok ) return null ; return normalizeMarket ( await r . json ());}
catch ( e ){ return null ;}
2026-06-21 16:19:34 -04:00
}
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 ));
2026-07-05 14:58:21 -04:00
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 da = m . volume_1wk ? m . volume_1wk / 7 : 0 ; if ( da <= 0 ) return m . volume_24hr > 0 ? 0.3 : 0 ; return clamp (( m . volume_24hr / da - 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 );
return clamp ( p + ( mom - 0.5 ) * 0.06 , 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 ;}
2026-06-21 16:19:34 -04:00
function analyzeMarket ( m ){
2026-07-05 14:58:21 -04:00
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 );
2026-06-21 16:19:34 -04:00
const mis = Math . min ( 1 , edge / 0.10 );
2026-07-05 14:58:21 -04:00
const conviction =+ (( W . LIQ * liq + W . MOM * mom + W . MIS * mis + W . TIME * timing ) * 100 ). toFixed ( 1 );
2026-06-21 16:19:34 -04:00
let side , entry , rationale ;
2026-07-05 14:58:21 -04:00
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.` ;}
2026-06-21 16:19:34 -04:00
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" );
2026-07-05 14:58:21 -04:00
return { market_id : m . id , question : m . question , event : m . event , url : m . url , category : m . category , tags : m . tags ,
2026-06-21 16:19:34 -04:00
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 ,
2026-07-05 14:58:21 -04:00
days_to_resolution : m . days_to_resolution != null ?+ m . days_to_resolution . toFixed ( 1 ) : null , drivers , rationale };
2026-06-21 16:19:34 -04:00
}
const pct = ( x )=> Math . round ( x * 100 ) + "%" ;
2026-07-05 14:58:21 -04:00
function generateSuggestions ( markets , perCategory = 12 ){
const actionable = markets . map ( analyzeMarket ). filter ( Boolean ). filter ( a => a . side !== "HOLD" ). sort (( a , b )=> b . conviction - a . conviction );
2026-06-21 16:19:34 -04:00
const byCat = {}; const out = [];
2026-07-05 14:58:21 -04:00
for ( const a of actionable ){ const c = a . category || "Other" ; byCat [ c ] = ( byCat [ c ] || 0 ) + 1 ; if ( byCat [ c ] <= perCategory ) out . push ( a );}
2026-06-21 16:19:34 -04:00
return out . sort (( a , b )=> b . conviction - a . conviction );
}
2026-07-05 14:58:21 -04:00
/* ---------- Multi-agent store ---------- */
function defaultPortfolio (){ return { cash : STARTING_BALANCE , starting_balance : STARTING_BALANCE , positions : [], closed : [], history : [], snapshots : []};}
function defaultState (){ const agents = {}; AGENTS . forEach ( a => agents [ a . id ] = defaultPortfolio ()); return { date : null , last_run : null , agents , whales : {}, copycatLeader : null };}
function loadState (){
let st ; try { st = JSON . parse ( localStorage . getItem ( AGENTS_KEY ));} catch ( e ){ st = null ;}
if ( ! st ||! st . agents ) st = defaultState ();
AGENTS . forEach ( a =>{ if ( ! st . agents [ a . id ]) st . agents [ a . id ] = defaultPortfolio ();});
if ( ! st . whales ) st . whales = {};
return st ;
2026-06-21 16:19:34 -04:00
}
2026-07-05 14:58:21 -04:00
function saveState ( st ){ localStorage . setItem ( AGENTS_KEY , JSON . stringify ( st ));}
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 p = { date : todayStr (), generated_at : nowIso (), suggestions : sugs }; localStorage . setItem ( SUG_KEY , JSON . stringify ( p )); return p ;}
2026-06-21 16:19:34 -04:00
2026-07-05 14:58:21 -04:00
/* ---------- Agent mechanics ---------- */
2026-06-21 16:19:34 -04:00
const equity = ( p )=> p . cash + p . positions . reduce (( s , x )=> s + x . shares * x . current_price , 0 );
const hasPosition = ( p , id )=> p . positions . some ( x => x . market_id === id );
2026-07-05 14:58:21 -04:00
function markToMarket ( p , priceMap ){
const stillOpen = [];
2026-06-21 16:19:34 -04:00
for ( const pos of p . positions ){
2026-07-05 14:58:21 -04:00
const fresh = priceMap [ pos . market_id ];
2026-06-21 16:19:34 -04:00
if ( fresh == null ){
2026-07-05 14:58:21 -04:00
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 );
2026-06-21 16:19:34 -04:00
p . closed . push ( pos );
2026-07-05 14:58:21 -04:00
p . history . push ({ date : todayStr (), action : "CLOSE" , question : pos . question , side : pos . side ,
detail : `Settled ' ${ pos . question . slice ( 0 , 40 ) } ' for ${ fmtUSD ( proceeds ) } (P&L ${ fmtUSD ( pos . realized_pnl ) } )` });
continue ;
2026-06-21 16:19:34 -04:00
}
const price = pos . side === "YES" ? fresh . yes_price : fresh . no_price ;
2026-07-05 14:58:21 -04:00
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 );
2026-06-21 16:19:34 -04:00
}
2026-07-05 14:58:21 -04:00
p . positions = stillOpen ;
2026-06-21 16:19:34 -04:00
}
2026-07-05 14:58:21 -04:00
function openPositions ( p , cfg , rankedSugs , focus ){
const cands = rankedSugs . filter ( s =>( s . side === "YES" || s . side === "NO" ) && s . conviction >= cfg . minConv
&&! hasPosition ( p , s . market_id ) && ( focus === "All" ||! focus || s . category === focus ));
2026-06-21 16:19:34 -04:00
let opened = 0 ;
for ( const s of cands ){
2026-07-05 14:58:21 -04:00
if ( opened >= cfg . maxNew ) break ;
const eq = equity ( p ), investable = p . cash - eq * 0.05 ;
if ( investable <= 50 ) break ;
let frac ;
if ( cfg . flat ){ frac = cfg . maxFrac ;}
else { const base = ( s . conviction / 100 ) * Math . min ( 1 , Math . abs ( s . edge ) / 0.10 ); frac = Math . min ( cfg . maxFrac , cfg . kelly * base );}
2026-06-21 16:19:34 -04:00
let stake = Math . min ( eq * frac , investable );
2026-07-05 14:58:21 -04:00
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 ;
2026-06-21 16:19:34 -04:00
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-07-05 14:58:21 -04:00
unrealized_pnl : 0 , conviction : s . conviction , category : s . category , opened_at : nowIso (), url : s . url || "" });
p . history . push ({ date : todayStr (), action : "OPEN" , question : s . question , side : s . side ,
detail : `Bought ${ shares } ${ s . side } ' ${ s . question . slice ( 0 , 40 ) } ' @ ${ pct ( entry ) } for ${ fmtUSD ( cost ) } ` });
2026-06-21 16:19:34 -04:00
opened ++ ;
}
}
function recordSnapshot ( p ){
2026-07-05 14:58:21 -04:00
const eq = equity ( p );
const snap = { date : todayStr (), timestamp : nowIso (), cash :+ p . cash . toFixed ( 2 ),
2026-06-21 16:19:34 -04:00
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 );
}
2026-07-05 14:58:21 -04:00
/* ---------- Copy-trading: Polymarket leaders ---------- */
const hasAsset = ( p , a )=> p . positions . some ( x => x . asset === a );
async function fetchTokenPrice ( asset ){
try { const r = await fetch ( ` ${ CLOB } /midpoint?token_id= ${ asset } ` );
if ( r . ok ){ const d = await r . json (); const m = parseFloat ( d . mid ); if ( m > 0 && m < 1 ) return m ;}} catch ( e ){}
return null ;
}
async function fetchTraderPositions ( wallet ){
try { const r = await fetch ( ` ${ DATA_API } /positions?user= ${ wallet } &sizeThreshold=1&limit=80` );
if ( ! r . ok ) return [];
const d = await r . json ();
return d . filter ( p =>( p . outcome === "Yes" || p . outcome === "No" ) && p . curPrice > 0.02 && p . curPrice < 0.98 &&! p . redeemable && p . size * p . curPrice >= 1 )
. map ( p =>({ asset : String ( p . asset ), conditionId : p . conditionId , outcome : p . outcome , side : String ( p . outcome ). toUpperCase (),
curPrice :+ p . curPrice , title : p . title || "market" , value : p . size * p . curPrice }));
} catch ( e ){ return [];}
}
function prettyName ( n , w ){
if ( n &&! /^0x/i . test ( n )) return n ; // real display name / pseudonym
return "0x" + String ( w ). slice ( 2 , 6 ) + "…" + String ( w ). slice ( - 4 ); // anonymous → short wallet
}
async function discoverWhales (){
const picked = [];
try {
const r = await fetch ( ` ${ LB_API } /profit?window=all&limit=40` );
const leaders = r . ok ? await r . json () : [];
for ( const L of leaders ){
if ( picked . length >= 4 ) break ;
const tps = await fetchTraderPositions ( L . proxyWallet );
if ( tps . length >= 1 ) picked . push ({ wallet : L . proxyWallet ,
name : prettyName ( L . name || L . pseudonym , L . proxyWallet ), profit : Math . round ( L . amount || 0 )});
}
} catch ( e ){}
return picked ;
}
async function runWhaleAgent ( p , wallet ){
// mark existing copied positions to market via CLOB token price
for ( const pos of p . positions ){
const pr = await fetchTokenPrice ( pos . asset );
if ( pr != null ){ pos . current_price =+ pr . toFixed ( 4 ); pos . value =+ ( pos . shares * pr ). toFixed ( 2 ); pos . unrealized_pnl =+ ( pos . value - pos . cost ). toFixed ( 2 );}
}
const tps = await fetchTraderPositions ( wallet );
const held = new Set ( tps . map ( t => t . asset ));
const keep = [];
for ( const pos of p . positions ){
if ( ! held . has ( pos . asset )){
const proceeds = pos . shares * pos . current_price ; p . cash += proceeds ;
pos . exit_price = pos . current_price ; pos . realized_pnl =+ ( proceeds - pos . cost ). toFixed ( 2 ); p . closed . push ( pos );
p . history . push ({ date : todayStr (), action : "CLOSE" , question : pos . question , side : pos . side ,
detail : `Trader exited ' ${ pos . question . slice ( 0 , 36 ) } ' — settled ${ fmtUSD ( proceeds ) } (P&L ${ fmtUSD ( pos . realized_pnl ) } )` });
} else keep . push ( pos );
}
p . positions = keep ;
const targets = [... tps ]. sort (( a , b )=> b . value - a . value ). slice ( 0 , 12 ); // copy their biggest 12 bets
const book = targets . reduce (( s , t )=> s + t . value , 0 ) || 1 ;
for ( const t of targets ){
if ( hasAsset ( p , t . asset )) continue ;
const eq = equity ( p ), investable = p . cash - eq * 0.05 ; if ( investable <= 50 ) break ;
const weight = t . value / book ;
const stake = Math . min ( eq * Math . min ( 0.34 , weight ), investable );
if ( stake < 40 ) continue ;
const entry = t . curPrice ; 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 ({ asset : t . asset , market_id : t . conditionId , question : t . title , side : t . side , category : "Copy" ,
shares , entry_price :+ entry . toFixed ( 4 ), current_price :+ entry . toFixed ( 4 ), cost , value : cost , unrealized_pnl : 0 , conviction : null });
p . history . push ({ date : todayStr (), action : "OPEN" , question : t . title , side : t . side ,
detail : `Copied trade — ${ shares } ${ t . side } ' ${ t . title . slice ( 0 , 36 ) } ' @ ${ pct ( entry ) } for ${ fmtUSD ( cost ) } ` });
}
recordSnapshot ( p );
}
/* ---------- Copycat: shadow the leading in-house strategy ---------- */
function runCopycat ( p , leader , priceMap ){
markToMarket ( p , priceMap );
const held = new Set ( leader . positions . map ( x => x . market_id ));
const keep = [];
for ( const pos of p . positions ){
if ( ! held . has ( pos . market_id )){
const proceeds = pos . shares * pos . current_price ; p . cash += proceeds ;
pos . exit_price = pos . current_price ; pos . realized_pnl =+ ( proceeds - pos . cost ). toFixed ( 2 ); p . closed . push ( pos );
p . history . push ({ date : todayStr (), action : "CLOSE" , question : pos . question , side : pos . side ,
detail : `Leader exited ' ${ pos . question . slice ( 0 , 36 ) } ' — settled ${ fmtUSD ( proceeds ) } ` });
} else keep . push ( pos );
}
p . positions = keep ;
const book = leader . positions . reduce (( s , x )=> s + ( x . value || x . cost ), 0 ) || 1 ;
for ( const lp of [... leader . positions ]. sort (( a , b )=>( b . value || b . cost ) - ( a . value || a . cost ))){
if ( hasPosition ( p , lp . market_id )) continue ;
const eq = equity ( p ), investable = p . cash - eq * 0.05 ; if ( investable <= 50 ) break ;
const weight = ( lp . value || lp . cost ) / book ;
const stake = Math . min ( eq * Math . min ( 0.2 , weight ), investable );
if ( stake < 50 ) continue ;
const entry = lp . current_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 : lp . market_id , question : lp . question , side : lp . side , category : lp . category ,
shares , entry_price :+ entry . toFixed ( 4 ), current_price :+ entry . toFixed ( 4 ), cost , value : cost , unrealized_pnl : 0 , conviction : lp . conviction });
p . history . push ({ date : todayStr (), action : "OPEN" , question : lp . question , side : lp . side ,
detail : `Copied leader — ${ shares } ${ lp . side } ' ${ lp . question . slice ( 0 , 36 ) } ' @ ${ pct ( entry ) } ` });
}
recordSnapshot ( p );
}
2026-06-21 16:19:34 -04:00
async function runDailyCycle (){
setStatus ( "fetching markets…" , true );
const markets = await fetchMarkets ( 4 , 100 );
setStatus ( "analyzing…" , true );
const sugs = generateSuggestions ( markets , 12 );
saveSuggestions ( sugs );
2026-07-05 14:58:21 -04:00
const st = loadState ();
const focus = getFocus ();
// Price map for suggestion-market positions (strategy + copycat only).
const ids = new Set ();
AGENTS . filter ( a => a . kind === "strategy" || a . kind === "copycat" ). forEach ( a =>
( st . agents [ a . id ]. positions || []). forEach ( pos =>{ if ( pos . market_id ) ids . add ( pos . market_id );}));
setStatus ( "marking positions…" , true );
const priceMap = {};
for ( const id of ids ){ priceMap [ id ] = await fetchMarketPrice ( id );}
// 1) in-house strategy agents
setStatus ( "strategy agents trading…" , true );
for ( const cfg of AGENTS . filter ( a => a . kind === "strategy" )){
const p = st . agents [ cfg . id ];
markToMarket ( p , priceMap );
openPositions ( p , cfg , cfg . rank ( sugs ), focus );
recordSnapshot ( p );
}
// 2) copycat shadows the leading strategy agent
const stratBoard = AGENTS . filter ( a => a . kind === "strategy" )
. map ( a =>({ id : a . id , eq : equity ( st . agents [ a . id ])})). sort (( x , y )=> y . eq - x . eq );
st . copycatLeader = stratBoard [ 0 ]. id ;
runCopycat ( st . agents [ "copycat" ], st . agents [ st . copycatLeader ], priceMap );
// 3) whale-copy agents mirror top Polymarket traders
if ( AGENTS . some ( a => a . kind === "whale" &&! st . whales [ a . id ])){
setStatus ( "finding top Polymarket traders…" , true );
const picked = await discoverWhales ();
AGENTS . filter ( a => a . kind === "whale" ). forEach ( a =>{ if ( ! st . whales [ a . id ] && picked [ a . slot ]) st . whales [ a . id ] = picked [ a . slot ];});
}
for ( const a of AGENTS . filter ( x => x . kind === "whale" )){
const wh = st . whales [ a . id ];
if ( ! wh ){ recordSnapshot ( st . agents [ a . id ]); continue ;}
setStatus ( `copying ${ wh . name } …` , true );
try { await runWhaleAgent ( st . agents [ a . id ], wh . wallet );}
catch ( e ){ recordSnapshot ( st . agents [ a . id ]);}
}
st . date = todayStr (); st . last_run = nowIso ();
saveState ( st );
2026-06-21 16:19:34 -04:00
setStatus ( "up to date" , false );
2026-07-05 14:58:21 -04:00
return { suggestions : sugs . length };
2026-06-21 16:19:34 -04:00
}
2026-07-05 14:58:21 -04:00
/* ---------- Leaderboard helpers ---------- */
function board (){
const st = loadState ();
return AGENTS . map ( c =>{ const p = st . agents [ c . id ] || defaultPortfolio (); const eq = equity ( p );
return { c , p , eq , pnl : eq - STARTING_BALANCE , ret : ( eq / STARTING_BALANCE - 1 ) * 100 };})
. sort (( a , b )=> b . eq - a . eq );
}
const MEDALS = [ "🥇" , "🥈" , "🥉" ];
2026-06-27 15:29:34 -04:00
2026-07-05 14:58:21 -04:00
/* ---------- Rendering ---------- */
function renderAll (){ renderOverview (); renderLeaderboard (); renderSuggestions (); renderPortfolioTab (); renderAbout ();}
function renderOverview (){
const b = board (); const lead = b [ 0 ];
const avgRet = b . reduce (( s , r )=> s + r . ret , 0 ) / b . length ;
const sugCount = ( loadSuggestions (). suggestions || []). length ;
const stats = [
{ ic : lead . c . emoji , label : "Leader" , value : lead . c . name . split ( " " )[ 0 ]},
{ ic : "📈" , label : "Leader return" , value : fmtPct ( lead . ret ), cls : signClass ( lead . pnl )},
{ ic : "⚖️" , label : "Avg return" , value : fmtPct ( avgRet ), cls : signClass ( avgRet )},
{ ic : "💡" , label : "Ideas today" , value : sugCount },
{ ic : "🏆" , label : "Agents" , value : AGENTS . length },
2026-06-27 15:29:34 -04:00
];
2026-07-05 14:58:21 -04:00
$ ( "stats" ). innerHTML = stats . 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 ( "" );
const st = loadState ();
if ( st . last_run ) $ ( "lastRun" ). textContent = "updated " + new Date ( st . last_run ). toLocaleString ();
drawCombo ( "chart" , "comboLegend" );
renderTopPicks ();
2026-06-27 15:29:34 -04:00
}
function renderTopPicks (){
2026-07-05 14:58:21 -04:00
const root = $ ( "topPicks" ); if ( ! root ) return ;
const top = (( loadSuggestions (). suggestions ) || []). slice ( 0 , 5 );
2026-06-27 15:29:34 -04:00
if ( ! top . length ){ root . innerHTML = `<div class="empty">Run a cycle to see top picks.</div>` ; return ;}
2026-07-05 14:58:21 -04:00
root . innerHTML = top . map ( s => `<div class="pos"><div>
<div class="pq"> ${ esc ( s . question . slice ( 0 , 58 )) }${ s . question . length > 58 ? "…" : "" } </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 ( "" );
}
function miniSpark ( snaps , color ){
if ( ! snaps || snaps . length < 2 ) return `<svg width="120" height="34"></svg>` ;
const pts = snaps . map ( s => s . equity ), W = 120 , H = 34 , p = 3 ;
const mn = Math . min (... pts ), mx = Math . max (... pts ), sp = ( mx - mn ) || 1 ;
const x = i => p + ( i / ( pts . length - 1 )) * ( W - 2 * p ), y = v => H - p - (( v - mn ) / sp ) * ( H - 2 * p );
const d = pts . map (( v , i )=> ` ${ i ? "L" : "M" }${ x ( i ). toFixed ( 1 ) } , ${ y ( v ). toFixed ( 1 ) } ` ). join ( " " );
return `<svg width=" ${ W } " height=" ${ H } "><path d=" ${ d } " fill="none" stroke=" ${ color } " stroke-width="2"/></svg>` ;
}
function agentBlurb ( cfg , st ){
if ( cfg . kind === "whale" ){ const w = st . whales [ cfg . id ]; return w ? `🐋 Mirrors <b> ${ esc ( w . name ) } </b> ${ w . profit ? ` · $ ${ ( w . profit / 1e6 ). toFixed ( 1 ) } M profit` : "" } ` : "Mirrors a top Polymarket trader (found on first run)." ;}
if ( cfg . kind === "copycat" ){ const L = AGENTS . find ( a => a . id === st . copycatLeader ); return `🐒 Copying <b> ${ L ? esc ( L . name ) : "the leader" } </b>` ;}
return cfg . blurb ;
}
function renderLeaderboard (){
const b = board (); const st = loadState ();
$ ( "lbSub" ). textContent = "· " + b [ 0 ]. c . name + " leads" ;
$ ( "leaderboard" ). innerHTML = b . map (( r , i )=> `
<div class="lb-card" data-agent=" ${ r . c . id } " style="border-top:3px solid ${ r . c . color } ">
<span class="rankpill"># ${ i + 1 } </span>
<div class="lb-top"><span class="medal"> ${ MEDALS [ i ] || "" } </span><span class="lb-emoji"> ${ r . c . emoji } </span>
<div><div class="lb-name"> ${ r . c . name } </div><div class="lb-blurb"> ${ agentBlurb ( r . c , st ) } </div></div></div>
<div class="lb-mid"><div class="lb-eq"> ${ fmtUSD ( r . eq ) } </div><div class="lb-ret ${ signClass ( r . pnl ) } "> ${ fmtPct ( r . ret ) } </div></div>
<div class="lb-foot"><span class="muted small"> ${ r . p . positions . length } open · ${ r . p . closed . length } closed</span> ${ miniSpark ( r . p . snapshots , r . c . color ) } </div>
2026-06-27 15:29:34 -04:00
</div>` ). join ( "" );
2026-07-05 14:58:21 -04:00
document . querySelectorAll ( "#leaderboard .lb-card" ). forEach ( el =>{
el . addEventListener ( "click" ,()=>{ localStorage . setItem ( VIEW_KEY , el . dataset . agent ); renderPortfolioTab (); showTab ( "portfolio" );});
});
drawCombo ( "chart2" , "comboLegend2" );
}
function drawCombo ( svgId , legendId ){
const st = loadState ();
const series = AGENTS . map ( c =>({ c , snaps : ( st . agents [ c . id ] && st . agents [ c . id ]. snapshots ) || []}));
const allDates = [... new Set ( series . flatMap ( s => s . snaps . map ( x => x . date )))]. sort ();
const svg = $ ( svgId ); if ( ! svg ) return ;
if ( ! allDates . length ){ svg . innerHTML = "" ; if ( $ ( legendId )) $ ( legendId ). innerHTML = `<span class="muted small">No history yet — click "Run cycle".</span>` ; return ;}
const W = 600 , H = 240 , pad = 10 ; const vals = [ STARTING_BALANCE ];
const lines = series . map ( s =>{ let last = STARTING_BALANCE ; const pts = allDates . map ( d =>{ const snap = s . snaps . find ( x => x . date === d ); if ( snap ) last = snap . equity ; vals . push ( last ); return last ;}); return { c : s . c , pts };});
const min = Math . min (... vals ) * 0.999 , max = Math . max (... vals ) * 1.001 , span = ( max - min ) || 1 ;
const x = i => allDates . length === 1 ? W / 2 : pad + ( i / ( allDates . length - 1 )) * ( W - 2 * pad );
const y = v => H - pad - (( v - min ) / span ) * ( H - 2 * pad );
let inner = "" ;
for ( const ln of lines ){
const d = ln . pts . map (( v , i )=> ` ${ i === 0 ? "M" : "L" }${ x ( i ). toFixed ( 1 ) } , ${ y ( v ). toFixed ( 1 ) } ` ). join ( " " );
const dot = allDates . length === 1 ? `<circle cx=" ${ x ( 0 ). toFixed ( 1 ) } " cy=" ${ y ( ln . pts [ 0 ]). toFixed ( 1 ) } " r="4" fill=" ${ ln . c . color } "/>` : "" ;
inner += `<path d=" ${ d } " fill="none" stroke=" ${ ln . c . color } " stroke-width="2.4" opacity=".95"/> ${ dot } ` ;
}
svg . innerHTML = inner ;
if ( $ ( legendId )){
$ ( legendId ). innerHTML = board (). map ( r => `<span class="leg"><span class="legdot" style="background: ${ r . c . color } "></span> ${ r . c . emoji } ${ r . c . name } <b class=" ${ signClass ( r . pnl ) } "> ${ fmtPct ( r . ret ) } </b></span>` ). join ( "" );
}
2026-06-27 15:29:34 -04:00
}
2026-06-21 16:19:34 -04:00
function renderFilterBar ( all ){
const focus = getFocus ();
2026-07-05 14:58:21 -04:00
const counts = {}; CATEGORIES . forEach ( c => counts [ c ] = 0 );
2026-06-21 16:19:34 -04:00
all . forEach ( s =>{ const c = counts [ s . category ] !== undefined ? s . category : "Other" ; counts [ c ] ++ ;});
counts [ "All" ] = all . length ;
2026-07-05 14:58:21 -04:00
$ ( "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 ();}));
2026-06-21 16:19:34 -04:00
}
function renderSuggestions (){
const data = loadSuggestions ();
$ ( "sugDate" ). textContent = data . date ? `· ${ data . date } ` : "" ;
2026-07-05 14:58:21 -04:00
const all = data . suggestions || []; const root = $ ( "suggestions" );
2026-06-21 16:19:34 -04:00
renderFilterBar ( all );
2026-07-05 14:58:21 -04:00
if ( ! all . length ){ root . innerHTML = `<div class="empty" style="grid-column:1/-1">No suggestions yet. Click "Run cycle".</div>` ; $ ( "focusNote" ). textContent = "" ; return ;}
2026-06-21 16:19:34 -04:00
const focus = getFocus ();
const filtered = ( focus === "All" ? all : all . filter ( s => s . category === focus )). slice ( 0 , 12 );
$ ( "focusNote" ). textContent = focus === "All"
2026-07-05 14:58:21 -04:00
? "All five agents trade from this pool — each in its own style."
: `Filtered to ${ focus } — agents will only open new ${ focus } positions while this is selected.` ;
if ( ! filtered . length ){ 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 } ">
2026-07-05 14:58:21 -04:00
<div class="sug-row" style="margin-bottom:2px"><span class="cat-badge" style="background: ${ col } 22;color: ${ col } "> ${ esc ( s . category || "Other" ) } </span></div>
2026-06-21 16:19:34 -04:00
<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">
2026-07-05 14:58:21 -04:00
<span>Vol <b> ${ fmtUSD ( s . volume ) } </b></span><span>24h <b> ${ fmtUSD ( s . volume_24hr ) } </b></span><span>Resolves <b> ${ days } </b></span>
2026-06-21 16:19:34 -04:00
${ s . url ? `<a class="market-link" href=" ${ s . url } " target="_blank" rel="noopener">View ↗</a>` : "" }
</div></div>` ;
}). join ( "" );
}
2026-07-05 14:58:21 -04:00
function renderPortfolioTab (){
const st = loadState ();
let viewId = localStorage . getItem ( VIEW_KEY ); if ( ! AGENTS . some ( a => a . id === viewId )) viewId = "value" ;
$ ( "agentSel" ). innerHTML = AGENTS . map ( a =>{
const active = a . id === viewId ;
const style = active ? `background:linear-gradient(135deg, ${ a . color } , ${ a . color } );border-color:transparent` : "" ;
return `<span class="segbtn ${ active ? "active" : "" } " data-agent=" ${ a . id } " style=" ${ style } "> ${ a . emoji } ${ a . name } </span>` ;}). join ( "" );
document . querySelectorAll ( "#agentSel .segbtn" ). forEach ( el => el . addEventListener ( "click" ,()=>{ localStorage . setItem ( VIEW_KEY , el . dataset . agent ); renderPortfolioTab ();}));
const p = st . agents [ viewId ] || defaultPortfolio (); const eq = equity ( p ), pnl = eq - p . starting_balance ;
const stats = [
{ ic : "💰" , label : "Equity" , value : fmtUSD ( eq )},
{ ic : "💵" , label : "Cash" , value : fmtUSD ( p . cash )},
{ ic : "📈" , label : "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" , value : p . positions . length },
];
$ ( "statsPf" ). innerHTML = stats . 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 ( "" );
renderPositions ( p . positions );
renderHistory (( p . history || []). slice ( - 50 ). reverse ());
2026-06-21 16:19:34 -04:00
}
function renderPositions ( positions ){
2026-07-05 14:58:21 -04:00
const root = $ ( "positions" ); if ( ! root ) return ;
if ( ! positions . length ){ root . innerHTML = `<div class="empty">No open positions.</div>` ; return ;}
root . innerHTML = positions . map ( p =>{ 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-07-05 14:58:21 -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 ) } ¢</div>
</div><div class="right"><div> ${ fmtUSD ( p . value ) } </div><div class=" ${ signClass ( pnl ) } "> ${ pnl >= 0 ? "+" : "" }${ fmtUSD ( pnl ) } </div></div></div>` ;}). join ( "" );
2026-06-21 16:19:34 -04:00
}
function renderHistory ( history ){
2026-07-05 14:58:21 -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 ;}
2026-07-05 14:58:21 -04:00
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 renderAbout (){
const root = $ ( "aboutAgents" ); if ( ! root ) return ;
const st = loadState ();
root . innerHTML = AGENTS . map ( a => `<div class="feature" style="border-top:3px solid ${ a . color } ">
<div class="fic"> ${ a . emoji } </div><h4> ${ a . name } </h4><p> ${ agentBlurb ( a , st ) } </p></div>` ). join ( "" );
2026-06-21 16:19:34 -04:00
}
function esc ( s ){ return String ( s ). replace ( /[&<>"]/g , c =>({ "&" : "&" , "<" : "<" , ">" : ">" , '"' : """ }[ c ]));}
2026-07-05 14:58:21 -04:00
/* ---------- Tab routing ---------- */
const TABS = [ "overview" , "leaderboard" , "suggestions" , "portfolio" , "about" ];
2026-06-27 15:29:34 -04:00
function showTab ( name ){
2026-07-05 14:58:21 -04:00
if ( ! TABS . includes ( name )) name = "overview" ;
2026-06-27 15:29:34 -04:00
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 ));
2026-07-05 14:58:21 -04:00
if ( location . hash . slice ( 1 ) !== name ) history . replaceState ( null , "" , "#" + name );
2026-06-27 15:29:34 -04:00
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…" ;
2026-07-05 14:58:21 -04:00
try { const r = await runDailyCycle (); const b = board (); toast ( `Cycle done · ${ r . suggestions } ideas · ${ b [ 0 ]. c . emoji } ${ b [ 0 ]. c . name } leads` ); 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" ,()=>{
2026-07-05 14:58:21 -04:00
if ( ! confirm ( "Reset all five agents to their $10,000 starting balance?" )) return ;
saveState ( defaultState ()); localStorage . removeItem ( SUG_KEY ); toast ( "All agents reset." ); renderAll ();
2026-06-21 16:19:34 -04:00
});
2026-07-05 14:58:21 -04:00
/* On load: route, render, 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…" ;
2026-07-05 14:58:21 -04:00
try { await runDailyCycle (); renderAll (); toast ( "Fresh daily competition ready." );}
2026-06-21 16:19:34 -04:00
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 >