PYTHON/AUTO TRADE SYSTEM

[자동 매매 시스템 구축하기] 알고리즘 구축하기 (7) - 알고리즘 구현하기 ②

  • -

지난 게시글에서는 이상한 차트 데이터를 재수정하는 기능을 추가했으니, 이번 게시글에서는 제발 매수 가격과 매도 가격을 계산할 수 있길 바란다. 

 

 

우리가 구현할 전략에 알맞은 데이터인가?

이전 게시글에서 우리가 수립할 전략에 대해 다음과 같은 세 가지 기준을 설정했었다.

  • 5일 이동평균선이 20일 이동평균선을 상향 돌파(골든 크로스)한 시점에서 매수
  • 5일 이동평균선이 10일 이동평균선을 하향 돌파(데드 크로스)한 시점에서 손절
  • 매수가 대비 5% 지점에서 수익 실현

하지만 이 전략을 보면, 명확한 기준이 없다. 왜냐하면 골든 크로스나 데드 크로스가 발생하는 시점에서, 해당 봉의 종가에 매수에 가담할 것인지 내지는 골든 크로스 또는 데드 크로스가 발생하게 되는 명확한 가격을 넘어설 때 매수에 가담할 것인지와 같은 세부적인 디테일이 제외되어 있다는 것이다.

물론 알고리즘 파일 내에서 구현할 기능들은 위의 세 가지 기준에서 출발하긴 하지만, 실질적으로 여러 기능들을 구현해나가다 보면 여러 가지 문제점에 봉착하게 될 것이다.

 

 

매수 가격 계산 기능

그렇다면 알고리즘은 어떤 식으로 구현해줘야 할까? 간단하게 결론부터 이야기하자면, 아직 5선이 20선을 돌파하지 않은 종목을 찾아줘야 한다. 왜냐? 5선이 20선을 돌파할 때 매수하도록 하는 알고리즘을 구현할 것인데 이미 조건검색식 상에서 5선이 20선을 돌파해있는 종목을 찾으면 우리는 영영 매수할 수 없다. 그렇지 않은가? 5일 이동평균선이 20일 이동평균선을 돌파할 때 매수하기 위한 알고리즘을 구현하고자 하는데 이미 돌파한 종목만 찾으면 매수를 어디서 하는가?

그럼에도 불구하고 우리는 조건검색식 상에서 5선이 20선을 상향 돌파한 종목을 찾았다. 그 이유는 5일 이동평균선이 20일 이동평균선을 상향 돌파한다는 것은 안타깝게도 장이 마감된 이후에야 확신할 수 있기 때문이다. 다시 말해, 장중에는 5선이 20선을 상향 돌파했다고 하더라도 그 확신이 부족하다는 것이다.(기본적으로 이동평균선 데이터는 종가를 기준으로 하고 있기도 하다.) 

위의 두 내용을 종합해보면 우리는 다음과 같이 정리할 수 있다.

  • 돌파 여부는 장이 마감된 후에야 판단이 가능하다
  • 돌파 시점에서의 매수는 리스크가 높다. (불가능하다는 것은 아님)

그렇다면 우리는 추후 이루어질 일정 부분의 조정이 발생했을 때 매수에 가담한다던가 하는 등의 전략을 구현함으로써 문제를 해결할 수 있을 것이다. 아래의 도식을 참고해보자.

좌측은 돌파가 이루어진 경우이고, 우측은 돌파가 실패한 경우이다.

좌측의 도식은 돌파가 이루어진 이후의 조정에서 매수에 가담하는 것이기 때문에 괜찮지만, 우측의 도식은 돌파가 이루어졌다 하더라도 매수에 가담하지 않았을 수 있다. 왜냐하면 조정 과정에서 이미 5일 이동평균선을 이탈해있기 때문이다. (물론 좌측의 도식에서도 조정이 5일 이동평균선을 이탈할 수도 있기는 하다. 여기서의 디테일들은 추후에 계속해서 수정해나가도록 하자.)

그렇다면 5일 이동평균선이 20일 이동평균선을 상향 돌파한 시점에서, 우리는 매수 가격을 어디로 설정할지에 대한 기준을 설정하면 되는데, 단순하게 생각해서 상향 돌파할 때 발생하는 흐름의 중간 지점에서 매수에 가담하도록 한다고 가정하고 매수 가능 가격을 계산해보자.(너무 디테일한 이야기가 나오면 너무 어려워진다.)

여기서 우리가 원하는 중간 지점이란, 상향 돌파가 완성되도록 하는 봉의 시가부터 이후에 형성된 고가까지의 데이터가 될 것이다. 아래의 세 가지 예시를 살펴보자. 어떤 알고리즘을 구현할 것인지 단 번에 이해가 될 것이다. 

그렇다면 우리가 여기서 필요한 데이터는 900원이라는 데이터와, 추후 형성될 고가 데이터인 1,000원, 1,500원, 2,000원이라는 데이터이다. 900원이라는 데이터는 고정시켜놓되, 1,000원과 1,500원, 2,000원이라는 데이터는 계속해서 갱신해줘야 한다는 것이다.

이제 구현하고자 하는 전략을 이해했으니 매수 가격을 계산하는 코드를 구현할 것인데, 그 전에 앞서 우리는 5일 이동평균선이 20일 이동평균선을 돌파한 시점을 확인할 수 있어야 한다. 왜냐하면 우리의 차트 데이터는 아래와 같이 구현되어 있기 때문이다. 아래의 데이터 중에서 5일 이동평균선이 20일 이동평균선을 상향 돌파했다는 사실을 어떻게 확인할 수 있을까? 불가능하다.

                time    now   open   high    low      
0      20220721153000  61800  61800  61800  61800  
1      20220721151500  61900  61800  61900  61700  
2      20220721151000  61800  61800  61900  61700  
3      20220721150500  61800  61700  61800  61600  
4      20220721150000  61700  61700  61800  61600  
...               ...    ...    ...    ...    ...       
20346  20210701092000  80200  80000  80300  80000  
20347  20210701091500  80100  80200  80200  80000
20348  20210701091000  80200  80100  80200  80000
20349  20210701090500  80100  80300  80300  80100 
20350  20210701090000  80300  80500  80600  80200 
[20351 rows x 5 columns]

물론 이 데이터를 기반으로 이동평균선 값을 확인할 수 있는 여러 가지 방법들이 있기는 하다. 일자 별로 데이터를 묶은 후에 해당 일자의 시가를 찾고 최고가를 고가로, 최저가를 저가로, 마지막 데이터의 현재가 데이터를 종가로 두어 일봉 데이터를 얻으려면 얻기야 하겠지만, 그 과정이 복잡하기 때문에 구현하기가 쉽지는 않다. 물론 일봉 차트 데이터를 조회한 후, 일봉 차트 데이터를 기반으로 이동평균선 데이터를 생성한 후 5일 이동평균선이 20일 이동평균선을 상향 돌파한 시점을 찾아낼 수도 있다. 하지만 모든 방법들을 차치하고 가장 좋은 방법은 무엇일까? 바로 5일 이동평균선과 20일 이동평균선이 골든 크로스되어 조건검색식에 집계된 일자 데이터를 얻는 것이다. 다시 말해, 해당 종목을 발견한 일자 데이터를 전달해주면 된다는 것이다. 

물론 이 기능은 추후에 조건검색식과 관련된 데이터를 저장할 때 모두 구현해줄 예정이니 현 시점에서는 임의로 일자를 전달해주도록 하자. 즉, 5일 이동평균선이 20일 이동평균선을 상향 돌파한 일자를 우리가 임의로 전달해주자는 것이다. 이번 알고리즘 구현 시 사용할 삼성전자의 경우, 2021.07.01부터 2022.07.20일까지의 데이터 중 5일 이동평균선이 20일 이동평균선을 상향 돌파한 가장 최근의 일자는 2022년 7월 15일이다. 따라서 이 데이터를 함께 전달해주도록 하자.

아래의 코드 중 Line 7에서는 def __init__(self)의 인자에 start_date라는 파라미터를 추가했고, 해당 파라미터를 통해 얻어온 값을 Line 9에서 self.start_date로 재정의해서 해당 클래스 전역에서 사용할 수 있도록 했다. 다음으로 Line 22에서는 start_date라는 변수에 "20220715"라는 데이터를 임의로 입력했고, Line 24에서는 algo1() 클래스의 인자로 min5_date와 start_date라는 변수를 전달해주었다.
※ Line : 7, 9, 22, 24

"""algorithm_1.py"""
import pandas as pd
import manage_db

class algo1:

    def __init__(self, min5_data, start_date):
        self.min5_data = min5_data
        self.start_date = start_date
        print(self.min5_data)
        pass


    def run(self):
        pass


if __name__ == "__main__":
    item_code = "005930"
    start_date = "20220715"
    min5_data = pd.read_sql(f"SELECT * FROM s{item_code}", manage_db.engine_5min)
    algo1(min5_data, start_date).run()

 

 


728x90

 

 

self.start_date의 시가 데이터 얻어오기

그렇다면 우리는 이제 우리가 전달받은 일자 데이터(self.start_date)에 해당하는 일자의 시가 데이터를 얻어와야 한다. 이제부터 데이터프레임을 가공하는 일이 상당히 잦을 것이다. 왜냐하면 알고리즘을 구현하는 것 자체가 데이터를 가공하는 것이기 때문이다. 

가장 먼저, 조회되어 있는 5분봉 차트 중 self.start_date에 해당하는 일자의 데이터를 얻어와야 한다. 하지만 우리가 조회한 self.min5_data 내에는 일자와 시간이 합쳐져 있는 데이터는 있더라도 일자만 표기되어 있는 데이터는 없다. 따라서 우리는 일자 데이터를 새롭게 생성해줄 예정이다. 데이터프레임은 여러 가지 계산이 손쉽게 이루어지는데, 그 중 칼럼을 새롭게 생성하는 대표적인 사용 방법은 DataFrame['new_column_name'] = DataFrame['origin_column_name']과 같은 형태로 사용한다. 우리가 여기서 생성할 데이터는 일자 데이터로, 일자 데이터는 ['date'] 칼럼으로 새로 생성해줄 것이기 때문에 아래와 같이 작성하면 된다.

  • DataFrame['date'] = DataFrame['time']~~~~

여기서 ['time'] 칼럼 내 입력되어 있는 데이터에 대한 조건을 달아주어야 한다. 왜냐하면 현재 ['time'] 칼럼 내에는 모든 데이터가 "YYYYMMDDHHMMSS"의 형태로 입력되어 있기 때문이다. 우리가 여기서 사용하고자 하는 부분은 "YYYYMMDD"의 8자리이다. 데이터프레임에서는 문자열을 다르는 것에 대해 .slice(start=, stop=) 메서드를 지원하고 있다. start는 자르는 것을 시작할 인덱스 번호를, stop에는 자르는 것을 종료할 인덱스 번호를 전달해주면 된다. 처음부터 8번째 자리까지를 자를 것이니 8을 전달해주면 된다.
※ Line : 10, 11

"""algorithm_1.py"""
import pandas as pd
import manage_db

class algo1:

    def __init__(self, min5_data, start_date):
        self.min5_data = min5_data
        self.start_date = start_date
        self.min5_data['date'] = self.min5_data['time'].str.slice(start=0, stop=8)
        print(self.min5_data)

 

                time    now   open   high    low      date
0      20210701090000  80300  80500  80600  80200  20210701
1      20210701090500  80100  80300  80300  80100  20210701
2      20210701091000  80200  80100  80200  80000  20210701
3      20210701091500  80100  80200  80200  80000  20210701
4      20210701092000  80200  80000  80300  80000  20210701
...               ...    ...    ...    ...    ...       ...
20346  20220721150000  61700  61700  61800  61600  20220721
20347  20220721150500  61800  61700  61800  61600  20220721
20348  20220721151000  61800  61800  61900  61700  20220721
20349  20220721151500  61900  61800  61900  61700  20220721
20350  20220721153000  61800  61800  61800  61800  20220721
[20351 rows x 6 columns]

 

그렇다면 이제 여기서 우리가 원하는 것은 이동평균선 간 골든 크로스가 발생한 2022년 7월 15일의 데이터를 얻어다가 시가 데이터를 계산해내는 것이다. 이는 이전에도 몇 번 사용했던 형태의 코드이다. print()문을 사용해 결과값을 보면 2022년 7월 15일의 데이터만 얻어왔음을 확인할 수 있다. (여기서 reset_index(drop=True) 메서드를 통해 인덱스를 재설정해주도록 하자.)
※ Line : 13, 14

"""algorithm_1.py"""
import pandas as pd
import manage_db

class algo1:

    def __init__(self, min5_data, start_date):
        self.min5_data = min5_data
        self.start_date = start_date
        self.min5_data['date'] = self.min5_data['time'].str.slice(start=0, stop=8)
        print(self.min5_data)

        data_of_start_date = self.min5_data[self.min5_data['date'] == self.start_date].reset_index(drop=True)
        print(data_of_start_date)

 

              time    now   open   high    low      date
0   20220715090000  58500  58400  58700  58300  20220715
1   20220715090500  58800  58500  58800  58500  20220715
2   20220715091000  58600  58700  58800  58500  20220715
3   20220715091500  58400  58600  58600  58300  20220715
4   20220715092000  58200  58400  58400  58200  20220715
..             ...    ...    ...    ...    ...       ...
73  20220715150500  59700  59800  59800  59600  20220715
74  20220715151000  59800  59700  59800  59600  20220715
75  20220715151500  59800  59700  59900  59600  20220715
76  20220715153000  60000  60000  60000  60000  20220715
77  20220715153500  60000  60000  60000  60000  20220715

 

하지만 순서가 거꾸로 되어 있다는 것 역시 확인할 수 있다. 다시 말해, "20220715090000"이 맨 위로 오고 20220715153500"이 맨 밑으로 가야 우리가 for문을 돌릴 때 살펴보기 편하다는 것이다. 물론 여기서 우리는 2022년 7월 15일의 시가 데이터만 구해오면 되기 때문에, 맨 마지막 데이터의 시가 데이터를 구해와도 되긴 하지만, 사전에 깔끔하게 정리해주면 좋다. 여기서 수정할 데이터는 data_of_start_date가 아닌 self.min5_data이다.
※ Line : 4

if __name__ == "__main__":
    item_code = "005930"
    start_date = "20220715"
    min5_data = pd.read_sql(f"SELECT * FROM s{item_code}", manage_db.engine_5min).sort_values(by='time').reset_index(drop=True)
    algo1(min5_data, start_date).run()

 

그 후 코드를 실행해보면 self.min5_data 변수와 data_of_start_date 변수 모두 옛날 데이터가 최상단에 위치하도록 재배열되었음을 확인할 수 있다.

## self.min5_data
                 time    now   open   high    low      date
0      20220721153000  61800  61800  61800  61800  20220721
1      20220721151500  61900  61800  61900  61700  20220721
2      20220721151000  61800  61800  61900  61700  20220721
3      20220721150500  61800  61700  61800  61600  20220721
4      20220721150000  61700  61700  61800  61600  20220721
...               ...    ...    ...    ...    ...       ...
20346  20210701092000  80200  80000  80300  80000  20210701
20347  20210701091500  80100  80200  80200  80000  20210701
20348  20210701091000  80200  80100  80200  80000  20210701
20349  20210701090500  80100  80300  80300  80100  20210701
20350  20210701090000  80300  80500  80600  80200  20210701

## data_of_start_date
              time    now   open   high    low      date
0   20220715153500  60000  60000  60000  60000  20220715
1   20220715153000  60000  60000  60000  60000  20220715
2   20220715151500  59800  59700  59900  59600  20220715
3   20220715151000  59800  59700  59800  59600  20220715
4   20220715150500  59700  59800  59800  59600  20220715
..             ...    ...    ...    ...    ...       ...
73  20220715092000  58200  58400  58400  58200  20220715
74  20220715091500  58400  58600  58600  58300  20220715
75  20220715091000  58600  58700  58800  58500  20220715
76  20220715090500  58800  58500  58800  58500  20220715
77  20220715090000  58500  58400  58700  58300  20220715
[78 rows x 6 columns]

 

 

이제 시가 데이터를 얻어오면 되는데, 시가 데이터는 self.start_price 변수에 입력해줄 예정이다. 특정 데이터프레임에서 특정 열, 칼럼에 위치한 데이터를 얻어오는 방법은 아래와 같다.

  • DataFrame.loc[index_num, 'column_name']

그 결과로 58400원이라는 데이터가 출력되었으며, 이 데이터는 실제 삼성전자의 2022년 7월 15일자 시가와 동일한 데이터라는 점을 확인할 수 있다. 이제 값이 정상적으로 출력된다는 것을 확인했으니, Line 16에 있는 self.start_price 데이터가 숫자형 데이터가 되도록 int()를 씌워주도록 하자.
※ Line : 16, 17

"""algorithm_1.py"""
import pandas as pd
import manage_db

class algo1:

    def __init__(self, min5_data, start_date):
        self.min5_data = min5_data
        self.start_date = start_date
        self.min5_data['date'] = self.min5_data['time'].str.slice(start=0, stop=8)
        print(self.min5_data)

        data_of_start_date = self.min5_data[self.min5_data['date'] == self.start_date]
        print(data_of_start_date)

        self.start_price = int(data_of_start_date.loc[0, 'open'])
        print(self.start_price)

 

 

이제 본격적으로 매수 가격을 계산해줘야 하는데, 위에서 여러 도식을 기반으로 설명했듯이 분봉 차트 데이터를 기반으로 for문을 돌면서 시가(self.start_price)에서부터 시작해서 고가가 갱신될 때마다 우리의 매수 예정 가격을 갱신해주면 된다. 여기서 왜 일봉 상 고가 데이터와 시가 데이터를 사용하지 않냐는 의문이 들 수도 있는데, 사실 일봉 차트는 분봉 상 여러 흐름들을 간과하고 있기 때문이다. 그냥 분봉으로 보는 게 좋다고 생각하고 넘어가도록 하고, 이 부분에 대해 너무 궁금하다면 다른 게시글의 중간 부분에 위치한 백테스팅 시에는 나타나지 않는 실 거래의 문제점에 서술된 내용을 살펴보도록 하자.

 

 


 

728x90
반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.