test change rate

This commit is contained in:
2026-05-02 20:55:02 +10:00
parent 29f35c5c87
commit b52b821baf
+39 -71
View File
@@ -341,86 +341,49 @@ def _build_performance_chart_data() -> Optional[str]:
if wk not in week_label_date or d > week_label_date[wk]:
week_label_date[wk] = d
# Date range for price lookups (portfolio series + benchmarks share these)
import datetime as dt
today = dt.date.today()
earliest_date = week_label_date[all_week_keys[0]]
latest_date = week_label_date[all_week_keys[-1]]
start_str = (earliest_date - timedelta(days=7)).isoformat()
end_str = (latest_date + timedelta(days=5)).isoformat()
# Always fetch up to today so benchmarks include the current (unsnapshot'd) week
end_str = (today + timedelta(days=1)).isoformat()
# Pre-fetch the full closing-price history for every currently-held stock in one
# yfinance call per ticker (not per week). This avoids O(stocks × weeks) fetches
# and is the same price series used by the dashboard cards.
def _fetch_closes(stock_code: str) -> list:
"""Return [(date, close), …] sorted ascending for stock_code over the chart period."""
try:
import yfinance as yf
hist = yf.Ticker(stock_code).history(start=start_str, end=end_str, auto_adjust=True)
if hist.empty:
return []
closes = []
for d, price in hist['Close'].items():
date_val = d.date() if hasattr(d, 'date') else d
closes.append((date_val, float(price)))
return sorted(closes)
except Exception as exc:
logger.warning("invest: price history failed for %s: %s", stock_code, exc)
return []
# Whether today is past the last snapshot — if so, append a live "Today" point
add_today = today > latest_date
def _closest_close(closes: list, target_date) -> Optional[float]:
"""Most recent closing price on or before target_date."""
result = None
for d, price in closes:
if d <= target_date:
result = price
else:
break
return result
stock_histories: dict = {}
for _p in Portfolio.objects.prefetch_related('stocks').all():
for _s in _p.stocks.filter(quantity__gt=0):
if _s.stock_code not in stock_histories:
stock_histories[_s.stock_code] = _fetch_closes(_s.stock_code)
# Per-portfolio cumulative % series — current holdings × historical prices.
#
# Using current share counts valued at each historical week's price removes the
# distortion caused by transactions (capital injections / withdrawals): a BUY that
# injects cash no longer inflates subsequent snapshot totals, and a SELL no longer
# deflates them. The resulting series reflects pure market price performance of the
# positions actually held today — identical in methodology to the week-change figures
# shown on each portfolio card on the dashboard.
# Per-portfolio cumulative % series — based on the actual weekly PortfolioSnapshot totals.
# The snapshot captures the true portfolio value at that moment (including all positions,
# before and after rebalancing), so it is the authoritative measure of portfolio performance.
# When add_today is True, the current live value is appended as an extra "Today" data point
# so the chart always includes the current week even before the Saturday snapshot runs.
portfolio_colors = ['#2563EB', '#7C3AED', '#0D9488', '#DB2777', '#EA580C']
datasets = []
for idx, portfolio in enumerate(Portfolio.objects.prefetch_related('stocks').all()):
current_stocks = list(portfolio.stocks.filter(quantity__gt=0))
if not current_stocks:
for idx, portfolio in enumerate(Portfolio.objects.all()):
pw = portfolio_weekly.get(portfolio.id, {})
if not pw:
continue
week_totals = []
for wk in all_week_keys:
label_date = week_label_date[wk]
total = 0.0
all_priced = True
for stock in current_stocks:
closes = stock_histories.get(stock.stock_code, [])
price = _closest_close(closes, label_date)
if price is None:
all_priced = False
break
total += price * float(stock.quantity)
week_totals.append(total if all_priced else None)
# Base = first week where every position has a price
base_val = next((v for v in week_totals if v), None)
first_week = min(pw.keys())
base_val = pw[first_week][1]
if not base_val:
continue
data_pts = [
round((v - base_val) / base_val * 100, 2) if v is not None else None
for v in week_totals
round((pw[wk][1] - base_val) / base_val * 100, 2) if wk in pw else None
for wk in all_week_keys
]
if add_today:
try:
live_total = get_portfolio_value(portfolio)['total_value']
today_pct = round((live_total - base_val) / base_val * 100, 2) if live_total else None
except Exception as exc:
logger.warning("invest: live value for today chart point failed (%s): %s", portfolio.name, exc)
today_pct = None
data_pts.append(today_pct)
color = portfolio_colors[idx % len(portfolio_colors)]
datasets.append({
'label': portfolio.name,
@@ -435,14 +398,12 @@ def _build_performance_chart_data() -> Optional[str]:
'fill': False,
})
# Benchmark series — start_str / end_str already computed above
# Benchmark series — fetched up to today so the final point aligns with portfolio live values
def _benchmark(ticker: str, label: str, color: str) -> Optional[dict]:
from .models import BenchmarkPrice
import datetime as dt
today = dt.date.today()
# Check DB coverage — refresh if we have no rows or latest price > 7 days stale
# Check DB coverage — refresh if no rows or latest price is stale
qs = BenchmarkPrice.objects.filter(ticker=ticker, date__gte=earliest_date - timedelta(days=7))
latest_db_date = qs.order_by('-date').values_list('date', flat=True).first()
need_refresh = latest_db_date is None or (today - latest_db_date).days > 7
@@ -469,7 +430,7 @@ def _build_performance_chart_data() -> Optional[str]:
for row in BenchmarkPrice.objects.filter(
ticker=ticker,
date__gte=earliest_date - timedelta(days=7),
date__lte=latest_date + timedelta(days=5),
date__lte=today + timedelta(days=1),
).order_by('date')
}
if not closes:
@@ -488,6 +449,11 @@ def _build_performance_chart_data() -> Optional[str]:
if closest_close(week_label_date[wk]) is not None else None
for wk in all_week_keys
]
if add_today:
today_close = closest_close(today)
data_pts.append(
round((today_close - base_price) / base_price * 100, 2) if today_close else None
)
return {
'label': label,
'data': data_pts,
@@ -512,6 +478,8 @@ def _build_performance_chart_data() -> Optional[str]:
datasets.append(qqq)
labels = [week_label_date[wk].strftime('%b %-d') for wk in all_week_keys]
if add_today:
labels.append('Today')
return json.dumps({'labels': labels, 'datasets': datasets})