Compare commits
4 Commits
f8c8b29531
...
218bf72874
| Author | SHA1 | Date | |
|---|---|---|---|
| 218bf72874 | |||
| 57ba71cb3b | |||
| 23cada68f7 | |||
| 67a9b23fa5 |
124
README.md
124
README.md
@ -1,9 +1,117 @@
|
|||||||
# 실행
|
# 콴트 매니저 (Quant Manager)
|
||||||
streamlit run app.py --server.port=20000
|
|
||||||
|
|
||||||
# pre-
|
한국 주식 시장을 위한 퀀트 투자 도구입니다.
|
||||||
Go to Build Tools for Visual Studio 2017
|
|
||||||
Select free download under Visual Studio Community 2017. This will download the installer. Run the installer.
|
## 프로젝트 구조
|
||||||
Select what you need under workload tab:
|
|
||||||
a. Under Windows, there are three choices. Only check Desktop development with C++.
|
```
|
||||||
b. Under Web & Cloud, there are seven choices. Only check Python development (I believe this is optional, but I have done it).
|
make-quant-py/
|
||||||
|
├── data/ # 데이터 수집 관련 코드
|
||||||
|
│ ├── krx.py # 한국거래소 데이터 수집
|
||||||
|
│ ├── prices.py # 가격 데이터 수집
|
||||||
|
│ ├── financial.py # 재무제표 데이터 수집
|
||||||
|
│ └── crawling.py # 웹 크롤링 유틸리티
|
||||||
|
├── db/ # 데이터베이스 유틸리티
|
||||||
|
│ └── common.py # DB 연결 및 쿼리 공통 기능
|
||||||
|
├── strategies/ # 전략 구현
|
||||||
|
│ ├── factors/ # 개별 요소 모듈
|
||||||
|
│ │ ├── value.py # 가치 전략
|
||||||
|
│ │ ├── quality.py # 퀄리티 전략
|
||||||
|
│ │ ├── momentum.py # 모멘텀 전략
|
||||||
|
│ │ ├── f_score.py # F-Score 전략
|
||||||
|
│ │ └── all_value.py # 종합 가치 전략
|
||||||
|
│ ├── composite/ # 복합 전략 구현
|
||||||
|
│ │ ├── magic_formula.py # 마법공식 전략
|
||||||
|
│ │ ├── multi_factor.py # 멀티팩터 전략
|
||||||
|
│ │ ├── super_quality.py # 슈퍼 퀄리티 전략
|
||||||
|
│ │ └── super_value_momentum.py # 슈퍼 밸류 모멘텀 전략
|
||||||
|
│ └── utils.py # 전략 공통 유틸리티
|
||||||
|
├── backtest/ # 백테스트 도구
|
||||||
|
│ └── engine.py # 백테스트 구현
|
||||||
|
└── streamlit/ # Streamlit 웹 앱
|
||||||
|
├── app.py # 메인 앱
|
||||||
|
├── pages/ # 개별 페이지
|
||||||
|
│ ├── data_page.py # 데이터 수집 페이지
|
||||||
|
│ ├── quality_page.py # 슈퍼 퀄리티 전략 페이지
|
||||||
|
│ └── value_momentum_page.py # 밸류 모멘텀 전략 페이지
|
||||||
|
└── components/ # 재사용 가능한 UI 컴포넌트
|
||||||
|
└── charts.py # 시각화 컴포넌트
|
||||||
|
```
|
||||||
|
|
||||||
|
## 설치 및 설정
|
||||||
|
|
||||||
|
### 필수 요구사항
|
||||||
|
|
||||||
|
- Python 3.8 이상
|
||||||
|
- MySQL 또는 MariaDB
|
||||||
|
|
||||||
|
Windows에서 필요한 빌드 도구:
|
||||||
|
- Visual Studio Build Tools 2017 이상
|
||||||
|
- Desktop development with C++ 워크로드 선택
|
||||||
|
- (선택사항) Python development 워크로드 선택
|
||||||
|
|
||||||
|
### 가상환경 설정
|
||||||
|
|
||||||
|
가상환경 생성:
|
||||||
|
```
|
||||||
|
python -m venv .venv
|
||||||
|
```
|
||||||
|
|
||||||
|
가상환경 활성화:
|
||||||
|
- Windows: `.venv\Scripts\activate`
|
||||||
|
- Linux/Mac: `. .venv/bin/activate`
|
||||||
|
|
||||||
|
패키지 설치:
|
||||||
|
```
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
가상환경 종료:
|
||||||
|
```
|
||||||
|
deactivate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 환경 변수 설정
|
||||||
|
|
||||||
|
프로젝트 루트에 `.env` 파일을 생성하고 다음 환경 변수를 설정하세요:
|
||||||
|
|
||||||
|
```
|
||||||
|
DB_USER=your_db_user
|
||||||
|
DB_PW=your_db_password
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DB=your_db_name
|
||||||
|
```
|
||||||
|
|
||||||
|
## 실행 방법
|
||||||
|
|
||||||
|
Streamlit 앱 실행:
|
||||||
|
```
|
||||||
|
streamlit run src/app.py --server.port=20000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 모듈
|
||||||
|
|
||||||
|
### 데이터 수집
|
||||||
|
|
||||||
|
- `data/krx.py`: 한국거래소(KRX)에서 주식 데이터를 수집합니다.
|
||||||
|
- `data/prices.py`: 네이버 금융에서 주가 데이터를 수집합니다.
|
||||||
|
- `data/financial.py`: FnGuide에서 재무제표 데이터를 수집합니다.
|
||||||
|
|
||||||
|
### 투자 전략
|
||||||
|
|
||||||
|
#### 개별 요소 전략
|
||||||
|
- `strategies/factors/value.py`: PER, PBR 등 가치 지표 기반 전략
|
||||||
|
- `strategies/factors/quality.py`: ROE, GPA 등 퀄리티 지표 기반 전략
|
||||||
|
- `strategies/factors/momentum.py`: 주가 모멘텀 기반 전략
|
||||||
|
- `strategies/factors/f_score.py`: Piotroski의 F-Score 계산 전략
|
||||||
|
|
||||||
|
#### 복합 전략
|
||||||
|
- `strategies/composite/magic_formula.py`: Joel Greenblatt의 마법공식 전략
|
||||||
|
- `strategies/composite/multi_factor.py`: 다중 요소 통합 전략
|
||||||
|
- `strategies/composite/super_quality.py`: F-Score와 GP/A를 결합한 슈퍼 퀄리티 전략
|
||||||
|
- `strategies/composite/super_value_momentum.py`: 가치와 모멘텀을 결합한 전략
|
||||||
|
|
||||||
|
### 백테스트
|
||||||
|
|
||||||
|
- `backtest/engine.py`: bt 패키지를 사용한 백테스트 기능 구현
|
||||||
@ -1,81 +0,0 @@
|
|||||||
# 패키지 불러오기
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from streamlit_quant import quantcommon
|
|
||||||
|
|
||||||
# DB 연결
|
|
||||||
common = quantcommon.QuantCommon()
|
|
||||||
engine = common.create_engine()
|
|
||||||
con = common.connect()
|
|
||||||
|
|
||||||
mycursor = con.cursor()
|
|
||||||
# 가치 지표 계산
|
|
||||||
# 분기 재무제표 불러오기
|
|
||||||
kor_fs = pd.read_sql("""
|
|
||||||
select * from kor_fs
|
|
||||||
where 공시구분 = 'q'
|
|
||||||
and 계정 in ('당기순이익', '자본', '영업활동으로인한현금흐름', '매출액');
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
# 티커 리스트 불러오기
|
|
||||||
ticker_list = pd.read_sql("""
|
|
||||||
select * from kor_ticker
|
|
||||||
where 기준일 = (select max(기준일) from kor_ticker)
|
|
||||||
and 종목구분 = '보통주';
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
engine.dispose()
|
|
||||||
|
|
||||||
# TTM 구하기
|
|
||||||
kor_fs = kor_fs.sort_values(['종목코드', '계정', '기준일'])
|
|
||||||
kor_fs['ttm'] = kor_fs.groupby(['종목코드', '계정'], as_index=False)['값'].rolling(
|
|
||||||
window=4, min_periods=4).sum()['값']
|
|
||||||
|
|
||||||
# 자본은 평균 구하기
|
|
||||||
kor_fs['ttm'] = np.where(kor_fs['계정'] == '자본', kor_fs['ttm'] / 4,
|
|
||||||
kor_fs['ttm'])
|
|
||||||
kor_fs = kor_fs.groupby(['계정', '종목코드']).tail(1)
|
|
||||||
|
|
||||||
kor_fs_merge = kor_fs[['계정', '종목코드',
|
|
||||||
'ttm']].merge(ticker_list[['종목코드', '시가총액', '기준일']],
|
|
||||||
on='종목코드')
|
|
||||||
kor_fs_merge['시가총액'] = kor_fs_merge['시가총액'] / 100000000
|
|
||||||
|
|
||||||
kor_fs_merge['value'] = kor_fs_merge['시가총액'] / kor_fs_merge['ttm']
|
|
||||||
kor_fs_merge['value'] = kor_fs_merge['value'].round(4)
|
|
||||||
kor_fs_merge['지표'] = np.where(
|
|
||||||
kor_fs_merge['계정'] == '매출액', 'PSR',
|
|
||||||
np.where(
|
|
||||||
kor_fs_merge['계정'] == '영업활동으로인한현금흐름', 'PCR',
|
|
||||||
np.where(kor_fs_merge['계정'] == '자본', 'PBR',
|
|
||||||
np.where(kor_fs_merge['계정'] == '당기순이익', 'PER', None))))
|
|
||||||
|
|
||||||
kor_fs_merge.rename(columns={'value': '값'}, inplace=True)
|
|
||||||
kor_fs_merge = kor_fs_merge[['종목코드', '기준일', '지표', '값']]
|
|
||||||
kor_fs_merge = kor_fs_merge.replace([np.inf, -np.inf, np.nan], None)
|
|
||||||
|
|
||||||
query = """
|
|
||||||
insert into kor_value (종목코드, 기준일, 지표, 값)
|
|
||||||
values (%s,%s,%s,%s) as new
|
|
||||||
on duplicate key update
|
|
||||||
값=new.값
|
|
||||||
"""
|
|
||||||
|
|
||||||
args_fs = kor_fs_merge.values.tolist()
|
|
||||||
mycursor.executemany(query, args_fs)
|
|
||||||
con.commit()
|
|
||||||
|
|
||||||
ticker_list['값'] = ticker_list['주당배당금'] / ticker_list['종가']
|
|
||||||
ticker_list['값'] = ticker_list['값'].round(4)
|
|
||||||
ticker_list['지표'] = 'DY'
|
|
||||||
dy_list = ticker_list[['종목코드', '기준일', '지표', '값']]
|
|
||||||
dy_list = dy_list.replace([np.inf, -np.inf, np.nan], None)
|
|
||||||
dy_list = dy_list[dy_list['값'] != 0]
|
|
||||||
|
|
||||||
args_dy = dy_list.values.tolist()
|
|
||||||
mycursor.executemany(query, args_dy)
|
|
||||||
con.commit()
|
|
||||||
|
|
||||||
engine.dispose()
|
|
||||||
con.close()
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
import requests as rq
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
from streamlit_quant import quantcommon
|
|
||||||
|
|
||||||
# DB 연결
|
|
||||||
common = quantcommon.QuantCommon()
|
|
||||||
engine = common.create_engine()
|
|
||||||
con = common.connect()
|
|
||||||
|
|
||||||
mycursor = con.cursor()
|
|
||||||
|
|
||||||
# 재무제표 크롤링
|
|
||||||
# 티커리스트 불러오기
|
|
||||||
ticker_list = pd.read_sql("""
|
|
||||||
select * from kor_ticker
|
|
||||||
where 기준일 = (select max(기준일) from kor_ticker)
|
|
||||||
and 종목구분 = '보통주';
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
# DB 저장 쿼리
|
|
||||||
query = """
|
|
||||||
insert into kor_fs (계정, 기준일, 값, 종목코드, 공시구분)
|
|
||||||
values (%s,%s,%s,%s,%s) as new
|
|
||||||
on duplicate key update
|
|
||||||
값=new.값
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 오류 발생시 저장할 리스트 생성
|
|
||||||
error_list = []
|
|
||||||
|
|
||||||
|
|
||||||
# 재무제표 클렌징 함수
|
|
||||||
def clean_fs(df, ticker, frequency):
|
|
||||||
|
|
||||||
df = df[~df.loc[:, ~df.columns.isin(['계정'])].isna().all(axis=1)]
|
|
||||||
df = df.drop_duplicates(['계정'], keep='first')
|
|
||||||
df = pd.melt(df, id_vars='계정', var_name='기준일', value_name='값')
|
|
||||||
df = df[~pd.isnull(df['값'])]
|
|
||||||
df['계정'] = df['계정'].replace({'계산에 참여한 계정 펼치기': ''}, regex=True)
|
|
||||||
print(df)
|
|
||||||
df['기준일'] = pd.to_datetime(df['기준일'],
|
|
||||||
format='%Y/%m') + pd.tseries.offsets.MonthEnd()
|
|
||||||
df['종목코드'] = ticker
|
|
||||||
df['공시구분'] = frequency
|
|
||||||
|
|
||||||
return df
|
|
||||||
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
ticker = ticker_list['종목코드'][i]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# url 생성
|
|
||||||
url = f'https://comp.fnguide.com/SVO2/ASP/SVD_Finance.asp?pGB=1&gicode=A{ticker}'
|
|
||||||
|
|
||||||
# 데이터 받아오기
|
|
||||||
data = pd.read_html(url, displayed_only=False)
|
|
||||||
|
|
||||||
# print([item.head(3) for item in data])
|
|
||||||
# 연간 데이터
|
|
||||||
data_fs_y = pd.concat([
|
|
||||||
data[0].iloc[:, ~data[0].columns.str.contains('전년동기')], data[2],
|
|
||||||
data[4]
|
|
||||||
])
|
|
||||||
data_fs_y = data_fs_y.rename(columns={data_fs_y.columns[0]: "계정"})
|
|
||||||
|
|
||||||
# 결산년 찾기
|
|
||||||
page_data = rq.get(url)
|
|
||||||
page_data_html = BeautifulSoup(page_data.content, 'html.parser')
|
|
||||||
|
|
||||||
fiscal_data = page_data_html.select('div.corp_group1 > h2')
|
|
||||||
fiscal_data_text = fiscal_data[1].text
|
|
||||||
fiscal_data_text = re.findall('[0-9]+', fiscal_data_text)
|
|
||||||
|
|
||||||
# 결산년에 해당하는 계정만 남기기
|
|
||||||
data_fs_y = data_fs_y.loc[:, (data_fs_y.columns == '계정') | (
|
|
||||||
data_fs_y.columns.str[-2:].isin(fiscal_data_text))]
|
|
||||||
|
|
||||||
# 클렌징
|
|
||||||
data_fs_y_clean = clean_fs(data_fs_y, ticker, 'y')
|
|
||||||
|
|
||||||
# 분기 데이터
|
|
||||||
data_fs_q = pd.concat([
|
|
||||||
data[1].iloc[:, ~data[1].columns.str.contains('전년동기')], data[3],
|
|
||||||
data[5]
|
|
||||||
])
|
|
||||||
data_fs_q = data_fs_q.rename(columns={data_fs_q.columns[0]: "계정"})
|
|
||||||
|
|
||||||
data_fs_q_clean = clean_fs(data_fs_q, ticker, 'q')
|
|
||||||
|
|
||||||
# 두개 합치기
|
|
||||||
data_fs_bind = pd.concat([data_fs_y_clean, data_fs_q_clean])
|
|
||||||
print(data_fs_bind.head(3))
|
|
||||||
|
|
||||||
# 재무제표 데이터를 DB에 저장
|
|
||||||
args = data_fs_bind.values.tolist()
|
|
||||||
mycursor.executemany(query, args)
|
|
||||||
con.commit()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
|
|
||||||
# 오류 발생시 해당 종목명을 저장하고 다음 루프로 이동
|
|
||||||
print(e)
|
|
||||||
error_list.append(ticker)
|
|
||||||
|
|
||||||
# DB 연결 종료
|
|
||||||
engine.dispose()
|
|
||||||
con.close()
|
|
||||||
@ -1,120 +0,0 @@
|
|||||||
import re
|
|
||||||
import time
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
import requests as rq
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
from streamlit_quant import quantcommon
|
|
||||||
|
|
||||||
# src/current-financial-statement.py 로 개선
|
|
||||||
# DB 연결
|
|
||||||
common = quantcommon.QuantCommon()
|
|
||||||
engine = common.create_engine()
|
|
||||||
con = common.connect()
|
|
||||||
|
|
||||||
mycursor = con.cursor()
|
|
||||||
|
|
||||||
# 재무제표 크롤링
|
|
||||||
# 티커리스트 불러오기
|
|
||||||
ticker_list = pd.read_sql("""
|
|
||||||
select * from kor_ticker
|
|
||||||
where 기준일 = (select max(기준일) from kor_ticker)
|
|
||||||
and 종목구분 = '보통주';
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
# DB 저장 쿼리
|
|
||||||
query = """
|
|
||||||
insert into kor_fs (계정, 기준일, 값, 종목코드, 공시구분)
|
|
||||||
values (%s,%s,%s,%s,%s) as new
|
|
||||||
on duplicate key update
|
|
||||||
값=new.값
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 오류 발생시 저장할 리스트 생성
|
|
||||||
error_list = []
|
|
||||||
|
|
||||||
|
|
||||||
# 재무제표 클렌징 함수
|
|
||||||
def clean_fs(df, ticker, frequency):
|
|
||||||
|
|
||||||
df = df[~df.loc[:, ~df.columns.isin(['계정'])].isna().all(axis=1)]
|
|
||||||
df = df.drop_duplicates(['계정'], keep='first')
|
|
||||||
df = pd.melt(df, id_vars='계정', var_name='기준일', value_name='값')
|
|
||||||
df = df[~pd.isnull(df['값'])]
|
|
||||||
df['계정'] = df['계정'].replace({'계산에 참여한 계정 펼치기': ''}, regex=True)
|
|
||||||
df['기준일'] = pd.to_datetime(df['기준일'],
|
|
||||||
format='%Y/%m') + pd.tseries.offsets.MonthEnd()
|
|
||||||
df['종목코드'] = ticker
|
|
||||||
df['공시구분'] = frequency
|
|
||||||
|
|
||||||
return df
|
|
||||||
|
|
||||||
|
|
||||||
# for loop
|
|
||||||
for i in tqdm(range(0, len(ticker_list))):
|
|
||||||
|
|
||||||
# 티커 선택
|
|
||||||
ticker = ticker_list['종목코드'][i]
|
|
||||||
|
|
||||||
# 오류 발생 시 이를 무시하고 다음 루프로 진행
|
|
||||||
try:
|
|
||||||
|
|
||||||
# url 생성
|
|
||||||
url = f'https://comp.fnguide.com/SVO2/ASP/SVD_Finance.asp?pGB=1&gicode=A{ticker}'
|
|
||||||
|
|
||||||
# 데이터 받아오기
|
|
||||||
data = pd.read_html(url, displayed_only=False)
|
|
||||||
|
|
||||||
# 연간 데이터
|
|
||||||
data_fs_y = pd.concat([
|
|
||||||
data[0].iloc[:, ~data[0].columns.str.contains('전년동기')], data[2],
|
|
||||||
data[4]
|
|
||||||
])
|
|
||||||
data_fs_y = data_fs_y.rename(columns={data_fs_y.columns[0]: "계정"})
|
|
||||||
|
|
||||||
# 결산년 찾기
|
|
||||||
page_data = rq.get(url)
|
|
||||||
page_data_html = BeautifulSoup(page_data.content, 'html.parser')
|
|
||||||
|
|
||||||
fiscal_data = page_data_html.select('div.corp_group1 > h2')
|
|
||||||
fiscal_data_text = fiscal_data[1].text
|
|
||||||
fiscal_data_text = re.findall('[0-9]+', fiscal_data_text)
|
|
||||||
|
|
||||||
# 결산년에 해당하는 계정만 남기기
|
|
||||||
data_fs_y = data_fs_y.loc[:, (data_fs_y.columns == '계정') | (
|
|
||||||
data_fs_y.columns.str[-2:].isin(fiscal_data_text))]
|
|
||||||
|
|
||||||
# 클렌징
|
|
||||||
data_fs_y_clean = clean_fs(data_fs_y, ticker, 'y')
|
|
||||||
|
|
||||||
# 분기 데이터
|
|
||||||
data_fs_q = pd.concat([
|
|
||||||
data[1].iloc[:, ~data[1].columns.str.contains('전년동기')], data[3],
|
|
||||||
data[5]
|
|
||||||
])
|
|
||||||
data_fs_q = data_fs_q.rename(columns={data_fs_q.columns[0]: "계정"})
|
|
||||||
|
|
||||||
data_fs_q_clean = clean_fs(data_fs_q, ticker, 'q')
|
|
||||||
|
|
||||||
# 두개 합치기
|
|
||||||
data_fs_bind = pd.concat([data_fs_y_clean, data_fs_q_clean])
|
|
||||||
|
|
||||||
# 재무제표 데이터를 DB에 저장
|
|
||||||
args = data_fs_bind.values.tolist()
|
|
||||||
mycursor.executemany(query, args)
|
|
||||||
con.commit()
|
|
||||||
|
|
||||||
except:
|
|
||||||
|
|
||||||
# 오류 발생시 해당 종목명을 저장하고 다음 루프로 이동
|
|
||||||
print(ticker)
|
|
||||||
error_list.append(ticker)
|
|
||||||
|
|
||||||
# 타임슬립 적용
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
# DB 연결 종료
|
|
||||||
engine.dispose()
|
|
||||||
con.close()
|
|
||||||
@ -1,193 +0,0 @@
|
|||||||
import re
|
|
||||||
import time
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
import requests as rq
|
|
||||||
from tqdm import tqdm
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from streamlit_quant import quantcommon
|
|
||||||
|
|
||||||
# src/current-stock.py 로 개선
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
GEN_OTP_URL = 'http://data.krx.co.kr/comm/fileDn/GenerateOTP/generate.cmd'
|
|
||||||
DOWN_URL = 'http://data.krx.co.kr/comm/fileDn/download_csv/download.cmd'
|
|
||||||
|
|
||||||
# 최근 영업일을 가져옴
|
|
||||||
def get_latest_biz_day():
|
|
||||||
url = 'https://finance.naver.com/sise/sise_deposit.nhn'
|
|
||||||
data = rq.post(url)
|
|
||||||
data_html = BeautifulSoup(data.content, 'lxml')
|
|
||||||
parse_day = data_html.select_one('div.subtop_sise_graph2 > ul.subtop_chart_note > li > span.tah').text
|
|
||||||
biz_day = re.findall('[0-9]+', parse_day)
|
|
||||||
biz_day = ''.join(biz_day)
|
|
||||||
return biz_day
|
|
||||||
|
|
||||||
|
|
||||||
# 업종 분류 현황 가져옴
|
|
||||||
def get_stock_data(biz_day, mkt_id):
|
|
||||||
# logging.basicConfig(level=logging.DEBUG)
|
|
||||||
gen_otp_data = {
|
|
||||||
'locale': 'ko_KR',
|
|
||||||
'mktId': mkt_id, # STK: 코스피, KSQ: 코스닥
|
|
||||||
'trdDd': biz_day,
|
|
||||||
'money': '1',
|
|
||||||
'csvxls_isNo': 'false',
|
|
||||||
'name': 'fileDown',
|
|
||||||
'url': 'dbms/MDC/STAT/standard/MDCSTAT03901'
|
|
||||||
}
|
|
||||||
headers = {
|
|
||||||
'Referer': 'http://data.krx.co.kr/contents/MDC/MDI/mdiLoader/index.cmd?menuId=MDC0201050201',
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
|
|
||||||
}
|
|
||||||
|
|
||||||
otp = rq.post(url=GEN_OTP_URL, data=gen_otp_data, headers=headers, verify=False)
|
|
||||||
# # 요청 디버깅
|
|
||||||
# print("===== Request Details =====")
|
|
||||||
# print(f"Method: {otp.request.method}")
|
|
||||||
# print(f"URL: {otp.request.url}")
|
|
||||||
# print(f"Headers: {otp.request.headers}")
|
|
||||||
# print(f"Body: {otp.request.body}")
|
|
||||||
#
|
|
||||||
# # 응답 디버깅
|
|
||||||
# print("===== Response Details =====")
|
|
||||||
# print(f"Status Code: {otp.status_code}")
|
|
||||||
# print(f"Headers: {otp.headers}")
|
|
||||||
# print(f"Body: {otp.text}")
|
|
||||||
|
|
||||||
down_sector = rq.post(url=DOWN_URL, data={'code': otp.text}, headers=headers)
|
|
||||||
return pd.read_csv(BytesIO(down_sector.content), encoding='EUC-KR')
|
|
||||||
|
|
||||||
|
|
||||||
# 개별 지표 조회
|
|
||||||
def get_ind_stock_data(biz_day):
|
|
||||||
gen_otp_data = {
|
|
||||||
'locale': 'ko_KR',
|
|
||||||
'searchType': '1',
|
|
||||||
'mktId': 'ALL',
|
|
||||||
'trdDd': biz_day,
|
|
||||||
'csvxls_isNo': 'false',
|
|
||||||
'name': 'fileDown',
|
|
||||||
'url': 'dbms/MDC/STAT/standard/MDCSTAT03501'
|
|
||||||
}
|
|
||||||
headers = {
|
|
||||||
'Referer': 'http://data.krx.co.kr/contents/MDC/MDI/mdiLoader/index.cmd?menuId=MDC0201050201',
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
|
|
||||||
}
|
|
||||||
|
|
||||||
otp = rq.post(url=GEN_OTP_URL, data=gen_otp_data, headers=headers, verify=False)
|
|
||||||
|
|
||||||
down_ind_sector = rq.post(url=DOWN_URL, data={'code': otp.text}, headers=headers)
|
|
||||||
return pd.read_csv(BytesIO(down_ind_sector.content), encoding='EUC-KR')
|
|
||||||
|
|
||||||
|
|
||||||
def process_for_total_stock(biz_day):
|
|
||||||
# 업종 분류 현황(코스피, 코스닥)
|
|
||||||
sector_stk = get_stock_data(biz_day, 'STK')
|
|
||||||
sector_ksq = get_stock_data(biz_day, 'KSQ')
|
|
||||||
# 각각 조회 후 합침
|
|
||||||
krx_sector = pd.concat([sector_stk, sector_ksq]).reset_index(drop=True)
|
|
||||||
krx_sector['종목명'] = krx_sector['종목명'].str.strip()
|
|
||||||
krx_sector['기준일'] = biz_day
|
|
||||||
|
|
||||||
# 개별 지표 조회
|
|
||||||
krx_ind = get_ind_stock_data(biz_day)
|
|
||||||
krx_ind['종목명'] = krx_ind['종목명'].str.strip()
|
|
||||||
krx_ind['기준일'] = biz_day
|
|
||||||
|
|
||||||
# 데이터 정리
|
|
||||||
# 종목, 개별 중 한군데만 있는 데이터 삭제(선박펀드, 광물펀드, 해외종목 등)
|
|
||||||
diff = list(set(krx_sector['종목명']).symmetric_difference(set(krx_ind['종목명'])))
|
|
||||||
|
|
||||||
kor_ticker = pd.merge(krx_sector,
|
|
||||||
krx_ind,
|
|
||||||
on=krx_sector.columns.intersection(
|
|
||||||
krx_ind.columns).tolist(),
|
|
||||||
how='outer')
|
|
||||||
# 일반적인 종목과 SPAC, 우선주, 리츠, 기타 주식을 구분
|
|
||||||
kor_ticker['종목구분'] = np.where(kor_ticker['종목명'].str.contains('스팩|제[0-9]+호'), '스팩',
|
|
||||||
np.where(kor_ticker['종목코드'].str[-1:] != '0', '우선주',
|
|
||||||
np.where(kor_ticker['종목명'].str.endswith('리츠'), '리츠',
|
|
||||||
np.where(kor_ticker['종목명'].isin(diff), '기타',
|
|
||||||
'보통주'))))
|
|
||||||
kor_ticker = kor_ticker.reset_index(drop=True)
|
|
||||||
kor_ticker.columns = kor_ticker.columns.str.replace(' ', '')
|
|
||||||
kor_ticker = kor_ticker[['종목코드', '종목명', '시장구분', '종가',
|
|
||||||
'시가총액', '기준일', 'EPS', '선행EPS', 'BPS', '주당배당금', '종목구분']]
|
|
||||||
kor_ticker = kor_ticker.replace({np.nan: None})
|
|
||||||
kor_ticker['기준일'] = pd.to_datetime(kor_ticker['기준일'])
|
|
||||||
|
|
||||||
save_ticker(kor_ticker)
|
|
||||||
|
|
||||||
|
|
||||||
def save_ticker(ticker):
|
|
||||||
con = quantcommon.QuantCommon().connect()
|
|
||||||
|
|
||||||
mycursor = con.cursor()
|
|
||||||
query = f"""
|
|
||||||
insert into kor_ticker (종목코드,종목명,시장구분,종가,시가총액,기준일,EPS,선행EPS,BPS,주당배당금,종목구분)
|
|
||||||
values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) as new
|
|
||||||
on duplicate key update
|
|
||||||
종목명=new.종목명,시장구분=new.시장구분,종가=new.종가,시가총액=new.시가총액,EPS=new.EPS,선행EPS=new.선행EPS,
|
|
||||||
BPS=new.BPS,주당배당금=new.주당배당금,종목구분 = new.종목구분;
|
|
||||||
"""
|
|
||||||
|
|
||||||
args = ticker.values.tolist()
|
|
||||||
|
|
||||||
mycursor.executemany(query, args)
|
|
||||||
con.commit()
|
|
||||||
|
|
||||||
con.close()
|
|
||||||
|
|
||||||
|
|
||||||
# WICS 기준 섹터정보 크롤링
|
|
||||||
def process_for_wics(biz_day):
|
|
||||||
sector_code = [
|
|
||||||
'G25', 'G35', 'G50', 'G40', 'G10', 'G20', 'G55', 'G30', 'G15', 'G45'
|
|
||||||
]
|
|
||||||
|
|
||||||
data_sector = []
|
|
||||||
|
|
||||||
# 모든 섹터에 대한 데이터 받아서 가공
|
|
||||||
for i in tqdm(sector_code):
|
|
||||||
url = f'''http://www.wiseindex.com/Index/GetIndexComponets?ceil_yn=0&dt={biz_day}&sec_cd={i}'''
|
|
||||||
data = rq.get(url).json()
|
|
||||||
data_pd = pd.json_normalize(data['list'])
|
|
||||||
|
|
||||||
data_sector.append(data_pd)
|
|
||||||
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
kor_sector = pd.concat(data_sector, axis=0)
|
|
||||||
kor_sector = kor_sector[['IDX_CD', 'CMP_CD', 'CMP_KOR', 'SEC_NM_KOR']]
|
|
||||||
kor_sector['기준일'] = biz_day
|
|
||||||
kor_sector['기준일'] = pd.to_datetime(kor_sector['기준일'])
|
|
||||||
save_sector(kor_sector)
|
|
||||||
|
|
||||||
|
|
||||||
def save_sector(sector):
|
|
||||||
con = quantcommon.QuantCommon().connect()
|
|
||||||
|
|
||||||
mycursor = con.cursor()
|
|
||||||
query = f"""
|
|
||||||
insert into kor_sector (IDX_CD, CMP_CD, CMP_KOR, SEC_NM_KOR, 기준일)
|
|
||||||
values (%s,%s,%s,%s,%s) as new
|
|
||||||
on duplicate key update
|
|
||||||
IDX_CD = new.IDX_CD, CMP_KOR = new.CMP_KOR, SEC_NM_KOR = new.SEC_NM_KOR
|
|
||||||
"""
|
|
||||||
|
|
||||||
args = sector.values.tolist()
|
|
||||||
|
|
||||||
mycursor.executemany(query, args)
|
|
||||||
con.commit()
|
|
||||||
|
|
||||||
con.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
latest_biz_day = get_latest_biz_day()
|
|
||||||
process_for_total_stock(latest_biz_day)
|
|
||||||
process_for_wics(latest_biz_day)
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
# 패키지 불러오기
|
|
||||||
|
|
||||||
import time
|
|
||||||
from datetime import date
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
import requests as rq
|
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
from streamlit_quant import quantcommon
|
|
||||||
|
|
||||||
# src/current-price.py 로 개선
|
|
||||||
|
|
||||||
# DB 연결
|
|
||||||
common = quantcommon.QuantCommon()
|
|
||||||
engine = common.create_engine()
|
|
||||||
con = common.connect()
|
|
||||||
|
|
||||||
mycursor = con.cursor()
|
|
||||||
|
|
||||||
# 티커리스트 불러오기
|
|
||||||
ticker_list = pd.read_sql("""
|
|
||||||
select * from kor_ticker
|
|
||||||
where 기준일 = (select max(기준일) from kor_ticker)
|
|
||||||
and 종목구분 = '보통주';
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
# DB 저장 쿼리
|
|
||||||
query = """
|
|
||||||
insert into kor_price (날짜, 시가, 고가, 저가, 종가, 거래량, 종목코드)
|
|
||||||
values (%s,%s,%s,%s,%s,%s,%s) as new
|
|
||||||
on duplicate key update
|
|
||||||
시가 = new.시가, 고가 = new.고가, 저가 = new.저가,
|
|
||||||
종가 = new.종가, 거래량 = new.거래량;
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 오류 발생시 저장할 리스트 생성
|
|
||||||
error_list = []
|
|
||||||
|
|
||||||
# 전종목 주가 다운로드 및 저장
|
|
||||||
for i in tqdm(range(0, len(ticker_list))):
|
|
||||||
|
|
||||||
# 티커 선택
|
|
||||||
ticker = ticker_list['종목코드'][i]
|
|
||||||
|
|
||||||
# 시작일과 종료일
|
|
||||||
fr = (date.today() + relativedelta(years=-5)).strftime("%Y%m%d")
|
|
||||||
to = (date.today()).strftime("%Y%m%d")
|
|
||||||
|
|
||||||
# 오류 발생 시 이를 무시하고 다음 루프로 진행
|
|
||||||
try:
|
|
||||||
|
|
||||||
# url 생성
|
|
||||||
url = f'''https://fchart.stock.naver.com/siseJson.nhn?symbol={ticker}&requestType=1
|
|
||||||
&startTime={fr}&endTime={to}&timeframe=day'''
|
|
||||||
|
|
||||||
# 데이터 다운로드
|
|
||||||
data = rq.get(url).content
|
|
||||||
data_price = pd.read_csv(BytesIO(data))
|
|
||||||
|
|
||||||
# 데이터 클렌징
|
|
||||||
price = data_price.iloc[:, 0:6]
|
|
||||||
price.columns = ['날짜', '시가', '고가', '저가', '종가', '거래량']
|
|
||||||
price = price.dropna()
|
|
||||||
price['날짜'] = price['날짜'].str.extract("(\d+)")
|
|
||||||
price['날짜'] = pd.to_datetime(price['날짜'])
|
|
||||||
price['종목코드'] = ticker
|
|
||||||
|
|
||||||
# 주가 데이터를 DB에 저장
|
|
||||||
args = price.values.tolist()
|
|
||||||
mycursor.executemany(query, args)
|
|
||||||
con.commit()
|
|
||||||
|
|
||||||
except:
|
|
||||||
|
|
||||||
# 오류 발생시 error_list에 티커 저장하고 넘어가기
|
|
||||||
print(ticker)
|
|
||||||
error_list.append(ticker)
|
|
||||||
|
|
||||||
# 타임슬립 적용
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
# DB 연결 종료
|
|
||||||
engine.dispose()
|
|
||||||
con.close()
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
import pandas as pd
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import seaborn as sns
|
|
||||||
import statsmodels.api as sm
|
|
||||||
import numpy as np
|
|
||||||
from streamlit_quant import quantcommon
|
|
||||||
|
|
||||||
# strategy/momentum에 구현
|
|
||||||
# 모멘텀 포트폴리오. 최근 12개월 수익률이 높은 주식
|
|
||||||
engine = quantcommon.QuantCommon().create_engine()
|
|
||||||
|
|
||||||
ticker_list = pd.read_sql(
|
|
||||||
"""
|
|
||||||
select * from kor_ticker
|
|
||||||
where 기준일 = (select max(기준일) from kor_ticker)
|
|
||||||
and 종목구분 = '보통주';
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
|
|
||||||
price_list = pd.read_sql(
|
|
||||||
"""
|
|
||||||
select 날짜, 종가, 종목코드
|
|
||||||
from kor_price
|
|
||||||
where 날짜 >= (select (select max(날짜) from kor_price) - interval 1 year);
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
engine.dispose()
|
|
||||||
|
|
||||||
price_pivot = price_list.pivot(index='날짜', columns='종목코드', values='종가')
|
|
||||||
|
|
||||||
ret_list = pd.DataFrame(data=(price_pivot.iloc[-1] / price_pivot.iloc[0]) - 1,
|
|
||||||
columns=['return'])
|
|
||||||
data_bind = ticker_list[['종목코드', '종목명']].merge(ret_list, how='left', on='종목코드')
|
|
||||||
|
|
||||||
momentum_rank = data_bind['return'].rank(axis=0, ascending=False)
|
|
||||||
|
|
||||||
price_momentum = price_list[price_list['종목코드'].isin(
|
|
||||||
data_bind.loc[momentum_rank <= 20, '종목코드'])]
|
|
||||||
|
|
||||||
plt.rc('font', family='Malgun Gothic')
|
|
||||||
g = sns.relplot(data=price_momentum,
|
|
||||||
x='날짜',
|
|
||||||
y='종가',
|
|
||||||
col='종목코드',
|
|
||||||
col_wrap=5,
|
|
||||||
kind='line',
|
|
||||||
facet_kws={
|
|
||||||
'sharey': False,
|
|
||||||
'sharex': True
|
|
||||||
})
|
|
||||||
g.set(xticklabels=[])
|
|
||||||
g.set(xlabel=None)
|
|
||||||
g.set(ylabel=None)
|
|
||||||
g.fig.set_figwidth(15)
|
|
||||||
g.fig.set_figheight(8)
|
|
||||||
plt.subplots_adjust(wspace=0.5, hspace=0.2)
|
|
||||||
# plt.show()
|
|
||||||
|
|
||||||
# k-ratio(모멘텀의 꾸준함 지표)
|
|
||||||
ret = price_pivot.pct_change().iloc[1:]
|
|
||||||
ret_cum = np.log(1 + ret).cumsum()
|
|
||||||
|
|
||||||
x = np.array(range(len(ret)))
|
|
||||||
y = ret_cum.iloc[:, 0].values
|
|
||||||
|
|
||||||
reg = sm.OLS(y, x).fit()
|
|
||||||
reg.summary()
|
|
||||||
|
|
||||||
x = np.array(range(len(ret)))
|
|
||||||
k_ratio = {}
|
|
||||||
|
|
||||||
for i in range(0, len(ticker_list)):
|
|
||||||
|
|
||||||
ticker = data_bind.loc[i, '종목코드']
|
|
||||||
|
|
||||||
try:
|
|
||||||
y = ret_cum.loc[:, price_pivot.columns == ticker]
|
|
||||||
reg = sm.OLS(y, x).fit()
|
|
||||||
res = float(reg.params / reg.bse)
|
|
||||||
except:
|
|
||||||
res = np.nan
|
|
||||||
|
|
||||||
k_ratio[ticker] = res
|
|
||||||
|
|
||||||
k_ratio_bind = pd.DataFrame.from_dict(k_ratio, orient='index').reset_index()
|
|
||||||
k_ratio_bind.columns = ['종목코드', 'K_ratio']
|
|
||||||
|
|
||||||
k_ratio_bind.head()
|
|
||||||
|
|
||||||
data_bind = data_bind.merge(k_ratio_bind, how='left', on='종목코드')
|
|
||||||
k_ratio_rank = data_bind['K_ratio'].rank(axis=0, ascending=False)
|
|
||||||
print(data_bind[k_ratio_rank <= 20])
|
|
||||||
|
|
||||||
k_ratio_momentum = price_list[price_list['종목코드'].isin(data_bind.loc[k_ratio_rank <= 20, '종목코드'])]
|
|
||||||
|
|
||||||
plt.rc('font', family='Malgun Gothic')
|
|
||||||
g = sns.relplot(data=k_ratio_momentum,
|
|
||||||
x='날짜',
|
|
||||||
y='종가',
|
|
||||||
col='종목코드',
|
|
||||||
col_wrap=5,
|
|
||||||
kind='line',
|
|
||||||
facet_kws={
|
|
||||||
'sharey': False,
|
|
||||||
'sharex': True
|
|
||||||
})
|
|
||||||
g.set(xticklabels=[])
|
|
||||||
g.set(xlabel=None)
|
|
||||||
g.set(ylabel=None)
|
|
||||||
g.fig.set_figwidth(15)
|
|
||||||
g.fig.set_figheight(8)
|
|
||||||
plt.subplots_adjust(wspace=0.5, hspace=0.2)
|
|
||||||
plt.show()
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
from streamlit_quant import quantcommon
|
|
||||||
|
|
||||||
# strategy/value 에서 구현
|
|
||||||
|
|
||||||
#가치주 포트폴리오. PER, PBR이 낮은 회사 20개
|
|
||||||
# DB 연결
|
|
||||||
engine = quantcommon.QuantCommon().create_engine()
|
|
||||||
|
|
||||||
ticker_list = pd.read_sql("""
|
|
||||||
select * from kor_ticker
|
|
||||||
where 기준일 = (select max(기준일) from kor_ticker)
|
|
||||||
and 종목구분 = '보통주';
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
value_list = pd.read_sql("""
|
|
||||||
select * from kor_value
|
|
||||||
where 기준일 = (select max(기준일) from kor_value);
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
engine.dispose()
|
|
||||||
|
|
||||||
value_list.loc[value_list['값'] <= 0, '값'] = np.nan
|
|
||||||
value_pivot = value_list.pivot(index='종목코드', columns='지표', values='값')
|
|
||||||
data_bind = ticker_list[['종목코드', '종목명']].merge(value_pivot,
|
|
||||||
how='left',
|
|
||||||
on='종목코드')
|
|
||||||
# print(data_bind.head())
|
|
||||||
value_rank = data_bind[['PER', 'PBR']].rank(axis = 0)
|
|
||||||
value_sum = value_rank.sum(axis = 1, skipna = False).rank()
|
|
||||||
print(data_bind.loc[value_sum <= 20, ['종목코드', '종목명', 'PER', 'PBR']])
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import seaborn as sns
|
|
||||||
from streamlit_quant import quantcommon
|
|
||||||
|
|
||||||
# 마법 공식 포트폴리오. 밸류와 퀄리티의 조합. 조엘 그린블라트의 '마법공식
|
|
||||||
engine = quantcommon.QuantCommon().create_engine()
|
|
||||||
|
|
||||||
ticker_list = pd.read_sql("""
|
|
||||||
select * from kor_ticker
|
|
||||||
where 기준일 = (select max(기준일) from kor_ticker)
|
|
||||||
and 종목구분 = '보통주';
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
fs_list = pd.read_sql("""
|
|
||||||
select * from kor_fs
|
|
||||||
where 계정 in ('매출액', '당기순이익', '법인세비용', '이자비용', '현금및현금성자산',
|
|
||||||
'부채', '유동부채', '유동자산', '비유동자산', '감가상각비')
|
|
||||||
and 공시구분 = 'q';
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
engine.dispose()
|
|
||||||
|
|
||||||
fs_list = fs_list.sort_values(['종목코드', '계정', '기준일'])
|
|
||||||
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'])
|
|
||||||
|
|
||||||
fs_list_clean = fs_list_clean.groupby(['종목코드', '계정']).tail(1)
|
|
||||||
fs_list_pivot = fs_list_clean.pivot(index='종목코드', columns='계정', values='ttm')
|
|
||||||
|
|
||||||
data_bind = ticker_list[['종목코드', '종목명', '시가총액']].merge(fs_list_pivot,
|
|
||||||
how='left',
|
|
||||||
on='종목코드')
|
|
||||||
data_bind['시가총액'] = data_bind['시가총액'] / 100000000
|
|
||||||
|
|
||||||
# 분자(EBIT)
|
|
||||||
magic_ebit = data_bind['당기순이익'] + data_bind['법인세비용'] + data_bind['이자비용']
|
|
||||||
|
|
||||||
# 분모
|
|
||||||
magic_cap = data_bind['시가총액']
|
|
||||||
magic_debt = data_bind['부채']
|
|
||||||
|
|
||||||
## 분모: 여유자금
|
|
||||||
magic_excess_cash = data_bind['유동부채'] - data_bind['유동자산'] + data_bind[
|
|
||||||
'현금및현금성자산']
|
|
||||||
magic_excess_cash[magic_excess_cash < 0] = 0
|
|
||||||
magic_excess_cash_final = data_bind['현금및현금성자산'] - magic_excess_cash
|
|
||||||
|
|
||||||
magic_ev = magic_cap + magic_debt - magic_excess_cash_final
|
|
||||||
|
|
||||||
# 이익수익률
|
|
||||||
magic_ey = magic_ebit / magic_ev
|
|
||||||
|
|
||||||
# 투하자본 수익률
|
|
||||||
magic_ic = (data_bind['유동자산'] - data_bind['유동부채']) + (data_bind['비유동자산'] -
|
|
||||||
data_bind['감가상각비'])
|
|
||||||
magic_roc = magic_ebit / magic_ic
|
|
||||||
|
|
||||||
# 열 입력하기
|
|
||||||
data_bind['이익 수익률'] = magic_ey
|
|
||||||
data_bind['투하자본 수익률'] = magic_roc
|
|
||||||
|
|
||||||
magic_rank = (magic_ey.rank(ascending=False, axis=0) +
|
|
||||||
magic_roc.rank(ascending=False, axis=0)).rank(axis=0)
|
|
||||||
print(data_bind.loc[magic_rank <= 20, ['종목코드', '종목명', '이익 수익률', '투하자본 수익률']].round(4))
|
|
||||||
|
|
||||||
data_bind['투자구분'] = np.where(magic_rank <= 20, '마법공식', '기타')
|
|
||||||
|
|
||||||
plt.rc('font', family='Malgun Gothic')
|
|
||||||
plt.subplots(1, 1, figsize=(10, 6))
|
|
||||||
sns.scatterplot(data=data_bind,
|
|
||||||
x='이익 수익률',
|
|
||||||
y='투하자본 수익률',
|
|
||||||
hue='투자구분',
|
|
||||||
style='투자구분',
|
|
||||||
s=200)
|
|
||||||
plt.xlim(0, 1)
|
|
||||||
plt.ylim(0, 1)
|
|
||||||
plt.show()
|
|
||||||
@ -1,248 +0,0 @@
|
|||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
import statsmodels.api as sm
|
|
||||||
from scipy.stats import zscore
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import seaborn as sns
|
|
||||||
from streamlit_quant import quantcommon
|
|
||||||
|
|
||||||
# strategy/multi-factor에서 구현
|
|
||||||
|
|
||||||
# 멀티 팩터 포트폴리오.
|
|
||||||
# 퀄리티: 자기자본이익률(ROE), 매출총이익(GPA), 영업활동현금흐름(CFO)
|
|
||||||
# 밸류: PER, PBR, PSR, PCR, DY
|
|
||||||
# 모멘텀: 12개월 수익률, K-Ratio
|
|
||||||
engine = quantcommon.QuantCommon().create_engine()
|
|
||||||
|
|
||||||
def col_clean(df, cutoff=0.01, asc=False):
|
|
||||||
|
|
||||||
q_low = df.quantile(cutoff)
|
|
||||||
q_hi = df.quantile(1 - cutoff)
|
|
||||||
|
|
||||||
df_trim = df[(df > q_low) & (df < q_hi)]
|
|
||||||
|
|
||||||
if asc == False:
|
|
||||||
df_z_score = df_trim.rank(axis=0, ascending=False).apply(
|
|
||||||
zscore, nan_policy='omit')
|
|
||||||
if asc == True:
|
|
||||||
df_z_score = df_trim.rank(axis=0, ascending=True).apply(
|
|
||||||
zscore, nan_policy='omit')
|
|
||||||
|
|
||||||
return(df_z_score)
|
|
||||||
|
|
||||||
|
|
||||||
def plot_rank(df):
|
|
||||||
ax = sns.relplot(data=df,
|
|
||||||
x='rank',
|
|
||||||
y=1,
|
|
||||||
col='variable',
|
|
||||||
hue='invest',
|
|
||||||
size='size',
|
|
||||||
sizes=(10, 100),
|
|
||||||
style='invest',
|
|
||||||
markers={'Y': 'X','N': 'o'},
|
|
||||||
palette={'Y': 'red','N': 'grey'},
|
|
||||||
kind='scatter')
|
|
||||||
ax.set(xlabel=None)
|
|
||||||
ax.set(ylabel=None)
|
|
||||||
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
|
|
||||||
ticker_list = pd.read_sql("""
|
|
||||||
select * from kor_ticker
|
|
||||||
where 기준일 = (select max(기준일) from kor_ticker)
|
|
||||||
and 종목구분 = '보통주';
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
fs_list = pd.read_sql("""
|
|
||||||
select * from kor_fs
|
|
||||||
where 계정 in ('당기순이익', '매출총이익', '영업활동으로인한현금흐름', '자산', '자본')
|
|
||||||
and 공시구분 = 'q';
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
value_list = pd.read_sql("""
|
|
||||||
select * from kor_value
|
|
||||||
where 기준일 = (select max(기준일) from kor_value);
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
price_list = pd.read_sql("""
|
|
||||||
select 날짜, 종가, 종목코드
|
|
||||||
from kor_price
|
|
||||||
where 날짜 >= (select (select max(날짜) from kor_price) - interval 1 year);
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
sector_list = pd.read_sql("""
|
|
||||||
select * from kor_sector
|
|
||||||
where 기준일 = (select max(기준일) from kor_sector);
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
engine.dispose()
|
|
||||||
|
|
||||||
fs_list = fs_list.sort_values(['종목코드', '계정', '기준일'])
|
|
||||||
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'])
|
|
||||||
fs_list_clean = fs_list_clean.groupby(['종목코드', '계정']).tail(1)
|
|
||||||
|
|
||||||
fs_list_pivot = fs_list_clean.pivot(index='종목코드', columns='계정', values='ttm')
|
|
||||||
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['자산']
|
|
||||||
|
|
||||||
fs_list_pivot.round(4).head()
|
|
||||||
|
|
||||||
value_list.loc[value_list['값'] <= 0, '값'] = np.nan
|
|
||||||
value_pivot = value_list.pivot(index='종목코드', columns='지표', values='값')
|
|
||||||
|
|
||||||
value_pivot.head()
|
|
||||||
|
|
||||||
price_pivot = price_list.pivot(index='날짜', columns='종목코드', values='종가')
|
|
||||||
ret_list = pd.DataFrame(data=(price_pivot.iloc[-1] / price_pivot.iloc[0]) - 1,
|
|
||||||
columns=['12M'])
|
|
||||||
|
|
||||||
ret = price_pivot.pct_change().iloc[1:]
|
|
||||||
ret_cum = np.log(1 + ret).cumsum()
|
|
||||||
|
|
||||||
x = np.array(range(len(ret)))
|
|
||||||
k_ratio = {}
|
|
||||||
|
|
||||||
for i in range(0, len(ticker_list)):
|
|
||||||
|
|
||||||
ticker = ticker_list.loc[i, '종목코드']
|
|
||||||
|
|
||||||
try:
|
|
||||||
y = ret_cum.loc[:, price_pivot.columns == ticker]
|
|
||||||
reg = sm.OLS(y, x).fit()
|
|
||||||
res = float(reg.params / reg.bse)
|
|
||||||
except:
|
|
||||||
res = np.nan
|
|
||||||
|
|
||||||
k_ratio[ticker] = res
|
|
||||||
|
|
||||||
k_ratio_bind = pd.DataFrame.from_dict(k_ratio, orient='index').reset_index()
|
|
||||||
k_ratio_bind.columns = ['종목코드', 'K_ratio']
|
|
||||||
|
|
||||||
k_ratio_bind.head()
|
|
||||||
|
|
||||||
data_bind = ticker_list[['종목코드', '종목명']].merge(
|
|
||||||
sector_list[['CMP_CD', 'SEC_NM_KOR']],
|
|
||||||
how='left',
|
|
||||||
left_on='종목코드',
|
|
||||||
right_on='CMP_CD').merge(
|
|
||||||
fs_list_pivot[['ROE', 'GPA', 'CFO']], how='left',
|
|
||||||
on='종목코드').merge(value_pivot, how='left',
|
|
||||||
on='종목코드').merge(ret_list, how='left',
|
|
||||||
on='종목코드').merge(k_ratio_bind,
|
|
||||||
how='left',
|
|
||||||
on='종목코드')
|
|
||||||
|
|
||||||
data_bind.loc[data_bind['SEC_NM_KOR'].isnull(), 'SEC_NM_KOR'] = '기타'
|
|
||||||
data_bind = data_bind.drop(['CMP_CD'], axis=1)
|
|
||||||
|
|
||||||
data_bind.round(4).head()
|
|
||||||
|
|
||||||
data_bind_group = data_bind.set_index(['종목코드',
|
|
||||||
'SEC_NM_KOR']).groupby('SEC_NM_KOR', as_index=False)
|
|
||||||
|
|
||||||
data_bind_group.head(1).round(4)
|
|
||||||
|
|
||||||
z_quality = data_bind_group[['ROE', 'GPA', 'CFO'
|
|
||||||
]].apply(lambda x: col_clean(x, 0.01, False)).sum(
|
|
||||||
axis=1, skipna=False).to_frame('z_quality')
|
|
||||||
data_bind = data_bind.merge(z_quality, how='left', on=['종목코드', 'SEC_NM_KOR'])
|
|
||||||
|
|
||||||
data_bind.round(4).head()
|
|
||||||
|
|
||||||
value_1 = data_bind_group[['PBR', 'PCR', 'PER',
|
|
||||||
'PSR']].apply(lambda x: col_clean(x, 0.01, True))
|
|
||||||
value_2 = data_bind_group[['DY']].apply(lambda x: col_clean(x, 0.01, False))
|
|
||||||
|
|
||||||
z_value = value_1.merge(value_2, on=['종목코드', 'SEC_NM_KOR'
|
|
||||||
]).sum(axis=1,
|
|
||||||
skipna=False).to_frame('z_value')
|
|
||||||
data_bind = data_bind.merge(z_value, how='left', on=['종목코드', 'SEC_NM_KOR'])
|
|
||||||
|
|
||||||
data_bind.round(4).head()
|
|
||||||
|
|
||||||
z_momentum = data_bind_group[[
|
|
||||||
'12M', 'K_ratio'
|
|
||||||
]].apply(lambda x: col_clean(x, 0.01, False)).sum(
|
|
||||||
axis=1, skipna=False).to_frame('z_momentum')
|
|
||||||
data_bind = data_bind.merge(z_momentum, how='left', on=['종목코드', 'SEC_NM_KOR'])
|
|
||||||
|
|
||||||
print(data_bind.round(4).head())
|
|
||||||
|
|
||||||
data_z = data_bind[['z_quality', 'z_value', 'z_momentum']].copy()
|
|
||||||
|
|
||||||
fig, axes = plt.subplots(3, 1, figsize=(10, 6), sharex=True, sharey=True)
|
|
||||||
for n, ax in enumerate(axes.flatten()):
|
|
||||||
ax.hist(data_z.iloc[:, n])
|
|
||||||
ax.set_title(data_z.columns[n], size=12)
|
|
||||||
fig.tight_layout()
|
|
||||||
|
|
||||||
data_bind_final = data_bind[['종목코드', 'z_quality', 'z_value', 'z_momentum'
|
|
||||||
]].set_index('종목코드').apply(zscore,
|
|
||||||
nan_policy='omit')
|
|
||||||
data_bind_final.columns = ['quality', 'value', 'momentum']
|
|
||||||
|
|
||||||
plt.rc('font', family='Malgun Gothic')
|
|
||||||
plt.rc('axes', unicode_minus=False)
|
|
||||||
fig, axes = plt.subplots(3, 1, figsize=(10, 6), sharex=True, sharey=True)
|
|
||||||
for n, ax in enumerate(axes.flatten()):
|
|
||||||
ax.hist(data_bind_final.iloc[:, n])
|
|
||||||
ax.set_title(data_bind_final.columns[n], size=12)
|
|
||||||
fig.tight_layout()
|
|
||||||
|
|
||||||
mask = np.triu(data_bind_final.corr())
|
|
||||||
fig, ax = plt.subplots(figsize=(10, 6))
|
|
||||||
sns.heatmap(data_bind_final.corr(),
|
|
||||||
annot=True,
|
|
||||||
mask=mask,
|
|
||||||
annot_kws={"size": 16},
|
|
||||||
vmin=0,
|
|
||||||
vmax=1,
|
|
||||||
center=0.5,
|
|
||||||
cmap='coolwarm',
|
|
||||||
square=True)
|
|
||||||
ax.invert_yaxis()
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
wts = [0.3, 0.3, 0.3]
|
|
||||||
data_bind_final_sum = (data_bind_final * wts).sum(axis=1,
|
|
||||||
skipna=False).to_frame()
|
|
||||||
data_bind_final_sum.columns = ['qvm']
|
|
||||||
port_qvm = data_bind.merge(data_bind_final_sum, on='종목코드')
|
|
||||||
port_qvm['invest'] = np.where(port_qvm['qvm'].rank() <= 20, 'Y', 'N')
|
|
||||||
|
|
||||||
port_qvm[port_qvm['invest'] == 'Y'].round(4)
|
|
||||||
|
|
||||||
data_melt = port_qvm.melt(id_vars='invest',
|
|
||||||
value_vars=[
|
|
||||||
'ROE', 'GPA', 'CFO', 'PER', 'PBR', 'PCR', 'PSR',
|
|
||||||
'DY', '12M', 'K_ratio'
|
|
||||||
])
|
|
||||||
|
|
||||||
data_melt['size'] = data_melt['invest'].map({'Y': 100, 'N': 10})
|
|
||||||
data_melt.head()
|
|
||||||
|
|
||||||
hist_quality = data_melt[data_melt['variable'].isin(['ROE', 'GPA',
|
|
||||||
'CFO'])].copy()
|
|
||||||
hist_quality['rank'] = hist_quality.groupby('variable')['value'].rank(
|
|
||||||
ascending=False)
|
|
||||||
plot_rank(hist_quality)
|
|
||||||
|
|
||||||
hist_value = data_melt[data_melt['variable'].isin(
|
|
||||||
['PER', 'PBR', 'PCR', 'PSR', 'DY'])].copy()
|
|
||||||
hist_value['value'] = np.where(hist_value['variable'] == 'DY',
|
|
||||||
1 / hist_value['value'], hist_value['value'])
|
|
||||||
hist_value['rank'] = hist_value.groupby('variable')['value'].rank()
|
|
||||||
plot_rank(hist_value)
|
|
||||||
|
|
||||||
hist_momentum = data_melt[data_melt['variable'].isin(['12M', 'K_ratio'])].copy()
|
|
||||||
hist_momentum['rank'] = hist_momentum.groupby('variable')['value'].rank(ascending = False)
|
|
||||||
plot_rank(hist_momentum)
|
|
||||||
|
|
||||||
port_qvm[port_qvm['invest'] == 'Y']['종목코드'].to_excel('model.xlsx', index=False)
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
import pandas as pd
|
|
||||||
import numpy as np
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import seaborn as sns
|
|
||||||
from streamlit_quant import quantcommon
|
|
||||||
|
|
||||||
# strategy/quality에서 구현
|
|
||||||
|
|
||||||
# 퀄리티(우량주) 포트폴리오. 영업수익성이 높은 주식
|
|
||||||
engine = quantcommon.QuantCommon().create_engine()
|
|
||||||
|
|
||||||
ticker_list = pd.read_sql("""
|
|
||||||
select * from kor_ticker
|
|
||||||
where 기준일 = (select max(기준일) from kor_ticker)
|
|
||||||
and 종목구분 = '보통주';
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
fs_list = pd.read_sql("""
|
|
||||||
select * from kor_fs
|
|
||||||
where 계정 in ('당기순이익', '매출총이익', '영업활동으로인한현금흐름', '자산', '자본')
|
|
||||||
and 공시구분 = 'q';
|
|
||||||
""", con=engine)
|
|
||||||
|
|
||||||
engine.dispose()
|
|
||||||
|
|
||||||
fs_list = fs_list.sort_values(['종목코드', '계정', '기준일'])
|
|
||||||
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'])
|
|
||||||
fs_list_clean = fs_list_clean.groupby(['종목코드', '계정']).tail(1)
|
|
||||||
|
|
||||||
fs_list_pivot = fs_list_clean.pivot(index='종목코드', columns='계정', values='ttm')
|
|
||||||
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='종목코드')
|
|
||||||
# print(quality_list.round(4).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()
|
|
||||||
|
|
||||||
quality_sum = quality_rank.sum(axis=1, skipna=False).rank()
|
|
||||||
print(quality_list.loc[quality_sum <= 20,
|
|
||||||
['종목코드', '종목명', 'ROE', 'GPA', 'CFO']].round(4))
|
|
||||||
44
src/app.py
Normal file
44
src/app.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
Main Streamlit application for the Quant Manager.
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
from ui.pages.data_page import render_data_page
|
||||||
|
from ui.pages.quality_page import render_quality_page
|
||||||
|
from ui.pages.value_momentum_page import render_value_momentum_page
|
||||||
|
|
||||||
|
# Configure the application
|
||||||
|
st.set_page_config(
|
||||||
|
page_title="콴트 매니저",
|
||||||
|
page_icon="📊",
|
||||||
|
layout="wide",
|
||||||
|
initial_sidebar_state="expanded"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define the sidebar navigation
|
||||||
|
def main():
|
||||||
|
"""Main application function."""
|
||||||
|
# Create sidebar navigation
|
||||||
|
st.sidebar.title("콴트 매니저")
|
||||||
|
|
||||||
|
# Navigation options
|
||||||
|
pages = {
|
||||||
|
"데이터 수집": render_data_page,
|
||||||
|
"슈퍼 퀄리티 전략": render_quality_page,
|
||||||
|
"슈퍼 밸류 모멘텀 전략": render_value_momentum_page
|
||||||
|
}
|
||||||
|
|
||||||
|
# Select page
|
||||||
|
selection = st.sidebar.radio("메뉴", list(pages.keys()))
|
||||||
|
|
||||||
|
# Render the selected page
|
||||||
|
pages[selection]()
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
st.sidebar.markdown("---")
|
||||||
|
st.sidebar.info(
|
||||||
|
"© 2023-2025 콴트 매니저\n\n"
|
||||||
|
"한국 주식 시장을 위한 퀀트 투자 도구"
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
0
src/backtest/__init__.py
Normal file
0
src/backtest/__init__.py
Normal file
@ -3,12 +3,12 @@ import matplotlib.pyplot as plt
|
|||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from streamlit_quant import quantcommon
|
from db.common import DBManager
|
||||||
import streamlit_quant.strategy.multi_factor as multi_factor
|
import streamlit_quant.strategy.multi_factor as multi_factor
|
||||||
import streamlit_quant.strategy.magic_formula as magic_formula
|
import streamlit_quant.strategy.magic_formula as magic_formula
|
||||||
|
|
||||||
|
|
||||||
qc = quantcommon.QuantCommon()
|
qc = DBManager()
|
||||||
mf = multi_factor.get_multi_factor_top(qc, 20)
|
mf = multi_factor.get_multi_factor_top(qc, 20)
|
||||||
magic_formula = magic_formula.get_magic_formula_top(20)
|
magic_formula = magic_formula.get_magic_formula_top(20)
|
||||||
|
|
||||||
1
src/data/__init__.py
Normal file
1
src/data/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__all__ = ['financial', 'krx', 'prices']
|
||||||
@ -6,27 +6,10 @@ import requests as rq
|
|||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from streamlit_quant import quantcommon
|
from db.common import DBManager
|
||||||
|
|
||||||
|
|
||||||
# 재무제표 크롤링
|
# 재무제표 크롤링
|
||||||
|
|
||||||
def get_ticker_list():
|
|
||||||
engine = quantcommon.QuantCommon().create_engine()
|
|
||||||
# 티커리스트 불러오기
|
|
||||||
ticker_list = {}
|
|
||||||
try:
|
|
||||||
ticker_list = pd.read_sql("""
|
|
||||||
select * from kor_ticker
|
|
||||||
where 기준일 = (select max(기준일) from kor_ticker)
|
|
||||||
and 종목구분 = '보통주';
|
|
||||||
""", con=engine)
|
|
||||||
finally:
|
|
||||||
engine.dispose()
|
|
||||||
|
|
||||||
return ticker_list
|
|
||||||
|
|
||||||
|
|
||||||
# 재무제표 클렌징 함수
|
# 재무제표 클렌징 함수
|
||||||
def clean_fs(df, ticker, frequency):
|
def clean_fs(df, ticker, frequency):
|
||||||
df = df[~df.loc[:, ~df.columns.isin(['계정'])].isna().all(axis=1)]
|
df = df[~df.loc[:, ~df.columns.isin(['계정'])].isna().all(axis=1)]
|
||||||
@ -43,11 +26,12 @@ def clean_fs(df, ticker, frequency):
|
|||||||
|
|
||||||
|
|
||||||
# ticker 별 재무제표 조회해서 DB에 저장
|
# ticker 별 재무제표 조회해서 DB에 저장
|
||||||
def process_for_fs(ticker_list):
|
def process_for_fs():
|
||||||
|
ticker_list = DBManager().get_ticker_list()
|
||||||
# DB 연결
|
# DB 연결
|
||||||
common = quantcommon.QuantCommon()
|
db_manager = DBManager()
|
||||||
engine = common.create_engine()
|
engine = db_manager.create_engine()
|
||||||
con = common.connect()
|
con = db_manager.connect()
|
||||||
mycursor = con.cursor()
|
mycursor = con.cursor()
|
||||||
|
|
||||||
# DB 저장 쿼리
|
# DB 저장 쿼리
|
||||||
@ -127,5 +111,4 @@ def process_for_fs(ticker_list):
|
|||||||
con.close()
|
con.close()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
tickers = get_ticker_list()
|
process_for_fs()
|
||||||
process_for_fs(tickers)
|
|
||||||
@ -8,7 +8,7 @@ import requests as rq
|
|||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from streamlit_quant import quantcommon
|
from db.common import DBManager
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ def process_for_total_stock(biz_day):
|
|||||||
|
|
||||||
|
|
||||||
def save_ticker(ticker):
|
def save_ticker(ticker):
|
||||||
con = quantcommon.QuantCommon().connect()
|
con = DBManager().connect()
|
||||||
|
|
||||||
mycursor = con.cursor()
|
mycursor = con.cursor()
|
||||||
query = f"""
|
query = f"""
|
||||||
@ -168,7 +168,7 @@ def process_for_wics(biz_day):
|
|||||||
|
|
||||||
|
|
||||||
def save_sector(sector):
|
def save_sector(sector):
|
||||||
con = quantcommon.QuantCommon().connect()
|
con = DBManager().connect()
|
||||||
|
|
||||||
mycursor = con.cursor()
|
mycursor = con.cursor()
|
||||||
query = f"""
|
query = f"""
|
||||||
@ -1,5 +1,3 @@
|
|||||||
# 패키지 불러오기
|
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
@ -8,28 +6,12 @@ import pandas as pd
|
|||||||
import requests as rq
|
import requests as rq
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from streamlit_quant import quantcommon
|
from db.common import DBManager
|
||||||
|
|
||||||
|
|
||||||
# 주가 크롤링
|
# 주가 크롤링
|
||||||
|
def process_for_price():
|
||||||
def get_ticker_list():
|
ticker_list = DBManager().get_ticker_list()
|
||||||
engine = quantcommon.QuantCommon().create_engine()
|
|
||||||
# 티커리스트 불러오기
|
|
||||||
ticker_list = {}
|
|
||||||
try:
|
|
||||||
ticker_list = pd.read_sql("""
|
|
||||||
select * from kor_ticker
|
|
||||||
where 기준일 = (select max(기준일) from kor_ticker)
|
|
||||||
and 종목구분 = '보통주';
|
|
||||||
""", con=engine)
|
|
||||||
finally:
|
|
||||||
engine.dispose()
|
|
||||||
|
|
||||||
return ticker_list
|
|
||||||
|
|
||||||
|
|
||||||
def process_for_price(ticker_list):
|
|
||||||
# DB 저장 쿼리
|
# DB 저장 쿼리
|
||||||
query = """
|
query = """
|
||||||
insert into kor_price (날짜, 시가, 고가, 저가, 종가, 거래량, 종목코드)
|
insert into kor_price (날짜, 시가, 고가, 저가, 종가, 거래량, 종목코드)
|
||||||
@ -40,7 +22,7 @@ def process_for_price(ticker_list):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# DB 연결
|
# DB 연결
|
||||||
common = quantcommon.QuantCommon()
|
common = DBManager()
|
||||||
engine = common.create_engine()
|
engine = common.create_engine()
|
||||||
con = common.connect()
|
con = common.connect()
|
||||||
|
|
||||||
@ -54,6 +36,7 @@ def process_for_price(ticker_list):
|
|||||||
# 티커 선택
|
# 티커 선택
|
||||||
ticker = ticker_list['종목코드'][i]
|
ticker = ticker_list['종목코드'][i]
|
||||||
|
|
||||||
|
# todo: 날짜 범위 수정
|
||||||
# 시작일과 종료일
|
# 시작일과 종료일
|
||||||
# fr = (date.today() + relativedelta(years=-5)).strftime("%Y%m%d")
|
# fr = (date.today() + relativedelta(years=-5)).strftime("%Y%m%d")
|
||||||
to = (date.today()).strftime("%Y%m%d")
|
to = (date.today()).strftime("%Y%m%d")
|
||||||
@ -97,5 +80,4 @@ def process_for_price(ticker_list):
|
|||||||
con.close()
|
con.close()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
ticker_list = get_ticker_list()
|
process_for_price()
|
||||||
process_for_price(ticker_list)
|
|
||||||
1
src/db/__init__.py
Normal file
1
src/db/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__all__ = ['common']
|
||||||
@ -7,7 +7,7 @@ from dotenv import load_dotenv
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
|
|
||||||
class QuantCommon:
|
class DBManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
self.user = os.getenv('DB_USER')
|
self.user = os.getenv('DB_USER')
|
||||||
1
src/strategies/__init__.py
Normal file
1
src/strategies/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__all__ = ['composite', 'factors', 'utils']
|
||||||
1
src/strategies/composite/__init__.py
Normal file
1
src/strategies/composite/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__all__ = ['multi_factor', 'multi_factor', 'super_quality', 'super_value_momentum']
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from streamlit_quant import quantcommon
|
from db.common import DBManager
|
||||||
|
|
||||||
|
|
||||||
# 마법 공식 포트폴리오. 밸류와 퀄리티의 조합. 조엘 그린블라트의 '마법공식'
|
# 마법 공식 포트폴리오. 밸류와 퀄리티의 조합. 조엘 그린블라트의 '마법공식'
|
||||||
def get_magic_formula_top(count):
|
def get_magic_formula_top(count):
|
||||||
qc = quantcommon.QuantCommon()
|
db = DBManager()
|
||||||
ticker_list = qc.get_ticker_list()
|
ticker_list = db.get_ticker_list()
|
||||||
fs_list = qc.get_expanded_fs_list()
|
fs_list = db.get_expanded_fs_list()
|
||||||
|
|
||||||
fs_list = fs_list.sort_values(['종목코드', '계정', '기준일'])
|
fs_list = fs_list.sort_values(['종목코드', '계정', '기준일'])
|
||||||
# TTM 값을 구하기 위해서 rolling() 메소드를 통해 4분기 합 구함. 4분기 데이터가 없는 경우 제외하기 위해서 min_periods=4
|
# TTM 값을 구하기 위해서 rolling() 메소드를 통해 4분기 합 구함. 4분기 데이터가 없는 경우 제외하기 위해서 min_periods=4
|
||||||
@ -1,8 +1,8 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import streamlit as st
|
from src import ui as st
|
||||||
|
|
||||||
from strategy import f_score
|
from strategies.factors import f_score
|
||||||
import quantcommon
|
from db.common import DBManager
|
||||||
|
|
||||||
|
|
||||||
# st.write("""
|
# st.write("""
|
||||||
@ -60,7 +60,7 @@ def get_last_year_end():
|
|||||||
st.write("투자 전략: 강환국 슈퍼 퀄리티 전략 2.0")
|
st.write("투자 전략: 강환국 슈퍼 퀄리티 전략 2.0")
|
||||||
|
|
||||||
date = get_last_year_end()
|
date = get_last_year_end()
|
||||||
data = f_score.get_f_score(quantcommon.QuantCommon(), date)
|
data = f_score.get_f_score(DBManager(), date)
|
||||||
|
|
||||||
config = {}
|
config = {}
|
||||||
|
|
||||||
@ -1,3 +1,3 @@
|
|||||||
import streamlit as st
|
from src import ui as st
|
||||||
|
|
||||||
st.write("슈퍼 밸류 모멘텀 전략 2.0")
|
st.write("슈퍼 밸류 모멘텀 전략 2.0")
|
||||||
1
src/strategies/factors/__init__.py
Normal file
1
src/strategies/factors/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__all__ = ['all_value', 'f_score', 'momentum', 'quality', 'value']
|
||||||
@ -1,14 +1,14 @@
|
|||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import seaborn as sns
|
import seaborn as sns
|
||||||
from streamlit_quant import quantcommon
|
from db.common import DBManager
|
||||||
|
|
||||||
|
|
||||||
#가치주 포트폴리오. PER, PBR, PCR, PSR, DY
|
#가치주 포트폴리오. PER, PBR, PCR, PSR, DY
|
||||||
def get_all_value_top(count):
|
def get_all_value_top(count):
|
||||||
qc = quantcommon.QuantCommon()
|
db = DBManager()
|
||||||
ticker_list = qc.get_ticker_list()
|
ticker_list = db.get_ticker_list()
|
||||||
value_list = qc.get_value_list()
|
value_list = db.get_value_list()
|
||||||
|
|
||||||
# 가치 지표가 0이하인 경우 nan으로 변경
|
# 가치 지표가 0이하인 경우 nan으로 변경
|
||||||
value_list.loc[value_list['값'] <= 0, '값'] = np.nan
|
value_list.loc[value_list['값'] <= 0, '값'] = np.nan
|
||||||
@ -1,6 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from streamlit_quant import quantcommon
|
from db.common import DBManager
|
||||||
|
|
||||||
|
|
||||||
# 흑자 기업이면 1점(당기순이익)
|
# 흑자 기업이면 1점(당기순이익)
|
||||||
@ -101,4 +101,4 @@ def get_f_score(qc, base_date):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
date = datetime(2024, 12, 31).date()
|
date = datetime(2024, 12, 31).date()
|
||||||
print(get_f_score(quantcommon.QuantCommon(), date).head(30))
|
print(get_f_score(DBManager(), date).head(30))
|
||||||
@ -3,7 +3,7 @@ import matplotlib.pyplot as plt
|
|||||||
import seaborn as sns
|
import seaborn as sns
|
||||||
import statsmodels.api as sm
|
import statsmodels.api as sm
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from streamlit_quant import quantcommon
|
from db.common import DBManager
|
||||||
|
|
||||||
|
|
||||||
def print_graph(values):
|
def print_graph(values):
|
||||||
@ -29,9 +29,9 @@ def print_graph(values):
|
|||||||
# strategy/momentum에 구현
|
# strategy/momentum에 구현
|
||||||
# 모멘텀 포트폴리오. 최근 12개월 수익률이 높은 주식
|
# 모멘텀 포트폴리오. 최근 12개월 수익률이 높은 주식
|
||||||
def get_momentum_top(count):
|
def get_momentum_top(count):
|
||||||
qc = quantcommon.QuantCommon()
|
db = DBManager()
|
||||||
ticker_list = qc.get_ticker_list()
|
ticker_list = db.get_ticker_list()
|
||||||
price_list = qc.get_price_list(interval_month=12)
|
price_list = db.get_price_list(interval_month=12)
|
||||||
|
|
||||||
price_pivot = price_list.pivot(index='날짜', columns='종목코드', values='종가')
|
price_pivot = price_list.pivot(index='날짜', columns='종목코드', values='종가')
|
||||||
|
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import seaborn as sns
|
import seaborn as sns
|
||||||
from streamlit_quant import quantcommon
|
from db.common import DBManager
|
||||||
|
|
||||||
|
|
||||||
# 퀄리티(우량주) 포트폴리오. 영업수익성이 높은 주식
|
# 퀄리티(우량주) 포트폴리오. 영업수익성이 높은 주식
|
||||||
def get_quality_top(count):
|
def get_quality_top(count):
|
||||||
qc = quantcommon.QuantCommon()
|
qc = DBManager()
|
||||||
ticker_list = qc.get_ticker_list()
|
ticker_list = qc.get_ticker_list()
|
||||||
fs_list = qc.get_fs_list()
|
fs_list = qc.get_fs_list()
|
||||||
|
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from streamlit_quant import quantcommon
|
from db.common import DBManager
|
||||||
|
|
||||||
|
|
||||||
#가치주 포트폴리오. PER, PBR이 낮은 회사 20개
|
#가치주 포트폴리오. PER, PBR이 낮은 회사 20개
|
||||||
def get_value_top(count):
|
def get_value_top(count):
|
||||||
qc = quantcommon.QuantCommon()
|
db = DBManager()
|
||||||
ticker_list = qc.get_ticker_list()
|
ticker_list = db.get_ticker_list()
|
||||||
value_list = qc.get_value_list()
|
value_list = db.get_value_list()
|
||||||
|
|
||||||
# 가치 지표가 0이하인 경우 nan으로 변경
|
# 가치 지표가 0이하인 경우 nan으로 변경
|
||||||
value_list.loc[value_list['값'] <= 0, '값'] = np.nan
|
value_list.loc[value_list['값'] <= 0, '값'] = np.nan
|
||||||
80
src/strategies/utils.py
Normal file
80
src/strategies/utils.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
"""
|
||||||
|
Common utilities for quantitative investment strategies.
|
||||||
|
"""
|
||||||
|
import pandas as pd
|
||||||
|
from db.common import DBManager
|
||||||
|
|
||||||
|
|
||||||
|
def get_sector_data():
|
||||||
|
"""Get sector data for all stocks."""
|
||||||
|
db = DBManager()
|
||||||
|
return db.get_sector_list()
|
||||||
|
|
||||||
|
|
||||||
|
def get_stock_data():
|
||||||
|
"""Get stock data for all available stocks."""
|
||||||
|
db = DBManager()
|
||||||
|
return db.get_ticker_list()
|
||||||
|
|
||||||
|
|
||||||
|
def get_financial_data():
|
||||||
|
"""Get financial statement data."""
|
||||||
|
db = DBManager()
|
||||||
|
return db.get_fs_list()
|
||||||
|
|
||||||
|
|
||||||
|
def get_expanded_financial_data():
|
||||||
|
"""Get expanded financial statement data."""
|
||||||
|
db = DBManager()
|
||||||
|
return db.get_expanded_fs_list()
|
||||||
|
|
||||||
|
|
||||||
|
def get_price_data(interval_months=60):
|
||||||
|
"""Get price data for the specified interval."""
|
||||||
|
db = DBManager()
|
||||||
|
return db.get_price_list(interval_months)
|
||||||
|
|
||||||
|
|
||||||
|
def get_value_metrics():
|
||||||
|
"""Get value metrics for all stocks."""
|
||||||
|
db = DBManager()
|
||||||
|
return db.get_value_list()
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_zscore(series):
|
||||||
|
"""Calculate z-score for a pandas series."""
|
||||||
|
return (series - series.mean()) / series.std()
|
||||||
|
|
||||||
|
|
||||||
|
def rank_by_metric(df, metric, ascending=True):
|
||||||
|
"""Rank stocks by a metric."""
|
||||||
|
return df.sort_values(by=metric, ascending=ascending)
|
||||||
|
|
||||||
|
|
||||||
|
def rank_combined(df, metrics, weights=None, ascending=True):
|
||||||
|
"""Rank stocks by combined metrics.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame with stock data
|
||||||
|
metrics: List of column names to use for ranking
|
||||||
|
weights: Optional weights for each metric
|
||||||
|
ascending: Direction for ranking (True = smaller is better)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame with combined rank
|
||||||
|
"""
|
||||||
|
if weights is None:
|
||||||
|
weights = [1] * len(metrics)
|
||||||
|
|
||||||
|
# Calculate rank for each metric
|
||||||
|
ranks = pd.DataFrame(index=df.index)
|
||||||
|
for i, metric in enumerate(metrics):
|
||||||
|
ranks[f'rank_{metric}'] = df[metric].rank(ascending=ascending) * weights[i]
|
||||||
|
|
||||||
|
# Calculate combined rank
|
||||||
|
ranks['combined_rank'] = ranks.mean(axis=1)
|
||||||
|
|
||||||
|
# Combine with original data
|
||||||
|
result = pd.concat([df, ranks['combined_rank']], axis=1)
|
||||||
|
|
||||||
|
return result.sort_values('combined_rank')
|
||||||
1
src/ui/__init__.py
Normal file
1
src/ui/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__all__ = ['components', 'pages']
|
||||||
1
src/ui/components/__init__.py
Normal file
1
src/ui/components/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__all__ = ['charts']
|
||||||
189
src/ui/components/charts.py
Normal file
189
src/ui/components/charts.py
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
"""
|
||||||
|
Charting components for the Streamlit Quant application.
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
import plotly.express as px
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
def plot_stock_price(price_data, ticker_column='종목코드', date_column='날짜',
|
||||||
|
price_column='종가', name_column='종목명'):
|
||||||
|
"""
|
||||||
|
Plot stock price chart for selected stocks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
price_data: DataFrame with price data
|
||||||
|
ticker_column: Column name for ticker code
|
||||||
|
date_column: Column name for date
|
||||||
|
price_column: Column name for price
|
||||||
|
name_column: Column name for stock name
|
||||||
|
"""
|
||||||
|
if price_data.empty:
|
||||||
|
st.warning("가격 데이터가 없습니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get unique tickers
|
||||||
|
tickers = price_data[ticker_column].unique()
|
||||||
|
|
||||||
|
if len(tickers) == 0:
|
||||||
|
st.warning("표시할 종목이 없습니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create figure
|
||||||
|
fig = go.Figure()
|
||||||
|
|
||||||
|
# Add traces
|
||||||
|
for ticker in tickers:
|
||||||
|
ticker_data = price_data[price_data[ticker_column] == ticker]
|
||||||
|
|
||||||
|
name = ticker
|
||||||
|
if name_column in ticker_data.columns:
|
||||||
|
name = ticker_data[name_column].iloc[0] if not ticker_data.empty else ticker
|
||||||
|
name = f"{name} ({ticker})"
|
||||||
|
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=ticker_data[date_column],
|
||||||
|
y=ticker_data[price_column],
|
||||||
|
mode='lines',
|
||||||
|
name=name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update layout
|
||||||
|
fig.update_layout(
|
||||||
|
title='주가 차트',
|
||||||
|
xaxis_title='날짜',
|
||||||
|
yaxis_title='가격',
|
||||||
|
legend_title='종목',
|
||||||
|
hovermode='x unified'
|
||||||
|
)
|
||||||
|
|
||||||
|
st.plotly_chart(fig, use_container_width=True)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_returns_comparison(returns_data, benchmark_data=None,
|
||||||
|
date_column='날짜', returns_column='누적수익률'):
|
||||||
|
"""
|
||||||
|
Plot returns comparison chart for strategy vs benchmark.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
returns_data: DataFrame with strategy returns
|
||||||
|
benchmark_data: DataFrame with benchmark returns (optional)
|
||||||
|
date_column: Column name for date
|
||||||
|
returns_column: Column name for returns
|
||||||
|
"""
|
||||||
|
if returns_data.empty:
|
||||||
|
st.warning("수익률 데이터가 없습니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create figure
|
||||||
|
fig = go.Figure()
|
||||||
|
|
||||||
|
# Add strategy trace
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=returns_data[date_column],
|
||||||
|
y=returns_data[returns_column],
|
||||||
|
mode='lines',
|
||||||
|
name='전략'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add benchmark trace if provided
|
||||||
|
if benchmark_data is not None and not benchmark_data.empty:
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=benchmark_data[date_column],
|
||||||
|
y=benchmark_data[returns_column],
|
||||||
|
mode='lines',
|
||||||
|
name='벤치마크'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update layout
|
||||||
|
fig.update_layout(
|
||||||
|
title='수익률 비교',
|
||||||
|
xaxis_title='날짜',
|
||||||
|
yaxis_title='누적수익률 (%)',
|
||||||
|
legend_title='전략',
|
||||||
|
hovermode='x unified'
|
||||||
|
)
|
||||||
|
|
||||||
|
st.plotly_chart(fig, use_container_width=True)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_factor_distribution(data, factor_column, title=None):
|
||||||
|
"""
|
||||||
|
Plot histogram for factor distribution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: DataFrame with factor data
|
||||||
|
factor_column: Column name for factor
|
||||||
|
title: Chart title (optional)
|
||||||
|
"""
|
||||||
|
if data.empty:
|
||||||
|
st.warning("데이터가 없습니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if factor_column not in data.columns:
|
||||||
|
st.warning(f"{factor_column} 칼럼이, 데이터에 없습니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create histogram
|
||||||
|
fig = px.histogram(
|
||||||
|
data,
|
||||||
|
x=factor_column,
|
||||||
|
nbins=30,
|
||||||
|
marginal="box",
|
||||||
|
title=title or f"{factor_column} 분포"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update layout
|
||||||
|
fig.update_layout(
|
||||||
|
xaxis_title=factor_column,
|
||||||
|
yaxis_title='종목 수'
|
||||||
|
)
|
||||||
|
|
||||||
|
st.plotly_chart(fig, use_container_width=True)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_scatter_matrix(data, columns, color_column=None, size_column=None, title=None):
|
||||||
|
"""
|
||||||
|
Plot scatter matrix for multiple factors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: DataFrame with factor data
|
||||||
|
columns: List of column names to plot
|
||||||
|
color_column: Column name for color (optional)
|
||||||
|
size_column: Column name for point size (optional)
|
||||||
|
title: Chart title (optional)
|
||||||
|
"""
|
||||||
|
if data.empty:
|
||||||
|
st.warning("데이터가 없습니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Validate columns
|
||||||
|
missing_cols = [col for col in columns if col not in data.columns]
|
||||||
|
if missing_cols:
|
||||||
|
st.warning(f"다음 칼럼이 데이터에 없습니다: {', '.join(missing_cols)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create scatter matrix
|
||||||
|
fig = px.scatter_matrix(
|
||||||
|
data,
|
||||||
|
dimensions=columns,
|
||||||
|
color=color_column if color_column in data.columns else None,
|
||||||
|
size=size_column if size_column in data.columns else None,
|
||||||
|
title=title or "팩터 상관관계"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update layout
|
||||||
|
fig.update_layout(
|
||||||
|
height=600,
|
||||||
|
width=800
|
||||||
|
)
|
||||||
|
|
||||||
|
st.plotly_chart(fig, use_container_width=True)
|
||||||
1
src/ui/pages/__init__.py
Normal file
1
src/ui/pages/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
__all__ = ['data_page', 'quality_page', 'value_momentum_page']
|
||||||
60
src/ui/pages/data_page.py
Normal file
60
src/ui/pages/data_page.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"""
|
||||||
|
Data collection page for the Streamlit Quant application.
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
from data import financial, krx, prices
|
||||||
|
|
||||||
|
|
||||||
|
def render_data_page():
|
||||||
|
"""Render the data collection page interface."""
|
||||||
|
st.title("데이터 수집")
|
||||||
|
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
if st.button(label='주식 데이터 수집', help="KOSPI, KOSDAQ 주식 데이터 수집"):
|
||||||
|
with st.spinner('주식 데이터 수집중...'):
|
||||||
|
try:
|
||||||
|
# Call the krx data collection function
|
||||||
|
biz_day = krx.get_latest_biz_day()
|
||||||
|
krx.process_for_total_stock(biz_day)
|
||||||
|
krx.process_for_wics(biz_day)
|
||||||
|
# Show success message
|
||||||
|
st.success('종목 데이터 수집 완료')
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"데이터 수집 실패: {e}")
|
||||||
|
|
||||||
|
if st.button(label='가격 데이터 수집', help="주가 데이터 수집"):
|
||||||
|
with st.spinner('가격 데이터 수집중...'):
|
||||||
|
try:
|
||||||
|
# Call the price data collection function
|
||||||
|
prices.process_for_price()
|
||||||
|
# Show success message
|
||||||
|
st.success(f'가격 데이터 수집 완료')
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"데이터 수집 실패: {e}")
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
if st.button(label='재무제표 데이터 수집', help="재무제표 데이터 수집"):
|
||||||
|
with st.spinner('재무제표 데이터 수집중...'):
|
||||||
|
try:
|
||||||
|
# Call the financial data collection function
|
||||||
|
financial.process_for_fs()
|
||||||
|
# Show success message
|
||||||
|
st.success(f'재무제표 데이터 수집 완료')
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"데이터 수집 실패: {e}")
|
||||||
|
|
||||||
|
if st.button(label='데이터 업데이트', help="모든 데이터 업데이트"):
|
||||||
|
with st.spinner('데이터 업데이트중...'):
|
||||||
|
try:
|
||||||
|
# Call all data collection functions
|
||||||
|
# biz_day = krx.get_latest_biz_day()
|
||||||
|
# stock_data = krx.get_stock_data(biz_day)
|
||||||
|
# sector_data = krx.get_sector_data(biz_day)
|
||||||
|
# price_data = prices.crawl_price()
|
||||||
|
# financial_data = financial.crawl_fs()
|
||||||
|
# Show success message
|
||||||
|
st.success('모든 데이터 업데이트 완료')
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"데이터 업데이트 실패: {e}")
|
||||||
118
src/ui/pages/quality_page.py
Normal file
118
src/ui/pages/quality_page.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
"""
|
||||||
|
Super Quality strategy page for the Streamlit Quant application.
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
from datetime import datetime
|
||||||
|
from strategies.factors.f_score import get_f_score
|
||||||
|
from db.common import DBManager
|
||||||
|
|
||||||
|
def render_quality_page():
|
||||||
|
"""Render the Super Quality strategy page."""
|
||||||
|
st.title("슈퍼 퀄리티 전략")
|
||||||
|
|
||||||
|
with st.expander("전략 설명", expanded=False):
|
||||||
|
st.write("""
|
||||||
|
'신F-스코어 3점 + 고GP/A 전략'을 '강환국 슈퍼 퀄리티 전략'이라 명명한다.
|
||||||
|
이 전략은 신F-스코어가 3점인 종목을 매수하되, GP/A로 순위를 매겨서 순위가 높은 종목만 매수하는 것이다.
|
||||||
|
이 경우 한국에서 수익이 어땠을지 분석해보자.
|
||||||
|
연도별로 신F-스코어 3점을 충족하는 종목은 600-700개였다.
|
||||||
|
|
||||||
|
신F-스코어 3점 기업 내에서도 GP/A가 높은 종목이 3점 종목 평균보다 CAGR 기준으로 3-4% 더 높았다.
|
||||||
|
반대로 GP/A가 낮은 종목의 수익률은 상대적으로 저조했다.
|
||||||
|
|
||||||
|
---
|
||||||
|
투자 전략: 강환국 슈퍼 퀄리티 전략 1.0
|
||||||
|
레벨: 초, 중급
|
||||||
|
스타일: 퀄리티
|
||||||
|
기대 CAGR: 약 20%
|
||||||
|
매수 전략:
|
||||||
|
- 신F-스코어 3점 종목만 매수
|
||||||
|
- 여기에 GP/A 순위를 부여, 순위 높은 20-30종목을 매수
|
||||||
|
매도 전략: 연 1회 리밸런싱
|
||||||
|
|
||||||
|
---
|
||||||
|
지금까지 소개한 거의 모든 전략에서 소형주 전략이 전체 주식 수익률보다 높았다.
|
||||||
|
시가총액 하위 20% 종목의 CAGR을 분석해보았다.
|
||||||
|
|
||||||
|
---
|
||||||
|
투자 전략: 강환국 슈퍼 퀄리티 전략 2.0
|
||||||
|
레벨: 초, 중급
|
||||||
|
스타일: 퀄리티
|
||||||
|
기대 CAGR: 20% 이상
|
||||||
|
매수 전략:
|
||||||
|
아래 조건을 만족하는 20-30종목 매수
|
||||||
|
- 신F-스코어 3점 종목만 매수
|
||||||
|
- 여기에 GP/A 순위를 부여, 순위 높은 종목만 매수
|
||||||
|
- 단, 소형주(시가총액 최저 20%)만 매수
|
||||||
|
매도 전략: 연 1회 리밸런싱
|
||||||
|
---
|
||||||
|
소형주 중 신F-스코어가 3점인 종목을 찾아보니 2004-2016년 구간에 80-100개 종목이 남았다.
|
||||||
|
그 주식들을 통째로 매수해도 CAGR 34.55%를 벌수 있었다!
|
||||||
|
정말 상당한 수익이다.
|
||||||
|
이 종목들을 다 샀으면 총 1,159개 종목 중 14개가 파산했다.(1.2%)
|
||||||
|
또 1년간 마이너스 수익을 기록한 종목이 29.7%였다.
|
||||||
|
|
||||||
|
신F-스코어가 3점인 종목 중 GP/A가 높은 종목 위주로 매수했으면 (1) CAGR도 조금 개선되고 (2) 최상 30개 종목을 매수했을 경우 선택받은 종목 360개 중 파산한 기업은 단 1개였다.
|
||||||
|
F-스코어와 GP/A는 엄청난 잠재력을 지닌 콤비네이션임이 분명하다.
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Strategy implementation
|
||||||
|
st.write("## 슈퍼 퀄리티 전략 2.0 포트폴리오")
|
||||||
|
|
||||||
|
# Get data
|
||||||
|
date = get_last_year_end()
|
||||||
|
db = DBManager()
|
||||||
|
data = get_f_score(db, date)
|
||||||
|
|
||||||
|
# Display options
|
||||||
|
col1, col2 = st.columns([1, 2])
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.write("### 설정")
|
||||||
|
min_f_score = st.slider("최소 F-스코어", min_value=0, max_value=3, value=3)
|
||||||
|
include_small_caps = st.checkbox("소형주만 포함", value=True)
|
||||||
|
num_stocks = st.slider("포트폴리오 종목수", min_value=5, max_value=50, value=20)
|
||||||
|
|
||||||
|
# Filter data
|
||||||
|
filtered_data = data[data['f-score'] >= min_f_score].copy()
|
||||||
|
|
||||||
|
if include_small_caps:
|
||||||
|
# Sort by market cap and keep only the bottom 20%
|
||||||
|
filtered_data = filtered_data.sort_values('시가총액')
|
||||||
|
filtered_data = filtered_data.head(int(len(filtered_data) * 0.2))
|
||||||
|
|
||||||
|
# Sort by GP/A in descending order
|
||||||
|
filtered_data = filtered_data.sort_values('GP/A', ascending=False)
|
||||||
|
|
||||||
|
# Get top N stocks
|
||||||
|
portfolio = filtered_data.head(num_stocks)
|
||||||
|
|
||||||
|
# Display portfolio
|
||||||
|
with col2:
|
||||||
|
st.write(f"### 선택된 {len(portfolio)} 종목")
|
||||||
|
st.dataframe(portfolio, use_container_width=True)
|
||||||
|
|
||||||
|
# Display metrics
|
||||||
|
st.write("### 포트폴리오 지표")
|
||||||
|
col1, col2, col3, col4 = st.columns(4)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.metric(label="평균 F-스코어", value=f"{portfolio['f-score'].mean():.2f}")
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
st.metric(label="평균 GP/A", value=f"{portfolio['GP/A'].mean():.2f}%")
|
||||||
|
|
||||||
|
with col3:
|
||||||
|
avg_market_cap = portfolio['시가총액'].mean() / 1_000_000_000
|
||||||
|
st.metric(label="평균 시가총액", value=f"{avg_market_cap:.1f}십억원")
|
||||||
|
|
||||||
|
with col4:
|
||||||
|
st.metric(label="종목 수", value=len(portfolio))
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_year_end():
|
||||||
|
"""Get the last year's end date."""
|
||||||
|
today = datetime.now()
|
||||||
|
last_year = today.year - 1
|
||||||
|
last_year_end = datetime(last_year, 12, 31)
|
||||||
|
return last_year_end.date()
|
||||||
146
src/ui/pages/value_momentum_page.py
Normal file
146
src/ui/pages/value_momentum_page.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
"""
|
||||||
|
Super Value Momentum strategy page for the Streamlit Quant application.
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
import pandas as pd
|
||||||
|
from db.common import DBManager
|
||||||
|
from strategies.factors.value import get_value_top
|
||||||
|
from strategies.factors.momentum import get_momentum_top
|
||||||
|
|
||||||
|
|
||||||
|
def render_value_momentum_page():
|
||||||
|
"""Render the Super Value Momentum strategy page."""
|
||||||
|
st.title("슈퍼 밸류 모멘텀 전략")
|
||||||
|
|
||||||
|
with st.expander("전략 설명", expanded=False):
|
||||||
|
st.write("""
|
||||||
|
슈퍼 밸류 모멘텀 전략은 밸류와 모멘텀 요소를 결합한 전략입니다.
|
||||||
|
|
||||||
|
- 밸류 요소: PER, PBR과 같은 가치 지표가 낮은 종목 선택
|
||||||
|
- 모멘텀 요소: 최근 가격 상승세가 강한 종목 선택
|
||||||
|
|
||||||
|
두 요소를 결합함으로써 저평가된 종목 중에서도 상승 모멘텀이 있는 종목만 선택하여
|
||||||
|
가치 함정(value trap)을 피하고 더 나은 성과를 기대할 수 있습니다.
|
||||||
|
|
||||||
|
**밸류 요소 지표**
|
||||||
|
- PER (주가수익비율): 주가 / 주당순이익
|
||||||
|
- PBR (주가순자산비율): 주가 / 주당순자산
|
||||||
|
|
||||||
|
**모멘텀 요소 지표**
|
||||||
|
- 6개월 수익률
|
||||||
|
- 12개월 수익률
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Strategy implementation
|
||||||
|
st.write("## 슈퍼 밸류 모멘텀 포트폴리오")
|
||||||
|
|
||||||
|
# Settings
|
||||||
|
col1, col2 = st.columns([1, 2])
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.write("### 설정")
|
||||||
|
value_weight = st.slider("밸류 가중치 (%)", min_value=0, max_value=100, value=50)
|
||||||
|
momentum_weight = 100 - value_weight
|
||||||
|
st.write(f"모멘텀 가중치: {momentum_weight}%")
|
||||||
|
|
||||||
|
num_stocks = st.slider("포트폴리오 종목수", min_value=5, max_value=50, value=20)
|
||||||
|
|
||||||
|
min_market_cap = st.number_input("최소 시가총액 (억원)", min_value=0, value=500)
|
||||||
|
|
||||||
|
exclude_industries = st.multiselect(
|
||||||
|
"제외할 산업",
|
||||||
|
options=["금융업", "보험", "은행", "증권", "지주사"],
|
||||||
|
default=["금융업", "보험", "은행", "증권"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get data
|
||||||
|
db = DBManager()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with st.spinner("데이터 로딩 중..."):
|
||||||
|
# Get value portfolio
|
||||||
|
value_portfolio = get_value_top(20)
|
||||||
|
|
||||||
|
# Get momentum portfolio
|
||||||
|
momentum_portfolio = get_momentum_top(20)
|
||||||
|
|
||||||
|
# Get stock info for filtering
|
||||||
|
stocks = db.get_ticker_list()
|
||||||
|
sectors = db.get_sector_list()
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"데이터 로딩 실패: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Merge data
|
||||||
|
stocks = pd.merge(stocks, sectors, on='종목코드', how='left')
|
||||||
|
|
||||||
|
# Filter by market cap
|
||||||
|
stocks = stocks[stocks['시가총액'] >= min_market_cap * 100000000]
|
||||||
|
|
||||||
|
# Filter by industry
|
||||||
|
if exclude_industries:
|
||||||
|
excluded_mask = stocks['산업'].isin(exclude_industries)
|
||||||
|
stocks = stocks[~excluded_mask]
|
||||||
|
|
||||||
|
# Combine value and momentum scores
|
||||||
|
combined_df = pd.DataFrame(index=stocks['종목코드'])
|
||||||
|
|
||||||
|
# Add value rank (normalized to 0-100, lower is better)
|
||||||
|
if not value_portfolio.empty:
|
||||||
|
value_ranks = value_portfolio.set_index('종목코드')['rank']
|
||||||
|
max_rank = value_ranks.max()
|
||||||
|
combined_df['value_score'] = value_ranks.reindex(combined_df.index).fillna(max_rank)
|
||||||
|
combined_df['value_score'] = 100 - (combined_df['value_score'] / max_rank * 100)
|
||||||
|
|
||||||
|
# Add momentum rank (normalized to 0-100, higher is better)
|
||||||
|
if not momentum_portfolio.empty:
|
||||||
|
momentum_ranks = momentum_portfolio.set_index('종목코드')['rank']
|
||||||
|
max_rank = momentum_ranks.max()
|
||||||
|
combined_df['momentum_score'] = momentum_ranks.reindex(combined_df.index).fillna(max_rank)
|
||||||
|
combined_df['momentum_score'] = 100 - (combined_df['momentum_score'] / max_rank * 100)
|
||||||
|
|
||||||
|
# Calculate combined score
|
||||||
|
combined_df['combined_score'] = (
|
||||||
|
combined_df['value_score'] * (value_weight / 100) +
|
||||||
|
combined_df['momentum_score'] * (momentum_weight / 100)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sort by combined score
|
||||||
|
combined_df = combined_df.sort_values('combined_score', ascending=False)
|
||||||
|
|
||||||
|
# Get top N stocks
|
||||||
|
top_stocks = combined_df.head(num_stocks)
|
||||||
|
|
||||||
|
# Merge with stock info
|
||||||
|
portfolio = pd.merge(
|
||||||
|
top_stocks.reset_index(),
|
||||||
|
stocks,
|
||||||
|
on='종목코드',
|
||||||
|
how='left'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Display portfolio
|
||||||
|
with col2:
|
||||||
|
st.write(f"### 선택된 {len(portfolio)} 종목")
|
||||||
|
display_cols = ['종목코드', '종목명', '현재가', '시가총액', 'PER', 'PBR',
|
||||||
|
'value_score', 'momentum_score', 'combined_score']
|
||||||
|
st.dataframe(portfolio[display_cols], use_container_width=True)
|
||||||
|
|
||||||
|
# Display metrics
|
||||||
|
st.write("### 포트폴리오 지표")
|
||||||
|
col1, col2, col3, col4 = st.columns(4)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
avg_per = portfolio['PER'].replace([float('inf'), -float('inf')], float('nan')).mean()
|
||||||
|
st.metric(label="평균 PER", value=f"{avg_per:.2f}")
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
avg_pbr = portfolio['PBR'].replace([float('inf'), -float('inf')], float('nan')).mean()
|
||||||
|
st.metric(label="평균 PBR", value=f"{avg_pbr:.2f}")
|
||||||
|
|
||||||
|
with col3:
|
||||||
|
avg_market_cap = portfolio['시가총액'].mean() / 1_000_000_000
|
||||||
|
st.metric(label="평균 시가총액", value=f"{avg_market_cap:.1f}십억원")
|
||||||
|
|
||||||
|
with col4:
|
||||||
|
st.metric(label="종목 수", value=len(portfolio))
|
||||||
@ -1 +0,0 @@
|
|||||||
__all__ = ['backtest', 'strategy', 'quantcommon']
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import streamlit as st
|
|
||||||
|
|
||||||
crawling_page = st.Page("crawling.py", title="크롤링")
|
|
||||||
super_quality_page = st.Page("super_quality.py", title="슈퍼 퀄리티 전략")
|
|
||||||
super_value_momentum_page = st.Page("super_value_momentum.py", title="슈퍼 밸류 모멘텀 전략")
|
|
||||||
|
|
||||||
pg = st.navigation({
|
|
||||||
'크롤링': [crawling_page],
|
|
||||||
'전략': [super_quality_page, super_value_momentum_page],
|
|
||||||
})
|
|
||||||
|
|
||||||
st.set_page_config(page_title="콴트 매니저", page_icon=":material/edit:")
|
|
||||||
pg.run()
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import streamlit as st
|
|
||||||
|
|
||||||
st.button(label='동작1')
|
|
||||||
st.button(label='동작2')
|
|
||||||
st.button(label='동작3')
|
|
||||||
st.button(label='동작4')
|
|
||||||
@ -1 +0,0 @@
|
|||||||
__all__ = ['multi_factor', 'f_score']
|
|
||||||
Loading…
x
Reference in New Issue
Block a user