from datetime import date, timedelta from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from ..database import get_db from .. import models from ..dependencies import get_current_user from ..services.btc import get_btc_price_eur router = APIRouter() def _first_of_month(year: int, month: int) -> date: return date(year, month, 1) def _next_month(year: int, month: int) -> tuple[int, int]: if month == 12: return year + 1, 1 return year, month + 1 @router.get("/dca") def get_dca( monthly_amount: float = Query(..., gt=0), start_date: Optional[str] = Query(default=None), db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user), ): # Determine start date if start_date: try: sim_start = date.fromisoformat(start_date) except ValueError: sim_start = None else: sim_start = None if sim_start is None: earliest = ( db.query(models.Purchase) .filter(models.Purchase.user_id == current_user.id) .order_by(models.Purchase.created_at.asc()) .first() ) if earliest: sim_start = earliest.created_at.date() else: sim_start = date.today() - timedelta(days=365) # Load all candles into a lookup dict {date_str: close_price} candles_db = db.query(models.OHLCCandle).all() price_by_date: dict[str, float] = {c.date: c.close for c in candles_db} today = date.today() current_price, _ = get_btc_price_eur() # Patch today's price in case the candle isn't refreshed yet if current_price: price_by_date[today.isoformat()] = current_price # Walk month by month and simulate buys year, month = sim_start.year, sim_start.month end_year, end_month = today.year, today.month dca_invested = 0.0 dca_btc = 0.0 monthly_series = [] while (year, month) <= (end_year, end_month): # Find the closest available candle on or after the 1st of this month buy_date = None buy_price = None for day_offset in range(8): candidate = _first_of_month(year, month) + timedelta(days=day_offset) key = candidate.isoformat() if key in price_by_date: buy_date = key buy_price = price_by_date[key] break if buy_price and buy_price > 0: btc_bought = monthly_amount / buy_price dca_btc += btc_bought dca_invested += monthly_amount monthly_series.append({ "month": f"{year:04d}-{month:02d}", "price_used": round(buy_price, 2), "btc_bought": round(btc_bought, 8), "cumulative_btc": round(dca_btc, 8), "cumulative_invested": round(dca_invested, 2), }) year, month = _next_month(year, month) dca_current_value = dca_btc * current_price if current_price else 0.0 dca_profit_loss = dca_current_value - dca_invested return { "start_date": sim_start.isoformat(), "monthly_amount": monthly_amount, "dca_total_invested": round(dca_invested, 2), "dca_total_btc": round(dca_btc, 8), "dca_current_value": round(dca_current_value, 2), "dca_profit_loss": round(dca_profit_loss, 2), "monthly_series": monthly_series, }