[object Object]

← back to Stock Vshape Viewer

CSV export: download scan matches (ticker/name/score/price/drawdown/RSI/MA-dist/zone) as a dated CSV; button appears after a scan

5ca5574396f3439740e20aef7a7cb31cde5cbe4b · 2026-08-28 00:21:32 -0700 · Steve

Files touched

Diff

commit 5ca5574396f3439740e20aef7a7cb31cde5cbe4b
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Aug 28 00:21:32 2026 -0700

    CSV export: download scan matches (ticker/name/score/price/drawdown/RSI/MA-dist/zone) as a dated CSV; button appears after a scan
---
 public/app.js     | 14 ++++++++++++++
 public/index.html |  1 +
 2 files changed, 15 insertions(+)

diff --git a/public/app.js b/public/app.js
index 5d51940..2bd971f 100644
--- a/public/app.js
+++ b/public/app.js
@@ -588,6 +588,8 @@ function pollScan(job) {
       if (j.done) {
         const btn = document.getElementById('scan-btn'); btn.disabled = false; btn.textContent = '⚡ Scan NASDAQ + DJIA (≥$8)';
         const N = 12;
+        state.lastMatches = j.matches || [];
+        document.getElementById('scan-csv').style.display = state.lastMatches.length ? 'block' : 'none';
         await autoLoadMatches(j.matches || [], N); // wait until the board actually has them
         document.getElementById('scan-prog').innerHTML += `<div style="margin-top:4px;color:#6f83a8">done in ${(j.elapsedMs/1000).toFixed(0)}s · loaded top ${Math.min(N, j.matchCount)} into the board · ◈ = cheap far-dated options · click any to add more</div>`;
         enrichResultsOptions();
@@ -630,6 +632,17 @@ async function enrichResultsOptions() {
   }
   await Promise.all(Array.from({ length: 6 }, worker));
 }
+// export the current scan matches to CSV
+function exportCSV() {
+  const m = state.lastMatches || []; if (!m.length) return;
+  const cols = ['ticker', 'name', 'buyScore', 'price', 'drawdown', 'weeksAgoActual', 'rsi', 'volSurge', 'd50', 'd200', 'zoneLabel'];
+  const esc = v => `"${String(v == null ? '' : v).replace(/"/g, '""')}"`;
+  const csv = [cols.join(',')].concat(m.map(r => cols.map(c => esc(r[c])).join(','))).join('\n');
+  const a = document.createElement('a');
+  a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
+  a.download = `vshape-scan-${new Date().toISOString().slice(0, 10)}.csv`;
+  a.click(); URL.revokeObjectURL(a.href);
+}
 // push a scan's top-ranked winners straight onto the 3D board
 async function autoLoadMatches(matches, N) {
   const top = matches.slice(0, N).map(m => m.ticker);
@@ -699,6 +712,7 @@ function renderBacktest(j) {
 function wireScanAndOptions() {
   document.getElementById('scan-btn').onclick = startScan;
   document.getElementById('go-scan').onclick = startScan;
+  document.getElementById('scan-csv').onclick = exportCSV;
   const sm = document.getElementById('scan-max');
   sm.oninput = () => { scanMax = +sm.value; document.getElementById('v-scanMax').textContent = sm.value; };
   // options criteria
diff --git a/public/index.html b/public/index.html
index aef9f70..e5797e6 100644
--- a/public/index.html
+++ b/public/index.html
@@ -181,6 +181,7 @@
         <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">

← ef0e646 Score transparency: expose scoreParts from the engine + rend  ·  back to Stock Vshape Viewer  ·  Evidence-driven sweet-spot flag: mark scores in the backtest d1e5b8a →