INVESTAR / StockAnalysisInPython

452 stars 411 forks source link

DBUpdater.py실행시 오류를 해결하는데 어려움이 있어요. 도와주세요~ #170

Open hawkeye213 opened 1 year ago

hawkeye213 commented 1 year ago

파이썬 증권데이터분석 책이 아주 잘 설명이 되어 있어 혼자 공부하기에 훌륭한 교재라고 생각되네요. 5장. 일별시세조회하는 부분 등에서 에러가 발생하기 시작하네요. 어떻게 해결해야 할지 며칠째 고민하다가 질문드립니다.

실행시 에러화면입니다.

UserWarning: pandas only support SQLAlchemy connectable(engine/connection) ordatabase string URI or sqlite3 DBAPI2 connectionother DBAPI2 objects are not tested, please consider using SQLAlchemy warnings.warn( C:\Anaconda3\lib\site-packages\pandas\io\sql.py:761: UserWarning: pandas only support SQLAlchemy connectable(engine/connection) ordatabase string URI or sqlite3 DBAPI2 connectionother DBAPI2 objects are not tested, please consider using SQLAlchemy warnings.warn(

DBUpdater.py 입력한 내용은

mport pandas as pd from bs4 import BeautifulSoup import urllib, pymysql, calendar, time, json import requests from urllib.request import urlopen from datetime import datetime from threading import Timer import pymysql

class DBUpdater: def init(self): """생성자: MariaDB 연결 및 종목코드 딕셔너리 생성""" self.conn = pymysql.connect(host='localhost', user='root', password=***', db='INVESTAR', charset='utf8')

    with self.conn.cursor() as curs:
        sql = """
       CREATE TABLE IF NOT EXISTS company_info (
            code VARCHAR(20),
            company VARCHAR(40),
            last_update DATE,
            PRIMARY KEY (code)
        );

        """
        curs.execute(sql)
        sql = """
        CREATE TABLE IF NOT EXISTS daily_price (
            code VARCHAR(20),
            date DATE,
            open BIGINT(20),
            high BIGINT(20),
            low BIGINT(20),
            close BIGINT(20),
            diff BIGINT(20),
            volume BIGINT(20),
            PRIMARY KEY (code, date)
        );

        """
        curs.execute(sql)

    self.conn.commit()

    self.codes = dict()
    self.update_comp_info() 

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

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

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:
        sql = "SELECT max(last_update) FROM company_info"
        curs.execute(sql)
        rs = curs.fetchone()  
        today = datetime.today().strftime('%Y-%m-%d')
        if rs[0] == None or rs[0].strftime('%Y-%m-%d') < today:  
            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_naver(self, code, company, pages_to_fetch):
    """네이버에서 주식 시세를 읽어서 데이터프레임으로 반환"""
    try:
        url = f"http://finance.naver.com/item/sise_day.nhn?code={code}"
        html = BeautifulSoup(requests.get(url,
            headers={'User-agent': 'Mozilla/5.0'}).text, "lxml")
        pgrr = html.find("td", class_="pgRR")
        if pgrr is None:
            return None
        s = str(pgrr.a["href"]).split('=')
        lastpage = s[-1]
        df = pd.DataFrame()
        pages = min(int(lastpage), pages_to_fetch)
        for page in range(1, pages + 1):
            pg_url = '{}&page={}'.format(url, page)
            df = df.append(pd.read_html(requests.get(pg_url,
                headers={'User-agent': 'Mozilla/5.0'}).text)[0])
            tmnow = datetime.now().strftime('%Y-%m-%d %H:%M')
            print('[{}] {} ({}) : {:04d}/{:04d} pages are downloading...'.
                format(tmnow, company, code, page, pages), end="\r")
        df = df.rename(columns={'날짜':'date','종가':'close','전일비':'diff'
            ,'시가':'open','고가':'high','저가':'low','거래량':'volume'})
        df['date'] = df['date'].replace('.', '-')
        df = df.dropna()
        df[['close', 'diff', 'open', 'high', 'low', 'volume']] = df[['close',
            'diff', 'open', 'high', 'low', 'volume']].astype(int)
        df = df[['date', 'open', 'high', 'low', 'close', 'diff', 'volume']]
    except Exception as e:
        print('Exception occured :', str(e))
        return None
    return df

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

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

def execute_daily(self):
    """실행 즉시 및 매일 오후 다섯시에 daily_price 테이블 업데이트"""
    self.update_comp_info()

    try:
        with open('config.json', 'r') as in_file:
            config = json.load(in_file)
            pages_to_fetch = config['pages_to_fetch']
    except FileNotFoundError:
        with open('config.json', 'w') as out_file:
            pages_to_fetch = 100
            config = {'pages_to_fetch': 1}
            json.dump(config, out_file)
    self.update_daily_price(pages_to_fetch)

    tmnow = datetime.now()
    lastday = calendar.monthrange(tmnow.year, tmnow.month)[1]
    if tmnow.month == 12 and tmnow.day == lastday:
        tmnext = tmnow.replace(year=tmnow.year+1, month=1, day=1,
            hour=17, minute=0, second=0)
    elif tmnow.day == lastday:
        tmnext = tmnow.replace(month=tmnow.month+1, day=1, hour=17,
            minute=0, second=0)
    else:
        tmnext = tmnow.replace(day=tmnow.day+1, hour=17, minute=0,
            second=0)
    tmdiff = tmnext - tmnow
    secs = tmdiff.seconds
    t = Timer(secs, self.execute_daily)
    print("Waiting for next update ({}) ... ".format(tmnext.strftime
        ('%Y-%m-%d %H:%M')))
    t.start()

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

확인해본 점검한 내용으로는

  1. pip install pymysql 등 필요한 모듈도 다 설치된 상태입니다.
  2. 마리아 디비를 설치한 상태이고 ch05_03_SelectVersion.py 점검시 아래와 같이 정상 시행됩니다. C:\Anaconda3\python.exe C:/Users/user/PycharmProjects/PyStudy/ch05_03_SelectVersion.py MariaDB version : ('10.10.2-MariaDB',)

Process finished with exit code 0

  1. INVESTAR 폴더내 Analyzer.py, MarketDB.py, DBUpdater.py 위치하고 있으며,

    Analyzer.py 내용은

import pandas as pd import pymysql from datetime import datetime from datetime import timedelta import re

class MarketDB: def init(self): """생성자: MariaDB 연결 및 종목코드 딕셔너리 생성""" self.conn = pymysql.connect(host='localhost', user='root', password=****', db='INVESTAR', charset='utf8') self.codes = {} self.get_comp_info()

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

def get_comp_info(self):
    """company_info 테이블에서 읽어와서 codes에 저장"""
    sql = "SELECT * FROM company_info"
    krx = pd.read_sql(sql, self.conn)
    for idx in range(len(krx)):
        self.codes[krx['code'].values[idx]] = krx['company'].values[idx]

def get_daily_price(self, code, start_date=None, end_date=None):
    """KRX 종목의 일별 시세를 데이터프레임 형태로 반환
        - code       : KRX 종목코드('005930') 또는 상장기업명('삼성전자')
        - start_date : 조회 시작일('2020-01-01'), 미입력 시 1년 전 오늘
        - end_date   : 조회 종료일('2020-12-31'), 미입력 시 오늘 날짜
    """
    if start_date is None:
        one_year_ago = datetime.today() - timedelta(days=365)
        start_date = one_year_ago.strftime('%Y-%m-%d')
        print("start_date is initialized to '{}'".format(start_date))
    else:
        start_lst = re.split('\D+', start_date)
        if start_lst[0] == '':
            start_lst = start_lst[1:]
        start_year = int(start_lst[0])
        start_month = int(start_lst[1])
        start_day = int(start_lst[2])
        if start_year < 1900 or start_year > 2200:
            print(f"ValueError: start_year({start_year:d}) is wrong.")
            return
        if start_month < 1 or start_month > 12:
            print(f"ValueError: start_month({start_month:d}) is wrong.")
            return
        if start_day < 1 or start_day > 31:
            print(f"ValueError: start_day({start_day:d}) is wrong.")
            return
        start_date = f"{start_year:04d}-{start_month:02d}-{start_day:02d}"

    if end_date is None:
        end_date = datetime.today().strftime('%Y-%m-%d')
        print("end_date is initialized to '{}'".format(end_date))
    else:
        end_lst = re.split('\D+', end_date)
        if end_lst[0] == '':
            end_lst = end_lst[1:]
        end_year = int(end_lst[0])
        end_month = int(end_lst[1])
        end_day = int(end_lst[2])
        if end_year < 1800 or end_year > 2200:
            print(f"ValueError: end_year({end_year:d}) is wrong.")
            return
        if end_month < 1 or end_month > 12:
            print(f"ValueError: end_month({end_month:d}) is wrong.")
            return
        if end_day < 1 or end_day > 31:
            print(f"ValueError: end_day({end_day:d}) is wrong.")
            return
        end_date = f"{end_year:04d}-{end_month:02d}-{end_day:02d}"

    codes_keys = list(self.codes.keys())
    codes_values = list(self.codes.values())

    if code in codes_keys:
        pass
    elif code in codes_values:
        idx = codes_values.index(code)
        code = codes_keys[idx]
    else:
        print(f"ValueError: Code({code}) doesn't exist.")
    sql = f"SELECT * FROM daily_price WHERE code = '{code}'" \
          f" and date >= '{start_date}' and date <= '{end_date}'"
    df = pd.read_sql(sql, self.conn)
    df.index = df['date']
    return df

MarketDB.py 내용은

import pandas as pd import pymysql from datetime import datetime import re

class MarketDB: def init(self): """생성자: MariaDB 연결 및 종목코드 딕셔너리 생성""" self.conn = pymysql.connect(host='localhost', user='root', password='*****', db='INVESTAR', charset='utf8') self.codes = dict() self.getCompanyInfo()

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

def get_comp_info(self):
    """company_info 테이블에서 읽어와서 companyData와 codes에 저장"""
    sql = "SELECT * FROM company_info"
    krx = pd.read_sql(sql, self.conn)
    for idx in range(len(krx)):
        self.codes[krx['code'].values[idx]] = krx['company'].values[idx]

def get_DailyPrice(self, code, start_Date=None, end_Date=None):
    """daily_price 테이블에서 읽어와서 데이터프레임으로 반환"""
    if start_date is None:
        one_year_ago = datetime.today() - timedelta(days=365)
        start_date = one_year_ago.strftime('%Y-%m-%d')
        print("start_date is initialized to '{}'".format(start_date))

    sql = "SELECT * FROM daily_price WHERE code = '{}'and date >='{}' and date <= '{}'".format(code, start_Date,end_date)
    df = pd.read_sql(sql, self.conn)
    df.index = df['date']
    return df
INVESTAR commented 1 year ago

경고 메시지만 봐서는 'pandas는 SQLAlchemy connectable이나 SQLite DBAPI2 connection만 지원한다'고 하는데 Anaconda3 환경에 설치된 pandas와 DB 연결 라이브러리 간에 뭔가 안 맞는 부분이 있는 것 같습니다.

https://stackoverflow.com/questions/71082494/getting-a-warning-when-using-a-pyodbc-connection-object-with-pandas https://lovelydiary.tistory.com/324

위 링크들을 참고해 보시고, 제 개인적인 의견으로는 Anaconda3를 삭제하시고 책에서 설명한 Python, pandas, pymysql 버전으로 다시 설치하는 것을 추천해 드립니다.

hawkeye213 commented 1 year ago

답변 감사합니다. 추가로 한가지만 더 확인 부탁드립니다. 제일 마지막에 있는 탑재되어 있는 MarketDB.py 내용작성이 올바르게 되어 있는지 궁금합니다. 책에서는 해당 전체소스코드 안내가 별도로 없어 여러각도로 작성해 보고 테스트 했습니다. Git-Hub에 탑재되어 있는 내용으로 대체하면 되는 것인지 궁금합니다.

INVESTAR commented 1 year ago

파이썬에서 대소문자가 다르면 다른 변수로 처리됩니다.

start_Date나 start_date 중 하나로 통일해서 사용하시고, end_Date와 end_date도 둘 중 하나로 통일해서 사용하셔야 할 것 같습니다.

그리고, if start_date is None: 경우에 대한 코드는 반드시 필요한 것은 아니니 GitHub에 올려진 소스 코드로 대체하시면 될 것 같습니다.

hawkeye213 commented 1 year ago

답변 감사드립니다, 훌륭한 책과 친절한 답변이 많은 도움이 되네요.

hawkeye213 commented 1 year ago

책으로 공부하면서 궁금한 점이 많습니다. 파이썬을 접하고 코딩에 대해 관심이 많아졌습니다. 막상 본격적으로 공부하려고 하니 여러방면으로 시도해보고 싶은게 많아졌습니다. 저자님의 도움이 꼭 필요한 상황입니다. 방향성을 잡고서 공부를 계속하고 싶습니다.

파이썬과 주식에 대해 일대일로 물어볼 분을 찾는게 쉽지는 않은 상황이라 용기내어 저자님께 몇가지 질문을 드립니다. 바쁘신 일정이 계시겠지만 관심을 가져주시고 많이 지도해주시면 감사하겠습니다.

1. 데이타베이스(데이타프레임) 자료 활용한 조건 검색 방법 질의 6장_12_TripleScreen.py 구문관련 질문드립니다. Investar 폴더내에 Analyzer.py, DBUpdater.py, MarketDB.py 작성해둔 상태입니다. 현재 일일 단위로 DBUpdater.py 정상작동하여 종목정보 및 가격정보가 최신화 되고 있습니다. 활용하고 있는 데이타베이스(데이타프레임) 응용활용하는 방법에 관해 질문입니다. p MariaDB에서 저장되어 있는 데이타프레임을 활용해 원하는 조건검색(macd 값 상호 비교)을 이용해서 조건을 만족하는 종목을 검색하고자 합니다.

import pandas as pd import matplotlib.pyplot as plt import datetime import mplfinance as mfp

from mplfinance import candlestick_ohlc

from mplfinance.original_flavor import candlestick_ohlc import matplotlib.dates as mdates from Investar import Analyzer

mk = Analyzer.MarketDB() df = mk.get_daily_price('엔씨소프트', '2017-01-01')

ema12 = df.close.ewm(span=12).mean() ema26 = df.close.ewm(span=26).mean() macd6 = ema12 - ema26 signal6 = macd6.ewm(span=9).mean() macdhist6 = macd6 - signal6 df = df.assign(ema12=ema12, ema26=ema26, macd6=macd6, signal6=signal6, macdhist6=macdhist6).dropna()

ema60 = df.close.ewm(span=60).mean() ema130 = df.close.ewm(span=130).mean() macd32 = ema60 - ema130 signal32 = macd32.ewm(span=45).mean() macdhist32 = macd32 - signal32 df = df.assign(ema130=ema130, ema60=ema60, macd32=macd32, signal32=signal32, macdhist32=macdhist32).dropna()

if df.macd6.values[-1] > df.macd32.values[-1]: print("상승전환2단계")

else: print()

plt.figure(figsize=(12, 12)) p1 = plt.subplot(3, 1, 1) plt.title('Triple Screen Trading ') plt.grid(True) candlestick_ohlc(p1, ohlc.values, width=.6, colorup='red', colordown='blue') p1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) plt.plot(df.number, df['ema130'], color='c', label='EMA130') plt.legend(loc='best')

p2 = plt.subplot(3, 1, 2) plt.title('MACD strategy1 ') plt.grid(True) p2.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))

plt.plot(df.number, df['macd6'], color='cornflowerblue', linewidth=0.5) plt.plot(df.number, df['macd32'], linestyle='dotted', color='blue', linewidth=0.5)

plt.legend(loc='best') plt.show()

위 구문이 단일 종목에 대해 원하는 지표(MACD1값, MACD2 값) 관련 조건 검색을 할 수 있는 상태입니다. 제가 구현하고 싶은 파이션 구문은 1) MariaDB(데이타베이스)에 저장된 전체 종목 가운데 조건검색을 만족하는 종목을 보여주는 파이썬 구문을 작성하려고 합니다. 가능하시다면 파이썬 구문 샘플로 부탁드립니다.

df = mk.get_daily_price('엔씨소프트', '2017-01-01') 구문을 어떻게 수정하면 좋을까요?

전체 DB 자료에서 불러오려면 변수를 어떻게 설정해야하고 종목 순서별로 원하는 조건검색식을 대입해서 조건을 만족하는 종목을 출력해 주는 방법을 우선 공부하고 그 이후에 조건을 충족하는 종목을 자동매수하는 방법을 공부하고자 합니다.

2) 그 다음 단계는 관심종목(150여개. 개별주식선물종목) 을 별도로 관리해서 그 종목들 가운데에서 조건을 충족하는 파이썬 구문을 작성하려고 합니다.

제가 공부하고자 하는 방향성이 잘못되었다면 말씀해 주시면 감사하겠습니다.

2.추가적으로 확인하고 싶은 부분은 틱자료를 데이타베이스로 관리할 수 있는 방법이 알고 싶습니다. 1) 대신크레온 이용해서 틱자료 DB를 만들 수 있는지? 아니면 대신 크레온 API 이용하지 않고 MariaDB 처럼 외부에서 틱자료를 DB 를 만들 수 있나요? 2) 개별주식선물 거래를 위해 집중해서 공부하다보니 틱자료 활용이 꼭 필요한 상태입니다. 도와 주시면 감사의 뜻을 꼭 전하고 싶습니다.

if df.macd78.values[-1] > df.macd162.values[-1]: print("상승전환2단계") else: print()

p2 = plt.subplot(3, 1, 2) plt.title('MACD strategy1 ') plt.grid(True) p2.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))

plt.plot(df.number, df['macd13'], color='cornflowerblue', linewidth=0.5) plt.plot(df.number, df['macd15'], linestyle='dotted', color='blue', linewidth=0.5) plt.plot(df.number, df['macd18'], linestyle='dotted', color='magenta', linewidth=0.5) plt.plot(df.number, df['macd20'], color='tab:brown', label='MACD3(20)', linewidth=0.5) plt.plot(df.number, df['macd26'], color='slategrey', label='MACD4(26)', linewidth=0.5)

위 구문은 개별종목(엔씨소프트)에 대해 조건 검색과 plot 함수를 이용해서 그리기 가능한 예제를 작성해 본 것입니다.

제가 구현하고 싶은 내용을 파이썬 구문을 작성하는데 자료 수집해서 기본 적인 예시구문을 아래와 같이 정리해 보았습니다. ta-lib 이용한 구문을 작성하는 것도 배우고 싶은 상태입니다. 이렇게 작성하는 방향이 맞더라도 제가 파이썬 초보단계라 응용하는 데 도움이 필요한 상태입니다.

import pandas as pd

import numpy as np import talib

def get_daily_price(ticker):

Retrieve the daily price data of a stock using its ticker symbol

df = mk.get_daily_price(ticker)
return df

def calculate_macd(df):

Calculate the MACD values for a stock using the daily price data

close = df['Close']
macd1, macd_signal1, macd_hist1 = talib.MACD(close, fastperiod=60,

slowperiod=130, signalperiod=9) macd2, macd_signal2, macd_hist2 = talib.MACD(close, fastperiod=240, slowperiod=520, signalperiod=9) df['MACD1'] = macd1 df['MACD2'] = macd2 return df

def compare_macd(df):

Compare the MACD1 and MACD2 values and output "Rising 1 stage"

if MACD1 > MACD2 for index, row in df.iterrows(): if row['MACD1'] > row['MACD2']: print(row['Ticker'], "Rising 1 stage")

Example usage:

tickers = mk.get_tickers() # get the list of tickers for all stocks in the Korean stock market for ticker in tickers: df = get_daily_price(ticker) df = calculate_macd(df) compare_macd(df)

위 구문은 제가 이곳저곳 검색하면서 초안상태로 만들어 놓은 것이라 참고만 해주세요~