코인 봇 알고리즘 - koin bos algolijeum

Q1코인봇24가 무엇이죠?

코인봇24는 누구나 쉽게 암호화폐 트레이딩을 할 수 있도록 유저에게 자동매매 시스템을 서비스하고 있는 IT 회사입니다.
코인봇24는 2018년 2월부터 현재까지 운영 되고 있으며, 앞으로 더 발전된 모습으로 운영되기 위해 최선을 다할 것입니다.
자체적으로 개발한 알고리즘으로 자동화된 트레이딩 시스템을 구축하여 서비스하고 있으며, 각 페이지의 도움말을 참고하여 쉽게 사용할 수 있습니다

Q2사용방법이 어떻게 되나요?

페이지 최상단의 '사용하기'버튼을 클릭, 본인의 카카오톡 계정으로 로그인을 하면 자동매매 시스템에 접근할 수 있습니다
로그인 후, "메뉴)설정 - API Key"에서 거래소에서 발급받은 본인의 Api Key 등록 후 "메뉴)봇 관리 - 봇 생성 - 실행"하면 자동매매를 시작합니다
* 메뉴얼 또는 각 페이지의 도움말을 참고하시면, 쉽게 설정할 수 있습니다

Q3PC를 켜두어야 할까요?

코인봇24의 자동매매 시스템은 설치형 프로그램이 아닌, 서버에서 24시간 작동이 되는 형태로 PC를 켜놓으실 필요가 없습니다
사용자는 웹 접속을 통해 "카카오 계정"으로 로그인 후, 봇에 접근하여 설정/명령할 수 있습니다

Q4현재 자동매매가 가능한 거래소는 어디인가요?

가상화폐의 종류와 거래량이 많은 바이낸스 거래소를 지원하고 있고, 앞으로 더 많은 거래소를 지원할 예정입니다

Q5무료로 사용할 수 있나요?

네, 처음 가입을 하시면 체험가능 하도록 운용가능한 봇 3개와 1,000포인트가 무료로 제공됩니다.
* 봇은 기한없이 사용가능하고, 포인트는 저희 서비스를 이용하는 수수료로 매수시 차감됩니다

암호화폐 트레이딩 봇

최근 블로그 포스팅이 한 동안 뜸했던 이유는, 어느 날 트레이딩 봇을 만들고 싶은 욕구가 생겨서 여기에 지속적으로 힘을 쓰고 있었기 때문이다. 개발자라면 누구나 한 번쯤은 만들어본다는 이것을, 아직 나는 만들어본 적이 없으니 괜찮은 기회라 여겨 해보기로 했다. 설계를 여러번 수정하다가 이제서야 어느정도 완성도를 보이고 있어 블로그에 적기로 했다. 참고로 개발 언어는 파이썬이 아닌 Go 다. 그 이유는 아래에서하자.

코인 봇 알고리즘 - koin bos algolijeum
트레이딩 봇을 구동한다. CLI 기반이기 때문에 GUI 같은건 없다.

봇 같은 경우 주식 트레이딩은 봇은 아니고, 암호화폐 거래소 중 하나인 업비트에 암호화폐를 주문하고, 조건에 맞는 코인을 감지, 이후 감지된 마켓을 대상으로 전략을 실행할 수 있는 봇을 개발했다. 이미 서문만으로도 봇의 구조가 이미 노출되었지만, 이는 그저 프레임워크를 만들어낸 것 뿐이며 가장 중요한 것은 전략인데, 이는 기업 비밀이라 비공개다. 애초에 수익을 제대로 내고 있지도 않지만.

docs.upbit.com/docs

업비트 개발자 센터

업비트 Open API 사용을 위한 개발 문서를 제공 합니다.업비트 Open API 사용하여 다양한 앱과 프로그램을 제작해보세요.

docs.upbit.com

코인 봇 알고리즘 - koin bos algolijeum

파이썬이 아닌 Go 언어로 개발한 이유

이 프로젝트는 내가 Go 로 작성한 첫 번째 사이드 프로젝트다. 일반적으로 트레이딩 봇은 파이썬으로 개발된 경우가 많은데, 나같은 경우에는 Go 를 선택했다. Go 를 사용한 이유는 물론 현재 내 주력 언어가 Go 인 것이 가장 큰 이유이기도 하지만, Go 를 봇 개발에 사용했을때 가지는 간편하고 채널을 통한 동시성 제어에서의 이점이 크다고 여겼기 때문이다. 트레이딩 봇에서 여러 마켓의 감시를 위해 고루틴을 사용하여 동시성을 사용할 일은 많은데, 그 예는 설계에서 살펴보도록 하자.

내가 개발한 트레이딩 봇은 오픈소스다. 따라서 봇의 사용법이나 코어 소스코드가 궁금하다면 아래의 깃허브 레포지토리를 확인하자. 이 포스트에서는 봇에 대한 전반적인 설계를 살펴본다.

https://github.com/pronist/upbot

GitHub - pronist/upbot: 암호화폐 트레이딩 봇 (feat. 업비트)

암호화폐 트레이딩 봇 (feat. 업비트). Contribute to pronist/upbot development by creating an account on GitHub.

github.com

코인 봇 알고리즘 - koin bos algolijeum

설계

봇은 가장 큰 관점에서 보자면, 봇은 업비트 서버의 관점에서 클라이언트라는 점이다. 어떤 서버에 요청을 보내는 클라이언트냐 하면 업비트 API 서버에 보내는 클라이언트라고 볼 수 있다. 어떠한 형태로든 트레이딩 봇은 업비트 서버에 요청을 보내게 된다. 그게 조회가 될 수도 있고 주문을 요청을 하는 것일 수도 있다.

코인 봇 알고리즘 - koin bos algolijeum
트레이딩 봇은 업비트 Open API 서버로 요청을 보낸다.

또한 시세를 주기적으로 감시하여 조건에 도달했는지를 판단하는 Detector, 각 마켓을 대상으로 개별적인 매수/매도 전략을 실행할 수 있는 Strategy 가 고루틴의 주요 사용처다. 이는 서로 독립적으로 돌아간다. Strategy 에서는 조건에 도달하면 업비트 API 서버에 주문을 보내기 때문에 다른 문맥에서 독립적으로 동작해도 아무런 영향이 없다. 봇의 전반적인 구조는 다음과 같다.

코인 봇 알고리즘 - koin bos algolijeum
Detector 에서 시작하여 전략을 실행하는 트레이딩 봇의 전반적인 구조를 보여준다.

다이어그램을 보면 알겠지만, 봇은 중간에서 중개인의 역할을 수행하게 되며 Detector 가 특정 조건에 해당하는 종목을 찾아서 봇에게 보고를 하면, 봇은 코인을 추상화한 Coin 객체를 생성하고 매수/매도를 위한 Strategy 에 생성한 Coin 객체를 전달하여 실행하게 될 것이다. 여기서 Detector 는 별개의 고루틴에서 동작, 봇이 실행하는 전략들도 모두 별개의 고루틴에서 실행되며 독립적으로 조건을 검증하여 매수/매도를 진행한다. 거의 동시에 여러 개의 마켓에 대해 전략을 실행할 수 있다.

예를 들어 Detector 가 특정 조건을 만족한 종목인 KRW-BTC 를 발견하여 봇에 보고하면, 봇은 BTC 코인에 해당하는 Coin 객체를 생성하고 KRW-BTC 마켓이 Strategy 에 따라 매수/매도 될 수 있도록 하게하는 것이다. 그래서 주목해볼만한 부분은 결론적으로 트레이딩 봇이라는 것이 의도대로 동작하기 위해서는 종목 선정(Detecting)매수/매도 전략(Strategy)이라는 두 가지의 주요 핵심 알고리즘이 있다는 것이며 이에따라 적절한 종목선정과 전략에 따라 봇의 성과가 결정된다는 것이다.

업비트 API 클라이언트

봇은 위에서 언급했듯 업비트의 API 서버에 요청을 보내는 클라이언트다. 따라서 업비트 API 에 요청을 보낼 수 있는 클라이언트 래핑 객체가 필요하게 된다. 물론 이 부분은 업비트 API 문서에 따라 작성된 것이기 때문에 그렇게 중요하지는 않지만, 실제로 업비트 API 서버에 요청을 보내는 역할을 하므로 짤막하게나마 이야기해본다.

코인 봇 알고리즘 - koin bos algolijeum
자산 조회나 주문 요청의 경우 Jwt 가 필요하고, 그렇지 않은 일반적인 정보는 그냥 보내도 상관없다.

업비트 API 서버는 두 종류로 나눌 수 있는데, Jwt 를 포함하여 요청을 보내야 하는 일반적인 Client 와 그저 Get 요청만 보내도 정보를 얻을 수 있는 QuotationClient 로 분리된다.

type Client

자산, 주문 요청을 업비트 서버에 보내기 위해 사용하는 클라이언트다. 당연하겠지만 여기에는 AccessKey, SecretKey 가 포함되어야 한다.

type Client struct {
	*http.Client
	AccessKey string
	SecretKey string
}

type QuotationClient

QuotationClient 는 단순한 Get 요청을 위해 사용한다. 이를 통해 종목에 대한 Tick, Trades 를 얻어오는 등 인증이 필요하지 않은 단순한 정보들을 얻어올 수 있다. 따라서 http.Client 만을 가진다.

type QuotationClient struct {
	*http.Client
}

이렇게 선언된 두 개의 클라이언트는 Bot 을 통해 접근할 수 있도록 하였다. 따라서 Client, QuotationClient 를 통해 업비트 서버에 요청을 보낼 수 있게된다.

트레이딩 봇

type Bot

Botmain 고루틴에서 사용되며 Bot.Run() 이라는 메서드를 main() 함수에서 호출할 것이다. 먼저 Bot 구조체는 다음과 같이 생겼다. 위에서 언급한 것처럼 클라이언트의 역할도 한다는 것을 잊어서는 안 된다.

type Bot struct {
	*client.Client
	*client.QuotationClient
	Accounts   Accounts
	Strategies []Strategy
}

Accounts, Strategy 타입은 모두 인터페이스다. 특히 Accounts 의 경우, 업비트는 기본적으로 모의투자를 지원하지 않는다. 따라서 안전하게 전략이 동작하는지 실험을 할 수 있어야 하는데, 그럴때 필요한 것이 프로그램에서 임의로 만든 테스트용 계정이다. 이는 실제 업비트 계정이 아니며 비슷한 동작을 하도록 구현이 된 것 뿐이다. 따라서 미묘한 차이가 발생한다.

또한 Bot 에서는 미리 마켓에 사용할 전략을 가지고 있다. Detector 가 조건에 도달한 마켓을 발견하게 되면 해당 마켓에 Strategies 에 있는 전략들을 실행하게 된다.

main()

Bot 을 호출하는 main() 함수는 아래와 같이 작성된다. 계정을 임의로 생성하여 전략을 테스트할 수 있다.

func main() {
	///// 봇에 사용할 전략을 설정한다.
	b := bot.New([]bot.Strategy{
		// https://wikidocs.net/21888
		&bot.PenetrationStrategy{},
	})
	/////

	///// 봇에 사용할 계정을 설정한다.
	//acc, err := bot.NewUpbitAccounts(b)
	acc, err := bot.NewFakeAccounts("accounts.db", 55000.0) // 테스트용 계정
	if err != nil {
		logrus.Fatal(err)
	}

	b.SetAccounts(acc)
	/////

	logrus.Panic(b.Run())
}

.Run()

.Run() 메서드는 main 고루틴이 실행하는 메서드이며, Detector보고를 받고, 전략을 실행하는 핵심 메서드다. Detector 에게 보고를 받을 때는 자연스럽게 채널을 사용한다. 참고로 아래의 코드가 실제 돌아가고 있는 봇의 코드랑 동일한 것이 아니다. 핵심적인 코드만을 가져와 포스트하기 편하도록 짜집기했다.

// 추적할 종목에 대한 조건이다.
func Predicate(t map[string]interface{}) bool {
	return true
}

func (b *Bot) Run() error {
	// 전략의 사전 준비를 해야한다.
	for _, strategy := range b.strategies {
		log.Logger <- log.Log{
			Msg:    "Register strategy...",
			Fields: logrus.Fields{"strategy": reflect.TypeOf(strategy).String()},
			Level:  logrus.DebugLevel,
		}
		if err := strategy.register(b); err != nil {
			return err
		}
	}

	///// 디텍터
	d := newDetector()
	go d.run(b, predicate) // 종목 찾기 시작!
	/////

	for tick := range d.d {
		// 디텍팅되어 가져온 코인에 대해서 전략 시작 ...
		market := tick["code"].(string)
        	
		// 코인 생성
		coin, err := newCoin(b.Accounts, market[4:], static.Config.TradableBalanceRatio)
		if err != nil {
			return err
		}
            
		// 전략에 주기적으로 가격 정보를 보낸다.
		go b.tick(coin)
		
		for _, strategy := range b.Strategies {
			if err := strategy.boot(b, coin); err != nil {
				return err
			}
			go b.strategy(coin, strategy)
		}
	}
}

추가적으로 Detector.run() 의 파라매터에 predicate 가 사용된 것이 있는데, 저것은 함수이며 디텍터가 찾을 종목에 대한 조건을 명시한다. 해당 함수가 true 를 반환하면 조건에 맞는 종목으로 판단하며 Detector.d 채널에 신호를 보낸다. Detector.run() 에서는 내부적으로 업비트 웹소켓 서버에 요청을 보내 가격을 얻어오고 조건을 처리한다.

코인 봇 알고리즘 - koin bos algolijeum
Bot, Detector, Strategy 가 데이터를 주고받는 모습을 보여준다.

Bot.tick() 메서드는 coin 구조체에 정의되어 있는 t 채널에 가격 정보를 보내고 이를 Strategy 에서 소모한다. Strategy 에서 직접 가격정보를 얻어와도 되지만, 요청의 수가 너무 많아지면 업비트 서버의 제약에 따라 요청이 거절된다. 업비트 서버의 제한은 초당 10번의 요청으로 파악되었다.

위의 다이어그램은 BotDetector.run() 를 실행하면 해당 메서드가 Detector.d 채널로 틱을 보내고 그것을 Bot 이 소비하는 모습을 보인다. 또한 Bot.tick() 이 실행되면 Coin.t 채널에 틱을 보내고 Strategy 가 이를 소비하게 된다. Strategy 가 소비를 하는 모습은 다음과 같다.

.strategy(*coin, Strategy)

func (b *Bot) strategy(coin *Coin, strategy Strategy) {
	for {
		// 이러한 tick 정보가 있다면 현재 시점의 전일 대비 가격 변화율, 마켓의 이름과 같은 정보를 얻어올 수 있다.
		// https://docs.upbit.com/docs/upbit-quotation-websocket#%ED%98%84%EC%9E%AC%EA%B0%80ticker-%EC%9D%91%EB%8B%B5
		t := <-coin.t
        
		// 전략을 실행한다.
		if _, err := strategy.run(b.Accounts, coin, t); err != nil {
			panic(err)
		}
        
		// 전략이 너무 자주 실행되지 않도록 해야한다.
		time.Sleep(time.Second * 1)
	}
 }

type Accounts

Accounts 는 인터페이스다. Accounts 는 업비트 계정을 포함한 테스트용 계정이 구현해야 할 메서드를 가진다. Accounts 가 가져야 하는 메서드 중 중요한 것이 바로 .order() 다. 주문은 봇, 또는 사람이 하지만 논리적으로 계정을 사람, 또는 봇과 동일시하여 Accounts 가 특정 코인에 대해 매수/매도 주문을 낼 수 있다.

type Accounts interface {
	// order 메서드는 주문을 하되 Config.Timeout 만큼이 지나가면 주문을 자동으로 취소한다.
	// 매수/매도에 둘다 사용한다.
	order(b *Bot, c *coin, side string, volume, price float64) (bool, error)
	// 내부에 있는 upbit.API 에서의 접근을 위해 accounts 를 반환해야 한다.
	accounts() ([]map[string]interface{}, error)
}

.order(*Bot, *Coin, string, float64, float64) (bool, error)

오더에서는 실제로 업비트 계정에서는 주문을 요청하고, 테스트 계정에서는 내부의 자산 현황을 갱신하게 된다. 여기서 살펴볼 것은 실사용 계정에서 주문을 넣었으나 체결되지 않고 계속 기다리기만 하면 트래킹 중인 해당 마켓의 전략 고루틴이 락이 되어버릴 수도 있다는 점이다. 따라서 타이머를 두고 체결을 기다렸다가 체결이 되지 않으면 주문을 캔슬한다.

func (acc *Accounts) order(b *Bot, coin *coin, side string, volume, price float64) (bool, error) {
	// 주문...
    
	timer := time.NewTimer(time.Second * 30)
    
	go acc.wait(b, done, uuid)

	select {
	// 주문이 체결되지 않고 무기한 기다리는 것을 방지하기 위해 타임아웃을 지정한다.
	case <-timer.C:
		// 주문 취소
		_, err := b.Client.Call("DELETE", "/order", struct {
			Uuid string `url:"uuid"`
		}{uuid})
		if err != nil {
			return false, err
		}
        // ...
	}
    
    // 계정 갱신...
}

여기서 log.Logger로그를 보내기 위한 채널이다. 이전에 적지는 않았지만, 로그 채널은 봇을 실행하기 이전에 초기화를 별도로 진행한다. 별도로 아래에서 언급하지는 않겠지만 나온김에 이야기했다. 또한 static.Config 객체는 글로벌 객체이며 config.yml 로 부터 Timeout 설정을 얻어와서 매핑한다.

type Strategy

Strategy 또한 인터페이스다. 해당 인터페이스를 만족하는 모든 전략은 봇에서 사용할 수 있도록 구성되었다. 여기서 .register() 는 전략을 실행하기 전에 준비해야 할 것을, .run() 메서드는 전략을 진행한다.

type Strategy interface {
	register(bot *Bot) error // 봇이 실행될 때 전략이 최초로 등록될 때
	boot(bot *Bot, c *coin) error // 코인을 생성하고 전략을 실행하기 직전
	run(bot *Bot, c *coin, t map[string]interface{}) (bool, error) // 전략
}

마치며

트레이딩 봇을 만드는 과정은 흥미롭다. 프레임워크에 해당하는 틀은 어느정도 구성되었기에 이제 전략을 재미나게 생각하는 일만 남았다. 봇은 사실 전략이 제일 중요하다. 전략에 따라 수익이 날 수도 있고 안 날수도 있기 때문이다.

그러나 내가 이렇게 까지 구조적으로 봇을 작성한 이유는 이것을 단순 경험만으로 끝낼게 아니라 무언가 결과를 도출해볼 것이기 때문이다. 또한 이 프로젝트는 나의 첫번째 Go 언어 사이드 프로젝트라는 점에서 의미가 있으며 부가적인 써드파티 라이브러리들을 사용해볼 기회또한 있어서 나름 괜찮은 프로젝트라고 생각한다.

더 읽을거리

Webpack 3 에서 Webpack 5 으로 바꾸기 위해 해야 할 일들

내가 개발한 티스토리 프로젝트 정리!

나만 알고 있기에는 너무 아깝잖아? 그래서 강의를 만들어봤어.

티스토리 구독 서비스 이전에 존재했던, 티스토리 이웃서비스 티네스(Tines) 개발 돌아보기