Files
links/invest/tasks.py
T

98 lines
3.4 KiB
Python

"""
Background tasks for the invest app.
"""
import logging
from decimal import Decimal
logger = logging.getLogger(__name__)
def snapshot_all_portfolios():
"""
Capture a PortfolioSnapshot for every portfolio.
Scheduled every Saturday at 08:00. Also callable manually for backfill.
"""
from django.utils import timezone
from .models import Portfolio, PortfolioSnapshot
from .services import get_portfolio_value, refresh_benchmark_prices
now = timezone.now()
today = now.date()
logger.info("invest: starting portfolio snapshot at %s", now)
count = 0
for portfolio in Portfolio.objects.prefetch_related('stocks').all():
try:
data = get_portfolio_value(portfolio)
total_value = Decimal(str(data['total_value']))
# One snapshot per portfolio per day — overwrite if run twice.
PortfolioSnapshot.objects.filter(
portfolio=portfolio,
captured_at__date=today,
).delete()
PortfolioSnapshot.objects.create(
portfolio=portfolio,
captured_at=now,
total_value=total_value,
)
count += 1
logger.info("invest: snapshot %s = $%.2f", portfolio.name, total_value)
except Exception as exc:
logger.error("invest: snapshot failed for %s: %s", portfolio.name, exc, exc_info=True)
refresh_benchmark_prices()
logger.info("invest: snapshot complete — %d portfolios", count)
# Also refresh benchmark prices so the chart has up-to-date SPY/QQQ data
refresh_benchmark_prices()
def refresh_benchmark_prices(tickers=None):
"""
Fetch and cache daily closing prices for benchmark tickers (SPY, QQQ).
Skips the fetch if data is already fresh (latest date within last 7 days).
Called weekly alongside snapshot_all_portfolios.
"""
import datetime as dt
import yfinance as yf
from .models import BenchmarkPrice
if tickers is None:
tickers = ['SPY', 'QQQ']
today = dt.date.today()
start = (today - dt.timedelta(days=365 * 2)).isoformat() # 2 years of history
end = (today + dt.timedelta(days=1)).isoformat()
for ticker in tickers:
latest = (
BenchmarkPrice.objects.filter(ticker=ticker)
.order_by('-date')
.values_list('date', flat=True)
.first()
)
if latest and (today - latest).days <= 7:
logger.info("invest: benchmark %s is fresh (latest=%s), skipping", ticker, latest)
continue
try:
hist = yf.Ticker(ticker).history(start=start, end=end)
if hist.empty:
logger.warning("invest: no data for benchmark %s", ticker)
continue
rows = []
for d, v in hist['Close'].items():
date_val = d.date() if hasattr(d, 'date') else d
rows.append(BenchmarkPrice(ticker=ticker, date=date_val, close=round(float(v), 4)))
BenchmarkPrice.objects.bulk_create(
rows,
update_conflicts=True,
unique_fields=['ticker', 'date'],
update_fields=['close'],
)
logger.info("invest: refreshed %d prices for %s (latest=%s)", len(rows), ticker, today)
except Exception as exc:
logger.error("invest: benchmark refresh failed for %s: %s", ticker, exc, exc_info=True)