Files
BTC-Portfolio/btc-portfolio/backend/app/routes/dca.py
T
2026-04-28 21:17:46 +02:00

109 lines
3.4 KiB
Python

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,
}