"""스윙 포지션 관리 -- DB 조회 전용 strategy_type='swing' 필터를 적용해 데이트레이딩 포지션과 완전히 분리. 모든 함수는 읽기 전용 -- 쓰기는 db_worker.py의 큐를 통해서만 수행. DB 테이블 매핑: - Portfolio: 보유 종목 (strategy_type 컬럼으로 swing/daytrading 구분) - ActiveTrade: 활성 매매 (strategy_name 컬럼으로 구분, 분할 청산 상태 추적) 사용 위치: - exit_service.py: 청산 대상 포지션 조회 - entry_service.py: 포지션 한도 체크 - scheduler.py: 스윙 스케줄러 사이클 """ import logging from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.models import Portfolio, ActiveTrade logger = logging.getLogger(__name__) # 스윙 전략 식별자 -- Portfolio.strategy_type과 매칭 STRATEGY_TYPE = "swing" async def get_swing_holdings(db: AsyncSession) -> list: """strategy_type='swing'인 보유 종목 목록 반환. Portfolio 테이블에서 수량 > 0이고 strategy_type='swing'인 레코드 조회. Args: db: AsyncSession 인스턴스 Returns: list[Portfolio]: 스윙 보유 종목 ORM 객체 목록 """ result = await db.execute( select(Portfolio).where( Portfolio.quantity > 0, Portfolio.strategy_type == STRATEGY_TYPE, ) ) return result.scalars().all() async def get_swing_active_trades(db: AsyncSession) -> list: """활성 스윙 ActiveTrade 목록 반환. strategy_name != 'day_trade' 조건으로 데이트레이딩 포지션 제외. 분할 청산 진행 상태(remaining_pct), 최고가(highest_since_entry) 포함. Args: db: AsyncSession 인스턴스 Returns: list[ActiveTrade]: 활성 스윙 매매 ORM 객체 목록 """ result = await db.execute( select(ActiveTrade).where( ActiveTrade.status == "active", ActiveTrade.strategy_name != "day_trade", ) ) return result.scalars().all() async def get_swing_active_trade(db: AsyncSession, stock_code: str) -> ActiveTrade | None: """특정 종목의 스윙 ActiveTrade 반환. 분할 청산 상태(remaining_pct)와 최고가(highest_since_entry) 조회용. 청산 로직에서 전략별 분기(모멘텀/평균회귀)에도 사용. Args: db: AsyncSession 인스턴스 stock_code: 종목 코드 Returns: ActiveTrade | None: 활성 매매 ORM 객체, 없으면 None """ # [2026-09-23] scalar_one_or_none() 은 행이 2개 이상이면 MultipleResultsFound 를 던지고, # 호출측 except 가 이를 삼켜 해당 종목의 청산(손절 포함)이 통째로 스킵됐다. # db_worker legacy 경로와 동일하게 가장 최근 행을 사용한다. result = await db.execute( select(ActiveTrade) .where( ActiveTrade.stock_code == stock_code, ActiveTrade.status == "active", ActiveTrade.strategy_name != "day_trade", ) .order_by(ActiveTrade.id.desc()) .limit(1) ) return result.scalars().first() async def count_swing_positions(db: AsyncSession) -> int: """현재 스윙 보유 종목 수 반환. MAX_SWING_POSITIONS(25) 한도 체크에 사용. Args: db: AsyncSession 인스턴스 Returns: int: 스윙 보유 종목 수 """ from sqlalchemy import func result = await db.execute( select(func.count(Portfolio.id)).where( Portfolio.quantity > 0, Portfolio.strategy_type == STRATEGY_TYPE, ) ) return result.scalar_one() or 0