galaxis-po/backend/app/api/screening.py

292 lines
9.7 KiB
Python
Raw Normal View History

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),
2026-05-14 07:52:28 +09:00
execute_orders: bool = Query(False, description="true일 때만 KIS 주문 전송"),
):
from app.core.config import get_settings
from app.services.trading.kis_executor import KISTradeExecutor
2026-05-14 07:52:28 +09:00
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)
2026-05-14 07:52:28 +09:00
.filter(ScreeningSignal.status.in_(["pending", "watching"]))
.all()
)
if not watching:
2026-05-14 07:52:28 +09:00
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,
)
2026-05-14 07:52:28 +09:00
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:
2026-05-14 07:52:28 +09:00
if signal.ticker in held_tickers:
results.append({
"ticker": signal.ticker,
"success": False,
"status": "skipped",
"message": "Already held",
})
continue
2026-05-14 07:52:28 +09:00
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()
)
2026-05-14 07:52:28 +09:00
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(
2026-05-14 07:52:28 +09:00
order_date=datetime.now(),
ticker=signal.ticker,
order_type="buy",
qty=qty,
2026-05-14 07:52:28 +09:00
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"
2026-05-14 07:52:28 +09:00
held_tickers.add(signal.ticker)
results.append({
"ticker": signal.ticker,
"success": order.success,
2026-05-14 07:52:28 +09:00
"status": "ordered" if order.success else "rejected",
"order_no": order.order_no,
2026-05-14 07:52:28 +09:00
"entry_price": entry_price,
"stop_price": stop_price,
"qty": qty,
"message": order.message,
})
db.commit()
2026-05-14 07:52:28 +09:00
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,
}