Files
Jonathan 5cf3726f59 Improve portfolio chart with historical price-based data points
Chart now plots weekly data points from first purchase to today using
candle/history price data, giving an accurate view of portfolio value
over time rather than just at purchase dates.

Backend seeds up to 365 days of daily close prices from CoinGecko as
synthetic OHLC candles, refreshing stale entries older than 31 days.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:30:48 +02:00

118 lines
4.3 KiB
Python

import logging
from datetime import datetime, timezone, timedelta, date as dt_date
from sqlalchemy.orm import Session
from ..models import OHLCCandle
from .btc import get_btc_ohlc_eur, aggregate_to_daily, get_btc_history_eur
logger = logging.getLogger(__name__)
def seed_candles(db: Session) -> None:
"""Fetch 30 days of daily OHLC candles from CoinGecko and store them.
Free tier gives 4-hour bars for days<=30, which aggregate cleanly to daily candles.
days>30 drops to 4-day granularity (unusable for a daily chart).
"""
raw = get_btc_ohlc_eur(days=30)
if not raw:
logger.warning("Candle seed: CoinGecko returned no data — will retry on next startup.")
return
daily = aggregate_to_daily(raw)
rows = [
OHLCCandle(date=date, open=v["open"], high=v["high"], low=v["low"], close=v["close"])
for date, v in sorted(daily.items())
]
for row in rows:
db.merge(row)
db.commit()
logger.info("Candle seed: stored %d daily candles (%s%s).", len(rows), min(daily.keys()), max(daily.keys()))
def seed_historical_prices(db: Session) -> None:
"""Backfill up to 365 days of daily close prices from CoinGecko market_chart.
Uses previous day's close as each day's open to produce red/green candles.
Clears entries older than 31 days on each run so the data stays fresh.
Real OHLC entries (last 30 days) are never touched.
"""
raw = get_btc_history_eur()
if not raw:
logger.warning("Historical price seed: CoinGecko returned no data.")
return
prices = {}
for ts_ms, price in raw:
date = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc).strftime("%Y-%m-%d")
prices[date] = price
# Remove stale historical entries (older than 31 days) so they get re-seeded with current data
cutoff = (datetime.now(tz=timezone.utc) - timedelta(days=31)).strftime("%Y-%m-%d")
db.query(OHLCCandle).filter(OHLCCandle.date < cutoff).delete()
db.commit()
existing = {c.date for c in db.query(OHLCCandle).all()}
new_rows = []
prev_close = None
for date, close in sorted(prices.items()):
if date in existing:
prev_close = close
continue
open_ = prev_close if prev_close is not None else close
high = max(open_, close)
low = min(open_, close)
new_rows.append(OHLCCandle(date=date, open=open_, high=high, low=low, close=close))
prev_close = close
if new_rows:
db.add_all(new_rows)
db.commit()
logger.info("Historical price seed: stored %d daily entries (%s%s).", len(new_rows), new_rows[0].date, new_rows[-1].date)
def refresh_latest_candles(db: Session) -> None:
"""Add any missing candles up to today. Seeds the DB if empty.
Also detects and replaces coarse (>2-day gap) legacy data from a previous days=365 seed.
"""
# Always backfill historical prices for dates not yet in DB (no-op once populated)
seed_historical_prices(db)
# Sparse-data detection: if existing candles have >2-day gaps, wipe and re-seed
first_two = db.query(OHLCCandle).order_by(OHLCCandle.date.asc()).limit(2).all()
if len(first_two) == 2:
d1 = dt_date.fromisoformat(first_two[0].date)
d2 = dt_date.fromisoformat(first_two[1].date)
if (d2 - d1).days > 2:
logger.warning("Candle refresh: detected coarse candle data (gap=%d days). Wiping and re-seeding with daily candles.", (d2 - d1).days)
db.query(OHLCCandle).delete()
db.commit()
seed_candles(db)
return
latest = db.query(OHLCCandle).order_by(OHLCCandle.date.desc()).first()
if latest is None:
seed_candles(db)
return
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
if latest.date >= today:
return # Already up to date
raw = get_btc_ohlc_eur(days=7)
if not raw:
logger.warning("Candle refresh: CoinGecko returned no data.")
return
daily = aggregate_to_daily(raw)
new_dates = [d for d in daily if d > latest.date]
if not new_dates:
return
for date in new_dates:
v = daily[date]
db.merge(OHLCCandle(date=date, open=v["open"], high=v["high"], low=v["low"], close=v["close"]))
db.commit()
logger.info("Candle refresh: upserted %d candle(s) up to %s.", len(new_dates), max(new_dates))