mirror of
https://github.com/wahyd4/links.git
synced 2026-08-09 05:06:16 +10:00
test change rate
This commit is contained in:
+39
-71
@@ -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})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user