diff --git a/invest/services.py b/invest/services.py index f543ef6..78de648 100644 --- a/invest/services.py +++ b/invest/services.py @@ -16,12 +16,15 @@ from .models import Portfolio, Stock, PortfolioSnapshot logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- -# In-process price cache (5 min TTL) +# In-process price cache (5 min TTL) + last-week price cache (1 hour TTL) # --------------------------------------------------------------------------- _price_cache: dict[str, tuple[float, datetime]] = {} _PRICE_CACHE_TTL_SECONDS = 300 +_last_week_price_cache: dict[str, tuple[Optional[float], datetime]] = {} +_LAST_WEEK_CACHE_TTL_SECONDS = 3600 + def _get_yfinance_price(stock_code: str) -> Optional[float]: try: @@ -36,6 +39,30 @@ def _get_yfinance_price(stock_code: str) -> Optional[float]: return None +def _get_last_week_price(stock_code: str) -> Optional[float]: + """Return the closing price ~7 calendar days ago (first available trading day in that window).""" + now = datetime.now() + cached = _last_week_price_cache.get(stock_code) + if cached: + price, cached_at = cached + if (now - cached_at).total_seconds() < _LAST_WEEK_CACHE_TTL_SECONDS: + return price + + try: + import yfinance as yf + import datetime as dt + end = dt.date.today() - dt.timedelta(days=5) + start = end - dt.timedelta(days=5) + hist = yf.Ticker(stock_code).history(start=start.isoformat(), end=end.isoformat()) + price = float(hist["Close"].iloc[-1]) if not hist.empty else None + except Exception as exc: + logger.warning("yfinance last-week price failed for %s: %s", stock_code, exc) + price = None + + _last_week_price_cache[stock_code] = (price, now) + return price + + def get_current_price(stock_code: str) -> Optional[float]: now = datetime.now() cached = _price_cache.get(stock_code) @@ -58,18 +85,29 @@ def get_current_price(stock_code: str) -> Optional[float]: # --------------------------------------------------------------------------- def get_portfolio_value(portfolio: Portfolio) -> dict: - """Return live holdings with current prices and total value.""" + """Return live holdings with current prices, total value, and weekly price change per stock.""" holdings = [] total_value = Decimal('0') for stock in portfolio.stocks.filter(quantity__gt=0): price = get_current_price(stock.stock_code) or 0.0 value = Decimal(str(price)) * stock.quantity + last_week_price = _get_last_week_price(stock.stock_code) + + price_change = None + price_change_pct = None + if price and last_week_price and last_week_price > 0: + price_change = round(price - last_week_price, 4) + price_change_pct = round((price_change / last_week_price) * 100, 2) + holdings.append({ 'stock_code': stock.stock_code, 'quantity': float(stock.quantity), 'current_price': price, 'current_value': float(value), + 'last_week_price': last_week_price, + 'price_change': price_change, + 'price_change_pct': price_change_pct, }) total_value += value diff --git a/invest/templates/invest/dashboard.html b/invest/templates/invest/dashboard.html index f9119d9..22002dc 100644 --- a/invest/templates/invest/dashboard.html +++ b/invest/templates/invest/dashboard.html @@ -126,6 +126,7 @@ Ticker Qty Price + Week Mkt Value @@ -141,6 +142,15 @@ {% if stock.current_price %}${{ stock.current_price|floatformat:2 }}{% else %}{% endif %} + + {% if stock.price_change is not None %} + + {% if stock.price_change >= 0 %}+{% endif %}{{ stock.price_change_pct|floatformat:1 }}% + + {% else %} + + {% endif %} + {% if stock.current_value %}${{ stock.current_value|floatformat:0 }}{% else %}{% endif %}