나만의 poloniex HTS를 만들어 보자 (1) – API 살펴보기

in kr •  7 years ago  (edited)

나만의 poloniex HTS를 만들어 보자 (1) – API 살펴보기

안녕하세요. 평소에 가끔 들려 정보만 얻다가 저도 ‘뭔가 도움이 되는 것을 해보자‘ 라는 생각으로 가입하고 포스팅을 진행해 보았습니다. 이 포스팅은 제가 최근 만들고 있는 poloniex HTS (home trading service)를 공개하면서 다른 분들도 쉽게 따라 만들 수 있는 것을 목적으로 하고 있습니다. 저도 최근에 python을 배우면서 만들고 있는 중이라 미흡한 점이 있을 수 있습니다. 사실 대단한 것을 만드는 것은 아니지만, Poloniex에 공개된 API (Application Programming Interface)를 사용해서 자신이 만든 규칙에 따라 자동으로 시세를 확인하고 거래를 진행하는 작업에 관심이 있으신 분들이 많이 계신 듯 하여 시작하여 봤습니다.
시작하기에 앞서 포스팅에 대한 몇가지 설명을 그리고자 합니다.

  1. 이 프로그램은 Python 2.7 언어로 만들어 졌습니다 (2.7.13). 제가 사용하는 환경은 윈도우10 64bit, IDE (Integrated development environment)는 PyCharm Community edition 2017.1.2 이며 anaconda 4.3.1 (64bit)를 사용하였습니다. 다른 것은 개인의 사정에 따라 맞춰가면 되지만 python 개발에 익숙하지 않으신 분은 anaconda만큼은 반드시 설치하고 따라해 주시면 되겠습니다.
  2. 이 프로그램은 공개된 API의 wrapper를 사용하여 – 조금 수정하여 – 만들었습니다. 해당 프로그램을 만드는데 참고가 된 자료들은 다음과 같습니다.
    https://poloniex.com/support/api/ - poloniex에서 제공하는 document입니다
    https://pastebin.com/fbkheaRb - 위 링크에 있는 wrapper 입니다. (필요에 따라 수정하거나 추가하였습니다.)
    https://github.com/Syriven/poloniex-holdings-tracker - 위 wrapper를 사용하여 간단하게 시세확인 등을 할 수 있는 예제입니다.
    https://wikidocs.net/53 - Python 2.7에 대한 간단한 설명이 있습니다. Python을 배우면서 많은 참고를 하였습니다. 이 포스팅은 프로그래밍 언어강좌가 아니기 대문에 python 언어를 처음 접하시는 분은 먼저 이것부터 읽고 시작하시면 많은 도움이 될 것 같습니다.
  3. 자동 거래시스템은 매우 위험합니다. OTP 사용등의 검증을 거치지 않기 때문에 private key만 알면 해커들이 재산의 손실을 낼 가능성이 있고, 급등락 하는 시세에서 빠르게 거래가 이루어 지기 때문에 그에 반응하지 못하고 쉽게 큰 손실이 날 가능성도 존재합니다. 또한 프로그램에 예상치 못한 문제가 있을 가능성도 존재합니다. 따라서 모든 것은 본인의 동의와 책임하에 진행되어야 하며 저는 이 포스팅을 따라하거나 참조하여 생긴 재산상의 손실등의 어떠한 상황에 대해서도 책임을 질 수 없음을 먼저 말씀드립니다.
  4. 개발 환경을 구축하고 Poloniex의 API key를 얻어내는 과정등에 대해서는 따로 적지는 않았습니다. 하지만 이 포스팅을 보고 따라하고 싶으신 분들 중에서는 프로그래밍 언어를 다뤄본 경험이 전혀 없어서 도움이 필요하신 분이 있을 가능성도 생각하고 있습니다. 구글링을 하면 어렵지 않게 찾을 수 있기는 하지만 요청이 많을 경우 환경설정을 하는 부분부터 설명 드리도록 하겠습니다.

서론이 길어지네요. 제가 앞으로 포스팅을 하려는 순서는 다음과 같습니다.

  1. API 살펴보기 – 본격적으로 프로그램을 제작하기에 앞서 Poloniex 에서는 어떤 API들을 제공하고 있는지에 대해 간단하게 알아봅니다. 위에 설명한 Wrapper를 중심으로 설명하나 제 편의상 약간의 수정이나 추가된 부분들이 존재합니다.
  2. Coin 정보 불러오기 – 각 coin들의 (암호화폐들을 의미합니다. 편의상 coin이라고 부르도록 하겠습니다) 최근 24시간 거래량, 최근 거래의 거래가격 등의 대략적인 정보들을 불러오는 예제를 만들어봅니다.
  3. 각 Coin의 chart 정보 불러오기 – 각 coin의 chart 정보를 불러와서 그래프를 그려보는 예제를 만들어봅니다.
  4. 조건에 맞는 coin을 찾고 거래하기 – 특정 조건을 지정한 후 그 조건을 만족하면 사용자에게 e-mail 알림과 거래가 진행되는 프로그램을 제작합니다.
    저도 아직 만들고 있는 프로그램이기 때문에 진행도중 수정이 될 수 있습니다만 일단 이런 순서대로 진행을 계획하고 있습니다. 1-2주에 한 부씩 올리는 것을 목표로 하고있는데 과연 그럴 수 있을까요? 그러면 1부를 시작하도록 하겠습니다.
  1. API 살펴보기

먼저 진행할 내용은 Poloniex의 API를 살펴보는 것입니다. API란 poloniex server와 HTS 프로그램간의 통신을 할 때 어떠한 형식으로 서버에 요청을 하는지에 양식이라고 보시면 됩니다. 우리가 은행이나 관공서에 어떠한 일을 처리해 달라고 요청을 할 때 일정한 양식에 따라 서류를 작성하여 제출하는 것과 같은 원리라고 보시면 됩니다. 우리가 poloniex 서버에 정보를 요청하거나 거래를 요청할 때도 정해진 양식에 따라 프로그램을 작성해서 요청을 해야 그에 대한 업무를 처리해 줍니다. 그런데 그런 양식은 너무나 복잡한 형태로 만들어 졌기 때문에 우리가 요청을 할 때 마다 일일이 그 양식을 적는다면 프로그램이 너무 복잡해 질 것입니다. 그래서 거래소 측은 wrapper라는 것을 제공해 줍니다. Wrapper란 요청에 사용되는 몇 개의 간단한 조건을 알려주면 거래소에서 쓰는 복잡한 양식으로 자동으로 변환해 주는 편리한 함수입니다. 아래는 앞으로 우리가 사용할 wrapper들이 어떤 것이 있는지, 그 내용들은 무엇이 있는지를 간략히 설명하도록 하겠습니다. 직접 손으로 따라 적으시면서 만들어 보시면 큰 도움이 될 것입니다. 편의상 Poloniex에 링크되어 있는 것과 약간의 차이가 있습니다.

파일명 poloniex_wrapper.py

#import libraries
import urllib
import urllib2
import json
import time
import hmac, hashlib
import datetime as DT
import calendar
import os

앞으로 사용할 libraries입니다. Library는 자주 사용되는 기능들을 쉽게 쓸 수 있도록 만들어준 함수나 class 등을 포함하고 있습니다. 앞서 말씀드린 anaconda에는 위 libraries가 모두 담겨져 있으니 우리는 해당 기능을 일일이 만들 필요 없이 그냥 가져다 사용하면 됩니다.

def createTimeStamp(datestr, format="%Y-%m-%d %H:%M:%S"):
    utcTime = DT.datetime.strptime(datestr, format)
    return calendar.timegm(utcTime.utctimetuple())

Timestamp를 만들어주는 함수 입니다. Poloniex에 요청하는 자료를 만들 때는 우리가 사용하는 ‘2017년 5월 21일 1시 8분 23초’ 같은 시간이 아니라 각 시간에 대응되는 serial number를 주어야 합니다. 이 각 시간에 대응하는 serial number를 timestamp라고 합니다. 위 함수는 사람이 알아볼 수 있는 시간형식, 예를 들면 ‘2017-05-21 01:08:23’ 을 poloniex가 알아볼 수 있는 timestamp로 변환해 주는 역할을 하는 함수입니다.
참고로 poloniex는 세계 각국에서 이용되는 거래소입니다. 따라서 거래하는 사람마다 각자 국가에 따라 서로 다른 시간에 거래를 합니다. 그렇다면 거래소는 어떤 시간을 쓸까요? 바로 세계협정시(Coordinated universal time; UTC)입니다. Poloniex에 시간을 알려줄때는 반드시 UTC로 알려주어야 한다는 점, 또 poloniex에서 시간을 알려줄때도 UTC로 알려준다는 점을 반드시 알고 있으셔야 합니다.

class poloniex:
    def __init__(self, APIKey, Secret):
        self.APIKey = APIKey
        self.Secret = Secret

Class는 사람, 핸드폰 같은 특정한 실체가 없는 범주를 의미합니다. 사람이 실체가 없다는 말에 의아한 분이 있으실 텐데 이런 질문을 보시면 금방 이해가 될 것입니다.
‘사람의 나이는 어떻게 되나요?’ (인류의 역사에 대한 질문이 아닙니다.)
‘사람의 이름이 무엇인가요?’
이름이나 나이 등의 속성은 글을 작성하는 저나 이 글을 읽고 계신 독자님 등의 개인 (객체지향 프로그래밍 언어들 에서는 object 혹은 instance라고 부릅니다) 에게 존재하는 것이지 사람이라는 범주(프로그래밍 언어에서는 class라고 부릅니다) 에 존재하는 것은 아니지요. 객체지향 프로그래밍 언어들 에서는 object는 두가지 요소로 이루어 진다고 이야기 하는데 하나는 object를 이루는 속성(variables)이고 다른 하나는 행동(method)입니다. 앞의 사람예를 다시 꺼내자면 속성이란 사람의 나이, 이름 등이고, 행동은 달리기, 밥먹기 등을 이야기 할 수 있지요. 이번에 만드는 poloniex란 class에서는 두가지 속성을 갖습니다. 그것은 바로 poloniex에 접근하기 위한 APIkey와 secretkey이죠. Object가 생성되기 전까지는 이러한 값들이 존재하지 않습니다. 이 값들은 object가 생성될 때 위의 init이라는 함수(함수는 앞서 말씀드린 object의 행동, 즉 method를 의미합니다)가 불리면서 받아오는 역할을 하게 됩니다. 앞에 ‘self.’ 가 붙어있는 것을 보실 수 있을 텐데 이는 함수내에 정의되는 내부 변수들과 class의 variable과의 구분을 위해 이런 식으로 접근을 합니다. 자세한 사용법은 추후에 다시 설명 드리도록 하겠습니다.

def api_query(self, command, req={}):

        if (command == "returnTicker" or command == "return24Volume"):
            ret = urllib2.urlopen(urllib2.Request('https://poloniex.com/public?command=' + command))
            return json.loads(ret.read())
        elif (command == "returnOrderBook"):
            ret = urllib2.urlopen(urllib2.Request(
                'https://poloniex.com/public?command=' + command + '&currencyPair=' + str(req['currencyPair'])))
            return json.loads(ret.read())
        elif (command == "returnMarketTradeHistory"):
            ret = urllib2.urlopen(urllib2.Request(
                'https://poloniex.com/public?command=' + "returnTradeHistory" + '&currencyPair=' + str(
                    req['currencyPair'])))
            return json.loads(ret.read())
        else:
            req['command'] = command
            req['nonce'] = int(time.time() * 1000)
            post_data = urllib.urlencode(req)

            sign = hmac.new(self.Secret, post_data, hashlib.sha512).hexdigest()
            headers = {
                'Sign': sign,
                'Key': self.APIKey
            }

            ret = urllib2.urlopen(urllib2.Request('https://poloniex.com/tradingApi', post_data, headers))
            jsonRet = json.loads(ret.read())
            return self.post_process(jsonRet)

아래 설명드릴 함수 (method)에서 사용되는 method입니다. 우리가 이것을 직접 사용하지는 않기 때문에 자세히 설명 드리지는 않겠지만 한가지만 집고 넘어가겠습니다. 이 코드를 보시다보면 중간중간 json이라는 단어가 나오는 것을 보실 수 있으실 겁니다. JSON은 JavaScript Object Notation의 약자로 서로 다른 프로그래밍 언어들끼리 정보를 주고받을 때 사용하는 형식입니다. “변수이름:변수의 값” 형식으로 이루어져 있으며 아래는 한가지 예를 보여주고 있습니다.
"BTC_ETH":{"id":148,"last":"0.06288890","lowestAsk":"0.06288890","highestBid":"0.06260001","percentChange":"0.04034574","baseVolume":"65226.58242374","quoteVolume":"1040824.91707012","isFrozen":"0","high24hr":"0.06587350","low24hr":"0.05937060"}
Python에서는 json parser를 제공해서 자동으로 변수 이름과 값을 나누어 주기 때문에 이 복잡한 형식의 문자들을 어떻게 잘라서 이름과 값을 추출해야 하지 하는 걱정은 하지 않으셔도 됩니다.
JSON에 대해 더 알고싶으신 분은 짧은 강좌가 있으니 한번 들어보시면 좋을 것 같습니다.

https://opentutorials.org/course/1375/6844

def returnTicker(self):
        return self.api_query("returnTicker")

위 api_query를 사용하여 coin들의 정보를 불러오는 함수입니다. 위의 JSON example에 BTC_ETH 있는 정보가 이 함수를 통해 얻어진 정보 중 일부입니다.

def returnChartData(self, currencyPair, period, start, end):
        #currencyPair: example: BTC_XRP
        #period: the duration of a tic unit: min example: 15 (min)
        #start: start time /format="%Y-%m-%d %H:%M:%S"
        #end: end time /format="%Y-%m-%d %H:%M:%S"

        startTs = createTimeStamp(start)
        endTs = createTimeStamp(end)

        periodSec = period * 60
        ret = urllib2.urlopen(urllib2.Request(
            'https://poloniex.com/public?command=returnChartData&currencyPair=' + str(currencyPair) + '&start=' + str(startTs) + '&end=' + str(endTs) + '&period=' + str(periodSec)
        ))

        return json.loads(ret.read())

차트 데이터를 받아오는 함수입니다. 어떠한 coin을 거래할 것인지(BTC_XRP 등), chart의 tic은 몇분으로 할 것인지 (15분 등), 자료를 받아오려는 기간의 시작과 끝은 언제 인지 (2017-05-21 2:15:26 의 형식으로 입력해야 합니다.)등을 입력하면 됩니다. 이때 시간은 반드시 UTC로 해야 원하는 답을 얻을 수 있습니다. 이렇게 요청을 했을 때 받아오는 값은 다음과 같습니다.

Volume: 해당 tic의 거래량(기준통화 (BTC) 단위)
quoteVolume: 해당 tic의 거래량 (거래 대상 통화 (XRP) 단위)
high: 해당 tic의 최고점
low: 해당 tic의 최저점
date: 해당 tic이 시작하는 timestamp
close: 해당 tic이 끝날 때 (혹은 해당 tic의 가장 마지막에 거래된) 가격
weightedAverage: 해당 tic의 거래량이 반영된 평균 거래가격
open: 해당 tic이 시작할 때의 가격

# Places a buy order in a given market. Required POST parameters are "currencyPair", "rate", and "amount". If successful, the method will return the order number.
    # Inputs:
    # currencyPair  The curreny pair
    # rate          price the order is buying at
    # amount        Amount of coins to buy
    # Outputs:
    # orderNumber   The order number
    def buy(self, currencyPair, rate, amount):
        return self.api_query('buy', {"currencyPair": currencyPair, "rate": rate, "amount": amount})

구매를 요청하는 함수입니다. 거래 통화 쌍(BTC_XRP), 구매 가격, 그리고 구매량을 적으면 구매 요청이 됩니다. 요청이 실패하거나 거절당할 수 있으니 그에 대한 대비도 필요할 수 있습니다. https://poloniex.com/support/api/ 문서를 참조해 주세요.

오늘은 앞으로 사용할 함수들에 대해 간단하게 살펴보았습니다. 사용 예정인 것들만 한번 간단하게 살펴 보았는데 제가 가지고 있는 파일을 모두 올려드리자면 아래와 같습니다.

import urllib
import urllib2
import json
import time
import hmac, hashlib
import datetime as DT
import calendar
import os




def createTimeStamp(datestr, format="%Y-%m-%d %H:%M:%S"):
    utcTime = DT.datetime.strptime(datestr, format)
    return calendar.timegm(utcTime.utctimetuple())




class poloniex:
    def __init__(self, APIKey, Secret):
        self.APIKey = APIKey
        self.Secret = Secret

    def post_process(self, before):
        after = before

        # Add timestamps if there isnt one but is a datetime
        if ('return' in after):
            if (isinstance(after['return'], list)):
                for x in xrange(0, len(after['return'])):
                    if (isinstance(after['return'][x], dict)):
                        if ('datetime' in after['return'][x] and 'timestamp' not in after['return'][x]):
                            after['return'][x]['timestamp'] = float(createTimeStamp(after['return'][x]['datetime']))

        return after

    def api_query(self, command, req={}):

        if (command == "returnTicker" or command == "return24Volume"):
            ret = urllib2.urlopen(urllib2.Request('https://poloniex.com/public?command=' + command))
            return json.loads(ret.read())
        elif (command == "returnOrderBook"):
            ret = urllib2.urlopen(urllib2.Request(
                'https://poloniex.com/public?command=' + command + '&currencyPair=' + str(req['currencyPair'])))
            return json.loads(ret.read())
        elif (command == "returnMarketTradeHistory"):
            ret = urllib2.urlopen(urllib2.Request(
                'https://poloniex.com/public?command=' + "returnTradeHistory" + '&currencyPair=' + str(
                    req['currencyPair'])))
            return json.loads(ret.read())
        else:
            req['command'] = command
            req['nonce'] = int(time.time() * 1000)
            post_data = urllib.urlencode(req)

            sign = hmac.new(self.Secret, post_data, hashlib.sha512).hexdigest()
            headers = {
                'Sign': sign,
                'Key': self.APIKey
            }

            ret = urllib2.urlopen(urllib2.Request('https://poloniex.com/tradingApi', post_data, headers))
            jsonRet = json.loads(ret.read())
            return self.post_process(jsonRet)

    def returnTicker(self):
        return self.api_query("returnTicker")

    def return24Volume(self):
        return self.api_query("return24Volume")

    def returnOrderBook(self, currencyPair):
        return self.api_query("returnOrderBook", {'currencyPair': currencyPair})

    def returnMarketTradeHistory(self, currencyPair):
        return self.api_query("returnMarketTradeHistory", {'currencyPair': currencyPair})

    def returnChartData(self, currencyPair, period, start, end):
        #currencyPair: example: BTC_XRP
        #period: the duration of a tic unit: min example: 15 (min)
        #start: start time /format="%Y-%m-%d %H:%M:%S"
        #end: end time /format="%Y-%m-%d %H:%M:%S"

        startTs = createTimeStamp(start)
        endTs = createTimeStamp(end)

        periodSec = period * 60
        ret = urllib2.urlopen(urllib2.Request(
            'https://poloniex.com/public?command=returnChartData&currencyPair=' + str(currencyPair) + '&start=' + str(startTs) + '&end=' + str(endTs) + '&period=' + str(periodSec)
        ))

        return json.loads(ret.read())

    # Returns all of your balances.
    # Outputs:
    # {"BTC":"0.59098578","LTC":"3.31117268", ... }
    def returnBalances(self):
        return self.api_query('returnBalances')

    # Returns your open orders for a given market, specified by the "currencyPair" POST parameter, e.g. "BTC_XCP"
    # Inputs:
    # currencyPair  The currency pair e.g. "BTC_XCP"
    # Outputs:
    # orderNumber   The order number
    # type          sell or buy
    # rate          Price the order is selling or buying at
    # Amount        Quantity of order
    # total         Total value of order (price * quantity)
    def returnOpenOrders(self, currencyPair):
        return self.api_query('returnOpenOrders', {"currencyPair": currencyPair})

    # Returns your trade history for a given market, specified by the "currencyPair" POST parameter
    # Inputs:
    # currencyPair  The currency pair e.g. "BTC_XCP"
    # Outputs:
    # date          Date in the form: "2014-02-19 03:44:59"
    # rate          Price the order is selling or buying at
    # amount        Quantity of order
    # total         Total value of order (price * quantity)
    # type          sell or buy
    def returnTradeHistory(self, currencyPair):
        return self.api_query('returnTradeHistory', {"currencyPair": currencyPair})

    # Places a buy order in a given market. Required POST parameters are "currencyPair", "rate", and "amount". If successful, the method will return the order number.
    # Inputs:
    # currencyPair  The curreny pair
    # rate          price the order is buying at
    # amount        Amount of coins to buy
    # Outputs:
    # orderNumber   The order number
    def buy(self, currencyPair, rate, amount):
        return self.api_query('buy', {"currencyPair": currencyPair, "rate": rate, "amount": amount})

    # Places a sell order in a given market. Required POST parameters are "currencyPair", "rate", and "amount". If successful, the method will return the order number.
    # Inputs:
    # currencyPair  The curreny pair
    # rate          price the order is selling at
    # amount        Amount of coins to sell
    # Outputs:
    # orderNumber   The order number
    def sell(self, currencyPair, rate, amount):
        return self.api_query('sell', {"currencyPair": currencyPair, "rate": rate, "amount": amount})

    # Cancels an order you have placed in a given market. Required POST parameters are "currencyPair" and "orderNumber".
    # Inputs:
    # currencyPair  The curreny pair
    # orderNumber   The order number to cancel
    # Outputs:
    # succes        1 or 0
    def cancel(self, currencyPair, orderNumber):
        return self.api_query('cancelOrder', {"currencyPair": currencyPair, "orderNumber": orderNumber})

    # Immediately places a withdrawal for a given currency, with no email confirmation. In order to use this method, the withdrawal privilege must be enabled for your API key. Required POST parameters are "currency", "amount", and "address". Sample output: {"response":"Withdrew 2398 NXT."}
    # Inputs:
    # currency      The currency to withdraw
    # amount        The amount of this coin to withdraw
    # address       The withdrawal address
    # Outputs:
    # response      Text containing message about the withdrawal
    def withdraw(self, currency, amount, address):
        return self.api_query('withdraw', {"currency": currency, "amount": amount, "address": address})
Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  
Loading...

환영합니다 ^^

블록체인 주제 관련 게시글 태그에는 한국어 태그로 #kr과 블록체인 및 암호화폐 관련 게시글 태그인 #coinkorea 태그를 붙여 주시면 더 많은 사람들이 보실 수 있을 것 같습니다.
그와 관련된 게시글을 한번 읽어주시고 동참해주시면 정말 감사하겠습니다.
Steemit KR 커뮤니티 CoinKorea 프로젝트

저도 추후 이글을 보면서 저만의 HTS를 만들어봐야겠습니다 ㅎㅎ
정말 좋은 개발 가이드가 될 것 같습니다 다음 글도 기대하고 있겠습니다. :)

Welcome to Steemit!

감사합니다.
태그는 수정했어요.

Congratulations @axiomier! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 2 years!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Vote for @Steemitboard as a witness to get one more award and increased upvotes!