프로젝트/[ing]_백테스팅_툴

OpenDartReader 로 종목을 분류해보자 (4) - 근데 pykrx를 곁들인...

728x90
반응형

 

 

 

** 새해 되고 첫 글이네요 ㅎㅎ 오시는 분들 새해 복 많이 받으세요!

본 글을 읽기 앞서, 앞의 포스팅을 보고 오시면 도움이 될 것입니다.

 

2021.12.02 - [프로젝트/[ing]_백테스팅_툴] - OpenDartReader 로 종목을 분류해보자 (1)

 

OpenDartReader 로 종목을 분류해보자 (1)

기업을 분석하는데는 기본적으로 여러 데이터가 필요합니다. 그 중에서도 기업의 기본 (펀더멘털) 이라 할 수 있는 재무데이터 분석이 필수적이라 할 수 있죠. 하지만 분석할 기업은 많습니다. 2

dragon1-honey1-wayfarer.tistory.com

 

2021.12.03 - [프로젝트/[ing]_백테스팅_툴] - OpenDartReader 로 종목을 분류해보자 (2)

 

OpenDartReader 로 종목을 분류해보자 (2)

앞선 포스팅에도 말씀 드렸듯이, 이번엔 Pykrx 를 사용해서 시가총액을 불러오고 그 데이터를 앞 포스팅의 dataframe 과 합쳐보도록 하겠습니다. 되도록 앞의 내용을 보고 오시는걸 추천드립니다 ~ 2

dragon1-honey1-wayfarer.tistory.com

 

2021.12.16 - [프로젝트/[ing]_백테스팅_툴] - OpenDartReader 로 종목을 분류해보자 (3)

 

OpenDartReader 로 종목을 분류해보자 (3)

지난번 포스팅에 이어, 이번에는 분류한 재무제표 데이터를 각종 지표로 만들어 보겠습니다. 이 글을 처음 보셨다면 시리즈 1, 2편을 보고 오시는게 도움이 되실 겁니다. 2021.12.02 - [프로젝트/[ing]

dragon1-honey1-wayfarer.tistory.com

 

이번엔 좀 더 확장시켜서, 여러 종목에 대해 재무지표를 수집하고 투자지표로 변환하는 과정을 포스팅 하겠습니다.

투자지표를 변환하여 이를 기반으로 다양한 퀀트투자 로직을 만들 수 있겠죠.

 

여기서 여러 종목이라 함은 시가총액 1 ~ 100 위를 대상으로 하였습니다. 

 

1. 추가 / 변경된 부분

 

사실 앞의 포스팅들에서 크게 바뀐건 없지만, 아무래도 여러종목을 대상으로 루프를 돌려야 하니 추가 / 변경된 부분이 있습니다.

 

1. 시가총액을 불러오고 정렬하는 부분 추가

본 포스팅에 맞는 프로그램을 구동하려면 제일 먼저 해야하는 부분입니다. pykrx에서 쉽게 얻어올 수 있습니다. -- __init__

 

import pykrx
import pandas

# 시가총액 큰 순서대로 나열
df_raw = stock.get_market_cap_by_ticker("20220105", market="ALL", acending=False)
df_2 = df_raw.reset_index()# 종목코드 추출을 위한 인덱스 초기화. 이걸 안하면 컬럼에서 추출할 수 없음
df_ls = df_2['티커'].values.tolist() # 리스트 형태로 변환
self.df_decended = [] # 빈 리스트 생성
for i in range(0, 100): # 종목코드만 받아옴
    self.df_decended.append(df_ls[i])

 

2. 종목 업데이트 현황을 체크하는 부분 추가

json 형태로 관리합니다. 종목을 업데이트 하지 않을 경우, OpenDartReader / pykrx 크롤링 중 간혹 끊어지는 현상이 발생하는데 이 경우 처음부터 다시 시작하는 불상사가 발생합니다. -- __init__

 

self.stock_kinds = len(self.df_decended) # 종목 개수를 미리 구해둔다. 
self.export_list = {} # 데이터 담을 리스트 생성

# 종목 업데이트 정보 확인
try:
	# 지정한 경로에서 .json 파일을 읽기 모드로 연다
	with open('파일경로/update_infos.json', 'r') as in_file:
    	self.export_list = json.load(in_file)
        
except FileNotFoundError: 
	# 위에서 열려고 시도했던 .json 파일이 존재하지 않는 경우
	with open('파일경로/update_infos.json', 'w') as out_file: # 파일을 새로 만든다
        for i in range(0, self.stock_kinds):
	        # {"종목코드" : 0 } 의 형태로 데이터를 만든다
            self.export_list[self.df_decended[i]] = 0

		# json 형태로 dumping 한다. indent 를 쓰면 가독성이 좋아지니 참고.
        json.dump(self.export_list, out_file, indent=4)

 

종목을 업데이트 하는 부분 

 

                self.export_list[stocks_in] = 1
                with open('파일경로/update_infos.json', 'w') as out_file:
                    json.dump(self.export_list, out_file, indent=4)

 

3. 재무지표 읽는 부분 클래스 내 다른 함수로 분리

여러 종목들의 재무지표를 읽다 보면 같은 개념인데 회사별로 미묘하게 다른 이름을 가지고 있는 경우가 종종 있습니다. 심지어 같은 회사여도 연도별, 심하면 분기별로도 다른 회사들이 있어 이 조건식들을 다 추가해주어야 하기 때문에 별도 함수로 분리하여 관리합니다. ex. 분기순이익, 반기순이익, 당기순이익(손실) 이런식으로 말이죠..

아래의 코드는 2022년 1월 12일 (수) 기준, 시가총액 상위 100개 기업의 2020 ~ 2021 재무지표를 읽을 수 있는 코드 입니다.  -- condition_init (new)

 

def condition_init(self, df):
    # 재무상태표 부분
    self.condition = (df.sj_nm == '재무상태표') & ((df.account_nm == '유동자산') | (df.account_nm == 'II. 유동자산') | \
                    (df.account_nm == 'Ⅰ.유동자산') | (df.account_nm == '유동자산 합계') | (df.account_nm == 'I.유동자산') | \
                    (df.account_nm == 'I. 유동자산') | (df.account_nm == 'l. 유동자산')) # 유동자산
    self.condition_2 = (df.sj_nm == '재무상태표') & ((df.account_nm == '부채총계') | (df.account_nm == '부  채  총  계') | \
                        (df.account_nm == '부채 총계')) # 부채총계
    self.condition_10 = (df.sj_nm == '재무상태표') & ((df.account_nm == '자산총계')| (df.account_nm == '자  산  총  계') | \
                        (df.account_nm == '자산 총계') | (df.account_nm == '자본과부채총계')) # 자산총계
    self.condition_3 = (df.sj_nm == '재무상태표') & \
                ((df.account_nm == '자본총계') | (df.account_nm == '반기말자본') | (df.account_nm == '3분기말자본') | \
                (df.account_nm == '분기말자본') | (df.account_nm == '1분기말자본') | (df.account_nm == '자  본  총  계') | \
                (df.account_nm == '기말') | (df.account_nm == '자본 총계') | (df.account_nm == '기말자본 잔액') | \
                (df.account_nm == '분기말') | (df.account_nm == '반기말') | (df.account_nm == '자 본 총 계') | \
                (df.account_nm == '분기말 잔액') | (df.account_nm == '반기말 잔액'))  #자본총계
    # 손익계산서 부분
    self.condition_4 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서')) & ((df.account_nm == '매출액') | \
                        (df.account_nm == '수익(매출액)') | (df.account_nm == '매출') | (df.account_nm == '영업수익') | \
                        (df.account_nm == '수익') | (df.account_nm == '영업수익(매출액)') | (df.account_nm == 'Ⅰ.매출') | \
                        (df.account_nm == 'I.매출액') | (df.account_nm == 'Ⅰ.매출액') | (df.account_nm == 'I. 매출액') | \
                        (df.account_nm == '매출액(영업수익)'))
    self.condition_5 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서')) & ((df.account_nm == '매출총이익') | \
                        (df.account_nm == '매출 총이익') | (df.account_nm == '매출총이익(손실)') | (df.account_nm == 'Ⅲ.매출총이익')| \
                        (df.account_nm == 'III.매출총이익') | (df.account_nm == 'Ⅲ.매출총이익') | (df.account_nm == 'III. 매출총이익(손실)') | \
                        (df.account_nm == 'III. 매출총이익') | (df.account_nm == '매출총이익(영업수익)'))
    self.condition_6 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서')) & \
                  ((df.account_nm == '영업이익(손실)') | (df.account_nm == '영업이익') | \
                    (df.account_nm == '영업손실(이익)') | (df.account_nm == '영업손익')| \
                    (df.account_nm == '계속영업이익(손실)') | (df.account_nm == 'Ⅳ.영업이익') | \
                    (df.account_nm == 'VI.영업이익(손실)') | (df.account_nm == 'V.영업이익') | (df.account_nm == 'V. 영업이익(손실)') | \
                    (df.account_nm == 'IV. 영업이익') | (df.account_nm == '영업이익 (손실)') | (df.account_nm == '영업손실'))
    self.condition_7 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서') | (df.sj_nm == '현금흐름표')) & \
                    ((df.account_nm == '당기순이익(손실)') | (df.account_nm == '당기순이익') | \
                    (df.account_nm == '분기순이익') | (df.account_nm == '분기순이익(손실)') | (df.account_nm == '반기순이익') | \
                    (df.account_nm == '반기순이익(손실)') | (df.account_nm == '연결분기순이익') | (df.account_nm == '연결반기순이익') | \
                    (df.account_nm == '연결당기순이익')|(df.account_nm == '연결분기(당기)순이익')|(df.account_nm == '연결반기(당기)순이익') | \
                    (df.account_nm == '연결분기순이익(손실)') | (df.account_nm == '당기순손익') | (df.account_nm == 'Ⅶ.당기순이익') | \
                    (df.account_nm == 'VIII.당기순이익(손실)') | (df.account_nm == 'XIII. 당기순이익(손실)') | (df.account_nm == '반기연결순이익(손실)') | \
                    (df.account_nm == '연결당기순이익(손실)') | (df.account_nm == '당기의 순이익') | (df.account_nm == '분기기순이익(손실)') | \
                    (df.account_nm == '분기순손익') | (df.account_nm == '분기순손실') | (df.account_nm == '반기순손익') | (df.account_nm == '분기연결순손실') | \
                    (df.account_nm == '분기연결순이익(손실)'))

    # 현금흐름표 부분
    self.condition_8 = (df.sj_nm == '현금흐름표') & ((df.account_nm == '영업활동으로 인한 현금흐름') | (df.account_nm == '영업활동 현금흐름') | \
                        (df.account_nm == '영업활동현금흐름') | (df.account_nm == '영업활동으로인한 현금흐름') | (df.account_nm == '영업활동으로인한순현금흐름')| \
                        (df.account_nm == 'Ⅰ. 영업활동으로 인한 현금흐름') | (df.account_nm == 'I.영업활동현금흐름')|(df.account_nm == '영업활동으로인한현금흐름')|\
                        (df.account_nm == '영업활동으로 인한 순현금흐름') | (df.account_nm == '영업활동으로인한 순현금흐름') | (df.account_nm == '1.영업활동현금흐름')|\
                        (df.account_nm == 'Ⅰ.영업활동현금흐름') | (df.account_nm == '영업활동으로 인한 현금 흐름') | (df.account_nm == 'I. 영업활동현금흐름') | \
                        (df.account_nm == 'I. 영업활동으로 인한 현금흐름') | (df.account_nm == 'I.영업활동으로 인한 현금흐름') | \
                        (df.account_nm == '영업활동순현금흐름 합계') | (df.account_nm == 'Ⅰ. 영업활동현금흐름') | \
                        (df.account_nm == '영업으로부터 창출된 현금흐름') | (df.account_nm == 'Ⅰ. 영업활동으로 인한 순현금흐름'))
    self.condition_9 = (df.sj_nm == '현금흐름표') & ((df.account_nm == '투자활동으로 인한 현금흐름') | (df.account_nm == '투자활동 현금흐름') | \
                        (df.account_nm == '투자활동현금흐름') | (df.account_nm == '투자활동으로인한 현금흐름') | (df.account_nm == '투자활동으로인한순현금흐름')| \
                        (df.account_nm == 'Ⅱ. 투자활동으로 인한 현금흐름') | (df.account_nm == 'II.투자활동현금흐름') | (df.account_nm == '투자활동으로인한현금흐름')|\
                        (df.account_nm == '투자활동으로 인한 순현금흐름') | (df.account_nm == '투자활동으로인한 순현금흐름')|(df.account_nm == '2.투자활동현금흐름')|\
                        (df.account_nm == 'Ⅱ.투자활동현금흐름') | (df.account_nm == 'Ⅱ. 투자활동현금흐름') | (df.account_nm == 'II. 투자활동으로 인한 현금흐름') | \
                        (df.account_nm == 'II.투자활동으로 인한 현금흐름') | (df.account_nm == 'II. 투자활동현금흐름') | \
                        (df.account_nm == '투자활동순현금흐름 합계') | (df.account_nm == '투자활동으로부터의 순현금유입(유출)') | (df.account_nm == 'Ⅱ. 투자활동으로 인한 순현금흐름'))

    self.condition_11 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서')) & (df.account_nm == '매출원가')
    self.condition_12 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서')) & (df.account_nm == '법인세비용차감전순이익(손실)')
    self.condition_13 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서')) & (df.account_nm == '법인세비용(혜택)')
    self.condition_14 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서')) & (df.account_nm == 'II.재료비') # 008770 매출총이익 계산하기 위한 것

 

4. 결과데이터의 DB 화

DB 프로그램은 마리아디비를 사용하였습니다. 여기선 마리아디비의 상세 내용은 다루진 않습니다만,, 가볍게 별개 포스팅으로 다루도록 하겠습니다. DB 없이 데이터를 취득하고 싶다면 결과값을 pandas 의 to_excel() 과 같은 함수를 사용하셔서 다루시길 추천드립니다. -- commit_into_DB (new)

 

5. 투자지표 변환 함수 추가

재무지표 데이터를 가공한 뒤 바로 투자지표 데이터프레임에 계산하여 이 또한 DB 에 반영할 수 있도록 하였습니다. 

 

def convInvests(self, df, dates):
    # 초기화
    df_inv = pd.DataFrame(columns=['PER','PBR','PSR','GP_A','POR','PCR','PFCR','NCAV'], index=['1900-01-01'])
    df_index = df.reset_index()
    for i in range(3, len(df)):
        per = df.iloc[i]['mk'] / (df.iloc[i-3]['net_inc'] + df.iloc[i-2]['net_inc'] + \
                                    df.iloc[i-1]['net_inc'] + df.iloc[i]['net_inc']) # 시가총액 / 순이익
        pbr = df.iloc[i]['mk'] / df.iloc[i]['debt_tot'] # 시가총액 / 부채총계
        psr = df.iloc[i]['mk'] / (df.iloc[i-3]['rev'] + df.iloc[i-2]['rev'] + \
                                    df.iloc[i-1]['rev'] + df.iloc[i]['rev']) # 시가총액 / 매출액
        gp_a = (df.iloc[i-3]['gp'] + df.iloc[i-2]['gp'] + df.iloc[i-1]['gp'] + \
                                df.iloc[i]['gp']) / df.iloc[i]['equity_tot'] # 매출총이익 / 자산총계
        por = df.iloc[i]['mk'] / (df.iloc[i-3]['inc'] + df.iloc[i-2]['inc'] + \
                                    df.iloc[i-1]['inc'] + df.iloc[i]['inc']) # 시가총액  / 영업이익
        pcr = df.iloc[i]['mk'] / (df.iloc[i-3]['cfo'] + df.iloc[i-2]['cfo'] + \
                                    df.iloc[i-1]['cfo'] + df.iloc[i]['cfo']) # 시가총액 / 영업활동현금흐름
        pfcr = df.iloc[i]['mk'] / (df.iloc[i-3]['fcf'] + df.iloc[i-2]['fcf'] + \
                                    df.iloc[i-1]['fcf'] + df.iloc[i]['fcf'])# 시가총액 / 잉여현금흐름
		# 청산가치 (유동자산 - 부채총계) / 시가총액                             
        ncav = (df.iloc[i]['asset_cur'] - df.iloc[i]['debt_tot'])/ df.iloc[i]['mk']
		 # 데이터프레임에 저장
        df_inv.loc[df_index.iloc[i]['index']] = [per, pbr, psr, gp_a, por, pcr, pfcr, ncav]
        df.tail()
    df_inv.drop(['1900-01-01'], inplace=True) # 첫 행 drop

    return df_inv

 

6. 매출총이익 계산못하는 회사 반영

재무지표 항목에 매출총이익이 없거나, 매출원가 항목이 없어서 매출총이익을 계산못하는 회사가 종종 있습니다. 이런 회사들을 만날경우 오류가 나서 프로그램이 멈춰버리므로 매출총이익을 계산 못하는 회사에 대해 건너뛰는 부분을 추가했습니다.

 

       nogp_list = ['035420', '035720', '036570', '017670', '251270', '263750', '030200', '293490', 
                    '112040', '259960', '032640', '180640'] # 매출총이익 계산 못하는 회사들

                            if stocks_in == '011810': # 매출총이익 항목이 없는 회사도 있다. 이 경우, 매출액 - 매출원가로 계산.
                                grossProfit[j] = revenue[j] - int(df1.loc[self.condition_11].iloc[0]['thstrm_amount'])
                            elif stocks_in in nogp_list: # 매출총이익도 없고 이를 계산할 매출원가도 없다.
                                grossProfit[j] = 1 
                            elif stocks_in == '008770':
                                grossProfit[j] = revenue[j] - int(df1.loc[self.condition_14].iloc[0]['thstrm_amount'])
                            else:
                                grossProfit[j] = int(df1.loc[self.condition_5].iloc[0]['thstrm_amount'])

 

 

2. 전체 코드

이제 이걸 반영한 전체코드는 아래와 같습니다. 참고하시길 바랍니다.

 

# 기업 funadmanetal 지표 조회 DB update
# 시가총액 상위 100개 종목 기준

import OpenDartReader
import pandas as pd
import time
from datetime import datetime
from pykrx import stock
import pymysql
import json

class DB_fundamental:
    def __init__(self): # DB_fundamental 의 생성자 내부에서 마리아디비에 연결.
        """생성자 : MariaDB 연결 """
        self.conn = pymysql.connect(host=' ', user=' ',
            password=' ', db=' ', charset='utf8')

        with self.conn.cursor() as curs:
            # fundamental - raw data DB
            sql = """
            CREATE TABLE IF NOT EXISTS fundamental_raw (
                code VARCHAR(20),
                date DATE,
                current_assets BIGINT(20),
                total_assets BIGINT(20),
                total_debt BIGINT(20),
                total_equity BIGINT(20),
                revenue BIGINT(20),
                gross_profit BIGINT(20),
                income BIGINT(20),
                net_income BIGINT(20),
                operating_cash_flow BIGINT(20),
                investment_cash_flow BIGINT(20),
                free_cash_flow BIGINT(20),
                market_cap BIGINT(20),
                PRIMARY KEY (code, date))
                """
            curs.execute(sql)
            # fundamental - investment data DB
            sql = """
            CREATE TABLE IF NOT EXISTS fundamental_investment (
                code VARCHAR(20),
                date DATE,
                per FLOAT(20),
                pbr FLOAT(20),
                psr FLOAT(20),
                gp_a FLOAT(20),
                por FLOAT(20),
                pcr FLOAT(20),
                pfcr FLOAT(20),
                ncav FLOAT(20),
                PRIMARY KEY (code, date))
                """
            curs.execute(sql)
        self.conn.commit()
        self.codes = dict()

        # 객체 생성 (API KEY 지정) 
        self.api_key = ' 4'
        self.dart = OpenDartReader(self.api_key)
        
        # 시가총액 큰 순서대로 나열
        df_raw = stock.get_market_cap_by_ticker("20220105", market="ALL", acending=False) 
        df_2 = df_raw.reset_index()
        df_ls = df_2['티커'].values.tolist()
        self.df_decended = []
        for i in range(0, 100):
            self.df_decended.append(df_ls[i])

        self.stock_kinds = len(self.df_decended)
        self.export_list = {}

        # initialize update infos
        # .json 파일을 읽기 모드로 연다
        try:
            with open('파일경로/update_infos.json', 'r') as in_file: 
                self.export_list = json.load(in_file)
        # 위에서 열려고 시도했던 .json 파일이 존재하지 않는 경우
        except FileNotFoundError: 
        	with open('파일경로/update_infos.json', 'w') as out_file:
                for i in range(0, self.stock_kinds):
                    self.export_list[self.df_decended[i]] = 0 
                    
                json.dump(self.export_list, out_file, indent=4)
        
    def update_fundamentals_raw(self):
        df2 = pd.DataFrame(columns=['asset_cur', 'asset_tot', 'debt_tot', 'equity_tot', 'rev', 'gp', 'inc',
                                    'net_inc', 'cfo', 'cfi', 'fcf', 'mk'], index=['1900-01-01']) 

        reprt_code = ['11013', '11012', '11014', '11011']
        nogp_list = ['035420', '035720', '036570', '017670', '251270', '263750', '030200', '293490', 
                    '112040', '259960', '032640', '180640'] # 매출총이익 계산 못하는 회사들
        for stocks in self.df_decended:
            stocks_in = stocks
            if self.export_list[stocks_in] == 1:
                pass
            else:
                for i in range(2020, 2022): # 데이터 취득 년도 구간. OpenDart는 2016년부터 정보를 제공한다.
                    print(stocks_in)
                    # 더미 리스트 초기화
                    current_assets = [0, 0, 0, 0] # 유동자산
                    liabilities = [0, 0, 0, 0] # 부채총계
                    equity = [0, 0, 0, 0] # 자본총계
                    total_assets = [0, 0, 0, 0] # 자산총계
                    revenue = [0, 0, 0, 0] # 매출액 
                    grossProfit = [0, 0, 0, 0] # 매출총이익
                    income = [0, 0, 0, 0] # 영업이익
                    net_income = [0, 0, 0, 0] # 당기순이익
                    cfo = [0, 0, 0, 0] # 영업활동현금흐름
                    cfi = [0, 0, 0, 0] # 투자활동현금흐름
                    fcf = [0, 0, 0, 0] # 잉여현금흐름 : 편의상 영업활동 - 투자활동 현금흐름으로 계산
                    market_cap = [0, 0, 0, 0] # 시가총액
                    date_year = str(i) # 년도 변수 지정
                    
                    for j, k in enumerate(reprt_code): # '11013'=1분기보고서, '11012' =반기보고서, '11014'=3분기보고서, '11011'=사업보고서 순서대로 루프를 태운다.
                        print(k)
                        df1 = pd.DataFrame() # Raw Data
                        if str(type(self.dart.finstate_all(stocks_in, i, reprt_code=k, fs_div='CFS'))) == "<class 'NoneType'>":
                            pass # 재무제표 데이터가 없거나 업데이트 한적이 있으면 지나간다 
                        else: # 타입이 NoneType 이 아니면 읽어온다.
    
                            df1 = df1.append(self.dart.finstate_all(stocks_in, i, reprt_code=k, fs_div='CFS')) 
                            self.condition_init(df1) 
                            current_assets[j] = int(df1.loc[self.condition].iloc[0]['thstrm_amount'])
                            liabilities[j] = int(df1.loc[self.condition_2].iloc[0]['thstrm_amount'])
                            equity[j] = int(df1.loc[self.condition_3].iloc[0]['thstrm_amount'])
                            if stocks_in == '003550': # LG의 경우, 매출이 쪼개져있으므로 매출원가 + 매출총이익을 더한다.
                                revenue[j] = int(df1.loc[self.condition_11].iloc[0]['thstrm_amount']) + \
                                            int(df1.loc[self.condition_5].iloc[0]['thstrm_amount'])
                            else:
                                revenue[j] = int(df1.loc[self.condition_4].iloc[0]['thstrm_amount'])

                            if stocks_in == '011810': # 매출총이익 항목이 없는 회사도 있다. 이 경우, 매출액 - 매출원가로 계산.
                                grossProfit[j] = revenue[j] - int(df1.loc[self.condition_11].iloc[0]['thstrm_amount'])
                            elif stocks_in in nogp_list: # 매출총이익도 없고 이를 계산할 매출원가도 없다.
                                grossProfit[j] = 1 
                            elif stocks_in == '008770':
                                grossProfit[j] = revenue[j] - int(df1.loc[self.condition_14].iloc[0]['thstrm_amount'])
                            else:
                                grossProfit[j] = int(df1.loc[self.condition_5].iloc[0]['thstrm_amount'])
                            
                            income[j] = int(df1.loc[self.condition_6].iloc[0]['thstrm_amount'])

                            if stocks_in == '008600':
                                net_income[j] = int(df1.loc[self.condition_12].iloc[0]['thstrm_amount']) - int(df1.loc[self.condition_13].iloc[0]['thstrm_amount'])
                            else:
                                net_income[j] = int(df1.loc[self.condition_7].iloc[0]['thstrm_amount'])
                            cfo[j] = int(df1.loc[self.condition_8].iloc[0]['thstrm_amount'])
                            cfi[j] = int(df1.loc[self.condition_9].iloc[0]['thstrm_amount'])
                            total_assets[j] = int(df1.loc[self.condition_10].iloc[0]['thstrm_amount'])

                            if k == '11013': # 1분기
                                date_month = '03'
                                date_day = 31 # 일만 계산할꺼니까 이것만 숫자로 지정

                            elif k == '11012': # 2분기. 현금흐름표는 2분기 - 1분기
                                date_month = '06'
                                date_day = 30
                                cfo[j] = cfo[j] - cfo[j-1]
                                cfi[j] = cfi[j] - cfi[j-1]
                            elif k == '11014': # 3분기
                                date_month = '09'
                                date_day = 30
                                cfo[j] = cfo[j] - (cfo[j-1] + cfo[j-2])
                                cfi[j] = cfi[j] - (cfi[j-1] + cfi[j-2])
                            else: # 4분기. 1 ~ 3분기 데이터를 더한다음 사업보고서에서 빼야 함
                                date_month = '12'
                                date_day = 30
                                revenue[j] = revenue[j] - (revenue[0] + revenue[1] + revenue[2])
                                grossProfit[j] = grossProfit[j] - (grossProfit[0] + grossProfit[1] + grossProfit[2])
                                income[j] = income[j] - (income[0] + income[1] + income[2])
                                net_income[j] = net_income[j] - (net_income[0] + net_income[1] + net_income[2])
                                cfo[j] = cfo[j] - (cfo[j-1] + cfo[j-2] + cfo[j-3])
                                cfi[j] = cfi[j] - (cfi[j-1] + cfi[j-2] + cfo[j-3])

                            fcf[j] = cfo[j] + cfi[j]
                            path_string = date_year + '-' + date_month + '-' + str(date_day)
                            # 날짜 계산을 위한 변수 정의
                            date = date_year + date_month + str(date_day)
                            date_formated = datetime.strptime(date,"%Y%m%d") # datetime format 으로 변환
                            
                            # 주말에 대한 처리
                            if date_formated.weekday() == 5:
                                if date_month == '12': 
                                    date_day -= 2 # 연말의 경우 2일을 뺀다. 
                                else:
                                    date_day -= 1 # 토요일일 경우 1일을 뺀다.
                            elif date_formated.weekday() == 6:
                                if date_month == '12': 
                                    date_day -= 3 # 연말의 경우 3일을 뺀다. 
                                else:
                                    date_day -= 2 # 일요일일 경우 2일을 뺀다.
                            elif date_formated.weekday() == 4 and date_month == '12':
                                date_day -= 1 # 연말인데 금요일이면 1일을 뺀다.

                            # 추석에 대한 처리
                            if date_month == '09' and date_year == '2020':
                                date_day -= 1
                            elif date_month == '09' and date_year == '2023':
                                date_day -= 3

                            date = date_year + date_month + str(date_day) # 뺀 날짜에 대해 재정의
                            date_2 = date_year + '-' + date_month + '-' + str(date_day) # 재정의된 날짜의 YYYY-MM-DD 형태
                            try:
                                df3 = stock.get_market_cap_by_date(date, date, str(stocks_in)) # 시가총액 데이터프레임 호출
                                market_cap[j] = df3.loc[date_2]['시가총액'] # 데이터 위치 찾기
                            except:
                                market_cap[j] = 0

                            # 데이터프레임에 저장하는 부분
                            df2.loc[path_string] = [current_assets[j], total_assets[j], liabilities[j], equity[j],
                                                revenue[j], grossProfit[j], income[j], net_income[j], cfo[j], cfi[j],
                                                fcf[j], market_cap[j]]             
                            df2.tail()
                    
                        time.sleep(1)
                df2.drop(['1900-01-01'], inplace=True) # 첫 행 drop 여기까지 하면 DB 에 저장할 수 있음.
                self.tp = 0
                self.commit_into_db(df2, stocks_in, self.tp) # 한 종목 DB 업데이트 완료
                # 투자지표로 바꾸는 코드 작성 필요
                if len(df2) >= 4: # 데이터프레임의 행이 4개 이상 있으면..
                    df_inv = self.convInvests(df2, path_string) # 투자지표 변환 함수 호출
                    self.tp = 1
                    self.commit_into_db(df_inv, stocks_in, self.tp) # 한 종목 DB 업데이트 완료
                else: # 그렇지 않으면 그냥 지나감
                    pass

                self.export_list[stocks_in] = 1
                with open('C:/Users/hs205/OneDrive/Projects_CODE/DB_Updater_simplified/src/update_infos.json', 'w') as out_file:
                    json.dump(self.export_list, out_file, indent=4)
                
                print(f"{stocks_in} updated")
                df2 = pd.DataFrame(columns=['asset_cur', 'asset_tot', 'debt_tot', 'equity_tot', 'rev', 'gp', 'inc',
                                        'net_inc', 'cfo', 'cfi', 'fcf', 'mk'], index=['1900-01-01']) 

    def commit_into_db(self, df, code, tp):
        """OpenDartReader 에서 읽어와 가공한 fundamental raw data를 DB에 REPLACE"""
        if tp == 0:
            with self.conn.cursor() as curs:
                for r in df.itertuples(): # 인수로 넘겨받은 데이터프레임을 튜플로 순회처리 한다.
                    sql = f"REPLACE INTO fundamental_raw VALUES ('{code}', '{r.Index}',"\
                        f"'{r.asset_cur}', '{r.asset_tot}', '{r.debt_tot}', '{r.equity_tot}', '{r.rev}', "\
                        f"'{r.gp}', '{r.inc}', '{r.net_inc}', '{r.cfo}', '{r.cfi}', '{r.fcf}','{r.mk}')"
                    curs.execute(sql) # REPLACE INTO 구문으로 daily_price 테이블을 업데이트 한다.
        else:
            with self.conn.cursor() as curs: 
                for r in df.itertuples(): # 인수로 넘겨받은 데이터프레임을 튜플로 순회처리 한다.
                    sql = f"REPLACE INTO fundamental_investment VALUES ('{code}', '{r.Index}',"\
                        f"'{r.PER}', '{r.PBR}', '{r.PSR}', '{r.GP_A}', '{r.POR}', "\
                        f"'{r.PCR}', '{r.PFCR}', '{r.NCAV}')"
                    curs.execute(sql) # REPLACE INTO 구문으로 daily_price 테이블을 업데이트 한다.

            self.conn.commit() # commit() 함수를 호출해 마리아디비에 반영한다.
    
    def condition_init(self, df):
        # 재무상태표 부분
        self.condition = (df.sj_nm == '재무상태표') & ((df.account_nm == '유동자산') | (df.account_nm == 'II. 유동자산') | \
                        (df.account_nm == 'Ⅰ.유동자산') | (df.account_nm == '유동자산 합계') | (df.account_nm == 'I.유동자산') | \
                        (df.account_nm == 'I. 유동자산') | (df.account_nm == 'l. 유동자산')) # 유동자산
        self.condition_2 = (df.sj_nm == '재무상태표') & ((df.account_nm == '부채총계') | (df.account_nm == '부  채  총  계') | \
                            (df.account_nm == '부채 총계')) # 부채총계
        self.condition_10 = (df.sj_nm == '재무상태표') & ((df.account_nm == '자산총계')| (df.account_nm == '자  산  총  계') | \
                            (df.account_nm == '자산 총계') | (df.account_nm == '자본과부채총계')) # 자산총계
        self.condition_3 = (df.sj_nm == '재무상태표') & \
                    ((df.account_nm == '자본총계') | (df.account_nm == '반기말자본') | (df.account_nm == '3분기말자본') | \
                    (df.account_nm == '분기말자본') | (df.account_nm == '1분기말자본') | (df.account_nm == '자  본  총  계') | \
                    (df.account_nm == '기말') | (df.account_nm == '자본 총계') | (df.account_nm == '기말자본 잔액') | \
                    (df.account_nm == '분기말') | (df.account_nm == '반기말') | (df.account_nm == '자 본 총 계') | \
                    (df.account_nm == '분기말 잔액') | (df.account_nm == '반기말 잔액'))  #자본총계
        # 손익계산서 부분
        self.condition_4 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서')) & ((df.account_nm == '매출액') | \
                            (df.account_nm == '수익(매출액)') | (df.account_nm == '매출') | (df.account_nm == '영업수익') | \
                            (df.account_nm == '수익') | (df.account_nm == '영업수익(매출액)') | (df.account_nm == 'Ⅰ.매출') | \
                            (df.account_nm == 'I.매출액') | (df.account_nm == 'Ⅰ.매출액') | (df.account_nm == 'I. 매출액') | \
                            (df.account_nm == '매출액(영업수익)'))
        self.condition_5 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서')) & ((df.account_nm == '매출총이익') | \
                            (df.account_nm == '매출 총이익') | (df.account_nm == '매출총이익(손실)') | (df.account_nm == 'Ⅲ.매출총이익')| \
                            (df.account_nm == 'III.매출총이익') | (df.account_nm == 'Ⅲ.매출총이익') | (df.account_nm == 'III. 매출총이익(손실)') | \
                            (df.account_nm == 'III. 매출총이익') | (df.account_nm == '매출총이익(영업수익)'))
        self.condition_6 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서')) & \
                      ((df.account_nm == '영업이익(손실)') | (df.account_nm == '영업이익') | \
                        (df.account_nm == '영업손실(이익)') | (df.account_nm == '영업손익')| \
                        (df.account_nm == '계속영업이익(손실)') | (df.account_nm == 'Ⅳ.영업이익') | \
                        (df.account_nm == 'VI.영업이익(손실)') | (df.account_nm == 'V.영업이익') | (df.account_nm == 'V. 영업이익(손실)') | \
                        (df.account_nm == 'IV. 영업이익') | (df.account_nm == '영업이익 (손실)') | (df.account_nm == '영업손실'))
        self.condition_7 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서') | (df.sj_nm == '현금흐름표')) & \
                        ((df.account_nm == '당기순이익(손실)') | (df.account_nm == '당기순이익') | \
                        (df.account_nm == '분기순이익') | (df.account_nm == '분기순이익(손실)') | (df.account_nm == '반기순이익') | \
                        (df.account_nm == '반기순이익(손실)') | (df.account_nm == '연결분기순이익') | (df.account_nm == '연결반기순이익') | \
                        (df.account_nm == '연결당기순이익')|(df.account_nm == '연결분기(당기)순이익')|(df.account_nm == '연결반기(당기)순이익') | \
                        (df.account_nm == '연결분기순이익(손실)') | (df.account_nm == '당기순손익') | (df.account_nm == 'Ⅶ.당기순이익') | \
                        (df.account_nm == 'VIII.당기순이익(손실)') | (df.account_nm == 'XIII. 당기순이익(손실)') | (df.account_nm == '반기연결순이익(손실)') | \
                        (df.account_nm == '연결당기순이익(손실)') | (df.account_nm == '당기의 순이익') | (df.account_nm == '분기기순이익(손실)') | \
                        (df.account_nm == '분기순손익') | (df.account_nm == '분기순손실') | (df.account_nm == '반기순손익') | (df.account_nm == '분기연결순손실') | \
                        (df.account_nm == '분기연결순이익(손실)'))

        # 현금흐름표 부분
        self.condition_8 = (df.sj_nm == '현금흐름표') & ((df.account_nm == '영업활동으로 인한 현금흐름') | (df.account_nm == '영업활동 현금흐름') | \
                            (df.account_nm == '영업활동현금흐름') | (df.account_nm == '영업활동으로인한 현금흐름') | (df.account_nm == '영업활동으로인한순현금흐름')| \
                            (df.account_nm == 'Ⅰ. 영업활동으로 인한 현금흐름') | (df.account_nm == 'I.영업활동현금흐름')|(df.account_nm == '영업활동으로인한현금흐름')|\
                            (df.account_nm == '영업활동으로 인한 순현금흐름') | (df.account_nm == '영업활동으로인한 순현금흐름') | (df.account_nm == '1.영업활동현금흐름')|\
                            (df.account_nm == 'Ⅰ.영업활동현금흐름') | (df.account_nm == '영업활동으로 인한 현금 흐름') | (df.account_nm == 'I. 영업활동현금흐름') | \
                            (df.account_nm == 'I. 영업활동으로 인한 현금흐름') | (df.account_nm == 'I.영업활동으로 인한 현금흐름') | \
                            (df.account_nm == '영업활동순현금흐름 합계') | (df.account_nm == 'Ⅰ. 영업활동현금흐름') | \
                            (df.account_nm == '영업으로부터 창출된 현금흐름') | (df.account_nm == 'Ⅰ. 영업활동으로 인한 순현금흐름'))
        self.condition_9 = (df.sj_nm == '현금흐름표') & ((df.account_nm == '투자활동으로 인한 현금흐름') | (df.account_nm == '투자활동 현금흐름') | \
                            (df.account_nm == '투자활동현금흐름') | (df.account_nm == '투자활동으로인한 현금흐름') | (df.account_nm == '투자활동으로인한순현금흐름')| \
                            (df.account_nm == 'Ⅱ. 투자활동으로 인한 현금흐름') | (df.account_nm == 'II.투자활동현금흐름') | (df.account_nm == '투자활동으로인한현금흐름')|\
                            (df.account_nm == '투자활동으로 인한 순현금흐름') | (df.account_nm == '투자활동으로인한 순현금흐름')|(df.account_nm == '2.투자활동현금흐름')|\
                            (df.account_nm == 'Ⅱ.투자활동현금흐름') | (df.account_nm == 'Ⅱ. 투자활동현금흐름') | (df.account_nm == 'II. 투자활동으로 인한 현금흐름') | \
                            (df.account_nm == 'II.투자활동으로 인한 현금흐름') | (df.account_nm == 'II. 투자활동현금흐름') | \
                            (df.account_nm == '투자활동순현금흐름 합계') | (df.account_nm == '투자활동으로부터의 순현금유입(유출)') | (df.account_nm == 'Ⅱ. 투자활동으로 인한 순현금흐름'))

        self.condition_11 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서')) & (df.account_nm == '매출원가')
        self.condition_12 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서')) & (df.account_nm == '법인세비용차감전순이익(손실)')
        self.condition_13 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서')) & (df.account_nm == '법인세비용(혜택)')
        self.condition_14 = ((df.sj_nm == '손익계산서') | (df.sj_nm == '포괄손익계산서')) & (df.account_nm == 'II.재료비') # 008770 매출총이익 계산하기 위한 것

    def convInvests(self, df, dates):
        # 초기화
        df_inv = pd.DataFrame(columns=['PER','PBR','PSR','GP_A','POR','PCR','PFCR','NCAV'], index=['1900-01-01'])
        df_index = df.reset_index()
        # print(df_index)
        # df 의 index 를 전부 얻어와서 하나씩 돌려야 함.
        for i in range(3, len(df)):
            per = df.iloc[i]['mk'] / (df.iloc[i-3]['net_inc'] + df.iloc[i-2]['net_inc'] + \
                                        df.iloc[i-1]['net_inc'] + df.iloc[i]['net_inc']) # 시가총액 / 순이익
            pbr = df.iloc[i]['mk'] / df.iloc[i]['debt_tot'] # 시가총액 / 부채총계
            psr = df.iloc[i]['mk'] / (df.iloc[i-3]['rev'] + df.iloc[i-2]['rev'] + \
                                        df.iloc[i-1]['rev'] + df.iloc[i]['rev']) # 시가총액 / 매출액
            gp_a = (df.iloc[i-3]['gp'] + df.iloc[i-2]['gp'] + df.iloc[i-1]['gp'] + \
                                    df.iloc[i]['gp']) / df.iloc[i]['equity_tot'] # 매출총이익 / 자산총계
            por = df.iloc[i]['mk'] / (df.iloc[i-3]['inc'] + df.iloc[i-2]['inc'] + \
                                        df.iloc[i-1]['inc'] + df.iloc[i]['inc']) # 시가총액  / 영업이익
            pcr = df.iloc[i]['mk'] / (df.iloc[i-3]['cfo'] + df.iloc[i-2]['cfo'] + \
                                        df.iloc[i-1]['cfo'] + df.iloc[i]['cfo']) # 시가총액 / 영업활동현금흐름
            pfcr = df.iloc[i]['mk'] / (df.iloc[i-3]['fcf'] + df.iloc[i-2]['fcf'] + \
                                        df.iloc[i-1]['fcf'] + df.iloc[i]['fcf'])# 시가총액 / 잉여현금흐름
            ncav = (df.iloc[i]['asset_cur'] - df.iloc[i]['debt_tot'])/ df.iloc[i]['mk']# 청산가치 (유동자산 - 부채총계) / 시가총액
        
            df_inv.loc[df_index.iloc[i]['index']] = [per, pbr, psr, gp_a, por, pcr, pfcr, ncav] # 데이터프레임에 저장하는 부분
            df.tail()
        df_inv.drop(['1900-01-01'], inplace=True) # 첫 행 drop 여기까지 하면 DB 에 저장할 수 있음.

        return df_inv

if __name__ == '__main__':
    dbf = DB_fundamental()
    dbf.update_fundamentals_raw()

 

 

3. 여담

 

pykrx 와 OpenDartReader 로 어느정도 편리하게 파이썬에서 종목 분류하는 작업을 할 수 있었네요.

 

해보면서 느낀 점은, (주로 아쉬운 점이죠)

1. 일부 종목은 데이터가 아예 없다. (주로 은행, 보험사) 정확하게는 Dart 에 재무공시는 되어있지만 OpenDart 에서 데이터 Return 이 안된다.

 

2. 일부 종목은 매출총이익이 없는 경우가 있다. 매출원가 항목을 찾을 수 없다보니 (사실 잘 찾아보면 있을지도 모른다) 매출총이익 계산하기도 힘들어서 GP/A 투자지표를 얻는데는 한계가 있었다.

 

2. 재무지표를 읽어오는 항목이 회사마다 미묘하게 다른 경우가 있어 재무지표 추출 조건식의 양이 커진다. 100개를 해도 저정도인데 약 2400개의 모든 회사를 다 한다면?

 

3. 시중에 나와있는 퀀트투자 프로그램은 비싸긴 해도 직접 구축할 여유가 없다면 써보는 것도 나쁘진 않겠다...?!

(국내에는 퀀트킹, 젠포트 등이 있습니다.)

 

어쩌다보니 결과가 퀀트투자는 사드세요.. 하는 느낌이 드네요. 만드는 것도 그 나름대로 가치가 있다곤 생각합니다.

많은 데이터를 내 손으로 직접 커스터마이징하고 섬세한 로직을 만들어 낼 수 있다는 점에서 말이죠.

 

이제 다음 포스팅에서 모아뒀던 이전 포스팅들을 정리하면서 이번 프로젝트는 마무리 짓도록 하겠습니다.

 

 

 

 

728x90
반응형