From b19c2027869d361bf9024a6197035cd00078ab63 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Mon, 1 Jun 2026 17:34:33 -0400 Subject: [PATCH] 06/01 Optimize porfolio page --- app/services/investment_service.py | 56 +++++++++++++++++++++++----- app/templates/investments/index.html | 14 +++++-- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/app/services/investment_service.py b/app/services/investment_service.py index fdd9dde..3a04b8a 100644 --- a/app/services/investment_service.py +++ b/app/services/investment_service.py @@ -132,11 +132,17 @@ def _extract_day_change_from_meta(meta): def fetch_day_change(ticker): """ - Lightweight call: fetch today's open price and current price only. - Uses meta.regularMarketOpen / meta.regularMarketPrice from Yahoo Finance. + Fetch today's open-to-current day change for a ticker. + + Strategy (in order): + 1. meta.regularMarketOpen + meta.regularMarketPrice (most accurate) + 2. Last bar open[] + last bar close[] from the OHLC array (fallback) + + Uses range=5d so the API always returns data even on weekends / holidays + when range=1d would return an empty result set. Returns dict: {ticker, open, current, day_change, day_change_pct} - or None on failure. + or None on complete failure. """ if not ticker: return None @@ -145,22 +151,50 @@ def fetch_day_change(ticker): for subdomain in ('query1', 'query2'): url = ( f'https://{subdomain}.finance.yahoo.com/v8/finance/chart/{ticker}' - f'?range=1d&interval=1d&includePrePost=false' + f'?range=5d&interval=1d&includePrePost=false' ) try: resp = requests.get(url, headers=HEADERS, timeout=10) if resp.status_code != 200: + log.warning('[investment] %s day-change: HTTP %s (%s)', ticker, resp.status_code, subdomain) continue - result = resp.json().get('chart', {}).get('result') + + chart_data = resp.json().get('chart', {}) + if chart_data.get('error'): + log.warning('[investment] %s day-change: API error %s', ticker, chart_data['error']) + continue + + result = chart_data.get('result') if not result: - return None + log.warning('[investment] %s day-change: empty result (%s)', ticker, subdomain) + continue meta = result[0].get('meta', {}) - open_p, curr, chg, chg_pct = _extract_day_change_from_meta(meta) - if open_p is None: - return None - log.info('[investment] %s day change: open=%.4f current=%.4f chg=%.4f (%.2f%%)', + # Strategy 1: meta fields (true intraday open → current) + open_p, curr, chg, chg_pct = _extract_day_change_from_meta(meta) + + # Strategy 2: fall back to last OHLC bar open/close + if open_p is None or curr is None: + try: + quote = result[0]['indicators']['quote'][0] + valid_opens = [v for v in quote.get('open', []) if v is not None] + valid_closes = [v for v in quote.get('close', []) if v is not None] + if valid_opens and valid_closes: + open_p = float(valid_opens[-1]) + curr = float(valid_closes[-1]) + chg = round(curr - open_p, 4) + chg_pct = round(chg / open_p * 100, 2) if open_p != 0 else 0 + log.info('[investment] %s day-change: using OHLC fallback', ticker) + except (KeyError, IndexError, TypeError) as exc: + log.warning('[investment] %s day-change: OHLC fallback failed: %s', ticker, exc) + + if open_p is None or curr is None: + log.warning('[investment] %s day-change: no open/current available (meta keys: %s)', + ticker, list(meta.keys())[:10]) + continue + + log.info('[investment] %s day-change: open=%.4f current=%.4f chg=%.4f (%.2f%%)', ticker, open_p, curr, chg, chg_pct) return { 'ticker': ticker, @@ -172,6 +206,8 @@ def fetch_day_change(ticker): except Exception as exc: log.warning('[investment] %s day-change fetch failed (%s): %s', ticker, subdomain, exc) + log.error('[investment] %s: day-change fetch failed on all subdomains', ticker) + return None diff --git a/app/templates/investments/index.html b/app/templates/investments/index.html index d0bafc4..b28e918 100644 --- a/app/templates/investments/index.html +++ b/app/templates/investments/index.html @@ -331,13 +331,19 @@ if (data && data.day_change !== undefined) { const badge = cell.querySelector('.chg-badge') || cell; badge.className = 'chg-badge ' + chgClass(data.day_change); - badge.title = `Open: ${fmtPrice(data.open)} → Current: ${fmtPrice(data.current)}`; + badge.title = `Today: open ${fmtPrice(data.open)} → current ${fmtPrice(data.current)}`; + const sign = data.day_change >= 0 ? '+' : ''; badge.innerHTML = `` + - (data.day_change >= 0 ? '+' : '') + - parseFloat(data.day_change_pct).toFixed(2) + '%'; + `${sign}${parseFloat(data.day_change_pct).toFixed(2)}%` + + `${sign}${fmtPrice(data.day_change)}`; } else { - cell.innerHTML = ''; + const badge = cell.querySelector('.chg-badge'); + if (badge) { + badge.className = 'chg-badge chg-flat'; + badge.title = 'Price data unavailable — check System Logs'; + badge.textContent = '—'; + } } }); });