from datetime import date 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.screening import ScreeningSignal, AutoOrder from app.schemas.screening import ( ScreeningSignalResponse, AutoOrderResponse, WatchlistItem, ScreeningSummary, ) router = APIRouter(tags=["screening"]) @router.get("/api/screening/today", response_model=List[ScreeningSignalResponse]) async def get_today_screening( current_user: CurrentUser, db: Session = Depends(get_db), ): today = date.today() signals = ( db.query(ScreeningSignal) .filter(ScreeningSignal.screen_date == today) .order_by(ScreeningSignal.ticker) .all() ) return signals @router.get("/api/screening/history", response_model=List[ScreeningSignalResponse]) async def get_screening_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), status: Optional[str] = Query(None), limit: int = Query(100, ge=1, le=1000), ): query = db.query(ScreeningSignal) if start_date: query = query.filter(ScreeningSignal.screen_date >= start_date) if end_date: query = query.filter(ScreeningSignal.screen_date <= end_date) if ticker: query = query.filter(ScreeningSignal.ticker == ticker) if status: query = query.filter(ScreeningSignal.status == status) signals = ( query.order_by(ScreeningSignal.screen_date.desc(), ScreeningSignal.ticker) .limit(limit) .all() ) return signals @router.get("/api/screening/watchlist", response_model=List[WatchlistItem]) async def get_watchlist( current_user: CurrentUser, db: Session = Depends(get_db), ): signals = ( db.query(ScreeningSignal) .filter(ScreeningSignal.status.in_(["pending", "watching"])) .order_by(ScreeningSignal.screen_date.desc(), ScreeningSignal.ticker) .all() ) return signals @router.post("/api/screening/execute", response_model=dict) async def execute_screening( current_user: CurrentUser, db: Session = Depends(get_db), execute_orders: bool = Query(False, description="true일 때만 KIS 주문 전송"), ): from app.core.config import get_settings from app.services.trading.kis_executor import KISTradeExecutor from app.models.stock import Price from app.services.strategy.kospi_screener import KJBScreeningSignalGenerator from jobs.screening_job import _latest_price_date, _price_rows_to_frame from datetime import datetime, timedelta settings = get_settings() if not settings.kis_app_key or not settings.kis_app_secret: raise HTTPException(status_code=400, detail="KIS API credentials not configured") watching = ( db.query(ScreeningSignal) .filter(ScreeningSignal.status.in_(["pending", "watching"])) .all() ) if not watching: return {"message": "No watching signals to execute", "orders": [], "execute_orders": execute_orders} executor = KISTradeExecutor( app_key=settings.kis_app_key, app_secret=settings.kis_app_secret, account_no=settings.kis_account_no, paper_trade=settings.kis_paper_trade, ) balance = executor.get_account_balance() capital = balance.total_amount or balance.available_amount if capital <= 0: raise HTTPException(status_code=400, detail="KIS account balance unavailable") signal_gen = KJBScreeningSignalGenerator() latest_date = _latest_price_date(db, market="KOSPI") or date.today() held_tickers = {position.ticker for position in executor.get_positions()} results = [] for signal in watching: if signal.ticker in held_tickers: results.append({ "ticker": signal.ticker, "success": False, "status": "skipped", "message": "Already held", }) continue rows = ( db.query(Price) .filter( Price.ticker == signal.ticker, Price.date >= (signal.screen_date - timedelta(days=20)), Price.date <= latest_date, ) .order_by(Price.date) .all() ) price_df = _price_rows_to_frame(rows) entry = signal_gen.check_entry(signal.ticker, price_df, signal.screen_date) if not entry: results.append({ "ticker": signal.ticker, "success": False, "status": "waiting", "message": "Entry condition not met", }) continue entry_price = float(entry["entry_price"]) stop_price = float(entry["stop_price"]) qty = signal_gen.calculate_position_size( entry_price=entry_price, stop_price=stop_price, capital=capital, risk_pct=settings.screening_risk_pct, ) max_qty_by_order = int(settings.screening_max_order_amount / entry_price) qty = min(qty, max_qty_by_order) if qty <= 0: results.append({ "ticker": signal.ticker, "success": False, "status": "skipped", "entry_price": entry_price, "stop_price": stop_price, "qty": 0, "message": "Calculated quantity is zero", }) continue signal.entry_date = entry["entry_date"].date() if hasattr(entry["entry_date"], "date") else entry["entry_date"] signal.entry_price = entry_price if not execute_orders: signal.status = "watching" results.append({ "ticker": signal.ticker, "success": True, "status": "planned", "entry_price": entry_price, "stop_price": stop_price, "qty": qty, "risk_amount": (entry_price - stop_price) * qty, "message": "Dry-run only. Add execute_orders=true to place KIS order.", }) continue order = executor.place_buy_order(ticker=signal.ticker, qty=qty, price=int(entry_price)) auto_order = AutoOrder( order_date=datetime.now(), ticker=signal.ticker, order_type="buy", qty=qty, price=entry_price, order_no=order.order_no, status="filled" if order.success else "rejected", screening_signal_id=signal.id, ) db.add(auto_order) if order.success: signal.status = "entered" held_tickers.add(signal.ticker) results.append({ "ticker": signal.ticker, "success": order.success, "status": "ordered" if order.success else "rejected", "order_no": order.order_no, "entry_price": entry_price, "stop_price": stop_price, "qty": qty, "message": order.message, }) db.commit() ordered_count = sum(1 for item in results if item.get("status") == "ordered") planned_count = sum(1 for item in results if item.get("status") == "planned") return { "message": f"ordered={ordered_count}, planned={planned_count}, total={len(results)}", "execute_orders": execute_orders, "orders": results, } @router.get("/api/trading/orders", response_model=List[AutoOrderResponse]) async def get_orders( current_user: CurrentUser, db: Session = Depends(get_db), limit: int = Query(100, ge=1, le=1000), ): orders = ( db.query(AutoOrder) .order_by(AutoOrder.order_date.desc()) .limit(limit) .all() ) return orders @router.get("/api/trading/positions", response_model=dict) async def get_trading_positions( current_user: CurrentUser, ): from app.core.config import get_settings from app.services.trading.kis_executor import KISTradeExecutor settings = get_settings() if not settings.kis_app_key or not settings.kis_app_secret: raise HTTPException(status_code=400, detail="KIS API credentials not configured") executor = KISTradeExecutor( app_key=settings.kis_app_key, app_secret=settings.kis_app_secret, account_no=settings.kis_account_no, paper_trade=settings.kis_paper_trade, ) positions = executor.get_positions() return {"positions": [ { "ticker": p.ticker, "name": p.name, "qty": p.qty, "avg_price": p.avg_price, "current_price": p.current_price, "pnl_amount": p.pnl_amount, "pnl_rate": p.pnl_rate, } for p in positions ]} @router.get("/api/trading/balance", response_model=dict) async def get_trading_balance( current_user: CurrentUser, ): from app.core.config import get_settings from app.services.trading.kis_executor import KISTradeExecutor settings = get_settings() if not settings.kis_app_key or not settings.kis_app_secret: raise HTTPException(status_code=400, detail="KIS API credentials not configured") executor = KISTradeExecutor( app_key=settings.kis_app_key, app_secret=settings.kis_app_secret, account_no=settings.kis_account_no, paper_trade=settings.kis_paper_trade, ) balance = executor.get_account_balance() return { "total_amount": balance.total_amount, "available_amount": balance.available_amount, "stock_amount": balance.stock_amount, "pnl_amount": balance.pnl_amount, }