feat: add per-stock weekly price change column to portfolio table

This commit is contained in:
2026-04-25 10:42:21 +10:00
parent d4183f30e6
commit 3acaa328cb
2 changed files with 50 additions and 2 deletions
+40 -2
View File
@@ -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
+10
View File
@@ -126,6 +126,7 @@
<th class="px-6 py-3 text-left">Ticker</th>
<th class="px-6 py-3 text-right">Qty</th>
<th class="px-6 py-3 text-right">Price</th>
<th class="px-6 py-3 text-right">Week</th>
<th class="px-6 py-3 text-right">Mkt Value</th>
</tr>
</thead>
@@ -141,6 +142,15 @@
<td class="px-6 py-3 text-right text-stone-500">
{% if stock.current_price %}${{ stock.current_price|floatformat:2 }}{% else %}<span class="text-stone-300"></span>{% endif %}
</td>
<td class="px-6 py-3 text-right">
{% if stock.price_change is not None %}
<span class="text-xs font-medium {% if stock.price_change >= 0 %}text-green-700{% else %}text-red-600{% endif %}">
{% if stock.price_change >= 0 %}+{% endif %}{{ stock.price_change_pct|floatformat:1 }}%
</span>
{% else %}
<span class="text-stone-300"></span>
{% endif %}
</td>
<td class="px-6 py-3 text-right font-semibold text-stone-800">
{% if stock.current_value %}${{ stock.current_value|floatformat:0 }}{% else %}<span class="text-stone-300"></span>{% endif %}
</td>