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

 

몇몇 쓸모 있는 함수에 대해서 기술하는 장입니다.

간단하게, % 올랐는지 계산해 보는 calcRate, if a < x <b 와 같은 게 프로그래밍 상에서는 구현이 안되니 이를 직관적으로 표현해주는 checkRange, isRange 정도가 있습니다.

checkRange 같은 경우, 저는 0 < x < 100 이런 게 직관적이라 생각해서 변수도 저렇게 배치 했는데, 최근 프로그래밍 언어 (C# 이나 C++17 이후) clamp 라는 함수가 이거 랑 같은 역할을 한다고 보시면 됩니다.

대신 저 clamp 라는 건 clamp(x, low, high) 식으로 사용하는데, 이게 보기 편하면, 파라메터 기술 순서만 바꿔 주시면 됩니다.

 

util.py

#----------------------------------------------------------#
# 유틸 함수들

def calcRate(origin, now):
    return (now - origin) / origin
    
def checkRange(start, now, end):
    return min(end, max(now, start))
    
def isRange(start, now, end):
    if now == checkRange(start, now, end):
        return True
    return False

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

8-1. 골든 크로스 전략  (0) 2020.11.08
8. 매매 전략에 대해서  (0) 2020.11.08
7. 유틸 함수들  (0) 2020.11.08
6. 메시지 알람 (텔레그램)  (0) 2020.11.08
5. 주식 차트 그리기  (0) 2020.11.08
4-1 주식 데이터 저장 (sqlite)  (0) 2020.11.08

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

 

시스템을 만들고 나면 그게 내가 판단할 수 있도록 알람을 주는 게 필요합니다.
아무리 좋은 시스템 만들어도 알람이 안 오면 무용지물이니까요.

가장 좋고 간단한 거는 email로 보내는 방법입니다. 회사에서 자주 보실 만한 그 거죠.
하지만 시대도 변했고, email보다 세련되게 오는 게 있는데, 그건 카카오나 라인같이 메신저로 보내는 방법입니다.

하지만, 카카오나 라인은 메시지 보내려면 뭐 등록하고 이것저것 준비할 게 많아서 간단하면서 대중성 있는 메신저 텔레그램을 사용하도록 하겠습니다. 

 

텔레그램은 http://www.telegram.pe.kr/ 페이지에서 프로그램을 받을 수 있습니다. 설치한 이후에 검색에 botFather를 검색합니다.

이제 채팅으로 /help를 입력하면 봇 생성 및 관리에 관한 명령어를 보실 수 있습니다.

일단, 우리의 새로운 봇을 만들기 위해 /newbot 입력해 봅시다.

처음에는 텔레그램에 표시될 이름을 입력해야 하는데, 저는 주식 탐색 로봇이라 입력했습니다.

다음에는 이를 텔레그램에서 관리할 이름을 넣어주는데 뒤에 bot으로 끝나야 합니다.

저는 stock_search_bot이라 했습니다.

그럼 이 봇과 대화창 링크, 프로그램 코드에서 쓸 API 토큰 값을 주면서 봇이 만들어 집니다.

t.me/stock_search_bot. 를 클릭하면 오른쪽 같은 창이 뜨고, 시작 하면 준비가 완료 되었습니다.

 

이제 아래와 같은 코드를 입력합니다.

15줄 정도로 이제 프로그램내 발생하는 메시지나, 사진 파일등을 텔레그램으로 전송할 수 있고, 밖에서 휴대폰으로 실시간으로 데이터를 받을 수 있습니다.

 

telegram.py

#pip install telepot
import telepot

class TelegramBot:
    def __init__(self, token, id):
      self.teleBot_ = telepot.Bot(token)
      self.id_ = id

    def sendMessage(self, message):
      self.teleBot_.sendMessage(self.id_, message)

    def sendPhoto(self, image, message):
      self.teleBot_.sendPhoto(self.id_, photo=open(image, 'rb'), caption=message)

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

8. 매매 전략에 대해서  (0) 2020.11.08
7. 유틸 함수들  (0) 2020.11.08
6. 메시지 알람 (텔레그램)  (0) 2020.11.08
5. 주식 차트 그리기  (0) 2020.11.08
4-1 주식 데이터 저장 (sqlite)  (0) 2020.11.08
4. 주식 데이터 저장 (sqlite)  (0) 2020.11.07

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

 

지금은 자동화 모듈을 붙이지 않는 테스트 단계인데,

현재 상황에서 print 문으로 글자를 출력해서 실제 이게 맞는지 아닌지 분석하기는 한계가 있습니다.

그래서 차트를 실제 그려보고, 우리 로직이 제대로 가고 있는지 검증을 해보는 것이 중요합니다.

또한 오른쪽 그림과 같이 차트를 보내서 실제 매매할 때 이 주식이 장기 상향인지 박스권에 있는건지 판단하기 쉽게 해주는 역할도 있습니다.

차트는 캔들 차트를 그릴수도 있지만, 복잡하고, 굳이 그렇게 까지 할 필요 없이 간단하게 line 차트를 그리는 것으로 설명하겠습니다.

 

printChart.py

# 주식 차트를 그리는 클래스
import numpy as np
import pandas as pd
import dataframe

import datetime
import plotly.graph_objects as go
import tracemalloc
import matplotlib.pyplot as plt

import os
from stockData import StockData

class PrintChart:
    def saveFigure(self, dir, sd):
        try:
            plt.style.use('fivethirtyeight')
            plt.rc('font', family='Droid Sans')
            
            title = "[%s] close price" % (sd.name_) 
            plt.title(title)
            
            plt.figure(figsize=(16,8))
            dataCount = 100
            indi = sd.chartData_.tail(dataCount)
            df = pd.DataFrame(indi)
            df['candleTime'] = pd.to_datetime(df['candleTime'], format='%Y-%m-%d', infer_datetime_format=True)
            df = df.set_index('candleTime')

            plt.xlabel('candleTime', fontsize=18)
            #plt.xticks(rotation=45)
            plt.ylabel('Close Price', fontsize=18)
            plt.scatter(df.index, df['BuySignal'], color='green', label = 'buy', marker='^', alpha=1)
            plt.scatter(df.index, df['SellSignal'], color='red', label = 'sell', marker='v', alpha=1)
            plt.plot(df['close'], label='Close Price', alpha=0.35)
            plt.legend(loc='upper left')
            
            filename = "flg_%s.png" % (sd.code_)
            if os.path.exists(dir) == False:
                os.makedirs(dir)

            p = os.getcwd()
            filePath = "%s/%s%s" % (p, dir, filename)
            if os.path.isfile(filePath) == True:
                os.remove(filePath)
            
            plt.savefig(filePath)

            print("$ 차트 갱신 [%s] => [%s]" % (sd.name_, filename))
            return filePath
        except:
            print("$ 차트 갱신 실패 [%s]" % (sd.name_))
            return None
      

파이썬에서 차트 그려주는 plt 라이브러리를 사용했습니다.

정말 파이썬이 무서운 게, 50여 코드 가지고 이런 차트를 그릴 수 있는 게 대단한 거입니다. 사견으로 C++로 이걸 구현하려고 하면 정말 토 나옵니다.

 

17라인에서 이 그래프 테마, 스타일을 지정해 줍니다.

차트 스타일은 여러가지 있고 다른 스타일을 보고 싶으시면 print(plt.style.available)라 하면 여러 값들을 확인할 수 있습니다.

18라인에서 차트에 쓸 폰트를 지정하는데, 가능하면 한글이 지원되는 폰트를 사용하는 게 좋습니다.

이후 24,25라인에서 이 차트에 쓸 값들을 지정하는데, 전체를 하면 차트 크기에 비해 너무 많은 값들이 들어가서 작게 보입니다.

저는 여기서 과거 100일치 데이터만 보려고 tail 명령으로 100일치 데이터만 잘라 와 사용하고 있습니다.

 

33, 34라인은 매수, 매도 시그널 표시 지점인데, 저 값들은 이 다음장인 매매 전략에 대한 기술에서 추가될 내용입니다. 매수이면 ^ 기호, 매도이면 v를 찍어서 표시하도록 했습니다.

 

35라인에 close , 종가 데이터를 가지고 선 차트를 그리도록 하고, 그냥 하면 위의 매수/매도 시그널이 보이지 않으므로 alpha, 투명도 값을 30%로 지정해서 그리도록 합니다.

 

38 라인부턴 이 차트를 png 파일로 저장하는 방법에 대해 기술했습니다.

문제없으면 없다고 print, 로그 찍을 것이고, 문제 있으면 except 문을 타고 갱신 실패라 뜰껍니다. 만약 갱신 실패가 나오면 뭐가 문제인지 디버깅 하면서 찾아봐야 합니다.

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

7. 유틸 함수들  (0) 2020.11.08
6. 메시지 알람 (텔레그램)  (0) 2020.11.08
5. 주식 차트 그리기  (0) 2020.11.08
4-1 주식 데이터 저장 (sqlite)  (0) 2020.11.08
4. 주식 데이터 저장 (sqlite)  (0) 2020.11.07
3. 주식 데이터  (0) 2020.11.07

소스파일은 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 소수점 입력을 받도록 커스터마이징을 해주었습니다.

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

이게, 코드상, 주식 데이터 가지고 와라 (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
4. 주식 데이터 저장 (sqlite)  (0) 2020.11.07
3. 주식 데이터  (0) 2020.11.07
2. 주식데이터 가지고 오기  (0) 2020.11.07
1. 프로그램 설계  (0) 2020.11.07

+ Recent posts