""" KJB Signal API endpoints. """ from datetime import date, datetime from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from app.core.database import get_db from app.api.deps import CurrentUser from app.models.signal import Signal, SignalStatus, SignalType from app.models.portfolio import Holding, Transaction, TransactionType from app.schemas.signal import SignalExecuteRequest, SignalResponse from app.schemas.portfolio import TransactionResponse router = APIRouter(prefix="/api/signal", tags=["signal"]) @router.get("/kjb/today", response_model=List[SignalResponse]) async def get_today_signals( current_user: CurrentUser, db: Session = Depends(get_db), ): """Get today's KJB trading signals.""" today = date.today() signals = ( db.query(Signal) .filter(Signal.date == today) .order_by(Signal.signal_type, Signal.ticker) .all() ) return signals @router.get("/kjb/history", response_model=List[SignalResponse]) async def get_signal_history( current_user: CurrentUser, db: Session = Depends(get_db), start_date: Optional[date] = Query(None), end_date: Optional[date] = Query(None), ticker: Optional[str] = Query(None), limit: int = Query(100, ge=1, le=1000), ): """Get historical KJB signals.""" query = db.query(Signal) if start_date: query = query.filter(Signal.date >= start_date) if end_date: query = query.filter(Signal.date <= end_date) if ticker: query = query.filter(Signal.ticker == ticker) signals = ( query.order_by(Signal.date.desc(), Signal.ticker) .limit(limit) .all() ) return signals @router.post("/{signal_id}/execute", response_model=dict) async def execute_signal( signal_id: int, data: SignalExecuteRequest, current_user: CurrentUser, db: Session = Depends(get_db), ): """Execute a signal by creating a portfolio transaction and updating signal status.""" from app.api.portfolio import _get_portfolio # 1. Look up the signal and verify it's active signal = db.query(Signal).filter(Signal.id == signal_id).first() if not signal: raise HTTPException(status_code=404, detail="Signal not found") if signal.status != SignalStatus.ACTIVE: raise HTTPException(status_code=400, detail="Signal is not active") # 2. Verify portfolio ownership portfolio = _get_portfolio(db, data.portfolio_id, current_user.id) # 3. Map signal type to transaction type if signal.signal_type == SignalType.BUY: tx_type = TransactionType.BUY else: tx_type = TransactionType.SELL # 4. Create transaction (reuse portfolio transaction logic) transaction = Transaction( portfolio_id=data.portfolio_id, ticker=signal.ticker, tx_type=tx_type, quantity=data.quantity, price=data.price, executed_at=datetime.utcnow(), memo=f"KJB signal #{signal.id}: {signal.signal_type.value}", ) db.add(transaction) # 5. Update holding holding = db.query(Holding).filter( Holding.portfolio_id == data.portfolio_id, Holding.ticker == signal.ticker, ).first() if tx_type == TransactionType.BUY: if holding: 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: holding = Holding( portfolio_id=data.portfolio_id, ticker=signal.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 {signal.ticker}" ) holding.quantity -= data.quantity if holding.quantity == 0: db.delete(holding) # 6. Update signal status to executed signal.status = SignalStatus.EXECUTED db.commit() db.refresh(transaction) db.refresh(signal) return { "transaction": { "id": transaction.id, "ticker": transaction.ticker, "tx_type": transaction.tx_type.value, "quantity": transaction.quantity, "price": float(transaction.price), "executed_at": transaction.executed_at.isoformat(), }, "signal": { "id": signal.id, "status": signal.status.value, }, }