INVESTAR / StockAnalysisInPython

456 stars 412 forks source link

DBUpdater를 FinanceDataReader 데이터로 불러올 때 오류 문의 #60

Open cahn70 opened 3 years ago

cahn70 commented 3 years ago

일전에 한 번 문의 드린 적이 있는데, 네이버는 수정종가를 지원하지 않아서, 수정종가 데이터를 가져오기 위해, FinanceDataReader를 사용하기로 하고, 책의 DBUpdater 코드를 응용해서 일일시세를 FinanceDataReader에서 읽어오는 코드를 작성해 보았는데요... 실행을 해보면 에러 발생은 없는데, 몇 개 데이터를 불러와서 데이터베이스에 Replace 한 후에는 무작위로 멈춰서 버리네요... 중간 중간 프린트 문을 넣어 확인해 보니.... 아래의 라인에서 멈추는 것 까지는 확인했습니다. fdr.DataReader(code, start_date, end_date)

위의 메소드에 들어갈 종목코드와 시작일, 종료일 모두 제대로 들어오는 것도 확인을 했는데... 어느 순간 freeze가 되네요... 어떤 때는 10개 내외의 데이터를 읽어오고 멈추고, 어떤 때는 50~60개 까지 가져오고 멈추고는 합니다. 읽어 온 부분까지는 시세 데이터가 MariaDB에 제대로 저장이 된 것 까지도 확인은 했습니다. 이유를 찾을 수가 없어 문의를 드려 봅니다. 감사합니다.

아래는 작성한 코드입니다.

import matplotlib.pyplot as plt import FinanceDataReader as fdr import mplfinance as mpf import pandas as pd from bs4 import BeautifulSoup import pymysql, calendar, time, json import requests from datetime import datetime from threading import Timer

class DBUpdater:

def __init__(self):
    """생성자: MariaDB 연결 및 종목코드 딕셔너리 생성"""
    self.conn = pymysql.connect(host='localhost', user='root',
        password='******', db='fdr_data', charset='utf8')
    self.conn.commit()
    self.codes = dict()        #codes를 딕셔너리 형태로 정의

def execute_daily(self):
    """company_info와 daily_price 테이블 업데이트"""
    self.update_comp_info()

    start_date = '2011-01-01'   # 일일 주가를 불러올 시작일과 종료일을 지정
    end_date = '2021-03-26'  
    self.update_daily_price(start_date, end_date)

def update_comp_info(self):
    """종목코드를 company_info 테이블에 업데이트 한 후 딕셔너리에 저장"""
    sql = "SELECT * FROM company_info"
    df = pd.read_sql(sql, self.conn)
    for idx in range(len(df)):
        self.codes[df['code'].values[idx]] = df['company'].values[idx]
    with self.conn.cursor() as curs:
        today = datetime.today().strftime('%Y-%m-%d')
        krx = self.read_krx_code()
        for idx in range(len(krx)):
            code = krx.code.values[idx]
            company = krx.company.values[idx]             
            sql = f"REPLACE INTO company_info (code, company, last"\
                  f"_update) VALUES ('{code}', '{company}', '{today}')"
            curs.execute(sql)
            self.codes[code] = company
            tmnow = datetime.now().strftime('%Y-%m-%d %H:%M')
            print(f"[{tmnow}] #{idx+1:04d} REPLACE INTO company_info "\
                  f"VALUES ({code}, {company}, {today})")
        self.conn.commit()
        print('')           

def read_krx_code(self):
    """KRX로부터 상장기업 목록 파일을 읽어와서 데이터프레임으로 반환"""
    url = 'http://kind.krx.co.kr/corpgeneral/corpList.do?method='\
        'download&searchType=13'
    krx = pd.read_html(url, header=0)[0]
    krx = krx[['종목코드', '회사명']]
    krx = krx.rename(columns={'종목코드': 'code', '회사명': 'company'})
    krx.code = krx.code.map('{:06d}'.format)
    return krx

def update_daily_price(self, start_date, end_date):
    """KRX 상장법인의 주식 시세를 FDR로부터 읽어서 DB에 업데이트"""
    for idx, code in enumerate(self.codes):
        df = self.read_fdr(code, self.codes[code], start_date, end_date)
        if df is None:
            continue
        self.replace_into_db(df, idx, code, self.codes[code])

def read_fdr(self, code, company, start_date, end_date):
    """FinanceDataReader에서 주식 시세를 읽어서 데이터프레임으로 반환"""

    try:
        df = pd.DataFrame()
        print(code, company, start_date, end_date)
        df = df.append(fdr.DataReader(code, start_date, end_date))   # 여기서 항상 freeze가 되어 넘어가지를 않습니다.
        print('{}({}) is downloading |'.format(company, code), end="\r")
        print('====================================================')
        df = df.reset_index()      # FinanceDataReader로 읽어오면 Date값이 인덱스로 설정되므로, 인덱스를 칼럼으로 변경
        df = df.rename(columns={'Date':'date','Close':'close','Change':'change'
            ,'Open':'open','High':'high','Low':'low','Volume':'volume'})
        df = df.dropna()
        df[['close', 'open', 'high', 'low', 'volume']] = df[['close',
            'open', 'high', 'low', 'volume']].astype(int)
        df = df[['date', 'open', 'high', 'low', 'close', 'change', 'volume']]
    except Exception as e:
        print('Exception occured :', str(e))
        return None
    return df

def replace_into_db(self, df, num, code, company):
    """FinanceDataReader에서 읽어서 만든 주식 시세를 DB에 REPLACE"""
    with self.conn.cursor() as curs:
        for r in df.itertuples():
            sql = f"REPLACE INTO daily_price VALUES ('{code}','{r.date}', {r.open}, {r.high}, {r.low}, {r.close}, {r.change}, {r.volume})"
            curs.execute(sql)
        self.conn.commit()
        print('[{}] #{:04d} {}({}) : {} rows > REPLACE INTO daily_'\
              'price!'.format(datetime.now().strftime('%Y-%m-%d'\
              ' %H:%M'), num+1, company, code, len(df)))

def __del__(self):
    """소멸자: MariaDB 연결 해제"""
    self.conn.close()

if name == 'main': dbu = DBUpdater() dbu.execute_daily()

MariaDB의 테이블 구성은 아래와 같이 하였습니다.

company_info 테이블 daily_price 테이블

cahn70 commented 3 years ago

추가로 확인해본 내용 입니다.

가설 1 FinanceDataReader에서 반복해서 데이터를 불러올 때 문제가 생기는 것 같다. -> for문으로 아래 코드를 1000회 반복해서 돌려보았는데 문제없이 동작했습니다. fdr.DataReader('005930','2011-01-01','2021-03-26')

가설2 fdr.DataReader(code, start_date, end_date)로 code, start_date, end_date 인수값이 제대로 입력이 안되는 것 같다. -> 가설1에서 사용한 것과 동일한 코드로 변경해서, 변수에 값을 전달하지 않고, 아예 고정된 데이터 값을 넣어서 돌려보았습니다. 위의 단순 for 반복문에서는 문제없이 1000회 반복해서 돌아갔던 동일한 코드로 돌렸는데도 fdr.DataReader부분에서, 역시나 수십회 정도 돌다가 멈춰 버립니다. 도는 횟수도 일정하지 않고 랜덤 입니다.

즉 fdr.DataReader(code, start_date, end_date)) 부분을

fdr.DataReader('005930','2011-01-01','2021-03-26') 로 변경해서 테스트

INVESTAR commented 3 years ago

알려주신 내용을 봐서는 일정한 규칙은 없는 것 같지만 FinanceDataReader 데이터를 조회하다가 멈추는 현상이 존재하는 것 같습니다.

내부적으로 특정 시간 동안 호출할 수 있는 최대 횟수를 정해놓진 않았는지 FinanceDataReader 깃허브에 문의해 보시는 게 좋겠습니다.

cahn70 commented 3 years ago

제가 수정한 코드 자체는 별 이상이 없다는 말씀이신 거죠? 심플한 For문으로 돌려봤을 때는 1000회 돌려도 문제가 없었던 걸로 봐서는 FinanceDataReader만의 문제는 아닌 듯 싶기도 하고요... FinanceDataReader 깃허브에도 문의 글 올려놓고 기다리는 중인데, 해결되기를 바래 봐야 겠네요.^^;

INVESTAR commented 3 years ago

결괏값이 수시로 달라진다는 것은 코드에 뭔가 문제가 있을 가능성이 크다는 반증인데 그것이 cahn70님이 작성하신 코드 때문인지 FinanceDataReader 자체의 문제인지는 저도 판단하기 어렵습니다.