46 분 소요

주식 관련 데이터 크롤링

1. 섹터, 종목 (kor_ticker, kor_sector)

  • “kor_ticker.pickle” : (krx) 코스피/코스닥 기준일의 종목코드, 종가, 시가총액, EPS, BPS, 업종명(소분류)
  • “kor_sector.pickle” : (wiseindex) 코스피/코스닥 업종명

날짜 크롤링

import requests as rq
from bs4 import BeautifulSoup
import re
#네이버증권-증시자금동향 #deposit : 예탁금
url = 'https://finance.naver.com/sise/sise_deposit.nhn' 
data = rq.get(url)
data # Response[200] : 데이터를 제대로 받아왔음d
<Response [200]>
data_html = BeautifulSoup(data.content)
parse_day = data_html.select_one('span.tah').text #copy element
parse_day
'\xa0\xa0|\xa0\xa02023.04.05'
biz_day = re.findall('[0-9]+', parse_day) #['2023','04','05']
biz_day = "".join(biz_day)
biz_day
'20230405'

(참고)
re.findall() 함수는 첫 번째 인수로 지정된 정규표현식 패턴(‘[0-9]+’ 이 경우)을 두 번째 인수인 문자열(parse_day 이 경우)에서 모두 찾아서, 찾은 모든 비중첩 매치(match)를 리스트로 반환합니다.
[0-9] : 0부터 9까지의 숫자 중 하나를 의미합니다.

  • : 앞의 패턴([0-9])이 하나 이상 반복되는 것을 의미합니다. 따라서, [0-9]+ 패턴은 숫자(0~9)가 하나 이상 연속으로 나오는 문자열과 매치됩니다. 예를 들어, “123”, “45”, “6789” 등이 매치됩니다.

반대로, “abc”, “12a”, “23b4” 등은 숫자가 하나 이상 연속되는 문자열이 아니므로 매치되지 않습니다.

❍ 업종분류 현황 & 개별종목 지표(kor_ticker)

업종분류

# data.krx.co.kr > 주식 > 세부안내 > 업종분류 현황
# 개발자 도구 열고, csv 다운로드 누르면 generate.cmd, download.cmd 파일이 생김
# generate.cmd > Headers > Request URL : 
import requests as rq
from io import BytesIO
import pandas as pd
def crawling_sector(market='STK'): 
    # generate.cmd > Headers > Request URL
    gen_otp_url = 'http://data.krx.co.kr/comm/fileDn/GenerateOTP/generate.cmd' 

    # generate.cmd > Payload
    gen_otp_stk = {
        'mktId' : market, # KOSPI : "STK", KOSDAQ : "KSQ"
        'trdDd' : biz_day,
        'money' : '1', 
        'csvxls_isNo' : 'false', 
        'name' : 'fileDown', 
        'url' : 'dbms/MDC/STAT/standard/MDCSTAT03901'
                  }

    # generate.cmd > Headers > Referer (로봇이 아님을..)
    headers = {'Referer' : 'http://data.krx.co.kr/contents/MDC/MDI/mdiLoader'}

    otp_stk = rq.post(gen_otp_url, gen_otp_stk, headers=headers).text

    # download.cmd > headers > Request URL
    down_url = 'http://data.krx.co.kr/comm/fileDn/download_csv/download.cmd'

    down_sector_stk = rq.post(down_url, {'code':otp_stk}, headers=headers)

    df= pd.read_csv(BytesIO(down_sector_stk.content), encoding='EUC-KR') # Binary String 형태로 변경
    return df

sector_stk = crawling_sector(market='STK') #KOSPI
sector_ksq = crawling_sector(market='KSQ') #KOSDAQ
krx_sector = pd.concat([sector_stk, sector_ksq]).reset_index(drop=True)
krx_sector['종목명'] = krx_sector['종목명'].str.strip() # 공백 제거
krx_sector['기준일'] = biz_day
krx_sector
종목코드 종목명 시장구분 업종명 종가 대비 등락률 시가총액 기준일
0 095570 AJ네트웍스 KOSPI 서비스업 4670 -65 -1.37 218660117650 20230405
1 006840 AK홀딩스 KOSPI 기타금융 18450 120 0.65 244417500450 20230405
2 027410 BGF KOSPI 기타금융 4480 0 0.00 428811223680 20230405
3 282330 BGF리테일 KOSPI 유통업 189300 1700 0.91 3271843405800 20230405
4 138930 BNK금융지주 KOSPI 기타금융 6510 20 0.31 2121838451460 20230405
... ... ... ... ... ... ... ... ... ...
2574 024060 흥구석유 KOSDAQ 유통 5890 -260 -4.23 88350000000 20230405
2575 010240 흥국 KOSDAQ 기계·장비 6260 -40 -0.63 77140076960 20230405
2576 189980 흥국에프엔비 KOSDAQ 음식료·담배 2695 -60 -2.18 108171443765 20230405
2577 037440 희림 KOSDAQ 기타서비스 9430 -20 -0.21 131288939250 20230405
2578 238490 힘스 KOSDAQ 반도체 7220 -60 -0.82 81674343920 20230405

2579 rows × 9 columns

bizday를 기준으로 코스피와 코스닥의 업종명을 확인할 수 있습니다.

개별종목

# 주식 > 세부안내 > PER/PBR/배당수익률(개별종목)

# generate.cmd > Headers > Request URL
gen_otp_url = 'http://data.krx.co.kr/comm/fileDn/GenerateOTP/generate.cmd' 

# generate.cmd > Payload
gen_otp_data = {
    'searchType':'1',
    'mktId' : 'ALL',
    'trdDd' : biz_day, #데이터가 안나올 경우 bizday 문제
    'csvxls_isNo' : 'false', 
    'name' : 'fileDown', 
    'url' : 'dbms/MDC/STAT/standard/MDCSTAT03501'
              }

# generate.cmd > Headers > Referer (로봇이 아님을..)
headers = {'Referer' : 'http://data.krx.co.kr/contents/MDC/MDI/mdiLoader'}
otp = rq.post(gen_otp_url, gen_otp_data, headers=headers).text

# download.cmd > headers > Request URL
down_url = 'http://data.krx.co.kr/comm/fileDn/download_csv/download.cmd'
krx_ind = rq.post(down_url, {'code': otp}, headers=headers)

krx_ind = pd.read_csv(BytesIO(krx_ind.content), encoding='EUC-KR') # Binary String 형태로 변경
krx_ind['종목명'] = krx_ind['종목명'].str.strip()
krx_ind['기준일'] = biz_day
krx_ind

sysmmetric difference

집합론에서, 두 집합의 대칭차 또는 대칭차집합은 둘 중 한 집합에는 속하지만 둘 모두에는 속하지는 않는 원소들의 집합이다.

set(krx_sector['종목명']).symmetric_difference(set(krx_ind['종목명']))
{'ESR켄달스퀘어리츠',
 'GRT',
 'JTC',
 'KB스타리츠',
 'NH올원리츠',
 'NH프라임리츠',
 'SBI핀테크솔루션즈',
 'SK리츠',
 '골든센츄리',
 '글로벌에스엠',
 '네오이뮨텍',
 '디앤디플랫폼리츠',
 '로스웰',
 '롯데리츠',
 '마스턴프리미어리츠',
 '맥쿼리인프라',
 '맵스리얼티1',
 '모두투어리츠',
 '미래에셋글로벌리츠',
 '미래에셋맵스리츠',
 '미투젠',
 '바다로19호',
 '소마젠',
 '신한서부티엔디리츠',
 '신한알파리츠',
 '씨케이에이치',
 '애머릿지',
 '에이리츠',
 '엑세스바이오',
 '엘브이엠씨홀딩스',
 '오가닉티코스메틱',
 '윙입푸드',
 '이리츠코크렙',
 '이스트아시아홀딩스',
 '이지스레지던스리츠',
 '이지스밸류리츠',
 '잉글우드랩',
 '제이알글로벌리츠',
 '컬러레이',
 '케이탑리츠',
 '코람코더원리츠',
 '코람코에너지리츠',
 '코오롱티슈진',
 '크리스탈신소재',
 '프레스티지바이오파마',
 '한국ANKOR유전',
 '한국패러랠',
 '한화리츠',
 '헝셩그룹'}

데이터 머징

# 섹터와 지표 데이터 간 공통된 칼럼
krx_sector.columns.intersection(krx_ind.columns).tolist()
['종목코드', '종목명', '종가', '대비', '등락률', '기준일']
kor_ticker = pd.merge(krx_sector,
                      krx_ind,
                      on= krx_sector.columns.intersection(krx_ind.columns).tolist(),
                      how='outer')
kor_ticker
종목코드 종목명 시장구분 업종명 종가 대비 등락률 시가총액 기준일 EPS PER 선행 EPS 선행 PER BPS PBR 주당배당금 배당수익률
0 095570 AJ네트웍스 KOSPI 서비스업 4670 -65 -1.37 218660117650 20230405 1707.0 2.74 619.0 7.55 8075.0 0.58 270.0 5.78
1 006840 AK홀딩스 KOSPI 기타금융 18450 120 0.65 244417500450 20230405 NaN NaN NaN NaN 45961.0 0.40 200.0 1.08
2 027410 BGF KOSPI 기타금융 4480 0 0.00 428811223680 20230405 684.0 6.55 700.0 6.40 16393.0 0.27 110.0 2.46
3 282330 BGF리테일 KOSPI 유통업 189300 1700 0.91 3271843405800 20230405 8547.0 22.15 12749.0 14.85 46849.0 4.04 3000.0 1.58
4 138930 BNK금융지주 KOSPI 기타금융 6510 20 0.31 2121838451460 20230405 2341.0 2.78 2637.0 2.47 28745.0 0.23 560.0 8.60
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2574 024060 흥구석유 KOSDAQ 유통 5890 -260 -4.23 88350000000 20230405 96.0 61.35 NaN NaN 5426.0 1.09 100.0 1.70
2575 010240 흥국 KOSDAQ 기계·장비 6260 -40 -0.63 77140076960 20230405 1027.0 6.10 NaN NaN 7439.0 0.84 200.0 3.19
2576 189980 흥국에프엔비 KOSDAQ 음식료·담배 2695 -60 -2.18 108171443765 20230405 164.0 16.43 NaN NaN 2004.0 1.34 30.0 1.11
2577 037440 희림 KOSDAQ 기타서비스 9430 -20 -0.21 131288939250 20230405 490.0 19.24 NaN NaN 4769.0 1.98 150.0 1.59
2578 238490 힘스 KOSDAQ 반도체 7220 -60 -0.82 81674343920 20230405 NaN NaN NaN NaN 6598.0 1.09 100.0 1.39

2579 rows × 17 columns

특정 종목들 찾기

# 스팩 / 제x호를 포함하는 종목
cond = kor_ticker['종목명'].str.contains('스팩|제[0-9]+호')
kor_ticker.loc[cond, '종목명']
577      엔에이치스팩19호
960      DB금융스팩10호
961       DB금융스팩8호
962       DB금융스팩9호
984     IBKS제17호스팩
           ...    
2485       하이제7호스팩
2504      한국제10호스팩
2505      한국제11호스팩
2531    한화플러스제2호스팩
2532    한화플러스제3호스팩
Name: 종목명, Length: 72, dtype: object
# 우선주 찾기 : 우리나라 ticker는 끝자리가 0이면 보통주, 0이 아니면 우선주
cond = kor_ticker['종목코드'].str[-1:] != '0'
kor_ticker.loc[cond, '종목명']
6           BYC우
9       CJ4우(전환)
12       CJ씨푸드1우
13           CJ우
15      CJ제일제당 우
          ...   
944        흥국화재우
1203      대호특수강우
1310     루트로닉3우C
1563       소프트센우
2534      해성산업1우
Name: 종목명, Length: 123, dtype: object
# 리츠 : 리츠로 끝나는 종목명
cond = kor_ticker['종목명'].str.endswith('리츠')
kor_ticker.loc[cond, '종목명']
35     ESR켄달스퀘어리츠
64         KB스타리츠
112        NH올원리츠
115       NH프라임리츠
143          SK리츠
344      디앤디플랫폼리츠
350          롯데리츠
364     마스턴프리미어리츠
375        모두투어리츠
383     미래에셋글로벌리츠
384      미래에셋맵스리츠
534     신한서부티엔디리츠
535        신한알파리츠
570          에이리츠
641     이지스레지던스리츠
642       이지스밸류리츠
669      제이알글로벌리츠
717         케이탑리츠
718       코람코더원리츠
719      코람코에너지리츠
871          한화리츠
Name: 종목명, dtype: object

종목 구분 및 저장

import numpy as np

# 한쪽만 있는 데이터
diff = list(set(krx_sector['종목명']).symmetric_difference(set(krx_ind['종목명'])))

# 조건
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}) # nan값을 None으로 변경 : SQL에서는 nan값을 저장할 수 없음
kor_ticker               
종목코드 종목명 시장구분 업종명 종가 시가총액 기준일 EPS 선행EPS BPS 주당배당금 종목구분
0 095570 AJ네트웍스 KOSPI 서비스업 4670 218660117650 20230405 1707.0 619.0 8075.0 270.0 보통주
1 006840 AK홀딩스 KOSPI 기타금융 18450 244417500450 20230405 None None 45961.0 200.0 보통주
2 027410 BGF KOSPI 기타금융 4480 428811223680 20230405 684.0 700.0 16393.0 110.0 보통주
3 282330 BGF리테일 KOSPI 유통업 189300 3271843405800 20230405 8547.0 12749.0 46849.0 3000.0 보통주
4 138930 BNK금융지주 KOSPI 기타금융 6510 2121838451460 20230405 2341.0 2637.0 28745.0 560.0 보통주
... ... ... ... ... ... ... ... ... ... ... ... ...
2574 024060 흥구석유 KOSDAQ 유통 5890 88350000000 20230405 96.0 None 5426.0 100.0 보통주
2575 010240 흥국 KOSDAQ 기계·장비 6260 77140076960 20230405 1027.0 None 7439.0 200.0 보통주
2576 189980 흥국에프엔비 KOSDAQ 음식료·담배 2695 108171443765 20230405 164.0 None 2004.0 30.0 보통주
2577 037440 희림 KOSDAQ 기타서비스 9430 131288939250 20230405 490.0 None 4769.0 150.0 보통주
2578 238490 힘스 KOSDAQ 반도체 7220 81674343920 20230405 None None 6598.0 100.0 보통주

2579 rows × 12 columns

# null값 확인
kor_ticker.isnull().sum()
종목코드        0
종목명         0
시장구분        0
업종명         0
종가          0
시가총액        0
기준일         0
EPS       960
선행EPS    2069
BPS       274
주당배당금      49
종목구분        0
dtype: int64
temp = pd.read_pickle("./data/kor_ticker.pickle")
temp = pd.concat([temp, kor_ticker]).drop_duplicates().reset_index(drop=True)
temp
종목코드 종목명 시장구분 종가 시가총액 기준일 EPS 선행EPS BPS 주당배당금 종목구분
0 095570 AJ네트웍스 KOSPI 4735 221703566825 20230404 1707.0 619.0 8075.0 270.0 보통주
1 006840 AK홀딩스 KOSPI 18330 242827793130 20230404 None None 45961.0 200.0 보통주
2 027410 BGF KOSPI 4480 428811223680 20230404 684.0 700.0 16393.0 110.0 보통주
3 282330 BGF리테일 KOSPI 187600 3242460765600 20230404 8547.0 13676.0 46849.0 3000.0 보통주
4 138930 BNK금융지주 KOSPI 6490 2115319746540 20230404 2341.0 2644.0 28745.0 560.0 보통주
... ... ... ... ... ... ... ... ... ... ... ...
5153 024060 흥구석유 KOSDAQ 5890 88350000000 20230405 96.0 None 5426.0 100.0 보통주
5154 010240 흥국 KOSDAQ 6260 77140076960 20230405 1027.0 None 7439.0 200.0 보통주
5155 189980 흥국에프엔비 KOSDAQ 2695 108171443765 20230405 164.0 None 2004.0 30.0 보통주
5156 037440 희림 KOSDAQ 9430 131288939250 20230405 490.0 None 4769.0 150.0 보통주
5157 238490 힘스 KOSDAQ 7220 81674343920 20230405 None None 6598.0 100.0 보통주

5158 rows × 11 columns

temp.to_pickle("./data/kor_ticker.pickle")
temp = pd.read_pickle("./data/kor_ticker.pickle")
temp
종목코드 종목명 시장구분 업종명 종가 시가총액 기준일 EPS 선행EPS BPS 주당배당금 종목구분
0 095570 AJ네트웍스 KOSPI 서비스업 4670 218660117650 20230405 1707.0 619.0 8075.0 270.0 보통주
1 006840 AK홀딩스 KOSPI 기타금융 18450 244417500450 20230405 None None 45961.0 200.0 보통주
2 027410 BGF KOSPI 기타금융 4480 428811223680 20230405 684.0 700.0 16393.0 110.0 보통주
3 282330 BGF리테일 KOSPI 유통업 189300 3271843405800 20230405 8547.0 12749.0 46849.0 3000.0 보통주
4 138930 BNK금융지주 KOSPI 기타금융 6510 2121838451460 20230405 2341.0 2637.0 28745.0 560.0 보통주
... ... ... ... ... ... ... ... ... ... ... ... ...
2574 024060 흥구석유 KOSDAQ 유통 5890 88350000000 20230405 96.0 None 5426.0 100.0 보통주
2575 010240 흥국 KOSDAQ 기계·장비 6260 77140076960 20230405 1027.0 None 7439.0 200.0 보통주
2576 189980 흥국에프엔비 KOSDAQ 음식료·담배 2695 108171443765 20230405 164.0 None 2004.0 30.0 보통주
2577 037440 희림 KOSDAQ 기타서비스 9430 131288939250 20230405 490.0 None 4769.0 150.0 보통주
2578 238490 힘스 KOSDAQ 반도체 7220 81674343920 20230405 None None 6598.0 100.0 보통주

2579 rows × 12 columns

temp['업종명'].unique()
array(['서비스업', '기타금융', '유통업', '섬유의복', '운수창고업', '음식료품', '증권', '보험', '전기전자',
       '건설업', '화학', '철강금속', '광업', '운수장비', '기계', '의약품', '비금속광물', '통신업',
       '기타제조업', '전기가스업', '종이목재', '은행', '의료정밀', '농업, 임업 및 어업', '기계·장비',
       '금융', '반도체', '통신장비', '운송장비·부품', '방송서비스', '기타서비스', '유통', '제약', '건설',
       '일반전기전자', '출판·매체복제', '섬유·의류', '오락·문화', '금속', '소프트웨어', 'IT부품',
       '디지털컨텐츠', '기타제조', '비금속', '운송', '인터넷', '정보기기', '음식료·담배', '종이·목재',
       '의료·정밀기기', '통신서비스', '컴퓨터서비스', '전기·가스·수도', '숙박·음식'], dtype=object)

❍ 섹터 데이터(kor_sector)

### wiseindex.com/index
### WICS > 에너지 > Components > 개발자도구 > 날짜 선택 > 개발자도구 Headers 
### json형식 : https://www.wiseindex.com/Index/GetIndexComponets?ceil_yn=0&dt=20230224&sec_cd=G10
import json
import requests as rq
import pandas as pd
url = f'''https://www.wiseindex.com/Index/GetIndexComponets?ceil_yn=0&dt={biz_day}&sec_cd=G10'''
data = rq.get(url).json()
data.keys()
dict_keys(['info', 'list', 'sector', 'size'])
# 구성종목 data['list']
# 섹터코드 data['sector']
data['list'][0]
{'IDX_CD': 'G10',
 'IDX_NM_KOR': 'WICS 에너지',
 'ALL_MKT_VAL': 22080412,
 'CMP_CD': '096770',
 'CMP_KOR': 'SK이노베이션',
 'MKT_VAL': 9273926,
 'WGT': 42.0,
 'S_WGT': 42.0,
 'CAL_WGT': 1.0,
 'SEC_CD': 'G10',
 'SEC_NM_KOR': '에너지',
 'SEQ': 1,
 'TOP60': 3,
 'APT_SHR_CNT': 51780716}
data_pd = pd.json_normalize(data['list'])
data_pd.head()
IDX_CD IDX_NM_KOR ALL_MKT_VAL CMP_CD CMP_KOR MKT_VAL WGT S_WGT CAL_WGT SEC_CD SEC_NM_KOR SEQ TOP60 APT_SHR_CNT
0 G10 WICS 에너지 22080412 096770 SK이노베이션 9273926 42.00 42.00 1.0 G10 에너지 1 3 51780716
1 G10 WICS 에너지 22080412 010950 S-Oil 3399100 15.39 57.39 1.0 G10 에너지 2 3 41655633
2 G10 WICS 에너지 22080412 267250 HD현대 2601084 11.78 69.17 1.0 G10 에너지 3 3 44236128
3 G10 WICS 에너지 22080412 078930 GS 1989504 9.01 78.19 1.0 G10 에너지 4 3 49245150
4 G10 WICS 에너지 22080412 112610 씨에스윈드 1719244 7.79 85.97 1.0 G10 에너지 5 3 23615986
import json
import time
import requests as rq
import pandas as pd
from tqdm import tqdm

sector_code = ['G25','G35','G50','G40','G10','G20','G55','G30','G15','G45']
data_sector = []

for i in tqdm(sector_code):
    url = f'''https://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)
100%|███████████████████████████████████████████| 10/10 [00:28<00:00,  2.81s/it]
# 데이터프레임 10개를 품은 리스트
type(data_sector)
list
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['기준일'])
kor_sector
IDX_CD CMP_CD CMP_KOR SEC_NM_KOR 기준일
0 G25 005380 현대차 경기관련소비재 2023-04-05
1 G25 000270 기아 경기관련소비재 2023-04-05
2 G25 012330 현대모비스 경기관련소비재 2023-04-05
3 G25 051900 LG생활건강 경기관련소비재 2023-04-05
4 G25 090430 아모레퍼시픽 경기관련소비재 2023-04-05
... ... ... ... ... ...
643 G45 009140 경인전자 IT 2023-04-05
644 G45 321260 프로이천 IT 2023-04-05
645 G45 033200 모아텍 IT 2023-04-05
646 G45 038340 MIT IT 2023-04-05
647 G45 174880 장원테크 IT 2023-04-05

2384 rows × 5 columns

kor_sector['SEC_NM_KOR'].unique()
array(['경기관련소비재', '건강관리', '커뮤니케이션서비스', '금융', '에너지', '산업재', '유틸리티',
       '필수소비재', '소재', 'IT'], dtype=object)
temp = pd.read_pickle('./data/kor_sector.pickle')
temp = pd.concat([temp, kor_sector]).drop_duplicates().reset_index(drop=True)
temp
IDX_CD CMP_CD CMP_KOR SEC_NM_KOR 기준일
0 G25 005380 현대차 경기관련소비재 2023-04-05
1 G25 000270 기아 경기관련소비재 2023-04-05
2 G25 012330 현대모비스 경기관련소비재 2023-04-05
3 G25 051900 LG생활건강 경기관련소비재 2023-04-05
4 G25 090430 아모레퍼시픽 경기관련소비재 2023-04-05
... ... ... ... ... ...
2379 G45 009140 경인전자 IT 2023-04-05
2380 G45 321260 프로이천 IT 2023-04-05
2381 G45 033200 모아텍 IT 2023-04-05
2382 G45 038340 MIT IT 2023-04-05
2383 G45 174880 장원테크 IT 2023-04-05

2384 rows × 5 columns

temp.to_pickle('./data/kor_sector.pickle')

❍ 월말기준으로 크롤링 (kor_ticker, kor_sector)

import datetime
import pandas as pd
from pandas.tseries.offsets import BMonthEnd

# create a date range from January 2010 to today
date_range = pd.date_range(start='2010-01-01', end='2023-04-30', freq='BM')

# get the last business day of each month
last_business_days = [d.date() for d in date_range - BMonthEnd()]

# 12월 마지막 영업일은 주식시장 문 닫음
last_business_days = [i-datetime.timedelta(days=1) if i.month==12 else i for i in last_business_days]
last_business_days = [i.strftime("%Y%m%d") for i in last_business_days]

# 수동조절
manual_dict = {'20121230':'20121228','20140131':'20140129','20181230':'20181228', '20200430':'20200429', '20200930':'20200929', '20220131':'20220128'}
last_business_days = [manual_dict[i] if i in manual_dict.keys() else i for i in last_business_days]

print(last_business_days)
dates = last_business_days
['20091230', '20100129', '20100226', '20100331', '20100430', '20100531', '20100630', '20100730', '20100831', '20100930', '20101029', '20101130', '20101230', '20110131', '20110228', '20110331', '20110429', '20110531', '20110630', '20110729', '20110831', '20110930', '20111031', '20111130', '20111229', '20120131', '20120229', '20120330', '20120430', '20120531', '20120629', '20120731', '20120831', '20120928', '20121031', '20121130', '20121228', '20130131', '20130228', '20130329', '20130430', '20130531', '20130628', '20130731', '20130830', '20130930', '20131031', '20131129', '20131230', '20140129', '20140228', '20140331', '20140430', '20140530', '20140630', '20140731', '20140829', '20140930', '20141031', '20141128', '20141230', '20150130', '20150227', '20150331', '20150430', '20150529', '20150630', '20150731', '20150831', '20150930', '20151030', '20151130', '20151230', '20160129', '20160229', '20160331', '20160429', '20160531', '20160630', '20160729', '20160831', '20160930', '20161031', '20161130', '20161229', '20170131', '20170228', '20170331', '20170428', '20170531', '20170630', '20170731', '20170831', '20170929', '20171031', '20171130', '20171228', '20180131', '20180228', '20180330', '20180430', '20180531', '20180629', '20180731', '20180831', '20180928', '20181031', '20181130', '20181228', '20190131', '20190228', '20190329', '20190430', '20190531', '20190628', '20190731', '20190830', '20190930', '20191031', '20191129', '20191230', '20200131', '20200228', '20200331', '20200429', '20200529', '20200630', '20200731', '20200831', '20200929', '20201030', '20201130', '20201230', '20210129', '20210226', '20210331', '20210430', '20210531', '20210630', '20210730', '20210831', '20210930', '20211029', '20211130', '20211230', '20220128', '20220228', '20220331', '20220429', '20220531', '20220630', '20220729', '20220831', '20220930', '20221031', '20221130', '20221229', '20230131', '20230228', '20230331']

kor_ticker

def crawling_kor_ticker(biz_day):
    def crawling_sector(market='STK'): 
        # generate.cmd > Headers > Request URL
        gen_otp_url = 'http://data.krx.co.kr/comm/fileDn/GenerateOTP/generate.cmd' 

        # generate.cmd > Payload
        gen_otp_stk = {
            'mktId' : market, # KOSPI : "STK", KOSDAQ : "KSQ"
            'trdDd' : biz_day,
            'money' : '1', 
            'csvxls_isNo' : 'false', 
            'name' : 'fileDown', 
            'url' : 'dbms/MDC/STAT/standard/MDCSTAT03901'
                      }

        # generate.cmd > Headers > Referer (로봇이 아님을..)
        headers = {'Referer' : 'http://data.krx.co.kr/contents/MDC/MDI/mdiLoader'}

        otp_stk = rq.post(gen_otp_url, gen_otp_stk, headers=headers).text

        # download.cmd > headers > Request URL
        down_url = 'http://data.krx.co.kr/comm/fileDn/download_csv/download.cmd'

        down_sector_stk = rq.post(down_url, {'code':otp_stk}, headers=headers)

        df= pd.read_csv(BytesIO(down_sector_stk.content), encoding='EUC-KR') # Binary String 형태로 변경
        return df

    sector_stk = crawling_sector(market='STK') #KOSPI
    sector_ksq = crawling_sector(market='KSQ') #KOSDAQ
    krx_sector = pd.concat([sector_stk, sector_ksq]).reset_index(drop=True)
    krx_sector['종목명'] = krx_sector['종목명'].str.strip() # 공백 제거
    krx_sector['기준일'] = biz_day

    # generate.cmd > Headers > Request URL
    gen_otp_url = 'http://data.krx.co.kr/comm/fileDn/GenerateOTP/generate.cmd' 

    # generate.cmd > Payload
    gen_otp_data = {
        'searchType':'1',
        'mktId' : 'ALL',
        'trdDd' : biz_day, #데이터가 안나올 경우 bizday 문제
        'csvxls_isNo' : 'false', 
        'name' : 'fileDown', 
        'url' : 'dbms/MDC/STAT/standard/MDCSTAT03501'
                  }

    # generate.cmd > Headers > Referer (로봇이 아님을..)
    headers = {'Referer' : 'http://data.krx.co.kr/contents/MDC/MDI/mdiLoader'}
    otp = rq.post(gen_otp_url, gen_otp_data, headers=headers).text

    # download.cmd > headers > Request URL
    down_url = 'http://data.krx.co.kr/comm/fileDn/download_csv/download.cmd'
    krx_ind = rq.post(down_url, {'code': otp}, headers=headers)

    krx_ind = pd.read_csv(BytesIO(krx_ind.content), encoding='EUC-KR') # Binary String 형태로 변경
    krx_ind['종목명'] = krx_ind['종목명'].str.strip()
    krx_ind['기준일'] = biz_day

    # 합치기
    kor_ticker = pd.merge(krx_sector, 
                          krx_ind,
                          on = krx_sector.columns.intersection(krx_ind.columns).tolist(), 
                          how='outer')

    # 한쪽만 있는 데이터
    diff = list(set(krx_sector['종목명']).symmetric_difference(set(krx_ind['종목명'])))

    # 조건
    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}) # nan값을 None으로 변경 : SQL에 nan을 저장할 수 없음
    return kor_ticker
kor_ticker = pd.DataFrame(columns = ['종목코드', '종목명', '시장구분', '업종명', 
                             '종가', '시가총액', '기준일', 'EPS', '선행EPS',
                             'BPS', '주당배당금', '종목구분'])
kor_ticker
종목코드 종목명 시장구분 업종명 종가 시가총액 기준일 EPS 선행EPS BPS 주당배당금 종목구분
for date in tqdm(dates):
    temp = crawling_kor_ticker(date)
    kor_ticker = pd.concat([kor_ticker,temp])
kor_ticker = kor_ticker.drop_duplicates().reset_index(drop=True)
kor_ticker 
100%|█████████████████████████████████████████| 160/160 [09:09<00:00,  3.44s/it]
종목코드 종목명 시장구분 업종명 종가 시가총액 기준일 EPS 선행EPS BPS 주당배당금 종목구분
0 004560 BNG스틸 KOSPI 철강금속 8650 130431715150.0 20091230 None None 11755.0 0.0 보통주
1 004565 BNG스틸우 KOSPI 철강금속 8370 919461240.0 20091230 None None None 0.0 우선주
2 001460 BYC KOSPI 섬유의복 125000 78076875000.0 20091230 7741.0 None 288027.0 750.0 보통주
3 001465 BYC우 KOSPI 섬유의복 55000 11846175000.0 20091230 None None None 800.0 우선주
4 084680 C&우방랜드 KOSPI 서비스업 1130 37149976050.0 20091230 None None 656.0 0.0 보통주
... ... ... ... ... ... ... ... ... ... ... ... ...
428558 024060 흥구석유 KOSDAQ 유통 5600 84000000000 20230331 96.0 None 5426.0 100.0 보통주
428559 010240 흥국 KOSDAQ 기계·장비 6140 75661353440 20230331 1027.0 None 7439.0 200.0 보통주
428560 189980 흥국에프엔비 KOSDAQ 음식료·담배 2720 109174889440 20230331 164.0 None 2004.0 30.0 보통주
428561 037440 희림 KOSDAQ 기타서비스 9120 126972972000 20230331 490.0 None 4769.0 150.0 보통주
428562 238490 힘스 KOSDAQ 반도체 7100 80316875600 20230331 None None 6598.0 100.0 보통주

428563 rows × 12 columns

kor_ticker.isnull().sum()
종목코드          0
종목명           0
시장구분      87536
업종명       87536
종가            0
시가총액      87536
기준일           0
EPS      215076
선행EPS    419184
BPS      124273
주당배당금     95744
종목구분          0
dtype: int64
# kor_ticker.to_pickle('./data/kor_ticker.pickle')
kor_ticker_history = pd.read_pickle('./data/kor_ticker.pickle')
kor_ticker_history = pd.concat([kor_ticker_history, kor_ticker]).drop_duplicates().reset_index(drop=True)
kor_ticker_history
#kor_ticker_history.to_pickle('./data/kor_ticker.pickle')
종목코드 종목명 시장구분 업종명 종가 시가총액 기준일 EPS 선행EPS BPS 주당배당금 종목구분
0 004560 BNG스틸 KOSPI 철강금속 8650 130431715150.0 20091230 None None 11755.0 0.0 보통주
1 004565 BNG스틸우 KOSPI 철강금속 8370 919461240.0 20091230 None None None 0.0 우선주
2 001460 BYC KOSPI 섬유의복 125000 78076875000.0 20091230 7741.0 None 288027.0 750.0 보통주
3 001465 BYC우 KOSPI 섬유의복 55000 11846175000.0 20091230 None None None 800.0 우선주
4 084680 C&우방랜드 KOSPI 서비스업 1130 37149976050.0 20091230 None None 656.0 0.0 보통주
... ... ... ... ... ... ... ... ... ... ... ... ...
428558 024060 흥구석유 KOSDAQ 유통 5600 84000000000 20230331 96.0 None 5426.0 100.0 보통주
428559 010240 흥국 KOSDAQ 기계·장비 6140 75661353440 20230331 1027.0 None 7439.0 200.0 보통주
428560 189980 흥국에프엔비 KOSDAQ 음식료·담배 2720 109174889440 20230331 164.0 None 2004.0 30.0 보통주
428561 037440 희림 KOSDAQ 기타서비스 9120 126972972000 20230331 490.0 None 4769.0 150.0 보통주
428562 238490 힘스 KOSDAQ 반도체 7100 80316875600 20230331 None None 6598.0 100.0 보통주

428563 rows × 12 columns

kor_sector

  • 2010년 1월 데이터부터 존재함
def crawling_kor_sector(biz_day):
    sector_code = ['G25','G35','G50','G40','G10','G20','G55','G30','G15','G45']
    data_sector = []

    for i in sector_code:
        url = f'''https://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['기준일'])
    return kor_sector
kor_sector = pd.DataFrame(columns = ['IDX_CD','CMP_CD','CMP_KOR','SEC_NM_KOR','기준일'])
kor_sector
IDX_CD CMP_CD CMP_KOR SEC_NM_KOR 기준일
for date in tqdm(dates[1:]): # 2010-01 부터
    temp = crawling_kor_sector(date)
    kor_sector = pd.concat([kor_sector,temp])
kor_sector = kor_sector.drop_duplicates().reset_index(drop=True)
kor_sector 
100%|█████████████████████████████████████████| 158/158 [17:09<00:00,  6.52s/it]
IDX_CD CMP_CD CMP_KOR SEC_NM_KOR 기준일
0 G25 005380 현대차 경기관련소비재 2010-01-29
1 G25 012330 현대모비스 경기관련소비재 2010-01-29
2 G25 004170 신세계 경기관련소비재 2010-01-29
3 G25 000270 기아 경기관련소비재 2010-01-29
4 G25 023530 롯데쇼핑 경기관련소비재 2010-01-29
... ... ... ... ... ...
294780 G45 174880 장원테크 IT 2023-02-28
294781 G45 043590 크로바하이텍 IT 2023-02-28
294782 G45 033200 모아텍 IT 2023-02-28
294783 G45 321260 프로이천 IT 2023-02-28
294784 G45 038340 MIT IT 2023-02-28

294785 rows × 5 columns

#kor_sector.to_pickle("./data/kor_sector.pickle")
kor_sector_history = pd.read_pickle("./data/kor_sector.pickle")
kor_sector_history = pd.concat([kor_sector_history, kor_sector]).drop_duplicates().reset_index(drop=True)
kor_sector_history
# kor_sector_history.to_pickle('./data/kor_sector.pickle')
IDX_CD CMP_CD CMP_KOR SEC_NM_KOR 기준일
0 G25 005380 현대차 경기관련소비재 2010-01-29
1 G25 012330 현대모비스 경기관련소비재 2010-01-29
2 G25 004170 신세계 경기관련소비재 2010-01-29
3 G25 000270 기아 경기관련소비재 2010-01-29
4 G25 023530 롯데쇼핑 경기관련소비재 2010-01-29
... ... ... ... ... ...
297166 G45 009140 경인전자 IT 2023-03-31
297167 G45 321260 프로이천 IT 2023-03-31
297168 G45 033200 모아텍 IT 2023-03-31
297169 G45 038340 MIT IT 2023-03-31
297170 G45 174880 장원테크 IT 2023-03-31

297171 rows × 5 columns


2. 주식 가격 크롤링 (stock_price)

# 수정주가가 필요
# 네이버 증권을 추천함 (차트는 수정주가를 기준으로 함) 
# 네이버증권 > 개발자도구 > 차트 > 일 
# https://api.finance.naver.com/siseJson.naver?symbol=005930&requestType=1&startTime=20210414&endTime=20211220&timeframe=day
import pandas as pd
from dateutil.relativedelta import relativedelta
import requests as rq
from io import BytesIO
from datetime import date
import time
from tqdm import tqdm

kor_sector = pd.read_pickle('./data/kor_sector.pickle')
kor_ticker = pd.read_pickle('./data/kor_ticker.pickle')

# 마지막 날 기준으로
kor_ticker_last_day = kor_ticker['기준일'].unique()[-1]
kor_sector_last_day = kor_sector['기준일'].unique()[-1]
cond = (kor_ticker['종목구분'] == '보통주') & \
        (kor_ticker['기준일']==kor_ticker_last_day)
ticker_list = kor_ticker[cond].reset_index(drop=True)

ticker_name_list = ticker_list[['종목코드','종목명']].set_index("종목코드")
ticker_name_list
종목명
종목코드
095570 AJ네트웍스
006840 AK홀딩스
027410 BGF
282330 BGF리테일
138930 BNK금융지주
... ...
024060 흥구석유
010240 흥국
189980 흥국에프엔비
037440 희림
238490 힘스

2334 rows × 1 columns

❍ 한종목 가격 가져오기

# 조회 시작일 ~ 종료일
fr = (date.today() + relativedelta(years=-5)).strftime("%Y%m%d")
to = (date.today()).strftime("%Y%m%d")
print(fr, to)

ticker = ticker_name_list.index[0]
print(ticker)

url = f'''https://fchart.stock.naver.com/siseJson.naver?symbol={ticker}&requestType=1&startTime={fr}&endTime={to}&timeframe=day'''
data = rq.get(url).content
data_price = pd.read_csv(BytesIO(data))
data_price
20180408 20230408
095570
[['날짜' '시가' '고가' '저가' '종가' '거래량' '외국인소진율'] Unnamed: 7
0 ["20180409" 6870.0 7030.0 6810.0 6870.0 26772.0 8.34] NaN
1 ["20180410" 6870.0 6960.0 6590.0 6940.0 70302.0 8.33] NaN
2 ["20180411" 6970.0 7000.0 6860.0 6860.0 31123.0 8.32] NaN
3 ["20180412" 6810.0 7000.0 6810.0 6950.0 19823.0 8.33] NaN
4 ["20180413" 6950.0 7000.0 6790.0 6990.0 35496.0 8.33] NaN
... ... ... ... ... ... ... ... ...
1229 ["20230404" 4680.0 4800.0 4580.0 4735.0 163332.0 4.03] NaN
1230 ["20230405" 4685.0 4765.0 4635.0 4670.0 147912.0 3.93] NaN
1231 ["20230406" 4685.0 4730.0 4590.0 4660.0 189535.0 3.9] NaN
1232 ["20230407" 4715.0 4745.0 4615.0 4660.0 156870.0 3.9] NaN
1233 ] NaN NaN NaN NaN NaN NaN NaN

1234 rows × 8 columns

import re
price = data_price.iloc[:, 0:6]
price.columns = ['날짜','시가','고가','저가','종가','거래량']
price = price.dropna()
price['날짜'] = price['날짜'].str.extract('(\d+)') #숫자만 뽑기
price['날짜'] = pd.to_datetime(price['날짜'])
price['종목코드'] = ticker
price.tail(3)
날짜 시가 고가 저가 종가 거래량 종목코드
1230 2023-04-05 4685.0 4765.0 4635.0 4670.0 147912.0 095570
1231 2023-04-06 4685.0 4730.0 4590.0 4660.0 189535.0 095570
1232 2023-04-07 4715.0 4745.0 4615.0 4660.0 156870.0 095570

❍ 여러종목 가격 한번에 불러오기

stock_price = pd.read_pickle('./data/stock_price.pickle')
stock_price.tail(1)
날짜 시가 고가 저가 종가 거래량 종목코드 종목명
5873725 2023-03-31 4130.0 4260.0 4030.0 4220.0 637385.0 450140 코오롱모빌리티그룹
df = pd.DataFrame(columns = ['날짜','시가','고가','저가','종가','거래량','종목코드'])
error_list = []

#fr = "20100101"
fr = (pd.to_datetime(sorted(stock_price['날짜'].unique())[-1]) + relativedelta(days=1)).strftime("%Y%m%d")
to = (date.today()).strftime("%Y%m%d")
print(fr, to)


for i in tqdm(range(0, len(ticker_list))):
#for i in tqdm(range(0, 2)):    
    ticker = ticker_list['종목코드'][i]
#    ticker = ticker_list[0][i]

    try : 
        url = f'''https://fchart.stock.naver.com/siseJson.naver?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
        df = pd.concat([df, price])
    
    except:
        error_list.append(ticker)
    
    time.sleep(2)
print('error list : ')    
print(error_list)
20230401 20230408


100%|███████████████████████████████████████████████████████████████████████████████████████████████████████| 2334/2334 [02:57<00:00, 13.12it/s]

error list : 
[]

stock_price = pd.concat([stock_price, df]).sort_values(by=['날짜','종목코드']).drop_duplicates(['날짜','종목코드'],keep='last').reset_index(drop=True)

# 종목명 매핑
stock_price['종목명'] = stock_price['종목코드'].map(ticker_name_list['종목명'])
stock_price
날짜 시가 고가 저가 종가 거래량 종목코드 종목명
0 2010-01-04 7540.0 7820.0 7480.0 7520.0 177197.0 000020 동화약품
1 2010-01-04 3419.0 3724.0 3419.0 3635.0 1123647.0 000040 KR모터스
2 2010-01-04 9883.0 10186.0 9768.0 9921.0 721.0 000050 경방
3 2010-01-04 46309.0 46516.0 45789.0 46517.0 13390.0 000070 삼양홀딩스
4 2010-01-04 40100.0 40300.0 39600.0 39950.0 100021.0 000080 하이트진로
... ... ... ... ... ... ... ... ...
5882123 2023-04-07 10820.0 10920.0 10250.0 10330.0 249003.0 425420 티에프이
5882124 2023-04-07 10250.0 10480.0 10030.0 10450.0 336032.0 441270 파인엠텍
5882125 2023-04-07 10360.0 10480.0 9980.0 10030.0 265553.0 446070 유니드비티플러스
5882126 2023-04-07 4795.0 4890.0 4480.0 4560.0 1546455.0 450140 코오롱모빌리티그룹
5882127 2023-04-07 2050.0 2090.0 2040.0 2045.0 6223645.0 452260 한화갤러리아

5882128 rows × 8 columns

%%time
stock_price_pivot = stock_price.pivot(index='날짜', columns='종목명', values='종가')
stock_price_pivot
CPU times: user 756 ms, sys: 123 ms, total: 879 ms
Wall time: 879 ms
종목명 3S AJ네트웍스 AK홀딩스 APS홀딩스 AP시스템 AP위성 BGF BGF리테일 BGF에코머티리얼즈 BNK금융지주 ... 휴온스 휴온스글로벌 휴젤 흥구석유 흥국 흥국에프엔비 흥국화재 흥아해운 희림 힘스
날짜
2010-01-04 1282.0 NaN 10331.0 3282.0 NaN NaN NaN NaN NaN NaN ... NaN 5108.0 NaN 2655.0 2330.0 NaN 6850.0 6883.0 10900.0 NaN
2010-01-05 1277.0 NaN 10206.0 3278.0 NaN NaN NaN NaN NaN NaN ... NaN 5139.0 NaN 2670.0 2195.0 NaN 6630.0 6839.0 10550.0 NaN
2010-01-06 1307.0 NaN 10206.0 3275.0 NaN NaN NaN NaN NaN NaN ... NaN 5139.0 NaN 2700.0 2150.0 NaN 6690.0 6795.0 10200.0 NaN
2010-01-07 1431.0 NaN 10300.0 3323.0 NaN NaN NaN NaN NaN NaN ... NaN 5121.0 NaN 2680.0 2150.0 NaN 6500.0 7344.0 10100.0 NaN
2010-01-08 1391.0 NaN 10362.0 3442.0 NaN NaN NaN NaN NaN NaN ... NaN 5121.0 NaN 2680.0 2150.0 NaN 6440.0 7289.0 10100.0 NaN
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2023-04-03 2305.0 4650.0 18180.0 14920.0 22450.0 21650.0 4520.0 180700.0 7890.0 6490.0 ... 31900.0 19740.0 126900.0 5720.0 6200.0 2760.0 3180.0 1379.0 9300.0 7110.0
2023-04-04 2355.0 4735.0 18330.0 14650.0 21900.0 21800.0 4480.0 187600.0 8040.0 6490.0 ... 33200.0 19740.0 130800.0 6150.0 6300.0 2755.0 3200.0 1385.0 9450.0 7280.0
2023-04-05 2345.0 4670.0 18450.0 14860.0 22050.0 22150.0 4480.0 189300.0 8250.0 6510.0 ... 32900.0 19920.0 127400.0 5890.0 6260.0 2695.0 3210.0 1388.0 9430.0 7220.0
2023-04-06 2290.0 4660.0 18480.0 14180.0 21350.0 20850.0 4410.0 189400.0 7970.0 6440.0 ... 32800.0 19790.0 129200.0 5740.0 6250.0 2615.0 3205.0 1368.0 9310.0 7050.0
2023-04-07 2400.0 4660.0 18900.0 14400.0 21750.0 21300.0 4465.0 183800.0 8080.0 6680.0 ... 32850.0 20300.0 135400.0 5750.0 6150.0 2620.0 3190.0 1352.0 9480.0 7260.0

3275 rows × 2334 columns

# stock_price.to_pickle('./data/stock_price.pickle')
# stock_price_pivot.to_pickle('./data/stock_price_pivot.pickle')

3. ETF 가격 크롤링 (etf_price)

from pykrx import stock
import requests
import json
from pandas.io.json import json_normalize
import pandas as pd
import datetime

date = datetime.datetime.now().strftime("%Y%m%d")
tickers = stock.get_etf_ticker_list(date)
tickers=pd.DataFrame(tickers,columns=['종목코드'])

url = 'https://finance.naver.com/api/sise/etfItemList.nhn'
json_data = json.loads(requests.get(url).text)
df = json_normalize(json_data['result']['etfItemList'])
df=df[['itemcode','itemname']]
df=df.rename(columns={"itemcode": "종목코드", "itemname": "종목명"})

etf=pd.merge(left=tickers,right=df,how='left',on='종목코드' )
etf
/var/folders/vr/g5rxclqn2qb11v47jh4p2lrw0000gn/T/ipykernel_1979/1657116227.py:14: FutureWarning: pandas.io.json.json_normalize is deprecated, use pandas.json_normalize instead.
  df = json_normalize(json_data['result']['etfItemList'])
종목코드 종목명
0 445690 BNK 주주가치액티브
1 292340 마이티 200커버드콜ATM레버리지
2 442260 마이티 다이나믹퀀트액티브
3 159800 마이티 코스피100
4 361580 KBSTAR 200TR
... ... ...
688 195980 ARIRANG 신흥국MSCI(합성 H)
689 433250 UNICORN R&D 액티브
690 215620 HK S&P코리아로우볼
691 391670 HK 베스트일레븐액티브
692 391680 HK 하이볼액티브

693 rows × 2 columns

etf[['종목명', '종목코드']].itertuples(index=False)
<map at 0x17ed36d10>
etf_price = pd.read_pickle("./data/etf_price.pickle")
etf_price
NAV 시가 고가 저가 종가 거래량 거래대금 기초지수 종목명 종목코드 날짜
0 22631.83 22390 22585 22390 22550 58738 1325723895 223.49 ACE 200 105190 2010-01-04
1 22566.21 22550 22725 22475 22555 47769 1081075485 222.84 ACE 200 105190 2010-01-05
2 22748.91 22625 22735 22620 22715 74937 1701637315 224.67 ACE 200 105190 2010-01-06
3 22409.69 22740 22740 22415 22455 26044 587893595 221.31 ACE 200 105190 2010-01-07
4 22544.98 22365 22515 22230 22515 11803 264454100 222.66 ACE 200 105190 2010-01-08
... ... ... ... ... ... ... ... ... ... ... ...
812503 7211.78 7245 7245 7180 7205 144 1037685 928.25 히어로즈 리츠이지스액티브 429870 2023-04-03
812504 7243.46 7205 7220 7175 7190 467 3351265 930.73 히어로즈 리츠이지스액티브 429870 2023-04-04
812505 7231.48 7285 7285 7190 7190 53 381195 929.02 히어로즈 리츠이지스액티브 429870 2023-04-05
812506 7183.86 0 0 0 7190 0 0 922.52 히어로즈 리츠이지스액티브 429870 2023-04-06
812507 7166.16 7155 7175 7115 7175 133 948425 920.66 히어로즈 리츠이지스액티브 429870 2023-04-07

812508 rows × 11 columns

import time
from tqdm import tqdm
df = pd.DataFrame(columns=['NAV','시가','고가','저가','종가','거래량','거래대금','기초지수','종목명','종목코드'])
error_list = []

start_date = (pd.to_datetime(sorted(etf_price['날짜'].unique())[-1])+datetime.timedelta(days=1)).strftime("%Y%m%d")
end_date = datetime.datetime.now().strftime("%Y%m%d")
print(start_date, end_date)
for a, b in tqdm(etf[['종목명', '종목코드']].itertuples(index=False)):
    try:
        price = stock.get_etf_ohlcv_by_date(start_date, end_date, b)
        price['종목명']=a
        price['종목코드']=b
        price = price.reset_index()
        df = pd.concat([df,price])
    except:
        error_list.append(a)
    time.sleep(0.01)
df = df.reset_index(drop=True)
df
error_list
# 신규 추가 종목 
set(df['종목명']).symmetric_difference(set(etf_price['종목명']))
etf_price = pd.concat([etf_price, df])
etf_price = etf_price.drop_duplicates(['종목명','종목코드','날짜'], keep='last').sort_values(by=['종목명','날짜']).reset_index(drop=True)
etf_price
# 종목명에 null값이 있을 때,
temp = etf_price[['종목명','종목코드']].dropna().drop_duplicates(['종목코드'], keep='last').set_index('종목코드')['종목명']
etf_price.loc[etf_price['종목명'].isnull(),'종목명'] = etf_price.loc[etf_price['종목명'].isnull(),'종목코드'].map(temp)
etf_price = etf_price.sort_values(by=['종목명','날짜']).reset_index(drop=True)
etf_price
%%time
etf_price_pivot = etf_price.pivot_table(index='날짜', columns = '종목명', values='종가')
etf_price_pivot
CPU times: user 23.7 s, sys: 107 ms, total: 23.8 s
Wall time: 23.8 s
종목명 ACE 200 ACE 200TR ACE 23-12 회사채(AA-이상)액티브 ACE 24-12 회사채(AA-이상)액티브 ACE ESG액티브 ACE Fn5G플러스 ACE Fn성장소비주도주 ACE G2전기차&자율주행액티브 ACE KRX금현물 ACE 골드선물 레버리지(합성 H) ... 에셋플러스 코리아플랫폼액티브 파워 200 파워 고배당저변동성 파워 코스피100 히어로즈 TDF2030액티브 히어로즈 TDF2040액티브 히어로즈 TDF2050액티브 히어로즈 글로벌리츠이지스액티브 히어로즈 단기채권ESG액티브 히어로즈 리츠이지스액티브
날짜
2010-01-04 22550.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2010-01-05 22555.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2010-01-06 22715.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2010-01-07 22455.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2010-01-08 22515.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2023-04-03 32940.0 20445.0 103155.0 105115.0 7290.0 7165.0 6285.0 9000.0 11800.0 17305.0 ... 6960.0 33010.0 28360.0 24730.0 10390.0 10535.0 10550.0 8905.0 101925.0 7205.0
2023-04-04 33075.0 20525.0 103140.0 105180.0 7320.0 7115.0 6365.0 8875.0 12000.0 17600.0 ... 7030.0 33130.0 28420.0 24700.0 10430.0 10520.0 10590.0 8855.0 101925.0 7190.0
2023-04-05 33255.0 20650.0 103175.0 105210.0 7345.0 7145.0 6350.0 8800.0 12210.0 18385.0 ... 7065.0 33315.0 28435.0 24870.0 10390.0 10485.0 10530.0 8795.0 102000.0 7190.0
2023-04-06 32680.0 20270.0 103210.0 105310.0 7215.0 7000.0 6320.0 8680.0 12275.0 18420.0 ... 7025.0 32765.0 28145.0 24545.0 10405.0 10515.0 10540.0 8825.0 102030.0 7190.0
2023-04-07 33260.0 20640.0 103245.0 105280.0 7330.0 7130.0 6365.0 8710.0 12460.0 18290.0 ... 7070.0 33300.0 28070.0 24850.0 10390.0 10525.0 10550.0 9000.0 102060.0 7175.0

3275 rows × 697 columns

# etf_price.to_pickle("./data/etf_price.pickle")
# data.to_pickle("./data/etf_price_pivot.pickle")

4. 재무제표 (data_fs)

  • “data_fs.pickle” : FNGUIDE 에서 최근 4분기 재무제표 크롤링
# 네이버증권에서 재무제표를 다운받으려면 동적으로 selenium을 써야하나 너무 느리다. 
# comp.fnguide.com

❍ 한 종목 재무제표 크롤링

# 최근 시점의 보통주 데이터 불러오기

import pandas as pd
kor_ticker = pd.read_pickle('./data/kor_ticker.pickle')
kor_sector = pd.read_pickle('./data/kor_sector.pickle')

kor_ticker_last_day = kor_ticker['기준일'].unique()[-1]
kor_sector_last_day = kor_sector['기준일'].unique()[-1]
cond = (kor_ticker['종목구분'] == '보통주') & \
        (kor_ticker['기준일']==kor_ticker_last_day)
ticker_list = kor_ticker[cond].reset_index(drop=True)

한 종목의 재무제표 데이터 다운로드

i=0
ticker = ticker_list['종목코드'][0]
url = f'https://comp.fnguide.com/SVO2/ASP/SVD_Finance.asp?pGB=1&gicode=A{ticker}'
data = pd.read_html(url, displayed_only = False) 
#displayed_only = False하면 +로 숨겨져 있는 것들도 모두 가져옴
print(len(data))
6
print(data[0].columns.tolist(),'\n',  #연   -  손익계산서
      data[1].columns.tolist(), '\n', #분기  -  손익계산서
      data[2].columns.tolist(), '\n', #연   -  재무상태표
      data[3].columns.tolist(), '\n', #분기  -  재무상태표
      data[4].columns.tolist(),'\n',  #연   -  현금흐름표
      data[5].columns.tolist())       #분기  -  현금흐름표
['IFRS(연결)', '2019/12', '2020/12', '2021/12', '2022/12', '전년동기', '전년동기(%)'] 
 ['IFRS(연결)', '2022/03', '2022/06', '2022/09', '2022/12', '전년동기', '전년동기(%)'] 
 ['IFRS(연결)', '2019/12', '2020/12', '2021/12', '2022/12'] 
 ['IFRS(연결)', '2022/03', '2022/06', '2022/09', '2022/12'] 
 ['IFRS(연결)', '2019/12', '2020/12', '2021/12', '2022/12'] 
 ['IFRS(연결)', '2022/03', '2022/06', '2022/09', '2022/12']
data[0].iloc[:, ~data[0].columns.str.contains('전년동기')].head(3)
IFRS(연결) 2019/12 2020/12 2021/12 2022/12
0 매출액 10003.0 8720.0 9819.0 12084.0
1 매출원가 NaN NaN NaN NaN
2 매출총이익 10003.0 8720.0 9819.0 12084.0
data[1].iloc[:, ~data[0].columns.str.contains('전년동기')].head(3)
IFRS(연결) 2022/03 2022/06 2022/09 2022/12
0 매출액 2958.0 2842.0 3451.0 2833.0
1 매출원가 NaN NaN NaN NaN
2 매출총이익 2958.0 2842.0 3451.0 2833.0
data[2].head(3)
IFRS(연결) 2019/12 2020/12 2021/12 2022/12
0 자산 18033.0 15882.0 13550.0 14814.0
1 유동자산계산에 참여한 계정 펼치기 5011.0 3547.0 2739.0 3295.0
2 재고자산 586.0 337.0 197.0 250.0
data[3].head(3)
IFRS(연결) 2022/03 2022/06 2022/09 2022/12
0 자산 13672.0 13932.0 15020.0 14814.0
1 유동자산계산에 참여한 계정 펼치기 2619.0 2702.0 3120.0 3295.0
2 재고자산 289.0 357.0 428.0 250.0
data[4].head(3)
IFRS(연결) 2019/12 2020/12 2021/12 2022/12
0 영업활동으로인한현금흐름 -379.0 177.0 -113.0 183.0
1 당기순손익 421.0 -33.0 767.0 88.0
2 법인세비용차감전계속사업이익 NaN NaN NaN NaN
data[5].head(3)
IFRS(연결) 2022/03 2022/06 2022/09 2022/12
0 영업활동으로인한현금흐름 30.0 -231.0 -312.0 696.0
1 당기순손익 109.0 97.0 103.0 -221.0
2 법인세비용차감전계속사업이익 NaN NaN NaN NaN

연기준 재무제표 만들기

# 결산월 확인하기 : 보통 12월 결산
import requests as rq
from bs4 import BeautifulSoup
import re

page_data = rq.get(url)
page_data_html = BeautifulSoup(page_data.content)
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)
print("결산월 : ", fiscal_data_text)
결산월 :  ['12']
# 데이터 클린징
def clean_fs(df, ticker, frequency): 
    # 계정을 제외하고 모든 열의 na인 행을 제외, 적어도 하나의 값 있는 행만 생존
    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
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] : "계정"})
# 12월 결산이 아니라면... 분기 재무제표(최근 결산기준) 함께 표시됨. 분기재무제표 열 제거
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_y_clean
계정 기준일 종목코드 공시구분
0 매출액 2019-12-31 10003.0 095570 y
1 매출총이익 2019-12-31 10003.0 095570 y
2 판매비와관리비 2019-12-31 9846.0 095570 y
3 인건비 2019-12-31 852.0 095570 y
4 유무형자산상각비 2019-12-31 109.0 095570 y
... ... ... ... ... ...
586 배당금지급(-) 2022-12-31 -121.0 095570 y
588 환율변동효과 2022-12-31 12.0 095570 y
589 현금및현금성자산의증가 2022-12-31 466.0 095570 y
590 기초현금및현금성자산 2022-12-31 852.0 095570 y
591 기말현금및현금성자산 2022-12-31 1319.0 095570 y

544 rows × 5 columns

분기기준 재무제표 만들기

# 분기 재무제표
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_q_clean
계정 기준일 종목코드 공시구분
0 매출액 2022-03-31 2958.0 095570 q
1 매출총이익 2022-03-31 2958.0 095570 q
2 판매비와관리비 2022-03-31 2766.0 095570 q
3 인건비 2022-03-31 259.0 095570 q
4 유무형자산상각비 2022-03-31 399.0 095570 q
... ... ... ... ... ...
547 배당금지급(-) 2022-12-31 0.0 095570 q
548 환율변동효과 2022-12-31 -17.0 095570 q
549 현금및현금성자산의증가 2022-12-31 559.0 095570 q
550 기초현금및현금성자산 2022-12-31 760.0 095570 q
551 기말현금및현금성자산 2022-12-31 1319.0 095570 q

467 rows × 5 columns

연간 재무제표 + 분기 재무제표

# 연간 재무제표 분기 재무제표 합치기
data_fs_bind = pd.concat([data_fs_y_clean, data_fs_q_clean]).reset_index(drop=True)
data_fs_bind
계정 기준일 종목코드 공시구분
0 매출액 2019-12-31 10003.0 095570 y
1 매출총이익 2019-12-31 10003.0 095570 y
2 판매비와관리비 2019-12-31 9846.0 095570 y
3 인건비 2019-12-31 852.0 095570 y
4 유무형자산상각비 2019-12-31 109.0 095570 y
... ... ... ... ... ...
1006 배당금지급(-) 2022-12-31 0.0 095570 q
1007 환율변동효과 2022-12-31 -17.0 095570 q
1008 현금및현금성자산의증가 2022-12-31 559.0 095570 q
1009 기초현금및현금성자산 2022-12-31 760.0 095570 q
1010 기말현금및현금성자산 2022-12-31 1319.0 095570 q

1011 rows × 5 columns

❍ 여러 종목 재무제표 크롤링

from tqdm import tqdm
import time
import pandas as pd
df = pd.DataFrame(columns = ['계정','기준일','값','종목코드','공시구분'])
error_list = []
for i in tqdm(range(0, len(ticker_list))): 
#for ticker in tqdm(diff):

    # ticker 선택
    ticker = ticker_list['종목코드'][i]
    
    try:     
        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 합치기
        df = pd.concat([df, data_fs_bind])
    except:
        print(ticker)
        error_list.append(ticker)
        break
    time.sleep(0.01)
diff = list(set(ticker_list['종목코드']).symmetric_difference(set(df['종목코드'].unique())))
diff
# 한화갤러리아, 코오롱모빌리티그룹 >>> 신규상장한 기업들 
['452260', '450140']
#df.to_pickle('./data/data_fs.pickle')
data_fs_history = pd.read_pickle('./data/data_fs.pickle')
data_fs_history = pd.concat([data_fs_history, df])
data_fs_history = data_fs_history.drop_duplicates(['계정','기준일','종목코드','공시구분'], keep='last')
data_fs_history = data_fs_history.sort_values(by=['종목코드','기준일','공시구분','계정'])
data_fs_history = data_fs_history.reset_index(drop=True)
data_fs_history
#data_fs_history.to_pickle('./data/data_fs.pickle')
계정 기준일 종목코드 공시구분
0 (재무활동으로인한현금유출액) 2019-12-31 16.0 000020 y
1 (투자활동으로인한현금유출액) 2019-12-31 1080.0 000020 y
2 (현금유입이없는수익등차감) 2019-12-31 99.0 000020 y
3 *영업에서창출된현금흐름 2019-12-31 164.0 000020 y
4 감가상각비 2019-12-31 104.0 000020 y
... ... ... ... ... ...
2000595 판매비와관리비 2022-12-31 32.0 446070 y
2000596 현금및현금성자산 2022-12-31 548.0 446070 y
2000597 현금및현금성자산의증가 2022-12-31 -270.0 446070 y
2000598 현금유출이없는비용등가산 2022-12-31 23.0 446070 y
2000599 환율변동효과 2022-12-31 0.0 446070 y

2000600 rows × 5 columns


5. 가치지표 계산 (data_value)

  • “data_value.pickle” : data_fs로부터 TTM 데이터를 구한 후 계산
  • PER(주가수익비율), PBR(주가순자산비율), PSR(주가매출비율), PCR(주가현금흐름비율)
  • DY(배당률)
  • 시가총액 기준일(ticker_list)을 기준으로 계산한 가치지표를 확인가능 (최신 재무제표 활용)

PER, PBR, PSR, PCR, DY

# 분기 재무제표 불러오기
import pandas as pd
import numpy as np
kor_fs = pd.read_pickle("./data/data_fs.pickle")
kor_fs.drop_duplicates(inplace=True)
cond = (kor_fs.공시구분 == 'q') & (kor_fs['계정'].isin(['당기순이익','자본','영업활동으로인한현금흐름','매출액']))
kor_fs = kor_fs[cond]

# TTM 구하기
kor_fs = kor_fs.sort_values(['종목코드','계정','기준일']).reset_index(drop=True)
kor_fs['ttm'] = kor_fs.groupby(['종목코드','계정'], as_index=False)['값'].rolling(window=4, min_periods=4).sum()['값']
#kor_fs_merge = kor_fs_merge.dropna()

# 자본은 평균으로
kor_fs['ttm'] = np.where(kor_fs['계정']=='자본',
                        kor_fs['ttm']/4, kor_fs['ttm'])
# tail(1) : 최근데이터만 남긴다 
kor_fs = kor_fs.groupby(['계정','종목코드']).tail(1).reset_index(drop=True)

# 합치기
kor_fs_merge = kor_fs[['계정','종목코드','ttm']].merge(ticker_list[['종목코드','시가총액','기준일']], on='종목코드')

# 시가총액 : 원단위를 억원으로 변경
kor_fs_merge['시가총액'] = kor_fs_merge['시가총액']/10**8

# 지표 계산

cond = kor_fs_merge['ttm']==0
kor_fs_merge.loc[cond, 'ttm'] = 0.1 #0인 값 처리

kor_fs_merge['value'] = kor_fs_merge['시가총액'] / kor_fs_merge['ttm']
kor_fs_merge['value'] = kor_fs_merge['value'].astype(float).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))))

# value 중에 nan 이나 inf 를 None으로 변경하기, SQL에 nan, inf 저장 안됨
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)


### 배당수익률
ticker_list['값'] = ticker_list['주당배당금'] / ticker_list['종가']
ticker_list['값'] = ticker_list['값'].astype(np.float64).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] #배당을 0으로 주는 경우는 제외


# PER, PBR, PSR, PCR, DY 합치기
value_df = pd.concat([kor_fs_merge, dy_list]).reset_index(drop=True)
value_df
종목코드 기준일 지표
0 000020 20230331 PER 10.7459
1 000020 20230331 PSR 0.6817
2 000020 20230331 PCR 7.9219
3 000020 20230331 PBR 0.629
4 000040 20230331 PER -3.7272
... ... ... ... ...
10299 024060 20230331 DY 0.0179
10300 010240 20230331 DY 0.0326
10301 189980 20230331 DY 0.011
10302 037440 20230331 DY 0.0164
10303 238490 20230331 DY 0.0141

10304 rows × 4 columns

여기서의 기준일은 시가총액 기준일

  • 재무제표는 2022.12.31 결산기준이며, 시가총액은 2023.03.31 기준 > 기준일 시가총액 기준
# value_df.to_pickle("./data/data_value.pickle")
kor_fs.head(1)
계정 기준일 종목코드 공시구분 ttm
0 당기순이익 2022-12-31 25.0 000020 q 216.0
kor_fs_merge.head(1)
종목코드 기준일 지표
0 000020 20230331 PER 10.7459
value_df.head(1)
종목코드 기준일 지표
0 000020 20230331 PER 10.7459
temp = pd.read_pickle('./data/data_value.pickle')
temp = pd.concat([temp, value_df]).drop_duplicates(['종목코드','기준일','지표'], keep='last').reset_index(drop=True)
temp
#temp.to_pickle("./data/data_value.pickle")
종목코드 기준일 지표
0 000020 20230331 PER 10.7459
1 000020 20230331 PSR 0.6817
2 000020 20230331 PCR 7.9219
3 000020 20230331 PBR 0.629
4 000040 20230331 PER -3.7272
... ... ... ... ...
10299 024060 20230331 DY 0.0179
10300 010240 20230331 DY 0.0326
10301 189980 20230331 DY 0.011
10302 037440 20230331 DY 0.0164
10303 238490 20230331 DY 0.0141

10304 rows × 4 columns

6. 상장폐지종목 (delistSymbol)

import FinanceDataReader as fdr
from pykrx import stock
import pandas_datareader.data as pdr
import yfinance as yf

# 가장 최근 영업일의 시장별 종목리스트를 가져옴
stocks = fdr.StockListing('KRX') # 코스피, 코스닥, 코넥스 전체
stocks
Code ISU_CD Name Market Dept Close ChangeCode Changes ChagesRatio Open High Low Volume Amount Marcap Stocks MarketId
0 005930 KR7005930003 삼성전자 KOSPI 65000 1 2700 4.33 63800 65200 63800 27476120 1778208943700 388035865750000 5969782550 STK
1 373220 KR7373220003 LG에너지솔루션 KOSPI 580000 3 0 0.00 581000 590000 579000 366225 213290884200 135720000000000 234000000 STK
2 000660 KR7000660001 SK하이닉스 KOSPI 89100 1 5300 6.32 87900 89400 86000 10867359 959641473100 64865010721500 728002365 STK
3 207940 KR7207940008 삼성바이오로직스 KOSPI 796000 2 -9000 -1.12 804000 805000 792000 61342 48882068000 56654504000000 71174000 STK
4 006400 KR7006400006 삼성SDI KOSPI 738000 2 -7000 -0.94 742000 747000 731000 259085 191613326000 50748223140000 68764530 STK
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2709 308700 KR7308700004 테크엔 KONEX 일반기업부 363 1 43 13.44 367 367 363 101 36667 1452000000 4000000 KNX
2710 322190 KR7322190000 베른 KONEX 일반기업부 118 5 -20 -14.49 118 137 118 532 71838 1053173246 8925197 KNX
2711 058420 KR7058420001 제이웨이 KOSDAQ 투자주의환기종목(소속부없음) 215 2 -83 -27.85 260 293 210 3697643 845032403 776330385 3610839 KSQ
2712 271850 KR7271850000 다이오진 KONEX 일반기업부 125 2 -79 -38.73 249 249 125 121711 19095800 400791625 3206333 KNX
2713 267060 KR7267060002 명진홀딩스 KONEX 일반기업부 30 2 -151 -83.43 53 53 25 1283696 44440461 274254120 9141804 KNX

2714 rows × 17 columns

# KRX stock delisting symbol list 상장폐지 종목 전체 리스트
krx_delisting = fdr.StockListing('KRX-DELISTING')
krx_delisting
Symbol Name Market SecuGroup Kind ListingDate DelistingDate Reason ArrantEnforceDate ArrantEndDate Industry ParValue ListingShares ToSymbol ToName
0 06031012 3S R KOSDAQ 신주인수권증서 보통주 2012-05-14 2012-05-21 NaT NaT 0.0 1194422.0
1 00684014 AK홀딩스8R KOSPI 신주인수권증서 보통주 2014-07-28 2014-08-04 NaT NaT 0.0 1278299.0
2 13893015 BNK금융지주 8R KOSPI 신주인수권증서 보통주 2015-12-24 2016-01-05 NaT NaT 0.0 55969410.0
3 13893014 BS금융지주5R KOSPI 신주인수권증서 보통주 2014-06-18 2014-06-25 NaT NaT 0.0 32791220.0
4 03204017 C&S자산관리 34R KOSDAQ 신주인수권증서 보통주 2017-02-02 2017-02-09 NaT NaT 0.0 3995063.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
3487 034370 럭키소재 KOSPI 주권 1979-06-23 1991-11-11 해산 사유 발생 NaT NaT NaN NaN
3488 028460 태평양건설 KOSPI 주권 1977-06-09 1991-10-05 영업활동정지 6월 계속 1991-08-28 1991-10-04 NaN NaN
3489 028450 금성투자금융 KOSPI 주권 1986-08-29 1991-09-02 해산 사유 발생 NaT NaT NaN NaN
3490 028440 삼화 KOSPI 주권 1974-06-12 1991-07-12 감사의견 의견거절 1991-06-05 1991-07-11 NaN NaN
3491 029260 금성전기 KOSPI 주권 1976-06-29 1991-06-13 해산 사유 발생 NaT NaT NaN NaN

3492 rows × 15 columns

import datetime
import pandas as pd
cond = (krx_delisting['DelistingDate'] > "2010-01-01")&(krx_delisting['Kind']=='보통주')
temp = krx_delisting[cond].sort_values(by='DelistingDate', ascending=False)
temp = [i for i in temp.Symbol.tolist() if len(i)==6]
# pd.DataFrame(temp, columns=['상장폐지종목코드']).to_pickle("./data/delistSymbol.pickle")
temp = pd.read_pickle("./data/delistSymbol.pickle")
temp
상장폐지종목코드
0 267810
1 056000
2 387310
3 353070
4 397500
... ...
696 037510
697 083120
698 000610
699 015940
700 045820

701 rows × 1 columns

7. 재무제표 - 다트 (API Key 필요)

  • openapi를 활용해서 재무정보 다운로드

https://opendart.fss.or.kr/

# !pip install keyring --quiet

❍ 다트 티커리스트 다운받기

import keyring
import requests
from io import BytesIO
import zipfile

#keyring.set_password('dart_api_key', 'User Name', 'Password') # System , 본인이름, 발급받은 API키
keyring.set_password('dart_api_key', '###', '$$$$$$$$$$$$$$$$$$$$') # System , 본인이름, 발급받은 API키
api_key = keyring.get_password('dart_api_key', '###')

codezip_url = f'''https://opendart.fss.or.kr/api/corpCode.xml?crtfc_key={api_key}'''
codezip_data = requests.get(codezip_url)
codezip_data.headers
{'Cache-Control': 'no-cache, no-store', 'Connection': 'keep-alive', 'Set-Cookie': 'WMONID=-Tq_CVAmWN2; Expires=Sun, 07-Apr-2024 20:6:17 GMT; Path=/', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Transfer-Encoding': 'binary', 'Content-Disposition': ': attachment; filename=CORPCODE.zip', 'Date': 'Sat, 08 Apr 2023 11:06:17 GMT', 'Content-Type': 'application/x-msdownload;charset=UTF-8', 'Content-Length': '1657392'}
codezip_data.headers['Content-Disposition']
': attachment; filename=CORPCODE.zip'
codezip_file = zipfile.ZipFile(BytesIO(codezip_data.content)) # 바이너리스트림 형태로 변경 > 집파일 풀기 
codezip_file.namelist() # 어떤 파일이 있는지
['CORPCODE.xml']
code_data = codezip_file.read("CORPCODE.xml").decode("utf-8") # utf-8로 디코딩 #한글이 보이기 시작
# xml 형태를 dictionary 형태로 변경
import xmltodict
import json
import pandas as pd

data_odict = xmltodict.parse(code_data)
data_dict = json.loads(json.dumps(data_odict))
data = data_dict.get('result').get('list')
corp_list = pd.DataFrame(data)
corp_list
corp_code corp_name stock_code modify_date
0 00434003 다코 None 20170630
1 00434456 일산약품 None 20170630
2 00430964 굿앤엘에스 None 20170630
3 00432403 한라판지 None 20170630
4 00388953 크레디피아제이십오차유동화전문회사 None 20170630
... ... ... ... ...
98028 00646510 미래에셋파트너스사호사모투자전문회사 None 20230228
98029 01184822 미래에셋파트너스9호사모투자 None 20230228
98030 00755252 시니안 None 20230228
98031 00227582 청호나이스 None 20230228
98032 01615845 메타버스월드 None 20230228

98033 rows × 4 columns

corp_list = corp_list[~corp_list.stock_code.isin([None])].reset_index(drop=True) # None은 비상장이므로 필터링
corp_list
corp_code corp_name stock_code modify_date
0 00260985 한빛네트 036720 20170630
1 00264529 엔플렉스 040130 20170630
2 00358545 동서정보기술 055000 20170630
3 00231567 애드모바일 032600 20170630
4 00247939 씨모스 037600 20170630
... ... ... ... ...
3560 00413417 우리손에프앤지 073560 20230403
3561 00440712 어반리튬 073570 20230403
3562 00483735 해성옵틱스 076610 20230403
3563 00516246 알에프세미 096610 20230403
3564 00525679 차바이오텍 085660 20230403

3565 rows × 4 columns

# corp_list.to_pickle("./data/tickers_dart.pickle")

❍ 공시자료 다운로드

오픈다트 > 개발가이드 > 공시정보 > 공시검색

# 전체 종목의 최근 100개의 공시에 해당하는 내역을 받아오게 됨

from datetime import date
from dateutil.relativedelta import relativedelta

bgn_date = (date.today() + relativedelta(days=-30)).strftime("%Y%m%d")
end_date = (date.today()).strftime("%Y%m%d")

notice_url = f'''https://opendart.fss.or.kr/api/list.json?crtfc_key={api_key}&bgn_de={bgn_date}&end_de={end_date}&page_no=1&page_count=100''' # 최대 100까지 가능

notice_data = requests.get(notice_url)
notice_data_df = notice_data.json().get('list')
notice_data_df = pd.DataFrame(notice_data_df)
notice_data_df
corp_code corp_name stock_code corp_cls report_nm rcept_no flr_nm rcept_dt rm
0 01101722 한송네오텍 226440 K [기재정정]기타시장안내(상장폐지 관련) 20230407901312 코스닥시장본부 20230407
1 00610490 비디아이 148140 K [기재정정]주권매매거래정지기간변경(상장폐지 사유 해소, 사업보고서 미제출) 20230407901311 코스닥시장본부 20230407
2 00610490 비디아이 148140 K 기타시장안내(상장폐지 관련) 20230407901308 코스닥시장본부 20230407
3 00962393 포인트모바일 318020 K [기재정정]기타시장안내(상장폐지 관련) 20230407901309 코스닥시장본부 20230407
4 00610490 비디아이 148140 K 주권매매거래정지기간변경(상장폐지 사유 해소) 20230407901306 코스닥시장본부 20230407 코정
... ... ... ... ... ... ... ... ... ...
95 01653128 네오플랜 E 감사보고서 (2022.12) 20230407003488 현대회계법인 20230407
96 01685774 두리랜드 E 감사보고서 (2022.12) 20230407003487 인덕회계법인 20230407
97 00440712 어반리튬 073570 K 주식등의대량보유상황보고서(일반) 20230407003486 리튬인사이트 20230407
98 00990262 피노텍 150440 N 사업보고서 (2022.12) 20230407003485 피노텍 20230407
99 01476219 코스텍시스 355150 K 주식등의대량보유상황보고서(약식) 20230407003484 유비쿼스인베스트먼트 20230407

100 rows × 9 columns

# 원하는 종목의 공시자료만 받아오기
corp_code = '00126380' # 종목티커(stock_code)와 별도로 종목코드(corp_code)가 있음

notice_url_ss = f'''https://opendart.fss.or.kr/api/list.json?crtfc_key={api_key}&corp_code={corp_code}&bgn_de={bgn_date}&end_de={end_date}&page_no=1&page_count=100''' # 최대 100까지 가능
notice_data_ss = requests.get(notice_url_ss)
notice_data_ss_df = notice_data_ss.json().get('list')
notice_data_ss_df = pd.DataFrame(notice_data_ss_df)
notice_data_ss_df.tail()
corp_code corp_name stock_code corp_cls report_nm rcept_no flr_nm rcept_dt rm
0 00126380 삼성전자 005930 Y 연결재무제표기준영업(잠정)실적(공정공시) 20230407800208 삼성전자 20230407
1 00126380 삼성전자 005930 Y 임원ㆍ주요주주특정증권등소유상황보고서 20230406002362 김이태 20230406
2 00126380 삼성전자 005930 Y 임원ㆍ주요주주특정증권등소유상황보고서 20230324000884 경계현 20230324
3 00126380 삼성전자 005930 Y 임원ㆍ주요주주특정증권등소유상황보고서 20230316000646 안재용 20230316
4 00126380 삼성전자 005930 Y 정기주주총회결과 20230315802179 삼성전자 20230315
# 공시번호를 이용해서 rcept_no 

notice_url_exam = notice_data_ss_df.loc[0, 'rcept_no']
notice_dart_url = f'https://dart.fss.or.kr/dsaf001/main.do?rcpNo={notice_url_exam}'
notice_dart_url
'https://dart.fss.or.kr/dsaf001/main.do?rcpNo=20230407800208'
# 개발가이드 > 사업보고서 주요정보 > 배당에 관한 사항 # https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS002&apiId=2019005

corp_code = '00126380'
bsns_year = '2022'
report_code = '11011'

url_div = f'''https://opendart.fss.or.kr/api/alotMatter.json?crtfc_key={api_key}&corp_code={corp_code}&bsns_year={bsns_year}&reprt_code={report_code}'''

div_data_ss = requests.get(url_div)
div_data_ss_df = div_data_ss.json().get('list')
div_data_ss_df = pd.DataFrame(div_data_ss_df)
div_data_ss_df
rcept_no corp_cls corp_code corp_name se thstrm frmtrm lwfr stock_knd
0 20230307000542 Y 00126380 삼성전자 주당액면가액(원) 100 100 100 NaN
1 20230307000542 Y 00126380 삼성전자 (연결)당기순이익(백만원) 54,730,018 39,243,791 26,090,846 NaN
2 20230307000542 Y 00126380 삼성전자 (별도)당기순이익(백만원) 25,418,778 30,970,954 15,615,018 NaN
3 20230307000542 Y 00126380 삼성전자 (연결)주당순이익(원) 8,057 5,777 3,841 NaN
4 20230307000542 Y 00126380 삼성전자 현금배당금총액(백만원) 9,809,438 9,809,438 20,338,075 NaN
5 20230307000542 Y 00126380 삼성전자 주식배당금총액(백만원) - - - NaN
6 20230307000542 Y 00126380 삼성전자 (연결)현금배당성향(%) 17.90 25.00 78.00 NaN
7 20230307000542 Y 00126380 삼성전자 현금배당수익률(%) 2.50 1.80 4.00 보통주
8 20230307000542 Y 00126380 삼성전자 현금배당수익률(%) 2.70 2.00 4.20 우선주
9 20230307000542 Y 00126380 삼성전자 주식배당수익률(%) - - - 보통주
10 20230307000542 Y 00126380 삼성전자 주식배당수익률(%) - - - 우선주
11 20230307000542 Y 00126380 삼성전자 주당 현금배당금(원) 1,444 1,444 2,994 보통주
12 20230307000542 Y 00126380 삼성전자 주당 현금배당금(원) 1,445 1,445 2,995 우선주
13 20230307000542 Y 00126380 삼성전자 주당 주식배당(주) - - - 보통주
14 20230307000542 Y 00126380 삼성전자 주당 주식배당(주) - - - 우선주
# API를 이용해서 전종목의 재무정보 다운받기
# 개발가이드 > 상장기업재무정보 > 단일회사 전체 재무제표 (금융회사 제외)
# https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS003&apiId=2019020

url_fs = f'''https://opendart.fss.or.kr/api/fnlttSinglAcntAll.json?crtfc_key={api_key}
&corp_code={corp_code}&bsns_year={bsns_year}&reprt_code={report_code}&fs_div=OFS''' #OFS : 연결

fs_data_ss = requests.get(url_fs)
fs_data_ss_df = fs_data_ss.json().get('list')
fs_data_ss_df = pd.DataFrame(fs_data_ss_df)
fs_data_ss_df
rcept_no reprt_code bsns_year corp_code sj_div sj_nm account_id account_nm account_detail thstrm_nm thstrm_amount frmtrm_nm frmtrm_amount bfefrmtrm_nm bfefrmtrm_amount ord currency thstrm_add_amount
0 20230307000542 11011 2022 00126380 BS 재무상태표 ifrs-full_CurrentAssets 유동자산 - 제 54 기 59062658000000 제 53 기 73553416000000 제 52 기 73798549000000 1 KRW NaN
1 20230307000542 11011 2022 00126380 BS 재무상태표 ifrs-full_CashAndCashEquivalents 현금및현금성자산 - 제 54 기 3921593000000 제 53 기 3918872000000 제 52 기 989045000000 2 KRW NaN
2 20230307000542 11011 2022 00126380 BS 재무상태표 dart_ShortTermDepositsNotClassifiedAsCashEquiv... 단기금융상품 - 제 54 기 137000000 제 53 기 15000576000000 제 52 기 29101284000000 3 KRW NaN
3 20230307000542 11011 2022 00126380 BS 재무상태표 dart_ShortTermTradeReceivable 매출채권 - 제 54 기 20503223000000 제 53 기 33088247000000 제 52 기 24736740000000 4 KRW NaN
4 20230307000542 11011 2022 00126380 BS 재무상태표 dart_ShortTermOtherReceivables 미수금 - 제 54 기 2925006000000 제 53 기 1832488000000 제 52 기 1898583000000 5 KRW NaN
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
109 20230307000542 11011 2022 00126380 SCE 자본변동표 ifrs-full_Equity 기말자본 별도재무제표 [member] 제 54 기 209416191000000 제 53 기 193193732000000 제 52 기 183316724000000 6 KRW NaN
110 20230307000542 11011 2022 00126380 SCE 자본변동표 ifrs-full_Equity 기말자본 자본 [member]|기타자본항목 제 54 기 -273232000000 제 53 기 -882010000000 제 52 기 -268785000000 6 KRW NaN
111 20230307000542 11011 2022 00126380 SCE 자본변동표 ifrs-full_Equity 기말자본 자본 [member]|이익잉여금 [member] 제 54 기 204388016000000 제 53 기 188774335000000 제 52 기 178284102000000 6 KRW NaN
112 20230307000542 11011 2022 00126380 SCE 자본변동표 ifrs-full_Equity 기말자본 자본 [member]|자본금 [member] 제 54 기 897514000000 제 53 기 897514000000 제 52 기 897514000000 6 KRW NaN
113 20230307000542 11011 2022 00126380 SCE 자본변동표 ifrs-full_Equity 기말자본 자본 [member]|주식발행초과금 제 54 기 4403893000000 제 53 기 4403893000000 제 52 기 4403893000000 6 KRW NaN

114 rows × 18 columns

8. FRED 장단기 금리차, 공포탐욕지수

FRED : Federal Reserve Economic Data

대표적인 보조지표 : 장단기 금리차 > 장기로 돈을 빌리는 것이 단기로 빌리는 것보다 위험하므로, 장기 프리미엄이 붙게 됨. 주요 금융시장 및 경제지표 가운데 경기침체에 대한 예측력 좋다고 알려짐 » 경기 침체 이전에 장단기 금리차가 역전됨

https://fred.stlouisfed.org/series/T10Y2Y

import pandas_datareader as web
import pandas as pd

t10y2y = web.DataReader("T10Y2Y", 'fred', start='1990-01-01')
t10y3m = web.DataReader("T10Y3M", 'fred', start='1990-01-01')

rate_diff = pd.concat([t10y2y, t10y3m], axis=1)
rate_diff.columns = ['10Y-2Y', '10Y-3M']
rate_diff.tail()
10Y-2Y 10Y-3M
DATE
2023-04-03 -0.54 -1.47
2023-04-04 -0.49 -1.53
2023-04-05 -0.49 -1.56
2023-04-06 -0.52 -1.61
2023-04-07 -0.58 -1.56
import matplotlib.pyplot as plt
import numpy as np
import yfinance as yf

sp = yf.download("^GSPC", start='1990-01-01')
[*********************100%***********************]  1 of 1 completed
plt.rc('font', family='AppleGothic') # window:Malgun Gothic
plt.rc('axes', unicode_minus=False)

fig, ax1 = plt.subplots(figsize=(8,5))
ax1.plot(t10y2y, color='black', lw=0.5, label='10Y-2Y')
ax1.plot(t10y3m, color='gray', lw=0.5, label='10Y-3M')
ax1.legend()
ax1.axhline(0, color='r', ls='dashed')
ax1.set_ylabel("장단기 금리차")
ax1.legend(loc='lower right')

ax2 = ax1.twinx()
ax2.plot(sp['Close'], label='S&P500')
ax2.set_ylabel("S&P500 지수(로그)")
ax2.legend(loc='upper right')
    

❍ Fear and Greed Index

https://edition.cnn.com/markets/fear-and-greed

7가지 지표를 활용해서 산정함

동적 페이지라서 셀레니움 필요

import selenium
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
import time

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
driver.get(url='https://edition.cnn.com/markets/fear-and-greed')
time.sleep(2)

[WDM] - Downloading: 100%|█████████████████████████████████████████████████████████████████████████████████| 8.01M/8.01M [00:00<00:00, 11.6MB/s]
idx = driver.find_element(By.CLASS_NAME, 'market-fng-gauge__dial-number-value').text
driver.close()
idx
'57'

9. 해외 종목 다운로드

❍ 미국 Ticker

한경글로벌마켓 https://www.hankyung.com/globalmarket/equities/amex/top-market-capitalization

import json
import requests
import pandas as pd
import time

markets = ['nyse','nasdaq','amex']
stock = []

for market in markets:

    url = f"https://www.hankyung.com/globalmarket/data/price?type={market}&sort=market_cap_top&sector_nm=&industry_nm=&chg_net_text="
    data = requests.get(url).json() # json 형태의 데이터를 불러옴
    data_pd = pd.json_normalize(data['list'])
    data_pd
    data_pd['symbol'] = data_pd['symbol'].str.replace("-US","")
    data_pd['symbol'] = data_pd['symbol'].str.replace(".","-")
    stock.append(data_pd)
    time.sleep(2)
/var/folders/vr/g5rxclqn2qb11v47jh4p2lrw0000gn/T/ipykernel_1979/770174442.py:16: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
  data_pd['symbol'] = data_pd['symbol'].str.replace(".","-")
/var/folders/vr/g5rxclqn2qb11v47jh4p2lrw0000gn/T/ipykernel_1979/770174442.py:16: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
  data_pd['symbol'] = data_pd['symbol'].str.replace(".","-")
/var/folders/vr/g5rxclqn2qb11v47jh4p2lrw0000gn/T/ipykernel_1979/770174442.py:16: FutureWarning: The default value of regex will change from True to False in a future version. In addition, single character regular expressions will *not* be treated as literal strings when regex=True.
  data_pd['symbol'] = data_pd['symbol'].str.replace(".","-")
stock_bind = pd.concat(stock)
stock_bind
symbol hname primary_exchange_name sector_nm industry_nm market_cap close_price chg_net chg_rate volume avg_volume turnover dps l52w_price h52w_price per open_price locdt
0 BRK-A 버크셔 해서웨이 NYSE 금융 상해보험 700187.7787 478005.0000 6505.0000 1.3796 5095 4814.4286 2408407.7811 0 393012.2500 531880.0000 0 473479.4488 2023-04-06
1 BRK-B 버크셔 해서웨이 NYSE 금융 상해보험 686652.9097 312.5100 2.1200 0.6830 3132148 4765457.7619 975452.3787 0 259.8500 354.3300 0 309.8200 2023-04-06
2 UNH 유나이티드헬스그룹 NYSE 헬스케어 서비스 건강관리 478373.2701 512.8100 3.5800 0.7030 3472551 3381885.4762 1775900.0744 6.6 449.7009 558.1000 25.027 511.0000 2023-04-06
3 XOM 엑슨모빌 NYSE 에너지 종합석유산업 468367.3995 115.0500 -1.9400 -1.6583 15777957 19682549.4286 1824534.6842 3.64 79.2900 119.6300 8.3055 116.8600 2023-04-06
4 TSM TSMC NYSE 전기전자 반도체 468029.7600 90.2400 0.0400 0.0443 5874515 9969092.4286 530225.7386 1.4112 59.4300 104.5000 11.376 89.6600 2023-04-06
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
230 NBY NovaBay Pharmaceuticals, Inc. AMEX 헬스케어 기술 의약품 3.2567 1.6000 -0.1000 -5.8824 2843 14109.5238 4.2799 0 1.2400 12.6350 0 1.6100 2023-04-06
231 EFSH 1847 Holdings LLC AMEX 상업 서비스 기타 산업서비스 3.2225 0.7900 -0.0599 -7.0479 3806 14881.9524 2.9055 0.525 0.7007 18.0000 0 0.7699 2023-04-06
232 CPHI China Pharma Holdings, Inc. AMEX 헬스케어 기술 의약품 2.9519 0.3490 0.0080 2.3460 107812 163925.3333 35.4662 0 0.3200 4.1480 0 0.3388 2023-04-06
233 DXF Dunxin Financial Holdings Ltd AMEX 금융 금융리스 2.6183 0.1254 -0.0271 -17.7705 910720 864171.1429 116.0838 0 0.1213 0.8000 0 0.1440 2023-04-06
234 UFAB Unique Fabricating Inc AMEX 제조업 자동차 부품 1.8268 0.1557 0.0015 0.9728 172462 242471.7619 11.7589 0 0.1400 1.8600 0 0.1500 2023-04-06

5941 rows × 18 columns

temp = pd.read_pickle('./data/tickers_usa_hankyung.pickle')
temp
symbol hname primary_exchange_name sector_nm industry_nm market_cap close_price chg_net chg_rate volume avg_volume turnover dps l52w_price h52w_price per open_price locdt
0 BRK-A 버크셔 해서웨이 NYSE 금융 상해보험 690659.1723 471500.0000 -8715.0000 -1.8148 4412 4584.5556 2110422.5624 0 393012.2500 544389.2600 0 479733.2450 2023-03-07
1 BRK-B 버크셔 해서웨이 NYSE 금융 상해보험 683598.7753 311.1200 -5.8500 -1.8456 3610304 3430777.6111 1126597.7294 0 259.8500 362.1000 0 316.3900 2023-03-07
2 TSM TSMC NYSE 전기전자 반도체 460783.2080 88.8500 -0.7300 -0.8149 8761066 12403414.5000 778860.1537 1.4144 59.4300 109.7550 11.376 89.9500 2023-03-07
3 XOM 엑슨모빌 NYSE 에너지 종합석유산업 459642.3469 111.6100 -2.2000 -1.9330 11525289 14504950.0000 1286414.6486 3.64 76.2500 119.6300 8.3055 112.8100 2023-03-07
4 V 비자 NYSE 상업 서비스 기타 산업서비스 459603.1114 223.1700 -3.5800 -1.5788 4386798 5017618.5000 984718.8474 1.8 174.6000 234.3000 29.6487 226.7500 2023-03-07
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
226 AMPE Ampio Pharmaceuticals Inc AMEX 헬스케어 기술 의약품 4.2014 0.2785 -0.0014 -0.5002 45572 114996.2222 12.4635 0 0.1990 9.7050 0 0.2611 2023-03-07
227 NBY NovaBay Pharmaceuticals, Inc. AMEX 헬스케어 기술 의약품 4.1319 2.0300 -0.1300 -6.0185 9634 12672.6667 20.1074 0 1.2400 12.6350 0 2.1900 2023-03-07
228 DXF Dunxin Financial Holdings Ltd AMEX 금융 금융리스 3.4409 0.1648 -0.0041 -2.4275 55625 148780.6667 9.0143 0 0.1505 0.8000 0 0.1625 2023-03-07
229 UFAB Unique Fabricating Inc AMEX 제조업 자동차 부품 3.1679 0.2700 0.0000 0.0000 292401 254710.6111 76.9324 0 0.2480 2.2600 0 0.2882 2023-03-07
230 CPHI China Pharma Holdings, Inc. AMEX 헬스케어 기술 의약품 3.0815 0.5967 -0.0333 -5.2857 160661 1687045.2778 94.3365 0 0.5700 4.8400 0 0.6110 2023-03-07

5987 rows × 18 columns

# stock_bind.to_pickle('./data/tickers_usa_hankyung.pickle')

❍ 그외 해외

investing.com > Market > Stocks > Stock screener

Select Equity Type > ORD : 보통주 DRC : DR종목, Prefrred : 우선주

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from bs4 import BeautifulSoup
import math
import pandas as pd
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
import numpy as np
from tqdm import tqdm
import time

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
nationcode = '5' # 국가코드 5: 미국

url = f"https://www.investing.com/stock-screener/?sp=country::{nationcode}|sector::a|industry::a|equityType::ORD|exchange::a%3Ceq_market_cap;1"
driver.get(url)

# driver 페이지가 열리고 엘리먼트가 보일 때까지 기다린다 ! 최대 10초 동안
WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.XPATH, '//*[@id="resultsTable"]/tbody')))

# 페이지 수 찾기
end_num = driver.find_element(By.CLASS_NAME, value = 'js-total-results').text
end_num = math.ceil(int(end_num) / 50) # 1페이지에 50개 종목

# 빈리스트 생성
all_data_df = []

for i in tqdm(range(1, end_num+1)):
    
    url = f"https://www.investing.com/stock-screener/?sp=country::{nationcode}|sector::a|industry::a|equityType::ORD|exchange::a%3Ceq_market_cap;{i}"
    driver.get(url)
    
    try : 
        # driver 페이지가 열리고 엘리먼트가 보일 때까지 기다린다 ! 최대 10초 동안
        WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.XPATH, '//*[@id="resultsTable"]/tbody')))
    
    except: # 오류가 있을 시
        time.sleep(1) # 쉬고
        driver.refresh() # 새로고침
        WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.XPATH, '//*[@id="resultsTable"]/tbody')))
        
    
    html = BeautifulSoup(driver.page_source, 'lxml')

    html_table = html.select('table.genTbl.openTbl.resultsStockScreenerTbl.elpTbl')
    
    df_table = pd.read_html(html_table[0].prettify()) # beautifulsoup로 파싱한 것을 unicode로 바꿔줍니다.
    df_table_select = df_table[0][["Name","Symbol","Exchange","Sector","Market Cap"]]
    
    all_data_df.append(df_table_select)
    time.sleep(1)

driver.quit()
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████| 171/171 [10:09<00:00,  3.56s/it]
all_data_df_bind = pd.concat(all_data_df, axis=0)
all_data_df_bind['country']='United States'

from datetime import datetime
all_data_df_bind['date'] = datetime.today().strftime("%Y-%m-%d")
all_data_df_bind = all_data_df_bind[~all_data_df_bind['Name'].isnull()] # Name이 빈칸인건 제외
all_data_df_bind = all_data_df_bind[all_data_df_bind['Exchange'].isin(['NASDAQ','NYSE','NYSE Amex'])] # 거래소 필터링
all_data_df_bind = all_data_df_bind.drop_duplicates(["Symbol"])
all_data_df_bind.reset_index(inplace=True, drop=True)
all_data_df_bind = all_data_df_bind.replace({np.nan : None})

all_data_df_bind
Name Symbol Exchange Sector Market Cap country date
0 Apple AAPL NASDAQ Technology 2.61T United States 2023-04-08
1 Microsoft MSFT NASDAQ Technology 2.17T United States 2023-04-08
2 Alphabet C GOOG NASDAQ Technology 1.39T United States 2023-04-08
3 Alphabet A GOOGL NASDAQ Technology 1.39T United States 2023-04-08
4 Amazon.com AMZN NASDAQ Consumer Cyclicals 1.05T United States 2023-04-08
... ... ... ... ... ... ... ...
5521 5E Advanced Materials FEAM NASDAQ Basic Materials - United States 2023-04-08
5522 MGO Global MGOL NASDAQ None - United States 2023-04-08
5523 CaliberCos CWD NASDAQ None - United States 2023-04-08
5524 Chanson International Holding CHSN NASDAQ None - United States 2023-04-08
5525 Hongli HLP NASDAQ None - United States 2023-04-08

5526 rows × 7 columns

# all_data_df_bind.to_pickle("./data/tickers_usa_investing.pickle")

10. 해외종목 가격, 재무제표

# 시간이 너무 오래걸려서 돌리지 않음

❍ 가격

# 미국 시장 데이터만 필요한 경우, 유료 데이터 벤더 이용하는 것도 좋음 
# stock data api
# polygon, alpha vantage, finhub, 
# tiingo > 10$/month >> api서비스 이용 , 안정적인 데이터를 더 빠르게 

# 미국 외의 데이터
# finance.yahoo.com
# 
import yfinance as yf
import pandas as pd
import numpy as np
from tqdm import tqdm
import time
#ticker_list = pd.read_pickle("./data/tickers_usa_hankyung.pickle")
ticker_list = pd.read_pickle("./data/tickers_usa_investing.pickle")
print(ticker_list.shape)
(5526, 7)
df = pd.DataFrame(columns = ['Date','Open','High','Low','Close','Adj Close','Volume','ticker'])
error_list = []
for i in tqdm(range(0, len(ticker_list))):
    
    ticker = ticker_list['Symbol'][i]

    # 오류 무시
    try : 
        price = yf.download(ticker, progress=False) #progress=False : 진행상황 표기 없애줌
        price = price.reset_index()
        price['ticker'] = ticker
        df = pd.concat([df, price])
        
    except:
        print(ticker)
        error_list.append(ticker)
        
    time.sleep(1)

❍ 재무제표

# !pip install yahooquery

from yahooquery import Ticker
import numpy as np
import pandas as pd
from tqdm import tqdm
import time

# 정보 다운로드
data = Ticker("AAPL")
# data.asset_profile
# data.summary_detail
# data.history()

# 연간 재무제표
data_y = data.all_financial_data(frequency='a') #'a': year, 'q':quarter
data_y.reset_index(inplace=True)
data_y = data_y.loc[:, ~data_y.columns.isin(['periodType','currencyCode'])] # 불필요열 삭제
data_y = data_y.melt(id_vars = ['symbol','asOfDate'])
data_y = data_y.replace([np.nan], None) # SQL에 np.nan은 저장안되므로 변경
data_y['freq'] = 'y'
data_y.columns = ['ticker','date','account','value','freq']

# 분기 재무제표
data_q = data.all_financial_data(frequency='q') #'a': year, 'q':quarter
data_q.reset_index(inplace=True)
data_q = data_q.loc[:, ~data_q.columns.isin(['periodType','currencyCode'])] # 불필요열 삭제
data_q = data_q.melt(id_vars = ['symbol','asOfDate'])
data_q = data_q.replace([np.nan], None) # SQL에 np.nan은 저장안되므로 변경
data_q['freq'] = 'q'
data_q.columns = ['ticker','date','account','value','freq']

# 데이터합치기
data_fs = pd.concat([data_y, data_q], axis=0)
data_fs
ticker date account value freq
0 AAPL 2019-09-30 AccountsPayable 46236000000.0 y
1 AAPL 2020-09-30 AccountsPayable 42296000000.0 y
2 AAPL 2021-09-30 AccountsPayable 54763000000.0 y
3 AAPL 2022-09-30 AccountsPayable 64115000000.0 y
4 AAPL 2019-09-30 AccountsReceivable 22926000000.0 y
... ... ... ... ... ...
603 AAPL 2022-12-31 TradeandOtherPayablesNonCurrent None q
604 AAPL 2022-03-31 WorkingCapital -9328000000.0 q
605 AAPL 2022-06-30 WorkingCapital -17581000000.0 q
606 AAPL 2022-09-30 WorkingCapital -18577000000.0 q
607 AAPL 2022-12-31 WorkingCapital -8509000000.0 q

1216 rows × 5 columns

ticker_list = pd.read_pickle("./data/tickers_usa_investing.pickle")

df_fs= pd.DataFrame(columns = ['ticker','date','account','value','freq'])
error_list_fs = []

for i in tqdm(range(0, len(ticker_list))):
    
    ticker = ticker_list['Symbol'][i]
    
    # 오류 무시
    try : 

        # 정보 다운로드
        data = Ticker(ticker)
        
        # 연간 재무제표
        data_y = data.all_financial_data(frequency='a') #'a': year, 'q':quarter
        data_y.reset_index(inplace=True)
        data_y = data_y.loc[:, ~data_y.columns.isin(['periodType','currencyCode'])] # 불필요열 삭제
        data_y = data_y.melt(id_vars = ['symbol','asOfDate'])
        data_y = data_y.replace([np.nan], None) # SQL에 np.nan은 저장안되므로 변경
        data_y['freq'] = 'y'
        data_y.columns = ['ticker','date','account','value','freq']

        # 분기 재무제표
        data_q = data.all_financial_data(frequency='q') #'a': year, 'q':quarter
        data_q.reset_index(inplace=True)
        data_q = data_q.loc[:, ~data_q.columns.isin(['periodType','currencyCode'])] # 불필요열 삭제
        data_q = data_q.melt(id_vars = ['symbol','asOfDate'])
        data_q = data_q.replace([np.nan], None) # SQL에 np.nan은 저장안되므로 변경
        data_q['freq'] = 'q'
        data_q.columns = ['ticker','date','account','value','freq']

        # 데이터합치기
        data_fs = pd.concat([data_y, data_q], axis=0)
        
        # 데이터프레임 합치기
        df_fs = pd.concat([df_fs, data_fs])
        
    except:
        print(ticker)
        error_list_fs.append(ticker)
        
    time.sleep(1)

❒ (참고)

  • 유투브 : 헨리의 퀀트대학 (https://www.youtube.com/channel/UCHfiWvw33aSBktAlWICfPKQ)

댓글남기기