Spaces:
Sleeping
Sleeping
Download decimal-bot.html from Arbitrage01/arbproxy: direct link, hf CLI and curl.
- Browser
- Download file 110 kB
-
https://huggingface.co/spaces/Arbitrage01/arbproxy/resolve/main/decimal-bot.html
- Command line
-
hf download hf://spaces/Arbitrage01/arbproxy/decimal-bot.html
-
curl -L -o decimal-bot.html https://huggingface.co/spaces/Arbitrage01/arbproxy/resolve/main/decimal-bot.html
110 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>IPL Arb v4</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js"></script> | |
| <style> | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0 | |
| } | |
| :root { | |
| --g: #00d47e; | |
| --g2: #00ff99; | |
| --r: #ff3a5e; | |
| --y: #ffd44a; | |
| --p: #9b95ff; | |
| --o: #ff8a60; | |
| --mum: #f5a623; | |
| --kol: #9b59ff; | |
| --bk: #6db3ff; | |
| --lk: #ff7099; | |
| --bg: #07090f; | |
| --s1: #090e1c; | |
| --s2: #0d1322; | |
| --bd: #1a2540; | |
| --bd2: #243050; | |
| --txt: #b8cae8; | |
| --txt2: #4a5a72; | |
| --head: #e8f0ff | |
| } | |
| body { | |
| background: var(--bg); | |
| color: var(--txt); | |
| font-family: monospace; | |
| font-size: 12px; | |
| min-height: 100vh | |
| } | |
| /* ββ HEADER ββ */ | |
| .hdr { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px 14px; | |
| background: #0a0f1e; | |
| border-bottom: 1px solid var(--bd); | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| position: sticky; | |
| top: 0; | |
| z-index: 200; | |
| backdrop-filter: blur(8px) | |
| } | |
| .logo { | |
| font-family: sans-serif; | |
| font-size: 18px; | |
| font-weight: 900; | |
| letter-spacing: 2px | |
| } | |
| .logo .lp { | |
| color: var(--p) | |
| } | |
| .logo .lo { | |
| color: var(--o) | |
| } | |
| .logo small { | |
| display: block; | |
| font-family: monospace; | |
| font-size: 9px; | |
| color: var(--txt2); | |
| letter-spacing: 3px | |
| } | |
| .hr { | |
| display: flex; | |
| gap: 6px; | |
| align-items: center; | |
| flex-wrap: wrap | |
| } | |
| .live { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| font-size: 10px; | |
| color: var(--g); | |
| letter-spacing: 1px | |
| } | |
| .dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background: var(--g); | |
| animation: blink 1.2s infinite | |
| } | |
| @keyframes blink { | |
| 50% { | |
| opacity: .15 | |
| } | |
| } | |
| .hbtn { | |
| background: var(--s2); | |
| border: 1px solid var(--bd2); | |
| color: var(--txt2); | |
| padding: 4px 10px; | |
| border-radius: 3px; | |
| cursor: pointer; | |
| font-family: monospace; | |
| font-size: 10px; | |
| letter-spacing: 1px; | |
| transition: .15s | |
| } | |
| .hbtn:hover { | |
| border-color: var(--p); | |
| color: var(--p) | |
| } | |
| .nav-link { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| #ncall { | |
| color: var(--p) | |
| } | |
| /* ββ SETTINGS ββ */ | |
| .stw { | |
| background: var(--s1); | |
| border-bottom: 1px solid var(--bd) | |
| } | |
| .sthd { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px 14px; | |
| cursor: pointer; | |
| user-select: none | |
| } | |
| .sthl { | |
| font-family: sans-serif; | |
| font-size: 12px; | |
| font-weight: 700; | |
| letter-spacing: 2px; | |
| color: var(--head) | |
| } | |
| .stsub { | |
| font-size: 10px; | |
| color: var(--txt2); | |
| margin-top: 1px | |
| } | |
| .stsub b { | |
| color: var(--y); | |
| font-weight: 400 | |
| } | |
| .stbd { | |
| display: none; | |
| padding: 0 14px 14px; | |
| border-top: 1px solid var(--bd) | |
| } | |
| .stbd.open { | |
| display: block | |
| } | |
| .sg { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| align-items: flex-end; | |
| margin-top: 10px | |
| } | |
| .f { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2px | |
| } | |
| .f label { | |
| font-size: 9px; | |
| letter-spacing: 1px; | |
| color: var(--txt2); | |
| text-transform: uppercase | |
| } | |
| .f input, | |
| .f select { | |
| background: var(--s2); | |
| border: 1px solid var(--bd); | |
| color: var(--head); | |
| padding: 5px 8px; | |
| font-family: monospace; | |
| font-size: 11px; | |
| border-radius: 3px; | |
| width: 130px; | |
| transition: .15s | |
| } | |
| .f.w input { | |
| width: 210px | |
| } | |
| .f input:focus, | |
| .f select:focus { | |
| outline: none; | |
| border-color: var(--p) | |
| } | |
| .sdiv { | |
| width: 100%; | |
| height: 1px; | |
| background: var(--bd); | |
| margin: 5px 0 | |
| } | |
| .slbl { | |
| font-size: 9px; | |
| letter-spacing: 2px; | |
| color: var(--txt2); | |
| width: 100% | |
| } | |
| .abtn { | |
| padding: 6px 14px; | |
| border-radius: 3px; | |
| border: none; | |
| cursor: pointer; | |
| font-family: monospace; | |
| font-size: 11px; | |
| font-weight: 700; | |
| letter-spacing: 1px | |
| } | |
| .abp { | |
| background: #6c63ff; | |
| color: #fff | |
| } | |
| .abb { | |
| background: #ff6030; | |
| color: #fff | |
| } | |
| .abtn:hover { | |
| opacity: .85 | |
| } | |
| /* ββ MATCH BAR ββ */ | |
| .mbar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 7px 14px; | |
| background: var(--s2); | |
| border-bottom: 1px solid var(--bd); | |
| flex-wrap: wrap; | |
| gap: 6px | |
| } | |
| .mt { | |
| font-family: sans-serif; | |
| font-size: 16px; | |
| font-weight: 900; | |
| color: var(--head); | |
| letter-spacing: 1px | |
| } | |
| .pills { | |
| display: flex; | |
| gap: 6px; | |
| flex-wrap: wrap; | |
| align-items: center | |
| } | |
| .pill { | |
| padding: 3px 12px; | |
| border-radius: 20px; | |
| font-family: sans-serif; | |
| font-size: 13px; | |
| font-weight: 700 | |
| } | |
| .pm { | |
| background: rgba(245, 166, 35, .12); | |
| color: var(--mum); | |
| border: 1px solid rgba(245, 166, 35, .3) | |
| } | |
| .pk { | |
| background: rgba(155, 89, 255, .12); | |
| color: var(--kol); | |
| border: 1px solid rgba(155, 89, 255, .3) | |
| } | |
| .mr { | |
| font-size: 10px; | |
| color: var(--txt2); | |
| display: flex; | |
| gap: 10px; | |
| flex-wrap: wrap | |
| } | |
| .mr span { | |
| color: var(--y) | |
| } | |
| /* ββ TABS ββ */ | |
| .tabs { | |
| display: flex; | |
| background: var(--s1); | |
| border-bottom: 1px solid var(--bd) | |
| } | |
| .tab { | |
| padding: 8px 14px; | |
| font-size: 11px; | |
| font-weight: 700; | |
| letter-spacing: 1px; | |
| color: var(--txt2); | |
| cursor: pointer; | |
| border-bottom: 2px solid transparent; | |
| transition: .15s | |
| } | |
| .tab.on { | |
| color: var(--p); | |
| border-bottom-color: #6c63ff | |
| } | |
| .tc { | |
| display: none | |
| } | |
| .tc.on { | |
| display: block | |
| } | |
| /* ββ ORDER BOOKS ββ */ | |
| .obg { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| border-bottom: 1px solid var(--bd) | |
| } | |
| @media(max-width:620px) { | |
| .obg { | |
| grid-template-columns: 1fr | |
| } | |
| } | |
| .obp { | |
| border-right: 1px solid var(--bd) | |
| } | |
| .obp:last-child { | |
| border-right: none | |
| } | |
| .obh { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 7px 12px; | |
| border-bottom: 1px solid var(--bd); | |
| background: rgba(255, 255, 255, .01) | |
| } | |
| .obt { | |
| font-family: sans-serif; | |
| font-size: 13px; | |
| font-weight: 700; | |
| letter-spacing: 2px | |
| } | |
| .obt.p { | |
| color: var(--p) | |
| } | |
| .obt.b { | |
| color: var(--o) | |
| } | |
| .bdg { | |
| font-size: 9px; | |
| padding: 2px 7px; | |
| border-radius: 10px; | |
| letter-spacing: 1px | |
| } | |
| .bdg-p { | |
| background: rgba(108, 99, 255, .15); | |
| color: var(--p); | |
| border: 1px solid rgba(108, 99, 255, .25) | |
| } | |
| .bdg-b { | |
| background: rgba(255, 96, 48, .15); | |
| color: var(--o); | |
| border: 1px solid rgba(255, 96, 48, .25) | |
| } | |
| .bdg-ok { | |
| background: rgba(0, 212, 126, .12); | |
| color: var(--g); | |
| border: 1px solid rgba(0, 212, 126, .25) | |
| } | |
| .bdg-w { | |
| background: rgba(255, 212, 74, .12); | |
| color: var(--y); | |
| border: 1px solid rgba(255, 212, 74, .25) | |
| } | |
| .pmeta { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 5px 12px; | |
| border-bottom: 1px solid var(--bd); | |
| color: var(--txt2); | |
| font-size: 10px; | |
| flex-wrap: wrap; | |
| gap: 4px | |
| } | |
| .pmeta span { | |
| color: var(--head) | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse | |
| } | |
| th { | |
| padding: 4px 8px; | |
| font-size: 9px; | |
| letter-spacing: 1px; | |
| color: var(--txt2); | |
| text-align: right; | |
| border-bottom: 1px solid var(--bd); | |
| font-weight: 400 | |
| } | |
| th:first-child { | |
| text-align: left | |
| } | |
| td { | |
| padding: 5px 8px; | |
| text-align: right; | |
| border-bottom: 1px solid rgba(26, 37, 64, .4); | |
| font-size: 11px | |
| } | |
| td:first-child { | |
| text-align: left | |
| } | |
| tr.ask td { | |
| color: var(--lk) | |
| } | |
| tr.bid td { | |
| color: var(--g) | |
| } | |
| tr.ask td.pr { | |
| color: var(--r); | |
| font-weight: 700 | |
| } | |
| tr.bid td.pr { | |
| color: var(--g2); | |
| font-weight: 700 | |
| } | |
| tr.spr td { | |
| text-align: center !important; | |
| color: var(--y); | |
| font-size: 9px; | |
| background: rgba(255, 212, 74, .03); | |
| padding: 3px; | |
| border-bottom: 1px solid var(--bd) !important | |
| } | |
| tr:hover td { | |
| background: rgba(255, 255, 255, .015) | |
| } | |
| /* BHAV */ | |
| .bhdr { | |
| display: grid; | |
| grid-template-columns: repeat(6, 1fr); | |
| border-bottom: 1px solid var(--bd) | |
| } | |
| .bh { | |
| text-align: center; | |
| font-size: 8px; | |
| padding: 3px 0; | |
| letter-spacing: 1px | |
| } | |
| .bh.bk { | |
| color: var(--bk); | |
| background: rgba(46, 143, 255, .05) | |
| } | |
| .bh.lk { | |
| color: var(--r); | |
| background: rgba(255, 58, 110, .05) | |
| } | |
| .brow { | |
| border-bottom: 1px solid var(--bd) | |
| } | |
| .brh { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 5px 12px; | |
| background: rgba(255, 255, 255, .01) | |
| } | |
| .btn { | |
| font-family: sans-serif; | |
| font-size: 13px; | |
| font-weight: 700 | |
| } | |
| .btn.m { | |
| color: var(--mum) | |
| } | |
| .btn.k { | |
| color: var(--kol) | |
| } | |
| .bcells { | |
| display: grid; | |
| grid-template-columns: repeat(6, 1fr) | |
| } | |
| .bc { | |
| padding: 6px 3px; | |
| text-align: center; | |
| border-right: 1px solid var(--bd) | |
| } | |
| .bc:last-child { | |
| border-right: none | |
| } | |
| .bc.bk { | |
| background: rgba(46, 143, 255, .05) | |
| } | |
| .bc.lk { | |
| background: rgba(255, 58, 110, .05) | |
| } | |
| .bc.bbk { | |
| background: rgba(46, 143, 255, .22) !important | |
| } | |
| .bc.blk { | |
| background: rgba(255, 58, 110, .24) !important | |
| } | |
| .bo { | |
| font-size: 12px; | |
| font-weight: 700 | |
| } | |
| .bc.bk .bo { | |
| color: var(--bk) | |
| } | |
| .bc.lk .bo { | |
| color: var(--lk) | |
| } | |
| .bc.bbk .bo, | |
| .bc.blk .bo { | |
| color: #fff | |
| } | |
| .bl { | |
| font-size: 9px; | |
| color: var(--txt2); | |
| margin-top: 1px | |
| } | |
| /* ββ CALCULATOR ββ */ | |
| .cw { | |
| padding: 12px 14px | |
| } | |
| .ctit { | |
| font-family: sans-serif; | |
| font-size: 16px; | |
| font-weight: 900; | |
| letter-spacing: 2px; | |
| color: var(--head); | |
| margin-bottom: 10px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px | |
| } | |
| .ctit::after { | |
| content: ''; | |
| flex: 1; | |
| height: 1px; | |
| background: var(--bd) | |
| } | |
| .ci { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin-bottom: 10px; | |
| align-items: flex-end | |
| } | |
| /* DIRECTION SELECTOR */ | |
| .dir-wrap { | |
| margin-bottom: 10px | |
| } | |
| .dir-lbl { | |
| font-size: 9px; | |
| letter-spacing: 2px; | |
| color: var(--txt2); | |
| margin-bottom: 5px | |
| } | |
| .dir-btns { | |
| display: flex; | |
| gap: 6px; | |
| flex-wrap: wrap | |
| } | |
| .dir-btn { | |
| padding: 5px 14px; | |
| border-radius: 4px; | |
| border: 1px solid var(--bd); | |
| background: var(--s2); | |
| color: var(--txt2); | |
| font-family: monospace; | |
| font-size: 11px; | |
| font-weight: 700; | |
| cursor: pointer; | |
| letter-spacing: 1px; | |
| transition: .15s | |
| } | |
| .dir-btn:hover { | |
| border-color: var(--head); | |
| color: var(--head) | |
| } | |
| .dir-btn.on.both { | |
| border-color: var(--g); | |
| color: var(--g); | |
| background: rgba(0, 212, 126, .08) | |
| } | |
| .dir-btn.on.mum { | |
| border-color: var(--mum); | |
| color: var(--mum); | |
| background: rgba(245, 166, 35, .08) | |
| } | |
| .dir-btn.on.kol { | |
| border-color: var(--kol); | |
| color: var(--kol); | |
| background: rgba(155, 89, 255, .08) | |
| } | |
| /* SLIPPAGE */ | |
| .slrow { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| flex-wrap: wrap; | |
| margin-bottom: 10px | |
| } | |
| .sllbl { | |
| font-size: 10px; | |
| color: var(--txt2); | |
| white-space: nowrap | |
| } | |
| .slbtn { | |
| padding: 3px 8px; | |
| border-radius: 3px; | |
| border: 1px solid var(--bd); | |
| background: var(--s2); | |
| color: var(--txt2); | |
| font-family: monospace; | |
| font-size: 10px; | |
| cursor: pointer; | |
| transition: .15s | |
| } | |
| .slbtn:hover, | |
| .slbtn.on { | |
| border-color: var(--y); | |
| color: var(--y); | |
| background: rgba(255, 212, 74, .07) | |
| } | |
| /* DEPLOY BAR */ | |
| .dbar { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 10px | |
| } | |
| .dbg { | |
| flex: 1; | |
| height: 4px; | |
| background: var(--bd); | |
| border-radius: 2px; | |
| overflow: hidden | |
| } | |
| .dbf { | |
| height: 100%; | |
| border-radius: 2px; | |
| background: linear-gradient(90deg, var(--p), var(--o)); | |
| transition: .4s | |
| } | |
| /* RESULTS */ | |
| .rt { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 11px | |
| } | |
| .rt th { | |
| padding: 5px 8px; | |
| font-size: 9px; | |
| letter-spacing: 1px; | |
| color: var(--txt2); | |
| text-align: right; | |
| border-bottom: 1px solid var(--bd); | |
| font-weight: 400; | |
| background: var(--s2); | |
| white-space: nowrap | |
| } | |
| .rt th:first-child { | |
| text-align: left | |
| } | |
| .rt td { | |
| padding: 6px 8px; | |
| text-align: right; | |
| border-bottom: 1px solid rgba(26, 37, 64, .4); | |
| white-space: nowrap | |
| } | |
| .rt td:first-child { | |
| text-align: left; | |
| color: var(--txt2); | |
| font-size: 10px | |
| } | |
| .rt tr.mx { | |
| background: rgba(0, 212, 126, .06); | |
| border-left: 2px solid var(--g) | |
| } | |
| .rt tr.mx td:first-child { | |
| color: var(--g); | |
| font-weight: 700 | |
| } | |
| .rt tr:hover td { | |
| background: rgba(255, 255, 255, .015) | |
| } | |
| .pos { | |
| color: var(--g2) !important; | |
| font-weight: 700 | |
| } | |
| .neg { | |
| color: var(--r) !important; | |
| font-weight: 700 | |
| } | |
| .hl { | |
| color: var(--head); | |
| font-weight: 700 | |
| } | |
| .ow { | |
| overflow-x: auto | |
| } | |
| /* SUMMARY CARDS */ | |
| .smg { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); | |
| gap: 6px; | |
| margin-top: 12px | |
| } | |
| .smc { | |
| background: var(--s2); | |
| border-radius: 4px; | |
| padding: 8px 10px; | |
| border: 1px solid var(--bd) | |
| } | |
| .sml { | |
| font-size: 9px; | |
| letter-spacing: 1px; | |
| color: var(--txt2); | |
| margin-bottom: 3px | |
| } | |
| .smv { | |
| font-family: sans-serif; | |
| font-size: 22px; | |
| font-weight: 900; | |
| color: var(--head); | |
| line-height: 1 | |
| } | |
| .sms { | |
| font-size: 9px; | |
| color: var(--txt2); | |
| margin-top: 2px | |
| } | |
| /* ββ CHART ββ */ | |
| .chwrap { | |
| padding: 12px 14px | |
| } | |
| .ch-hdr { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 10px; | |
| flex-wrap: wrap; | |
| gap: 6px | |
| } | |
| .ch-leg { | |
| display: flex; | |
| gap: 12px | |
| } | |
| .ch-li { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| font-size: 10px | |
| } | |
| .ch-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50% | |
| } | |
| canvas { | |
| max-height: 220px | |
| } | |
| /* ββ SCREEN FLASH ββ */ | |
| .flash-overlay { | |
| display: none; | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0, 212, 126, .08); | |
| z-index: 400; | |
| pointer-events: none; | |
| animation: flashFade .8s ease-out forwards | |
| } | |
| @keyframes flashFade { | |
| 0% { | |
| opacity: 1 | |
| } | |
| 100% { | |
| opacity: 0 | |
| } | |
| } | |
| /* ββ MISC ββ */ | |
| .nodata { | |
| padding: 20px; | |
| text-align: center; | |
| color: var(--txt2) | |
| } | |
| footer { | |
| padding: 8px 14px; | |
| background: var(--s1); | |
| border-top: 1px solid var(--bd); | |
| display: flex; | |
| justify-content: space-between; | |
| flex-wrap: wrap; | |
| gap: 4px; | |
| font-size: 9px; | |
| color: var(--txt2); | |
| letter-spacing: 1px | |
| } | |
| @media(max-width:500px) { | |
| .rt th, | |
| .rt td { | |
| padding: 4px 5px; | |
| font-size: 10px | |
| } | |
| .f input, | |
| .f select { | |
| width: 110px | |
| } | |
| .ci .f input { | |
| width: 110px | |
| } | |
| } | |
| .calc-panels { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| } | |
| @media(max-width: 600px) { | |
| .calc-panels { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .calc-panel { | |
| background: #0f121b; | |
| border: 1px solid var(--bd); | |
| border-radius: 6px; | |
| padding: 12px 16px; | |
| } | |
| .cp-tit { | |
| font-family: monospace; | |
| font-size: 10px; | |
| color: #7b859a; | |
| letter-spacing: 1px; | |
| margin-bottom: 12px; | |
| text-transform: uppercase; | |
| display: flex; | |
| justify-content: space-between; | |
| } | |
| .cp-cols { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 15px; | |
| } | |
| .cp-col { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .cp-lbl { | |
| font-size: 9px; | |
| color: #5d6b82; | |
| margin-bottom: 5px; | |
| letter-spacing: 1px; | |
| } | |
| .cp-input { | |
| background: #090b10; | |
| border: 1px solid var(--bd); | |
| color: var(--head); | |
| font-family: monospace; | |
| font-size: 14px; | |
| font-weight: 700; | |
| padding: 6px 10px; | |
| border-radius: 4px; | |
| outline: none; | |
| transition: 0.15s; | |
| width: 100%; | |
| box-sizing: border-box; | |
| } | |
| .cp-input:focus { | |
| border-color: var(--p); | |
| } | |
| .cp-sub { | |
| margin-top: 6px; | |
| font-size: 9px; | |
| font-weight: 700; | |
| min-height: 12px; | |
| } | |
| .cp-up { | |
| color: var(--g); | |
| } | |
| .cp-dn { | |
| color: var(--r); | |
| } | |
| .arb-toggle { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 15px; | |
| } | |
| .arb-toggle-lbl { | |
| font-size: 10px; | |
| color: #7b859a; | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| } | |
| .arb-btn { | |
| background: var(--bg); | |
| border: 1px solid var(--bd); | |
| color: var(--txt2); | |
| padding: 5px 12px; | |
| font-family: monospace; | |
| font-size: 10px; | |
| font-weight: 700; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| letter-spacing: 1px; | |
| transition: 0.15s; | |
| } | |
| .arb-btn.on.m { | |
| border-color: var(--mum); | |
| color: var(--mum); | |
| background: rgba(245, 166, 35, 0.08); | |
| } | |
| .arb-btn.on.k { | |
| border-color: var(--kol); | |
| color: var(--kol); | |
| background: rgba(155, 89, 255, 0.08); | |
| } | |
| .w-alloc { | |
| background: #0f121b; | |
| border: 1px solid var(--bd); | |
| border-radius: 6px; | |
| padding: 12px; | |
| margin-bottom: 15px; | |
| border-color: rgba(255, 255, 255, 0.05); | |
| } | |
| .wa-tit { | |
| font-size: 10px; | |
| color: #7b859a; | |
| letter-spacing: 1px; | |
| margin-bottom: 8px; | |
| text-transform: uppercase; | |
| } | |
| .wa-bar-wrap { | |
| height: 10px; | |
| background: var(--bg); | |
| border-radius: 4px; | |
| display: flex; | |
| overflow: hidden; | |
| margin-bottom: 8px; | |
| } | |
| .wa-bhav { | |
| background: #ff8a60; | |
| transition: 0.4s; | |
| } | |
| .wa-poly { | |
| background: #6c63ff; | |
| transition: 0.4s; | |
| } | |
| .wa-free { | |
| background: #243050; | |
| transition: 0.4s; | |
| } | |
| .wa-leg { | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 9px; | |
| color: var(--txt2); | |
| } | |
| .wa-leg-left { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| } | |
| .wa-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| .wa-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 2px; | |
| } | |
| .wa-item.b .wa-dot { | |
| background: #ff8a60; | |
| } | |
| .wa-item.p .wa-dot { | |
| background: #6c63ff; | |
| } | |
| .wa-item.f .wa-dot { | |
| background: #243050; | |
| } | |
| .wa-item span { | |
| font-weight: 700; | |
| } | |
| .wa-item.b span { | |
| color: #ff8a60; | |
| } | |
| .wa-item.p span { | |
| color: #9b95ff; | |
| } | |
| .wa-roi { | |
| color: var(--y); | |
| font-weight: 700; | |
| font-size: 10px; | |
| } | |
| .rth { | |
| background: #0f121b !important; | |
| } | |
| .rth-g { | |
| font-size: 9px; | |
| text-align: center !important; | |
| padding: 6px !important; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.05) !important; | |
| letter-spacing: 1px; | |
| color: #7b859a; | |
| } | |
| .rth-gw { | |
| background: #0f121b; | |
| color: var(--g) !important; | |
| } | |
| /* ββ ARB LIVE BOX ββ */ | |
| .arb-box { | |
| display: flex; | |
| align-items: stretch; | |
| background: var(--s1); | |
| border: 1px solid var(--bd); | |
| border-radius: 6px; | |
| margin: 8px 14px; | |
| overflow: hidden; | |
| min-height: 80px; | |
| } | |
| .arb-box-left { | |
| flex: 1; | |
| padding: 10px 14px; | |
| border-right: 1px solid var(--bd); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 4px; | |
| text-align: center; | |
| } | |
| .arb-box-mid { | |
| width: 150px; | |
| min-width: 130px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 8px 6px; | |
| background: var(--s2); | |
| } | |
| .arb-box-right { | |
| flex: 1; | |
| padding: 10px 14px; | |
| border-left: 1px solid var(--bd); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 4px; | |
| text-align: center; | |
| } | |
| .arb-box-label { | |
| font-size: 9px; | |
| letter-spacing: 2px; | |
| color: var(--txt2); | |
| text-transform: uppercase; | |
| } | |
| .arb-box-team { | |
| font-size: 11px; | |
| color: var(--head); | |
| font-weight: 700; | |
| letter-spacing: 1px; | |
| } | |
| .arb-box-big { | |
| font-size: 28px; | |
| font-weight: 900; | |
| font-family: sans-serif; | |
| line-height: 1; | |
| color: var(--head); | |
| } | |
| .arb-box-sub { | |
| font-size: 11px; | |
| color: var(--txt2); | |
| margin-top: 2px; | |
| } | |
| .arb-box-meta { | |
| margin-top: 4px; | |
| font-size: 10px; | |
| color: var(--txt2); | |
| text-align: center; | |
| line-height: 1.35; | |
| } | |
| .arb-alt-list { | |
| margin-top: 10px; | |
| display: grid; | |
| gap: 6px; | |
| } | |
| .arb-alt-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 6px 8px; | |
| border: 1px solid var(--bd2); | |
| border-radius: 6px; | |
| background: rgba(26, 37, 64, .22); | |
| font-size: 10px; | |
| color: var(--txt2); | |
| } | |
| .arb-alt-item b { | |
| color: var(--txt); | |
| font-size: 11px; | |
| } | |
| .arb-alt-pos { | |
| color: var(--g); | |
| } | |
| .arb-alt-neg { | |
| color: var(--r); | |
| } | |
| .arb-pct { | |
| font-size: 20px; | |
| font-weight: 900; | |
| font-family: sans-serif; | |
| line-height: 1; | |
| max-width: 100%; | |
| text-align: center; | |
| } | |
| .arb-pct.arb-yes { | |
| color: var(--g); | |
| } | |
| .arb-pct.arb-no { | |
| color: var(--r); | |
| } | |
| .arb-pct-lbl { | |
| font-size: 9px; | |
| letter-spacing: 2px; | |
| margin-top: 4px; | |
| color: var(--txt2); | |
| } | |
| .arb-pct-lbl.arb-yes { | |
| color: var(--g); | |
| } | |
| .arb-pct-lbl.arb-no { | |
| color: var(--r); | |
| } | |
| .arb-box-cents { | |
| font-size: 22px; | |
| font-weight: 900; | |
| color: var(--p); | |
| font-family: sans-serif; | |
| } | |
| /* ββ POLY TEAM TABS ββ */ | |
| .poly-tab { | |
| flex: 1; | |
| padding: 4px 8px; | |
| border-radius: 3px; | |
| border: 1px solid var(--bd); | |
| background: var(--s2); | |
| color: var(--txt2); | |
| font-family: monospace; | |
| font-size: 10px; | |
| font-weight: 700; | |
| letter-spacing: 1px; | |
| cursor: pointer; | |
| transition: .15s; | |
| text-transform: uppercase; | |
| } | |
| .poly-tab:hover { | |
| border-color: var(--p); | |
| color: var(--p); | |
| } | |
| .poly-tab.on { | |
| border-color: var(--p); | |
| color: var(--p); | |
| background: rgba(108, 99, 255, .12); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- FLASH OVERLAY --> | |
| <div class="flash-overlay" id="flashOv"></div> | |
| <!-- HEADER --> | |
| <div class="hdr"> | |
| <div class="logo"><span class="lp">POLY</span>Γ<span class="lo">BHAV</span><small>IPL ARB v4</small></div> | |
| <div class="hr"> | |
| <span class="live"><span class="dot"></span>LIVE</span> | |
| <span style="font-size:10px;color:var(--txt2)">CALLS:<span id="ncall">0</span></span> | |
| <!-- Navigation Buttons --> | |
| <div style="display:flex; gap:6px; margin-right:10px; border-right:1px solid var(--bd); padding-right:10px; position: relative; z-index: 300;"> | |
| <a href="/login" target="_blank" class="hbtn nav-link" style="text-decoration:none; color:var(--y); border-color:var(--y)">π LOGIN</a> | |
| <a href="/save-session" target="_blank" class="hbtn nav-link" style="text-decoration:none; color:var(--g); border-color:var(--g)">πΎ SAVE SESSION</a> | |
| <a href="/debug" target="_blank" class="hbtn nav-link" style="text-decoration:none; color:var(--p); border-color:var(--p)">πͺ² DEBUG</a> | |
| </div> | |
| <button class="hbtn" onclick="toggleStg()">β SETTINGS</button> | |
| <button class="hbtn" onclick="refreshAll()">β³ REFRESH</button> | |
| </div> | |
| </div> | |
| <!-- SETTINGS --> | |
| <div class="stw"> | |
| <div class="sthd" onclick="toggleStg()"> | |
| <div> | |
| <div class="sthl">β SETTINGS</div> | |
| <div class="stsub">Match:<b id="sbm">MI vs KKR</b> Β· Capital:<b id="sbc">βΉ1,00,000</b> Β· INR:<b | |
| id="sbi">βΉ84</b> Β· Fallback Poll:<b id="sbr">5s</b> Β· MinGap:<b id="sbg">2.0%</b> Β· Alert | |
| Hold:<b id="sah">6s</b></div> | |
| </div> | |
| <span id="sarr" style="font-size:10px;color:var(--txt2)">β²</span> | |
| </div> | |
| <div class="stbd open" id="stbd"> | |
| <div class="sg"> | |
| <div class="slbl">MATCH</div> | |
| <div class="f"><label>Match Name</label><input id="sMatch" value="MI vs KKR" oninput="onMN()" /></div> | |
| <div class="f"><label id="sT1Lbl">Team 1 (MUM side)</label><input id="sT1" value="Mumbai Indians" oninput="onT()" /> | |
| </div> | |
| <div class="f"><label id="sT2Lbl">Team 2 (KOL side)</label><input id="sT2" value="Kolkata Knight Riders" | |
| oninput="onT()" /></div> | |
| <div class="sdiv"></div> | |
| <div class="slbl">DATA SOURCES</div> | |
| <div class="f w"><label>Polymarket URL / Condition ID</label><input id="sPolyUrl" | |
| placeholder="https://polymarket.com/event/... or token ID" /></div> | |
| <div class="f w"><label>Rebel777 Match URL <span style="color:var(--g);font-size:9px">β | |
| LIVE</span></label><input id="sRebel777Url" placeholder="https://rebel777.co/sport/game/..." | |
| oninput="onRebel777UrlChange()" /></div> | |
| <div class="f"><label>Fallback Poll</label> | |
| <select id="sRef" onchange="onRC()"> | |
| <option value="2">2s</option> | |
| <option value="3">3s</option> | |
| <option value="5" selected>5s</option> | |
| <option value="10">10s</option> | |
| <option value="15">15s</option> | |
| <option value="0">Off</option> | |
| </select> | |
| </div> | |
| <div> | |
| <button class="abtn abp" onclick="fetchPoly()" style="margin-right:6px">⬑ LOAD POLY</button> | |
| <button class="abtn" onclick="fetchRebel777()" | |
| style="background:#1a7a4a;color:#fff;margin-right:6px">β‘ START REBEL777</button> | |
| <button class="abtn" onclick="stopRebel777()" | |
| style="background:#3a1a1a;color:#ff6060;border:1px solid #5a2a2a;margin-right:6px">β | |
| STOP</button> | |
| <div style="font-size:9px;color:var(--txt2);margin-top:4px">Session calls: <span id="ncall2" | |
| style="color:var(--p)">0</span></div> | |
| </div> | |
| <div class="sdiv"></div> | |
| <div class="slbl">CAPITAL & RATES</div> | |
| <div class="f"><label>Capital (βΉ)</label><input type="number" id="sCap" value="100000" step="5000" | |
| oninput="onCap()" /></div> | |
| <div class="f"><label>Fixed Rebel Stake (βΉ)</label><input type="number" id="sRebelStake" | |
| value="20000" step="1000" oninput="onRebelStake()" /></div> | |
| <div class="f"><label>Bhav Win Step (βΉ)</label><input type="number" id="sStep" value="5000" step="1000" | |
| oninput="onStep()" /></div> | |
| <div class="f"><label>USD β INR (βΉ)</label><input type="number" id="sInr" value="84" step="0.5" | |
| oninput="onInr()" /></div> | |
| <div class="sdiv"></div> | |
| <div class="slbl">ARB ALERTS</div> | |
| <div class="f"><label>Min Gap Threshold</label><input type="number" id="sMinGap" value="2.0" step="0.5" | |
| oninput="onMG()" /></div> | |
| <div class="f"><label>Alert Hold (s)</label><input type="number" id="sAlertHold" value="6" min="1" | |
| step="1" oninput="onAH()" /></div> | |
| <div class="f w"><label>Telegram Bot Token</label><input id="sTgToken" | |
| placeholder="123456:ABC-DEF..." /></div> | |
| <div class="f"><label>Telegram Chat ID</label><input id="sTgChat" placeholder="-1001234567890" /></div> | |
| <div class="f"><label>Sound Alert</label> | |
| <select id="sSnd"> | |
| <option value="1">On</option> | |
| <option value="0">Off</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- MATCH BAR --> | |
| <div class="mbar"> | |
| <div class="pills"> | |
| <div class="mt" id="mTitle">MI vs KKR</div> | |
| <div class="pill pm"><span id="mumLbl">MUM</span> <span id="mumO">β</span></div> | |
| <div class="pill pk"><span id="kolLbl">KOL</span> <span id="kolO">β</span></div> | |
| </div> | |
| <div class="mr">1 USD=<span id="inrD">βΉ84</span> Β· Updated:<span id="tsD">β</span></div> | |
| </div> | |
| <!-- LIVE ARB BOX --> | |
| <div class="arb-box" id="arbBox"> | |
| <div class="arb-box-left"> | |
| <div class="arb-box-label">POLYMARKET</div> | |
| <div class="arb-box-team" id="abPolyTeam">β</div> | |
| <div class="arb-box-big" id="abPolyShares">β</div> | |
| <div class="arb-box-sub" id="abPolySharesLbl">net shares after fee</div> | |
| <div class="arb-box-meta" id="abPolyGross">gross β Β· fee β</div> | |
| <div class="arb-box-cents" id="abPolyCents">β</div> | |
| <div class="arb-box-sub">best odds (Β’)</div> | |
| </div> | |
| <div class="arb-box-mid"> | |
| <div class="arb-pct arb-no" id="abPct">β</div> | |
| <div class="arb-pct-lbl arb-no" id="abPctLbl">NO ARB</div> | |
| </div> | |
| <div class="arb-box-right"> | |
| <div class="arb-box-label">REBEL777 LAY</div> | |
| <div class="arb-box-team" id="abRebelTeam">β</div> | |
| <div class="arb-box-big" id="abRebelOdds">β</div> | |
| <div class="arb-box-sub">best lay odds</div> | |
| <div style="font-size:11px;color:var(--txt2);margin-top:4px">Stake: <span | |
| style="color:#0f0;font-size:13px;font-weight:700" id="abRebelLayStake">βΉ20,000</span></div> | |
| <div style="font-size:11px;color:var(--txt2);margin-top:2px">Liability: <span | |
| style="color:var(--y);font-size:13px;font-weight:700" id="abRebelStake">β</span></div> | |
| <div class="arb-alt-list"> | |
| <div class="arb-alt-item" id="abLayAlt1">L1 β</div> | |
| <div class="arb-alt-item" id="abLayAlt2">L2 β</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- TABS --> | |
| <div class="tabs"> | |
| <div class="tab on" onclick="swTab(this,'ob')">ORDER BOOKS</div> | |
| <div class="tab" onclick="swTab(this,'calc')">ARB CALCULATOR</div> | |
| <div class="tab" onclick="swTab(this,'chart')">ODDS CHART</div> | |
| </div> | |
| <!-- ORDER BOOKS TAB --> | |
| <div class="tc on" id="tc-ob"> | |
| <div class="obg"> | |
| <div class="obp"> | |
| <div class="obh" style="flex-direction:column;align-items:stretch;gap:6px;padding-bottom:8px"> | |
| <div style="display:flex;align-items:center;justify-content:space-between"> | |
| <span class="obt p">POLYMARKET</span> | |
| <span class="bdg bdg-p" id="pSt">NOT LOADED</span> | |
| </div> | |
| <div style="display:flex;gap:6px" id="polyTeamTabs"> | |
| <button class="poly-tab on" id="polyTab0" onclick="switchPolyTab(0, true)">β</button> | |
| <button class="poly-tab" id="polyTab1" onclick="switchPolyTab(1, true)">β</button> | |
| </div> | |
| </div> | |
| <div class="pmeta">LAST:<span id="pLast">β</span> SPREAD:<span id="pSprd">β</span> VOL:<span | |
| id="pVol">β</span></div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>SIDE</th> | |
| <th>ODDS (Β’)</th> | |
| <th>SHARES</th> | |
| <th>$ TOTAL</th> | |
| </tr> | |
| </thead> | |
| <tbody id="pBook"> | |
| <tr> | |
| <td colspan="4"> | |
| <div class="nodata">Load Polymarket URL</div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| <div class="obp"> | |
| <div class="obh"><span class="obt b">REBEL777 RATES</span><span class="bdg bdg-b" id="bSt">NOT | |
| LOADED</span> | |
| </div> | |
| <div class="bhdr"> | |
| <div class="bh bk">BACK</div> | |
| <div class="bh bk">BACK</div> | |
| <div class="bh bk">BACKβ </div> | |
| <div class="bh lk">LAYβ </div> | |
| <div class="bh lk">LAY</div> | |
| <div class="bh lk">LAY</div> | |
| </div> | |
| <div id="bBook"> | |
| <div class="nodata">Paste Rebel777 match URL β START</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- CALCULATOR TAB --> | |
| <div class="tc" id="tc-calc"> | |
| <div class="cw"> | |
| <div class="ctit">ARB CALCULATOR</div> | |
| <!-- RATE INPUTS --> | |
| <!-- RATE INPUTS --> | |
| <div class="calc-panels"> | |
| <div class="calc-panel"> | |
| <div class="cp-tit">POLYMARKET ASK <span | |
| style="text-transform:none;opacity:0.6;font-weight:400">(cheapest to buy)</span></div> | |
| <div class="cp-cols"> | |
| <div class="cp-col"> | |
| <label class="cp-lbl"><span id="cpL1">MUM</span> YES ASK</label> | |
| <input type="number" id="cP1" class="cp-input" value="2.18" step="0.01" | |
| oninput="onCalcManualInput()" /> | |
| <div class="cp-sub" id="cpS1"></div> | |
| </div> | |
| <div class="cp-col"> | |
| <label class="cp-lbl"><span id="cpL2">KOL</span> YES ASK</label> | |
| <input type="number" id="cP2" class="cp-input" value="1.82" step="0.01" | |
| oninput="onCalcManualInput()" /> | |
| <div class="cp-sub" id="cpS2"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="calc-panel"> | |
| <div class="cp-tit">BHAV / BETFAIR LAY odds</div> | |
| <div class="cp-cols"> | |
| <div class="cp-col"> | |
| <label class="cp-lbl"><span id="cbL1">MUM</span> LAY</label> | |
| <input type="number" id="cB1" class="cp-input" value="2.00" step="0.01" | |
| oninput="onCalcManualInput()" /> | |
| <div class="cp-sub" id="cbS1"></div> | |
| </div> | |
| <div class="cp-col"> | |
| <label class="cp-lbl"><span id="cbL2">KOL</span> LAY</label> | |
| <input type="number" id="cB2" class="cp-input" value="2.00" step="0.01" | |
| oninput="onCalcManualInput()" /> | |
| <div class="cp-sub" id="cbS2"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- WALLET ALLOCATION --> | |
| <div class="w-alloc"> | |
| <div class="wa-tit" style="display:flex;justify-content:space-between"> | |
| <span>WALLET ALLOCATION <span style="text-transform:none;opacity:0.6">(best row β)</span></span> | |
| <span class="wa-roi" id="waRoi">Guaranteed ROI: +0.00%</span> | |
| </div> | |
| <div class="wa-bar-wrap"> | |
| <div class="wa-bhav" id="waBhav" style="width:0%"></div> | |
| <div class="wa-poly" id="waPoly" style="width:0%"></div> | |
| <div class="wa-free" id="waFree" style="width:100%"></div> | |
| </div> | |
| <div class="wa-leg"> | |
| <div class="wa-leg-left"> | |
| <div class="wa-item b"> | |
| <div class="wa-dot"></div><span id="waBhavT">Bhav: βΉ0 (0.0%)</span> | |
| </div> | |
| <div class="wa-item p"> | |
| <div class="wa-dot"></div><span id="waPolyT">Poly: βΉ0 = $0 (0.0%)</span> | |
| </div> | |
| <div class="wa-item f"> | |
| <div class="wa-dot"></div><span id="waFreeT">Free: βΉ100,000 (100.0%)</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- RESULTS TABLE --> | |
| <div class="ow"> | |
| <table class="rt"> | |
| <thead> | |
| <tr> | |
| <th colspan="6" class="rth-g rth-gw" id="btnBoth" onclick="setMode('both')" | |
| style="cursor:pointer; transition:0.2s; user-select:none">BOTH OUTCOMES</th> | |
| <th colspan="1" class="rth-g" id="btnM1" onclick="setMode('m1')" | |
| style="cursor:pointer; border-left:1px solid #1a2540; transition:0.2s; user-select:none"> | |
| MUM WINS | |
| </th> | |
| <th colspan="3" class="rth-g" id="btnM2" onclick="setMode('m2')" | |
| style="cursor:pointer; border-left:1px solid #1a2540; transition:0.2s; user-select:none"> | |
| KOL WINS | |
| </th> | |
| </tr> | |
| <tr> | |
| <th class="rth">BHAV RETURN IF LAY WINS (βΉ)</th> | |
| <th class="rth">BHAV LIABILITY (βΉ)</th> | |
| <th class="rth">POLY BUY ($ / Β’)</th> | |
| <th class="rth">POLY SHARES (GROSS β NET)</th> | |
| <th class="rth">TOTAL DEPLOYED (βΉ)</th> | |
| <th class="rth">UNUSED CAPITAL (βΉ)</th> | |
| <th class="rth" id="thWin">IF MUM WINS</th> | |
| <th class="rth" id="thLose">IF MUM LOSES</th> | |
| <th class="rth" id="thMinP">GUARANTEED PROFIT</th> | |
| <th class="rth">ROI ON DEPLOYED %</th> | |
| </tr> | |
| </thead> | |
| <tbody id="calcTb"> | |
| <tr> | |
| <td colspan="10"> | |
| <div class="nodata">Loading calculator...</div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- CHART TAB --> | |
| <div class="tc" id="tc-chart"> | |
| <div class="chwrap"> | |
| <div class="ch-hdr"> | |
| <div style="font-family:sans-serif;font-size:14px;font-weight:700;color:var(--head)">ODDS HISTORY β | |
| Since Page Load</div> | |
| <div class="ch-leg"> | |
| <div class="ch-li"> | |
| <div class="ch-dot" style="background:var(--p)"></div><span style="color:var(--txt2)">Poly | |
| YES</span> | |
| </div> | |
| <div class="ch-li"> | |
| <div class="ch-dot" style="background:var(--lk)"></div><span style="color:var(--txt2)">Bhav | |
| LAY</span> | |
| </div> | |
| <div class="ch-li"> | |
| <div class="ch-dot" style="background:var(--g)"></div><span style="color:var(--txt2)">Gap | |
| %</span> | |
| </div> | |
| </div> | |
| </div> | |
| <canvas id="oddsChart"></canvas> | |
| <div style="margin-top:8px;font-size:10px;color:var(--txt2);text-align:center" id="chartInfo">No data yet β | |
| load rates to start tracking</div> | |
| </div> | |
| </div> | |
| <footer> | |
| <span>POLYΓBHAV ARB v4</span> | |
| <span>β OBSERVATION ONLY β NOT FINANCIAL ADVICE</span> | |
| <span id="ts">β</span> | |
| </footer> | |
| <script> | |
| // ββ STATE ββ | |
| let sl = 0, ncalls = 0, dir = 'both', rtmr = null; | |
| let polyPrice = 2.00, bhavLay = 1.82; | |
| let _polyPrices = [null, null]; // decimal odds for [team1, team2] on Polymarket | |
| let chartData = { labels: [], poly: [], bhav: [], gap: [] }; | |
| let myChart = null; | |
| let lastGap = 0; | |
| let manualCalcOverrideUntil = 0; | |
| const MANUAL_CALC_HOLD_MS = 10000; | |
| let arbHoldTimer = null; | |
| let arbHoldStartedAt = 0; | |
| let arbHoldKey = ''; | |
| let arbHoldMs = 6000; | |
| let arbHoldCandidate = null; | |
| let arbAlertSentKey = ''; | |
| const $ = id => document.getElementById(id); | |
| const fmt = n => Math.round(n).toLocaleString('en-IN'); | |
| const fu = n => parseFloat(n).toFixed(2); | |
| const inrR = () => parseFloat($('sInr').value) || 84; | |
| const minGap = () => parseFloat($('sMinGap').value) || 2.0; | |
| const alertHoldSeconds = () => Math.max(1, parseInt($('sAlertHold')?.value || '6', 10) || 6); | |
| const t1 = () => $('sT1').value || 'Mumbai Indians'; | |
| const t2 = () => $('sT2').value || 'Kolkata Knight Riders'; | |
| const t1s = () => t1().split(' ')[0].toUpperCase(); | |
| const t2s = () => t2().split(' ')[0].toUpperCase(); | |
| function teamNameAt(idx) { | |
| const fallback = idx === 0 ? t1() : t2(); | |
| const polyName = _polyTeamNames?.[idx]; | |
| return polyName && polyName !== 'β' ? polyName : fallback; | |
| } | |
| function teamShortLabel(name) { | |
| const clean = (name || '').trim(); | |
| if (!clean) return 'β'; | |
| const parts = clean.split(/\s+/).filter(Boolean); | |
| if (parts.length >= 2) return parts.slice(0, 3).map(p => p[0]).join('').toUpperCase(); | |
| return clean.slice(0, 3).toUpperCase(); | |
| } | |
| function syncTeamLabels() { | |
| const team1 = teamNameAt(0); | |
| const team2 = teamNameAt(1); | |
| const short1 = teamShortLabel(team1); | |
| const short2 = teamShortLabel(team2); | |
| if ($('mumLbl')) $('mumLbl').textContent = short1; | |
| if ($('kolLbl')) $('kolLbl').textContent = short2; | |
| if ($('sT1Lbl')) $('sT1Lbl').textContent = `Team 1 (${short1} side)`; | |
| if ($('sT2Lbl')) $('sT2Lbl').textContent = `Team 2 (${short2} side)`; | |
| if ($('cpL1')) $('cpL1').textContent = short1; | |
| if ($('cpL2')) $('cpL2').textContent = short2; | |
| if ($('cbL1')) $('cbL1').textContent = short1; | |
| if ($('cbL2')) $('cbL2').textContent = short2; | |
| if ($('btnM1')) $('btnM1').textContent = `${team1.toUpperCase()} WINS`; | |
| if ($('btnM2')) $('btnM2').textContent = `${team2.toUpperCase()} WINS`; | |
| } | |
| function getPolyFeeRate() { | |
| return 0.03; | |
| } | |
| function getPolyBuyFeeFromOdds(p, grossUsd) { | |
| if (!p || p <= 1 || !grossUsd || grossUsd <= 0) return { cents: 0, feeRate: getPolyFeeRate(), feeUsd: 0, feeShares: 0 }; | |
| const priceUsd = 1 / p; | |
| const feeRate = getPolyFeeRate(); | |
| const feeUsd = grossUsd * feeRate * Math.max(0, 1 - priceUsd); | |
| const feeShares = priceUsd > 0 ? feeUsd / priceUsd : 0; | |
| return { | |
| cents: Math.round(priceUsd * 100), | |
| feeRate, | |
| feeUsd, | |
| feeShares | |
| }; | |
| } | |
| function calcArbRows(p, b, cap, inr, mode = 'both') { | |
| const rows = []; | |
| const fixedLayStake = parseFloat($('sRebelStake').value) || 20000; | |
| if (!p || !b || !cap || !fixedLayStake || p <= 1 || b <= 1) return rows; | |
| const priceUsd = 1 / p; | |
| const feeModel = getPolyBuyFeeFromOdds(p, 1); | |
| const polyCents = feeModel.cents; | |
| const feeRate = feeModel.feeRate; | |
| const netMultiplier = 1 - feeRate * Math.max(0, 1 - priceUsd); | |
| const effectivePolyOdds = p * netMultiplier; | |
| if (effectivePolyOdds <= 1) return rows; | |
| const w = fixedLayStake; | |
| const bs = w * (b - 1); | |
| let poly_inr, profW, profL; | |
| if (mode === 'W') { | |
| poly_inr = w; | |
| } else if (mode === 'L') { | |
| poly_inr = bs / (effectivePolyOdds - 1); | |
| } else if (mode === 'both') { | |
| poly_inr = (w * b) / effectivePolyOdds; | |
| } else if (typeof mode === 'number') { | |
| // For slider: interpolate between L (min) and W (max) | |
| const min_poly = bs / (effectivePolyOdds - 1); | |
| const max_poly = w; | |
| // Mode is 0..1 where 1 represents W (Team wins = max profit) and 0 represents L | |
| poly_inr = min_poly + mode * (max_poly - min_poly); | |
| } | |
| const poly_usd = poly_inr / inr; | |
| const grossShares = poly_usd * p; | |
| const feeBreakup = getPolyBuyFeeFromOdds(p, poly_usd); | |
| const feeUsd = feeBreakup.feeUsd; | |
| const feeShares = feeBreakup.feeShares; | |
| const netShares = Math.max(0, grossShares - feeShares); | |
| const winPayoutInr = netShares * inr; | |
| profW = winPayoutInr - poly_inr - bs; | |
| profL = w - poly_inr; | |
| const dep = bs + poly_inr; | |
| if (dep > cap) return rows; | |
| const left = cap - dep; | |
| const minProf = Math.min(profW, profL); | |
| const tgtProf = mode === 'both' ? minProf : Math.max(profW, profL); | |
| const roi = dep > 0 ? (tgtProf / dep) * 100 : 0; | |
| rows.push({ | |
| w, | |
| bs, | |
| pu: poly_usd, | |
| ps: poly_inr, | |
| dep, | |
| left, | |
| profW, | |
| profL, | |
| min: minProf, | |
| tgtProf, | |
| roi, | |
| sh: Math.round(netShares), | |
| grossSh: grossShares, | |
| netSh: netShares, | |
| feeUsd, | |
| feeSh: feeShares, | |
| feeRate, | |
| cents: polyCents | |
| }); | |
| return rows; | |
| } | |
| function fmtAltLay(level, p, cap, inr) { | |
| if (!level || !p || p <= 1) return 'β'; | |
| const row = calcArbRows(p, level, cap, inr, 'both')[0]; | |
| if (!row) return `${level.toFixed(2)} Β· over cap`; | |
| const roiCls = row.roi > 0 ? 'arb-alt-pos' : 'arb-alt-neg'; | |
| const roiTxt = `${row.roi > 0 ? '+' : ''}${row.roi.toFixed(1)}%`; | |
| const profTxt = `${row.min > 0 ? '+' : ''}βΉ${fmt(Math.abs(row.min))}`; | |
| return `<span><b>${level.toFixed(2)}</b> Β· ${fu(row.netSh)} net Β· ${profTxt}</span><span class="${roiCls}">${roiTxt}</span>`; | |
| } | |
| function isManualCalcActive() { | |
| return Date.now() < manualCalcOverrideUntil; | |
| } | |
| function getCalcPairsFromInputs() { | |
| return { | |
| polyPrices: [ | |
| parseFloat($('cP1')?.value || 0) || null, | |
| parseFloat($('cP2')?.value || 0) || null | |
| ], | |
| rebelLays: [ | |
| parseFloat($('cB1')?.value || 0) || null, | |
| parseFloat($('cB2')?.value || 0) || null | |
| ] | |
| }; | |
| } | |
| function pickBestArbPairFromValues(polyPrices, rebelLays, fallbackIdx = _polyActiveTab) { | |
| const cap = parseFloat($('sCap').value) || 100000; | |
| const inr = inrR(); | |
| let best = null; | |
| polyPrices.forEach((p, i) => { | |
| const b = rebelLays[i]; | |
| if (!p || !b || p <= 1 || b <= 1) return; | |
| const rows = calcArbRows(p, b, cap, inr, 'both'); | |
| if (!rows.length) return; | |
| const positiveRows = rows.filter(r => r.tgtProf > 0); | |
| const bestRow = positiveRows.length ? positiveRows[positiveRows.length - 1] : rows[rows.length - 1]; | |
| if (!best || bestRow.roi > best.row.roi) { | |
| best = { idx: i, p, b, row: bestRow, rows }; | |
| } | |
| }); | |
| if (!best) { | |
| const p = polyPrices[fallbackIdx]; | |
| const b = rebelLays[fallbackIdx]; | |
| if (p > 1 && b > 1) { | |
| const rows = calcArbRows(p, b, cap, inr, 'both'); | |
| if (rows.length) best = { idx: fallbackIdx, p, b, row: rows[rows.length - 1], rows }; | |
| } | |
| } | |
| return best; | |
| } | |
| function pickBestArbPair() { | |
| const rebelLays = [window._r0Lay || null, window._r1Lay || null]; | |
| return pickBestArbPairFromValues(_polyPrices, rebelLays, _polyActiveTab); | |
| } | |
| // ββ CLOCK ββ | |
| setInterval(() => { const t = new Date().toLocaleTimeString('en-IN'); $('ts').textContent = t; $('tsD').textContent = t; }, 1000); | |
| // ββ SETTINGS TOGGLE ββ | |
| function toggleStg() { | |
| const b = $('stbd'), open = b.classList.contains('open'); | |
| b.classList.toggle('open', !open); | |
| $('sarr').textContent = open ? 'β²' : 'βΌ'; | |
| } | |
| // ββ SETTINGS HANDLERS ββ | |
| function onMN() { const v = $('sMatch').value; $('mTitle').textContent = v; $('sbm').textContent = v; } | |
| let calcMode = 'both'; | |
| function setMode(m) { | |
| calcMode = m; | |
| $('btnBoth').className = 'rth-g' + (m === 'both' ? ' rth-gw' : ''); | |
| $('btnM1').className = 'rth-g' + (m === 'm1' ? ' rth-gw' : ''); | |
| $('btnM2').className = 'rth-g' + (m === 'm2' ? ' rth-gw' : ''); | |
| recalc(); | |
| } | |
| function updateSubtexts() { | |
| if (!$('cP1')) return; | |
| const p1 = parseFloat($('cP1').value), b1 = parseFloat($('cB1').value); | |
| const p2 = parseFloat($('cP2').value), b2 = parseFloat($('cB2').value); | |
| const u = (cp, cb, p, b) => { | |
| const g = p - b; | |
| if (g > 0) { | |
| $(cp).innerHTML = `<span class="cp-up">β² +${g.toFixed(3)} vs Bhav LAY</span>`; | |
| $(cb).innerHTML = `<span class="cp-up">LAY < Poly β</span>`; | |
| } else { | |
| $(cp).innerHTML = `<span class="cp-dn">βΌ ${g.toFixed(3)}</span>`; | |
| $(cb).innerHTML = `<span class="cp-dn">LAY > Poly β</span>`; | |
| } | |
| }; | |
| if (p1 && b1) u('cpS1', 'cbS1', p1, b1); | |
| if (p2 && b2) u('cpS2', 'cbS2', p2, b2); | |
| } | |
| function updateTopPolyBadges() { | |
| if ($('mumO')) $('mumO').textContent = _polyPrices[0] ? _polyPrices[0].toFixed(3) : 'β'; | |
| if ($('kolO')) $('kolO').textContent = _polyPrices[1] ? _polyPrices[1].toFixed(3) : 'β'; | |
| syncTeamLabels(); | |
| } | |
| function onCalcManualInput() { | |
| manualCalcOverrideUntil = Date.now() + MANUAL_CALC_HOLD_MS; | |
| recalc(); | |
| } | |
| function onT() { | |
| syncTeamLabels(); | |
| updateTopPolyBadges(); | |
| recalc(); | |
| } | |
| function onCap() { const v = $('sCap').value; $('sbc').textContent = 'βΉ' + fmt(parseFloat(v)); recalc(); } | |
| function onStep() { recalc(); } | |
| function onInr() { const v = $('sInr').value; $('inrD').textContent = 'βΉ' + v; $('sbi').textContent = 'βΉ' + v; recalc(); } | |
| function onMG() { $('sbg').textContent = $('sMinGap').value + '%'; recalc(); } | |
| function onAH() { arbHoldMs = alertHoldSeconds() * 1000; if ($('sah')) $('sah').textContent = alertHoldSeconds() + 's'; } | |
| function onRebelStake() { | |
| const v = parseFloat($('sRebelStake').value) || 20000; | |
| if ($('abRebelLayStake')) $('abRebelLayStake').textContent = 'βΉ' + v.toLocaleString('en-IN'); | |
| recalc(); | |
| } | |
| // ββ LIVE ARB BOX ββ | |
| // Called whenever poly or rebel odds update | |
| function updateArbBox() { | |
| const manualActive = isManualCalcActive(); | |
| const best = pickBestArbPair(); | |
| const bestTabIdx = best?.idx ?? _polyActiveTab; | |
| const p = best?.p ?? null; | |
| const b = best?.b ?? null; | |
| const row = best?.row ?? null; | |
| const arbPct = row ? row.roi : null; | |
| const cap = parseFloat($('sCap').value) || 100000; | |
| const inr = inrR(); | |
| const layLevels = bestTabIdx === 0 ? (window._r0LayLevels || []) : (window._r1LayLevels || []); | |
| // Team name β from stored names or settings | |
| const teamName = _polyTeamNames[bestTabIdx] || (bestTabIdx === 0 ? t1s() : t2s()); | |
| const rebelTeam = teamName; | |
| const hasPositiveArb = row && row.roi > 0; | |
| // Highlight/switch only when there is a real positive arbitrage. | |
| if (hasPositiveArb && bestTabIdx !== null && bestTabIdx !== _polyActiveTab) { | |
| document.querySelectorAll('.poly-tab').forEach((t, i) => { | |
| t.style.borderColor = i === bestTabIdx ? 'var(--g)' : ''; | |
| t.style.color = i === bestTabIdx ? 'var(--g)' : ''; | |
| }); | |
| if (!_polyAutoSwitching && Date.now() >= _polyManualOverrideUntil) { | |
| _polyAutoSwitching = true; | |
| Promise.resolve(switchPolyTab(bestTabIdx)) | |
| .catch(() => { }) | |
| .finally(() => { _polyAutoSwitching = false; }); | |
| } | |
| } else { | |
| document.querySelectorAll('.poly-tab').forEach(t => { t.style.borderColor = ''; t.style.color = ''; }); | |
| } | |
| if ($('abPolyTeam')) $('abPolyTeam').textContent = teamName + ' YES'; | |
| if ($('abRebelTeam')) $('abRebelTeam').textContent = rebelTeam + ' LAY'; | |
| if ($('abPolyShares')) $('abPolyShares').textContent = row ? fu(row.grossSh) : 'β'; | |
| if ($('abPolySharesLbl')) $('abPolySharesLbl').textContent = 'buy shares (gross)'; | |
| if ($('abPolyGross')) $('abPolyGross').textContent = row ? `net ${fu(row.netSh)} Β· fee ${fu(row.feeSh)} sh` : 'net β Β· fee β'; | |
| if ($('abPolyCents')) $('abPolyCents').textContent = row ? row.cents + 'Β’' : 'β'; | |
| if ($('abRebelOdds')) $('abRebelOdds').textContent = (b > 1) ? b.toFixed(2) : 'β'; | |
| if ($('abRebelLayStake')) $('abRebelLayStake').textContent = row ? 'βΉ' + row.w.toLocaleString('en-IN') : 'β'; | |
| if ($('abRebelStake')) $('abRebelStake').textContent = row ? 'βΉ' + Math.round(row.bs).toLocaleString('en-IN') : 'β'; | |
| if ($('abLayAlt1')) $('abLayAlt1').innerHTML = `L1 ${fmtAltLay(layLevels[0], p, cap, inr)}`; | |
| if ($('abLayAlt2')) $('abLayAlt2').innerHTML = `L2 ${fmtAltLay(layLevels[1], p, cap, inr)}`; | |
| const pctEl = $('abPct'), lblEl = $('abPctLbl'); | |
| if (pctEl && arbPct !== null) { | |
| const isArb = arbPct > 0; | |
| pctEl.textContent = (isArb ? '+' : '') + arbPct.toFixed(1) + '%'; | |
| pctEl.className = 'arb-pct ' + (isArb ? 'arb-yes' : 'arb-no'); | |
| lblEl.textContent = isArb ? 'ARB β' : 'NO ARB'; | |
| lblEl.className = 'arb-pct-lbl ' + (isArb ? 'arb-yes' : 'arb-no'); | |
| } | |
| } | |
| function onRC() { const v = $('sRef').value; $('sbr').textContent = v === '0' ? 'Off' : v + 's'; startAR(); } | |
| // ββ TABS ββ | |
| function swTab(el, n) { | |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('on')); | |
| document.querySelectorAll('.tc').forEach(t => t.classList.remove('on')); | |
| el.classList.add('on'); $('tc-' + n).classList.add('on'); | |
| } | |
| // ββ DIRECTION ββ | |
| function setDir(d, btn) { | |
| dir = d; | |
| document.querySelectorAll('.dir-btn').forEach(b => b.classList.remove('on')); | |
| btn.classList.add('on'); | |
| recalc(); | |
| } | |
| // ββ SLIPPAGE ββ | |
| function setSl(v, btn) { | |
| sl = v; | |
| document.querySelectorAll('.slbtn').forEach(b => b.classList.remove('on')); | |
| btn.classList.add('on'); | |
| recalc(); | |
| } | |
| // ββ ARB LOGIC + NOTIFICATIONS ββ | |
| function checkArb() { | |
| const best = pickBestArbPair(); | |
| if (!best || !best.row) { | |
| if (arbHoldTimer) { clearTimeout(arbHoldTimer); arbHoldTimer = null; } | |
| arbHoldStartedAt = 0; | |
| arbHoldKey = ''; | |
| arbHoldCandidate = null; | |
| arbAlertSentKey = ''; | |
| lastGap = 0; | |
| return; | |
| } | |
| const tgt = _polyTeamNames[best.idx] || (best.idx === 0 ? t1s() : t2s()); | |
| const roiPct = best.row.roi; | |
| const holdKey = `${best.idx}:${tgt}`; | |
| if (roiPct >= minGap()) { | |
| arbHoldCandidate = { ...best, tgt, holdKey, roiPct }; | |
| if (arbHoldKey !== holdKey) { | |
| arbHoldKey = holdKey; | |
| arbHoldStartedAt = Date.now(); | |
| arbAlertSentKey = ''; | |
| } | |
| if (arbHoldTimer) { clearTimeout(arbHoldTimer); arbHoldTimer = null; } | |
| if (arbAlertSentKey !== holdKey && arbHoldStartedAt && (Date.now() - arbHoldStartedAt) >= arbHoldMs) { | |
| arbAlertSentKey = holdKey; | |
| triggerAlerts(arbHoldCandidate); | |
| } | |
| } else { | |
| if (arbHoldTimer) { clearTimeout(arbHoldTimer); arbHoldTimer = null; } | |
| arbHoldStartedAt = 0; | |
| arbHoldKey = ''; | |
| arbHoldCandidate = null; | |
| arbAlertSentKey = ''; | |
| } | |
| lastGap = roiPct; | |
| } | |
| function triggerAlerts(candidate) { | |
| if ($('sSnd').value === '1') playBeep(); | |
| triggerFlash(); | |
| sendTelegram(candidate); | |
| } | |
| function playBeep() { | |
| try { | |
| const ac = new (window.AudioContext || window.webkitAudioContext)(); | |
| if (!ac) return; | |
| const osc = ac.createOscillator(), gain = ac.createGain(); | |
| osc.connect(gain); gain.connect(ac.destination); | |
| osc.frequency.value = 880; osc.type = 'sine'; | |
| gain.gain.setValueAtTime(0.3, ac.currentTime); | |
| gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + 0.6); | |
| osc.start(ac.currentTime); osc.stop(ac.currentTime + 0.6); | |
| } catch (e) { } | |
| } | |
| function triggerFlash() { | |
| const f = $('flashOv'); | |
| f.style.display = 'block'; f.style.animation = 'none'; | |
| requestAnimationFrame(() => { f.style.animation = 'flashFade .8s ease-out forwards'; }); | |
| setTimeout(() => f.style.display = 'none', 900); | |
| } | |
| async function sendTelegram(candidate) { | |
| const token = $('sTgToken').value.trim(), chat = $('sTgChat').value.trim(); | |
| if (!token || !chat) { | |
| console.warn('[TG] Missing bot token or chat id'); | |
| return; | |
| } | |
| const inr = inrR(); | |
| const cap = parseFloat($('sCap').value) || 100000; | |
| const p = candidate?.p, b = candidate?.b, tgt = candidate?.tgt, row = candidate?.row, roiPct = candidate?.roiPct; | |
| if (!p || !b || !row) return; | |
| const grossShares = fu(row.grossSh); | |
| const netShares = fu(row.netSh); | |
| const prof = Math.max(0, row.min); | |
| const msg = `β‘ IPL ARB ALERT\n\nMatch: ${$('sMatch').value}\nProfit: ${roiPct >= 0 ? '+' : ''}${roiPct.toFixed(2)}%\n\nπ POLYMARKET\nBUY ${tgt} YES @ ${p.toFixed(3)}\nStake: $${fu(row.pu)} (βΉ${fmt(row.ps)})\nGross Shares: ${grossShares}\nNet Hedge Shares: ${netShares}\nFee: $${fu(row.feeUsd)} (${fu(row.feeSh)} sh)\n\nπ BHAV\nLAY ${tgt} @ ${b.toFixed(3)}\nStake: βΉ${fmt(row.w)}\nLiability: βΉ${fmt(Math.round(row.bs))}\n\nβ EQUAL PROFIT: +βΉ${fmt(prof)}\nβοΈ Capital: βΉ${fmt(cap)} | INR: ${inr}`; | |
| try { | |
| const resp = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ chat_id: chat, text: msg }) | |
| }); | |
| if (!resp.ok) { | |
| const body = await resp.text().catch(() => ''); | |
| console.error('[TG] Send failed', resp.status, body); | |
| return; | |
| } | |
| console.log('[TG] Alert sent'); | |
| } catch (e) { | |
| console.error('[TG] Send error', e); | |
| } | |
| } | |
| function recalc() { | |
| updateSubtexts(); | |
| const p1 = parseFloat($('cP1').value || 0), b1 = parseFloat($('cB1').value || 0); | |
| const p2 = parseFloat($('cP2').value || 0), b2 = parseFloat($('cB2').value || 0); | |
| const cap = parseFloat($('sCap').value) || 100000; | |
| const inr = inrR(); | |
| const manualActive = isManualCalcActive(); | |
| const best = manualActive | |
| ? pickBestArbPairFromValues([p1, p2], [b1, b2], _polyActiveTab) | |
| : pickBestArbPair(); | |
| let currArb = best?.idx === 1 ? 2 : 1; | |
| let p = best?.p ?? p1; | |
| let b = best?.b ?? b1; | |
| const mName = teamNameAt(0), kName = teamNameAt(1); | |
| if ($('btnM1')) $('btnM1').textContent = `${mName.toUpperCase()} WINS`; | |
| if ($('btnM2')) $('btnM2').textContent = `${kName.toUpperCase()} WINS`; | |
| if (currArb === 1) { | |
| $('thWin').textContent = `IF ${mName} WINS`; | |
| $('thLose').textContent = `IF ${kName} WINS`; | |
| } else { | |
| $('thWin').textContent = `IF ${kName} WINS`; | |
| $('thLose').textContent = `IF ${mName} WINS`; | |
| } | |
| if ($('thMinP')) $('thMinP').textContent = calcMode === 'both' ? 'GUARANTEED PROFIT' : 'TARGET PROFIT'; | |
| if (!p || !b || !cap || p <= 1) { $('calcTb').innerHTML = '<tr><td colspan="10"><div class="nodata">Capital too low or invalid odds</div></td></tr>'; return; } | |
| polyPrice = p; bhavLay = b; | |
| if (!manualActive) checkArb(); | |
| if (!manualActive) updateChart(p, b); | |
| updateArbBox(); | |
| let targetMax = 'both'; | |
| if (calcMode === 'm1') targetMax = (currArb === 1) ? 'W' : 'L'; | |
| if (calcMode === 'm2') targetMax = (currArb === 1) ? 'L' : 'W'; | |
| const rows = calcArbRows(p, b, cap, inr, targetMax); | |
| renderCalc(rows, p, b, cap, inr); | |
| } | |
| function renderCalc(rows, p, b, cap, inr) { | |
| const tb = $('calcTb'); | |
| if (!rows.length) { tb.innerHTML = '<tr><td colspan="10"><div class="nodata">Capital too low or invalid rates</div></td></tr>'; return; } | |
| const profRows = rows.filter(r => r.tgtProf > 0); | |
| const mx = profRows.length ? profRows[profRows.length - 1] : rows[rows.length - 1]; | |
| const sorted = [mx, ...rows.filter(r => r !== mx).reverse()]; | |
| const pf = v => v > 0 ? `<span class="pos">+βΉ${fmt(v)}</span>` : v < 0 ? `<span class="neg">-βΉ${fmt(Math.abs(v))}</span>` : `<span style="color:var(--txt2)">βΉ0</span>`; | |
| const pfR = v => v > 0 ? `<span class="pos">+${v.toFixed(2)}%</span>` : `<span class="neg">${v.toFixed(2)}%</span>`; | |
| let h = ''; | |
| sorted.forEach(r => { | |
| const isMx = r === mx; | |
| h += `<tr${isMx ? ' class="mx"' : ''}> | |
| <td>βΉ${fmt(r.w)}${isMx ? ' β' : ''}</td> | |
| <td class="hl">βΉ${fmt(r.bs)}</td> | |
| <td class="hl">$${fu(r.pu)} <span style="color:var(--txt2);font-size:10px">(${r.cents}Β’ Β· fee $${fu(r.feeUsd)})</span></td> | |
| <td style="color:var(--txt2)">${fu(r.grossSh)} gross <span style="color:var(--p)">β ${fu(r.netSh)} net</span></td> | |
| <td>βΉ${fmt(r.dep)}</td> | |
| <td style="color:var(--txt2)">βΉ${fmt(r.left)}</td> | |
| <td>${pf(r.profW)}</td> | |
| <td>${pf(r.profL)}</td> | |
| <td>${pf(r.tgtProf)}</td> | |
| <td>${pfR(r.roi)}</td> | |
| </tr>`; | |
| }); | |
| tb.innerHTML = h; | |
| let pctBhav = (mx.bs / cap) * 100; | |
| let pctPoly = (mx.ps / cap) * 100; | |
| let pctFree = (mx.left / cap) * 100; | |
| if (pctBhav < 0) pctBhav = 0; if (pctPoly < 0) pctPoly = 0; if (pctFree < 0) pctFree = 0; | |
| if ($('waBhav')) { | |
| $('waBhav').style.width = pctBhav + '%'; | |
| $('waPoly').style.width = pctPoly + '%'; | |
| $('waFree').style.width = pctFree + '%'; | |
| $('waBhavT').innerHTML = `Bhav: βΉ${fmt(mx.bs)} (${pctBhav.toFixed(1)}%)`; | |
| $('waPolyT').innerHTML = `Poly: βΉ${fmt(mx.ps)} = $${fu(mx.pu)} (${mx.cents}Β’, ${pctPoly.toFixed(1)}%)`; | |
| $('waFreeT').innerHTML = `Free: βΉ${fmt(mx.left)} (${pctFree.toFixed(1)}%)`; | |
| $('waRoi').innerHTML = mx.roi > 0 ? `Guaranteed ROI: +${mx.roi.toFixed(2)}%` : `Loss ROI: ${mx.roi.toFixed(2)}%`; | |
| $('waRoi').style.color = mx.roi > 0 ? 'var(--y)' : 'var(--r)'; | |
| } | |
| } | |
| // ββ CHART ββ | |
| function initChart() { | |
| const ctx = $('oddsChart').getContext('2d'); | |
| myChart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: chartData.labels, | |
| datasets: [ | |
| { label: 'Poly YES', data: chartData.poly, borderColor: '#9b95ff', backgroundColor: 'rgba(155,149,255,.08)', tension: .3, pointRadius: 2, borderWidth: 2 }, | |
| { label: 'Bhav LAY', data: chartData.bhav, borderColor: '#ff7099', backgroundColor: 'rgba(255,112,153,.08)', tension: .3, pointRadius: 2, borderWidth: 2 }, | |
| { label: 'Gap %', data: chartData.gap, borderColor: '#00d47e', backgroundColor: 'rgba(0,212,126,.08)', tension: .3, pointRadius: 2, borderWidth: 2, yAxisID: 'y2' } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, animation: { duration: 300 }, | |
| plugins: { legend: { display: false } }, | |
| scales: { | |
| x: { ticks: { color: '#4a5a72', font: { size: 9 }, maxTicksLimit: 8 }, grid: { color: 'rgba(26,37,64,.5)' } }, | |
| y: { ticks: { color: '#4a5a72', font: { size: 9 } }, grid: { color: 'rgba(26,37,64,.5)' }, title: { display: true, text: 'Odds', color: '#4a5a72', font: { size: 9 } } }, | |
| y2: { position: 'right', ticks: { color: '#00d47e', font: { size: 9 } }, grid: { display: false }, title: { display: true, text: 'Gap %', color: '#00d47e', font: { size: 9 } } } | |
| } | |
| } | |
| }); | |
| } | |
| function updateChart(p, b) { | |
| const now = new Date().toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); | |
| const gapPct = b > 0 ? ((p - b) / b) * 100 : 0; | |
| chartData.labels.push(now); | |
| chartData.poly.push(parseFloat(p.toFixed(3))); | |
| chartData.bhav.push(parseFloat(b.toFixed(3))); | |
| chartData.gap.push(parseFloat(gapPct.toFixed(2))); | |
| // Keep last 60 points | |
| if (chartData.labels.length > 60) { chartData.labels.shift(); chartData.poly.shift(); chartData.bhav.shift(); chartData.gap.shift(); } | |
| if (myChart) { myChart.update('none'); } | |
| const pts = chartData.labels.length; | |
| $('chartInfo').textContent = `${pts} data points Β· Poly range: ${Math.min(...chartData.poly).toFixed(3)}β${Math.max(...chartData.poly).toFixed(3)} Β· Bhav range: ${Math.min(...chartData.bhav).toFixed(3)}β${Math.max(...chartData.bhav).toFixed(3)}`; | |
| } | |
| // ββ FETCH COUNTER ββ | |
| function incNC() { ncalls++; $('ncall').textContent = ncalls; $('ncall2').textContent = ncalls; } | |
| function tokenIndex(tokenId) { | |
| return _polyTokenIds.findIndex(t => t === tokenId); | |
| } | |
| function getPolyBook(tokenId) { | |
| if (!_polyBooks[tokenId]) _polyBooks[tokenId] = { asks: {}, bids: {} }; | |
| return _polyBooks[tokenId]; | |
| } | |
| function setPolyBookSnapshot(tokenId, data) { | |
| const book = getPolyBook(tokenId); | |
| book.asks = {}; | |
| book.bids = {}; | |
| (data.asks || []).forEach(a => { | |
| const p = parseFloat(a.price), sz = parseFloat(a.size); | |
| if (p > 0 && sz > 0) book.asks[p] = sz; | |
| }); | |
| (data.bids || []).forEach(b => { | |
| const p = parseFloat(b.price), sz = parseFloat(b.size); | |
| if (p > 0 && sz > 0) book.bids[p] = sz; | |
| }); | |
| _polySnapshots[tokenId] = { | |
| asks: Object.entries(book.asks).map(([p, sz]) => ({ price: p, size: sz })), | |
| bids: Object.entries(book.bids).map(([p, sz]) => ({ price: p, size: sz })), | |
| last_trade_price: data.last_trade_price || data.last || 0, | |
| volume: data.volume | |
| }; | |
| renderP(_polySnapshots[tokenId], tokenId); | |
| } | |
| function applyPolyDelta(tokenId, data) { | |
| const book = getPolyBook(tokenId); | |
| if (data.asks) data.asks.forEach(a => { | |
| const p = parseFloat(a.price), sz = parseFloat(a.size); | |
| if (sz === 0) delete book.asks[p]; | |
| else book.asks[p] = sz; | |
| }); | |
| if (data.bids) data.bids.forEach(b => { | |
| const p = parseFloat(b.price), sz = parseFloat(b.size); | |
| if (sz === 0) delete book.bids[p]; | |
| else book.bids[p] = sz; | |
| }); | |
| _polySnapshots[tokenId] = { | |
| asks: Object.entries(book.asks).map(([p, sz]) => ({ price: p, size: sz })), | |
| bids: Object.entries(book.bids).map(([p, sz]) => ({ price: p, size: sz })), | |
| last_trade_price: data.last_trade_price || data.last || _polyLastByToken[tokenId] || 0, | |
| volume: data.volume || _polyVolumeByToken[tokenId] || 'β' | |
| }; | |
| renderP(_polySnapshots[tokenId], tokenId); | |
| } | |
| // ββ POLY TAB SWITCH ββ | |
| async function switchPolyTab(idx, isManual = false) { | |
| if (isManual) _polyManualOverrideUntil = Date.now() + 8000; | |
| _polyActiveTab = idx; | |
| _polyTokenId = _polyTokenIds[idx]; | |
| const requestedTokenId = _polyTokenId; | |
| _pBookRows = []; // reset DOM diff cache so full rebuild happens cleanly | |
| // Update tab UI | |
| document.querySelectorAll('.poly-tab').forEach((t, i) => t.classList.toggle('on', i === idx)); | |
| if (!_polyTokenId) return; | |
| const cached = _polySnapshots[requestedTokenId]; | |
| if (cached && ((cached.asks && cached.asks.length) || (cached.bids && cached.bids.length))) { | |
| renderP(cached, requestedTokenId); | |
| checkArb(); | |
| updateArbBox(); | |
| setSt('pSt', _polyWsConnectedCount > 0 ? 'LIVE β' : 'SNAPSHOT β', _polyWsConnectedCount > 0 ? 'bdg-ok' : 'bdg-w'); | |
| return; | |
| } | |
| setSt('pSt', 'LOADING...', 'bdg-w'); | |
| $('pBook').innerHTML = `<tr><td colspan="4"><div class="nodata">Loading ${_polyTeamNames[idx] || 'market'} book...</div></td></tr>`; | |
| $('pLast').textContent = 'β'; | |
| $('pSprd').textContent = 'β'; | |
| $('pVol').textContent = 'β'; | |
| try { | |
| const d = await polyFetch(`https://clob.polymarket.com/book?token_id=${requestedTokenId}`); | |
| if (_polyTokenId !== requestedTokenId) return; | |
| if (d.bids || d.asks) { | |
| setPolyBookSnapshot(requestedTokenId, d); | |
| checkArb(); | |
| updateArbBox(); | |
| setSt('pSt', _polyWsConnectedCount > 0 ? 'LIVE β' : 'SNAPSHOT β', _polyWsConnectedCount > 0 ? 'bdg-ok' : 'bdg-w'); | |
| } | |
| } catch (e) { setSt('pSt', 'ERR', 'bdg-r'); } | |
| } | |
| // ββ FETCH POLYMARKET ββ | |
| let _polyWsByToken = {}; | |
| let _wsHeartbeatTimers = {}; | |
| let _wsReconnectTimers = {}; | |
| let _polyWsConnectedCount = 0; | |
| let _polyAutoSwitching = false; | |
| let _sliderValue = 0.5; | |
| function onArbSlider(val) { | |
| _sliderValue = parseFloat(val) / 100; | |
| document.getElementById('arbSliderVal').innerText = val + '%'; | |
| if (val == 50) { | |
| setCalcMode('both'); | |
| } else { | |
| setCalcMode('slider'); | |
| } | |
| } | |
| let _polyManualOverrideUntil = 0; | |
| function updatePolyWsStatus() { | |
| const openCount = Object.values(_polyWsByToken).filter(ws => ws && ws.readyState === WebSocket.OPEN).length; | |
| _polyWsConnectedCount = openCount; | |
| if (openCount > 0) setSt('pSt', 'LIVE β', 'bdg-ok'); | |
| else if (_polyTokenId && _polySnapshots[_polyTokenId]) setSt('pSt', 'SNAPSHOT β', 'bdg-w'); | |
| } | |
| function connectPolyWs(tokenId) { | |
| if (!tokenId) return; | |
| const existingWs = _polyWsByToken[tokenId]; | |
| if (existingWs && existingWs.readyState < 2) return; | |
| if (_wsHeartbeatTimers[tokenId]) { clearInterval(_wsHeartbeatTimers[tokenId]); delete _wsHeartbeatTimers[tokenId]; } | |
| if (_wsReconnectTimers[tokenId]) { clearTimeout(_wsReconnectTimers[tokenId]); delete _wsReconnectTimers[tokenId]; } | |
| const ws = new WebSocket('wss://ws-subscriptions-clob.polymarket.com/ws/market'); | |
| _polyWsByToken[tokenId] = ws; | |
| ws.onopen = () => { | |
| ws.send(JSON.stringify({ type: 'subscribe', channel: 'market', assets_id: tokenId })); | |
| updatePolyWsStatus(); | |
| _wsHeartbeatTimers[tokenId] = setInterval(() => { | |
| if (ws.readyState === WebSocket.OPEN) { | |
| try { ws.send(JSON.stringify({ type: 'ping' })); } catch { } | |
| } | |
| }, 10000); | |
| }; | |
| ws.onmessage = (msg) => { | |
| try { | |
| const data = JSON.parse(msg.data); | |
| if (data.type === 'pong' || data.type === 'ping') return; // ignore heartbeat | |
| if (data.asks || data.bids) applyPolyDelta(tokenId, data); | |
| } catch { } | |
| }; | |
| ws.onerror = () => { | |
| polyPollLoop(tokenId); | |
| updatePolyWsStatus(); | |
| }; | |
| ws.onclose = () => { | |
| if (_wsHeartbeatTimers[tokenId]) { clearInterval(_wsHeartbeatTimers[tokenId]); delete _wsHeartbeatTimers[tokenId]; } | |
| polyPollLoop(tokenId); | |
| updatePolyWsStatus(); | |
| _wsReconnectTimers[tokenId] = setTimeout(() => connectPolyWs(tokenId), 2000); | |
| }; | |
| } | |
| let _polyTokenId = null; | |
| let _polyBooks = {}; | |
| let _lastBestAskByToken = {}; | |
| let _polyLastByToken = {}; | |
| let _polyVolumeByToken = {}; | |
| let _polySnapshots = {}; | |
| let _polyTokenIds = [null, null]; // [team1, team2] | |
| let _polyActiveTab = 0; // 0 = team1, 1 = team2 | |
| let _polyTeamNames = ['β', 'β']; | |
| // Fetch via proxy.js /poly-fetch β uses headless Chrome TLS fingerprint, instant, no relay | |
| async function polyFetch(targetUrl) { | |
| const r = await fetch(`http://localhost:3000/poly-fetch?url=${encodeURIComponent(targetUrl)}`, { signal: AbortSignal.timeout(20000) }); | |
| if (!r.ok) throw new Error(`poly-fetch ${r.status}`); | |
| return await r.json(); | |
| } | |
| async function fetchPoly() { | |
| let url = $('sPolyUrl').value.trim(); | |
| if (!url) { demoP(); return; } | |
| setSt('pSt', 'LOADING...', 'bdg-w'); incNC(); | |
| const slug = (url.includes('polymarket.com') ? url.split('/').filter(Boolean).pop().split('?')[0] : url.trim()) | |
| .replace(/^\/+/, '') | |
| .trim(); | |
| try { | |
| // Step 1: Resolve slug β token ID | |
| setSt('pSt', 'RESOLVING...', 'bdg-w'); | |
| const events = await polyFetch(`https://gamma-api.polymarket.com/events?slug=${slug}`); | |
| if (!events || !events[0]) throw new Error('No event found for slug: ' + slug); | |
| const ev = events[0]; | |
| // International/PSL events bundle Toss-Winner, Completed-Match, Tie, Super Over alongside the Moneyline. | |
| // The moneyline market's slug always equals the event slug (sub-markets get suffixes like -toss-winner). | |
| const evSlug = (ev.slug || '').toLowerCase(); | |
| const isTradable = m => m && m.clobTokenIds && !m.closed && !m.archived; | |
| const hasTeamOutcomes = m => { | |
| try { | |
| const outs = JSON.parse(m.outcomes || '[]').map(o => String(o).toLowerCase().trim()); | |
| return outs.length >= 2 && !outs.every(o => o === 'yes' || o === 'no'); | |
| } catch { return false; } | |
| }; | |
| const candidates = (ev.markets || []).filter(m => isTradable(m) && hasTeamOutcomes(m)); | |
| const market = | |
| candidates.find(m => (m.slug || '').toLowerCase() === evSlug) || | |
| candidates.sort((a, b) => (Number(b.volume) || 0) - (Number(a.volume) || 0))[0] || | |
| ev.markets[0]; | |
| if (!market || !market.clobTokenIds) throw new Error('No tradable moneyline market in event: ' + slug); | |
| const tokenIds = JSON.parse(market.clobTokenIds); | |
| _polyTokenIds = [tokenIds[0], tokenIds[1] || tokenIds[0]]; | |
| _polyTokenId = _polyTokenIds[_polyActiveTab]; | |
| // Extract team names from market outcomes or use settings | |
| try { | |
| const outcomes = JSON.parse(market.outcomes || '[]'); | |
| _polyTeamNames = [outcomes[0] || t1s(), outcomes[1] || t2s()]; | |
| } catch { _polyTeamNames = [t1s(), t2s()]; } | |
| syncTeamLabels(); | |
| // Update tab labels | |
| if ($('polyTab0')) $('polyTab0').textContent = _polyTeamNames[0]; | |
| if ($('polyTab1')) $('polyTab1').textContent = _polyTeamNames[1]; | |
| // Step 2: Initial orderbook snapshots for both tokens | |
| _polyBooks = {}; | |
| _polySnapshots = {}; | |
| _lastBestAskByToken = {}; | |
| setSt('pSt', 'LOADING BOOK...', 'bdg-w'); | |
| await Promise.all(_polyTokenIds.filter(Boolean).map(async tokenId => { | |
| const d = await polyFetch(`https://clob.polymarket.com/book?token_id=${tokenId}`); | |
| if (d.bids || d.asks) setPolyBookSnapshot(tokenId, d); | |
| })); | |
| // Step 3: Close old WebSockets | |
| Object.values(_polyWsByToken).forEach(ws => { try { ws.close(); } catch { } }); | |
| _polyWsByToken = {}; | |
| Object.values(_wsHeartbeatTimers).forEach(timer => clearInterval(timer)); | |
| Object.values(_wsReconnectTimers).forEach(timer => clearTimeout(timer)); | |
| _wsHeartbeatTimers = {}; | |
| _wsReconnectTimers = {}; | |
| // Step 4: Live WebSocket with heartbeat + auto-reconnect for both tokens | |
| setSt('pSt', 'CONNECTING WS...', 'bdg-w'); | |
| _polyTokenIds.filter(Boolean).forEach(tokenId => connectPolyWs(tokenId)); | |
| } catch (e) { | |
| setSt('pSt', 'ERR', 'bdg-r'); | |
| $('pBook').innerHTML = `<tr><td colspan="4"><div class="nodata" style="color:var(--r)"> | |
| β οΈ ${e.message}<br> | |
| <span style="font-size:10px;color:var(--txt2)">slug: <b style="color:var(--y)">${slug}</b></span> | |
| </div></td></tr>`; | |
| } | |
| } | |
| async function polyPollLoop(tokenId = _polyTokenId) { | |
| // Single snapshot only β called once on WS drop, then WS reconnects | |
| // Never loop β that floods the Cloudflare Worker | |
| if (!tokenId) return; | |
| try { | |
| const d = await polyFetch(`https://clob.polymarket.com/book?token_id=${tokenId}`); | |
| if (d.bids || d.asks) { | |
| setPolyBookSnapshot(tokenId, d); | |
| if (tokenId === _polyTokenId) setSt('pSt', 'SNAPSHOT β', 'bdg-w'); | |
| } | |
| } catch { } | |
| // No setTimeout here β WS reconnect handles continuity | |
| } | |
| function demoP() { | |
| renderP({ | |
| asks: [{ price: '2.00', size: '2643.99' }, { price: '1.92', size: '124.61' }, { price: '1.85', size: '200' }, { price: '1.79', size: '264' }], | |
| bids: [{ price: '2.08', size: '904.49' }, { price: '2.13', size: '87' }, { price: '2.17', size: '10' }, { price: '2.22', size: '30' }], | |
| last: '1.96', spread: '2Β’', volume: '$208K' | |
| }, 'demo'); | |
| setSt('pSt', 'DEMO', 'bdg-w'); $('cP1').value = '2.00'; recalc(); | |
| } | |
| // ββ POLYMARKET ORDERBOOK RENDERER (no-flicker DOM diff) ββ | |
| // Builds a row signature array, compares to existing DOM rows, | |
| // only patches cells that actually changed β zero full redraws. | |
| let _pBookRows = []; // last rendered row signatures | |
| function renderP(d, tokenId = _polyTokenId) { | |
| const rawAsks = (d.asks || []).map(a => ({ c: parseFloat(a.price), sz: parseFloat(a.size) })); | |
| const rawBids = (d.bids || []).map(b => ({ c: parseFloat(b.price), sz: parseFloat(b.size) })); | |
| const idx = tokenIndex(tokenId); | |
| const asks = [...rawAsks].sort((a, b) => a.c - b.c).slice(0, 5); | |
| const bids = [...rawBids].sort((a, b) => b.c - a.c).slice(0, 5); | |
| const toOdds = c => c > 0 ? 1 / c : 0; | |
| const incomingLast = parseFloat(d.last_trade_price || d.last || 0); | |
| if (tokenId && incomingLast > 0) _polyLastByToken[tokenId] = incomingLast; | |
| if (tokenId && d.volume) _polyVolumeByToken[tokenId] = d.volume; | |
| const lastC = tokenId ? (_polyLastByToken[tokenId] || 0) : incomingLast; | |
| const lastOdds = lastC > 0 ? toOdds(lastC).toFixed(2) : 'β'; | |
| const lastCents = lastC > 0 ? Math.round(lastC * 100) + 'Β’' : 'β'; | |
| const bestAskC = asks[0]?.c, bestBidC = bids[0]?.c; | |
| const spread = bestAskC && bestBidC ? Math.round((bestAskC - bestBidC) * 100) + 'Β’' : 'β'; | |
| const volume = tokenId ? (_polyVolumeByToken[tokenId] || d.volume || 'β') : (d.volume || 'β'); | |
| if (bestAskC && idx >= 0) { | |
| const bestOdds = toOdds(bestAskC); | |
| _polyPrices[idx] = bestOdds; | |
| updateTopPolyBadges(); | |
| if (!isManualCalcActive()) { | |
| if (idx === 0) $('cP1').value = bestOdds.toFixed(3); | |
| else if (idx === 1) $('cP2').value = bestOdds.toFixed(3); | |
| } | |
| polyPrice = bestOdds; | |
| const prevBestAskC = _lastBestAskByToken[tokenId]; | |
| if (prevBestAskC === undefined || Math.abs(bestAskC - prevBestAskC) >= 0.01) { | |
| _lastBestAskByToken[tokenId] = bestAskC; | |
| recalc(); updateArbBox(); | |
| } | |
| } | |
| if (tokenId !== _polyTokenId && tokenId !== 'demo') return; | |
| // Build row descriptors: [type, odds, cents, shares, total] | |
| const newRows = []; | |
| [...asks].reverse().forEach(a => newRows.push({ | |
| type: 'ask', cls: 'ask', | |
| col1: `<span style="color:var(--r);font-size:9px;background:rgba(255,58,94,.1);padding:1px 4px;border-radius:2px">ASK</span>`, | |
| col2: `${toOdds(a.c).toFixed(2)} <span style="font-size:9px;color:var(--txt2)">(${Math.round(a.c * 100)}Β’)</span>`, | |
| col3: a.sz.toLocaleString() + ' sh', | |
| col4: '$' + (a.c * a.sz).toFixed(0), | |
| key: 'ask-' + Math.round(a.c * 100) | |
| })); | |
| newRows.push({ | |
| type: 'spr', cls: 'spr', | |
| col1: `LAST: ${lastOdds} (${lastCents}) Β· SPREAD: ${spread} Β· VOL: ${volume}`, | |
| key: 'spr' | |
| }); | |
| bids.forEach(b => newRows.push({ | |
| type: 'bid', cls: 'bid', | |
| col1: `<span style="color:var(--g);font-size:9px;background:rgba(0,212,126,.1);padding:1px 4px;border-radius:2px">BID</span>`, | |
| col2: `${toOdds(b.c).toFixed(2)} <span style="font-size:9px;color:var(--txt2)">(${Math.round(b.c * 100)}Β’)</span>`, | |
| col3: b.sz.toLocaleString() + ' sh', | |
| col4: '$' + (b.c * b.sz).toFixed(0), | |
| key: 'bid-' + Math.round(b.c * 100) | |
| })); | |
| const tb = $('pBook'); | |
| // If row count or structure changed, full rebuild (only on book shape change) | |
| const keysMatch = _pBookRows.length === newRows.length && | |
| _pBookRows.every((r, i) => r.key === newRows[i].key); | |
| if (!keysMatch) { | |
| // Full rebuild β rows added/removed/reordered | |
| let h = ''; | |
| newRows.forEach(r => { | |
| if (r.type === 'spr') { | |
| h += `<tr class="spr"><td colspan="4">${r.col1}</td></tr>`; | |
| } else { | |
| h += `<tr class="${r.cls}"><td>${r.col1}</td><td class="pr">${r.col2}</td><td>${r.col3}</td><td>${r.col4}</td></tr>`; | |
| } | |
| }); | |
| tb.innerHTML = h; | |
| } else { | |
| // Patch only changed cells β no DOM replacement = zero flicker | |
| const rows = tb.querySelectorAll('tr'); | |
| newRows.forEach((r, i) => { | |
| const prev = _pBookRows[i]; | |
| const tr = rows[i]; | |
| if (!tr) return; | |
| if (r.type === 'spr') { | |
| if (r.col1 !== prev.col1) tr.cells[0].innerHTML = r.col1; | |
| } else { | |
| if (r.col2 !== prev.col2) tr.cells[1].innerHTML = r.col2; // odds+cents | |
| if (r.col3 !== prev.col3) tr.cells[2].textContent = r.col3; // shares (text only) | |
| if (r.col4 !== prev.col4) tr.cells[3].textContent = r.col4; // total (text only) | |
| } | |
| }); | |
| } | |
| _pBookRows = newRows; | |
| // Meta row β simple text, no flicker risk | |
| $('pLast').textContent = lastOdds + ' (' + lastCents + ')'; | |
| $('pSprd').textContent = spread; | |
| $('pVol').textContent = volume; | |
| } | |
| function renderPM(m) { | |
| const y = parseFloat(m.outcomePrices?.[1] || 0.5); | |
| renderP({ asks: [{ price: (1 / y).toFixed(3), size: '1000' }], bids: [{ price: ((1 / y) + 0.05).toFixed(3), size: '800' }], last: (1 / y).toFixed(3), spread: '1Β’', volume: m.volume ? `$${(m.volume / 1000).toFixed(0)}K` : 'β' }); | |
| } | |
| // ββ REBEL777 WebSocket CLIENT ββ | |
| let rebelWs = null; | |
| let rebelWsActive = false; | |
| function onRebel777UrlChange() { } // placeholder | |
| function fetchRebel777() { | |
| const url = $('sRebel777Url').value.trim(); | |
| if (!url) { alert('Enter a rebel777 match URL first'); return; } | |
| // Tell proxy to start scraping this URL | |
| fetch(`http://localhost:3000/start-rebel777?url=${encodeURIComponent(url)}`) | |
| .catch(() => { | |
| setSt('bSt', 'PROXY DOWN', 'bdg-r'); | |
| $('bBook').innerHTML = `<div class="nodata" style="color:var(--r)">β οΈ proxy.js not running<br><span style="font-size:10px;color:var(--txt2)">Run: <b style="color:var(--y)">node proxy.js</b></span></div>`; | |
| }); | |
| // Connect WebSocket if not already connected | |
| if (rebelWs && rebelWs.readyState < 2) return; // already open/connecting | |
| connectRebelWs(); | |
| } | |
| function connectRebelWs() { | |
| setSt('bSt', 'CONNECTING...', 'bdg-w'); | |
| try { | |
| // Dynamic WebSocket URL to work on both localhost and cloud (Hugging Face) | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const host = window.location.host || 'localhost:3000'; | |
| const wsUrl = `${protocol}//${host}`; | |
| console.log(`[REBEL777] Connecting to ${wsUrl}...`); | |
| rebelWs = new WebSocket(wsUrl); | |
| } catch (e) { | |
| setSt('bSt', 'WS ERROR', 'bdg-r'); return; | |
| } | |
| rebelWs.onopen = () => { | |
| rebelWsActive = true; | |
| setSt('bSt', 'LOADING...', 'bdg-w'); | |
| // Only show spinner if we haven't received odds yet β preserve live display on reconnect | |
| if (!window._hasOdds) { | |
| $('bBook').innerHTML = `<div class="nodata" style="color:var(--y)">β³ Opening rebel777 page (first load ~6s)...</div>`; | |
| } | |
| console.log('[WS] Connected to proxy'); | |
| }; | |
| rebelWs.onmessage = (e) => { | |
| try { | |
| const msg = JSON.parse(e.data); | |
| if (msg.type === 'odds') { | |
| window._hasOdds = true; | |
| renderB(msg.data); | |
| setSt('bSt', 'REBEL LIVE β', 'bdg-ok'); | |
| incNC(); | |
| } else if (msg.type === 'status') { | |
| setSt('bSt', msg.state === 'ready' ? 'REBEL LIVE β' : 'LOADING...', msg.state === 'ready' ? 'bdg-ok' : 'bdg-w'); | |
| // Only update display if no odds have arrived yet β don't wipe a live book | |
| if (!window._hasOdds) { | |
| if (msg.state === 'ready') { | |
| $('bBook').innerHTML = `<div class="nodata" style="color:var(--g)">β Connected β waiting for first odds...</div>`; | |
| } else { | |
| $('bBook').innerHTML = `<div class="nodata" style="color:var(--y)">β³ Opening rebel777 page (first load ~6s)...</div>`; | |
| } | |
| } | |
| } else if (msg.type === 'debug') { | |
| console.warn('[rebel debug]', msg.data); | |
| if (!window._hasOdds) { | |
| $('bBook').innerHTML = `<div class="nodata" style="color:var(--y)"> | |
| β οΈ Connected but no odds found. bl-boxes: ${msg.data.blBoxCount}, odds spans: ${msg.data.oddsSpanCount}<br> | |
| <span style="font-size:10px;color:var(--txt2)">URL: ${msg.data.url}</span> | |
| </div>`; | |
| } | |
| } else if (msg.type === 'error') { | |
| setSt('bSt', 'ERR', 'bdg-r'); | |
| $('bBook').innerHTML = `<div class="nodata" style="color:var(--r)">β οΈ ${msg.msg}</div>`; | |
| } | |
| // ping = heartbeat, ignore | |
| } catch (err) { } | |
| }; | |
| rebelWs.onerror = () => { setSt('bSt', 'WS ERR', 'bdg-r'); rebelWsActive = false; }; | |
| rebelWs.onclose = () => { | |
| rebelWsActive = false; | |
| setSt('bSt', 'DISCONNECTED', 'bdg-w'); | |
| // Auto-reconnect after 3s if URL still set | |
| if ($('sRebel777Url').value.trim()) { | |
| setTimeout(connectRebelWs, 3000); | |
| } | |
| }; | |
| } | |
| function stopRebel777() { | |
| fetch('http://localhost:3000/stop-rebel777').catch(() => { }); | |
| if (rebelWs) rebelWs.close(); | |
| setSt('bSt', 'STOPPED', 'bdg-w'); | |
| } | |
| // ββ FETCH BHAV ββ | |
| async function fetchRebel777() { | |
| const url = $('sRebel777Url').value.trim(); | |
| if (!url) { demoB(); return; } | |
| setSt('bSt', 'LOADING...', 'bdg-w'); incNC(); | |
| // Dynamic host detection for local/cloud | |
| const host = window.location.host || 'localhost:3000'; | |
| const protocol = window.location.protocol; | |
| const proxyUrl = `${protocol}//${host}/start-rebel777?url=${encodeURIComponent(url)}`; | |
| const controller = new AbortController(); | |
| const timer = setTimeout(() => controller.abort(), 10000); | |
| try { | |
| const r = await fetch(proxyUrl, { signal: controller.signal }); | |
| clearTimeout(timer); | |
| if (!r.ok) throw new Error(`HTTP ${r.status}`); | |
| const d = await r.json(); | |
| if (d.error) throw new Error(d.error); | |
| renderB(d); setSt('bSt', 'LIVE β', 'bdg-ok'); | |
| } catch (e) { | |
| clearTimeout(timer); | |
| const isProxyDown = e.name === 'AbortError' || | |
| (e instanceof TypeError && ( | |
| e.message.toLowerCase().includes('failed to fetch') || | |
| e.message.toLowerCase().includes('networkerror') || | |
| e.message.toLowerCase().includes('load failed') || | |
| e.message.toLowerCase().includes('connection refused') | |
| )); | |
| setSt('bSt', isProxyDown ? 'PROXY DOWN' : 'ERR', 'bdg-r'); | |
| $('bBook').innerHTML = `<tr><td colspan="4" style="color:var(--r);padding:10px;text-align:center;font-size:11px"> | |
| β οΈ ${isProxyDown ? 'proxy.js not running β start it first' : e.message}<br> | |
| <span style="color:var(--txt2);font-size:10px">Run: <b style="color:var(--y)">node proxy.js</b> in the same folder, then refresh</span><br><br> | |
| <button onclick="demoB()" style="margin-top:6px;background:var(--s2);color:var(--txt2);border:1px solid var(--bd2);padding:4px 12px;border-radius:3px;cursor:pointer;font-family:monospace;font-size:10px">USE DEMO</button> | |
| </td></tr>`; | |
| } | |
| } | |
| function loadManualJson() { | |
| try { | |
| const raw = document.getElementById('manualJson')?.value.trim(); | |
| if (!raw) return; | |
| const d = JSON.parse(raw); | |
| renderB(d); setSt('bSt', 'MANUAL β', 'bdg-ok'); | |
| } catch (e) { alert('Invalid JSON: ' + e.message); } | |
| } | |
| function demoB() { | |
| renderB({ | |
| runners: [ | |
| { name: t1(), back: [{ odds: 2.16, size: 11394 }, { odds: 2.18, size: 17660 }, { odds: 2.20, size: 40718 }], lay: [{ odds: 2.24, size: 26933 }, { odds: 2.26, size: 16449 }, { odds: 2.28, size: 12005 }] }, | |
| { name: t2(), back: [{ odds: 1.79, size: 15108 }, { odds: 1.80, size: 21108 }, { odds: 1.81, size: 32611 }], lay: [{ odds: 1.82, size: 26228 }, { odds: 1.83, size: 21732 }, { odds: 1.84, size: 21083 }] } | |
| ] | |
| }); | |
| setSt('bSt', 'DEMO', 'bdg-w'); $('cB1').value = '1.82'; recalc(); | |
| } | |
| function renderB(d) { | |
| // Support: { result: [...] } with mtype=MATCH_ODDS β your bhav API format | |
| let runners = []; | |
| if (d.result && Array.isArray(d.result)) { | |
| const matchOdds = d.result.find(m => m.mtype === 'MATCH_ODDS' || m.name === 'Match Odds'); | |
| if (matchOdds && matchOdds.runners) runners = matchOdds.runners; | |
| } | |
| // Fallbacks for other formats | |
| if (!runners.length) runners = d.runners || d.markets?.[0]?.runners || []; | |
| if (!runners.length && d.team1) runners = [{ name: d.team1.name, back: d.team1.back, lay: d.team1.lay }, { name: d.team2.name, back: d.team2.back, lay: d.team2.lay }]; | |
| let h = ''; | |
| runners.forEach((runner, ri) => { | |
| const isT1 = ri === 0, cls = isT1 ? 'm' : 'k'; | |
| const displayName = _polyTeamNames[ri] && _polyTeamNames[ri] !== 'β' ? _polyTeamNames[ri] : (runner.name || `Runner ${ri + 1}`); | |
| const bk = (runner.back || runner.availableToBack || []).slice(0, 3); | |
| const lk = (runner.lay || runner.availableToLay || []).slice(0, 3); | |
| const layLevels = lk | |
| .map(c => parseFloat(c.odds || c.price)) | |
| .filter(v => Number.isFinite(v) && v > 1) | |
| .slice(0, 2); | |
| const bestLay = lk[0] ? parseFloat(lk[0].odds || lk[0].price) : null; | |
| // Store per-runner best lay for same-team arb mapping | |
| if (isT1) { | |
| window._r0Lay = bestLay; | |
| window._r0LayLevels = layLevels; | |
| } else { | |
| window._r1Lay = bestLay; | |
| window._r1LayLevels = layLevels; | |
| } | |
| let cells = ''; | |
| for (let i = 0; i < 3; i++) { | |
| const c = bk[i]; | |
| cells += c ? `<div class="bc bk${i === 2 ? ' bbk' : ''}"><div class="bo">${parseFloat(c.odds || c.price).toFixed(2)}</div><div class="bl">${(parseInt(c.size) || 0) >= 50000 ? '50K+' : (parseInt(c.size) || 0).toLocaleString()}</div></div>` : `<div class="bc bk"><div class="bo" style="color:var(--bd)">β</div></div>`; | |
| } | |
| for (let i = 0; i < 3; i++) { | |
| const c = lk[i]; | |
| cells += c ? `<div class="bc lk${i === 0 ? ' blk' : ''}"><div class="bo">${parseFloat(c.odds || c.price).toFixed(2)}</div><div class="bl">${(parseInt(c.size) || 0) >= 50000 ? '50K+' : (parseInt(c.size) || 0).toLocaleString()}</div></div>` : `<div class="bc lk"><div class="bo" style="color:var(--bd)">β</div></div>`; | |
| } | |
| h += `<div class="brow"><div class="brh"><span class="btn ${cls}">${displayName}</span><span style="font-size:9px;color:var(--txt2)">LAYβ : <span style="color:var(--lk)">${bestLay ? bestLay.toFixed(2) : 'β'}</span></span></div><div class="bcells">${cells}</div></div>`; | |
| }); | |
| $('bBook').innerHTML = h; | |
| const lay0 = window._r0Lay, lay1 = window._r1Lay; | |
| if (!isManualCalcActive()) { | |
| if (lay0) $('cB1').value = lay0.toFixed(3); | |
| if (lay1) $('cB2').value = lay1.toFixed(3); | |
| } | |
| bhavLay = lay0 || lay1 || null; | |
| recalc(); | |
| updateArbBox(); | |
| } | |
| function setSt(id, t, c) { $(id).textContent = t; $(id).className = 'bdg ' + c; } | |
| // ββ FALLBACK POLL ββ | |
| function startAR() { | |
| if (rtmr) clearInterval(rtmr); | |
| const s = parseInt($('sRef').value); | |
| if (!s) { | |
| // Refresh turned off β tell proxy to use a sensible default | |
| fetch('http://localhost:3000/set-interval?ms=4000').catch(() => { }); | |
| return; | |
| } | |
| const ms = s * 1000; | |
| // Sync proxy poll rate with UI refresh setting | |
| fetch('http://localhost:3000/set-interval?ms=' + ms).catch(() => { }); | |
| // Polymarket should not reconnect its WS on every refresh tick. | |
| rtmr = setInterval(() => { | |
| if ($('sPolyUrl').value.trim()) { | |
| if (_polyTokenIds.filter(Boolean).length) { | |
| _polyTokenIds.filter(Boolean).forEach(tokenId => polyPollLoop(tokenId)); | |
| } else if (_polyTokenId) { | |
| polyPollLoop(); | |
| } | |
| else fetchPoly(); | |
| } | |
| // rebel777 is handled entirely by proxy poll loop β no fetchRebel777() here | |
| }, ms); | |
| } | |
| function refreshAll() { | |
| if ($('sPolyUrl').value.trim()) fetchPoly(); else demoP(); | |
| if ($('sRebel777Url').value.trim()) fetchRebel777(); else demoB(); | |
| } | |
| document.addEventListener('visibilitychange', () => { | |
| if (document.hidden) return; | |
| if (_polyTokenIds.filter(Boolean).length) { | |
| _polyTokenIds.filter(Boolean).forEach(tokenId => { | |
| polyPollLoop(tokenId); | |
| connectPolyWs(tokenId); | |
| }); | |
| } | |
| if ($('sRebel777Url')?.value.trim() && (!rebelWs || rebelWs.readyState >= 2)) { | |
| connectRebelWs(); | |
| } | |
| checkArb(); | |
| }); | |
| // ββ INIT ββ | |
| window.onload = () => { | |
| initChart(); | |
| onT(); // set initial team labels | |
| onAH(); | |
| onRC(); | |
| recalc(); | |
| demoP(); | |
| demoB(); | |
| }; | |
| </script> | |
| </body> | |
| </html> | |