본문 바로가기
공대생/무작정 해보는 파이썬

Python 퀀트 투자 구현

by 흔한 공대생 2020. 4. 19.
728x90
반응형

본 포스팅에서는 파이썬을 이용하여 퀀트 투자를 구현하는 내용에 대해 다룬다.


 

들어가며

 지난 포스팅에서 퀀트 투자에 대해 다루었다. 이번 포스팅에서는 지난 포스팅에 이어서 마법 공식을 실제로 구현, 30개의 종목을 추천해주는 프로그램을 만들어보도록 하겠다.

 퀀트 투자 자체에 대한 내용은 다음을 참고하자. 2020/04/13 - [재테크] - 퀀트 투자

 

퀀트 투자

본 포스팅에서는 퀀트 투자에 대해서 소개한다. 주식 투자에 관심을 기울이고 공부하다보면 '퀀트'라는 단어를 자주 목격할 수 있다. 최근에는 퀀트 강의를 해준다는 광고도 심심찮게 만날 수 있다. 과연 퀀트가..

commonengineerr.tistory.com


 

마법공식

 우리가 구현할 마법 공식에 대한 설명부터 하겠다. 마법 공식은 '조엘 그린블란트'라는 사람이 제안한 것으로, 다음과 같다.

 이율과 투하자본수익률 각각의 순위를 매겨 합산, 순위가 높은(숫자가 작은) 30개의 기업에 분산투자하고, 1년 후 모두 매도하는 전력이다.

이때,

 이율은 기업의 수익을 가치로 나눈 값으로, 기업의 가치를 판단한다. 간단히 PER의 역수와 비슷하다고 생각할 수 있다.

 투하자본수익률은 기업의 수익을 투자한 자본으로 나눈 값으로, 기업의 퀄리티를 판단한다. 간단히 ROE와 비슷하다고 생각할 수 있다.

 

 이율과 투하자본수익률을 계산하기 위해서는 다양한 지표가 필요하기 때문에 복잡해질 수 있다. 따라서 우리는 간단하게 PER과 ROE를 이용해서 순위를 매겨보도록 하자.

 PER은 기업에 내는 수익에 비해 주가가 어느 정도인지를 나타내는 값으로, 작을수록 좋다(주가 성장가능성이 있다는 뜻). 하지만 이 값이 음수라면, 기업이 내는 수익이 음수라는 뜻이 되므로 피해야한다.

 ROE는 기업이 자본을 이용하여 이익을 낸 정도를 나타내는 값으로, 클수록 좋다(돈을 많이 벌어오는 기업이라는 뜻).


 

데이터 크롤링

 계산에 사용할 데이터는 네이버 증권(https://finance.naver.com/)에서 가져오도록 하겠다. 국내 증시의 배당 탭을 확인해보면 별 다른 작업 없이 ROE와 PER 값을 확인할 수 있다는 것을 알 수 있다.

 

 크롤링에는 다음 링크를 사용하도록 하겠다.

https://finance.naver.com/sise/dividend_list.nhn?field=roe&sosok=&ordering=desc

 이유는 잘 모르겠지만, 링크의 구조에 따라 몇몇 기업들이 제외되는 현상을 발견했다. 보통 페이지 상단에서 밀린 기업들이다. 위 링크를 이용하면 ROE가 높은 순으로 정렬되기 때문에, 페이지 상단에 우리가 원하는 기업이 위치할 확률이 높아진다. 이유를 모르니 이렇게라도 하는 것이다..

 이전과 마찬가지로 페이지 소스를 확인해보면, bnd_wid라는 클래스 아래 table, tbody, tr, td 까지 내려가면 테이블의 각 값들이 들어있는 것을 확인할 수 있다.

 

 또한 링크에는 25페이지까지 있는 것을 확인할 수 있다. 페이지 이동은 url 마지막에 "page=(숫자)" 부분을 바꾸어줌으로써 할 수 있기 때문에 for문을 이용해보자.

 지금까지 이야기한 것을 토대로 코드를 작성해보면 다음과 같다.

import requests
from bs4 import BeautifulSoup

header = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'}

for i in range(25):     # 25페이지까지 있음
    url = "https://finance.naver.com/sise/dividend_list.nhn?field=roe&sosok=&ordering=desc&page="+str(i+1)
    r = requests.get(url, headers = header)
    html = r.content
    soup = BeautifulSoup(html, 'html.parser')

    c = soup.select('.bnd_wid > table > tbody > tr > td')

 

데이터 골라내기

 위 소스코드를 사용하여 c를 출력해보면 엄청 긴 리스트가 나온다. 테이블의 모든 데이터가 구분되어지지 않고 쭉 저장되기 때문이다. 따라서 우리가 필요한 정보만 골라내는 작업이 필요하다.

 우리가 사용할 데이터는 기업 이름, 현재가, ROE, PER이다. 한 줄(한 기업)에 있는 데이터는 기업명을 포함하여 총 12개, 따라서 리스트의 항을 12로 나누었을 때 각각 0, 1, 6, 7이 되는 값들을 가져오면 된다. 이때 ROE와 PER은 파이썬 내에서 정렬을 시켜야하기 때문에 숫자 형식이어야한다. (제공되는 값은 , 를 포함한 문자열이다.) 따라서 함수를 이용하여 숫자 형식으로 형변환 시켜주도록 하겠다.

 이때 빈 항들도 존재한다. 네이버에서 정보를 제공할 때 가독성이 좋도록 5개 기업씩 묶어서 보여주는데, 그 구분선을 빈 테이블("\xa0"로 표시됨)로 구현하기 때문이다. 굳이 필요하지 않으니 크롤링과 동시에 체크, 저장하지 않도록 하겠다.

  ROE와 PER에 대한 데이터가 없는 기업도 있다. 그런 기업은 해당 칸에 숫자 대신 "-"가 들어가기 때문에 확인 후 빼주어야한다. 특별한 방법을 떠올리지 못해, 미리 t라는 test 변수를 설정하고 1을 대입, 만약 데이터가 없는 기업이라면 t에 0을 넣어주고, t가 0인 기업은 리스트에 넣지 않도록 확인해주는 (고전적인) 작업을 수행토록 했다.

 설명이 좀 장황했다. 위 내용을 소스 코드로 표현하면 다음과 같다.

import requests
from bs4 import BeautifulSoup

def trans(data): # ,표기변환함수
    dataa = data.split(',')
    data=''
    for k in dataa:
        data+=k
    return data
    
l = []  #최종적으로 우리가 원하는 데이터가 담길 리스트

header = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'}

for i in range(25):
    url = "https://finance.naver.com/sise/dividend_list.nhn?field=roe&sosok=&ordering=desc&page="+str(i+1)
    r = requests.get(url, headers = header)
    html = r.content
    soup = BeautifulSoup(html, 'html.parser')

    c = soup.select('.bnd_wid > table > tbody > tr > td')


    # 데이터 수집 시 필요한 정보들
    ll = [] #[기업, 현재가, ROE, PER] 형식으로 l 리스트에 넣을 것이다.
    t = 1  # -데이터판별변수
    
    for j in range(len(c)):
        if j%12==0:     #기업이름
            data = c[j].text
            if data=='\xa0':
                continue
            else:
                ll = [data]

        elif j%12==1:   #현재가
            data = c[j].text
            if data=='\xa0':
                continue
            else:
                ll.append(data)

        elif j%12==6:   #ROE
            data = trans(c[j].text)
            if data=='\xa0':
                continue
            elif data=='-' or float(data)<0:    # 데이터 없거나 음수라면 X
                t = 0
            else:
                ll.append(float(data))
                
        elif j%12==7:   #PER
            data = trans(c[j].text)    
            if data=='\xa0':
                continue
            elif data=='-' or float(data)<0:    # 데이터 없거나 음수라면 X
                t = 0
            else:
                ll.append(float(data))


# l 리스트에 삽입, PER이 마지막 데이터이기 때문에 PER 크롤링과 동시에 수행한다.
            if t>0:  #조건에 부합한다면,
                l.append(ll)  #저장
                t = 1  #변수초기화

 

데이터 가공

 데이터는 확보했으니 잘 가공해서 쓸만한 내용을 끄집어낼 차례다. 처리는 간단하다.

 sorted() 메소드를 사용해서 ROE, PER에 따라 정렬한 후, 순위를 매겨줄 것이다. 순위 역시 리스트에 추가한다. 그리고 나서 ROE 순위와 PER 순위를 더해주고, 그 값을 기준으로 다시 정렬한다.

 정렬할 때, 우리는 리스트 안의 리스트 안의 항의 값을 기준으로 제일 바깥 리스트를 정렬해야한다. 예를 들면 [[1, 2, 3], [1, 3, 5], [-1, -2, -3]] 이런 리스트가 있을 때, 3번 항의 값을 기준으로 정렬을 하면 [[-1, -2, -3], [1, 2, 3], [1, 3, 5]] 이런 식으로 정렬되어야하는 것이다. sorted() 메소드 안에 key 값을 넣어 사용하면 간단히 구현할 수 있다.

# 2번 항인 ROE (내림차)순으로 정렬
l = sorted(l, key = lambda x: x[2])

# ROE 순위 기입
for i in range(len(l)):
    l[i].append(len(l)-i)

# 3번 항인 PER (오름차)순으로 정렬
l = sorted(l, key = lambda x: x[3])

# PER 순위 기입
for i in range(len(l)):
    l[i].append(i+1)

    #PER 순위와 ROE 순위의 합 기입
    l[i].append(l[i][4]+l[i][5])
    
# 6번 항 순으로 정렬
l = sorted(l, key = lambda x: x[6])

 

 마지막으로 상위 30개 (혹은 전부) 의 기업을 우리가 볼 수 있도록 출력해주면 된다. 우리는 엑셀을 이용해보자.

from openpyxl import Workbook
from datetime import datetime

# 자료를 수집한 날짜 기입을 위해
t = datetime.today()
y = str(t.year)
m = str(t.month).zfill(2)
d = str(t.day).zfill(2)

# 엑셀에 저장
wb = Workbook()
ws = wb.active

# 상단에 각 항의 의미(라벨)를 기입
ws.append(['기업명', '현재가', 'ROE', 'PER', 'ROE 순위', 'PER 순위', '합계 순위'])

# 상위 30개 기업만 출력하도록
for i in range(30):
    ws.append(l[i])
wb.save("quant_"+y+m+d+".xlsx")
wb.close()

 

마치며

 이렇게 퀀트 투자를 직접 구현해보았다. 물론 종목 선정뿐이지만...

 앞으로는, 이전 데이터들을 수집할 수 있는 방법을 찾아 공식을 검증할 수 있도록 프로그래밍할 필요가 있다. 또한 보다 다양한 지표들을 조합하여 최고의 공식을 찾아낼 수 있도록 만들면 좋을 것이다.

 마지막으로 전체 소스코드를 올린다.

import requests
from bs4 import BeautifulSoup
from openpyxl import Workbook
from datetime import datetime


def trans(data): # , 표기 변환
    dataa = data.split(',')
    data=''
    for k in dataa:
        data+=k
    return data
    
l = []

header = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'}

for i in range(25):     # 25페이지까지 있음
    url = "https://finance.naver.com/sise/dividend_list.nhn?field=roe&sosok=&ordering=desc&page="+str(i+1)
    r = requests.get(url, headers = header)
    html = r.content
    soup = BeautifulSoup(html, 'html.parser')

    c = soup.select('.bnd_wid > table > tbody > tr > td')


    # 데이터 수집 시 필요한 정보들
    ll = [] #[기업, 현재가, ROE, PER]
    t = 1
    
    for j in range(len(c)):
        if j%12==0:     #기업이름
            data = c[j].text
            if data=='\xa0':
                continue
            else:
                ll = [data]

        elif j%12==1:   #현재가
            data = c[j].text
            if data=='\xa0':
                continue
            else:
                ll.append(data)

        elif j%12==6:   #ROE
            data = trans(c[j].text)
            if data=='\xa0':
                continue
            elif data=='-' or float(data)<0:
                t = 0
            else:
                ll.append(float(data))
                
        elif j%12==7:   #PER
            data = trans(c[j].text)    
            if data=='\xa0':
                continue
            elif data=='-' or float(data)<0:
                t = 0
            else:
                ll.append(float(data))


            # 조건에 부합한다면,
            if t>0:
                l.append(ll)
                t = 1


# 2번 항인 ROE (내림차)순으로 정렬
l = sorted(l, key = lambda x: x[2])

# ROE 순위 기입
for i in range(len(l)):
    l[i].append(len(l)-i)

# 3번 항인 PER (오름차)순으로 정렬
l = sorted(l, key = lambda x: x[3])

# PER 순위 기입
for i in range(len(l)):
    l[i].append(i+1)

    #PER 순위와 ROE 순위의 합 기입
    l[i].append(l[i][4]+l[i][5])
    
# 6번 항 순으로 정렬
l = sorted(l, key = lambda x: x[6])


t = datetime.today()
y = str(t.year)
m = str(t.month).zfill(2)
d = str(t.day).zfill(2)

# 엑셀에 저장
wb = Workbook()
ws = wb.active
ws.append(['기업명', '현재가', 'ROE', 'PER', 'ROE 순위', 'PER 순위', '합계 순위'])
for i in range(30):
    ws.append(l[i])
wb.save("quant_"+y+m+d+".xlsx")
wb.close()

 

코드 실행 결과. 저작권 문제로 인해 일부러 깨트림

728x90
반응형

댓글