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 ──────────────────────────────────────────────────────────────────
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:
import yfinance as yf
ticker = yf.Ticker('USDVND=X')
hist = ticker.history(period='5d')
if not hist.empty:
rate = float(hist['Close'].iloc[-1])
log.info(f'[fx] yfinance: 1 USD = {rate:,.0f} VND')
resp = requests.get(url, timeout=REQUEST_TIMEOUT,
headers={'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json'})
if resp.status_code == 200:
result = resp.json().get('chart', {}).get('result')
if result:
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
except Exception as e:
log.warning(f'[fx] yfinance failed: {e}')
log.warning(f'[fx] Yahoo v8 {subdomain}: {e}')
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 requests
from datetime import datetime
from app.extensions import db
from app.models.investment import Investment
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):
"""
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.
"""
if not ticker:
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:
import yfinance as yf
t = yf.Ticker(ticker.upper())
hist = t.history(period='2d')
if hist.empty:
resp = requests.get(url, headers=HEADERS, timeout=REQUEST_TIMEOUT)
if resp.status_code == 200:
price = _parse_v8_price(resp.json())
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 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:
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
@@ -63,9 +136,7 @@ def update_prices(investment_ids=None):
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()
total_cost = sum(i.total_cost for i in investments)
@@ -98,25 +169,3 @@ def get_portfolio_summary():
'allocation': allocation,
'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
gunicorn==23.0.0
groq==0.13.1
yfinance==0.2.54
# yfinance removed — using Yahoo Finance v8 API directly (more reliable)
weasyprint==63.1
openpyxl==3.1.5
Pillow==11.1.0