mirror of
https://github.com/wahyd4/links.git
synced 2026-08-08 21:04:53 +10:00
feat: add per-stock weekly price change column to portfolio table
This commit is contained in:
+40
-2
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user