AndyKandy26 commited on
Commit
c3743f6
Β·
verified Β·
1 Parent(s): cec1030

Upload picks.html

Browse files
Files changed (1) hide show
  1. picks.html +177 -61
picks.html CHANGED
@@ -219,6 +219,20 @@
219
  .stat-grid{grid-template-columns:1fr}
220
  .tab-bar{width:100%;overflow-x:auto}
221
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  </style>
223
  </head>
224
  <body>
@@ -244,7 +258,7 @@
244
  <div class="page-header">
245
  <div>
246
  <div class="page-title">Stock &amp; <span>Option Picks</span></div>
247
- <div class="page-subtitle">Live prices via Yahoo Finance Β· Python backend (app.py) Β· 15-min delayed Β· Options data estimated</div>
248
  </div>
249
  <div class="header-actions">
250
  <div class="search-box">
@@ -265,7 +279,7 @@
265
  <span class="last-updated" id="lastUpdatedLabel">Not yet fetched β€” press "Fetch Live Data" to load Yahoo Finance prices</span>
266
  </div>
267
  <div style="font-size:.72rem;color:var(--text2)">
268
- Source: Yahoo Finance via yfinance (Python) Β· Same-origin /api/quotes Β· Options: estimated Β· 10-min cooldown per tab
269
  </div>
270
  </div>
271
 
@@ -380,7 +394,7 @@
380
 
381
  <!-- Option Selling -->
382
  <div class="sub-panel active" id="sub-optSell">
383
- <div class="opt-data-note">⚠️ <strong>Prices:</strong> live from Yahoo Finance. <strong>Premium / IV Rank:</strong> estimated from seed data scaled to live price β€” not real option chain data. Tradier API upgrade available on request.</div>
384
  <div class="filter-panel">
385
  <div class="filter-header" onclick="toggleFilter(this)">
386
  <div class="filter-title">βš™οΈ Option Selling Filters</div>
@@ -610,17 +624,28 @@
610
  /* ─────────────────────────────────────────────
611
  CONSTANTS & CACHE CONFIG
612
  ───────────────────────────────────────────── */
613
- const COOLDOWN_MS = 10 * 60 * 1000; // 10 minutes per tab
614
- const CACHE_TTL_MS = 15 * 60 * 1000; // 15-minute data freshness
615
- const SPARK_DELAY = 500; // ms between sparkline fetches
616
-
617
- // ── Local backend endpoints (served by app.py in the same HF Space) ──────────
618
- // No CORS proxies needed β€” the browser calls our own FastAPI server directly.
619
- const API_BASE = '/api'; // same origin β†’ zero CORS issues
620
- const API_QUOTE = `${API_BASE}/quotes?symbols=`;
621
- const API_CHART = `${API_BASE}/chart?symbol=`;
622
-
623
- let lastProxyLabel = 'local backend';
 
 
 
 
 
 
 
 
 
 
 
624
 
625
  // Active tab / sub-tab tracking
626
  let activeTab = 'stocks';
@@ -759,42 +784,74 @@ function remainingCooldown(tab) {
759
  function canRefresh(tab) { return remainingCooldown(tab) === 0; }
760
 
761
  /* ─────────────────────────────────────────────
762
- YAHOO FINANCE via LOCAL BACKEND (app.py)
763
- No CORS proxies β€” same-origin calls to /api/quotes and /api/chart
 
 
764
  ───────────────────────────────────────────── */
765
- async function fetchQuotes(symbols) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
766
  try {
767
- const res = await fetch(API_QUOTE + symbols.join(','), {
768
- signal: AbortSignal.timeout(15000),
769
- });
770
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
771
- const json = await res.json(); // { NVDA: {price, chg1d, vol}, ... }
772
- // Normalise to same shape the rest of the code expects
773
- const map = {};
774
- for (const [sym, q] of Object.entries(json)) {
775
- map[sym] = { price: q.price ?? null, chg1d: q.chg1d ?? null, vol: q.vol ?? null };
776
- }
777
- return map;
 
778
  } catch (e) {
779
- console.warn('[FinWise] /api/quotes failed:', e.message);
780
  return null;
781
  }
782
  }
783
 
 
 
 
 
 
784
  async function fetchSparkline(ticker) {
785
  if (sparkCache[ticker]) return sparkCache[ticker];
 
 
786
  try {
787
- const res = await fetch(`${API_CHART}${ticker}&days=8`, {
788
- signal: AbortSignal.timeout(12000),
789
- });
 
790
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
791
- const json = await res.json(); // { symbol, closes: [...] }
792
- const closes = (json.closes || []).filter(v => v != null).slice(-7);
 
 
793
  if (closes.length < 2) return null;
794
  sparkCache[ticker] = closes;
795
  return closes;
796
  } catch (e) {
797
- console.warn(`[FinWise] /api/chart ${ticker} failed:`, e.message);
798
  return null;
799
  }
800
  }
@@ -918,15 +975,16 @@ function getVisibleTickers() {
918
  USER-TRIGGERED REFRESH (button click)
919
  ───────────────────────────────────────────── */
920
  async function userRefresh() {
921
- const tab = getVisibleDataKey();
922
- const rem = remainingCooldown(tab);
923
-
924
- if (rem > 0) {
925
- // Already in cooldown β€” don't fetch, just show warning
926
- flashCooldownWarning(rem);
927
  return;
928
  }
929
 
 
 
 
 
930
  setCooldown(tab);
931
  startCooldownDisplay(tab);
932
  await fetchForActiveTab();
@@ -969,22 +1027,29 @@ function flashCooldownWarning(rem) {
969
  STATUS BAR
970
  ───────────────────────────────────────────── */
971
  function updateStatusBar(state, tab) {
972
- const badge = document.getElementById('dataSourceBadge');
973
- const label = document.getElementById('lastUpdatedLabel');
974
- const ts = localStorage.getItem(cacheTimeKey(tab));
975
- const ago = ts ? Math.round((Date.now() - parseInt(ts)) / 60000) : null;
976
 
977
  const states = {
978
- loading: { cls:'seed', icon:'⏳', text:'Fetching live prices from Yahoo Finance via local backend…' },
979
- live: { cls:'live', icon:'🟒', text:`Yahoo Finance · ${ago === 0 ? 'just now' : ago + ' min ago'} · 15-min market delay` },
980
- cached: { cls:'cached', icon:'πŸ“¦', text:`Cached Β· fetched ${ago} min ago Β· press Fetch to refresh` },
981
- error: { cls:'error', icon:'❌', text:'Backend fetch failed. Check that app.py is running and yfinance is installed (see requirements.txt).' },
982
- seed: { cls:'seed', icon:'πŸ“¦', text:'Seed data shown β€” press "Fetch Live Data" to load Yahoo Finance prices via backend' },
 
983
  };
984
  const s = states[state] || states.seed;
985
- badge.className = 'data-badge ' + s.cls;
986
- badge.textContent = `${s.icon} ${state === 'live' ? 'Live (15-min delay)' : state === 'cached' ? 'Cached' : state === 'error' ? 'Backend Error' : state === 'loading' ? 'Loading…' : 'Seed Data'}`;
987
- label.textContent = s.text;
 
 
 
 
 
 
988
  }
989
 
990
  /* ─────────────────────────────────────────────
@@ -1399,29 +1464,80 @@ function updateStats() {
1399
  }
1400
 
1401
  /* ─────────────────────────────────────────────
1402
- INIT
1403
  ───────────────────────────────────────────── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1404
  document.addEventListener('DOMContentLoaded', () => {
1405
- // Render with seed data immediately
1406
- renderStocks();
1407
- renderOptSell();
1408
- renderLeaps();
1409
- renderETFHolds();
1410
- renderETFOpts();
1411
  updateStats();
1412
 
1413
- // Restore cooldown display if tab is still in cooldown
1414
  const tab = getVisibleDataKey();
1415
  if (remainingCooldown(tab) > 0) startCooldownDisplay(tab);
1416
 
1417
- // Show cached status if we already have data
1418
  const cached = getCache(tab);
1419
  if (cached) {
1420
  mergePrices(tab, cached);
1421
  renderStocks();
1422
  updateStatusBar('cached', tab);
 
 
1423
  }
1424
  });
1425
  </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1426
  </body>
1427
  </html>
 
219
  .stat-grid{grid-template-columns:1fr}
220
  .tab-bar{width:100%;overflow-x:auto}
221
  }
222
+ /* ── API Key Modal ── */
223
+ .modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:1000;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px)}
224
+ .modal-backdrop.hidden{display:none}
225
+ .modal-box{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:2rem;width:min(460px,92vw);box-shadow:0 24px 64px rgba(0,0,0,.5)}
226
+ .modal-title{font-size:1.1rem;font-weight:800;margin-bottom:.4rem}
227
+ .modal-title span{color:var(--cyan)}
228
+ .modal-sub{font-size:.82rem;color:var(--text2);margin-bottom:1.25rem;line-height:1.65}
229
+ .modal-sub a{color:var(--cyan);text-decoration:none}
230
+ .modal-sub a:hover{text-decoration:underline}
231
+ .modal-input{width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:8px;color:var(--text);padding:.65rem .85rem;font-size:.88rem;font-family:monospace;outline:none;transition:border-color .15s}
232
+ .modal-input:focus{border-color:var(--cyan)}
233
+ .modal-actions{display:flex;gap:.75rem;margin-top:1rem;flex-wrap:wrap}
234
+ .modal-note{font-size:.72rem;color:var(--text2);margin-top:.85rem;line-height:1.55;padding:.65rem .75rem;background:var(--bg3);border-radius:8px}
235
+ .modal-note strong{color:var(--emerald)}
236
  </style>
237
  </head>
238
  <body>
 
258
  <div class="page-header">
259
  <div>
260
  <div class="page-title">Stock &amp; <span>Option Picks</span></div>
261
+ <div class="page-subtitle">Real-time prices via Finnhub Β· Free API key Β· No backend needed Β· Static HF Space compatible</div>
262
  </div>
263
  <div class="header-actions">
264
  <div class="search-box">
 
279
  <span class="last-updated" id="lastUpdatedLabel">Not yet fetched β€” press "Fetch Live Data" to load Yahoo Finance prices</span>
280
  </div>
281
  <div style="font-size:.72rem;color:var(--text2)">
282
+ Source: Finnhub.io (free tier) Β· 60 calls/min Β· Real-time US quotes Β· <span style="cursor:pointer;color:var(--cyan)" onclick="showKeyModal()">πŸ”‘ Manage API key</span>
283
  </div>
284
  </div>
285
 
 
394
 
395
  <!-- Option Selling -->
396
  <div class="sub-panel active" id="sub-optSell">
397
+ <div class="opt-data-note">⚠️ <strong>Prices:</strong> live from Finnhub. <strong>Premium / IV Rank:</strong> estimated from seed data scaled to live price β€” not real option chain data. Tradier API upgrade available on request.</div>
398
  <div class="filter-panel">
399
  <div class="filter-header" onclick="toggleFilter(this)">
400
  <div class="filter-title">βš™οΈ Option Selling Filters</div>
 
624
  /* ─────────────────────────────────────────────
625
  CONSTANTS & CACHE CONFIG
626
  ───────────────────────────────────────────── */
627
+ const COOLDOWN_MS = 10 * 60 * 1000; // 10 min cooldown per tab
628
+ const CACHE_TTL_MS = 15 * 60 * 1000; // 15 min cache freshness
629
+ const SPARK_DELAY = 400; // ms between sparkline requests
630
+
631
+ // ── Finnhub API (free tier, CORS-enabled, no backend needed) ─────────────────
632
+ // Free key: https://finnhub.io/register (takes 30 seconds, no credit card)
633
+ // Free tier: 60 calls/min Β· real-time US quotes Β· works from browser
634
+ const FH_BASE = 'https://finnhub.io/api/v1';
635
+ const FH_QUOTE = `${FH_BASE}/quote`; // ?symbol=NVDA&token=KEY
636
+ const FH_CANDLE = `${FH_BASE}/stock/candle`; // sparkline
637
+
638
+ // ── API key management ────────────────────────────────────────────────────────
639
+ // Key is stored in localStorage so user only enters it once.
640
+ const KEY_STORAGE = 'fw_finnhub_key';
641
+ let FINNHUB_KEY = localStorage.getItem(KEY_STORAGE) || '';
642
+
643
+ function getKey() { return FINNHUB_KEY; }
644
+ function saveKey(k) {
645
+ FINNHUB_KEY = k.trim();
646
+ localStorage.setItem(KEY_STORAGE, FINNHUB_KEY);
647
+ }
648
+ function hasKey() { return FINNHUB_KEY.length > 0; }
649
 
650
  // Active tab / sub-tab tracking
651
  let activeTab = 'stocks';
 
784
  function canRefresh(tab) { return remainingCooldown(tab) === 0; }
785
 
786
  /* ─────────────────────────────────────────────
787
+ FINNHUB FETCH HELPERS
788
+ β€’ fetchQuotes: parallel with concurrency cap of 5
789
+ β€’ fetchSparkline: sequential with SPARK_DELAY gap
790
+ β€’ Both return null on error (falls back to seed data)
791
  ───────────────────────────────────────────── */
792
+
793
+ // Concurrency-limited parallel fetch β€” avoids hammering 60/min limit
794
+ async function fetchAllQuotes(symbols) {
795
+ const key = getKey();
796
+ if (!key) return null;
797
+
798
+ const CONCURRENCY = 5;
799
+ const results = {};
800
+ const chunks = [];
801
+ for (let i = 0; i < symbols.length; i += CONCURRENCY)
802
+ chunks.push(symbols.slice(i, i + CONCURRENCY));
803
+
804
+ for (const chunk of chunks) {
805
+ const batch = await Promise.all(chunk.map(sym => fetchOneQuote(sym, key)));
806
+ batch.forEach((q, i) => { if (q) results[chunk[i]] = q; });
807
+ if (chunks.indexOf(chunk) < chunks.length - 1) await sleep(220); // ~5/220ms = safe under 60/min
808
+ }
809
+ return Object.keys(results).length ? results : null;
810
+ }
811
+
812
+ async function fetchOneQuote(symbol, key) {
813
  try {
814
+ const url = `${FH_QUOTE}?symbol=${encodeURIComponent(symbol)}&token=${key}`;
815
+ const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
 
816
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
817
+ const j = await res.json();
818
+ // Finnhub quote: { c: current, d: change, dp: %change, h: high, l: low, pc: prev close }
819
+ if (!j.c || j.c === 0) return null; // 0 = no data (market closed or invalid ticker)
820
+ return {
821
+ price: parseFloat(j.c.toFixed(2)),
822
+ chg1d: parseFloat((j.dp || 0).toFixed(2)),
823
+ vol: null, // not in Finnhub basic quote β€” volume via candles if needed
824
+ };
825
  } catch (e) {
826
+ console.warn(`[FinWise] Finnhub quote failed (${symbol}):`, e.message);
827
  return null;
828
  }
829
  }
830
 
831
+ // Alias used by existing orchestration code
832
+ async function fetchQuotes(symbols) {
833
+ return await fetchAllQuotes(symbols);
834
+ }
835
+
836
  async function fetchSparkline(ticker) {
837
  if (sparkCache[ticker]) return sparkCache[ticker];
838
+ const key = getKey();
839
+ if (!key) return null;
840
  try {
841
+ const to = Math.floor(Date.now() / 1000);
842
+ const from = to - 8 * 24 * 60 * 60; // 8 calendar days back β†’ ~5-6 trading days
843
+ const url = `${FH_CANDLE}?symbol=${ticker}&resolution=D&from=${from}&to=${to}&token=${key}`;
844
+ const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
845
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
846
+ const j = await res.json();
847
+ // Finnhub candle: { c: [closes], s: "ok" }
848
+ if (j.s !== 'ok' || !j.c?.length) return null;
849
+ const closes = j.c.filter(v => v != null).slice(-7);
850
  if (closes.length < 2) return null;
851
  sparkCache[ticker] = closes;
852
  return closes;
853
  } catch (e) {
854
+ console.warn(`[FinWise] Finnhub candle failed (${ticker}):`, e.message);
855
  return null;
856
  }
857
  }
 
975
  USER-TRIGGERED REFRESH (button click)
976
  ───────────────────────────────────────────── */
977
  async function userRefresh() {
978
+ // Prompt for API key if not set
979
+ if (!hasKey()) {
980
+ showKeyModal();
 
 
 
981
  return;
982
  }
983
 
984
+ const tab = getVisibleDataKey();
985
+ const rem = remainingCooldown(tab);
986
+ if (rem > 0) { flashCooldownWarning(rem); return; }
987
+
988
  setCooldown(tab);
989
  startCooldownDisplay(tab);
990
  await fetchForActiveTab();
 
1027
  STATUS BAR
1028
  ───────────────────────────────────────────── */
1029
  function updateStatusBar(state, tab) {
1030
+ const badge = document.getElementById('dataSourceBadge');
1031
+ const label = document.getElementById('lastUpdatedLabel');
1032
+ const ts = localStorage.getItem(cacheTimeKey(tab));
1033
+ const ago = ts ? Math.round((Date.now() - parseInt(ts)) / 60000) : null;
1034
 
1035
  const states = {
1036
+ nokey: { cls:'seed', icon:'πŸ”‘', text:'No API key set β€” click "Fetch Live Data" to enter your free Finnhub key (finnhub.io)' },
1037
+ loading: { cls:'seed', icon:'⏳', text:'Fetching live quotes from Finnhub…' },
1038
+ live: { cls:'live', icon:'🟒', text:`Finnhub · ${ago === 0 ? 'just now' : ago + ' min ago'} · real-time US quotes` },
1039
+ cached: { cls:'cached', icon:'πŸ“¦', text:`Cached Β· ${ago} min ago Β· press Fetch to refresh` },
1040
+ error: { cls:'error', icon:'❌', text:'Finnhub fetch failed. Check your API key is valid at finnhub.io β€” or try again in a moment.' },
1041
+ seed: { cls:'seed', icon:'πŸ“¦', text:'Seed data shown β€” press "Fetch Live Data" and enter your free Finnhub key' },
1042
  };
1043
  const s = states[state] || states.seed;
1044
+ badge.className = 'data-badge ' + s.cls;
1045
+ badge.textContent = `${s.icon} ${
1046
+ state==='live' ? 'Live (Finnhub)' :
1047
+ state==='cached' ? 'Cached' :
1048
+ state==='error' ? 'API Error' :
1049
+ state==='nokey' ? 'No API Key' :
1050
+ state==='loading' ? 'Loading…' : 'Seed Data'
1051
+ }`;
1052
+ label.textContent = s.text;
1053
  }
1054
 
1055
  /* ─────────────────────────────────────────────
 
1464
  }
1465
 
1466
  /* ─────────────────────────────────────────────
1467
+ API KEY MODAL
1468
  ───────────────────────────────────────────── */
1469
+ function showKeyModal() {
1470
+ document.getElementById('keyModal').classList.remove('hidden');
1471
+ document.getElementById('keyInput').value = FINNHUB_KEY || '';
1472
+ document.getElementById('keyInput').focus();
1473
+ }
1474
+ function hideKeyModal() {
1475
+ document.getElementById('keyModal').classList.add('hidden');
1476
+ }
1477
+ async function confirmKey() {
1478
+ const k = document.getElementById('keyInput').value.trim();
1479
+ if (!k) { document.getElementById('keyInput').style.borderColor='var(--rose)'; return; }
1480
+ saveKey(k);
1481
+ hideKeyModal();
1482
+ document.getElementById('refreshLabel').textContent = '⬇️ Fetch Live Data';
1483
+ // Auto-trigger fetch now that we have a key
1484
+ const tab = getVisibleDataKey();
1485
+ setCooldown(tab);
1486
+ startCooldownDisplay(tab);
1487
+ await fetchForActiveTab();
1488
+ }
1489
+ function clearKey() {
1490
+ saveKey('');
1491
+ document.getElementById('keyInput').value = '';
1492
+ updateStatusBar('nokey', getVisibleDataKey());
1493
+ }
1494
+ // Allow Enter key in the input
1495
+ document.addEventListener('DOMContentLoaded', () => {
1496
+ document.getElementById('keyInput')?.addEventListener('keydown', e => {
1497
+ if (e.key === 'Enter') confirmKey();
1498
+ if (e.key === 'Escape') hideKeyModal();
1499
+ });
1500
+ });
1501
  document.addEventListener('DOMContentLoaded', () => {
1502
+ renderStocks(); renderOptSell(); renderLeaps(); renderETFHolds(); renderETFOpts();
 
 
 
 
 
1503
  updateStats();
1504
 
 
1505
  const tab = getVisibleDataKey();
1506
  if (remainingCooldown(tab) > 0) startCooldownDisplay(tab);
1507
 
 
1508
  const cached = getCache(tab);
1509
  if (cached) {
1510
  mergePrices(tab, cached);
1511
  renderStocks();
1512
  updateStatusBar('cached', tab);
1513
+ } else {
1514
+ updateStatusBar(hasKey() ? 'seed' : 'nokey', tab);
1515
  }
1516
  });
1517
  </script>
1518
+
1519
+ <!-- ═══════════ API KEY MODAL ═══════════ -->
1520
+ <div class="modal-backdrop hidden" id="keyModal" onclick="if(event.target===this)hideKeyModal()">
1521
+ <div class="modal-box">
1522
+ <div class="modal-title">πŸ”‘ Enter your <span>Finnhub API Key</span></div>
1523
+ <div class="modal-sub">
1524
+ FinWise uses <a href="https://finnhub.io/register" target="_blank">Finnhub.io</a> for real-time stock quotes.
1525
+ The free tier gives you <strong>60 calls/minute</strong> with no credit card required.<br><br>
1526
+ <strong>Steps:</strong> 1) Go to <a href="https://finnhub.io/register" target="_blank">finnhub.io/register</a>
1527
+ β†’ 2) Sign up free β†’ 3) Copy your API key from the dashboard β†’ 4) Paste it below.
1528
+ </div>
1529
+ <input class="modal-input" id="keyInput" type="text"
1530
+ placeholder="e.g. d0abc123xyz456..." autocomplete="off" spellcheck="false" />
1531
+ <div class="modal-actions">
1532
+ <button class="btn btn-primary" onclick="confirmKey()">βœ… Save &amp; Fetch Data</button>
1533
+ <button class="btn btn-ghost" onclick="hideKeyModal()">Cancel</button>
1534
+ <button class="btn btn-ghost" onclick="clearKey()" style="margin-left:auto;color:var(--rose);border-color:var(--rose)">πŸ—‘ Clear Key</button>
1535
+ </div>
1536
+ <div class="modal-note">
1537
+ <strong>πŸ”’ Privacy:</strong> Your key is stored only in your browser's localStorage β€” never sent to any server other than Finnhub directly. You can clear it anytime with the button above.
1538
+ </div>
1539
+ </div>
1540
+ </div>
1541
+
1542
  </body>
1543
  </html>