159 lines
4.7 KiB
Python
Raw Permalink Normal View History

2026-01-31 23:30:51 +09:00
"""Super Quality Strategy (F-Score + GPA)."""
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.strategies.factors.f_score import FScoreStrategy
from app.utils.data_helpers import (
get_ticker_list,
get_financial_statements,
get_prices_on_date
)
class SuperQualityStrategy(BaseStrategy):
"""
슈퍼 퀄리티 전략 (F-Score + GPA).
- F-Score 3점인 소형주
- GPA (Gross Profit to Assets) 높은 종목 선정
"""
def __init__(self, config: Dict = None):
"""
초기화.
Args:
config: 전략 설정
- count: 선정 종목 (기본 20)
- min_f_score: 최소 F-Score (기본 3)
- size_filter: 시가총액 필터 (기본 '소형주')
"""
super().__init__(config)
self.count = config.get('count', 20)
self.min_f_score = config.get('min_f_score', 3)
self.size_filter = config.get('size_filter', '소형주')
# F-Score 전략 인스턴스
self.f_score_strategy = FScoreStrategy(config={
'count': 1000, # 많은 종목 선정 (GPA로 필터링)
'min_score': self.min_f_score,
'size_filter': self.size_filter
})
def select_stocks(self, rebal_date: datetime, db_session: Session) -> List[str]:
"""
종목 선정.
Args:
rebal_date: 리밸런싱 날짜
db_session: 데이터베이스 세션
Returns:
선정된 종목 코드 리스트
"""
try:
# 1. F-Score 계산
f_score_df = self.f_score_strategy._calculate_f_score(rebal_date, db_session)
if f_score_df.empty:
return []
# 2. F-Score 3점 & 소형주 필터
filtered = f_score_df[
(f_score_df['f_score'] >= self.min_f_score) &
(f_score_df['분류'] == self.size_filter)
]
if filtered.empty:
print(f"F-Score {self.min_f_score}{self.size_filter} 종목 없음")
return []
# 3. GPA 계산
gpa_df = self._calculate_gpa(rebal_date, db_session, filtered['종목코드'].tolist())
if gpa_df.empty:
return []
# 4. GPA 병합
result = filtered.merge(gpa_df, on='종목코드', how='left')
result['GPA'] = result['GPA'].fillna(-1).astype(float)
# 5. GPA 순으로 상위 N개 종목 선정
top_stocks = result.nlargest(self.count, 'GPA')
print(f"F-Score {self.min_f_score}{self.size_filter}: {len(filtered)}")
print(f"GPA 상위 {self.count}개 선정")
return top_stocks['종목코드'].tolist()
except Exception as e:
print(f"Super Quality 종목 선정 오류: {e}")
return []
def _calculate_gpa(
self,
base_date: datetime,
db_session: Session,
tickers: List[str]
) -> pd.DataFrame:
"""
GPA (Gross Profit to Assets) 계산.
Args:
base_date: 기준일
db_session: 데이터베이스 세션
tickers: 종목 리스트
Returns:
GPA DataFrame
"""
# 재무제표 데이터 조회
fs_list = get_financial_statements(db_session, tickers, base_date)
if fs_list.empty:
return pd.DataFrame()
# 필요한 계정만 필터링
fs_filtered = fs_list[fs_list['계정'].isin(['매출총이익', '자산'])].copy()
if fs_filtered.empty:
return pd.DataFrame()
# Pivot
fs_pivot = fs_filtered.pivot_table(
index='종목코드',
columns='계정',
values='',
aggfunc='first'
)
# GPA 계산
if '매출총이익' in fs_pivot.columns and '자산' in fs_pivot.columns:
fs_pivot['GPA'] = fs_pivot['매출총이익'] / fs_pivot['자산']
else:
fs_pivot['GPA'] = None
return fs_pivot.reset_index()[['종목코드', 'GPA']]
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)