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))