소스파일은 github.com/galaxywiz/StockCrawler_py 에서 확인 가능합니다.

 

sqliteDB.py

import pandas as pd 
import dataframe
import sqlite3
import datetime
import os

class SqliteDB:
    def __init__(self, dbName, tableName):
        self.tableStruct_ = ""
        self.columns_ = []
        self.initDB(dbName, tableName)

    def initDB(self, dbName, tableName):
        dir = "DB/"
        if os.path.exists(dir) == False:
            os.makedirs(dir)
        self.conn_ = sqlite3.connect(dir + dbName)
        self.dbName_ = dbName
        self.tableName_ = tableName

    def _tableName(self, code):
        code = code.replace("=","_")
        name = "%s_%s" % (self.tableName_, code)
        return name

    #----------------------------------------------------------#
    # 테이블이 있는지 확인하고, 있으면 -1, 없으면 0, 생성했으면 1
    def getTable(self, code):
        if self.checkTable(code) == False:
            if self.createTable(code) == False:
                return 0
            else:
                return 1
        return -1

    # 테이블 이름이 있는지 확인
    def checkTable(self, code):
        tableName = self._tableName(code)
        with self.conn_:
            cur = self.conn_.cursor()
            sql = "SELECT count(*) FROM sqlite_master WHERE Name = \'%s\'" % tableName
            cur.execute(sql)
            rows = cur.fetchall()
            for row in rows:          
                if str(row[0]) == "1":
                    return True
            return False
    
    
    # 테이블 생성
    def createTable(self, code):
        tableName = self._tableName(code)
        with self.conn_:
            try:
                cur = self.conn_.cursor()
                sql = "CREATE TABLE %s (%s);" % (tableName, self.tableStruct_)
                cur.execute(sql)
                return True
            except:
                log = "! [%s] table make fail" % tableName 
                print(log)
                return False
    
    # 데이터 저장
    def save(self, code, dataframe):    
        tableName = self._tableName(code)
        with self.conn_:
            try:
                cur = self.conn_.cursor()
                columns = ""
                value = ""
                for col in self.columns_:
                    if len(columns) == 0:
                        columns = col
                    else:
                        columns = "%s, '%s'" % (columns, col)

                    if len(value) == 0:
                        value = "?"
                    else:
                        value = "%s, ?" % (value)

                sql = "INSERT OR REPLACE INTO \'%s\' (%s) VALUES(%s)" % (tableName, columns, value)
                cur.executemany(sql, dataframe.values)    
                self.conn_.commit()
            except:
                return None
            
    # 데이터 로드
    def load(self, code, orderBy = "candleTime ASC"):
        tableName = self._tableName(code)
        with self.conn_:
            try:  
                columns = ""
                for col in self.columns_:
                    if len(columns) == 0:
                        columns = col
                    else:
                        columns = "%s, %s" % (columns, col)              

                sql = "SELECT %s FROM \'%s\' ORDER BY %s" % (columns, tableName, orderBy)
                df = pd.read_sql(sql, self.conn_, index_col=None)
                if len(df) == 0:
                    return False, None
            except:
                return False, None

        return True, df     

Sqlite 기본 기능을 정의한 클래스 입니다.

기본적으로 테이블 생성 / 체크 / 로드 / 저장을 합니다.

원래는 하나의 파일로 만들었으나, 미국 주식과 한국 주식의 데이터들이 한국 주식은 센트 / 전 같은 소수점 자리를 쓰지 않으니까 INT 정수형으로 처리가 가능했는데, 미국 주식은 소수점 문제로 FLOAT 소수점 처리 가능하도록 컬럼 설정하다 보니 분리 되었습니다.

처음에 보면 생성자로, DB파일을 지정합니다.

22_tableName 이란 함수가 있습니다.

이는 테이블 이름을 지정하는 매크로인데, 저는 [생성자에서 받은 기본 이름]_[코드] 형태로 가져 가려고 합니다. 즉 하나의 주식 종목 마다 하나의 테이블을 생성해서 가져가는 방식입니다.

 

38번째 줄 보면, checkTableAndMake 함수가 보이실 겁니다.
이건 새로운 주식종목이 생성되면, DB파일에 테이블이 있는지 체크해 보고 없으면 생성해주는 함수입니다.

42번 라인보시면 "SELECT count(*) FROM sqlite_master WHERE Name = \'%s\'" 같이 되어있는데, 이는 sqlite_master 라는 관리 테이블에서 사용자가 생성한 테이블 이름을 검색하는 명령어입니다. 만약 검색한 테이블 있다면, 46라인처럼 결과값을 받아서 True리턴하고, 아니면 False 리턴 합니다.

 

57 라인에서는 테이블 생성 명령어 "CREATE TABLE %s (%s);" % (self.tableName_, self.tableStruct_) 와 같이 테이블을 생성합니다. 보시면, tableName_tableStruct_ 를 받아와서 문장으로 만든 뒤 쿼리를 실행하는데, tableNamestruct 는 이 밑의 sqliteStockDB에서 조절 가능하도록 만들어주는 변수 입니다.

 

84 라인에서는 "INSERT OR REPLACE INTO \'%s\' (%s) VALUES(%s)" 와 같이 있는데, 이는 레코드(엑셀 시트로 생각하면 한 행 데이터)를 넣어 주는 명령어입니다. 만약 같은 값이 있으면 이를 새로 덮어쓰라는 의미로 INSERT OR REPLACE 명령어를 썼습니다.

이 문법은 MSSQL에서는 MERGE 라던가, MYSQL 에서는 REPLACE 라던가 다르기 때문에 혹시 다른 SQL제품을 사용한다면 적절히 수정하시는 게 좋습니다.

 

102라인에 로드 함수에 보면 "SELECT %s FROM \'%s\' ORDER BY %s" 와 같이 테이블 데이터를 가지고 오는 명령어가 있습니다.

뒤에 ORDER BY 하고 컬럼 이름을 입력하는데, 그 컬럼을 중심으로 정령해서 데이터를 달라는 뜻입니다. 기본은 오름차순이고, 내림차순 하려면 ORDER BY [컬럼명] DESC를 붙여줘야 합니다.

이제 sqlite에 접속해서 로딩 / 저장 등 최소한 기능을 구현 하였으니, 우리 주식 프로그램에 사용할 db 클래스를 생각해 봅시다.

 

웹에서 데이터를 가지고 오는 것이니 1일봉 데이터로 시가, 고가, 저가, 종가, 거래량만 저장하도록 합시다, 어차피 기술지표 값들은 저것들 가지고 그때 그때 계산할 수 있으니 굳이 다 저장할 필요 없습니다. (계산하는데 시간이 많이 걸리지도 않고요)

이를 구현해 보면 아래처럼 작성할 수 있습니다.

 

sqliteStockDB.py

import pandas as pd 
import dataframe
import sqlite3
from datetime import datetime
from datetime import timedelta
import time
import os

from stockData import StockData
from sqliteDB import SqliteDB

class DayPriceDB(SqliteDB):
    def initDB(self, dbName, tableName):
        super().initDB(dbName, tableName)

        self.tableStruct_ = "candleTime DATETIME PRIMARY KEY, start INT, high INT, low INT, close INT, vol INT"
        self.columns_ = ['candleTime', 'start', 'high', 'low', 'close', 'vol']

class DayPriceFloatDB(SqliteDB):
    def initDB(self, dbName, tableName):
        super().initDB(dbName, tableName)

        self.tableStruct_ = "candleTime DATETIME PRIMARY KEY, start Float, high Float, low Float, close Float, vol INT"
        self.columns_ = ['candleTime', 'start', 'high', 'low', 'close', 'vol']

 

보시면 tableStruct_.의 컬럼 타입의 start(시작가), high(고가), low(저가), close(종가) DayPriceDB INT, 정수만 입력 받고, DayPriceFloatDB 를 보시면 Float 소수점 입력을 받도록 커스터마이징을 해주었습니다.

'재테크 > 주식 알람 시스템 만들기' 카테고리의 다른 글

6. 메시지 알람 (텔레그램)  (0) 2020.11.08
5. 주식 차트 그리기  (0) 2020.11.08
4. 주식 데이터 저장 (sqlite)  (0) 2020.11.07
3. 주식 데이터  (0) 2020.11.07
2. 주식데이터 가지고 오기  (0) 2020.11.07

웹에서 데이터를 가지고 오다 보면 느끼실 텐데, 데이터를 가지고 오는 속도가 꽤 걸립니다.

이게, 코드상, 주식 데이터 가지고 와라 (stockCrawler.py 42, 91, 148) 하면 내부에서 웹페이지를 열었다 닫았다 하면서 데이터들 가지고 오기 때문에, 한 두종목이면 금방 하지만, 한국 주식 시장의 2천개 넘는 종목이나, 미국 주식 시장의 S&P, NASDAP 종목 3,4천여 개 가지고 오려면 시간이 꽤 걸립니다.

 

그래서 처음에 데이터를 가지고 올 때는 3년치 데이터를 가지고 오고, 이후에는 1주일(설날 / 추석 같은 휴일처럼 1주일동안 쉬는 주일을 생각해서) 데이터만 가지고 오도록 하면 데이터 요청양도 줄어들고, 갱신 시간도 빨라질 것 입니다.  

이를 구현하려면 처음 데이터는 가지고 왔을 때 이를 하드 디스크에 저장하고, 추후 필요(프로그램 재 실행 시) 이 데이터를 우선 로딩 하는 로직이 필요합니다.

 

데이터를 저장 로딩 방법에는 여러가지가 있습니다.

가장 간단한 건, 그냥 데이터를 메모장에서 쓰는 text 파일로 저장하는 방법입니다. (보통 이런 데이터는 확장자를 csv로 저장합니다)

주식 데이터 csv 로 저장할 경우

엑셀파일은 어떠냐라고 하실수도 있는데, 엑셀 파일 경우, ms office 라이브러리 읽고 그걸로 저장 / 로딩해야 하는것에 반해, csv파일 경우 그냥 텍스트 파일에 , 로 컬럼 구분된 값이라 보통 언어 배울 때 기본중에 기본 텍스트 파일 읽고/저장 하는 부분을 응용해서 바로 구현 할 수 있기 때문에 많이 사용하는 편입니다.

엑셀로 저장하는 경우

그런데 이렇게 저장하면 문제가 데이터를 일부분만 가지고 오고 싶을 때라던가 (작년 데이터 로만 시뮬레이션 해보고 싶다던가) 여러 조건에 제약이 발생합니다.

그래서 데이터를 다루는 언어 SQL에 대해서 간단히 집고 SQLite 를 사용해서 주식 데이터를 저장 / 로드 하는 방법에 대해서 기술 하도록 하겠습니다.

 

간단한 sql 문법에 대해서

데이터를 조회 / 입력 / 수정에 대해서 표준 언어가 있는데 이를 SQL문법이라고 합니다.

원래 아래와 같은 문법을 많이 사용하는데, SQL은 강제가 아니라, 제품에 따라 문법의 차이가 있습니다. (MSSQLTOP 이니 VARCHAR 같은 키워드가 있고, MYSQLLIMIT 란것도 있고 오라클도 몇몇 있습니다)

다만 공통중 공통만 기술하면 아래와 같고, 아래 예시는 Sqlite 를 기준으로 작성했습니다.

문법

설명

예시

CREATE

엑셀 시트 같이 데이터를 넣을 테이블을 생성합니다.

CREAET TABLE [테이블이름] ([컬럼 명] 타입)

타입은INT, FLOAT, TEXT등이 있고, 속성으로 PRIMARY KEY 라는 중복 없는 컬럼은 하나 지정을 해주는 게 중요합니다. (그래야 데이터를 집어서 찾기 편하거든요)

SELECT

엑셀 시트에서 특정 값을 검색하는 명령어입니다.

SELECT * FROM [테이블이름] WHERE 조건 ORDER BY [정렬 컬럼 명]

INSERT

하나의 행을 입력합니다.

INSERT INTO [테이블이름] (컬럼 명) VALUES(입력 컬럼 값들)

UPDATE

테이블 데이터를 수정합니다.

UPDATE [테이블이름] SET 컬럼 명 = 변수 WHERE 조건

DELETE

테이블 데이터를 삭제합니다.

DELETE FROM [테이블이름] WHERE 조건

DROP

테이블을 삭제합니다.

DROP TABLE [테이블이름]

이런 SQL문 연습은 그림과 같이 SQLite DB Browser SQL 실행으로 확인 가능합니다.

 

검색 조건이나 사용법에 대해 더 자세히 알고 싶으시면, 구글로 SELECT SQL 식으로 검색하시면 다양한 예문을 검색할 수 있습니다.

여기서는 간단한 쓰임 정도 기술 하였고 이에 대해서는 하기에 자세히 기술 하겠습니다.

 

 

'재테크 > 주식 알람 시스템 만들기' 카테고리의 다른 글

5. 주식 차트 그리기  (0) 2020.11.08
4-1 주식 데이터 저장 (sqlite)  (0) 2020.11.08
3. 주식 데이터  (0) 2020.11.07
2. 주식데이터 가지고 오기  (0) 2020.11.07
1. 프로그램 설계  (0) 2020.11.07

소스파일은 github.com/galaxywiz/StockCrawler_py 에서 확인 가능합니다.

 

이전 장에서 가지고 온 주식데이터를 담을 그릇을 만들어볼 차례입니다.

이전 장, 야후 파이낸셜이나, 네이버 금융 페이지에서 데이터를 가지고 오는데, 해당 페이지를 방문하면 오른쪽과 같은 페이지의 데이터를 가지고 온 것입니다.

이제 저 데이터들을 웹으로부터 가지고 왔으니, 이를 우리 프로그램에 맞게 사용할 수 있도록 만들어 봅니다.

저는 오른쪽과 같이 구성해보고자 합니다.

기본데이터는 웹에서 가지고 온 데이터(앞 글자 영어 따서 ohlc 데이터라 합니다), 이를 기반으로 계산한 보조 지표, 매수 정보 같은 건 추후 실제 매매 API를 붙이기 위해 만들어 두었습니다.

 

stockData.py

from enum import Enum
import os

import talib
import talib.abstract as ta
from talib import MA_Type
import dataframe

import pandas as pd 
import numpy as np

import util

class BuyState (Enum):
    STAY = 0
    BUY = 1
    SELL = 2

class StockData:
    def __init__(self, code, name, df):
        self.code_ = code
        self.name_ = name
        self.chartData_ = df
        
        self.buyCount_ = 0
        self.buyPrice_ = 0
        self.position_ = BuyState.STAY
        self.predicPrice_ = 0        # 머신러닝으로 예측한 다음날 주식값
        self.strategyAction_ = BuyState.STAY
        self.teleLog_ = ""
        self.marketCapRanking_ = 0

    def canPredic(self):
        size = len(self.chartData_)
        if size < 300:
            return False
        return True

    def calcPredicRate(self):
        if self.canPredic() == False:
            return 0
        
        nowPrice = self.candle0()
        rate = util.calcRate(nowPrice["close"], self.predicPrice_)
        return rate

    # 최신 캔들
    def candle0(self):
        rowCnt = self.chartData_.shape[0]
        if rowCnt == 0:
            return None
        return self.chartData_.iloc[-1]

    # 전날 캔들
    def candle1(self):
        rowCnt = self.chartData_.shape[0]
        if rowCnt <= 1:
            return None
        return self.chartData_.iloc[-2]

    # 
    def candle2(self):
        rowCnt = self.chartData_.shape[0]
        if rowCnt <= 2:
            return None
        return self.chartData_.iloc[-3]

    def calcProfit(self):
        if self.buyCount_ == 0:
            return 0
     
        profit = self.buyCount_ * self.buyPrice_
        return profit    

    # 각종 보조지표, 기술지표 계산
    def calcIndicator(self):        
        arrClose = np.asarray(self.chartData_["close"], dtype='f8')
        arrHigh = np.asarray(self.chartData_["high"], dtype='f8')
        arrLow = np.asarray(self.chartData_["low"], dtype='f8')
     
        # 이평선 계산
        self.chartData_["sma5"] = ta._ta_lib.SMA(arrClose, 5)
        self.chartData_["sma10"] = ta._ta_lib.SMA(arrClose, 10)
        self.chartData_["sma20"] = ta._ta_lib.SMA(arrClose, 20)
        self.chartData_["sma50"] = ta._ta_lib.SMA(arrClose, 50)
        self.chartData_["sma100"] = ta._ta_lib.SMA(arrClose, 100)
        self.chartData_["sma200"] = ta._ta_lib.SMA(arrClose, 200)

        self.chartData_["ema5"] = ta._ta_lib.EMA(arrClose, 5)
        self.chartData_["ema10"] = ta._ta_lib.EMA(arrClose, 10)
        self.chartData_["ema20"] = ta._ta_lib.EMA(arrClose, 20)
        self.chartData_["ema50"] = ta._ta_lib.EMA(arrClose, 50)
        self.chartData_["ema100"] = ta._ta_lib.EMA(arrClose, 100)
        self.chartData_["ema200"] = ta._ta_lib.EMA(arrClose, 200)

        self.chartData_["wma5"] = ta._ta_lib.WMA(arrClose, 5)
        self.chartData_["wma10"] = ta._ta_lib.WMA(arrClose, 10)
        self.chartData_["wma20"] = ta._ta_lib.WMA(arrClose, 20)
        self.chartData_["wma50"] = ta._ta_lib.WMA(arrClose, 50)
        self.chartData_["wma100"] = ta._ta_lib.WMA(arrClose, 100)
        self.chartData_["wma200"] = ta._ta_lib.WMA(arrClose, 200)

        macd, signal, osi = ta._ta_lib.MACD(arrClose, fastperiod=12, slowperiod=26, signalperiod=9)
        self.chartData_["MACD"] = macd
        self.chartData_["MACDSignal"] = signal
        self.chartData_["MACDOsi"] = osi
  
        #볼린저 계산
        upper, middle, low = ta._ta_lib.BBANDS(arrClose, 20, 2, 2, matype=MA_Type.SMA)
        self.chartData_["bbandUp"] = upper
        self.chartData_["bbandMid"] = middle
        self.chartData_["bbandLow"] = low

        # 기타 자주 사용되는 것들
        self.chartData_["rsi"] = ta._ta_lib.RSI(arrClose, 14)
        self.chartData_["cci"] = ta._ta_lib.CCI(arrHigh, arrLow, arrClose, 14)
        self.chartData_["williumR"] = ta._ta_lib.WILLR(arrHigh, arrLow, arrClose, 14)
        self.chartData_["parabol"] = ta._ta_lib.VAR(arrClose, 5, 1)
        self.chartData_["adx"]  = ta._ta_lib.ADX(arrHigh, arrLow, arrClose, 14)
        self.chartData_["plusDI"]  = ta._ta_lib.PLUS_DI(arrHigh, arrLow, arrClose, 14)
        self.chartData_["plusDM"]  = ta._ta_lib.PLUS_DM(arrHigh, arrLow, 14)
       
        self.chartData_["atr"] = ta._ta_lib.ATR(arrHigh, arrLow, arrClose, 30)
        

사실 이 프로그램의 핵심 기능 보조 지표 계산 calcIndicator 가 이 클래스의 핵심입니다.

기술지표는 시가, 고가, 저가, 종가를 특정 목적에 맞게 계산식에 의해 도출된 값들입니다.

보통 추세, 지금 주식 가격이 계산상 적정 범위인가, 과 매수 매도 구간인가 판단하는데 도움을 주는 툴이며 인터넷 웹이나, hts 에 아래와 같이 여러 지표들을 보여주고 있습니다.

calcIndicator 함수 보시면 이 기술지표 데이터를 TaLib라는 라이브러리를 사용하여 계산 결과값만 받아왔습니다.

물론 원하는 기술지표를 직접 계산하시어도 문제없습니다만, 그 경우 결과 값이 정확한지 엑셀에 수식으로 구현해서 그 값과 같은 값이 나오는지 체크해서 써야 합니다만, 프로그램이 복잡해지는 문제가 발생됩니다.

그리고 이런 공개된 라이브러리에 계산 값이 다르거나 버그 나면, 전세계 사람들이 달려들어 수정하기 때문에 굳이 안 믿고 안 쓸 이유가 없다고 생각합니다.

TaLib 에 대해서는 https://ta-lib.org/ 홈페이지에 자세한 설명이 나와있고, 자세하건 오른쪽 홈페이지의 function list를 확인해서 원하는 기술지표 함수를 체크해서 사용하시는 게 좋습니다.

소스파일은 github.com/galaxywiz/StockCrawler_py 에서 확인 가능합니다.

 

주식 트레이딩에 성공적인 사람들은 대부분 장기 투자자입니다.

워렌버핏도 10년이상 볼 것이 아니면 10분도 가지지 말라는 말을 한적이 있죠.

저도 처음에는 1, 1, 3, 5분봉 보면서 단타를 해봤지만, 결국 다 잃고,

1년 이상 홀딩 한 종목이 엄청난 수익을 안겨주면서 위 말을 믿고 있습니다.

 

굳이 이런 말을 하는 이유는 구태여 분봉 데이터를 얻어서 트레이딩 하는 건 해외선물이 아닌 이상의미가 없다는 걸 말씀드리고 싶었습니다.

 

우선 인터넷에 흔히 있는 데이터를 모으는 것부터 시작합시다.

보통 인터넷에선 네이버나 야후 파이낸셜 데이터를 가지고 오는데, 저는 야후 쪽을 사용합니다.

왜냐면, 미국 주식을 가지고 오기도 편하고, 주식 분할 등을 했을 경우,

주식 분할 전 데이터도 다 분할해서 데이터를 전달해 주기 때문에, 정확한 데이터를 얻어 오기 좋습니다.

Crawler 라고 수집 클래스를 아래와 같이 작성합니다.

 

stockCrawler.py

# 인터넷에서 주식 데이터를 가지고 옵니다.
# 한국주식, 미국주식
#  
import pandas as pd 
from pandas import Series, DataFrame
from pandas_datareader import data as web
import numpy as np
import dataframe
import urllib.request
from bs4 import BeautifulSoup
import os
import time
import yfinance as yf
 
from datetime import datetime
from datetime import timedelta
 
#----------------------------------------------------------#
# 주식 목록 갖고오기 (상위)
class StockCrawler:
    def __getNaverURLCode(self, code):    
        url = 'http://finance.naver.com/item/sise_day.nhn?code={code}'.format(code=code)
        print("요청 URL = {}".format(url))
        return url

    # 종목 이름을 입력하면 종목에 해당하는 코드를 불러오기
    def __getNaverStockURL(self, item_name, stockDf):
        code = stockDf.query("name=='{}'".format(item_name))['code'].to_string(index=False)
        url = self.__getNaverURLCode(code)
        return url

    # 네이버에서 주식 데이터를 긁어 온다.
    def getKoreaStockDataFromNaver(self, ticker, loadDays):
        # 일자 데이터를 담을 df라는 DataFrame 정의
        df = pd.DataFrame()
        try:
            url = self.__getNaverURLCode(ticker)
            loadDays = (loadDays / 10) + 1
            # 1페이지가 10일. 100페이지 = 1000일 데이터만 가져오기 
            for page in range(1, int(loadDays)):
                pageURL = '{url}&page={page}'.format(url=url, page=page)
                df = df.append(pd.read_html(pageURL, header=0)[0], ignore_index=True)
            
            # df.dropna()를 이용해 결측값 있는 행 제거 
            df = df.dropna()
            df.reset_index(inplace=True, drop=False)
            stockDf = pd.DataFrame(df, columns=['날짜', '시가', '고가', '저가', '종가', '거래량'])
            stockDf.rename(columns={'날짜': 'candleTime', '고가': 'high', '저가': 'low', '시가': 'start', '종가': 'close', '거래량' : 'vol'}, inplace = True)
            stockDf['candleTime'] = stockDf['candleTime'].str.replace(".", "-")
            
            print(stockDf)
            return stockDf
        except:
            return None

    #파일에서 로딩할떄 “종목:코드:순위” 형식으로 로딩
    def _loadFromFile(self, targetList):
        stockDf = DataFrame(columns = ("name", "code","ranking"))
        for text in targetList:
            tokens = text.split(':')
            row = DataFrame(data=[tokens], columns=["name", "code","ranking"])
            stockDf = stockDf.append(row)
            stockDf = stockDf.reset_index(drop=True)
        return stockDf
   
    #일부 종목만 지정해서 로딩하고 싶을 때
    def getStocksListFromFile(self, fileName):
        with open(fileName, "r", encoding="utf-8") as f:
            targetList = f.read().splitlines()
        return self._loadFromFile(targetList)
 
#----------------------------------------------------------#
### 아후에서 데이터 긁어오기.
class KoreaStockCrawler(StockCrawler):
    def __getTicker(self, ticker):
        # 코스피는 KS, 코스닥은 KQ
        t = ticker + ".KS"
        try:
            p = web.get_quote_yahoo(t)['marketCap']
            return t
        except:
            return ticker +".KQ"
 
    def getStockData(self, ticker, loadDays):
        rowTicker = ticker
 
        try:
            ticker = self.__getTicker(ticker)
            oldDate = datetime.now() - timedelta(days=loadDays)
            fromDate = oldDate.strftime("%Y-%m-%d")
            stockDf = web.DataReader(ticker, start = fromDate, data_source='yahoo')
            if len(stockDf) == 0:
                return self.getKoreaStockDataFromNaver(rowTicker, loadDays)
 
            stockDf.reset_index(inplace=True, drop=False)
            stockDf.rename(columns={'Date': 'candleTime', 'High': 'high', 'Low': 'low', 'Open': 'start', 'Close': 'close', 'Volume' : 'vol'}, inplace = True)
            stockDf['candleTime'] = stockDf['candleTime'].dt.strftime("%Y-%m-%d")
 
            stockDf = pd.DataFrame(stockDf, columns=['candleTime', 'start', 'high', 'low', 'close', 'vol'])
            print(stockDf)
            return stockDf
 
        except:
            return self.getKoreaStockDataFromNaver(rowTicker, loadDays)
   
    def getStocksList(self, limit):
        # 한국 주식 회사 등록 정보 가지고 오기
        stockDf = pd.read_html('http://kind.krx.co.kr/corpgeneral/corpList.do?method=download&searchType=13', header=0)[0]
        stockDf.종목코드 = stockDf.종목코드.map('{:06d}'.format)
        stockDf = stockDf[['회사명', '종목코드']] 
        stockDf = stockDf.rename(columns={'회사명': 'name', '종목코드': 'code'})
        
        # 시총 구하기
        marketCapList = []
        ranking = []
        dropIdx = []
        for idx, row in stockDf.iterrows():
            try:
                ticker = self.__getTicker(row['code'])
                p = web.get_quote_yahoo(ticker)['marketCap']
                marketCap = int(p.values[0])
                marketCapList.append(marketCap)
            except:
                dropIdx.append(idx)
                print("[%s][%s] 시총 갖고오기 에러" % (row['name'], row['code']))
        
        stockDf.drop(dropIdx, inplace = True)
 
        rank = 1
        for i in marketCapList:
            ranking.append(rank)
            rank += 1
 
        stockDf['MarketCap'] = marketCapList
        stockDf = stockDf.sort_values(by='MarketCap', ascending=False)
        stockDf['ranking'] = ranking
        if limit > 0:
            stockDf = stockDf[:limit]
        return stockDf
 
# 미국 주식 긁어오기
class USAStockCrawler(StockCrawler):
    def getStockData(self, ticker, loadDays):
        oldDate = datetime.now() - timedelta(days=loadDays)
        fromDate = oldDate.strftime("%Y-%m-%d")
        try:
            stockDf = web.DataReader(ticker, start = fromDate, data_source='yahoo')
            
            stockDf.reset_index(inplace=True, drop=False)
            stockDf.rename(columns={'Date': 'candleTime', 'High': 'high', 'Low': 'low', 'Open': 'start', 'Close': 'close', 'Volume' : 'vol'}, inplace = True)
            stockDf['candleTime'] = stockDf['candleTime'].dt.strftime('%Y-%m-%d')
 
            features =['candleTime', 'start', 'high', 'low', 'close', 'vol']
            stockDf = stockDf[features]
            print(stockDf)
            return stockDf
 
        except:
            print("[%s] Ticker load fail" % ticker)
            return None
    
    def getStocksList(self, limit):
        sp500 = pd.read_html("https://en.wikipedia.org/wiki/List_of_S%26P_500_companies")[0]
        sp500 = sp500[['Security', 'Symbol']] 
        sp500 = sp500.rename(columns={'Security': 'name', 'Symbol': 'code'})
        
        nasdaqDf = pd.read_html("https://en.wikipedia.org/wiki/NASDAQ-100#External_links")[3]
        nasdaqDf = nasdaqDf.rename(columns={'Company': 'name', 'Ticker': 'code'})
 
        stockDf = sp500.append(nasdaqDf)
        stockDf = stockDf.drop_duplicates(['code'], keep='last')
        # 시총 구하기
        marketCapList = []
        ranking = []
        dropIdx = []
        for idx, row in stockDf.iterrows():
            try:
                tickers = row['code']
                p = web.get_quote_yahoo(tickers)['marketCap']
                marketCap = int(p.values[0])
                marketCapList.append(marketCap)
            except:
                dropIdx.append(idx)
                marketCapList.append(0)
                print("[%s][%s] 시총 갖고오기 에러" % (row['name'], row['code']))
        
        rank = 1
        for i in marketCapList:
            ranking.append(rank)
            rank += 1
 
        stockDf['MarketCap'] = marketCapList
        stockDf = stockDf.sort_values(by='MarketCap', ascending=False)
        stockDf['ranking'] = ranking
 
        stockDf.drop(dropIdx, inplace = True)
        if limit > 0:
            stockDf = stockDf[:limit]
        
        print(stockDf)
        return stockDf 

각각의 클래스는 StockCrawler 라는 상위 공통 집합이 있고, 이를 상속받는 KoreaStockCrawler (74라인) USAStockCrawler (142라인) 클래스를 정의했습니다.

각 클래스들은 공통의 함수 getStockList getStockData를 갖고 있습니다. 또한 공통 기능으로 StockCrawler getStockListFromFile 함수가 있습니다.

이제 위 3함수는 아래와 같은 기능으로 작성되어 있습니다.

 

KoreaStockCrawler에 대한 기술을 읽어 보시고, USAStockCrawler 에서 어떻게 응용하고 어느 부분이 바뀌었는지 보시면 큰 도움이 되실 것 같습니다.

getStockList, 입력 파라메터 limit

108줄을 보시면 http://kind.krx.co.kr/corpgeneral/corpList.do?method=download&searchType=13 라는 페이지를 읽어 오는데 실제 이 링크를 웹브라우저에 입력해 보시면 한국 기업 공시 채널 홈페이지의 상장목록 파일을 다운 받을 수 있습니다.

이 글을 쓰고 있는 2020-10-20일 최근 상장한 빅히트 종목도 있네요.

111라인까지는 컬럼명을 프로그램 사용에 맞게 통일하는 작업 정도입니다.

이대로 return 해도 되겠지만, 여기선 시총을 구해서 시총 순으로 정렬하여 리턴 하고자 합니다.

시총을 구하는 이유는 아무리 그래프가 좋아도 시총이 작으면 작전 세력의 먹이가 될 수 있기 때문에 이런 불확실한 종목은 배제하기 위함입니다.

어느 정도 시총이 크면 작전 세력들이 주가를 흔들기 어렵기 떄문에 큰 시총에 투자하는 게 안전하다고 생각하고, 개인적으로 한국 주식은 시총 200위 정도면 볼만하고 그 이하는 프로그래밍 매매하기 위험하다고 생각합니다.

위에 limit 파라메터 받아오는데, 이는 시총 몇 위까지 잘라서 리턴 하기 위함입니다.

 

그럼 시총을 어떻게 구하는가?

이는 119라인부터 기술되어 있습니다.

get_quote_yahoo라고, 야후에서 시총 항목을 구하는 함수가 있는데,
이를 웹에서 가지고 와서 새로운 컬럼 열을 만들어 줍니다.

그리고 129라인에 1부터 차례대로 증가하는 배열을 만들고
,135 라인에서 sort로 시총 순으로 정렬한 뒤, 136라인처럼 이를 붙여 줍니다.

getStockListFromFile 입력 파라메터 filename

67라인에 파일로부터 주식 목록을 가지고 오는 함수를 정의했습니다.

웹에서 매일 가지고 오기 귀찮고, 내가 알고 있는 주식에 대해서만 (사실 이게 가장 정답인 것 같아요) 모니터닝 하고자 할 때 사용하는 함수입니다.

파라메터로 file이름을 넘겨주면,

그 파일을 열어서 종목:코드:순위 로 읽어 옵니다.

파일 기술은 오른쪽 그림과 같이 정의합니다

(개인적으로 많이 보고 있는 주식들입니다).

getStockData 파라메터 ticker, loadDays

주식의 일자, 시가, 고가, 저가, 종가, 거래량을 웹에서 얻어오는 함수입니다.

우선 88라인 보시면 내부 함수로 (밖에서 못쓰는 함수) codeticker 로 변환합니다.

이후 89라인으로 얼마만큼 데이터를 가지고 올지 startDate 를 구하고, 91 라인으로부터 yahoo에서 주식 데이터를 얻어 옵니다.

만약 얻어오는데 실패했다면, except: 문을 따라104라인에 네이버 웹에서 데이터를 얻어 오도록 하였습니다.

96 라인 부터는 컬럼명을 프로그램에 맞게 재조정하는 코드입니다.

'재테크 > 주식 알람 시스템 만들기' 카테고리의 다른 글

5. 주식 차트 그리기  (0) 2020.11.08
4-1 주식 데이터 저장 (sqlite)  (0) 2020.11.08
4. 주식 데이터 저장 (sqlite)  (0) 2020.11.07
3. 주식 데이터  (0) 2020.11.07
1. 프로그램 설계  (0) 2020.11.07

소스파일은 github.com/galaxywiz/StockCrawler_py 에서 확인 가능합니다.

 

직장일을 하면서 재테크로 주식으로 재산을 불리고 싶지만,

제대로 된 주식을 일일이 분석하기 어려우니 이런 일련의 작업 목록을 정의해서

일을 컴퓨터에게 시키게 하는게 목적이니 여기에 맞게 작성해 봅니다.

마인드맵으로 주식 매매 알람 시스템이니 아래와 같이 생각해 봤습니다.

이제 이걸 가지고 각각의py 파일을 만들어 주도록 하고 코드를 작성해 보도록 합시다.

작성하실 때는, 레고 테크닉 하 듯이, 각각 python 파일이 각각의 모듈 들입니다.

이들을 조합하여 사용하는 시스템 부분이 완성되어야,

프로그램이 톱니바퀴에 맞물려 제대로 도는 모습을 볼 수 있으니 너무 조급하지 마시고 천천히 따라해 보시기 바랍니다.

'재테크 > 주식 알람 시스템 만들기' 카테고리의 다른 글

5. 주식 차트 그리기  (0) 2020.11.08
4-1 주식 데이터 저장 (sqlite)  (0) 2020.11.08
4. 주식 데이터 저장 (sqlite)  (0) 2020.11.07
3. 주식 데이터  (0) 2020.11.07
2. 주식데이터 가지고 오기  (0) 2020.11.07

+ Recent posts