| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8"/> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| | <title>Options Intelligence β MSFT Β· BMNR Β· IREN Β· CRCL</title> |
| | <link href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/> |
| | <style> |
| | :root{ |
| | --bg:#F7F8FA;--surface:#fff;--surface2:#F0F2F7; |
| | --border:#E2E5EE;--border2:#C8CCDB; |
| | --t1:#0D0F1A;--t2:#3D4460;--t3:#8B92AA; |
| | --blue:#1A56DB;--blue-l:#EBF1FF;--blue-d:#1341B0; |
| | --green:#0A7A45;--green-l:#E8F8F0;--green-m:#D1F0E0; |
| | --red:#CC2828;--red-l:#FFF0F0;--red-m:#FFD8D8; |
| | --amber:#B45309;--amber-l:#FFF8ED;--amber-m:#FDECC8; |
| | --purple:#6326C7;--purple-l:#F3EEFF; |
| | --sh:0 1px 3px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04); |
| | --sh2:0 4px 16px rgba(0,0,0,.09),0 2px 6px rgba(0,0,0,.05); |
| | --r:10px; |
| | } |
| | *{box-sizing:border-box;margin:0;padding:0} |
| | body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--t1);-webkit-font-smoothing:antialiased;min-height:100vh} |
| | a{color:var(--blue);text-decoration:none}a:hover{text-decoration:underline} |
| | button,input{font-family:inherit;cursor:pointer} |
| | textarea:focus,input:focus{outline:none} |
| | ::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:var(--bg)} |
| | ::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px} |
| |
|
| | /* ββ HEADER ββ */ |
| | .hdr{background:var(--surface);border-bottom:1px solid var(--border);padding:0 28px;position:sticky;top:0;z-index:100;box-shadow:var(--sh)} |
| | .hdr-in{max-width:1280px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;height:58px} |
| | .logo{display:flex;align-items:center;gap:10px} |
| | .logo-mark{width:34px;height:34px;background:linear-gradient(135deg,#1A56DB,#6326C7);border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:17px;box-shadow:0 2px 8px rgba(26,86,219,.3)} |
| | .logo-title{font-size:16px;font-weight:700;letter-spacing:-.4px} |
| | .logo-sub{font-size:11px;color:var(--t3);margin-top:1px} |
| | .hdr-right{display:flex;align-items:center;gap:12px} |
| | .hdr-time{font-size:11px;color:var(--t3)} |
| | .hdr-vix{font-size:12px;font-weight:600;padding:4px 10px;border-radius:6px;background:var(--surface2);border:1px solid var(--border);display:none} |
| | .btn-refresh{display:flex;align-items:center;gap:6px;padding:8px 18px;background:var(--blue);color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;letter-spacing:-.1px;transition:all .15s} |
| | .btn-refresh:hover:not(:disabled){background:var(--blue-d);transform:translateY(-1px);box-shadow:0 3px 10px rgba(26,86,219,.35)} |
| | .btn-refresh:disabled{background:var(--border2);color:var(--t3);cursor:not-allowed;transform:none;box-shadow:none} |
| | @keyframes spin{to{transform:rotate(360deg)}} |
| | .spin{animation:spin .8s linear infinite} |
| | @keyframes fadeUp{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}} |
| |
|
| | /* ββ GATE ββ */ |
| | #gate{min-height:calc(100vh - 58px);display:flex;align-items:center;justify-content:center;padding:32px 16px} |
| | .gate-card{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:40px;max-width:460px;width:100%;box-shadow:var(--sh2)} |
| | .gate-logo{width:52px;height:52px;background:linear-gradient(135deg,#1A56DB,#6326C7);border-radius:14px;display:flex;align-items:center;justify-content:center;font-size:26px;margin:0 auto 20px;box-shadow:0 4px 12px rgba(26,86,219,.3)} |
| | .gate-h{font-size:21px;font-weight:700;text-align:center;margin-bottom:8px;letter-spacing:-.4px} |
| | .gate-p{font-size:13px;color:var(--t2);text-align:center;line-height:1.65;margin-bottom:28px} |
| | .gate-lbl{font-size:12px;font-weight:600;color:var(--t2);margin-bottom:6px} |
| | .gate-inp{width:100%;padding:11px 14px;border:1.5px solid var(--border);border-radius:8px;font-size:13px;font-family:'JetBrains Mono',monospace;color:var(--t1);background:var(--bg);transition:border-color .15s;margin-bottom:6px} |
| | .gate-inp:focus{border-color:var(--blue);background:#fff} |
| | .gate-hint{font-size:11px;color:var(--t3);line-height:1.6;margin-bottom:20px} |
| | .gate-err{font-size:12px;color:var(--red);margin-bottom:10px;display:none} |
| | .btn-start{width:100%;padding:12px;background:var(--blue);color:#fff;border:none;border-radius:8px;font-size:14px;font-weight:700;transition:all .15s} |
| | .btn-start:hover{background:var(--blue-d)} |
| | .feat-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-top:24px} |
| | .feat{padding:11px 14px;background:var(--bg);border-radius:8px;border:1px solid var(--border);font-size:11px;color:var(--t2);display:flex;align-items:flex-start;gap:8px;line-height:1.4} |
| | .feat-ic{font-size:15px;flex-shrink:0} |
| |
|
| | /* ββ PAGE ββ */ |
| | .page{max-width:1280px;margin:0 auto;padding:24px 28px 48px} |
| |
|
| | /* ββ SUMMARY STRIP ββ */ |
| | .summary-strip{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:0;margin-bottom:24px;overflow:hidden;box-shadow:var(--sh)} |
| | .summary-hdr{padding:12px 20px;background:var(--surface2);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between} |
| | .summary-hdr-title{font-size:12px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;color:var(--t3)} |
| | .summary-table{width:100%;border-collapse:collapse} |
| | .summary-table th{padding:10px 16px;font-size:10px;font-weight:700;letter-spacing:.7px;text-transform:uppercase;color:var(--t3);text-align:left;border-bottom:1px solid var(--border);background:var(--surface)} |
| | .summary-table td{padding:13px 16px;font-size:13px;border-bottom:1px solid var(--border);vertical-align:middle} |
| | .summary-table tr:last-child td{border-bottom:none} |
| | .summary-table tr:hover td{background:#FAFBFF} |
| | .sticker{font-size:14px;font-weight:800;letter-spacing:-.3px} |
| | .sprice{font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:500} |
| | .schg{font-size:11px;font-weight:600;padding:2px 7px;border-radius:4px;font-family:'JetBrains Mono',monospace;margin-left:6px} |
| | .up{background:var(--green-m);color:var(--green)}.dn{background:var(--red-m);color:var(--red)}.fl{background:var(--surface2);color:var(--t3)} |
| | .strategy-pill{display:inline-flex;align-items:center;padding:4px 10px;border-radius:6px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;white-space:nowrap} |
| | .conf-badge{font-size:10px;font-weight:700;padding:2px 7px;border-radius:4px;text-transform:uppercase;letter-spacing:.3px} |
| | .conf-high{background:var(--green-m);color:var(--green)} |
| | .conf-med{background:var(--amber-m);color:var(--amber)} |
| | .conf-low{background:var(--surface2);color:var(--t3)} |
| | .mono{font-family:'JetBrains Mono',monospace;font-size:12px} |
| | .summary-loading td{color:var(--t3);font-style:italic;font-size:12px} |
| |
|
| | /* ββ TICKER CARDS ββ */ |
| | .cards-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:20px} |
| | @media(max-width:900px){.cards-grid{grid-template-columns:1fr}} |
| |
|
| | .tcard{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);overflow:hidden;box-shadow:var(--sh);animation:fadeUp .35s ease both} |
| | .tcard:hover{box-shadow:var(--sh2)} |
| |
|
| | .tcard-hdr{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:flex-start;justify-content:space-between;gap:12px} |
| | .tcard-ticker{font-size:20px;font-weight:800;letter-spacing:-.5px} |
| | .tcard-name{font-size:11px;color:var(--t3);margin-top:2px} |
| | .tcard-price{text-align:right;flex-shrink:0} |
| | .price-big{font-size:20px;font-weight:700;font-family:'JetBrains Mono',monospace} |
| | .price-chg{font-size:11px;font-weight:600;padding:2px 8px;border-radius:5px;margin-top:4px;display:inline-block;font-family:'JetBrains Mono',monospace} |
| |
|
| | /* stats row */ |
| | .stats-band{display:grid;grid-template-columns:repeat(4,1fr);border-bottom:1px solid var(--border)} |
| | .sc{padding:11px 14px;border-right:1px solid var(--border)} |
| | .sc:last-child{border-right:none} |
| | .sc-l{font-size:10px;font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--t3);margin-bottom:4px} |
| | .sc-v{font-size:14px;font-weight:700;font-family:'JetBrains Mono',monospace} |
| | .sc-s{font-size:10px;color:var(--t3);margin-top:2px} |
| |
|
| | /* IV bar */ |
| | .iv-bar-wrap{padding:13px 20px;border-bottom:1px solid var(--border)} |
| | .iv-bar-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:7px} |
| | .iv-bar-lbl{font-size:11px;font-weight:600;color:var(--t2)} |
| | .iv-regime-tag{font-size:10px;font-weight:700;padding:2px 9px;border-radius:10px;text-transform:uppercase;letter-spacing:.4px} |
| | .rt-sell{background:var(--red-l);color:var(--red)} |
| | .rt-buy{background:var(--green-l);color:var(--green)} |
| | .rt-neutral{background:var(--amber-l);color:var(--amber)} |
| | .iv-track{height:6px;background:var(--surface2);border-radius:3px;overflow:hidden;border:1px solid var(--border)} |
| | .iv-fill{height:100%;border-radius:3px;transition:width .8s ease} |
| |
|
| | /* news pill */ |
| | .news-row{padding:9px 20px;border-bottom:1px solid var(--border);display:flex;align-items:flex-start;gap:8px;background:#FAFBFE} |
| | .news-tag{font-size:10px;font-weight:700;padding:2px 7px;border-radius:4px;background:var(--blue-l);color:var(--blue);flex-shrink:0;margin-top:1px;text-transform:uppercase;letter-spacing:.3px} |
| | .news-text{font-size:11px;color:var(--t2);line-height:1.55;flex:1} |
| | .news-src{font-size:10px;color:var(--t3);margin-top:2px} |
| |
|
| | /* recommendation block */ |
| | .rec-block{padding:18px 20px;border-bottom:1px solid var(--border)} |
| | .rec-block-title{font-size:10px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--t3);margin-bottom:12px} |
| |
|
| | .rec-banner{border-radius:9px;padding:14px;margin-bottom:14px;border:1.5px solid} |
| | .rec-top-row{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:10px} |
| | .rec-strat-name{font-size:15px;font-weight:800;letter-spacing:-.3px} |
| | .rec-dir{font-size:11px;color:var(--t2);margin-top:2px} |
| | .rec-conf{text-align:right;flex-shrink:0} |
| | .rec-strike-row{display:flex;flex-wrap:wrap;gap:10px} |
| | .rec-chip{display:flex;flex-direction:column;padding:7px 12px;border-radius:6px;background:rgba(255,255,255,.7);border:1px solid rgba(0,0,0,.06)} |
| | .rec-chip-l{font-size:9px;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--t3);margin-bottom:3px} |
| | .rec-chip-v{font-size:13px;font-weight:700;font-family:'JetBrains Mono',monospace} |
| |
|
| | /* P&L row */ |
| | .pnl-row{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:12px} |
| | .pnl-box{padding:9px 11px;border-radius:7px;border:1px solid var(--border);background:var(--bg)} |
| | .pnl-l{font-size:9px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--t3);margin-bottom:3px} |
| | .pnl-v{font-size:12px;font-weight:700;font-family:'JetBrains Mono',monospace} |
| |
|
| | /* greeks row */ |
| | .greeks-row{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:14px} |
| | .greek-box{padding:8px 10px;background:var(--surface2);border-radius:6px;text-align:center} |
| | .greek-sym{font-size:14px;font-weight:700;color:var(--t2);margin-bottom:2px} |
| | .greek-val{font-size:11px;font-family:'JetBrains Mono',monospace;color:var(--t1);font-weight:500} |
| | .greek-lbl{font-size:9px;color:var(--t3);margin-top:1px} |
| |
|
| | /* plain english */ |
| | .plain-block{padding:16px 20px;border-bottom:1px solid var(--border);background:#FAFCFF} |
| | .plain-title{font-size:10px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--blue);margin-bottom:8px;display:flex;align-items:center;gap:5px} |
| | .plain-text{font-size:13px;line-height:1.75;color:var(--t2)} |
| | .plain-text strong{color:var(--t1);font-weight:600} |
| |
|
| | /* risk + pass */ |
| | .risk-block{padding:12px 20px;border-bottom:1px solid var(--border);background:#FFFBF5} |
| | .risk-tag{font-size:10px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--amber);margin-bottom:5px} |
| | .risk-text{font-size:12px;line-height:1.65;color:#78350F} |
| |
|
| | .pass-block{padding:12px 20px;border-bottom:1px solid var(--border)} |
| | .pass-tag{font-size:10px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--t3);margin-bottom:5px} |
| | .pass-text{font-size:12px;line-height:1.65;color:var(--t2)} |
| |
|
| | /* alt trade */ |
| | .alt-block{padding:12px 20px;border-bottom:1px solid var(--border)} |
| | .alt-tag{font-size:10px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--t3);margin-bottom:6px} |
| | .alt-inner{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;background:var(--bg);border-radius:7px;border:1px solid var(--border)} |
| | .alt-text{font-size:12px;line-height:1.6;color:var(--t2)} |
| |
|
| | /* scores */ |
| | .scores-block{padding:14px 20px} |
| | .scores-title{font-size:10px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;color:var(--t3);margin-bottom:10px} |
| | .score-row{display:flex;align-items:center;gap:8px;margin-bottom:5px} |
| | .score-nm{font-size:11px;color:var(--t2);min-width:130px} |
| | .score-tr{flex:1;height:5px;background:var(--surface2);border-radius:3px;overflow:hidden} |
| | .score-fi{height:100%;border-radius:3px;transition:width 1s ease} |
| | .score-n{font-size:11px;font-weight:700;min-width:26px;text-align:right;font-family:'JetBrains Mono',monospace} |
| |
|
| | /* loading state */ |
| | .card-loading{padding:32px 20px;display:flex;flex-direction:column;align-items:center;gap:10px} |
| | .ld-spinner{width:28px;height:28px;border:2.5px solid var(--border);border-top-color:var(--blue);border-radius:50%;animation:spin .7s linear infinite} |
| | .ld-text{font-size:13px;color:var(--t3)} |
| | .ld-steps{font-size:11px;color:var(--border2);text-align:center;line-height:1.8} |
| |
|
| | /* error state */ |
| | .card-err{padding:28px 20px;text-align:center} |
| | .card-err-ico{font-size:30px;margin-bottom:8px} |
| | .card-err-msg{font-size:13px;color:var(--t2);line-height:1.6;margin-bottom:14px} |
| | .btn-retry{padding:7px 16px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--blue);font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;transition:background .15s} |
| | .btn-retry:hover{background:var(--blue-l)} |
| |
|
| | /* strategy color classes */ |
| | .s-sp{background:var(--purple-l);border-color:#C4B5FD}.b-sp{background:var(--purple-l);color:var(--purple)} |
| | .s-sc{background:var(--amber-l);border-color:#FCD34D}.b-sc{background:var(--amber-l);color:var(--amber)} |
| | .s-bc{background:var(--green-l);border-color:#6EE7B7}.b-bc{background:var(--green-l);color:var(--green)} |
| | .s-bp{background:var(--red-l);border-color:#FCA5A5}.b-bp{background:var(--red-l);color:var(--red)} |
| | .s-st{background:var(--blue-l);border-color:#93C5FD}.b-st{background:var(--blue-l);color:var(--blue)} |
| | .s-sg{background:var(--blue-l);border-color:#BFDBFE}.b-sg{background:var(--blue-l);color:var(--blue)} |
| | .s-ic{background:#F0FDF4;border-color:#86EFAC}.b-ic{background:#F0FDF4;color:#16A34A} |
| | .s-bs{background:var(--green-l);border-color:#6EE7B7}.b-bs{background:var(--green-l);color:var(--green)} |
| | .s-ps{background:var(--red-l);border-color:#FCA5A5}.b-ps{background:var(--red-l);color:var(--red)} |
| | .s-df{background:var(--bg);border-color:var(--border)}.b-df{background:var(--bg);color:var(--t2)} |
| |
|
| | footer{padding:20px 28px;text-align:center;font-size:11px;color:var(--t3);border-top:1px solid var(--border);line-height:1.8;background:var(--surface)} |
| | @media(max-width:700px){.page{padding:14px}.stats-band{grid-template-columns:repeat(2,1fr)}.pnl-row{grid-template-columns:repeat(2,1fr)}.greeks-row{grid-template-columns:repeat(2,1fr)}.hdr{padding:0 14px}} |
| | </style> |
| | </head> |
| | <body> |
| |
|
| | |
| | <header class="hdr"> |
| | <div class="hdr-in"> |
| | <div class="logo"> |
| | <div class="logo-mark">π</div> |
| | <div> |
| | <div class="logo-title">Options Intelligence</div> |
| | <div class="logo-sub">MSFT Β· BMNR Β· IREN Β· CRCL β Live AI Recommendations</div> |
| | </div> |
| | </div> |
| | <div class="hdr-right"> |
| | <span class="hdr-vix" id="hdr-vix"></span> |
| | <span class="hdr-time" id="hdr-time"></span> |
| | <button class="btn-refresh" id="btn-refresh" onclick="refreshAll()" disabled> |
| | <svg id="ref-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 019-9 9.75 9.75 0 016.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 01-9 9 9.75 9.75 0 01-6.74-2.74L3 16"/><path d="M3 21v-5h5"/></svg> |
| | Refresh |
| | </button> |
| | </div> |
| | </div> |
| | </header> |
| |
|
| | |
| | <div id="gate"> |
| | <div class="gate-card"> |
| | <div class="gate-logo">π</div> |
| | <div class="gate-h">Options Intelligence Dashboard</div> |
| | <div class="gate-p">Live AI-powered options recommendations for <strong>MSFT, BMNR, IREN, CRCL</strong>. Each refresh searches live market data and generates a fresh trade recommendation with plain English explanations.</div> |
| | <div class="gate-lbl">Anthropic API Key</div> |
| | <input type="password" id="gate-key" class="gate-inp" placeholder="sk-ant-api03-β¦"/> |
| | <div class="gate-hint">Your key stays in your browser only β never stored anywhere. Get one free at <a href="https://console.anthropic.com" target="_blank">console.anthropic.com</a>. Cost per refresh: ~$0.08β0.12.</div> |
| | <div class="gate-err" id="gate-err"></div> |
| | <button class="btn-start" onclick="startApp()">Launch Dashboard β</button> |
| | <div class="feat-grid"> |
| | <div class="feat"><div class="feat-ic">π</div><div>Live web search for current prices, IV and news on every refresh</div></div> |
| | <div class="feat"><div class="feat-ic">π°</div><div>News sources shown β know where the information came from</div></div> |
| | <div class="feat"><div class="feat-ic">π¬</div><div>Plain English explanation of every recommendation</div></div> |
| | <div class="feat"><div class="feat-ic">π―</div><div>Exact strike, expiry, entry price and breakevens</div></div> |
| | <div class="feat"><div class="feat-ic">β‘</div><div>Greeks: Delta, Gamma, Theta, Vega for every trade</div></div> |
| | <div class="feat"><div class="feat-ic">β οΈ</div><div>Risk scenario and "skip if" conditions per ticker</div></div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div id="app" style="display:none"> |
| | <div class="page"> |
| |
|
| | |
| | <div class="summary-strip"> |
| | <div class="summary-hdr"> |
| | <span class="summary-hdr-title">π At-a-Glance Summary β What to trade today</span> |
| | <span style="font-size:11px;color:var(--t3)" id="summary-ts"></span> |
| | </div> |
| | <table class="summary-table" id="summary-tbl"> |
| | <thead> |
| | <tr> |
| | <th>Ticker</th> |
| | <th>Price</th> |
| | <th>Strategy</th> |
| | <th>Strike</th> |
| | <th>Expiry / DTE</th> |
| | <th>Entry (Mid)</th> |
| | <th>Max Profit</th> |
| | <th>Max Loss</th> |
| | <th>Confidence</th> |
| | </tr> |
| | </thead> |
| | <tbody id="summary-body"> |
| | <tr class="summary-loading"> |
| | <td colspan="9" style="padding:18px 16px;color:var(--t3);font-style:italic;font-size:12px">β³ Loading live dataβ¦</td> |
| | </tr> |
| | </tbody> |
| | </table> |
| | </div> |
| |
|
| | |
| | <div class="cards-grid" id="cards-grid"></div> |
| |
|
| | </div> |
| | <footer> |
| | Options Intelligence Β· Powered by Claude AI + Live Web Search<br/> |
| | <strong>β Not financial advice.</strong> Options trading involves substantial risk of loss. Always verify recommendations independently before placing any trade. |
| | </footer> |
| | </div> |
| |
|
| | <script> |
| | 'use strict'; |
| | |
| | // ββ CONFIG ββββββββββββββββββββββββββββββββββββββββββββββββββ |
| | const TICKERS = [ |
| | {sym:'MSFT', name:'Microsoft Corporation'}, |
| | {sym:'BMNR', name:'BitMine Immersion Technologies'}, |
| | {sym:'IREN', name:'Iris Energy'}, |
| | {sym:'CRCL', name:'Circle Internet Financial'}, |
| | ]; |
| | |
| | // Strategy type β CSS class key |
| | const TYPE_MAP = { |
| | sell_put:'sp', sell_call:'sc', buy_call:'bc', buy_put:'bp', |
| | straddle:'st', strangle:'sg', condor:'ic', |
| | bull_spread:'bs', bear_spread:'ps', |
| | }; |
| | |
| | let API_KEY = ''; |
| | let running = false; |
| | let results = {}; // sym β parsed data |
| | |
| | // ββ GATE βββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| | document.getElementById('gate-key').addEventListener('keydown', e => { if(e.key==='Enter') startApp(); }); |
| | |
| | function startApp(){ |
| | const k = document.getElementById('gate-key').value.trim(); |
| | const err = document.getElementById('gate-err'); |
| | if(!k.startsWith('sk-ant-')){ err.textContent='β Key must start with sk-ant-'; err.style.display='block'; return; } |
| | err.style.display='none'; |
| | API_KEY = k; |
| | document.getElementById('gate').style.display='none'; |
| | document.getElementById('app').style.display='block'; |
| | document.getElementById('btn-refresh').disabled=false; |
| | initCards(); |
| | refreshAll(); |
| | } |
| | |
| | // ββ INIT CARDS ββββββββββββββββββββββββββββββββββββββββββββββββ |
| | function initCards(){ |
| | const grid = document.getElementById('cards-grid'); |
| | grid.innerHTML=''; |
| | TICKERS.forEach((t,i)=>{ |
| | const d=document.createElement('div'); |
| | d.className='tcard'; |
| | d.id='card-'+t.sym; |
| | d.style.animationDelay=(i*0.08)+'s'; |
| | d.innerHTML=loadingHTML(t); |
| | grid.appendChild(d); |
| | }); |
| | } |
| | |
| | function loadingHTML(t){ |
| | return `<div class="tcard-hdr"><div><div class="tcard-ticker">${t.sym}</div><div class="tcard-name">${t.name}</div></div></div> |
| | <div class="card-loading"><div class="ld-spinner"></div><div class="ld-text">Searching live dataβ¦</div> |
| | <div class="ld-steps">Fetching price & options chain<br>Calculating IV Rank & Greeks<br>Generating AI recommendation</div></div>`; |
| | } |
| | |
| | // ββ REFRESH ALL βββββββββββββββββββββββββββββββββββββββββββββββ |
| | async function refreshAll(){ |
| | if(running) return; |
| | running=true; |
| | results={}; |
| | const btn=document.getElementById('btn-refresh'); |
| | const ico=document.getElementById('ref-icon'); |
| | btn.disabled=true; ico.classList.add('spin'); |
| | document.getElementById('summary-body').innerHTML= |
| | '<tr><td colspan="9" style="padding:18px 16px;color:var(--t3);font-style:italic;font-size:12px">β³ Loadingβ¦</td></tr>'; |
| | |
| | // Reset cards |
| | TICKERS.forEach(t=>{ document.getElementById('card-'+t.sym).innerHTML=loadingHTML(t); }); |
| | |
| | // Run all in parallel |
| | await Promise.allSettled(TICKERS.map(t=>fetchAndRender(t))); |
| | |
| | // Rebuild summary after all done |
| | buildSummary(); |
| | |
| | const now=new Date(); |
| | const ts=now.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit'}); |
| | document.getElementById('hdr-time').textContent='Updated '+ts; |
| | document.getElementById('summary-ts').textContent='Last updated '+ts; |
| | |
| | ico.classList.remove('spin'); |
| | btn.disabled=false; |
| | running=false; |
| | } |
| | |
| | // ββ FETCH + RENDER ONE TICKER βββββββββββββββββββββββββββββββββ |
| | async function fetchAndRender(ticker){ |
| | try{ |
| | const d=await callClaude(ticker); |
| | results[ticker.sym]=d; |
| | renderCard(ticker,d); |
| | }catch(e){ |
| | renderError(ticker,e.message); |
| | } |
| | } |
| | |
| | // ββ CLAUDE API ββββββββββββββββββββββββββββββββββββββββββββββββ |
| | async function callClaude(ticker){ |
| | const today=new Date().toISOString().split('T')[0]; |
| | const dow=new Date().toLocaleDateString('en-US',{weekday:'long'}); |
| | |
| | const resp=await fetch('https://api.anthropic.com/v1/messages',{ |
| | method:'POST', |
| | headers:{ |
| | 'Content-Type':'application/json', |
| | 'x-api-key':API_KEY, |
| | 'anthropic-version':'2023-06-01', |
| | 'anthropic-dangerous-direct-browser-access':'true', |
| | }, |
| | body:JSON.stringify({ |
| | model:'claude-sonnet-4-20250514', |
| | max_tokens:1800, |
| | tools:[{type:'web_search_20250305',name:'web_search'}], |
| | system:`You are a senior options trader and quantitative analyst. Today is ${dow} ${today}. |
| | Your job: Search for LIVE current data on the given stock ticker, then produce a precise options recommendation as JSON. |
| | RULES: |
| | 1. Always search the web first β never use stale training data for prices or IV |
| | 2. Search for: current stock price, today's % change, options IV, recent news/catalysts, upcoming earnings, current VIX |
| | 3. After gathering data, return ONLY valid JSON matching the exact schema. No markdown fences, no prose outside the JSON. |
| | 4. Include the actual source name (e.g. "Reuters", "Yahoo Finance", "Bloomberg") for any news cited.`, |
| | messages:[{role:'user',content:buildPrompt(ticker,today)}], |
| | }), |
| | }); |
| | |
| | if(!resp.ok){ |
| | const e=await resp.json().catch(()=>({})); |
| | throw new Error((e.error&&e.error.message)||'API error '+resp.status); |
| | } |
| | |
| | const data=await resp.json(); |
| | // Extract text blocks (web search tool may produce mixed content blocks) |
| | const text=(data.content||[]).filter(b=>b.type==='text').map(b=>b.text).join(''); |
| | |
| | // Robust JSON extraction |
| | let js=text; |
| | const fm=js.match(/```(?:json)?\s*([\s\S]*?)```/); |
| | if(fm) js=fm[1]; |
| | const a=js.indexOf('{'), b=js.lastIndexOf('}'); |
| | if(a!==-1&&b!==-1) js=js.slice(a,b+1); |
| | |
| | try{ return JSON.parse(js); } |
| | catch(e){ throw new Error('Could not parse AI response β try refreshing.'); } |
| | } |
| | |
| | // ββ PROMPT ββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| | function buildPrompt(ticker,today){ |
| | return `Analyse ticker: ${ticker.sym} (${ticker.name}). Today: ${today}. |
| | |
| | STEP 1 β Search the web for: |
| | β’ Current price and today's % change for ${ticker.sym} |
| | β’ Current implied volatility (IV) and options chain activity |
| | β’ Most recent news or catalyst (past 7 days) β note the SOURCE |
| | β’ Next earnings date for ${ticker.sym} |
| | β’ Current VIX level and US 3-month T-bill yield |
| | |
| | STEP 2 β Respond with ONLY this exact JSON (no other text): |
| | |
| | { |
| | "ticker": "${ticker.sym}", |
| | "company": "${ticker.name}", |
| | "price": <number>, |
| | "price_change_pct": <number e.g. 1.23>, |
| | "price_trend": "<BULLISH|BEARISH|NEUTRAL>", |
| | "vix": <number>, |
| | "risk_free_rate": <number e.g. 4.8>, |
| | "iv_estimate": <number e.g. 42.0>, |
| | "iv_rank": <number 0-100>, |
| | "iv_regime": "<HIGH_SELL_PREMIUM|LOW_BUY_OPTIONS|NEUTRAL>", |
| | "hv_30d": <number>, |
| | "rsi_estimate": <number>, |
| | "sma20": <number>, |
| | "sma50": <number>, |
| | "earnings_date": "<YYYY-MM-DD or Unknown>", |
| | "days_to_earnings": <number or null>, |
| | "has_liquid_options": <true|false>, |
| | "options_note": "<one line on liquidity>", |
| | "news_headline": "<most important recent news headline in one sentence>", |
| | "news_source": "<source name e.g. Reuters, Yahoo Finance, Bloomberg, SEC filing>", |
| | "news_date": "<YYYY-MM-DD or approximate>", |
| | "news_impact": "<BULLISH|BEARISH|NEUTRAL β how this news affects the options strategy>", |
| | "data_quality": "<LIVE|ESTIMATED>", |
| | |
| | "top_strategy": { |
| | "name": "<Sell Put|Sell Covered Call|Buy Call|Buy Put|Buy Straddle|Buy Strangle|Iron Condor|Bull Call Spread|Bear Put Spread>", |
| | "type_class": "<sell_put|sell_call|buy_call|buy_put|straddle|strangle|condor|bull_spread|bear_spread>", |
| | "direction": "<Bullish|Bearish|Neutral|Non-Directional>", |
| | "confidence": "<HIGH|MEDIUM|LOW>", |
| | "strike": "<e.g. $415>", |
| | "expiry": "<e.g. Apr 17 2025>", |
| | "dte": "<e.g. 42 DTE>", |
| | "entry_mid": "<e.g. $3.20>", |
| | "max_profit": "<e.g. $320 per contract>", |
| | "max_loss": "<e.g. $680 per contract>", |
| | "breakeven": "<e.g. $411.80 or Up: $420 / Down: $400>", |
| | "delta": "<e.g. -0.28>", |
| | "gamma": "<e.g. 0.012>", |
| | "theta": "<e.g. -$0.09/day>", |
| | "vega": "<e.g. $0.18 per 1pt IV>", |
| | "technical_rationale": "<2-3 sentences: the professional analysis using IV rank, trend, Greeks, VIX>", |
| | "plain_english": "<3-4 sentences in simple language explaining what this trade is, why it makes sense right now, and what outcome you are expecting. Avoid jargon. Write as if explaining to a smart friend who does not trade options.>", |
| | "risk_scenario": "<one specific event that immediately kills this trade>", |
| | "pass_if": "<two specific conditions under which you skip this trade today>" |
| | }, |
| | |
| | "alternative_strategy": { |
| | "name": "<strategy name>", |
| | "type_class": "<type class>", |
| | "direction": "<direction>", |
| | "strike": "<strike>", |
| | "expiry": "<expiry>", |
| | "entry_mid": "<mid price>", |
| | "rationale": "<2 sentences: why this is the second best option>" |
| | }, |
| | |
| | "strategy_scores": [ |
| | {"name":"Sell Put","score":<0-100>}, |
| | {"name":"Sell Call","score":<0-100>}, |
| | {"name":"Buy Call","score":<0-100>}, |
| | {"name":"Buy Put","score":<0-100>}, |
| | {"name":"Buy Straddle","score":<0-100>}, |
| | {"name":"Iron Condor","score":<0-100>}, |
| | {"name":"Bull Spread","score":<0-100>}, |
| | {"name":"Bear Spread","score":<0-100>} |
| | ] |
| | } |
| | |
| | If ${ticker.sym} has no liquid options (very small cap / newly listed), set has_liquid_options:false and adjust top_strategy to an equity-level recommendation. Always include your news source.`; |
| | } |
| | |
| | // ββ BUILD SUMMARY TABLE βββββββββββββββββββββββββββββββββββββββ |
| | function buildSummary(){ |
| | const tbody=document.getElementById('summary-body'); |
| | if(!Object.keys(results).length){ tbody.innerHTML='<tr><td colspan="9" style="padding:16px;color:var(--t3);font-size:12px">No results yet.</td></tr>'; return; } |
| | tbody.innerHTML=''; |
| | TICKERS.forEach(t=>{ |
| | const d=results[t.sym]; |
| | if(!d){ |
| | const tr=document.createElement('tr'); |
| | tr.innerHTML=`<td class="sticker">${t.sym}</td><td colspan="8" style="color:var(--t3);font-size:12px;font-style:italic">Failed to load</td>`; |
| | tbody.appendChild(tr); |
| | return; |
| | } |
| | const ts=d.top_strategy||{}; |
| | const tc=TYPE_MAP[ts.type_class]||'df'; |
| | const chgSign=d.price_change_pct>=0?'+':''; |
| | const chgCls=d.price_change_pct>0?'up':d.price_change_pct<0?'dn':'fl'; |
| | const confCls=ts.confidence==='HIGH'?'conf-high':ts.confidence==='MEDIUM'?'conf-med':'conf-low'; |
| | const tr=document.createElement('tr'); |
| | tr.innerHTML=` |
| | <td><span class="sticker">${t.sym}</span><br><span style="font-size:10px;color:var(--t3)">${t.name}</span></td> |
| | <td><span class="sprice">$${fmt(d.price)}</span><span class="schg ${chgCls}">${chgSign}${fmt(d.price_change_pct)}%</span></td> |
| | <td><span class="strategy-pill b-${tc}">${ts.name||'β'}</span></td> |
| | <td><span class="mono">${ts.strike||'β'}</span></td> |
| | <td><span class="mono" style="font-size:11px">${ts.expiry||'β'}<br><span style="color:var(--t3)">${ts.dte||''}</span></span></td> |
| | <td><span class="mono">${ts.entry_mid||'β'}</span></td> |
| | <td style="color:var(--green);font-weight:600;font-size:12px">${ts.max_profit||'β'}</td> |
| | <td style="color:var(--red);font-weight:600;font-size:12px">${ts.max_loss||'β'}</td> |
| | <td><span class="conf-badge ${confCls}">${ts.confidence||'β'}</span></td>`; |
| | tbody.appendChild(tr); |
| | }); |
| | } |
| | |
| | // ββ RENDER CARD βββββββββββββββββββββββββββββββββββββββββββββββ |
| | function renderCard(ticker,d){ |
| | const card=document.getElementById('card-'+ticker.sym); |
| | if(!card) return; |
| | |
| | // Update VIX in header once |
| | if(d.vix && document.getElementById('hdr-vix').style.display==='none'||!document.getElementById('hdr-vix').textContent){ |
| | const vixEl=document.getElementById('hdr-vix'); |
| | vixEl.textContent=`VIX ${fmt(d.vix)}`; |
| | vixEl.style.display='block'; |
| | } |
| | |
| | const ts=d.top_strategy||{}; |
| | const alt=d.alternative_strategy||{}; |
| | const tc=TYPE_MAP[ts.type_class]||'df'; |
| | const altc=TYPE_MAP[alt.type_class]||'df'; |
| | const chgSign=d.price_change_pct>=0?'+':''; |
| | const chgCls=d.price_change_pct>0?'up':d.price_change_pct<0?'dn':'fl'; |
| | const ivFill=Math.min(100,d.iv_rank||0); |
| | const ivColor=d.iv_rank>60?'var(--red)':d.iv_rank<35?'var(--green)':'var(--amber)'; |
| | const regClass=d.iv_regime==='HIGH_SELL_PREMIUM'?'rt-sell':d.iv_regime==='LOW_BUY_OPTIONS'?'rt-buy':'rt-neutral'; |
| | const regLabel=d.iv_regime==='HIGH_SELL_PREMIUM'?'Sell Premium β':d.iv_regime==='LOW_BUY_OPTIONS'?'Buy Options β':'Neutral IV'; |
| | const trendColor=d.price_trend==='BULLISH'?'var(--green)':d.price_trend==='BEARISH'?'var(--red)':'var(--amber)'; |
| | const dq=d.data_quality||''; |
| | const dqPill=dq.includes('LIVE')? |
| | `<span style="font-size:10px;padding:2px 7px;border-radius:4px;background:var(--green-m);color:var(--green);font-weight:700">β LIVE</span>`: |
| | `<span style="font-size:10px;padding:2px 7px;border-radius:4px;background:var(--amber-m);color:var(--amber);font-weight:700">β EST</span>`; |
| | const confCls=ts.confidence==='HIGH'?'conf-high':ts.confidence==='MEDIUM'?'conf-med':'conf-low'; |
| | const scores=(d.strategy_scores||[]).sort((a,b)=>b.score-a.score); |
| | const newsImpactColor=d.news_impact==='BULLISH'?'var(--green)':d.news_impact==='BEARISH'?'var(--red)':'var(--t3)'; |
| | |
| | card.innerHTML=` |
| | |
| | <div class="tcard-hdr"> |
| | <div> |
| | <div class="tcard-ticker">${d.ticker} ${dqPill}</div> |
| | <div class="tcard-name">${d.company}</div> |
| | </div> |
| | <div class="tcard-price"> |
| | <div class="price-big">$${fmt(d.price)}</div> |
| | <div class="price-chg ${chgCls}">${chgSign}${fmt(d.price_change_pct)}%</div> |
| | </div> |
| | </div> |
| | |
| | |
| | <div class="stats-band"> |
| | <div class="sc"> |
| | <div class="sc-l">Trend</div> |
| | <div class="sc-v" style="color:${trendColor}">${d.price_trend||'β'}</div> |
| | <div class="sc-s">RSI ${fmt(d.rsi_estimate,0)}</div> |
| | </div> |
| | <div class="sc"> |
| | <div class="sc-l">ATM IV</div> |
| | <div class="sc-v">${fmt(d.iv_estimate)}%</div> |
| | <div class="sc-s">HV30 ${fmt(d.hv_30d)}%</div> |
| | </div> |
| | <div class="sc"> |
| | <div class="sc-l">Earnings</div> |
| | <div class="sc-v" style="font-size:12px">${d.earnings_date&&d.earnings_date!=='Unknown'?d.earnings_date:'β'}</div> |
| | <div class="sc-s">${d.days_to_earnings?d.days_to_earnings+'d away':''}</div> |
| | </div> |
| | <div class="sc"> |
| | <div class="sc-l">Options</div> |
| | <div class="sc-v" style="font-size:12px;color:${d.has_liquid_options?'var(--green)':'var(--red)'}"> |
| | ${d.has_liquid_options?'β Liquid':'β Thin'} |
| | </div> |
| | <div class="sc-s" style="font-size:9px">${(d.options_note||'').substring(0,28)}</div> |
| | </div> |
| | </div> |
| | |
| | |
| | <div class="iv-bar-wrap"> |
| | <div class="iv-bar-row"> |
| | <span class="iv-bar-lbl">IV Rank: <strong>${fmt(d.iv_rank,0)}</strong> / 100</span> |
| | <span class="iv-regime-tag ${regClass}">${regLabel}</span> |
| | </div> |
| | <div class="iv-track"><div class="iv-fill" style="width:${ivFill}%;background:${ivColor}"></div></div> |
| | </div> |
| | |
| | |
| | ${d.news_headline?`<div class="news-row"> |
| | <span class="news-tag" style="color:${newsImpactColor}">News</span> |
| | <div> |
| | <div class="news-text">${esc(d.news_headline)}</div> |
| | <div class="news-src">π° Source: <strong>${esc(d.news_source||'Unknown')}</strong>${d.news_date?' Β· '+d.news_date:''} Β· Impact: <span style="color:${newsImpactColor};font-weight:600">${d.news_impact||'β'}</span></div> |
| | </div> |
| | </div>`:''} |
| | |
| | |
| | <div class="rec-block"> |
| | <div class="rec-block-title">β Top Recommendation</div> |
| | |
| | <div class="rec-banner s-${tc}"> |
| | <div class="rec-top-row"> |
| | <div> |
| | <div class="rec-strat-name">${ts.name||'β'}</div> |
| | <div class="rec-dir">${ts.direction||''}</div> |
| | </div> |
| | <div class="rec-conf"> |
| | <span class="conf-badge ${confCls}">${ts.confidence||'β'}</span> |
| | <div style="font-size:9px;color:var(--t3);margin-top:4px;text-align:right">confidence</div> |
| | </div> |
| | </div> |
| | <div class="rec-strike-row"> |
| | <div class="rec-chip"> |
| | <span class="rec-chip-l">Strike</span> |
| | <span class="rec-chip-v">${ts.strike||'β'}</span> |
| | </div> |
| | <div class="rec-chip"> |
| | <span class="rec-chip-l">Expiry</span> |
| | <span class="rec-chip-v" style="font-size:11px">${ts.expiry||'β'}</span> |
| | </div> |
| | <div class="rec-chip"> |
| | <span class="rec-chip-l">DTE</span> |
| | <span class="rec-chip-v">${ts.dte||'β'}</span> |
| | </div> |
| | <div class="rec-chip"> |
| | <span class="rec-chip-l">Entry Mid</span> |
| | <span class="rec-chip-v">${ts.entry_mid||'β'}</span> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | |
| | <div class="pnl-row"> |
| | <div class="pnl-box"><div class="pnl-l">Max Profit</div><div class="pnl-v" style="color:var(--green)">${ts.max_profit||'β'}</div></div> |
| | <div class="pnl-box"><div class="pnl-l">Max Loss</div><div class="pnl-v" style="color:var(--red)">${ts.max_loss||'β'}</div></div> |
| | <div class="pnl-box"><div class="pnl-l">Breakeven</div><div class="pnl-v" style="font-size:11px">${ts.breakeven||'β'}</div></div> |
| | </div> |
| | |
| | |
| | <div class="greeks-row"> |
| | <div class="greek-box"><div class="greek-sym">Ξ</div><div class="greek-val">${ts.delta||'β'}</div><div class="greek-lbl">Delta</div></div> |
| | <div class="greek-box"><div class="greek-sym">Ξ</div><div class="greek-val">${ts.gamma||'β'}</div><div class="greek-lbl">Gamma</div></div> |
| | <div class="greek-box"><div class="greek-sym">ΞΈ</div><div class="greek-val">${ts.theta||'β'}</div><div class="greek-lbl">Theta/day</div></div> |
| | <div class="greek-box"><div class="greek-sym">Ξ½</div><div class="greek-val">${ts.vega||'β'}</div><div class="greek-lbl">Vega/pt</div></div> |
| | </div> |
| | |
| | |
| | ${ts.technical_rationale?`<div style="font-size:12px;line-height:1.7;color:var(--t2);margin-bottom:0">${esc(ts.technical_rationale)}</div>`:''} |
| | </div> |
| | |
| | |
| | ${ts.plain_english?`<div class="plain-block"> |
| | <div class="plain-title">π¬ In Plain English</div> |
| | <div class="plain-text">${esc(ts.plain_english)}</div> |
| | </div>`:''} |
| | |
| | |
| | ${ts.risk_scenario?`<div class="risk-block"> |
| | <div class="risk-tag">β Key Risk</div> |
| | <div class="risk-text">${esc(ts.risk_scenario)}</div> |
| | </div>`:''} |
| | |
| | |
| | ${ts.pass_if?`<div class="pass-block"> |
| | <div class="pass-tag">π« Skip this trade ifβ¦</div> |
| | <div class="pass-text">${esc(ts.pass_if)}</div> |
| | </div>`:''} |
| | |
| | |
| | ${alt.name?`<div class="alt-block"> |
| | <div class="alt-tag">π Alternative Trade</div> |
| | <div class="alt-inner"> |
| | <span class="strategy-pill b-${altc}" style="font-size:10px;flex-shrink:0">${alt.name}</span> |
| | <div class="alt-text"><strong>${alt.strike||''} ${alt.expiry||''}</strong> Β· Mid ${alt.entry_mid||'β'}<br>${esc(alt.rationale||'')}</div> |
| | </div> |
| | </div>`:''} |
| | |
| | |
| | <div class="scores-block"> |
| | <div class="scores-title">All Strategy Scores</div> |
| | ${scores.map(s=>{ |
| | const sc=s.score||0; |
| | const col=sc>=70?'var(--green)':sc>=50?'var(--blue)':sc>=35?'var(--amber)':'var(--border2)'; |
| | return `<div class="score-row"> |
| | <span class="score-nm">${s.name}</span> |
| | <div class="score-tr"><div class="score-fi" style="width:${sc}%;background:${col}"></div></div> |
| | <span class="score-n" style="color:${col}">${sc}</span> |
| | </div>`; |
| | }).join('')} |
| | </div>`; |
| | } |
| | |
| | // ββ RENDER ERROR ββββββββββββββββββββββββββββββββββββββββββββββ |
| | function renderError(ticker,msg){ |
| | const card=document.getElementById('card-'+ticker.sym); |
| | if(!card) return; |
| | card.innerHTML=` |
| | <div class="tcard-hdr"><div><div class="tcard-ticker">${ticker.sym}</div><div class="tcard-name">${ticker.name}</div></div></div> |
| | <div class="card-err"> |
| | <div class="card-err-ico">β οΈ</div> |
| | <div class="card-err-msg">${esc(msg)}</div> |
| | <button class="btn-retry" onclick="retryOne('${ticker.sym}')">β© Retry ${ticker.sym}</button> |
| | </div>`; |
| | } |
| | |
| | async function retryOne(sym){ |
| | const t=TICKERS.find(x=>x.sym===sym); |
| | if(!t) return; |
| | document.getElementById('card-'+sym).innerHTML=loadingHTML(t); |
| | await fetchAndRender(t); |
| | buildSummary(); |
| | } |
| | |
| | // ββ HELPERS βββββββββββββββββββββββββββββββββββββββββββββββββββ |
| | function fmt(n,dec=2){ |
| | if(n===null||n===undefined||n==='') return 'β'; |
| | const x=parseFloat(n); |
| | return isNaN(x)?String(n):x.toFixed(dec); |
| | } |
| | function esc(s){ |
| | return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); |
| | } |
| | </script> |
| | </body> |
| | </html> |
| |
|