본문 바로가기
데이터분석/크롤링

파이썬 퀀트투자(6): 네이버금융 수정주가 크롤링, 적재

by 코듀킹 2024. 10. 12.

주가 데이터는 투자를 함에 있어 반드시 필요한 데이터입니다. 인터넷에서 주가를 수집할 수 있는 방법은 매우 많지만, 퀀트 투자를 위한 백테스트나 종목선정을 위해서는 수정주가가 필요합니다. 그래서 이 글에서는 일반적인 주가가 아닌 수정주가 크롤링 방법을 알아보겠습니다.

 

 

수정주가가 필요한 이유를 알아보기 위해 실제 사례를 살펴보겠습니다. 삼성전자는 2018년 5월 기존의 1주를 50주로 나누는 액면분할을 실시했고, 265만 원이던 주가는 다음날 50분의 1인 5만 3000원으로 거래되었습니다. 이러한 이벤트를 고려하지 않고 주가만 살펴본다면 마치 -98% 수익률을 기록한 것 같지만, 투자자 입장에서는 1주이던 주식이 50주로 늘어났기 때문에 자산에는 아무런 변화가 없습니다. 이를 고려하는 방법은 액면분할 전 모든 주가를 50으로 나누어 연속성을 갖게 만드는 것이며, 이를 '수정주가'라고 합니다. 따라서 백테스트 혹은 퀀트 지표 계산에는 수정주가가 사용되어야 하며, 네이버 금융에서 제공하는 정보를 통해 모든 종목의 수정주가를 손쉽게 구할 수 있습니다.

 

 

먼저 네이버 금융에서 특정종목(예: 삼성전자)의 [차트]탭을 선택합니다. 해당 차트는 주가 데이터를 받아와 화면에 그래프를 그려주는 형태입니다. 따라서 데이터가 어디에서 오는지 알기 위해 개발자도구 화면을 이용해야합니다. 화면을 연 상태에서 [일]을 선택하면 나오는 항목 중 가장 상단 항목 [info?symbol=..]의 Request URL이 주가 데이터를 요청하는 주소입니다.

 

 

이 Request URL에 접속해보면, 날짜별로 시가, 고가, 저가, 종가, 거래량, 외국인소진율을 볼 수 있습니다. 이 때, 주가는 모두 수정주가 기준입니다. URL에서 'symbol=' 뒤에 6자리 티커만 변경하면 해당 종목의 주가 데이터가 있는 페이지로 이동할 수 있으며, 이를 이용하여 원하는 모든 종목의 수정주가 데이터를 크롤링할 수 있습니다. 또한 'startTime=' 에는 시작일자를, 'endTime=' 에는 종료일자를 입력하여 원하는 기간 만큼의 데이터를 받을 수도 있습니다.

https://m.stock.naver.com/front-api/external/chart/domestic/info?symbol=005930&requestType=1&startTime=20230806&endTime=20241012&timeframe=day

 

 

그러면 위 Request URL에서 symbol부분에 넣을 티커 정보를 불러오기 위해, DB에서 티커 데이터를 먼저 불러오겠습니다.

  1. create_engine() 함수를 통해 데이터베이스에 접속하기 위한 엔진을 만듭니다.
  2. 티커 데이터를 불러오는 쿼리를 작성하며, 가장 최근일자에 해당하는 내용을 불러오기 위해 서브쿼리에 select max(기준일) from kor_ticker을 입력합니다. 또한 종목구분에서 보통주에 해당하는 종목만 선택합니다.(스팩, 리츠, 우선주, 기타 종목 제외)
  3. read_sql() 함수를 통해 해당 쿼리를 보낸 후 데이터를 받아옵니다.
  4. engine.dipose()로 접속을 종료합니다.
from sqlalchemy import create_engine
import pandas as pd

engine = create_engine('mysql+pymysql://root:1234@127.0.0.1:3306/stock_db')
query = """
select * from kor_ticker
where 기준일 = (select max(기준일) from kor_ticker) 
	and 종목구분 = '보통주';
"""
ticker_list = pd.read_sql(query, con=engine)
engine.dispose()

ticker_list.head()

 

이제 위에서 살펴본 주가 데이터 페이지를 크롤링 하도록 하겠습니다.

  1. 향후 for문을 통해 ticker_list의 길이만큼 i를 반복시켜줄 것입니다. 이렇게하면 모든 종목의 주가를 다운로드할 수 있습니다.
  2. ticker_list['종목코드'][i]를 통해 원하는 종목의 티커를 선택합니다.
  3. 시작일(fr)과 종료일(to)에 해당하는 날짜를 만들어줍니다. today() 메서드를 이용해 오늘 날짜를 불러온 후, 시작일은 relativedelta() 클래스를 이용해 5년을 빼줍니다. (본인이 원하는 기간 만큼을 빼주면 됩니다.) 그 후 strftime() 메서드를 통해 'yyyymmdd' 형식을 만들어 줍니다. 종료일은 오늘 날짜를 그대로 사용합니다.
  4. 티커, 시작일, 종료일을 이용해 주가 데이터가 있는 URL을 생성합니다.
  5. get() 함수를 통해 페이지의 데이터를 불러온 후, content 부분을 추출합니다.
  6. BytesIO()를 이용해 바이너리스트림 형태로 만든 후, read_csv() 함수를 통해 데이터를 읽어옵니다.
from dateutil.relativedelta import relativedelta
import requests as rq
from io import BytesIO
from datetime import date

i = 0
ticker = ticker_list['종목코드'][i]
fr = (date.today() + relativedelta(years=-5)).strftime("%Y%m%d")
to = (date.today()).strftime("%Y%m%d")

url = f'''https://m.stock.naver.com/front-api/external/chart/domestic/info?\
symbol={ticker}&requestType=1&startTime={fr}&endTime={to}&timeframe=day'''

data = rq.get(url).content
data_price = pd.read_csv(BytesIO(data))

data_price.head()

 

결과를 확인해보면 날짜 및 주가, 거래량, 외국인소진율 데이터가 추출됩니다. 이제 클렌징 작업을 해주어야합니다.

  1. iloc() 인덱서를 통해 날짜와 가격(시가, 고가, 저가, 종가), 거래량에 해당하는 데이터만을 선택합니다.
  2. 열 이름을 변경합니다.
  3. dropna() 함수를 통해 NA 데이터를 삭제합니다.
  4. extract() 메서드 내에 정규 표현식을 이용해 날짜 열에서 숫자만을 추출합니다.
  5. '날짜'열을 datetime 형태로 변경합니다.
  6. '종목코드'열을 새로 생성하여 티커를 입력합니다.
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.head()

 

위 과정을 응용해 모든 종목의 주가를 크롤링한 후 DB에 저장하는 과정을 살펴보도록 하겠습니다. 먼저 SQL에서 주가가 저장될 테이블(kor_price)을 만들어줍니다.

 

use stock_db;

create table kor_price
(
    날짜 date,
    시가 double,
    고가 double,
    저가 double,
    종가 double,
    거래량 double,
    종목코드 varchar(6),
    primary key(날짜, 종목코드)
);

 

 

위 코드를 for문으로 돌린 후, 새로 생성한 테이블에 적재하는 코드를 추가하면, 전종목 주가가 upsert 방식으로 DB에 저장됩니다. 

  1. DB에 연결합니다.
  2. 기준일이 최대, 즉 최근일 기준 보통주에 해당하는 티커 리스트(ticker_list)만 불러옵니다.
  3. DB에 저장할 쿼리(query)를 입력합니다.
  4. 오류 발생시 저장할 리스트(error_list)를 만듭니다.
  5. for문을 통해 전종목 주가를 다운로드 받으며, 진행상황을 알기위해 tqdm() 함수를 이용합니다.
  6. URL 생성, 데이터 다운로드 및 데이터 클렌징 및 DB에 저장은 위와 동일하며, try except문을 통해 오류가 발생시 티커를 출력 후 error_list에 저장합니다.
  7. 무한 크롤링을 방지하기 위해 한 번의 루프가 끝날 때마다 타임슬립을 적용합니다.
  8. 모든 작업이 끝나면 DB와의 연결을 종료합니다.
# 패키지 불러오기
import pymysql
from sqlalchemy import create_engine
import pandas as pd
from datetime import date
from dateutil.relativedelta import relativedelta
import requests as rq
import time
from tqdm import tqdm
from io import BytesIO

# DB 연결
engine = create_engine('mysql+pymysql://root:1234@127.0.0.1:3306/stock_db')
con = pymysql.connect(user='root',
                      passwd='1234',
                      host='127.0.0.1',
                      db='stock_db',
                      charset='utf8')
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://m.stock.naver.com/front-api/external/chart/domestic/info?\
		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()

 

 

작업이 끝난 후 SQL의 kor_price 테이블을 확인해보면 전 종목 주가가 저장되어 있습니다. 시간이 지나 위 코드를 다시 실행하면 upsert 형식을 통해 수정된 주가는 update를, 새로 입력된 주가는 insert를 합니다.

 

 

참고자료

https://github.com/hyunyulhenry/quant_py/blob/main/data_korea.ipynb

 

다음편

 

파이썬 퀀트투자(7): 재무제표 크롤링, 적재

주가와 더불어 재무제표와 가치지표 역시 투자에 있어 핵심이 되는 데이터입니다. 이번에는 가치지표를 계산하기 위한 재무제표를 데이터를 크롤링해보겠습니다. 전편: 파이썬 퀀트투자(6): 네

coduking.tistory.com

 

댓글