Compare commits

..

4 Commits

Author SHA1 Message Date
218bf72874 chore: 재구성한 example 삭제
All checks were successful
CI Build / build (push) Successful in 5s
2025-03-30 21:20:59 +09:00
57ba71cb3b chore: 리팩토링 전 패키지 삭제 2025-03-30 21:20:34 +09:00
23cada68f7 feat: 구조 리팩토링 2025-03-30 21:19:46 +09:00
67a9b23fa5 feat: 구조 리팩토링 2025-03-30 21:19:41 +09:00
44 changed files with 805 additions and 1240 deletions

124
README.md
View File

@ -1,9 +1,117 @@
# 실행
streamlit run app.py --server.port=20000
# 콴트 매니저 (Quant Manager)
# 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 패키지를 사용한 백테스트 기능 구현

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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']])

View File

@ -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()

View File

@ -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)

View File

@ -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
View 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
View File

View File

@ -3,12 +3,12 @@ import matplotlib.pyplot as plt
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.magic_formula as magic_formula
qc = quantcommon.QuantCommon()
qc = DBManager()
mf = multi_factor.get_multi_factor_top(qc, 20)
magic_formula = magic_formula.get_magic_formula_top(20)

1
src/data/__init__.py Normal file
View File

@ -0,0 +1 @@
__all__ = ['financial', 'krx', 'prices']

View File

@ -6,27 +6,10 @@ import requests as rq
from bs4 import BeautifulSoup
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):
df = df[~df.loc[:, ~df.columns.isin(['계정'])].isna().all(axis=1)]
@ -43,11 +26,12 @@ def clean_fs(df, ticker, frequency):
# ticker 별 재무제표 조회해서 DB에 저장
def process_for_fs(ticker_list):
def process_for_fs():
ticker_list = DBManager().get_ticker_list()
# DB 연결
common = quantcommon.QuantCommon()
engine = common.create_engine()
con = common.connect()
db_manager = DBManager()
engine = db_manager.create_engine()
con = db_manager.connect()
mycursor = con.cursor()
# DB 저장 쿼리
@ -127,5 +111,4 @@ def process_for_fs(ticker_list):
con.close()
if __name__ == '__main__':
tickers = get_ticker_list()
process_for_fs(tickers)
process_for_fs()

View File

@ -8,7 +8,7 @@ import requests as rq
from tqdm import tqdm
from bs4 import BeautifulSoup
from dotenv import load_dotenv
from streamlit_quant import quantcommon
from db.common import DBManager
load_dotenv()
@ -123,7 +123,7 @@ def process_for_total_stock(biz_day):
def save_ticker(ticker):
con = quantcommon.QuantCommon().connect()
con = DBManager().connect()
mycursor = con.cursor()
query = f"""
@ -168,7 +168,7 @@ def process_for_wics(biz_day):
def save_sector(sector):
con = quantcommon.QuantCommon().connect()
con = DBManager().connect()
mycursor = con.cursor()
query = f"""

View File

@ -1,5 +1,3 @@
# 패키지 불러오기
import time
from datetime import date
from io import BytesIO
@ -8,28 +6,12 @@ import pandas as pd
import requests as rq
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 process_for_price(ticker_list):
def process_for_price():
ticker_list = DBManager().get_ticker_list()
# DB 저장 쿼리
query = """
insert into kor_price (날짜, 시가, 고가, 저가, 종가, 거래량, 종목코드)
@ -40,7 +22,7 @@ def process_for_price(ticker_list):
"""
# DB 연결
common = quantcommon.QuantCommon()
common = DBManager()
engine = common.create_engine()
con = common.connect()
@ -54,6 +36,7 @@ def process_for_price(ticker_list):
# 티커 선택
ticker = ticker_list['종목코드'][i]
# todo: 날짜 범위 수정
# 시작일과 종료일
# fr = (date.today() + relativedelta(years=-5)).strftime("%Y%m%d")
to = (date.today()).strftime("%Y%m%d")
@ -97,5 +80,4 @@ def process_for_price(ticker_list):
con.close()
if __name__ == '__main__':
ticker_list = get_ticker_list()
process_for_price(ticker_list)
process_for_price()

1
src/db/__init__.py Normal file
View File

@ -0,0 +1 @@
__all__ = ['common']

View File

@ -7,7 +7,7 @@ from dotenv import load_dotenv
from sqlalchemy import create_engine
class QuantCommon:
class DBManager:
def __init__(self):
load_dotenv()
self.user = os.getenv('DB_USER')

View File

@ -0,0 +1 @@
__all__ = ['composite', 'factors', 'utils']

View File

@ -0,0 +1 @@
__all__ = ['multi_factor', 'multi_factor', 'super_quality', 'super_value_momentum']

View File

@ -1,12 +1,12 @@
import numpy as np
from streamlit_quant import quantcommon
from db.common import DBManager
# 마법 공식 포트폴리오. 밸류와 퀄리티의 조합. 조엘 그린블라트의 '마법공식'
def get_magic_formula_top(count):
qc = quantcommon.QuantCommon()
ticker_list = qc.get_ticker_list()
fs_list = qc.get_expanded_fs_list()
db = DBManager()
ticker_list = db.get_ticker_list()
fs_list = db.get_expanded_fs_list()
fs_list = fs_list.sort_values(['종목코드', '계정', '기준일'])
# TTM 값을 구하기 위해서 rolling() 메소드를 통해 4분기 합 구함. 4분기 데이터가 없는 경우 제외하기 위해서 min_periods=4

View File

@ -1,8 +1,8 @@
from datetime import datetime
import streamlit as st
from src import ui as st
from strategy import f_score
import quantcommon
from strategies.factors import f_score
from db.common import DBManager
# st.write("""
@ -60,7 +60,7 @@ def get_last_year_end():
st.write("투자 전략: 강환국 슈퍼 퀄리티 전략 2.0")
date = get_last_year_end()
data = f_score.get_f_score(quantcommon.QuantCommon(), date)
data = f_score.get_f_score(DBManager(), date)
config = {}

View File

@ -1,3 +1,3 @@
import streamlit as st
from src import ui as st
st.write("슈퍼 밸류 모멘텀 전략 2.0")

View File

@ -0,0 +1 @@
__all__ = ['all_value', 'f_score', 'momentum', 'quality', 'value']

View File

@ -1,14 +1,14 @@
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
from streamlit_quant import quantcommon
from db.common import DBManager
#가치주 포트폴리오. PER, PBR, PCR, PSR, DY
def get_all_value_top(count):
qc = quantcommon.QuantCommon()
ticker_list = qc.get_ticker_list()
value_list = qc.get_value_list()
db = DBManager()
ticker_list = db.get_ticker_list()
value_list = db.get_value_list()
# 가치 지표가 0이하인 경우 nan으로 변경
value_list.loc[value_list[''] <= 0, ''] = np.nan

View File

@ -1,6 +1,6 @@
from datetime import datetime
import pandas as pd
from streamlit_quant import quantcommon
from db.common import DBManager
# 흑자 기업이면 1점(당기순이익)
@ -101,4 +101,4 @@ def get_f_score(qc, base_date):
if __name__ == '__main__':
date = datetime(2024, 12, 31).date()
print(get_f_score(quantcommon.QuantCommon(), date).head(30))
print(get_f_score(DBManager(), date).head(30))

View File

@ -3,7 +3,7 @@ import matplotlib.pyplot as plt
import seaborn as sns
import statsmodels.api as sm
import numpy as np
from streamlit_quant import quantcommon
from db.common import DBManager
def print_graph(values):
@ -29,9 +29,9 @@ def print_graph(values):
# strategy/momentum에 구현
# 모멘텀 포트폴리오. 최근 12개월 수익률이 높은 주식
def get_momentum_top(count):
qc = quantcommon.QuantCommon()
ticker_list = qc.get_ticker_list()
price_list = qc.get_price_list(interval_month=12)
db = DBManager()
ticker_list = db.get_ticker_list()
price_list = db.get_price_list(interval_month=12)
price_pivot = price_list.pivot(index='날짜', columns='종목코드', values='종가')

View File

@ -1,12 +1,12 @@
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from streamlit_quant import quantcommon
from db.common import DBManager
# 퀄리티(우량주) 포트폴리오. 영업수익성이 높은 주식
def get_quality_top(count):
qc = quantcommon.QuantCommon()
qc = DBManager()
ticker_list = qc.get_ticker_list()
fs_list = qc.get_fs_list()

View File

@ -1,13 +1,13 @@
import numpy as np
from streamlit_quant import quantcommon
from db.common import DBManager
#가치주 포트폴리오. PER, PBR이 낮은 회사 20개
def get_value_top(count):
qc = quantcommon.QuantCommon()
ticker_list = qc.get_ticker_list()
value_list = qc.get_value_list()
db = DBManager()
ticker_list = db.get_ticker_list()
value_list = db.get_value_list()
# 가치 지표가 0이하인 경우 nan으로 변경
value_list.loc[value_list[''] <= 0, ''] = np.nan

80
src/strategies/utils.py Normal file
View 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
View File

@ -0,0 +1 @@
__all__ = ['components', 'pages']

View File

@ -0,0 +1 @@
__all__ = ['charts']

189
src/ui/components/charts.py Normal file
View 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
View File

@ -0,0 +1 @@
__all__ = ['data_page', 'quality_page', 'value_momentum_page']

60
src/ui/pages/data_page.py Normal file
View 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}")

View 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()

View 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))

View File

@ -1 +0,0 @@
__all__ = ['backtest', 'strategy', 'quantcommon']

View File

@ -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()

View File

@ -1,6 +0,0 @@
import streamlit as st
st.button(label='동작1')
st.button(label='동작2')
st.button(label='동작3')
st.button(label='동작4')

View File

@ -1 +0,0 @@
__all__ = ['multi_factor', 'f_score']