코드를 끊임없이 돌아가게 할 수 있는 뼈대를 만들었다면, 이제 살지 말지를 결정할 뇌(알고리즘)와 실제로 사고 팔고를 할 근육을 붙여줄 차례다.
그전에, 소개해야 할 파이썬 라이브러리가 있다. Technical Analysis library in python이라는 라이브러리인데, 이게 굉장히 쓸모가 많다. 실제로 이 라이브러리가 없을 때는 일일이 관련된 함수를 만들어 줬어야 했는데 (예를 들면 이동평균이라던지) 이 라이브러리를 사용 한 후로는 그런 귀찮은 과정 없이 많은 종류의 보조지표(인디케이터)들을 자유롭게 사용할 수 있다. (홈페이지는 https://technical-analysis-library-in-python.readthedocs.io/en/latest/index.html)
사용법도 굉장히 간단하다. 그냥 원하는 보조지표가 있으면 함수에 OHLCV 데이터를 리스트 형식으로 넣어주면 된다. 물론 window size와 같은 파라메터들을 바꿔주는 것도 가능하다. 물론 하이킨-아시(Heikin-ashi) 나 다른 해괴망측한 보조지표는 없지만, 일반적으로 생각하는 보조지표들은 다 있다고 보면 된다.
하나 더. 편의상 df_master의 인덱스(행)를 두 부분으로 나눠야 할 것 같다. 위의 예시처럼 판단의 근거가 될 보조지표들이 기입되는 위쪽 두줄('prev_200 ema'부터 'now_+di'까지)은 '데이터 부분'이라 부를 것이고, 맨 아래 한 줄의 정보는 '명령 부분'이라고 부를 것이다.
While-> Try-> if 문을 활용해서 어떻게 코드가 돌아가는지는 저번 글에서 다뤘다. 다음은 df_master에 저장한 정보를 바탕으로 살지, 팔지를 결정하는 부분이다. 코드의 메인디쉬랄까.
가장 먼저, 코드가 실행될 때 df_master의 '데이터 부분'을 갱신해주는 get_coin_info함수다. 여기선 모든 코인의 OHLCV데이터를 받고, 보조지표를 추가하여 필요한 정보를 모은다. 기존의 df_master와 몇 분봉 데이터를 사용할지를 입력받고 다시 df_master를 뱉는다.
decide 함수는 df_master에 입력된 최신 데이터를 바탕으로 살지 말지를 결정하여 정보를 df_master의 '명령 부분'에 저장하고, 다시 df_master를 뱉는다.
df_master의 '명령 부분'의 주문에 따라 실제로 사고팔고를 진행한다. 이때 주문이 들어가는 시간이 있으므로, 모든 주문이 들어간 후 계좌 총액이 제대로 계산되게 하기 위해 잠시 쉬어준다.
다음은 상황을 저장하여 알고리즘의 효율을 알아볼 수 있게 하는 부분이다. 현재 시각과 현재 총잔액으로 구성된 딕셔너리를 만들고, 이를 total_balance 데이터 프레임에 추가한 후, 엑셀로 저장하여 실시간으로 잔액 변화를 알아볼 수 있게 한다.
이제 하나하나 더 자세히 살펴보자. 첫 번째는 get_coin_info 부분이다.
위에서 설명했다시피, get_coin_info 함수는 df_master와 몇 분봉을 사용할지 데이터를 받아 df_master의 데이 터부를 갱신하는 역할을 한다. 이때 coins 에는 모든 코인의 티커(ticker)가 리스트 형태로 저장되어 있다.
이때 for 문은 전체 코인에 대해 작동하며, coins안의 티커를 활용해 OHLCV데이터를 가공한다. 다음은 for 문 안의 코드를 설명한다.
pyupbit.get_ohlcv는 티커와 분 봉 정보 그리고 몇 개 데이터를 받을지를 입력하여 OHLCV를 내놓는다. 참고로 이 과정에서 가장 JSONdecoder error 가 가장 많이 난다. 요청을 너무 짧은 시간에 많이 하면 나타나는 오류다. 참고로 pyupbit.get_ohlcv를 실행시키면 아래와 같이 나온다.
applytechnicals는 각 코인의 ohlcv데이터를 바탕으로 보조지표를 넣는 부분이다. 여기서 이 글 초반에 말한 technical-analysis library가 사용된다. applytechnicals는 각 코인의 ohlcv데이터 프레임을 받아서 원하는 보조지표를 열로 추가해준다. 아래 예시에서는 100 지수 이동평균(EMA, Exponential Moving Average)과 200 지수 이동평균, MACD가 사용되었다.
이렇게 생성된 ohlcv와 보조지표를 사용할 것 만 각 코인의 df_master에 넣는다.
그 밑의 if 문은 혹시 잔고가 남아있으면 open position 하라는 건데....... 나중에 설명하겠다.
이렇게 df_master의 데이 터부에 데이터를 채워 넣었으면, 이 정보를 바탕으로 살지 말지를 결정하는 두 번째, decide() 함수를 설명할 차례다.
decide 도 get_coin_info와 마찬가지로 df_master에 저장된 코인들의 ticker를 기준으로 코드를 실행한다. 아래는 for 문 안에 대한 설명이다.
pyupbit.get_current_price는 티커를 넣으면 현재 가격을 알려주는 함수이다.
df_master에서 판단을 진행할 한 개의 코인 열 만 저장한다.
그 아래로 'open_position'이 1일 때와 아닐 때로 실행되는 if문이 있다. 이때 open_position은 해당 코인의 구매 여부를 알아보기 위해 사용한다. 예를 들어서 open_position이 1이 아닐 때, 그러니까 코인을 산 적이 없으면 그 아래로 구매 알고리즘이 적용되고, open_position에 1을 저장한다. 그러면 다음번 시간이 되어 다시 decide 함수가 실행되면 open_position이 1이기 때문에, 구매 알고리즘이 아니라 판매 알고리즘이 적용된다.
구매 알고리즘: 여기서부턴 get_coin_info에 나타난 정보를 바탕으로 살지 말지를 결정하는 부분이다. 예를 들면 위 사진에서는 '이전 봉 저가(low)가 200 ema보다 크고, 현재 봉 저가가 200 ema보다 크고, 이전 봉 MACD값이 0보다 작고 현재 봉 MACD값이 0보다 클 때' 사기로 되어 있다.
사기로 결정했다면, if문 안에서 df_master의 'buy'에 1을 저장한다. 그리고 현재 'current_close'에 살 때의 가격을 저장해놓고, stop_loss와 current_target을 저장한다. 그리고 open_position에 1을 저장하여 다음에 이미 코인을 산 상태에서 또 사지 않도록 한다.
코인 구매 알고리즘이 실행될 때 가장 중요한 것은, 물론 구매를 정하는 알고리즘 자체도 있겠지만, 적절한 stop_loss와 target을 설정하는 것이다. 100% 이기는 알고리즘은 없기 때문에, 생각한 것과 다르게 산 직후에 떨어지는 경우를 대비한 최소한의 안전장치를 마련해 놓아야 한다. 반대로 target은 얼마나 먹을 수 있을지에 관한 것인데...... 이견이 있겠지만 고정적인 비율로(예를 들면 지금 위 코드에서는 현재 가격의 1% 이익을 보면) 설정해놓는 것이 가장 좋다고 생각한다.
팔기로 결정하는 방법은 따라서 (위의 예에선) 두 가지가 있다. 첫 번째는 현재 가격이 stop_loss보다 작은 경우, 두 번째는 현재 가격이 target price보다 큰 경우이다.
물론 경우에 따라서는 이익을 최대화할 수 있게 구매 알고리즘에서 처럼 보조지표를 활용할 수도 있다.
이렇게 decide 함수로 df_master의 명령 부분을 채워 넣었다.
이제 다음은 실질적인 코인 주문을 넣어 줄 buy_or_sell 함수가 이어진다.
buy_or_sell 함수는 앞에서 소개한 다른 함수와 같이 df_master를 파라미터로 받지만, 그것과 같이 한 번에 얼마나 구매할 것인지를 가리키는 'money' 변수와 현재 시각을 전달하는 'clock'을 추가로 받는다.
모든 코인 목록에 대해 실행한다. df_master의 한 코인의 명령 부분 중 'buy'가 1일 때는 'money' 만큼 사고, 2일 때는 전량 판매하는 간단한 함수이다.
이때 업비트 정책 상, 한화 기준 5000원 이상만 구매가 가능하며, 팔 때도 한화 기준 주문 총액 5000원 이상일 때만 주문 가능하다. 따라서 현금 잔액이 5000원 이하라면 코드가 작동되지 않게 한다(사실 그냥 해도 되는 거 아닌가 싶기는 하다).
실제로 업비트에 주문을 넣는 함수는 upbit.buy_market_order(coin, money*0.9995)와 upbit.sell_market_order(coin, coin_balance) 부분이다. 이 부분이 실행되면 바로 매수/매도가 실행되므로 주의해야 한다......
매수 시에는 잔액만 있으면 매수가 진행되고, 업비트 수수료 0.05% 를 포함하여 주문해야 원하는 만큼 주문할 수 있다. 예를 들어서, 위의 예처럼 200,000원 코인 주문을 넣으면, 수수료를 포함하여 200,100원 이 실제로 주문에 들어간다. 따라서 잔액을 이쁘게 관리하고 싶다면 200,000*0.9995 만큼을 주문하여 200,000원으로 맞추는 게 좋다고 본다.
매도는 조금 더 복잡한데(사실 그리 복잡하지 않다) 업비트 api는 코인의 현재 가격 총합을 표시해주지 않는다. 업비트 앱에서 나오는 '보유 코인' 섹션에서 코인의 현재 총액을 표시해주는 것과 다르게 말이다. 주문 역시 코인 개수로만 받으므로, 주문을 위해선 df_master의 명령 부분의 'buy'가 2이고, '코인 개수'X'현재 가격' 이 5000을 넘을 때 시작하게 된다. 이를 위해서 upbit.get_balance() 함수로 잔액을 불러와야 한다.
각 매수/매도 주문 뒤에는 꼭 df_master 명령부의 'buy'를 0(혹은 1이나 2가 아닌 아무거나)으로 맞추어 다음 반복에서 주문이 실행되지 않도록 한다.
참고로 post_massage()와 print()는 다음 기회에 설명하도록 하겠다. 관리를 위한 trivia이다.
리뉴얼된 df_master를 리턴한다.
다음은 코드의 효율을 따지기 위한 업비트 계좌 총잔액을 지속적으로 저장하는 부분이다.
윗 그림은 반복되는 부분을 가져온 것이고, 아래의 get_total_balance()는 현재 잔고를 계산하는 코드이다.
get_total_balance() 안의 upbit.get_balances()는 바로 전 단계에서의 get_balance()와 달리 계좌 안의 모든 원화, 코인의 잔고를 딕셔너리 형태로 가져온다. 우리는 각 코인의 잔고 따위는 중요하지 않으므로, 그냥 몽땅 더해서 리턴할 생각이다.
데이터 프레임으로 가져온 잔고의 첫 번째 열은 'KRW' 즉 원화가 저장되어 있다. 나머지 잔고는 보유 코인 개수에 현재 가격을 곱해 코인의 잔액을 원화로 환산한다. 그리고 단순히 다 더해준다.
balance 변수에 시간과 현재 총잔액을 딕셔너리로 만들고, total_balance 데이터 프레임의 뒤에 붙여준 다음, 엑셀로 저장한다. 엑셀 결과는 다음과 같다.
이렇게, 코드를 구성하는 방식을 풀어보았다. 분명 뭔가 고칠 것도 많고 허점 투성이이며, 전혀 세련되지 않은 코드란 걸 알지만...... 나는 여기서 더 어떻게 할 수 있을지 모르겠다. 혹시 누군가 답을 알고 있으면 알려주시기 바랍니다.
다음에는 코드와 관련된 트리비아를 조금 풀고, 본격적으로 알고리즘 자체와, 어떻게 내가 실패했는지를 풀 예정이다.
아래는 코드 전문이다.