"""All Value Strategy (PER, PBR, PCR, PSR, DY).""" from typing import List, Dict from decimal import Decimal from datetime import datetime from sqlalchemy.orm import Session import pandas as pd from app.strategies.base import BaseStrategy from app.utils.data_helpers import ( get_ticker_list, get_value_indicators, calculate_value_rank, get_prices_on_date ) class AllValueStrategy(BaseStrategy): """ 종합 가치 투자 전략. - PER, PBR, PCR, PSR, DY 5가지 가치 지표 통합 - 낮은 밸류에이션 종목 선정 """ def __init__(self, config: Dict = None): """ 초기화. Args: config: 전략 설정 - count: 선정 종목 수 (기본 20) """ super().__init__(config) self.count = config.get('count', 20) def select_stocks(self, rebal_date: datetime, db_session: Session) -> List[str]: """ 종목 선정. Args: rebal_date: 리밸런싱 날짜 db_session: 데이터베이스 세션 Returns: 선정된 종목 코드 리스트 """ try: # 1. 종목 리스트 조회 ticker_list = get_ticker_list(db_session) if ticker_list.empty: return [] tickers = ticker_list['종목코드'].tolist() # 2. 5가지 밸류 지표 조회 (PER, PBR, DY, PSR, PCR) value_list = get_value_indicators( db_session, tickers, base_date=rebal_date, include_psr_pcr=True ) if value_list.empty: return [] # 3. 가로로 긴 형태로 변경 (pivot) value_pivot = value_list.pivot(index='종목코드', columns='지표', values='값') # 4. 티커 테이블과 가치 지표 테이블 병합 data_bind = ticker_list[['종목코드', '종목명']].merge( value_pivot, how='left', on='종목코드' ) # 5. 5개 지표 중 적어도 3개 이상 있는 종목만 필터링 required_cols = ['PER', 'PBR', 'PCR', 'PSR', 'DY'] available_cols = [col for col in required_cols if col in data_bind.columns] if len(available_cols) < 3: return [] # 최소 3개 이상의 지표가 있는 종목만 data_bind['valid_count'] = data_bind[available_cols].notna().sum(axis=1) data_bind = data_bind[data_bind['valid_count'] >= 3] if data_bind.empty: return [] # 6. 순위 계산 (DY는 높을수록 좋으므로 calculate_value_rank에서 처리) value_sum = calculate_value_rank( data_bind.set_index('종목코드'), available_cols ) # 7. 상위 N개 선정 data_bind['rank'] = value_sum data_bind = data_bind.dropna(subset=['rank']) selected = data_bind.nsmallest(self.count, 'rank') return selected['종목코드'].tolist() except Exception as e: print(f"All Value 전략 종목 선정 오류: {e}") return [] def get_prices( self, tickers: List[str], date: datetime, db_session: Session ) -> Dict[str, Decimal]: """ 종목 가격 조회. Args: tickers: 종목 코드 리스트 date: 조회 날짜 db_session: 데이터베이스 세션 Returns: {ticker: price} 딕셔너리 """ return get_prices_on_date(db_session, tickers, date)