chore: streamlit 임시 commit
This commit is contained in:
parent
48e76a4106
commit
b1a898c1e8
2
README.md
Normal file
2
README.md
Normal file
@ -0,0 +1,2 @@
|
||||
# 실행
|
||||
streamlit run .\streamlit-quant\app.py --server.port 20000
|
||||
13
streamlit-quant/app.py
Normal file
13
streamlit-quant/app.py
Normal file
@ -0,0 +1,13 @@
|
||||
import streamlit as st
|
||||
|
||||
crawling_page = st.Page("pages/crawling.py", title="크롤링")
|
||||
super_quality_page = st.Page("pages/super_quality.py", title="슈퍼 퀄리티 전략")
|
||||
super_value_momentum_page = st.Page("pages/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()
|
||||
6
streamlit-quant/pages/crawling.py
Normal file
6
streamlit-quant/pages/crawling.py
Normal file
@ -0,0 +1,6 @@
|
||||
import streamlit as st
|
||||
|
||||
st.button(label='동작1')
|
||||
st.button(label='동작2')
|
||||
st.button(label='동작3')
|
||||
st.button(label='동작4')
|
||||
41
streamlit-quant/pages/super_quality.py
Normal file
41
streamlit-quant/pages/super_quality.py
Normal file
@ -0,0 +1,41 @@
|
||||
import streamlit as st
|
||||
|
||||
st.write("""
|
||||
'신F-스코어 3점 + 고GP/A 전략'을 '강환국 슈퍼 퀄리티 전략'이라 명명한다.
|
||||
이 전략은 신F-스코어가 3점인 종목을 매수하되, GP/A로 순위를 매겨서 순위가 높은 종목만 매수하는 것이다.
|
||||
이 경우 한국에서 수익이 어땠을지 분석해보자.
|
||||
연도별로 신F-스코어 3점을 충족하는 종목은 600-700개였다.\n
|
||||
신F-스코어 3점 기업 내에서도 GP/A가 높은 종목이 3점 종목 평균보다 CAGR 기준으로 3-4% 더 높았다.
|
||||
반대로 GP/A가 낮은 종목의 수익률은 상대적으로 저조했다.\n\n
|
||||
---
|
||||
투자 전략: 강환국 슈퍼 퀄리티 전략 1.0\n
|
||||
레벨: 초, 중급\n
|
||||
스타일: 퀄리티\n
|
||||
기대 CAGR: 약 20%\n
|
||||
매수 전략:
|
||||
- 신F-스코어 3점 종목만 매수\n
|
||||
- 여기에 GP/A 순위를 부여, 순위 높은 20-30종목을 매수\n
|
||||
매도 전략: 연 1회 리밸런싱\n\n\n
|
||||
---
|
||||
지금까지 소개한 거의 모든 전략에서 소형주 전략이 전체 주식 수익률보다 높았다.
|
||||
시가총액 하위 20% 종목의 CAGR을 분석해보았다.\n\n
|
||||
---
|
||||
투자 전략: 강환국 슈퍼 퀄리티 전략 2.0\n
|
||||
레벨: 초, 중급\n
|
||||
스타일: 퀄리티\n
|
||||
기대 CAGR: 20% 이상\n
|
||||
매수 전략:
|
||||
아래 조건을 만족하는 20-30종목 매수\n
|
||||
- 신F-스코어 3점 종목만 매수\n
|
||||
- 여기에 GP/A 순위를 부여, 순위 높은 종목만 매수\n
|
||||
- 단, 소형주(시가총액 최저 20%)만 매수\n
|
||||
매도 전략: 연 1회 리밸런싱\n
|
||||
---
|
||||
소형주 중 신F-스코어가 3점인 종목을 찾아보니 2004-2016년 구간에 80-100개 종목이 남았다.
|
||||
그 주식들을 통째로 매수해도 CAGR 34.55%를 벌수 있었다!
|
||||
정말 상당한 수익이다.
|
||||
이 종목들을 다 샀으면 총 1,159개 종목 중 14개가 파산했다.(1.2%)
|
||||
또 1년간 마이너스 수익을 기록한 종목이 29.7%였다.\n
|
||||
신F-스코어가 3점인 종목 중 GP/A가 높은 종목 위주로 매수했으면 (1) CAGR도 조금 개선되고 (2) 최상 30개 종목을 매수했을 경우 선택받은 종목 360개 중 파산한 기업은 단 1개였다.
|
||||
F-스코어와 GP/A는 엄청난 잠재력을 지닌 콤비네이션임이 분명하다.
|
||||
""")
|
||||
3
streamlit-quant/pages/super_value_momentum.py
Normal file
3
streamlit-quant/pages/super_value_momentum.py
Normal file
@ -0,0 +1,3 @@
|
||||
import streamlit as st
|
||||
|
||||
st.write("슈퍼 밸류 모멘텀 전략 2.0")
|
||||
192
streamlit-quant/src/current_stock.py
Normal file
192
streamlit-quant/src/current_stock.py
Normal file
@ -0,0 +1,192 @@
|
||||
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
|
||||
import quantcommon
|
||||
|
||||
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)
|
||||
19
streamlit-quant/strategy/f-score.py
Normal file
19
streamlit-quant/strategy/f-score.py
Normal file
@ -0,0 +1,19 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import quantcommon
|
||||
|
||||
# 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()
|
||||
Loading…
x
Reference in New Issue
Block a user