파이썬은 참 좋은 언어인데, C나 C#같이 컴파일 방식이 아니라 인터프리터 방식이다보니 실제 코드가 실행되는 순간에 문법적인 오류가 발생하면 죽는 문제가 생깁니다. 컴파일 방식이라면 실행하기 전에 인지할 수 있겠지만 파이썬은 쉽지가 않습니다. 이 문제를 해결하기 위해서는 수정한 부분을 실제 조건을 만족시켜 돌려봐야하는데요. 이것도 쉬운 것은 아닙니다. 실제 조건문이 만족하도록 설정하는 행위가 소스를 수정하는 것이기 때문에 이 또한 문제를 야기할 수 있습니다.

 

이런 문제를 풀기 위하여 보통 유닛테스트를 많이 사용하는데요. 유닛테스트를 전체 프로그램을 test할 수 있으면 수정시 발생하는 오류를 가장 빨리 발견할 수 있습니다. 하지만 소스 전체를 유닛테스트로 적용하기 위해서는  최초 설계부터 반영을 해야하기 때문에 쉽지 않습니다.

 

결론적으로 유닛테스트를 적용하면 좋은데, 전체 소스코드에 적용하기에는 좀 어렵다인데요. 대안으로 특정 함수만 별도로 유닛테스트한 후 사용하는 것입니다.

 

이번에 주식 자동매매 프로그램을 만들던 중 매매 가격에 따라 유효한 주문 가격을 결정하는 함수를 유닛테스트를 적용한 예를 설명하도록 하겠습니다. 특히 주식 자동매매에서 유효한 주문가격을 구하지 못하면 주문 거부가 발생하기 때문에 꼼꼼하게 test해보아야 합니다. 모든 case를 손으로 직접 test하는 것은 쉬운 일이 아니기 때문에 유닛테스트를 적용하는 것이 바람직 합니다. 특히 해당 함수 내부 소스가 변경되는 경우에는 유닛테스트의 진가가 발휘됩니다.

 

올해 변경된 주식 가격별 호가 단위입니다. 자동매매 코드에서는 소숫점 계산을 한 목표 매수가가 나오고 이를 버림/반올림/올림 등의 조건을 추가하여 호가 단위에 맞는 가격으로 변경해주는 함수가 필요합니다.

우선 아래 package를 import합니다.

 

import unittest

 

그리고 원하는 함수를 작성합니다. 일단 2천원 미만은 1원 단위의 가격을 돌려주는 함수를 만들어봅시다. 

 

def get_order_price(price) :
    tick_size = 0
    if price < 2000:
            tick_size = int(price)
    return tick_size


다음은 unittest를 사용하는 방법입니다. unittest에서는 아래와 같은 CustomTests class를 지원합니다. 원하는 함수안에 필요한 코드를 넣으면 됩니다. test case는 test_process_statement() 함수 안에서 작성하면 됩니다.

 

 

 

class CustomTests(unittest.TestCase): 
    def setUp(self):    # 무시
        pass
    def tearDown(self): # 무시
        pass
    def test_process_statement(self) :
        in_price = 45
        result = get_order_price(in_price)
        self.assertEqual(result, 45) # 계산 결과와 원하는 값 입력

 

위와 같이 test_process_statement() 함수 안에 test case를 만들면 됩니다. 그 후 unittest.main()을 호출하면 계산값과 원하는 결과가 틀리면 self.assertEqual함수에서 fail이 뜹니다.

 

unittest.main()

 

실행을 해보면 아래와 같은 결과 나옵니다.

 

 

만약 in_price와 다른 값을 입력한다면

 

    def test_process_statement(self) :
    	in_price = 45
        result = get_order_price(in_price)
        self.assertEqual(result, 44) # 계산 결과와 다른 값 입력

 

친절하게도 틀리다는 메세지가 출력됩니다.

 

 

다음은 주식 거래시 사용하는 실제 코드입니다. 이번 전략에서는 목표 가격이 오면 가능한 빨리 매수하기를 원하기 때문에 버림을 적용합니다. float를 int로 변경하기만 하면 자동으로 버림이 적용되기 때문에 간단하게 구현할 수 있습니다.

 

def get_order_price(price):
    tick_size = 0.0
    if price >= 500000:
        tick_size = int(price/1000)*1000
    elif price >= 100000:
        tick_size = int(price/500)*500
    elif price >= 50000:
        tick_size = int(price/100)*100
    elif price >= 10000:
        tick_size = int(price/50)*50
    elif price >= 5000.0:
        tick_size = int(price/10)*10
    elif price >= 1000.0:
        tick_size = int(price / 5)*5
    else:
        tick_size = int(price)
    return tick_size

 

위 코드를 바탕으로 test case를 만들어보면 아래와 같습니다. 이외에도 모든 가격대를 test하는 문장을 추가하여야합니다.

 

    def test_process_statement(self) :
        result = get_order_price(999.99) # 1000원 미만 test
        self.assertEqual(result, 999)

        result = get_order_price(1000.01) # 1000원 이상 test
        self.assertEqual(result, 1000)

        result = get_order_price(4999.9999) # 5000원 이하 test
        self.assertEqual(result, 4995)

        result = get_order_price(9999.9999) # 10000원 이하 test
        self.assertEqual(result, 9990)

 

 

실행결과는 아래와 같습니다. 즉 문제가 없다는 의미입니다.

 

 

만약 함수의 출력과 예상결과가  틀린 경우에는 오류 메세지가 뜹니다. 오류 메세지가 뜨면 해당 가격에 대한 코드를 자세하게 살펴보면서 문제점을 수정하면 됩니다.

 

 

이렇게 완성된 함수를 본 프로그램에 추가하여 개발을 진행하면 됩니다. 모든 함수를 유닛테스트하기에는 시간이 많이 걸리는 문제가 있으므로, case가 복잡하거나 경우의 수가 많은 함수를 만드는 경우에는 해당 함수만 뽑아서 유닛테스트 환경하에서 test하는 습관을 들이는 것이 좋을 듯 합니다. 당장은 힘들겠지만 시간이 가면 갈수록 코드의 안정성을 유지하는 것이 유닛테스트 통과한 코드입니다.

 

반응형

설정

트랙백

댓글

일전에 쓴 grid trading을 기반으로 주식 자동 매매를 돌리고 있습니다.

https://money-expert.tistory.com/74?category=781442 

 

[시스템트레이딩] 주식 grid trading

현재 upbit에서 일부 암호화폐에 대하여 grid trading 중인데요. 매매 대상을 넓히는 것이 수익률에 좋을 것 같아서 ebest api로 주식도 함께 거래하도록 개발을 하였습니다. 이전에 만든 소스가 있어서

money-expert.tistory.com

 

지난 6월 중순부터 계속 돌리고 있는데요.

중간 점검 차 현재 상태를 올려봅니다.

 

6월은 주가가 계속 내려가는 시기여서 평가손실이 발생하였으나 7월에는 매매가 어느 정도 이루어지면서 수익이 났습니다. 결과적으로 두달에 약 1.7% 수익이 발생하였습니다.

 

 

6/23일 부터는 kodex200도 매매 종목에 추가하였습니다. 그 이유는 거래세가 없기 때문에 grid를 좁게 가져가도 수익이 발생할 수 있기 때문입니다. 또한 kospi가 급등락을 반복하고 있어서 grid grading하기 좋은 종목으로 판단이 되었습니다.

 

현재 보유 종목은 총 27개이며 적당히 +- 상태를 유지하고 있습니다.

grid trading에서 수익이 많이 나오려면 매매가 많아야 합니다. 오늘까지 총 103회 매도가 되었습니다.

1회 투자금이 약 33만원. grid는 3.3%이므로 대략 1백여만원 매매 수익이 발생하였습니다.

현재 잔고 상태는 - 이지만 매매 차익 덕분에 +로 전환한 상태입니다. 

 

간단한 소회입니다.

- 종목 선택이 역시 중요하다. 하지만 영원히 하락하는 종목은 별로 없다. 즉 적절히 빠진 시점에 grid trading을 시작하면 결국 수익이 날 수 있다.

- 계속 빠지는 경우도 많다. 손실을 제한하기 위하며 grid 수를 제한한다. 최대로 살 수 있는 grid 수를 제한함으로써 지속적으로 우하향하는 경우에 손실을 제한할 수 있다. 현재는 grid 3개로 제한함(약 -10%)

- 매수 시작한 시점 이후 바로 상승하여 매수를 못한 경우도 있고, 한번 정도 매매하고 우상향한 경우도 있다. 이런 건 어쩔 수 없을 듯.

- 매수 종목이 많아야 매매가 자주 발생하는데, 종목을 늘리기 위해서는 투자금이 많이 들어간다. 적절한 투자 규모를 정하는 것이 필요하다. 증거금 관리가 중요한데 40% 증거금이 필요한 종목 위주로 포트폴리오를 구축하는 것도 좋다.

 

지금까지 운용해본 경험상 적당히 빠진 시총 높은 종목으로 grid trading을 하면 연 10% 정도는 나올 듯 합니다. 

 

한달에 한번씩 투자 상황을 정리해서 공유하겠습니다.

 

반응형

설정

트랙백

댓글

 

현재 upbit에서 일부 암호화폐에 대하여 grid trading 중인데요.

매매 대상을 넓히는 것이 수익률에 좋을 것 같아서 ebest api로 주식도 함께 거래하도록 개발을 하였습니다.

이전에 만든 소스가 있어서 참고를 하려고 했는데, 너무 옛날에 만들 소스라서 그동안 개선한 사항들 update가 되어 있지 않아서 upbit에서 돌리는 소스를 기반으로 ebest에서도 동작할 수 있도록 수정하였습니다.

 

upbit api와 ebest api는 사용방법이 틀리기 때문에 upbit api 기반으로 ebest api를 변경한 wrapper를 하나 만들었습니다.

 

구조적으로 보면 아래 형태를 띕니다.

 

 

 

이건 class 개념을 알아야 사용할 수 있는데요. 일단 class를 안다고 가정합니다.

 

Exchange라고 하는 base class를 선언하고 Exchange를 부모 class로 갖는 Ebest, Upbit을 선언합니다. 그리고 관련 함수를 추가합니다.

 

이렇게 한 후 EBest를 사용하는 경우에는 EBEST라는 변수를 1로 설정하고 돌리면 전략 관련 코드는 수정할 필요없이 ebest용 api가 동작합니다. 전략 코드 내부에서는 exchange라고 하는 base class에 있는 함수만 사용하기 때문에 거래소가 어디인지 구분할 필요가 없습니다.

EBEST = 1
class Exchange:
   :
   :

class EBest(Exchange):
   :
   :

class Upbit(Exchange):
   :
   :

exchange = None
if EBEST :
   ebest = EBest()
   exchange = ebest
else :
   upbit = Upbit()
   exchange = upbit

do_trading(exchange)
 

 

특정 거래소만 사용하는 경우에는 이런 개념을 적용할 필요는 없습니다. 하지만 암호화폐 거래소도 거래량도 많고 변동성도 좋기 때문에 좋은 전략이 있다면 암호화폐 거래소에도 적용하지 않을 이유가 없습니다.

 

전략 자체가 워낙 간단하기 때문에 특별하게 언급할 필요는 없습니다만, 거래 여부를 확인하는 방법에 대하여 간단하게 기술합니다.

 

ebest api의 경우에는 거래가 발생하면 callback이 날아옵니다. 따라서 거래 여부를 실시간으로 확인이 가능합니다. 하지만 제가 사용한 원 소스가 upbit 기반이다 보니 거래 callback을 사용하지 않고 주기적으로 미체결 주문을 확인하여 거래 여부를 판단합니다.

 

코드 내부적을 관리해야할 정보입니다.

- 이전 주문 기록

- 거래가 발생한 경우 다음 주문 가격 결정

- 더 이상 매수 주문이 나가지 않는 가격 설정 (하락시 최소 매수가)

- 매도 후 다음 매수 주문을 낼 때 더 이상 매수 주문이 나가지 않는 가격 설정(상승시 최대 매수가)

 

이를 바탕으로 로직의 개략적인 흐름입니다.

 

초기화과정
while(1) :
  미체결 주문을 받는다.
  if 이전 주문과 차이가 있는가? :
       매수인지 매도인지 구분하여 다음 주문을 낸다.
 

흐름은 간단하지만 중간 중간 점검해야할 사항이 조금 많습니다. 흐름도에서는 단순히 "주문을 낸다" 이지만 주문시 이런 저런 조건이 있기 때문에 모두 check하고 주문을 내야할 상황인 경우에만 주문을 내야합니다. 이런 부분이 코드의 완성도를 좌우하며 조건에 따라 난이도가 틀립니다.

 

 

주말에 관련 개발을 하면서 발생했던 문제점들을 정리합니다. 향후 이와 유사한 전략을 개발하실 때 반드시 고려하시기 바랍니다.

 

1. 어떤 이유이건 매수 혹은 매도 주문이 실패할 수 있습니다. 이런 경우에 중복으로 주문이 나갈 수 있으므로, 이에 대한 대처 필요. 즉 주문을 하기 전에 동일한 주문이 있는지 반드시 확인

2. 가격에 따라 1 tick이 틀리다. 특히 1 tick이 변하는 구간에서는 주의해야한다. 예를들어 100,000원 이하는 100원 그 이상은 500원 단위

3. 장 종료시 일부만 거래가 된 경우에 다음날 거래시 반영(이 부분은 코드 난이도가 높기 때문에 수동으로 처리)

 

오늘 장이 급락하였을 때 일부 종목 매수하여 자동매매 시작하였습니다. 오늘 매도된 종목은 없고, 내일부터는 매수/매도가 반복될 듯 합니다.

 

grid trading은 박스권 중간 위치에서 매수를 시작하는 것이 좋습니다. 혹은 바닥에서 수량을 mutiple로 매수하고 시작하여도 되고요.

 

마지막으로 희망(?) 수익률도 정리해봅니다.

 

3.25% 구간으로 grid를 만든다고 가정합니다. 0.25%는 거래세. 따라서 실제 수익은 3% 정도이며, 한번 매매될 때 마다 9,900원 수익입니다. 2-3번 매매되면 치킨 한마리군요.

매매가 많이 될 수록 수익이 쌓이는 구조입니다. 이틀에 한번씩만 거래가 되어도 연 10%

 

물론 그리드 하방을 돌파하여 손실이 난 종목도 있겠죠?

보수적으로(?) 반은 손실이라고 가정하고 평균 20% 손실이면 -1백만원.. 이틀에 한번만 거래가 되면 even은 되겠군요.

 

결국 하방이 어느정도 막혀있는 박스권 종목을 잘 찾는 다면 좋은 전략이 될 듯 합니다.

아마도 지주사들, 금융사들, 고배당주들 정도?

 

 

반응형

설정

트랙백

댓글

개발은 파이썬을 이용하도록 하겠습니다. 관련 예제가 블로그에 많이 있습니다. dev center에 있는 예제와 블로그들을 참고하였습니다.

개발 IDE로는 spyder나 Visual Studio Code 등 본인이 익숙한 것을 사용하면 됩니다.

전 기존에 visual studio를 사용하던 습관이 있어서 Visual Studio Code를 사용하기로 했습니다.

이베스트 xingApi와 통신을 위해서는 win32com을 사용합니다. 이를 위하여 win32com을 설치해야하는데요. 아나콘다32를 설치하면 일반적으로 함께 설치가 됩니다. 혹시라도 win32com을 찾을 수 없다고 나오면 아래와 같은 명령어로 설치하시면 됩니다.

pip install pywin32

우선 login 과정을 보도록 하겠습니다.

그 절차는 다음과 같습니다.

  1. 로그인에 관련된 정보를 입력한 후 xing api에게 login 명령어를 보냅니다.
  2. xingApi에서 답이 오기를 기다립니다.
  3. 등록한 callback 함수로 login 결과가 옵니다. login 성공 여부를 여기에서 확인합니다.

이베스트 주식 거래를 위한 class를 만들었습니다.

앞으로 선물거래 그리고 암호화폐 거래까지 포함하기 위하여 거래소 별로 class를 만들어 보도록 하겠습니다.

거래소별 base class인 Exchange를 기반으로 이베스트주식 거래 class입니다.

class XASessionEventHandler:  
    login_state = 0

def OnLogin(self, code, msg):  
    print('on login start')  
    if code == "0000":  
        print("login succ")  
        XASessionEventHandler.login\_state = 1  
    else:  
        print("login fail")  
def wait_for_event(code) :
    while XAQueryEventHandler.query_state == 0:
        pythoncom.PumpWaitingMessages()
    if XAQueryEventHandler.query_code != code :
        print('diff code : wish(',code,')', XAQueryEventHandler.query_code)
        return 0
    XAQueryEventHandler.query_state = 0
    XAQueryEventHandler.query_code = ''
    return 1
class EBestStock(Exchange):  
    def __init__(self):
        self.operation_begin = '085000'
        self.operation_end = '153000'

    def login(self, server, id, pwd, cer_pwd, acc, acc_pwd) :
        self.instXASession = win32com.client.DispatchWithEvents("XA_Session.XASession", XASessionEventHandler)
        self.id = id
        self.passwd = pwd
        self.cert_passwd = cer_pwd
        self.account_number = acc
        self.account_pwd = acc_pwd
        self.instXASession.ConnectServer(server, 20001)
        self.instXASession.Login(self.id, self.passwd, self.cert_passwd, 0, 0)
        while XASessionEventHandler.login_state == 0:
            pythoncom.PumpWaitingMessages()

        self.login = XASessionEventHandler.login_state
        return self.login        
if __name__ == "__main__":
    print('\\nebest testing')

server = "hts.ebestsec.co.kr"  # or "demo.ebestsec.co.kr" 모의투자
id = "user id"
passwd = "user password"
cert_passwd = "공인인증서암호"
account_number = "계좌번호"
account_pwd = "계좌비밀번호"

ebest_st = EBestStock()
ret = ebest_st.login(server, id, passwd, cert_passwd, account_number, account_pwd)
if ret == 0 :
    print('fail to login')
    quit(0)

print('login ok')  

실행 결과입니다. 계정 정보를 정확하게 입력하였다면 login 성공할 것입니다.

 

 

반응형

설정

트랙백

댓글