< 5. 알고리즘을 통한 매수/매도 테스트, 수익율 계산 (키움 open api 주식 알고리즘 테스트 프로그래밍) >


1. 키움 open api를 이용한 주식 알고리즘 테스트 프로그래밍

2. DB 설계

3. DB 구현

4. 주식 데이터 불러오기


해당 포스트를 읽기 전에 위의 포스트들을 읽기 바란다.


우리는 이전 포스트까지 KODEX 코스닥 150 레버리지 종목의 일봉, 분봉 데이터를 불러와서 DB에 저장했다. 이번 포스트는 분봉 데이터를 토대로 자신이 구현한 알고리즘에 의해 가상으로 매수, 매도하고 수익율을 계산하는 코드에 대해 배운다. 블로그 저자가 실제 구현한 알고리즘은 공개하지 않겠다. 이번 포스트는 세부적인 코드는 공개하지 않을 것이다. 알고리즘은 각자 자신의 생각으로 구현하길 바란다. 


필자는 매수/매도 알고리즘에 사용하는 변수로 두 개를 사용했다. 이를 var1, var2라고 할 것이다. var1과 var2를 기준으로 매수, 매도한다. 이번 포스트에서 var1과 var2를 기준으로 매수/매도하고 수익율을 계산하는 코드를 포스팅한다. 다음 포스트는 알고리즘을 구현하기 전인 KODEX 코스닥 150 레버리지 당일 시가 매수, 당일 종가 매도 수익율과 알고리즘을 적용한 당일 수익율을 비교하는 코드와 수익율이 최대가 되는 var1과 var2를 구하는 코드에 대해 포스팅할 예정이다. 


이번에 구현할 코드는 특정 일의 주식 장 시작 시간부터 주식 끝날 때까지의 분봉 데이터와 var1, var2를 토대로 매수/매도하고 당일 수익율을 구하는 기능과 사용자가 지정한 기간동안 알고리즘 매수/매도를 통한 총 수익율을 구하는 기능이 있다.  



먼저 다음 코드를 보자. OrderBacktracking 클래스를 초기화하는 부분이다.

NO_CHANGE = 0 # 이번 minute에 아무 행동 안 함 CHANGE =1 # 이번 minute에 매수하거나 매도함 BUY_REASON = "VAR1과 VAR2 때문에 매수" SELL_REASON_VAR1 = "VAR1 때문에 매도" SELL_REASON_VAR2 = "VAR2 때문에 매도" class OrderBacktracking: def __init__(self,var1, var2): self.db = Db.StockDb() self.change = NO_CHANGE self.reason="" self.var1=var1 self.var2=var2

'Db.StockDb()'는 '3.DB 구현' 포스트에서 배웠다. 'self.change'는 알고리즘을 적용한 분에 매수했는 지 안 했는지 유무를 나타낸다. 즉, 장 시작 9시부터 장 끝나는 15시 30분까지 차례대로 분마다 알고리즘을 적용해 매수할 지, 매도할 지 결정하는 데 해당 분에 매수 또는 매도 했다면 'self.change'가 'CHANGE'가 되고 안 햇으면 'NO_CHANGE'가 된다. 'reason'은 위 코드에서 보이는 'BUY_REASON', 'SELL_REASON_VAR1', 'SELL_REASON_VAR2'을 나타낸다. 



아래 메서드는 알고리즘을 적용할 day를 정하고 day의 분봉 데이터과 알고리즘을 토대로 day일 매수/매도 리스트를 만드는 기능을 한다. 

def set_day(self,day): self.day = day self.daily_data = self.db.select_MinuteData(day) self.daily_start = self.db.select_DailyStart(day) self._init_by_day() self._order_history()

'select_MinuteData(day)' 를 이용해 day일의 분봉 데이터를 가져와 'daily_data' 데이터 프레임 객체에 넣는다. 'select_DailyStart(day)'는 day일의 일봉 시초가 데이터를 가져온다. '_init_by_day()' 메서드는 다음과 같다.


    def _init_by_day(self):
        self.buy_list = pd.DataFrame(columns=('date', 'buy_price', 'reason'))
        self.sell_list = pd.DataFrame(columns=('date', 'sell_price', 'reason'))
        self.order_id = 0
        self.order = False  # False : 가진 주식 없음, True : 가진 주식 있음
'_init_by_day()'는 특정 day의 매수, 매도 정보를 저장할 데이터를 초기화한다. 'set_day(day)' 메서드에서 '_init_by_day()'를 호출하는 이유는 위의 'buy_list'와 'sell_list'가 특정 day의 매수, 매도 정보를 나타내기 때문에 OrderBacktracking 객체의 day 정보가 바뀌면 'buy_list'와 'sell_list'의 기존 데이터는 필요없어지기 때문이다. 수익율을 계산할 때 특정 분에 매수한 가격과 바로 다음에 매도한 가격을 매칭해야 한다. 매칭하려면 매수정보와 매도정보에서 같은 값이 필요한데 이 때문에 'order_id'를 사용한다.   


'_order_history()'는 var1과 var2를 토대로 알고리즘을 적용하여 매수, 매도 데이터를 저장하는 기능을 한다.

def _order_history(self): if(self.is_order_data()): return data_len = len(self.daily_data) - 1 #3시 20분에서 3시 30분 제외 for i in range(data_len):

분봉 데이터, var1, var2 를 토대로 알고리즘을 적용해 매수/매도를 buy_list와 sell_list에 저장한다.             'i'분에 매수 또는 매도했다면 self.change가 'CHANGE'가 된다. 매수했다면 self.order가 True가 되고

매도했다면 self.order가 False가 된다.

if self.change == CHANGE: if self.order: data = self.buy_list.ix[self.order_id] self.db.insert_Buy(data['date'],data['buy_price'],data['reason']) else: data = self.sell_list.ix[self.order_id] self.db.insert_Sell(data['date'], data['sell_price'], data['reason']) self.order_id += 1 self.change = NO_CHANGE if self.order: price = self.daily_data['high'][data_len-1] date = self.daily_data['date'][data_len-1] self.sell_list = self.sell_list.append(pd.Series({'date': date, 'sell_price': price, 'reason': "당일 종가"}),ignore_index=True) self.db.insert_Sell(date, int(price), "당일 종가") self.db.commit()

'self.is_order_data()'는 DB에 지금 알고리즘을 적용할 day의 매수, 매도 데이터가 있는 지 확인한다. DB에 데이터가 있다면 데이터를 다시 계산할 필요없이 DB에서 데이터를 가져오기만 하면 된다. 'self.is_order_data()' 코드는 위 메서드를 설명 후 다시 설명하겠다. 

'data_len'은 분봉 데이터 갯수를 의미한다. 참고로 -1을 하는 이유는 3시 20분부터 30분까지는 분단위로 매매를 할 수 없기 때문에 3시 20분까지만 알고리즘을 이용한 매수/매도를 한다. 'for i in range(data_len)'을 통해 9시 00분부터 3시 20분까지 알고리즘 매수/매도를 한다. 구체적인 알고리즘 개인이 구현하길 바란다. 위 코드에 제시한 대로 매수와 매도 데이터(매수/매도 가격, 매수/매도 시간, 매수/매도 이유)를 저장한다. 해당 분에 매수/매도 했다면 'self.change'값을 'CHANGE'로 바꾼다. 매수했다면 'self.order'를 True로 바꾸고 매도했다면 False로 바꾼다. 

알고리즘을 통한 매수/매도를 했다면 매수/매도 데이터를 DB에 저장해야 한다. 먼저 'self.change'값을 통해 매수/매도 했는지 확인하고 'self.order'을 통해 매도했는지 매수했는지 확인한다. 이 조건들을 판별한 후 'db.insert_Sell()' 또는 'db.insert_Buy()'를 통해 DB에 데이터를 저장한다.  그리고 'self.change'를 NO_CHANGE로 바꾼다. 

위 메서드 마지막 'if self.order:'은 당일 수익율을 계산하려면 무조건 당일이 끝난 후 가지고 있는 주식이 없어야 정당하다. 따라서 주식을 계속 들고 있다면 당일 종가로 매도한다. 

마지막으로 db.commit()를 통해 insert한 데이터를 최종적으로 DB에 저장한다.  


'is_order_data()' 메서드는 위에서 설명한 것처럼 DB에 day일의 매수, 매도 데이터가 있는 지 확인한다.

    def is_order_data(self):
        self.buy_list = self.db.select_Buy(self.day)
        if len(self.buy_list) == 0:
            return False
        else:
            self.sell_list = self.db.select_Sell(self.day)
            return True

'db.select_Buy(day)'로 매수 데이터를 불러온다. 'len(self.buy_list)'를 이용해 매수 데이터가 있는 지 확인하고 없다면 False를 리턴하고 매수 데이터가 있다면 'self.db.select_Sell(day)'로 매도 데이터를 가져오고 True를 리턴한다. 



여기까지가 알고리즘을 통한 매수, 매도 데이터를 저장하는 코드였다. 다음으로 수익율을 계산하는 코드를 보자.


'get_day_profit(day)' 는 day일의 수익율을 계산한다.

def get_day_profit(self,day): self.set_day(day) profit = self.cal_profit() self.print_Profit(day,profit) return profit

위에서 배운 'set_day(day)' 메서드를 통해 day일의 매수, 매도 데이터를 계산한다. 'cal_profit()' 메서드를 이용해 당일 수익율을 계산하고 'print_Profit(day,profit)' 메서드를 이용해 day일의 수익율을 출력한다. 이 두 메서드에 대해 알아보자


'cal_profit()' 메서드는 매수, 매도 데이터를 토대로 당일 수익율을 계산한다.

    def cal_profit(self):
        self.profit=1
        buy_len = len(self.buy_list)
        for i in range(len(self.buy_list)):
            self.profit = self.profit * self.sell_list['sell_price'][i]/self.buy_list['buy_price'][i]*0.9997
        return self.profit

키움증권의 수수료가 0.015%이고 매수, 매도할 때마다 수수료가 붙기 때문에 총 0.03%의 수수료가 부과된다. 따라서 0.9997을 곱한다.


'print_Profit()' 메서드는 day일의 수익율을 출력한다.

    def print_Profit(self,day, profit):
        profit = (profit-1)*100
        print(str(day)+ " 수익률 : "+ str(profit))


지금까지 당일 수익율을 계산해보았다. 다음은 가장 중요한 지정된 기간 사이의 총 수익율을 계산하는 메서드이다. 이 메서드를 통해 해당 알고리즘이 몇일, 몇달을 걸쳐 수익이 나는지 확인할 수 있다.



'get_days_profit(start,end)' 메서드는 start 일부터 end 일까지의 총 수익율을 계산한다.

    def get_days_profit(self, start, end):
        date_data = self.db.select_Date(start, end)
        date_len = len(date_data)
        self.profit_datas= pd.DataFrame(columns=('date','profit','total_profit'))
        total_profit = 1
        if date_len == 0:
            print("두 날짜 사이의 거래일이 없음")

        for i in range(date_len):
            day = date_data['date'][i][0:8]
            self.set_day(day)
            profit = self.cal_profit()
            total_profit *= profit
            self.profit_datas = self.profit_datas.append(pd.Series({'date' : day, 'profit' : profit, 'total_profit' : total_profit}),ignore_index=True)
        self.print_Total_Profit(day, total_profit)
        return self.profit_datas

'self.db.select_Data(start,end)'를 이용해 start일에서 end일까지의 day 데이터를 불러온다. 이렇게 하는 이유는 공휴일이나 주말에는 주식시장이 개장되지 않기 때문이다. 'self.profit_datas'는 date일의 당일 수익율과 start 일부터 date일까지의 총 수익율을 저장한다. 'for i in range(data_len)'을 이용해 start 일부터 end 일까지 'set_day(day)'와 'cal_profit()' 메서드를 이용해 당일 수익율을 계산한 다음 총수익율을 구하고 'profit_datas'에 도출된 데이터를 저장한다. 마지막으로 'print_Total_Profit()'를 통해 총 수익율을 출력한다. 'print_Total_Profit()' 메서드는 다음과 같다.

    def print_Total_Profit(self,day,profit):
        profit = (profit - 1) * 100
        print(str(day) + " 총 수익률 : " + str(profit))


여기까지가 일정 기간동안 총 수익율을 구하는 코드였다. 마지막으로 함수 하나를 추가해야 한다. 다음 포스트부터는 알고리즘을 통한 매수/매도가 얼마나 효율적인지, 어떻게 하면 더 이익을 많이 날 수 있을지 확인할 것이다. 이를 위해서 우리가 알고리즘을 구현할 때 사용한 변수 var1과 var2 변수 값을 계산 변경하면서 최고의 수익율을 내는 var1과 var2 변수를 찾아야 한다. 따라서 var1과 var2에 다른 값을 넣고 초기화하는 함수를 만들어야 한다. 이 함수는 다음과 같다.

def set_var1_and_var2(self,var1,var2): if(self.var1==var1 and self.var2 == var2): return self.db.delete_Buy_Sell() self.var1 = var1 self.var2 = var2 self._init_by_day()

var1과 var2를 다른 값으로 바꾼다. var1과 var2가 바꼈기 때문에 기존에 있던 매수, 매도 데이터는 무가치하다. 따라서 'db.delete_Buy_Sell()' 메서드를 이용해 데이터를 지우고 다시 'self._init_by_day()'를 이용해 새로운 매수, 매도 데이터를 구한다.


여기까지가 알고리즘을 통한 매수, 매도를 테스트하고 수익율을 계산하는 코드였다. 다음은 이에 대한 전체 소스이다.

import sqlite3
import pandas as pd
import Db

NO_CHANGE = 0  # 이번 minute에 아무 행동 안 함
CHANGE =1   # 이번 minute에 매수하거나 매도함

BUY_REASON = "VAR1과 VAR2 때문에 매수"
SELL_REASON_VAR1 = "VAR1 때문에 매도"
SELL_REASON_VAR2 = "VAR2 때문에 매도"

class OrderBacktracking:
    def __init__(self,var1, var2):
        self.db = Db.StockDb()
        self.change = NO_CHANGE
        self.reason=""
        self.var1=var1
        self.var2=var2

    def _init_by_day(self):
        self.buy_list = pd.DataFrame(columns=('date', 'buy_price', 'reason'))
        self.sell_list = pd.DataFrame(columns=('date', 'sell_price', 'reason'))
        self.order_id = 0
        self.order = False  # False : 가진 주식 없음, True : 가진 주식 있음


    def set_day(self,day):
        self.day = day
        self.daily_data = self.db.select_MinuteData(day)
        self.daily_start = self.db.select_DailyStart(day)
        self._init_by_day()
        self._order_history()

    def set_var1_and_var2(self,var1,var2):
        if(self.var1==var1 and self.var2 == var2):
            return
        self.db.delete_Buy_Sell()
        self.var1 = var1
        self.var2 = var2
        self._init_by_day()


	....알고리즘 매수, 매도 관련 함수



    def _order_history(self):
        if(self.is_order_data()):
            return

        data_len = len(self.daily_data) - 1 #3시 20분에서 3시 30분 제외

        for i in range(data_len):
            
            분봉 데이터, var1, var2 를 토대로 알고리즘을 적용해 매수/매도를 buy_list와 sell_list에 저장한다.
            'i'분에 매수 또는 매도했다면 self.change가 'CHANGE'가 된다. 매수했다면 self.order가 True가 되고
            매도했다면 self.order가 False가 된다.

            if self.change == CHANGE:
                if self.order:
                    data = self.buy_list.ix[self.order_id]
                    self.db.insert_Buy(data['date'],data['buy_price'],data['reason'])
                else:
                    data = self.sell_list.ix[self.order_id]
                    self.db.insert_Sell(data['date'], data['sell_price'], data['reason'])
                    self.order_id += 1
                self.change = NO_CHANGE

        if self.order:
            price = self.daily_data['high'][data_len-1]
            date = self.daily_data['date'][data_len-1]
            self.sell_list = self.sell_list.append(pd.Series({'date': date, 'sell_price': price, 'reason': "당일 종가"}),ignore_index=True)
            self.db.insert_Sell(date, int(price), "당일 종가")

        self.db.commit()

    def cal_profit(self):
        self.profit=1
        buy_len = len(self.buy_list)
        for i in range(len(self.buy_list)):
            self.profit = self.profit * self.sell_list['sell_price'][i]/self.buy_list['buy_price'][i]*0.9997
        return self.profit

    def is_order_data(self):
        self.buy_list = self.db.select_Buy(self.day)
        if len(self.buy_list) == 0:
            return False
        else:
            self.sell_list = self.db.select_Sell(self.day)
            return True

    def get_day_profit(self,day):
        self.set_day(day)
        profit = self.cal_profit()
        self.print_Profit(day,profit)
        return profit

    def get_days_profit(self, start, end):
        date_data = self.db.select_Date(start, end)
        date_len = len(date_data)
        self.profit_datas= pd.DataFrame(columns=('date','profit','total_profit'))
        total_profit = 1
        if date_len == 0:
            print("두 날짜 사이의 거래일이 없음")

        for i in range(date_len):
            day = date_data['date'][i][0:8]
            self.set_day(day)
            profit = self.cal_profit()
            total_profit *= profit
            self.profit_datas = self.profit_datas.append(pd.Series({'date' : day, 'profit' : profit, 'total_profit' : total_profit}),ignore_index=True)
        self.print_Total_Profit(day, total_profit)
        return self.profit_datas


    def print_Profit(self,day, profit):
        profit = (profit-1)*100
        print(str(day)+ " 수익률 : "+ str(profit))

    def print_Total_Profit(self,day,profit):
        profit = (profit - 1) * 100
        print(str(day) + " 총 수익률 : " + str(profit))


if __name__ == "__main__":
    main = OrderBacktraking()
    main.get_days_profit("20170922","20171107")



여기까지가 알고리즘을 통한 매수, 매도 테스트, 수익율 계산 코드이다. 마지막으로 다음 포스트에서는 알고리즘을 적용 후와 적용 전 비교, 최대 수익율을 얻기 위해 알고리즘에 대입하는 변수들을 최적화하는 코드에 대해서 알아볼 것이다. 

+ Recent posts