mirror of
https://github.com/wahyd4/links.git
synced 2026-08-08 21:04:53 +10:00
update performance chart logic!
This commit is contained in:
+72
-14
@@ -341,23 +341,85 @@ 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
|
||||
|
||||
# Per-portfolio cumulative % series — each portfolio bases off its own first week
|
||||
# Date range for price lookups (portfolio series + benchmarks share these)
|
||||
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()
|
||||
|
||||
# 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 []
|
||||
|
||||
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.
|
||||
portfolio_colors = ['#2563EB', '#7C3AED', '#0D9488', '#DB2777', '#EA580C']
|
||||
datasets = []
|
||||
|
||||
for idx, portfolio in enumerate(Portfolio.objects.all()):
|
||||
pw = portfolio_weekly.get(portfolio.id, {})
|
||||
if not pw:
|
||||
for idx, portfolio in enumerate(Portfolio.objects.prefetch_related('stocks').all()):
|
||||
current_stocks = list(portfolio.stocks.filter(quantity__gt=0))
|
||||
if not current_stocks:
|
||||
continue
|
||||
# base = value in the earliest week available for this portfolio
|
||||
first_week = min(pw.keys())
|
||||
base_val = pw[first_week][1]
|
||||
|
||||
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)
|
||||
if not base_val:
|
||||
continue
|
||||
|
||||
data_pts = [
|
||||
round((pw[wk][1] - base_val) / base_val * 100, 2) if wk in pw else None
|
||||
for wk in all_week_keys
|
||||
round((v - base_val) / base_val * 100, 2) if v is not None else None
|
||||
for v in week_totals
|
||||
]
|
||||
color = portfolio_colors[idx % len(portfolio_colors)]
|
||||
datasets.append({
|
||||
@@ -373,11 +435,7 @@ def _build_performance_chart_data() -> Optional[str]:
|
||||
'fill': False,
|
||||
})
|
||||
|
||||
# Benchmark series — start 7 days before the earliest label date
|
||||
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()
|
||||
# Benchmark series — start_str / end_str already computed above
|
||||
|
||||
def _benchmark(ticker: str, label: str, color: str) -> Optional[dict]:
|
||||
from .models import BenchmarkPrice
|
||||
|
||||
Reference in New Issue
Block a user