← back to Stock Vshape Viewer

public/index.html

331 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Stock V-Shape · Live 3D Screener</title>
<style>
  :root{
    --bg:#070b14; --panel:rgba(14,20,34,.92); --stroke:#1e2a44;
    --ink:#e8eefc; --dim:#8ea3c8; --pass:#39d98a; --fail:#ff5c7a;
    --high:#39d98a; --trough:#ff5c7a; --newhigh:#ffd54a; --ma50:#5cc8ff; --ma200:#c792ff;
    --buy:#39d98a; --buy2:#2ee6c8; --watch:#ffd54a; --ext:#ff9f43; --neu:#8ea3c8;
  }
  *{box-sizing:border-box}
  html,body{margin:0;height:100%;background:var(--bg);color:var(--ink);overflow:hidden;
    font:14px/1.45 -apple-system,BlinkMacSystemFont,"SF Pro Text",Segoe UI,Roboto,sans-serif}
  #app{position:fixed;inset:0}
  canvas{display:block}
  .glass{background:var(--panel);border:1px solid var(--stroke);border-radius:14px;
    backdrop-filter:blur(10px);box-shadow:0 12px 40px rgba(0,0,0,.45)}
  button{font-family:inherit}
  .btn{background:#101a30;border:1px solid var(--stroke);color:var(--ink);border-radius:9px;
    padding:7px 11px;font-size:12px;cursor:pointer}
  .btn:hover{border-color:#3a568f}.btn.on{background:#1c2c50;border-color:#4a6bb5}

  /* ===== top criteria bar ===== */
  #topbar{position:fixed;top:0;left:0;right:0;height:52px;z-index:30;display:flex;align-items:center;
    gap:14px;padding:0 14px 0 62px;background:linear-gradient(180deg,rgba(10,15,26,.97),rgba(10,15,26,.82));
    border-bottom:1px solid var(--stroke);backdrop-filter:blur(8px)}
  #topbar .brand{font-weight:700;font-size:14px;white-space:nowrap}
  #topbar .brand span{color:var(--dim);font-weight:400}
  .crit{display:flex;gap:9px;flex:1;overflow-x:auto;scrollbar-width:none}
  .crit::-webkit-scrollbar{display:none}
  .cchip{display:flex;align-items:center;gap:6px;padding:5px 10px;border:1px solid var(--stroke);
    border-radius:20px;font-size:11.5px;white-space:nowrap;color:var(--dim);background:rgba(255,255,255,.02)}
  .cchip b{color:var(--ink);font-weight:600}
  .cchip .num{display:inline-flex;width:16px;height:16px;border-radius:50%;background:#22345c;color:#cfe0ff;
    align-items:center;justify-content:center;font-size:10px;font-weight:700}
  #live{font-size:11px;color:var(--dim);white-space:nowrap;display:flex;align-items:center;gap:6px}
  #live .dotlive{width:8px;height:8px;border-radius:50%;background:var(--pass);box-shadow:0 0 8px var(--pass);
    animation:pulse 1.6s infinite}
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}

  /* ===== hamburger + left panel ===== */
  #burger{position:fixed;top:9px;left:12px;z-index:40;width:38px;height:34px;border-radius:9px;
    background:#101a30;border:1px solid var(--stroke);color:var(--ink);cursor:pointer;font-size:16px}
  #burger:hover{border-color:#4a6bb5}
  #left{position:fixed;top:52px;left:0;bottom:0;width:308px;z-index:25;transform:translateX(-100%);
    transition:transform .22s ease;display:flex;flex-direction:column;
    background:var(--panel);border-right:1px solid var(--stroke);backdrop-filter:blur(10px)}
  #left.open{transform:translateX(0)}
  #left .scroll{overflow-y:auto;padding:12px;flex:1}
  .sec{margin-bottom:16px}
  .sec h3{margin:0 0 8px;font-size:11px;letter-spacing:.6px;text-transform:uppercase;color:var(--dim)}
  .addrow{display:flex;gap:6px}
  .addrow input{flex:1;background:#0c1424;border:1px solid var(--stroke);color:var(--ink);border-radius:8px;
    padding:8px 10px;font-size:13px;text-transform:uppercase}
  .wl{display:flex;flex-direction:column;gap:5px;margin-top:8px}
  .wl .item{display:flex;align-items:center;gap:8px;padding:7px 9px;border-radius:8px;background:rgba(255,255,255,.03)}
  .wl .item.dim{opacity:.4}
  .wl .tk{font-weight:700;width:52px;font-size:13px}
  .wl .item .nm{flex:1;min-width:0;font-size:11.5px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .wl .vb{font-size:9.5px;font-weight:700;padding:2px 5px;border-radius:5px}
  .wl .vb.PASS{background:rgba(57,217,138,.16);color:var(--pass)}
  .wl .vb.FAIL{background:rgba(255,92,122,.16);color:var(--fail)}
  .wl .vb.ERR{background:rgba(255,159,67,.16);color:var(--ext)}
  .wl .x{cursor:pointer;color:#63769c;font-size:14px}.wl .x:hover{color:var(--fail)}
  .wl .eye{cursor:pointer;font-size:13px;opacity:.8}

  .fld{margin-bottom:12px}
  .fld label{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dim);margin-bottom:4px}
  .fld label b{color:var(--ink)}
  .fld input[type=range]{width:100%;accent-color:#5c7cff}
  .segs{display:flex;gap:4px}
  .segs button{flex:1;padding:5px 0;font-size:10.5px;background:#0c1424;border:1px solid var(--stroke);
    color:var(--dim);border-radius:7px;cursor:pointer}
  .segs button.on{background:#1c2c50;border-color:#4a6bb5;color:var(--ink)}
  .chk{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dim);margin-bottom:9px;cursor:pointer}
  .chk input{accent-color:#5c7cff;width:15px;height:15px}
  .ssrow{display:flex;gap:6px;margin-bottom:8px}
  .ssrow input{flex:1;background:#0c1424;border:1px solid var(--stroke);color:var(--ink);border-radius:8px;padding:7px 9px;font-size:12px}
  .saved{display:flex;flex-direction:column;gap:5px}
  .saved .s{display:flex;align-items:center;gap:8px;padding:6px 9px;border-radius:8px;background:rgba(255,255,255,.03);font-size:12px;cursor:pointer}
  .saved .s:hover{background:rgba(87,140,255,.12)}
  .saved .s .x{margin-left:auto;color:#63769c}.saved .s .x:hover{color:var(--fail)}
  .matchline{font-size:11px;color:var(--dim);padding:6px 2px}
  .btn.scan{background:linear-gradient(180deg,#1c3a5e,#14263f);border-color:#2f5a8f}
  .btn.scan:hover{border-color:#5c8fd6}
  .btn.go{width:100%;margin-top:8px;padding:11px;font-weight:700;font-size:13px;
    background:linear-gradient(180deg,#1c5e3a,#124029);border-color:#2f8f5a;color:#d8ffe9}
  .btn.go:hover{border-color:#39d98a;box-shadow:0 0 14px rgba(57,217,138,.25)}
  .scanrow{margin:8px 0}.scanrow label{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dim);margin-bottom:4px}
  .scanrow input[type=range]{width:100%;accent-color:#5c7cff}
  .scanprog{font-size:11px;color:var(--dim);min-height:14px;margin:4px 0}
  .scanprog .bar{height:5px;background:#0c1424;border-radius:3px;overflow:hidden;margin-top:4px}
  .scanprog .bar i{display:block;height:100%;background:linear-gradient(90deg,#5c8fd6,#39d98a);width:0}
  .results{display:flex;flex-direction:column;gap:3px;margin-top:6px;max-height:230px;overflow:auto}
  .results .r{display:flex;align-items:center;gap:7px;padding:5px 7px;border-radius:7px;background:rgba(255,255,255,.03);cursor:pointer;font-size:11.5px}
  .results .r:hover{background:rgba(87,140,255,.12)}
  .results .r .bs{font-weight:700;width:26px;text-align:center;border-radius:5px;font-size:11px}
  .results .r .tk{font-weight:700;width:50px}
  .results .r .mm{flex:1;color:var(--dim);font-size:10.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  table.opts td,table.opts th{padding:3px 5px}
  .opt-call{color:var(--pass)}.opt-put{color:var(--fail)}
  #d-opts{font-size:11px;max-height:180px;overflow:auto;margin:2px 0}

  /* ===== bottom controls ===== */
  #ctl{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);z-index:15;display:flex;gap:8px;padding:8px}
  #hint{position:fixed;bottom:60px;left:50%;transform:translateX(-50%);color:var(--dim);font-size:12px;z-index:5;pointer-events:none;opacity:.8}
  #legend3d{position:fixed;bottom:14px;left:14px;z-index:12;padding:8px 10px;font-size:10.5px;color:var(--dim);opacity:.9}
  #legend3d .lg-title{font-weight:700;color:var(--ink);font-size:11px;margin-bottom:4px;cursor:pointer;user-select:none}
  #legend3d #lg-body{display:flex;flex-direction:column;gap:3px}
  #legend3d #lg-body.hidden{display:none}
  #legend3d .sw{display:inline-block;width:9px;height:9px;border-radius:2px;vertical-align:middle;margin-right:2px}
  #legend3d .ln{display:inline-block;width:13px;height:2px;vertical-align:middle;margin-right:2px}
  #legend3d .glowdot{display:inline-block;width:9px;height:9px;border-radius:50%;background:#fff;box-shadow:0 0 5px #9fd8ff;vertical-align:middle;margin-right:2px}

  /* ===== detail panel ===== */
  #detail{position:fixed;bottom:14px;right:14px;width:min(470px,46vw);max-height:calc(100vh - 80px);
    overflow:auto;padding:14px;z-index:20;display:none}
  #detail.show{display:block}
  #detail .dh{display:flex;justify-content:space-between;align-items:baseline;gap:8px}
  #detail .dh b{font-size:16px}#detail .dh .x{cursor:pointer;color:var(--dim);font-size:18px}
  #detail .sub{color:var(--dim);font-size:12px;margin:2px 0 8px}
  .zone{display:inline-block;padding:4px 10px;border-radius:8px;font-size:12px;font-weight:600;margin-bottom:8px}
  .zone.buy{background:rgba(57,217,138,.16);color:var(--buy)}
  .zone.buy2{background:rgba(46,230,200,.16);color:var(--buy2)}
  .zone.watch{background:rgba(255,213,74,.16);color:var(--watch)}
  .zone.ext{background:rgba(255,159,67,.16);color:var(--ext)}
  .zone.neu{background:rgba(142,163,200,.14);color:var(--neu)}
  .tfrow{display:flex;gap:5px;margin:2px 0 8px}
  .tfrow button{flex:1;padding:5px 0;font-size:11px;background:#0c1424;border:1px solid var(--stroke);
    color:var(--dim);border-radius:7px;cursor:pointer}
  .tfrow button.on{background:#1c2c50;border-color:#4a6bb5;color:var(--ink)}
  #chart{width:100%;height:196px;display:block}
  .brushwrap{margin:4px 0 2px}
  #d-brush{width:100%;height:38px;display:block;cursor:ew-resize;border:1px solid var(--stroke);border-radius:6px;background:#0a1120}
  .brushhint{font-size:9.5px;color:#5c6f92;margin-top:2px;text-align:center}
  .results .r .optbadge{font-size:10px;font-weight:700;padding:1px 5px;border-radius:5px;min-width:18px;text-align:center}
  .presets{display:flex;flex-direction:column;gap:4px}
  .presets .p{padding:7px 9px;border-radius:8px;background:rgba(255,255,255,.03);cursor:pointer;border:1px solid transparent}
  .presets .p:hover{background:rgba(87,140,255,.12);border-color:#2f4d8f}
  .presets .p.on{background:rgba(87,140,255,.16);border-color:#4a6bb5}
  .presets .p .pn{font-size:12px;font-weight:600}
  .presets .p .pd{font-size:10px;color:var(--dim);margin-top:1px}
  #vchart{width:100%;height:56px;display:block;margin-top:3px}
  .malegend{display:flex;gap:12px;font-size:10.5px;color:var(--dim);margin:6px 0}
  .malegend i{display:inline-block;width:14px;height:2px;vertical-align:middle;margin-right:4px}
  .kpis{display:flex;gap:12px;flex-wrap:wrap;margin:6px 0}
  .kpi{font-size:11px;color:var(--dim)}.kpi b{display:block;font-size:13.5px;color:var(--ink)}
  .kpi.high b{color:var(--high)}.kpi.trough b{color:var(--trough)}.kpi.newhigh b{color:var(--newhigh)}
  .scoreparts{display:flex;flex-wrap:wrap;gap:5px;align-items:center;margin:2px 0 6px;font-size:10.5px;color:var(--dim)}
  .scoreparts .sp{padding:2px 7px;border-radius:6px;font-weight:600}
  .scoreparts .sp.pos{background:rgba(57,217,138,.14);color:var(--pass)}
  .scoreparts .sp.neg{background:rgba(255,92,122,.14);color:var(--fail)}
  .listtoggle{margin:8px 0 4px;cursor:pointer;color:#8fb2ff;font-size:12px}
  table.daily{width:100%;border-collapse:collapse;font-size:11px;display:none}
  table.daily.show{display:table}
  table.daily th,table.daily td{padding:4px 6px;text-align:right;border-bottom:1px solid #141d30}
  table.daily th{color:var(--dim);position:sticky;top:0;background:var(--panel)}
  table.daily td:first-child,table.daily th:first-child{text-align:left}
  .up{color:var(--pass)}.down{color:var(--fail)}
  .vnote{color:#6f83a8;font-size:10.5px;margin-top:6px}
</style>
</head>
<body>
<div id="app"></div>

<button id="burger" title="Watchlist · filters · saved searches">☰</button>

<div id="topbar">
  <div class="brand">📈 V-Shape <span>screener</span></div>
  <div class="crit">
    <span class="cchip"><i class="num">1</i>Earlier high <b id="crit-weeks">~6 wks ago</b></span>
    <span class="cchip"><i class="num">2</i>Dip <b id="crit-dip">≥10%</b> in between</span>
    <span class="cchip"><i class="num">3</i><b>Fresh new high</b> now</span>
    <span class="cchip">Entry: pullback to <b style="color:var(--ma50)">50-DMA</b> / <b style="color:var(--ma200)">200-DMA</b></span>
  </div>
  <div id="live"><span class="dotlive"></span><span id="live-txt">loading live prices…</span></div>
</div>

<div id="left">
  <div class="scroll">
    <div class="sec">
      <h3>Scan the market</h3>
      <button class="btn scan" id="scan-btn" style="width:100%;padding:10px;font-weight:600">⚡ Scan NASDAQ + DJIA (≥$8)</button>
      <div class="scanrow"><label>Max symbols <b id="v-scanMax">1500</b></label>
        <input type="range" id="scan-max" min="100" max="4200" step="100" value="1500"></div>
      <div id="scan-prog" class="scanprog"></div>
      <div class="results" id="scan-results"></div>
      <button class="btn" id="scan-csv" style="width:100%;margin-top:6px;display:none">⬇ Export matches to CSV</button>
    </div>

    <div class="sec">
      <h3>Preset strategies</h3>
      <div class="presets" id="presets"></div>
    </div>

    <div class="sec">
      <h3>Watchlist · add a stock</h3>
      <div class="addrow">
        <input id="add-tk" placeholder="ticker e.g. AAPL" maxlength="8" />
        <button class="btn" id="add-btn">+ Add</button>
      </div>
      <div class="wl" id="wl"></div>
      <div style="display:flex;gap:6px;margin-top:8px">
        <button class="btn" id="reset-watchlist" style="flex:1">↺ Watchlist</button>
        <button class="btn" id="reset-all" style="flex:1">↺ Reset all</button>
      </div>
    </div>

    <div class="sec">
      <h3>Screen criteria — all adjustable</h3>
      <div class="fld"><label>New high was <b id="v-weeksAgo">6 wks</b> ago</label>
        <input type="range" id="f-weeksAgo" min="1" max="16" step="1" value="6"></div>
      <div class="fld"><label>± window <b id="v-weeksTol">3 wks</b></label>
        <input type="range" id="f-weeksTol" min="1" max="8" step="1" value="3"></div>
      <div class="fld"><label>Min dip from high <b id="v-minDip">10%</b></label>
        <input type="range" id="f-minDip" min="0" max="40" step="1" value="10"></div>
      <div class="fld"><label>Max % below its recent high <b id="v-maxFromHigh">60%</b></label>
        <input type="range" id="f-maxFromHigh" min="0" max="80" step="1" value="60"></div>
      <div class="fld"><label>Tradeable price floor <b id="v-minPrice">$8</b></label>
        <input type="range" id="f-minPrice" min="0" max="500" step="1" value="8"></div>
      <label class="chk"><input type="checkbox" id="f-reqNewHigh" checked> Require a fresh new high</label>
      <label class="chk"><input type="checkbox" id="f-dipTo50"> Dipped to the 50-DMA</label>
      <label class="chk"><input type="checkbox" id="f-dipTo200"> Dipped to the 200-DMA</label>
      <label class="chk"><input type="checkbox" id="f-below50"> Currently below 50-DMA</label>
      <label class="chk"><input type="checkbox" id="f-below200"> Currently below 200-DMA</label>
      <div class="matchline" id="matchline"></div>
      <button class="btn" id="reset-filters">Reset filters</button>
      <button class="btn go" id="go-scan">▶ GO — scan with these filters</button>
    </div>

    <div class="sec">
      <h3>Cheap far-dated options</h3>
      <div class="fld"><label>Premium range <b id="v-optRange">$0.05–$0.15</b></label>
        <input type="range" id="f-optMin" min="0.01" max="0.30" step="0.01" value="0.05">
        <input type="range" id="f-optMax" min="0.05" max="1.00" step="0.01" value="0.15"></div>
      <div class="fld"><label>Min months to expiry <b id="v-optMonths">3 mo</b></label>
        <input type="range" id="f-optMonths" min="1" max="24" step="1" value="3"></div>
      <div class="fld"><label>Type</label>
        <div class="segs" id="seg-optType">
          <button data-v="both" class="on">Both</button><button data-v="calls">Calls</button><button data-v="puts">Puts</button></div></div>
      <div style="font-size:10.5px;color:#6f83a8">Applied to the selected stock's chain (CBOE). Deep-OTM lottery tickets — pennies risked for convex upside.</div>
    </div>

    <div class="sec">
      <h3>Saved searches</h3>
      <div class="ssrow"><input id="ss-name" placeholder="name this screen…" maxlength="28">
        <button class="btn" id="ss-save">Save</button></div>
      <div class="saved" id="saved"></div>
    </div>
  </div>
</div>

<div id="ctl" class="glass">
  <button class="btn on" id="spin">⟳ Auto-rotate</button>
  <button class="btn" id="reset">⤢ Reset view</button>
  <button class="btn" id="volnorm" title="volume-thickness scale: per-stock (each ribbon vs its own peak) or comparable (all ribbons on one scale)">⬍ Vol: per-stock</button>
  <button class="btn" id="refresh">↻ Refresh prices</button>
</div>
<div id="hint">drag to orbit · scroll to zoom · click a layer for its chart · ☰ to screen</div>

<div id="legend3d" class="glass">
  <div class="lg-title">3D key <span id="lg-toggle">▸</span></div>
  <div id="lg-body" class="hidden">
    <div><b>height</b> = price · <b>thicker + brighter</b> = more volume</div>
    <div><i class="sw" style="background:var(--pass)"></i>PASS &nbsp;<i class="sw" style="background:var(--fail)"></i>fails screen</div>
    <div><span class="glowdot"></span> highest-volume day</div>
    <div><i class="sw" style="background:var(--high)"></i>high &nbsp;<i class="sw" style="background:var(--trough)"></i>trough &nbsp;<i class="sw" style="background:var(--newhigh)"></i>recent high</div>
    <div><i class="ln" style="background:var(--ma50)"></i>50-DMA &nbsp;<i class="ln" style="background:var(--ma200)"></i>200-DMA</div>
  </div>
</div>

<div id="detail" class="glass">
  <div class="dh"><b id="d-tk"></b><span class="x" id="d-x">✕</span></div>
  <div class="sub" id="d-sub"></div>
  <div class="zone" id="d-zone"></div>
  <div id="d-why" style="font-size:12px;color:var(--dim);margin-bottom:8px"></div>
  <div class="tfrow" id="d-tf"></div>
  <canvas id="chart"></canvas>
  <div class="malegend">
    <span><i style="background:var(--ma50)"></i>50-DMA</span>
    <span><i style="background:var(--ma200)"></i>200-DMA</span>
    <span><i style="background:var(--high)"></i>high</span>
    <span><i style="background:var(--trough)"></i>trough</span>
    <span><i style="background:var(--newhigh)"></i>recent high</span>
    <span id="vol-layer-toggle" style="cursor:pointer;margin-left:auto;color:#5c8fd6;font-weight:600" title="toggle the volume-by-price layer">◧ Vol layer</span>
  </div>
  <canvas id="vchart"></canvas>
  <div class="brushwrap"><canvas id="d-brush"></canvas>
    <div class="brushhint" id="d-brushhint">↔ drag across the strip to select a custom range · click a timeframe to reset</div></div>
  <div class="kpis" id="d-kpis"></div>
  <div class="scoreparts" id="d-scoreparts"></div>
  <div class="listtoggle" id="d-listtoggle">▸ Daily list (entry analysis)</div>
  <table class="daily" id="d-daily"></table>
  <div class="listtoggle" id="d-opttoggle" style="color:#2ee6c8">▸ Cheap far-dated options (puts & calls)</div>
  <div id="d-opts"></div>
  <div class="listtoggle" id="d-bttoggle" style="color:#ffd54a">▸ Score backtest — does the buy-score actually work?</div>
  <div id="d-backtest"></div>
  <div class="vnote" id="d-vnote"></div>
</div>

<script>
/* creds-in-URL fetch guard (fleet pattern): if the page is opened as
   http://user:pass@host/ the baseURI carries credentials and relative
   fetch() throws. Resolve root-relative URLs against the clean origin. */
(function () {
  var of = window.fetch.bind(window);
  window.fetch = function (input, init) {
    try {
      if (typeof input === 'string' && input.charAt(0) === '/') input = location.origin + input;
      else if (input && typeof input.url === 'string' && input.url.charAt(0) === '/')
        input = new Request(location.origin + input.url, input);
    } catch (e) {}
    return of(input, init);
  };
})();
</script>
<script src="screen.js"></script>
<script src="data.js"></script>
<script type="importmap">
{ "imports": {
  "three": "/vendor/three/three.module.js",
  "three/addons/": "/vendor/three/addons/"
}}
</script>
<script type="module" src="app.js"></script>
</body>
</html>