기존 [시스템트레이딩] API를 이용한 자동매매(3) 프로그램은 ticker 하나를 매매하는 예제입니다. ticker 여러 개를 동시에 매매하는 예제에 대한 요청이 있어서 프로그램 업그래이드를 하였습니다.

 

기존 프로그램에서는 ticker 하나를 매매하므로 Trader() 클래스 하나만 사용하였지만 ticker 여러 개를 동시에 매매를 하기 위해서는 Trader()를 관리하는 class가 추가로 필요합니다. 이를 위하여 TraderMgr() 클래스를 추가하였습니다.

 

그리고 simultation을 위하여 사용하는 가상 exchange class를 수정하였습니다. 기존 가상거래소 class는 매매 기능만 있었으나, 특정 거래소의 현재가를 받아서 가상 매매를 하는 방식으로 수정하였습니다.

 

만약 upbit이 아닌 다른 거래소 시세를 이용하여 가상 매매를 한다면 아래 코드에서 해당하는 거래소로 변경하면 됩니다.

class DummyExchange(Exchange):
    def __init__(self, exchange_name, access, secret):
        super().__init__()
        self.type = ''
        self.name = 'dummy'
        self.holding = []
        if exchange_name == 'upbit' :
           self.exchange = MyUpbit(access, secret)

        self.seq_uuid = int(time.time())

 

여러 ticker를 동시에 매매하기 위하여 수정한 부분에 대하여 설명합니다.

 

우선 사용할 거래소를 생성합니다. 매매 로직에 대한 test를 먼저하여야 하므로 일단은 DummyExchange를 사용합니다. logic test가 끝나면 DummyExchange를 지우고 upbit 클래스를 사용하시면 됩니다.

#    upbit = MyUpbit(access, secret)
    upbit = DummyExchange('upbit', access, secret)

이 프로그램에서는 4개의 trading 변수를 사용하지만, 일반적으로 더 많은 변수를 사용합니다. 변수가 많아질수록 class에 넘겨주는 코드가 길어집니다. 그래서 tr_param라는 class를 만들었습니다. 변수가 추가될 때 마다 tr_params class에 추가하면 됩니다.

 

    # trading용 parameter 설정
    # 모든 ticker에 대하여 같은 설정 사용. 만약 ticker별로 별도 설정 값이 필요한 경우에는 ticker별로 설정 필요
    buy_perc = 0.03  # 시작가 대비 3% 오르면 매수
    sell_perc = 0.01 # 매수가 대비 1% 오르면 매도(익절)
    losscut = 0.01   # 매수가 대비 1% 내리면 losscut 매도(손절)
    seed_for_each_ticker = 10000
    
    tr_param = tr_params(buy_perc, sell_perc, losscut, seed_for_each_ticker)

trading에 사용할 ticker를 전달하는 방법은 여러가지가 있습니다. list 변수에 하나씩 넣어도 되지만, ticker가 많은 경우에는 부담이 갑니다. 따라서 별도 파일을 만들어서 원하는 ticker를 기록하도록 기능 추가하였습니다.

 

현재 거래할 ticker 명은  tr_tickers.txt 파일에 저장되어 있습니다. 

형태는 단순한 list입니다. 거래하고자하는 ticker를 추가/삭제하시면 됩니다.

 

[
	"KRW-MANA",
	"KRW-ANKR",
	"KRW-NPXS",
	"KRW-OBSR"
]

trader를 모아놓은 TraderMgr를 생성할 때 필요한 인자를 전달해주고, 매매를 위한 준비를 하는 함수인 prepare_to_start() 함수를 호출합니다. 이 함수에서는 ticker의 시작가, trading logic 등등 생성합니다.

    fname = '.\\tr_tickers.txt' 
    trader_mgr = TraderMgr(upbit, fname, tr_param)
    trader_mgr.prepare_to_start()

이번 예제에서는 REST 방식으로 10초에 한번씩 시세를 받아서 매매하는 방식으로 개발을 하였습니다.

 

        # 10초에 한번씩 최근 거래 값을 받아서 자동매매
        while(1) :
            trader_mgr.do_trading()
            time.sleep(10)

 websocket을 사용하는 경우에도 비슷한 방식으로 개발이 가능합니다.

 

새로 추가한 함수인 TraderMgr class의 do_trading()은 아래와 같습니다. trading할 ticker들의 현재 가격을 얻은 후 각 ticker별 trader에게 시세를 전달하여 매매 여부를 처리하도록 합니다. 기존 프로그램과의 차이는 main에서 특정 ticker용 trader의 do_trading() 함수를 부르는 부분이 TraderMgr class에 등록된 ticker용 trader를 찿아서 호출하는 방식으로 변경된 것입니다.

 

    # 주기적으로 call된다. 현재가를 읽어서 trading여부를 판단한다.
    def do_trading(self) :
        # trading에 사용될 tickers에 대하여 현재 가격을 받는다.
        prices = self.exchange.get_cur_price_all(self.int_tickers)
        if 'error' in prices[0]:
            print('could not get prepare_to_start::get_cur_price_all()', prices[0]['error']['message'], self.int_tickers)
            return

        if len(prices[0]) != len(self.int_tickers) :
            print("abnormal, ticker cnt is not the same ", len(prices), len(self.int_tickers))

        for ticker in self.traders :
            trader = self.traders[ticker]
            trader.do_trading(prices[0][ticker])  # 각 ticker용 trader에게 현재 시세 전달, trading 여부 처리

빠른 결과 확인을 위하여 매수 후 매수가 대비 1% 이상 오르면 익절, 매수 가격 대비 1% 이상 내리면 손절하도록 설정하였습니다.

 

    buy_perc = 0.03  # 시작가 대비 3% 오르면 매수
    sell_perc = 0.01 # 매수가 대비 1% 오르면 매도(익절)
    losscut = 0.01   # 매수가 대비 1% 내리면 losscut 매도(손절)

 

upbit 거래소 시세를 이용한 가상거래소에서 자동 매매 동작 결과입니다. 정상적으로 동작하고 있습니다. 실 거래소에서 매매는 아직 확인해보지 않았습니다. 익절 혹은 손절 후 재 매매하기 위해서는 매매 조건을 정교하게 정의하여야 합니다. 다. 자동 재매매를 위하여 매매 조건에 대하여 고민을 한 후 관련 기능도 update하도록 하겠습니다.

 

 

관련 소스코드는 아래 github에 저장되어 있습니다.

github.com/multizone-quant/system-trading-crypto-multi

반응형

설정

트랙백

댓글

BTC 불장으로 암호화폐 시장이 활황입니다. 이 흐름이 알트 코인으로 넘어오지는 않았지만 100% 넘게 오르는 코인도 자주나옵니다.

 

 

이런 종목이 오르기 시작하는 시점에 잡아서 쭉~~ 들고 있으면 수익이 많이 나겠죠.

 

암호화폐 종목은 특이하게도 아침 9시 이후에 급등하는 경우가 많습니다.

 

그렇다면 아침 9시 근처에 체결정보를 보다가 급등하는 종목이 나오면 냉큼 매수하면 대박이 나는 종목을 모두 매수할 수 있을 것입니다. 물론 오르다 마는 종목도 있을테니 손절 종목도 나오겠지만 기프트 같이 100% 오르는 종목에서 손해를 만회할 수 있겠죠.

 

이런 전략을 개발하기 위해서는 과거 tick 정보를 가져와서 시뮬레이션을 해봐야 합니다.

이번에는 과거 tick 정보를 연속으로 가져오는 방법을 기술합니다.

 

업빗에서 tick 정보를 가져오는 url은 아래와 같은 형식을 가지고 있습니다.

api.upbit.com/v1/trades/ticks?market=KRW-BTC&count=500&daysAgo=0&to=10:00:00

 

받은 데이터의 형태는 아래와 같습니다.

 

기존 과거 candle 정보와는 형식이 틀립니다. 각 인자는 아래와 같은 의미를 가지고 있습니다.

 

- dayAgo : 오늘 기준으로 며칠 전 (0: 오늘 1 : 어제 최대 7일 전 )

- to : 몇시 까지 가져올지 결정 (형식 : HH:mm:ss)

- count : 최대 tick 수 (최대 : 500)

 

tick 자료를 연속으로 가져올 때 주의할 사항은 한번에 받는 데이터의 크기가 500개로 제한되므로, 제일 마지막 sec 자료는 모두 받지 못하는 경우가 있습니다. 따라서 연속으로 tick 정보를 받아서 합치는 경우에는 이 점을 고려해서 코딩을 해야 합니다.

 

또한 tick 데이터는 utc 기준으로 일자를 계산합니다. 따라서 dayAgo 값이 0이면 오늘 tick 값을 받을 수 있는데, utc 기준임을 꼭 염두에 두시고 처리하시기 바랍니다.

 

프로그램을 실행시킬 때 아래 인자를 수정하면 됩니다. 

 

utc 기준으로 오늘 08시 부터 09시 BTC tick 데이터를 얻는 방법입니다.

COIN            = 'KRW-BTC'     # 원하는 코인 정보
TICK_AGO        = 0             # tick정보는 ago를 to 개념으로 봐야함 ( 0: 오늘 1 : 어제 최대 7일 전 )
TICK_FROM       = '08:00:00'    # HH:mm:ss  (utc 기준임)  or None (00:00:00)
TICK_TO         = '09:00:00'    # HH:mm:ss  (utc 기준임)  or None (최근)

 

TICK_AGO 값이 0이면 오늘입니다. 최대 7일 전까지만 검색이 가능하므로, 1주일에 한 번씩은 tick 데이터를 받아야 오랜 기간 데이터를 확보할 수 있습니다. 

 

최근 시간까지 tick 정보를 원하면 TICK_TO 값을 None으로, 0시까지 tick 정보를 원하면 TICK_FROM 값을 None으로 하면 됩니다.

 

파이썬 소스코드는 아래 github에 있습니다.

github.com/multizone-quant/System_trading_ex/blob/main/get_upbit_ticks.py

 

반응형

설정

트랙백

댓글

 

이전 분봉 가져오기 글에서 설명한 코드를 정리해서 github에 올립니다.

github.com/multizone-quant/System_trading_ex/blob/main/get_upbit_candles.py

 

multizone-quant/System_trading_ex

Contribute to multizone-quant/System_trading_ex development by creating an account on GitHub.

github.com

 

과거 데이터 중 tick과 candle 데이터는 형태가 틀립니다. 따라서 이 데이터를 가져오는 방식도 조금 차이가 있는데요. 기존에는 한 프로그램으로 tick과 candle을 모두 처리하다보니 조금 복잡한 것 같아서 우선 candle만 가져오는 프로그램으로 분리를 했습니다. 

 

동작방식은 아래와 같습니다.

 

우선 분/일봉 데이터를 최대한 가져옵니다. 여기에 from 보다 과거 candle이 있으면 지운 후 파일에 저장합니다. 그 반대로 아직 받아야할 candle이 더 있으면 to 값을 변경하여 다시 받는 것을 반복합니다.

 

이렇게 만들어진 파일을 한 파일로 합칩니다.

프로그램 동작시킬 때 필요한 변수 값들입니다.

CANDLE_TYPE     = 'min' # 'min' or 'day'
CANDLE_INTERVAL = 1     # 1, 3,5,10,30,60

COIN            = 'KRW-HUNT'              # 원하는 코인 정보
FROM            = '2021-01-11 20:00:00'   # 특정 일자부터 받고 싶을 때 : '2021-01-12 10:00:00' (KST임)
TO              = None                    # None:최근시간부터, 특정시간 : '2021-01-12 10:00:00' (KST임)

 

아직 TO 값을 변경하면서 test하지 않았기 때문에 최근 candle 부터 FROM까지 분/일봉 데이터를 받을 수 있습니다.

 

TO 값을 입력하여도 정상동작하도록 수정하였습니다. 기존에는 타임존까지 표시하게 하였으나, 그냥 타임존은 생략하도록 (KST기준임) 수정하였습니다. 직관적으로 TO 값을 입력할 수 있도록 TO 값도 KST 기준으로 입력하도록 수정하였습니다. 이때 TO보다 작은 값까지 저장합니다.

 

만약 하루치를 원하는 경우에는 아래와 같이 입력하시면 됩니다.

 

 

 

 

----------

 

버그 수정

 

2021/1/12 오후 3시 이후로 to 값을 변경하면서 과거 데이터를 모두 가져오는 기능이 동작하지 않습니다. 참고하세요.

 

연속가져오기에 들어가는 to 시간값은 utc 시간입니다. 따라서 next_to 부분에 들어가는 시간값을 utc 값으로 수정하니 잘 동작합니다.

 

그리고 일부 데이터가 빠져있는 문제는 아래와 같이 timestamp 값이 None인 구간이 있어서 발생한 문제입니다. 따라서 sort 할때 key값으로 KST 값을 이용하여 문제를 해결하였습니다. 

 

 

 

결론적으로 이제 잘 동작합니다.

반응형

설정

트랙백

댓글

개발하기 좋은 추운 주말입니다. 이럴때는 조용이 집에서 개발에 집중할 시간을 벌 수 있어서 좋은 것 같습니다.

 

websocket을 이용한 자동 매매 예제에서 매매 로직의 시초가를 입력을 하였었는데요. test 용으로는 문제가 없지만 제대로 전략을 돌리려면 본인이 원하는 시작값을 설정할 수 있어야 합니다.

 

오늘 소개할 방법은 오늘 시초가를 로직의 시작가로 설정하는 방법입니다. 오늘 시초가를 받아오는 방법은 크게 두가지가 있습니다. 하나는 오늘 일봉을 읽는 것이고, 두 번째는 현재 시세를 받는 것입니다. 현재 시세를 받으면 오늘 시초가가 들어있습니다. 

 

그리고 websocket의 경우에 이유는 정확하지 않지만 연결이 끊어지는 경우가 종종 있습니다. 또한 매매할 coin이 많으면 websocket으로 모든 코인의 실시간 체결 데이터를 받기에 부담이 생깁니다. 따라서 전 종목을 대상으로 매매할 종목을 찾는 경우에는 현실적으로 현재 시초가를 받아서 처리하는 것이 대안이 될 수 있습니다.

 

그래서 이번에는  현재 시세를 받아서 매매하는 방법도 함께 소개하도록 하겠습니다.

 

우선 현재 시세를 받아오는 방법입니다. 해당 함수는 get_cur_price_all() 입니다. 입력은 원하는 암호화폐명이 기술된 list입니다. upbit에 등록된 모든 암호화폐 코드를 입력하면 전 종목 시세를 한번에 얻어올 수 있습니다. return되는 형태는 list이며 list의 첫 번째 항목에 dict 형태로 저장되어 있습니다. dict의 key는 암호화폐명입니다. return되는 값은 아주 다양한 정보가 들어있습니다. 그 중 시작가는 'opening_price'입니다.

 

이렇게 받은 시작가격을 로직의 시작가격으로 설정하면 됩니다.

 

    ticker = 'KRW-SBD'	
	info = upbit.get_cur_price_all([ticker])
    if 'error' not in info[0] :
        start_price = info[0][ticker]['opening_price']
        
    tr_logic.set_start_price(start_price)        

다음으로는 주기적으로 현재 시세를 받아서 기존 자동매매 프로그램을 작동시키는 방법입니다.

 

앞에서 설명한 함수인 get_cur_price_all()를 사용하면 됩니다. get_cur_price_all()에서 돌려주는 자료의 형태는 websocket에서 돌려주는 형식과 틀립니다. 하지만 기존 trader를 수정하지 않고 사용하기 위하여 websocket을 사용하던 형태로 변환하여 사용하면 됩니다. 이를 위하여 만든 함수가 make_info_from_upbit_tickers()입니다. 나머지 부분은 수정할 필요없습니다. 이렇게 거래 부분은 시세와 독립적으로 개발을 하면 향후 다른 거래소 혹은 다른 방식으로 거래하는 경우에도 수정할 부분을 최소화할 수 있습니다.

 

지금 예제는 코인 하나에 대하여 동작하지만, 복수개의 코인에 대하여 동작하도록 수정할 수 있습니다. 

방법은 원하는 코인에 대하여 trader를 여러 개 만들어서 list를 만듭니다. 원하는 코인 리스트를 만든 후 get_cur_price_all()에 해당 코인명을 넣으면 됩니다. 

    USING_WEBSOCKET = 0
    # websocket 실간 시세를 이용하여 자동매매하기
    if USING_WEBSOCKET :
    	:
        :
    else :
        # 10초에 한번씩 최근 거래 값을 받아서 자동매매
        target_coins = [ticker] 
        while(1) :
            prices = upbit.get_cur_price_all(target_coins) # 원하는 코인을 list로 넣는다.
            if 'error' not in prices :
                ticker = target_coins[0]
                info = make_info_from_upbit_tickers(prices[0][ticker])
                trader.do_trading(info)

            time.sleep(10)
        

 

다음에는 ebest api를 추가하는 방법에 대하여 기술하도록 하겠습니다. 증권사 API는 암호화폐 API와는 많이 틀립니다. 하지만 매매에서 사용하는 api는 몇 개 없으므로 기존 pyupbit의 api와 같은 형태로 부를 수 있도록 개발할 예정입니다. 이렇게 되면 기존 프로그램에서 아래에 있는 거래소 생성하는 부분만 변경하면 됩니다. 거래소와 관련된 부분을 독립하여 개발을 하면 향후 다른 거래소를 추가하는 경우에도 수정할 부분을 최소화할 수 있습니다.

 

    upbit = MyUpbit(access, secret)
반응형

설정

트랙백

댓글

upbit에서 주는 실시간 데이터를 받는 방법은 아래 글에 기술해 놓았습니다. 여기에 나오는 함수를 사용할 예정입니다.

https://money-expert.tistory.com/32?category=757693 

 

실시간 거래를 위하여 기존 시뮬레이터를 다시 refactoring해봅니다. 프로그램 내용을 최대한 간단하게 만들기 위하여 로그관련된 부분은 과감하게 삭제하고, 매매 로직에서도 클래스 하나로 구현해보았습니다.

 

최대한 간단한 구조로 만들었으며, 로직도 래리윌리암스가 아닌 아래와 같이 아주 간단한 로직으로 개발하였습니다.

 

매매로직 :

매수 :

   기준가 대비 x% 오르면 매수

매도 :

    1) 익절 : 매수가 대비 y% 오르면 매도

    2) 손절 : 매수가 대비 z% 내리면 매도 

 

 

우선 Trader()라는 class를 하나 추가했습니다.

Trader 클래스에서는 실제 매매를 담당합니다.

Trader의 간단한 동작 방식은 아래와 같습니다.

 

 

실시간 거래 정보가 들어온다.

if 매수 대기 중이면 :

   if 현재 가격이 매수 조건이면 :

        매수주문

else 매도 대기 중이면 :

   if 현재 가격이 매도 조건이면 :

        매도주문

        매수 대기중으로 변경

  

코딩할 내용은 단순하지만 이런 저런 조건문이 많이 붙기 때문에 실제 코드는 약간 복잡합니다. 예를들어 매수할 수량을 정하는 부분에서도 암호화폐의 특성상 소숫점 이하까지 주문을 할 수 있습니다. 그렇다고 소숫점 10자리까지 주문을 할 수 없으니 매수할 수량에 따라서 적절한 수량을 계산해야합니다. 아래와 같은 방식으로요. 이런식으로 매매 흐름과는 관계없이 세세하게 코딩하여야 하는 부분이 나오면 생각외로 시간이 많이 걸리게 됩니다.

 

                # 살 수량 결정
                buying_vol = amount / buy_price    # 매수 주수
                if buying_vol > 10 :    #10개 보다 크면 소숫점 0
                    buying_vol = int(buying_vol)
                elif buying_vol > 0 :   #0개 보다 크면 소숫점 2
                    buying_vol = float(format(buying_vol,'.2f'))
                else :                  #0개 적으면 크면 소숫점 4
                    buying_vol = float(format(buying_vol,'.4f'))

 다음은 프로그램을 시작하는 부분입니다.

우선 거래소를 생성합니다. 여기에서는 upbit 거래소를 사용합니다. 이때 본인의 키로 변경하셔야 합니다.

사용할 로직도 생성합니다. 일반적으로 candle 정보를 사용하기 때문에 인자로 넘겨주지만 본 예제에서는 candle은 사용하지 않습니다. 대신에 로직에서 사용할 매수기준, 매도기준, 손절기준을 %를 logic에 설정을 합니다.

 

다음으로는 로직에서 사용할 시작가격을 설정합니다. 일반적으로는 candle 정보를 읽어서 적절한 값을 설정하거나, 아니면 일정 기간 돌리면서 candle 정보를 쌓아나가야 합니다. 이 부분도 일단은 간단하게 입력하도록 하였습니다.

 

이렇게 필요한 class를 만들고 변수 값을 설정한 후 Trader를 생성하면 됩니다. 

    access = 'my acess'  # 본인의 access 키값
    secret = 'my secret' # 본인의 secret 키값
    
    upbit = MyUpbit(access, secret)
    # 상승 따라가기
    tr_logic = TR_FOLLOW_TREND('min', 1) # candle정보는 무시

    ticker = 'KRW-LBC'
    seed = 1000
    buy_perc = 0.03  # 시작가 대비 3% 오르면 매수
    sell_perc = 0.01 # 매수가 대비 10% 오르면 매도(익절)
    losscut = 0.03   # 매수가 대비 losscut 내리면 매도(손절)
    tr_logic.init_set(buy_perc, sell_perc, losscut)

    start_price = 102 # 시작가, 로직에 따라 시초가일 수도 있고, 이전 30분 candle의 open 가격일 수 있음.
    tr_logic.set_start_price(start_price)

    trader = Trader(upbit, ticker, tr_logic, seed)

 

다음에는 websocket를 설정하는 부분입니다. myconnect() 함수에 앞에서 만든 trader와 관련 정보를 설정하면 됩니다. 만약 받은 실시간 정보를 출력하고 싶으면 display_web_socket_recv 값을 1로 하면 됩니다. 체결 정보가 너무 많으면 시스템이 느려질 수 있으므로 정상적으로 동작하는지 확인할 때는 1로 설정하고, 그 후에는 0으로 하는 것을 권합니다. 막상 돌려보니 연결이 끊어지는 경우가 빈번하더군요. 따라서 접속이 끝어지면 잠시 쉬었다가 다시 연결하는 방식으로 코딩을 했습니다.

 

    display_web_socket_recv = 0
    while(1) :
        try :
            asyncio.get_event_loop().run_until_complete(my_connect(trader, ticker, display_web_socket_recv))
        except Exception as x: 
            print('websocket error : ', x)

        time.sleep(10)

 


이제 체결 정보를 실시간으로 받을 수 있습니다. upbit에서 넘겨주는 체결정보에는 당양한 정보가 있는데요. 이걸 제가 필요한 내용만 추려서 dict 형태 정보로 변경을 합니다. 이렇게 하는 이유는 향후 다른 거래소에서 거래를 할 때 거래소 의존적인 부분을 최소화하기 위함입니다. 실거래 데이터가 왔으니 이제는 거래를 한는 trader에게 이 정보를 넘겨줍니다.

 

            data_rev = await websocket.recv()
            my_json = data_rev.decode('utf8').replace("'", '"')
            data = json.loads(my_json)
            if show :
                print(data['code'], data['trade_time'], data['ask_bid'], data['trade_price'], data['trade_volume'])
            if 'type' in  data :
                if data['type'] == 'trade' :
                    info = make_info_from_upbit_real(data)
                    real.do_trading(info)

 

거래를 하는 방식은 앞에서 설명한대로입니다. 매수 조건이 되면 매수, 매수되었다면 매도 조건이면 매도. 간단하죠.

그런데 실제로는 좀더 작업을 해야합니다. 예를들어 매수 주문을 했다고 매수가 된 것이 아닙니다. 시장가 주문을 하면 되겠지만 이런 경우에는 수익률이 떨어질 수 있습니다. 만약 지정가 매수를 하게되면 매수가 되었는지 확인하는 코드를 꼭 추가하여야 합니다. 이번 소스에서는 빠졌지만 실제 매매를 하기 위해서는 꼭 추가하여야 합니다. 

 

    def do_trading(self, info) :
        if self.buying_order_info == {} : # 매수 대기 중
            buy_price = self.tr_logic.is_enter_condition(info) 
            if buy_price > 0 : # 매수조건임
                amount = min(self.balance, self.init_seed)
                # 살 수량 결정
                buying_vol = amount / buy_price    # 매수 주수
                self.do_new_order('buy', buy_price, buying_vol)

        else : # 이미 매수함
            sell_price, losscut = self.tr_logic.is_exit_condition(info)  #
            if sell_price > 0 : # 매도 조건
                # 매도 주문 
                # 매도 가격 : tr_logic에서 결정한 값, 
                # 매도 수량 : 매수 수량
                self.do_new_order('sell', sell_price, self.buying_order_info['org_qty'])

 

로직은 앞에서 설명한 것과 같이 일정 % 오르면 매수하고, 매수한 후 일정 % 이상 오르면 매도, 일정 %이하 내리면 손절입니다. 필요한 초기값들 설정하는 부분도 추가를 해야합니다. 주 부분은 매수조건, 매도저건입니다. 예로 만든 로직은 아주 간단합니다. 매수는 현재가가 기준가 대비하여 올랐으면 현재가로 매수 가격으로 돌려줍니다. 매도는 익절/손절 조건을 만족하면 현재가를 매도 가격으로 돌려줍니다.   

 

class TR_FOLLOW_TREND() 

    def is_enter_condition(self, info) :
        # buy_price는 시작가 * 상승률
        buy_price = self.open * (1+self.buy_percent) 
        if info['close'] >= buy_price :
            self.bought_price = info['close']  # 매수 가격 저장
            return info['close']
        return 0

    # 조건 조사
    # cut : 익절(1) 혹은 손절 (2)
    def is_exit_condition(self, info) :
        sell_price = self.bought_price * (1+self.sell_percent)  # 매수 가격 대비 올랐으면
        if info['close'] >= sell_price :
            self.bought_price = 0
            return info['close'], 1    # 1: 익절

        sell_price = self.bought_price * (1-self.losscut)  # 매수 가격 대비 내렸으면
        if info['close'] <= sell_price :
            self.bought_price = 0
            return info['close'], 2    # 2: 손절

        return 0, 0

 

upbit websocket으로 실시간 체결 데이터를 받아서 정의한 로직에 맞게 매매를 하는 프로그램이 완성되었습니다.

물론 매수/매도 주문 후 실제 체결이 되었는지 확인하는 부분을 추가하여야 완벽한 실전 매매가 됩니다. 

 

이 부분은 좀 복잡하기 때문에 일전에 소개해드린 전략매매 흐름도를 바탕으로 trader class를 변경하면 향후 기능 변경시에 적응하기 좋습니다.

 

money-expert.tistory.com/12

 

다음에는 업비트에서 전종목 시세를 받아서 (open 가격 자동 설정) 돌아가는 방식을 설명하도록 하겠습니다. 

현재 개인적으로 upbit에서 실시간 시세를 받아서 래리윌리엄스 로직을 돌리고 있는데요. 생각만큼 수익이 나오지는 않습니다. 시뮬레이션에서는 수익이 좋았는데, 실제 적용해보니 생각만큼 수익이 안나는군요. 세상에 쉬운 일이 없군요. 

 

관련 코드는 아래 깃허브에 있습니다. pyupbit 수정한 버전도 함께 올려 놓았습니다.

 

github.com/multizone-quant/system-trading-crypto

반응형

설정

트랙백

댓글

그동안 전략 시뮬레이션을 개발하는 과정을 기술하였는데요. 이제는 실제 매매를 해 보겠습니다.

 

이글에서는 upbit에서 자동매매를 개발하는 과정을 기술합니다.

우선 실제 매매를 위해서는 거래소에서 API key를 받아야 합니다. upbit 거래소에 로그인 한 후 고객센터/Open API 안내/OPen API 사용하기를 누른 후 access키와 secret 키를 받아야 합니다.

 

 

 

이렇게 받은 API 키를 upbit.txt 파일에 아래와 같은 형태로 저장하면 됩니다.

acess key

secret key

 

옆에 있는 업비트 개발자 센터를 누르면 API관련 문서가 나옵니다. API에 대하여 궁금한 사항이 있을 때 확인하시면 됩니다.

 

github에서 찾아보면 upbit api를 사용하기 쉽게 class로 만든 소스가 많이 있습니다. 이중 많이 사용하는 pyupbit을 fork하여 사용하도록 하겠습니다.

 

 pyupbit에는 대부분의 API가 구현되어 있지만 몇가지 함수도 추가하고, return 값을 변경하기도 하였습니다.

이렇게 변경한 소스는 아래 위치에서 다운받을 수 있습니다.

github.com/multizone-quant/pyupbit

 

다만 위 링크에 접속한 후 아래 그림과 같이 브렌치인 my_upbit-0.2를 선택한 후 오른쪽에 있는 code download를 누릅니다. 소스 zip 파일이 다운되면 개발할 곳에 압축해제하시면 됩니다. 

 

pyupbit을 바로 사용할 수 있겠지만 앞으로 다양한 거래소 자동매매를 개발할 예정이기 때문에 거래소에 독립적인 나만의 API가 필요합니다. 그래서 여러 거래소의 특성을 무시하고, 매매에 필요한 정보를 얻을 수 있는 wrapper class를 별도로 개발합니다.

 

pyupbit class를 기반으로 class MyUpbit를 만들었습니다. 따라서 pyupbit을 직접 사용하지 않기 때문에 pyupbit에 있는 readme.txt는 무시하셔도 됩니다.

 

앞으로 개발할 upbit 거래소 자동 매매 프로그램은 아래 링크에서 다운받으시면 됩니다.

github.com/multizone-quant/system-trading-crypto

 

거래소 관련 class를 먼저 올렸습니다.

jpy_basic_ex.py : 거래소 base class
jpy_dummy_ex.py : simulation을 위한 dummy class
jpy_upbit.py    : pyupbit wrapper class

jpy_upbit.py가 앞으로 사용할 upbit 거래소에서 자동매매를 위한 class입니다. jpy_upbit.py 단독으로 실행을 하면 제공하는 API test를 하는 함수들이 동작을 합니다.

 

jpy_upbit에서 제공하는 함수들의 return 값은 list의 array로 구성되어 있습니다. 만약 오류가 생기면 [0]번째 항목에 'error'라는 key word가 있습니다. 오류 message는 ret[0]['error']['message']에 포함되어 있습니다.

 

실제 매매하는 함수는 아래 5가지입니다. 이중 시장가 주문의 경우에는 buy의 경우에는 매수할 금액을 입력하고, sell의 경우에는 매도할 수량을 입력하면 됩니다.

 

buy_limit_order(ticker, price, qty)

buy_market_order(ticker, amount)

sell_limit_order(ticker, price, qty)

sell_market_order(ticker, qty)

cancel_order(uuid)

 

아직 체결되지 않은 주문은 아래 함수를 사용합니다. 특정 코인에 대한 미체결 주문을 알 수 있을 뿐 아니라, 'ALL'을 입력하면 현재 체결되지 않은 모든 코인 정보를 얻어올 수 있습니다.

 

pending_orders(ticker)  # or 'ALL'

 

잔고를 확인하는 방법입니다. 'KRW'를 인자로 넣으면 현재 원화 잔고가 특정 암호화폐 (예, 'BTC')를 넣으면 해당 암호화폐가, 'ALL'을 인자로 넣으면 모든 잔고를 얻을 수 있습니다. 

return값은 list의 array로 [0]번째 값에 하나 혹은 복수 코인의 잔고가 들어있습니다. 또한 오류인 경우에는 'error' keyword가 포함되어 있습니다. 사용예는 jpy_upbit.py에 밑 부분에 있습니다.

잔고는 ['total']과 ['orderable'] 항목이 있는데, 'orderable'은 잔고 중 신규 주문이 가능한 수량입니다.

 

다음에는 이전에 기술한 전략 시뮬레이터와 거래소 API를 합쳐서 upbit에서 특정 전략으로 자동매매를 하는 방법을 기술하도록 하겠습니다.

 

반응형

설정

트랙백

댓글

전략 시뮬레이션(1) 편에서 upbit 일봉 데이터를 가져오는 부분에 대하여 설명을 하였습니다. 일봉은 최대 400개만 가져올 수 있기 때문에 아주 간단하게 코딩을 했었는데요. 

이번에는 분봉 및 tick 데이터를 가져오는 방법에 대하여 설명하도록 하겠습니다.

 

분봉과 틱은 아래 형식을 사용하면 됩니다.

  • 분봉 :   1, 3, 5, 10, 30, 60분 봉을 가져올 수 있습니다. 아래 형식에서 minutes 뒤에 원하는 분 정보를 넣으면 됩니다.
crix-api-endpoint.upbit.com/v1/crix/candles/minutes/1?code=CRIX.UPBIT.KRW-BTC&count=10

 

  • 틱봉 : 틱봉을 가져오는 url입니다. 틱봉은 접속 주소도 틀리고 인자들도 조금 틀립니다. 특히 코드명 방식도 틀리니 참고하시기 바랍니다.
api.upbit.com/v1/trades/ticks?market=KRW-JST&count=500

 

우선 분봉은 숫자 상 제한이 없는 것 같기도 합니다. 1분 봉을 받아보니 한참을 받더군요.

문제는 분봉도 한번에 받을 수 있는 수량은 500개로 제한된다는 것입니다. 계속 이어 받기 위해서는 to 필드에 적절한 값을 넣어서 데이터를 받는 함수를 계속 호출하여야 합니다.

형식은 아래와 같습니다.

https://crix-api-endpoint.upbit.com/v1/crix/candles/minutes/1?code=CRIX.UPBIT.KRW-BTC&count=10&to=2019-09-01 00:00:00

 

to 값은 앞에서 받은 데이터 중 가장 마지막 데이터의 'candleDateTime' 값을 사용하면 됩니다.

 

그리고 받는 중간에 연결이 끊어질 수 있으므로 500개씩 받은 데이터를 파일에 계속 써 나가는게 좋을 것 같습니다. 방식은 코인이름과 일자 그리고 일련번호를 쭉 붙여나가는 방식이면 될 것 같습니다. 우선 간단하게 관련 코드만 소개해드리고, tick 데이터를 모두 받는 코드가 정리되면 함께 github에 올리겠습니다. 

 

# ago : tick인 경우에만 사용
#       오늘 기준으로 며칠 전 : 예 오늘:0, 어제:1
def get_data_continue(candle, coin, ty='day', interval=1, count=10, frm=None, to=None, ago=0) :  
    end = False
    cnt = 1
    while(end == False) :
        t = int(time.time())
        if candle : 
            ret = get_candle_history(coin, ty, interval, count, to)

        if ret != None :
            if len(ret) < 2 : # no more data
                return

            # 마지막에서 두 번째 시간을 to로 사용함. 
            # to를 포함하지 않는 시간까지 자료가 넘어옴. 
            # 이전에 검색한 to까지 자료가 부족할 수 있음
            # 차후에 merge할 때 to 시간이 겹칠 수 있으므로 빼는 부분 추가하여야 함
            info = ret[-2]  

            # candle과 tick 정보가 다름.
            # dt, to용 값을 구함
            if candle :
                dt = info['candleDateTime'].split('+')
                to = dt[0].replace('T', ' ')
                day = to.split(' ')[0]
                
            if frm != None : # from보다 이전 데이터인지 확인
                info = ret[-1] # 마지막 데이터 
                if candle :
                    if info['candleDateTime'] < frm : 
                        ret = remove_data(ret, frm, 'candleDateTime')
                        end = True
                    
            # cnt 번호를 추가하여 파일이름 생성
            fname = coin+'_' + ty + '_' + str(interval) + '_' + format(cnt, '03d') + '_' + day + '.csv'
            cnt += 1
            save_to_file_csv(fname, ret)
            print ('save ', fname)

            if ty == 'day' : # day는 400개만 받을 수 있다.ㅣ
                end = True
            else :  # 분 봉은 계속 받을 수 있다.
                time.sleep(1)

        else :
            end = True

 

프로그램이 실행되고 나면 아래와 같은 형식으로 파일이 생성됩니다.

 

 

 

이후에는 파일 하나로 합치면 됩니다. 이 부분도 현재 작업 중이므로, 함께 소개하도록 하겠습니다.

 

시뮬레이션은 데이터를 만드는 과정을 잘 정의해놓아야 필요할 때 원하는 구간의 데이터를 쉽게 빼올 수 있습니다. 처음에는 간단하게 시작했는데, 이 부분에서 작업할 양이 많은 것 같습니다.

 

 

 

 

 

반응형

설정

트랙백

댓글

기존 코드를 한번 더 리펙토링을 해야할 것 같습니다.

 

이번에 수정하고자 하는 부분은 아래와 같습니다.

1. 지표를 추가할 때 변경하는 부분 최소화

2. 기존 전략을 수정하거나 새로운 전략을 추가할 때 변경할 부분 최소화

 

우선 지표를 추가하는 부분을 다시 보겠습니다.

일단 새로운 지표인 경우에는 지표를 코딩하는 부분은 어쩔 수가 없이 추가하여야 합니다. 다만 새로운 지표가 추가되었을 때 simulation 과정에서 중간 값을 저장하는 부분에 새로운 지표를 추가하여야 하는데요. 이런 경우에 관계없는 부분도 변경하여야 하기 때문에 좋은 구조라고 볼 수 없습니다.

 

우선 기존 코드입니다. save_mid_values 함수 하나로 모든 변수를 저장하였습니다. 이러다 보니 관계없는 부분에서도 수정을 해야합니다.

def save_mid_values(candle_data, buy_price, sell_price, profit, total_profit, dd, trend, ma7) :

 

이 문제를 해결하는 방법은 함수를 쪼개는 것입니다.

관련 변수를 모아서 몇 개의 함수로 나눠보겠습니다. 우선 save_mid_values()에 default 값을 추가하여 초기화하는 부분에서는 save_mid_values()만 호출하면 됩니다. 

만약 새로운 지표가 추가된다면 save_mid_values()에 인자를 추가하고, save_mid_values_jipyo()에 관련 변수를 추가하면 됩니다. 

# 지표가 추가되는 경우에는 마지막에 변수 추가 
def save_mid_values(candle_data, buy_price=0, sell_price=0, profit=0, total_profit=0, dd=0, trend=0, ma7=0) :
    save_mid_values_buy_sell(candle_data, buy_price, sell_price)
    save_mid_values_result(candle_data, profit, total_profit, dd)
    save_mid_values_jipyo(candle_data, trend, ma7)

def save_mid_values_buy_sell(candle_data, buy_price, sell_price) :
    candle_data['buy_price'] = buy_price # buy 여부 저장
    candle_data['sell_price'] = sell_price # buy 여부 저장

def save_mid_values_result(candle_data, profit, total_profit, dd) :
    candle_data['profit'] = profit # profit 저장
    candle_data['total_profit'] = total_profit # total_profit 저장
    candle_data['dd'] = dd # downd draw 저장

def save_mid_values_jipyo(candle_data, trend, ma7) :
    candle_data['trend'] = trend # 추세값 저장
    candle_data['ma7'] = ma7 # downd draw 저장

 

이 함수를 사용하는 부분도 적절하게 변경하였습니다.

 

 

다음은 전략 부분을 refactoring해보겠습니다.

이전 글에서 횡보구간에서는 매수를 하지 않는 전략을 추가하였는데요. 아래와 같이 적용할 전략이 아닌 부분은 # 처리했었습니다. 이렇게 되면 코드 관리가 어려워집니다. 따라서 어떤 방식건 # 처리를 하지 않고 각 전략에 쉽게 적용할 수 있어야 합니다. 이 문제를 풀기위해 전략 코딩 부분을 refactoring 해보겠습니다.

    def is_enter_condition(self, candle) :  
        enter = 0
        # 아래 세 조건 중에 원하는 조건 선택
#        if self.range > 0 :
#        if self.range > 0 and self.trend > 0.2 # trend 동시 적용
        if self.range > 0 and self.trend > 0.2 and (self.MA != 0 and self.MA < candle.open) : # trend & MA 동시 적용

 

전략코딩 부분을 찬찬히 살펴보면 이런 식으로 구성되어 있습니다.

 

candle이 변경되었을 때 관련 변수 수정

if 매수 중이면 

   전략에 따른 매도할 가격을 받는다.

   현재 candle 기준으로 매도 조건이면

      매도주문    

else # 매수대기

   전략에 따른 매수할 가격을 받는다.

   현재 candle 기준으로 매수 조건이면

       매수주문

 

따라서 이런 구조로 class를 정의하면 새로운 전략을 추가하는 경우에 필요한 부분만 변경하면 됩니다. class를 사용하면 이런 구조를 아주 효과적으로 개발할 수 있습니다.

 

기존적인 전략에 대한 코딩은 base class에서 하고, 새로운 전략을 적용하는 경우에는 매수/매도 가격 결정, 매수/매도 조건 결정하는 함수만 개발하면 됩니다.

 

class의 가장 큰 장점이 상속기능입니다. 개념이 조금 복잡하지만 쉽게 설명하면 공통으로 사용하는 함수는 부모 클래스가 가지고 있고 차이가 나는 부분은 자식클래스에서 개발하자는 취지입니다.

 

앞에서 소개한 TR_LW를 base class로 옮기고, 이 중 전략별로 다르게 개발하여햐하는 함수만 자식 class에서 개발하면 됩니다.

 

우선 TR_Basic()은 기존 TR_LW와 같다고 보시면 됩니다. 특이한 부분은 자식 클래스입니다. 우선 TR_LW_SIM 선언시에 누가 부모인지를 인자로 넣습니다. 그 후 필요한 경우에는 초기화를 하는데요. 이때 부모 클래스의 초기화를 진행해야하기 때문에 super()라는 키워드를 사용하여 부모 클래스에 있는 함수를 호출합니다. 그 후 자기 class에서 필요한 변수를 만들고 초기화합니다. LW 전략이므로 range와 k 값을 초기화합니다.

class TR_Basic() :  # 전략 부모 클래스
    def __init__(self, interval, num) :
        # 공통 초기화 부분
        
class TR_LW_SIM(TR_Basic) : # LW 시뮬레이션용 클래스 
    def __init__(self, interval, num) :
        super().__init__(interval, num)
        self.range = 0
        self.k = 0.5

 

전략에서 필요한 함수는 크게 5가지로 구분할 수 있습니다.

    def update_new_candle(self, cur) : # candle이 변경되었을 때 관련 변수 변경
    def get_buy_price(self, candle) : # 매수할 가격 구하기
    def is_meet_buy_cond(self, candle, buy_price) : # 매수 조건 만족?
    def get_sell_price(self, candle) : # 매도할 가격 구하기
    def is_meet_sell_cond(self, candle, sell_price) : # 매도 조건 만족?

 

이렇게 만든 상태에서 비추세 구간에는 매수를 하지 않는 전략으로 수정을 한다고 가정을 합시다.

기존 TR_LW_SIM과 비교하여 차이가 나는 부분은 매수 가격을 정하는 부분입니다. 따라서 TR_LW_SIM을 부모로 갖는 TR_LW_TREND라는 classs를 만들고 get_buy_price함수에 관련 코드를 작성하면 됩니다.

 

class TR_LW_TREND(TR_LW_SIM) :
    # 매수 조건을 만족하면 매수가를 돌려준다.
    def get_buy_price(self, candle) :
        if self.range > 0 and self.trend > 0.2 : # trend 동시 적용
            buy_price = (candle.open + self.range * self.k)
            return buy_price
        return 0

 이렇게 만든 전략을 사용하는 경우에는 아래와 같이 class 명을 명시하시면 됩니다. 비추세와 이평을 이용하는 전략도 함께 수정해놓았으니 참고하시기 바랍니다.

 

tr_logic = TR_LW_TREND('min', 1)

 

시뮬레이션은 비교적 간단한 개발이기 때문에 이 정도로 동작을 하지만 실제 매매와 연동을 하게되면 구조가 좀 더 복잡해집니다. 이것도 시간이 되면 정리해서 올리도록 하겠습니다.

 

관련 src코드 파일명과 github 위치는 아래와 같습니다.

Larry_williams4.py
TR_LW_1.py
my_candle.py
my_util.py

github.com/multizone-quant/System_trading_ex

반응형

설정

트랙백

댓글

LW 전략 적용해보고, 그 결과를 차트로 그려서 매매 상황을 확인해 볼 수 있는 부분까지 마무리를 하였습니다. 이번에는 기본 LW 전략에 몇몇 아이디어를 추가해보도록 하겠습니다.

 

일단 비추세 구간에서는 LW 매수 시그널이 나오더라도 이익도 적고, 손실이 발생할 확률이 높습니다. 따라서 비추세 기간에는 매매를 하지 않도록 수정해보겠습니다.

 

비추세 구간을 확인하는 방법은 여러가지가 있겠지만, 이번에는 아래 글에 나오는 추세추종 필터를 적용해보겠습니다.

 

cafe.naver.com/invest79/939

 

여기에서 사용하는 추세추종 필터는 아래와 같습니다. 

 

A : 7일간 코인 가격 차이의 절대값 = abs(1일전 코인종가 - 7일전 코인종가) 

B : 7일간 당일 코인 움직임 절대값의 총합 : abs(7일전 시/종가 차이)+ ...... + abs(1일전 시/종가 차이)

 

추세추정필터는  A/B 값으로 계산합니다. 값이 크면 추세가 강하다는 의미입니다.

결국 매일의 시/종 가격 변화의 합과 시작/킅 날의 가격 차이의 비율로 추세를 판단하는 방법입니다.

 

이전에 만들었던 프로그램에서 추가할 사항들은 아래와 같습니다.

1. simultaion 중간에 저장할 값으로 추세추종 필터 값 추가 (debuging용)

2. tr_lw에서 추세추종 필터 계산하는 함수

3. candle update하는 update_new_range() 함수에서 추세주총필터 계산하는 함수 호출하기

4. tr_lw에서 is_enter_condition() 수정

 

비교적 변경하는 부분이 적다고 볼 수 있습니다. 

 

그럼 하나씩 추가해보겠습니다.  

 

1. 중간값을 저장하는 함수 수정

def save_mid_values(candle_data, buy_price, sell_price, profit, total_profit, dd, trend) :
    candle_data['buy_price'] = buy_price # buy 여부 저장
    candle_data['sell_price'] = sell_price # buy 여부 저장
    candle_data['profit'] = profit # profit 저장
    candle_data['total_profit'] = total_profit # total_profit 저장
    candle_data['dd'] = dd # downd draw 저장
    candle_data['trend'] = trend # 추세값 저장

 

중간값을 저장하는 함수를 부르는 곳에도 trend 값을 전달할 수 있도록 수정합니다.

        # simulation 중 파일에 저장할 변수들 추가
        trend = tr_logic.get_trend()
        save_mid_values(candle_data[i], 0, 0, 0, 0, 0, trend)

 

2. TR-LW.py에 추세를 계산하는 함수를 추가합니다.

 

def __init__(self, interval, num) :
		:
        self.trend = 1 # 변수 추가
        
def get_trend(self) :
        cur = self.history[-1] # last one

        day_sum = 0
        candle = None
        hist_len = len(self.history)
        if hist_len >= 8 :
            range_total = 0
            pos = len(self.history)-1
            for i in range(7) :
                candle = self.history[pos]
                day_sum += abs(candle.open - candle.close) # candle 시/종가 차이의 절대치 합
                pos -= 1

            period_range = abs(candle.close - cur.close)
            self.trend = period_range / day_sum
            self.trend = float(format(self.trend, '.2f')) # 소숫점 2자리 값으로 변경

        return self.trend

 

history에는 candle 정보가 오름차순으로 저장되어 있으므로, 최신 7 candle의 값을 구하기 위해서는 뒤에서 부터 계산을 해야합니다. 그리고 float 값은 소숫점 이하 긴 숫자로 저장되므로, 소숫점 이하 2자리만 가지고 있기 위해서는 .2f 포맷으로 변경한 후(문자열) 이것을 다시 float로 변경하면 소숫점 2자리 실수를 가지고 있을 수 있습니다.

 

3. 다음은 get_trend() 함수를 부르는 곳을 추가해야 합니다.

    def update_new_range(self, cur) : # 
        self.pre  = cur
        self.history.append(cur)
        self.range = cur.high - cur.low # 전 candle의 range

        self.get_trend()

 새로운 candle이 들어오면 range값을 계산하고 history에 추가하는 함수입니다. 여기에서 get_trend()를 부르면 될 것 같습니다.

 

4. 마지막으로 매수 조건에 trend 값이 특정 값  이상이면 매수하도록 수정합니다. 일단 0.4로 해보겠습니다.

    def is_enter_condition(self, candle) :  
        enter = 0
        if self.range > 0 and self.trend > 0.4 : # 매수 조건
            buy_price = (candle.open + self.range * self.k)
            if candle.high > buy_price :  # 최고가가 buy_price보다 높으면 매수되었다고 가정
                enter = 1
                self.bought_price = buy_price

 

추세추종 필터를 추가하기 위하여 필요한 코딩이 마무리되었습니다. 새로운 기능이 들어오면 변경할 곳이 많아집니다. 이 중 한 곳이라도 수정하지 않으면 정확하게 동작하지 않게 됩니다. 따라서 본인이 작성한 프로그램 구조를 정확하게 파악을 해야합니다.

 

이렇게 반영한 결과를 확인해보겠습니다.

 

profit : ticker :  KRW-BTC
total # trading  :  215
total profit     :  1,926,201.75
trading fee      :  151,161.74
total Net Profit :  1,775,040.00
# winning        :  121
# losing         :  94
MDD              :  51.78
max loss         :  -17,060.96
max gain         :  1,926,201.75

 

아쉽게도 추세추종 필터를 추가하기 전 보다 수익이 더 떨어졌군요. 그 원인을 확인해 보도록 하겠습니다. 두 그래프를 비교해보니 아래 그림과 같이 급락하였다가 다시 원래로 돌아오는 경우에 추세추종 필터가 추가된 경우에는 매수에 참여를 안하고 있습니다. 추세추종 값이 0.18, 0.33이군요. 

추세추종 정도를 변경하면서 돌려본 결과입니다. 수익률 면에서는 추세추종 필터를 사용하지 않은 경우가 제일 좋습니다. 다만 추세추종 필터를 추가하면 수익률은 다소 떨어지지만 MDD 값은 개선이 됨을 알 수 있습니다. 각자 장단점이 있을 것 같습니다.

 

 

결국 어떤 전략에서 특정 필터를 사용할 때 적용 여부를 판단하는 파라미터 값을 찾는 것이 중요합니다. 트레이딩하는 코인의 종류에 따라 최적의 값은 달라질 것입니다. 따라서 실전에서는 많은 시도를 해보면서 계속 변경해야할 것 같습니다.

 

매매를 한 시점을 분석해보니 하락 시점에 매수를 하면 손실이 발생하는 경우가 많군요.

 

하락 기간에는 매수를 하지 않으면 수익이 좋아질지 궁금합니다.

하락 추세를 판단하는 방법도 여러가지가 있겠지만 일단 이동평균선을 사용해보겠습니다. 주식에서는 MA(5일)을 사용하지만 코인은 주말에도 거래가 되므로 MA(7일)을 사용하겠습니다. 

 

하락추세추종 필터와 하는 역할이 같으니 변경되는 부분도 비슷합니다.

 

1. save_mid_values()에 ma값 추가하고 save_mid_values()를 부르는 함수에 ma값 추가

2. TR-LW.py에 MA 계산하는 함수 추가, 원하는 일자로 변경할 수 있도록 일자를 함수 인자로 받도록 하겠습니다.

def __init__(self, interval, num) :
		:
        self.MA = 0 # 변수 추가

def get_MA(self, num_days) :
        hist_len = len(self.history)
        if hist_len >= num_days :
            day_sum = 0
            pos = len(self.history)-1
            for i in range(num_days) :
                candle = self.history[pos]
                pos -= 1
                day_sum += candle.close # 종가 합
            self.MA = day_sum / num_days
            self.MA = float(format(self.MA, '.2f')) # 소숫점 2자리 값으로 변경
        return self.MA

3. get_MA()를 부르는 곳 변경. get_trend()와 같이 update_new_range() 함수에 추가하면 됩니다.

4. is_enter_condition() 변경

   MA 값이 candle의 open 가격보다 낮으면 상승 추세로 가정합니다. 

    def is_enter_condition(self, candle) :  
        enter = 0
        if self.range > 0 and self.trend > 0.4 and (self.MA != 0 and self.MA < candle.open)  :
            buy_price = (candle.open + self.range * self.k)

 

이렇게 변경한 내용을 반영해보았습니다. 추세추종 값은 0.2, MA(7일) 적용 결과입니다. 

 

위에서 언급한 구간에서 매수 후 손실은 사라졌습니다만 수익률은 더 떨어지는군요. 아마 수익이 날 곳에서 매수를 못하고 있을 것 같습니다.

 

그래프를 다시 비교해보니 아래와 같이 하락 후 상승하는 시기에 나오는 양봉에 매수를 못하고 있습니다. 당연한 결과인 것 같습니다. 하락 추세 중 발생하는 양봉은 아직 하락 추세 중이므로, 당연히 매수가 안됩니다. 하지만 이런 경우에 장대 양봉이 나오는 경우가 종종있습니다.  

하락시에 손실이 발생하는 매수를 없애기 위하여 적용한 상승추세 시 매수 전략의 단점이 있군요. A 문제를 풀려고 하니 B 문제가 생기는 셈인데요. 어떻게 할지는 좀 더 고민해봐야 할 것 같습니다.

 

결론적으로 다양한 필터의 특성을 정확하게 파악해서 특정 움직임에 맞는 필터를 자유자재로 적용할 수 있는 내공이 필요해보입니다. 필터에 필수적으로 필요한 파마미터 값을 결정하는 것도 큰 숙제입니다.

 

파이썬을 이용하면 최적의 필터와 파라미터의 조합을 비교적 빠르게 찾을 수 있습니다. 파이썬을 이용하여 시뮬레이션을 하는 이유도 이것 때문이고요.

 

지금까지 간단하게 전략 시뮬레이터를 만들어 보았습니다. 최대한 간단하게 코딩을 했으니, 코드를 읽는데 큰 문제는 없을 것이라고 생각합니다. 수정한 코드는 아래위치에 있습니다. Larry_williams3.py에 지금까지의 수정 사항이 반영되어 있습니다.

 

github.com/multizone-quant/System_trading_ex

 

multizone-quant/System_trading_ex

Contribute to multizone-quant/System_trading_ex development by creating an account on GitHub.

github.com

여기에서 아래 프로그램을 다운받으시면 됩니다.

Larry_williams3.py
TR_LW.py
my_candle.py
my_util.py
sim_data/*

 

이외에도 lw 매수 조건 계산할 때 k 값을 평균 값으로 변경하기 등 추가할 사항이 더 있습니다. 이 부분도 차후에 반영하겠습니다.

 

오늘 지표를 추가하면서 수정할 부분을 찬찬히 살펴보면 tr_lw.py에 기능 추가하는 부분 이외에 Larry_williams3.py에 추가하는 지표를 저장하기 위하여 코드를 수정하는 부분이 있습니다. 이렇게 여러 파이썬 파일을 수정하는 경우에는 실수할 확률이 높아지므로, 이 부분도 모으면 좋습니다. 

 

 

다음에는 업비트에서 실시간 시세를 받으면서 실제로 투자하는 실전용 코드를 소개하도록 하겠습니다.

 

 

반응형

설정

트랙백

댓글

LW 전략을 돌려본 후 mdd, 수익률 등등을 보면서 성과가 얼마나 나왔는지 확인이 가능합니다. 그런데 전략을 개선하기 위해서는 언제 수익이 났고 언제 손실이 발생하였는지를 chart 그래프로 확인해볼 수 있으면 인사이트를 얻기가 더 좋을 것 같습니다.

 

일전에 candle chart에 text를 출력하는 방법에 대하여 글을 올렸었는데요. 무난하게 동작하였지만 시간만 가지고 있는 경우에 출력이 제대로 되지 않는 문제를 발견하였습니다. 또한 차트를 그리는데 시간이 너무 많이 걸려서 다른 방법을 찾아보기로 했습니다.

 

구글링을 열심히 한 결과 아래 글을 발견했습니다.

info.cloudquant.com/2019/08/candlestick_plotly/

 

Candlestick Charts in Python with Plotly - CloudQuant

A candlestick chart is one of the best ways to show Traders data. Topics: Python, Plotly, OHLC, Candlestick Charts, Jupyter, Pandas, Traders

info.cloudquant.com

 

이 글을 바탕으로 plotly를 이용하여 chart를 그리고 필요한 위치에 text를 그리는 코드를 만들어보았습니다.

 

plotly를 사용하기 위해서는 pandas를 사용하여야 합니다. 그래서 차트를 그리는 함수에서 date/open/high/low/close 데이터를 저장한 후 pandas로 읽는 부분을 추가하였습니다.

 

def draw_chart_plotly(candle_data, ticker, stat) :
    fname = 'chart.csv'
    save_ohlc(fname, candle_data)

    mcd = pd.read_csv(fname)

 

LW 전략을 수행하면 매매가 발생한 경우에 'profit' 부분에 수익금을 기록합니다. 이 정보를 바탕으로 +이면 'P(rofit)'를 -이면 'L(oss)'를 high 값 위치에 표시하도록 해보겠습니다. 

 

매매가 발생한 위치에 annotation을 추가한 후 이후에 그를 candle에 추가하면 됩니다.

 

그럼 매매가 발생한 candle에 annotation을 추가해보겠습니다. candle_data를 뒤에서 앞으로 읽으면서 profit 값이 0이 아니면 매매가 있었다는 의미입니다. 이때 값이 0보다 크면 이익(P), 아니면 손실(L)을 출력하는 annotation을 생성하여 추가합니다. 글을 쓰는 위치는 마음대로 정할 수 있습니다. 여기에서는 x값은 거래가 발생한 시간, y값은 high 값으로 정하겠습니다. default 값은 (시간,high) 위치입니다. 여기에 출력을 해보면 chadle과 겹치게 됩니다. 그래서 x, y 축 방향으로 약간 shift를 합니다. 이 값은 본인이 원하는 값을 입력하시면 됩니다. 일단은 xshift로 1, yshift로 10을 넣었습니다. 이렇게 되면 chadle 기준으로 x 방향으로는 center, y 방향으로는 high candle보다 약간 위에 text가 출력됩니다.

 

    annotations = []

    for i in range(len(candle_data)-1, -1, -1)  :# 뒤에서 앞으로
        text = 'L'
        if candle_data[i]['profit'] > 0 : # profit 이면 P lost이면 L 출력
            text = 'P'
        if candle_data[i]['profit'] != 0 :
            annotations.append(go.layout.Annotation(x=candle_data[i]['candleDateTimeKst'],
                                                    y=candle_data[i]['highPrice'],
                                                    xshift=1,  # x 축 기준으로 오른쪽으로 x칸 이동
                                                    yshift=10,  # y 축 기준으로 오른쪽으로 y칸 이동
                                                    showarrow=False,
                                                    text=text))

다음은 화면에 보여줄 layout를 정하는 부분입니다. 보여줄 데이터의 양에 따라 width를 조정합니다. test를 해보니 보여줄 데이터 수의 10배 정도가 적당하더군요. 화면에 보여 줄 title에 적절한 내용을 만듭니다. layout를 만들 때 앞에서 만든 annotations를 추가합니다. 

 

    # draw할 layout 생성
    width = len(candle_data) * 10
    layout = dict(
            title=ticker+':: # tradings : ' + str(stat.num_trading) + '  profit : ' + format(stat.get_profit_percent(), '.2f') + '%   mdd : ' + format(stat.mdd, '.2f'),
            xaxis=go.layout.XAxis(title=go.layout.xaxis.Title( text="Time"), rangeslider=dict (visible = False)),
            yaxis=go.layout.YAxis(title=go.layout.yaxis.Title( text="Price")),
            width=width,
            height=800,
            annotations=annotations
    )

candle 차트는 만드는 방법은 아주 간단합니다. 단 두 줄

    data_candle = go.Candlestick(x=data.Date,open=data.Open,high=data.High,low=data.Low,close=data.Close)
    data = [data_candle]

이렇게 만든 candle을 화면에 보여주면 됩니다. 이것도 단 두 줄

    fig = go.Figure(data=data,layout=layout)
    fig.show()

화면에 보이는 모습니다.

 

아래에 스크롤바가 있어서 좌우로 옮기면서 볼 수 있습니다.

차트를 보면 예상한대로 움직이는군요. 장대 양봉은 잘 따라가서 수익을 내고 있고, 아래/위 긴 꼬리가 나오는 경우에는 손실이 발생합니다.

 

충분히 수익이 좋아보이지만 개선할 점도 보입니다. 아래 그림과 같이 행보장에서 매수하면 손실이 발생하는 경우가 많습니다. 이렇게 횡보장에서는 매수 신호가 나오도 무시하는 코드를 넣으면 성능이 더 좋아질 것 같습니다.

 

 

수정한 소스는 아래 github에 있습니다.

 

github.com/multizone-quant/System_trading_ex/tree/main

 

multizone-quant/System_trading_ex

Contribute to multizone-quant/System_trading_ex development by creating an account on GitHub.

github.com

여기에서 아래 프로그램들을 다운받으시면 됩니다.

Larry_williams3.py
TR_LW.py
my_candle.py
my_util.py
sim_data/*

 

다음 글에서는 횡보장을 빼는 방법에 대하여 적어보도록 하겠습니다.

 

 

반응형

설정

트랙백

댓글