galaxis-po/backend/app/api/screening.py
머니페니 34d09d9d34
Some checks failed
Deploy to Production / deploy (push) Failing after 6m46s
feat: 김종봉식 KOSPI 종목발굴 전략 구현
- KOSPIMarketStateDetector: KOSPI MA 기반 시장 상태 판단 (bull/neutral/bear/crash)
- VolumeScreener: 거래대금 2000억+ 스크리닝 (상한가 우선, 희소성 체크, 대형주 예외)
- SectorPortfolioManager: 섹터 기반 비중 배분
- KJBScreeningSignalGenerator: 눌림목 진입, 5MA 손절, 단계적 익절
- KISTradeExecutor: KIS API 자동 매수/매도 (기본값 모의투자)
- ScreeningSignal / AutoOrder DB 모델 추가
- screening API 엔드포인트 추가
- 스케줄러 잡 3종 추가 (08:30/5분/15:35)
- Price.trading_value 컬럼 추가
- MarketIndex 테이블 추가 (KOSPI/KOSDAQ 지수 일봉)
- IndexCollector 추가 (일일 수집 잡 등록)
- intraday_exit_check 시간 필터 추가 (09:05~15:20 KST)
- 드라이런 스크립트 추가 (scripts/screening_dryrun.py)
2026-05-05 23:03:53 +09:00

205 lines
6.2 KiB
Python

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),
):
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")
watching = (
db.query(ScreeningSignal)
.filter(ScreeningSignal.status == "watching")
.all()
)
if not watching:
return {"message": "No watching signals to 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,
)
results = []
for signal in watching:
if not signal.entry_price:
continue
qty = 1 # placeholder - actual sizing handled by screening_job
order = executor.place_buy_order(
ticker=signal.ticker,
qty=qty,
price=int(signal.entry_price),
)
auto_order = AutoOrder(
order_date=date.today(),
ticker=signal.ticker,
order_type="buy",
qty=qty,
price=signal.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"
results.append({
"ticker": signal.ticker,
"success": order.success,
"order_no": order.order_no,
"message": order.message,
})
db.commit()
return {"message": f"Executed {len(results)} 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,
}