diff --git a/btc-portfolio/backend/app/main.py b/btc-portfolio/backend/app/main.py index b458409..b2b8aad 100644 --- a/btc-portfolio/backend/app/main.py +++ b/btc-portfolio/backend/app/main.py @@ -3,7 +3,7 @@ from fastapi.middleware.cors import CORSMiddleware from sqlalchemy import text from .database import engine, Base, SessionLocal -from .routes import users, purchases, stats, history, admin, candles +from .routes import users, purchases, stats, history, admin, candles, sells Base.metadata.create_all(bind=engine) @@ -23,6 +23,7 @@ app.include_router(stats.router) app.include_router(history.router) app.include_router(admin.router, prefix="/admin") app.include_router(candles.router) +app.include_router(sells.router) @app.on_event("startup") diff --git a/btc-portfolio/backend/app/models.py b/btc-portfolio/backend/app/models.py index 30959f7..a40a218 100644 --- a/btc-portfolio/backend/app/models.py +++ b/btc-portfolio/backend/app/models.py @@ -13,6 +13,7 @@ class User(Base): is_admin = Column(Boolean, default=False, nullable=False, server_default='0') purchases = relationship("Purchase", back_populates="owner", cascade="all, delete") + sells = relationship("Sell", back_populates="owner", cascade="all, delete") class Purchase(Base): @@ -27,6 +28,18 @@ class Purchase(Base): owner = relationship("User", back_populates="purchases") +class Sell(Base): + __tablename__ = "sells" + + id = Column(Integer, primary_key=True, index=True) + btc_amount = Column(Float, nullable=False) + price_eur = Column(Float, nullable=False) + created_at = Column(DateTime, default=datetime.utcnow) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + + owner = relationship("User", back_populates="sells") + + class OHLCCandle(Base): __tablename__ = "ohlc_candles" diff --git a/btc-portfolio/backend/app/routes/purchases.py b/btc-portfolio/backend/app/routes/purchases.py index f8c3c65..454ccd1 100644 --- a/btc-portfolio/backend/app/routes/purchases.py +++ b/btc-portfolio/backend/app/routes/purchases.py @@ -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) diff --git a/btc-portfolio/backend/app/routes/sells.py b/btc-portfolio/backend/app/routes/sells.py new file mode 100644 index 0000000..ba3d070 --- /dev/null +++ b/btc-portfolio/backend/app/routes/sells.py @@ -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() diff --git a/btc-portfolio/backend/app/routes/stats.py b/btc-portfolio/backend/app/routes/stats.py index b619099..ae263e3 100644 --- a/btc-portfolio/backend/app/routes/stats.py +++ b/btc-portfolio/backend/app/routes/stats.py @@ -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), diff --git a/btc-portfolio/frontend/src/components/AddPurchase.js b/btc-portfolio/frontend/src/components/AddPurchase.js index a4c1add..9aa7d1a 100644 --- a/btc-portfolio/frontend/src/components/AddPurchase.js +++ b/btc-portfolio/frontend/src/components/AddPurchase.js @@ -12,6 +12,7 @@ const styles = { }; export default function AddPurchase({ onAdded }) { + const [purchaseDate, setPurchaseDate] = useState(new Date().toISOString().split('T')[0]); const [amountEur, setAmountEur] = useState(''); const [priceEur, setPriceEur] = useState(''); const [error, setError] = useState(''); @@ -30,12 +31,14 @@ export default function AddPurchase({ onAdded }) { body: JSON.stringify({ amount_eur: parseFloat(amountEur), price_eur: parseFloat(priceEur), + created_at: new Date(purchaseDate + 'T12:00:00').toISOString(), }), }); if (!res.ok) { setError('Failed to add purchase'); return; } + setPurchaseDate(new Date().toISOString().split('T')[0]); setAmountEur(''); setPriceEur(''); onAdded(); @@ -50,6 +53,13 @@ export default function AddPurchase({ onAdded }) {