From 95f97eeef9e31e282e15530d34d5d28d839e6b59 Mon Sep 17 00:00:00 2001 From: zephyrdark Date: Tue, 3 Feb 2026 07:08:21 +0900 Subject: [PATCH] feat: add transactions API with holdings update --- backend/app/api/portfolio.py | 83 +++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/backend/app/api/portfolio.py b/backend/app/api/portfolio.py index 60e5131..2c01e38 100644 --- a/backend/app/api/portfolio.py +++ b/backend/app/api/portfolio.py @@ -8,11 +8,12 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.api.deps import CurrentUser -from app.models.portfolio import Portfolio, PortfolioType, Target, Holding +from app.models.portfolio import Portfolio, PortfolioType, Target, Holding, Transaction, TransactionType from app.schemas.portfolio import ( PortfolioCreate, PortfolioUpdate, PortfolioResponse, PortfolioDetail, TargetCreate, TargetResponse, HoldingCreate, HoldingResponse, + TransactionCreate, TransactionResponse, ) router = APIRouter(prefix="/api/portfolios", tags=["portfolios"]) @@ -208,3 +209,83 @@ async def set_holdings( db.commit() return new_holdings + + +@router.get("/{portfolio_id}/transactions", response_model=List[TransactionResponse]) +async def get_transactions( + portfolio_id: int, + current_user: CurrentUser, + db: Session = Depends(get_db), + limit: int = 50, +): + """Get transaction history for a portfolio.""" + _get_portfolio(db, portfolio_id, current_user.id) + transactions = ( + db.query(Transaction) + .filter(Transaction.portfolio_id == portfolio_id) + .order_by(Transaction.executed_at.desc()) + .limit(limit) + .all() + ) + return transactions + + +@router.post("/{portfolio_id}/transactions", response_model=TransactionResponse, status_code=status.HTTP_201_CREATED) +async def add_transaction( + portfolio_id: int, + data: TransactionCreate, + current_user: CurrentUser, + db: Session = Depends(get_db), +): + """Add a transaction and update holdings accordingly.""" + _get_portfolio(db, portfolio_id, current_user.id) + + tx_type = TransactionType(data.tx_type) + + # Create transaction + transaction = Transaction( + portfolio_id=portfolio_id, + ticker=data.ticker, + tx_type=tx_type, + quantity=data.quantity, + price=data.price, + executed_at=data.executed_at, + memo=data.memo, + ) + db.add(transaction) + + # Update holding + holding = db.query(Holding).filter( + Holding.portfolio_id == portfolio_id, + Holding.ticker == data.ticker, + ).first() + + if tx_type == TransactionType.BUY: + if holding: + # Update average price + total_value = (holding.quantity * holding.avg_price) + (data.quantity * data.price) + new_quantity = holding.quantity + data.quantity + holding.quantity = new_quantity + holding.avg_price = total_value / new_quantity if new_quantity > 0 else 0 + else: + # Create new holding + holding = Holding( + portfolio_id=portfolio_id, + ticker=data.ticker, + quantity=data.quantity, + avg_price=data.price, + ) + db.add(holding) + elif tx_type == TransactionType.SELL: + if not holding or holding.quantity < data.quantity: + raise HTTPException( + status_code=400, + detail=f"Insufficient quantity for {data.ticker}" + ) + holding.quantity -= data.quantity + if holding.quantity == 0: + db.delete(holding) + + db.commit() + db.refresh(transaction) + return transaction