fd21aa7f4e
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
109 lines
3.4 KiB
Python
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,
|
|
}
|