05/31 Fix yfinance issue

This commit is contained in:
2026-05-31 21:57:44 -04:00
parent 04dc35388a
commit 9bc8bb3238
3 changed files with 115 additions and 57 deletions
+17 -8
View File
@@ -77,17 +77,26 @@ def force_refresh():
# ── Fetchers ────────────────────────────────────────────────────────────────── # ── Fetchers ──────────────────────────────────────────────────────────────────
def _fetch_yfinance(): def _fetch_yfinance():
"""Fetch USDVND=X via yfinance. Most reliable — uses Yahoo Finance forex.""" """Fetch USDVND=X via Yahoo Finance v8 chart API (direct HTTP, no yfinance needed)."""
for subdomain in ('query1', 'query2'):
url = (
f'https://{subdomain}.finance.yahoo.com/v8/finance/chart/USDVND=X'
'?range=5d&interval=1d&includePrePost=false'
)
try: try:
import yfinance as yf resp = requests.get(url, timeout=REQUEST_TIMEOUT,
ticker = yf.Ticker('USDVND=X') headers={'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json'})
hist = ticker.history(period='5d') if resp.status_code == 200:
if not hist.empty: result = resp.json().get('chart', {}).get('result')
rate = float(hist['Close'].iloc[-1]) if result:
log.info(f'[fx] yfinance: 1 USD = {rate:,.0f} VND') closes = result[0]['indicators']['quote'][0]['close']
valid = [c for c in closes if c is not None]
if valid:
rate = float(valid[-1])
log.info(f'[fx] Yahoo v8: 1 USD = {rate:,.0f} VND')
return rate return rate
except Exception as e: except Exception as e:
log.warning(f'[fx] yfinance failed: {e}') log.warning(f'[fx] Yahoo v8 {subdomain}: {e}')
return None return None
+82 -33
View File
@@ -1,31 +1,104 @@
""" """
Investment Service — price fetching via yfinance, portfolio calculations. Investment Service — price fetching and portfolio calculations.
Yahoo Finance v8 chart API (direct HTTP, no yfinance dependency).
Endpoint: https://query1.finance.yahoo.com/v8/finance/chart/{ticker}?range=2d&interval=1d
No API key needed. Uses a browser User-Agent header.
Falls back to query2 subdomain if query1 fails.
""" """
import logging import logging
import requests
from datetime import datetime from datetime import datetime
from app.extensions import db from app.extensions import db
from app.models.investment import Investment from app.models.investment import Investment
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
REQUEST_TIMEOUT = 10
HEADERS = {
'User-Agent': (
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/120.0.0.0 Safari/537.36'
),
'Accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.9',
}
# Consistent colors per asset type
ASSET_COLORS = {
'stock': '#3b82f6',
'etf': '#06b6d4',
'crypto': '#f59e0b',
'real_estate': '#10b981',
'bond': '#8b5cf6',
'cash': '#64748b',
'other': '#ec4899',
}
ASSET_TYPE_LABELS = {
'stock': 'Stock',
'etf': 'ETF',
'crypto': 'Crypto',
'real_estate': 'Real Estate',
'bond': 'Bond',
'cash': 'Cash',
'other': 'Other',
}
def fetch_price(ticker): def fetch_price(ticker):
""" """
Fetch latest price for a ticker via yfinance. Fetch latest closing price for a ticker via Yahoo Finance v8 chart API.
Tries query1 then query2 subdomain as fallback.
Returns float or None on failure. Returns float or None on failure.
""" """
if not ticker: if not ticker:
return None return None
ticker = ticker.upper().strip()
for subdomain in ('query1', 'query2'):
url = (
f'https://{subdomain}.finance.yahoo.com/v8/finance/chart/{ticker}'
f'?range=5d&interval=1d&includePrePost=false'
)
try: try:
import yfinance as yf resp = requests.get(url, headers=HEADERS, timeout=REQUEST_TIMEOUT)
t = yf.Ticker(ticker.upper()) if resp.status_code == 200:
hist = t.history(period='2d') price = _parse_v8_price(resp.json())
if hist.empty: if price is not None:
log.info(f'[investment] {ticker}: {price:.4f} via {subdomain}')
return price
elif resp.status_code == 404:
log.warning(f'[investment] {ticker}: not found on Yahoo Finance')
return None return None
return float(hist['Close'].iloc[-1]) else:
log.warning(f'[investment] {ticker} {subdomain}: HTTP {resp.status_code}')
except requests.exceptions.Timeout:
log.warning(f'[investment] {ticker} {subdomain}: timeout')
except Exception as e: except Exception as e:
log.warning(f'[investment] price fetch failed for {ticker}: {e}') log.warning(f'[investment] {ticker} {subdomain}: {e}')
log.error(f'[investment] {ticker}: all sources failed')
return None
def _parse_v8_price(data):
"""Extract the most recent closing price from a v8 chart API response."""
try:
result = data['chart']['result']
if not result:
return None
closes = result[0]['indicators']['quote'][0]['close']
# Filter out None values (market closed / missing data)
valid = [c for c in closes if c is not None]
if not valid:
return None
return float(valid[-1])
except (KeyError, IndexError, TypeError) as e:
log.warning(f'[investment] v8 parse error: {e}')
return None return None
@@ -63,9 +136,7 @@ def update_prices(investment_ids=None):
def get_portfolio_summary(): def get_portfolio_summary():
""" """Return portfolio-level aggregates across all active investments."""
Return portfolio-level aggregates across all active investments.
"""
investments = Investment.query.filter_by(is_active=True).all() investments = Investment.query.filter_by(is_active=True).all()
total_cost = sum(i.total_cost for i in investments) total_cost = sum(i.total_cost for i in investments)
@@ -98,25 +169,3 @@ def get_portfolio_summary():
'allocation': allocation, 'allocation': allocation,
'count': len(investments), 'count': len(investments),
} }
# Consistent colors per asset type
ASSET_COLORS = {
'stock': '#3b82f6',
'etf': '#06b6d4',
'crypto': '#f59e0b',
'real_estate': '#10b981',
'bond': '#8b5cf6',
'cash': '#64748b',
'other': '#ec4899',
}
ASSET_TYPE_LABELS = {
'stock': 'Stock',
'etf': 'ETF',
'crypto': 'Crypto',
'real_estate': 'Real Estate',
'bond': 'Bond',
'cash': 'Cash',
'other': 'Other',
}
+1 -1
View File
@@ -7,7 +7,7 @@ pymysql==1.1.1
python-dotenv==1.0.1 python-dotenv==1.0.1
gunicorn==23.0.0 gunicorn==23.0.0
groq==0.13.1 groq==0.13.1
yfinance==0.2.54 # yfinance removed — using Yahoo Finance v8 API directly (more reliable)
weasyprint==63.1 weasyprint==63.1
openpyxl==3.1.5 openpyxl==3.1.5
Pillow==11.1.0 Pillow==11.1.0