159 lines
4.7 KiB
Python
159 lines
4.7 KiB
Python
|
|
"""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)
|