Add purchase date picker and sells feature

- Purchase form now includes a date picker (defaults to today)
- New Sell model, CRUD endpoints (/sells), and stats integration
- AddSell and SellList components added to dashboard
- Portfolio chart updated to reflect sells over time

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-06 19:52:24 +02:00
parent 5cf3726f59
commit 5bb67d6663
10 changed files with 367 additions and 11 deletions
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
from typing import List
from typing import List, Optional
from datetime import datetime
from ..database import get_db
@@ -14,6 +14,7 @@ router = APIRouter()
class PurchaseCreate(BaseModel):
amount_eur: float = Field(gt=0, le=10_000_000)
price_eur: float = Field(gt=0, le=10_000_000)
created_at: Optional[datetime] = None
class PurchaseUpdate(BaseModel):
@@ -54,6 +55,7 @@ def add_purchase(
purchase = models.Purchase(
amount_eur=purchase_in.amount_eur,
price_eur=purchase_in.price_eur,
created_at=purchase_in.created_at or datetime.utcnow(),
user_id=current_user.id,
)
db.add(purchase)
+101
View File
@@ -0,0 +1,101 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime
from ..database import get_db
from .. import models
from ..dependencies import get_current_user
router = APIRouter()
class SellCreate(BaseModel):
btc_amount: float = Field(gt=0, le=21_000_000)
price_eur: float = Field(gt=0, le=10_000_000)
created_at: Optional[datetime] = None
class SellUpdate(BaseModel):
btc_amount: float = Field(gt=0, le=21_000_000)
price_eur: float = Field(gt=0, le=10_000_000)
created_at: datetime
class SellOut(BaseModel):
id: int
btc_amount: float
price_eur: float
created_at: datetime
class Config:
from_attributes = True
@router.get("/sells", response_model=List[SellOut])
def list_sells(
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
return (
db.query(models.Sell)
.filter(models.Sell.user_id == current_user.id)
.order_by(models.Sell.created_at)
.all()
)
@router.post("/sells", response_model=SellOut, status_code=status.HTTP_201_CREATED)
def add_sell(
sell_in: SellCreate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
sell = models.Sell(
btc_amount=sell_in.btc_amount,
price_eur=sell_in.price_eur,
created_at=sell_in.created_at or datetime.utcnow(),
user_id=current_user.id,
)
db.add(sell)
db.commit()
db.refresh(sell)
return sell
@router.put("/sells/{sell_id}", response_model=SellOut)
def update_sell(
sell_id: int,
sell_in: SellUpdate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
sell = db.query(models.Sell).filter(
models.Sell.id == sell_id,
models.Sell.user_id == current_user.id,
).first()
if not sell:
raise HTTPException(status_code=404, detail="Sell not found")
sell.btc_amount = sell_in.btc_amount
sell.price_eur = sell_in.price_eur
sell.created_at = sell_in.created_at
db.commit()
db.refresh(sell)
return sell
@router.delete("/sells/{sell_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_sell(
sell_id: int,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
sell = db.query(models.Sell).filter(
models.Sell.id == sell_id,
models.Sell.user_id == current_user.id,
).first()
if not sell:
raise HTTPException(status_code=404, detail="Sell not found")
db.delete(sell)
db.commit()
+13 -6
View File
@@ -15,17 +15,24 @@ def get_stats(
current_user: models.User = Depends(get_current_user),
):
purchases = db.query(models.Purchase).filter(models.Purchase.user_id == current_user.id).all()
sells = db.query(models.Sell).filter(models.Sell.user_id == current_user.id).all()
total_invested = sum(p.amount_eur for p in purchases)
total_btc = sum(p.amount_eur / p.price_eur for p in purchases) if purchases else 0.0
average_price = total_invested / total_btc if total_btc > 0 else 0.0
total_btc_bought = sum(p.amount_eur / p.price_eur for p in purchases) if purchases else 0.0
total_btc_sold = sum(s.btc_amount for s in sells)
proceeds_eur = sum(s.btc_amount * s.price_eur for s in sells)
net_btc = total_btc_bought - total_btc_sold
net_invested = total_invested - proceeds_eur
average_price = net_invested / net_btc if net_btc > 0 else 0.0
current_price = get_btc_price_eur()
portfolio_value = total_btc * current_price
profit_loss = portfolio_value - total_invested
portfolio_value = net_btc * current_price
profit_loss = portfolio_value - net_invested
return {
"total_invested": round(total_invested, 2),
"total_btc": round(total_btc, 8),
"total_invested": round(net_invested, 2),
"total_btc": round(net_btc, 8),
"average_price": round(average_price, 2),
"current_price": round(current_price, 2),
"portfolio_value": round(portfolio_value, 2),