From c5f82a819b678e954186addf3820940c58e91e6e Mon Sep 17 00:00:00 2001 From: Ayuriel Date: Fri, 14 Mar 2025 15:47:19 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=80=84=EB=A6=AC=ED=8B=B0(=EC=9A=B0?= =?UTF-8?q?=EB=9F=89=EC=A3=BC)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/13-quality-portfolio.py | 2 + quantcommon.py | 16 +++++++- streamlit-quant/strategy/quality.py | 57 +++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 streamlit-quant/strategy/quality.py diff --git a/example/13-quality-portfolio.py b/example/13-quality-portfolio.py index 4f4b168..7380c64 100644 --- a/example/13-quality-portfolio.py +++ b/example/13-quality-portfolio.py @@ -4,6 +4,8 @@ import matplotlib.pyplot as plt import seaborn as sns import quantcommon +# strategy/quality에서 구현 + # 퀄리티(우량주) 포트폴리오. 영업수익성이 높은 주식 engine = quantcommon.QuantCommon().create_engine() diff --git a/quantcommon.py b/quantcommon.py index fbb8c14..5a2da0b 100644 --- a/quantcommon.py +++ b/quantcommon.py @@ -67,4 +67,18 @@ class QuantCommon: finally: engine.dispose() - return price_list \ No newline at end of file + return price_list + + def get_fs_list(self): + engine = self.create_engine() + + try: + fs_list = pd.read_sql(""" + select * from kor_fs + where 계정 in ('당기순이익', '매출총이익', '영업활동으로인한현금흐름', '자산', '자본') + and 공시구분 = 'q'; + """, con=engine) + finally: + engine.dispose() + + return fs_list \ No newline at end of file diff --git a/streamlit-quant/strategy/quality.py b/streamlit-quant/strategy/quality.py new file mode 100644 index 0000000..28a6e2e --- /dev/null +++ b/streamlit-quant/strategy/quality.py @@ -0,0 +1,57 @@ +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns +import quantcommon + +# 퀄리티(우량주) 포트폴리오. 영업수익성이 높은 주식 +def get_quality_top(count): + qc = quantcommon.QuantCommon() + ticker_list = qc.get_ticker_list() + fs_list = qc.get_fs_list() + + fs_list = fs_list.sort_values(['종목코드', '계정', '기준일']) + # TTM 값을 구하기 위해서 rolling() 메소드를 통해 4분기 합 구함. 4분기 데이터가 없는 경우 제외하기 위해서 min_periods=4 + fs_list['ttm'] = fs_list.groupby(['종목코드', '계정'], as_index=False)['값'].rolling( + window=4, min_periods=4).sum()['값'] + fs_list_clean = fs_list.copy() + # 자산과 자본은 재무상태표 항목이므로 합이 아닌 평균을 구하며, 나머지 항목은 합을 그대로 사용 + fs_list_clean['ttm'] = np.where(fs_list_clean['계정'].isin(['자산', '자본']), + fs_list_clean['ttm'] / 4, fs_list_clean['ttm']) + # tail(1)을 통해 종목코드와 계정별 최근 데이터만 선택 + fs_list_clean = fs_list_clean.groupby(['종목코드', '계정']).tail(1) + + fs_list_pivot = fs_list_clean.pivot(index='종목코드', columns='계정', values='ttm') + # 수익성 지표에 해당하는 ROE, GPA, CFO 계산 + fs_list_pivot['ROE'] = fs_list_pivot['당기순이익'] / fs_list_pivot['자본'] + fs_list_pivot['GPA'] = fs_list_pivot['매출총이익'] / fs_list_pivot['자산'] + fs_list_pivot['CFO'] = fs_list_pivot['영업활동으로인한현금흐름'] / fs_list_pivot['자산'] + + # 티커 테이블과 합침 + quality_list = ticker_list[['종목코드', '종목명']].merge(fs_list_pivot, + how='left', + on='종목코드') + # quality_list.round(count).head() + + quality_list_copy = quality_list[['ROE', 'GPA', 'CFO']].copy() + quality_rank = quality_list_copy.rank(ascending=False, axis=0) + + mask = np.triu(quality_rank.corr()) + fig, ax = plt.subplots(figsize=(10, 6)) + sns.heatmap(quality_rank.corr(), + annot=True, + mask=mask, + annot_kws={"size": 16}, + vmin=0, + vmax=1, + center=0.5, + cmap='coolwarm', + square=True) + ax.invert_yaxis() + + # 위에서 구한 3개 지표들의 순위를 더한 후 다시 순위를 매김 + quality_sum = quality_rank.sum(axis=1, skipna=False).rank() + # 최종 순위가 낮은 종목 선택 + return quality_list.loc[quality_sum <= count, ['종목코드', '종목명', 'ROE', 'GPA', 'CFO']].round(4) + +if __name__ == '__main__': + print(get_quality_top(20)) \ No newline at end of file