본문 바로가기
파이썬으로 웹 App 작성하기/파이썬으로 Upbit 자동거래하기

파이썬만 사용해서 커스텀 코인 트레이더 웹사이트 만들기 - 1 : 기본 API

by Slate_Knowledge 2023. 4. 17.
728x90

모의투자 시스템 링크(구글 인증을 통해서 모의투자 자동 가입 가능)(찝찝하면 https://temp-mail.org/ 과 같은 임시 메일 서비스를 이용해 가입후 초기 1회 인증하면 이후 해당 아이디로 로그인 가능)

데모 :

 

이전 포스팅 : 

2023.04.06 - [파이썬으로 웹 App 작성하기/파이썬으로 Upbit 자동거래하기] - Anvil과 파이썬 Upbit API로 커스텀 코인 트레이더 어플 만들기 - 0 : 준비단계 (환경 설정, 분봉차트)

 

Anvil과 파이썬 Upbit API로 커스텀 코인 트레이더 어플 만들기 - 0 : 준비단계 (환경 설정, 분봉차트)

이전 포스팅(2020.03.16 - [파이썬으로 주식 해보기] - PYTHON과 대신증권 API를 이용한 주식 자동화 입문(1)) 에서는, 대신증권 API를 통해서 자동화 봇을 만들어보려고 했었는데, 아무래도 주식 장이 열

doodlrudco.tistory.com

깃허브 주소 :

https://github.com/dlrudco/CoinAutoTrader_Anvil_UPbit

 

GitHub - dlrudco/CoinAutoTrader_Anvil_UPbit

Contribute to dlrudco/CoinAutoTrader_Anvil_UPbit development by creating an account on GitHub.

github.com

** 깃허브 코드는 블로그 진도에 맞추어 계속 업데이트 될 예정

** 이후 업데이트로 블로그 글과 상이하게 바뀔 수 있음. 

기본 거래 API 작성하기

이전 준비단계 포스팅을 통해 개발 준비를 마쳤으니, 그 다음 차례로 본 포스팅에서는 Upbit API reference에서 확인할 수 있는 기본적인 기능들을 향후 웹 서비스에 사용할 수 있도록 준비하는 과정을 수행한다.

본 포스팅에서 다룰 기능들은, 현재가 조회하기, 시장별 정보 조회하기, 주문(지정가 매수,매도, 시장가 매수,매도)하기, 주문 상태 확인하기, 주문 취소하기 이다. 앞으로 다루게 될 자동거래 알고리즘은 상기 기능들을 토대로 작동할 것이기 때문에 필수적인 과정이라 할 수 있겠다.

1. 차트 조회하기

시장가로 매수/매도를 하는 경우에는 사실상 현재가가 필요없기는 하지만, 제일 기본이 되는 정보는 역시 현재가일 것이다. 이전 준비단계에서 쿼리했던 캔들정보로도 어느정도는 이를 얻는것이 가능하긴 해도, 추가적으로 현재 정보를 요청하는 API를 준비한다. <ticker>

import requests

def get_current_price(market_code):
    url = f"https://api.upbit.com/v1/ticker"
    querystring = {"markets": market_code}
    headers = {"accept": "application/json"}
    res = requests.get(url, headers=headers, params=querystring)
    return {'body':res.json(), 'headers':res.headers}

현재가 정보는 캔들 결과와 유사하게 나오는데, 아래의 정보를 담은 json을 하나 반환한다.

현재 시점에서의 candle에 대한 시가, 고가, 저가, 현재가, 이전 종가, 등락 정보, 거래량 등을 담고 있는 json이 반환되므로 향후 이 정보를 자동거래에 사용할 수 있도록 설계할 수 있겠다. 

이때, 마켓 정보를 일일히 손으로 타이핑 해줘야하는 것은 비효율적일 뿐더러, 이후 어플리케이션을 만들었을 때에도 사용에 무리가 생기므로 현재 거래 가능한 마켓을 조회하는 함수도 필요하게 된다. 이 또한 기본적으로 제공되는 API 목록에 있으므로 문서를 참조하여 아래와 같이 작성한다.

def get_market_list():
    url = "https://api.upbit.com/v1/market/all?isDetails=true"
    headers = {"accept": "application/json"}
    res = requests.get(url, headers=headers)
    return {'body':res.json(), 'headers':res.headers}

상기 함수를 실행하면, 아래와 같이 현재 업비트 거래소에 등록되어있는 마켓들의 정보가 담긴 json 규격 정보가 반환된다.

이 정보를 이용해서, 이후에는 아래와 같이 사용자가 검색 및 선택을 한 시장에 대해서 정보를 요청할 수 있도록 할 수 있다.

k2m = {k['korean_name']:k['market'] for k in get_market_list()['body'] if 'KRW' in k['market']}
# --> 현재 마켓 리스트에서 KRW 기준 시장에 대해서만 korean_name to market 정보 저장
import re
tess = [k['korean_name'] for k in get_market_list()['body'] if 'KRW' in k['market']]
regex = re.compile('비트.')
# --> 비트XXX 와 같은 꼴로 이루어진 스트링을 찾기 위한 정규 표현식 컴파일
print([s for s in tess if re.match(regex,s)])
# ['비트코인', '비트코인골드', '비트코인캐시', '비트코인에스브이', '비트토렌트']
get_current_price(k2m['비트코인'])

중간에 잠깐 나오는 re 라이브러리의 정규 표현식 wild-card 검색을 이용한 시장 검색 기능은 추후 어플리케이션 프론트엔드 연동 시 추가적으로 다루도록 하겠다.

2. 주문하기

시장에 대한 현재 정보를 확인했다면, 그 다음은 본인 계좌의 정보를 확인하고 그에 맞는 주문을 넣는것이 순서일 것이다. 가장 먼저 해당 거래종목에 대한 현재 나의 계좌 상태를 조회하는 check_chance 함수를 통해서 아래와 같이 특정 코인에 대한 현재 상황을 조회할 수 있다. (이때, payload는 업비트 API에서 공통으로 요구되는 jwt 인코딩 내용을 묶어둔 utility 함수이다.)

def check_chance(market):
    params = {
    'market': market,
    }
    
    headers = payload.encode_payload(params)

    res = requests.get(creds.server_url + '/v1/orders/chance', params=params, headers=headers)
    return {'body':res.json(), 'headers':res.headers}

상기 check_chance 함수를 호출하면 아래와 같이 마켓 기준 화폐(예시에서는 KRW) 잔고 보유량과 매수/매도시 발생하는 거래 수수료(0.0005 = 0.05%) 가능한 거래 형식 등 거래의 기본 정보와 현재 해당 마켓에 대한 나의 구매(bid) 매도(ask) 계좌 현황 등을 담은 정보가 반환된다.

{
    'bid_fee': '0.0005', 
    'ask_fee': '0.0005', 
    'maker_bid_fee': '0.0005', 
    'maker_ask_fee': '0.0005', 
    'market': 
    	{
            'id': 'KRW-BTC', 
            'name': 'BTC/KRW', 
            'order_types': ['limit'], 
            'order_sides': ['ask', 'bid'], 
            'bid_types': ['limit', 'price'], 
            'ask_types': ['limit', 'market'], 
            'bid': 
            	{
                    'currency': 'KRW', 
                    'min_total': '5000'
                },
            'ask': 
            	{
                    'currency': 'BTC', 
                    'min_total': '5000'
                }, 
            'max_total': '1000000000', 
            'state': 'active'
          }, 
     'bid_account': 
     	{
            'currency': 'KRW', 
            'balance': '13401.84910753', 
            'locked': '0', 
            'avg_buy_price': '0', 
            'avg_buy_price_modified': True, 
            'unit_currency': 'KRW'
         }, 
     'ask_account': 
     	{
            'currency': 'BTC', 
            'balance': '0', 
            'locked': '0', 
            'avg_buy_price': '31413646.3773', 
            'avg_buy_price_modified': False, 
            'unit_currency': 'KRW'
        }
 }

이제 주문에 필요한 잔고 및 현재가 정보를 모두 얻었으니 주문을 넣어볼 차례다. 주문은 지정가와 시장가 매매가 있는데, 지정가 매매는 이후 주문 취소하기와 함께 알아보고 먼저 시장가 매매부터 다룬다. 지정가든 시장가든 실제 API 호출 양식은 아래와 같이 동일한데, 이때의 파라미터를 어떻게 넘기느냐에 따라서 다른 매매 방식을 차용하게 되는 식이다.

def post_order(market:str, side:str, ord_type:str, price:float, volume:float):
    """주어진 파라미터에 상응하는 거래를 요청한다

    :param market: 거래를 요청할 마켓코드
    :type market: str
    :param side: 매수('bid')/매도('ask') 구분
    :type side: str
    :param ord_type: 시장가 주문('market/price')/지정가 주문('limit') 구분
    :type ord_type: str
    :param price: 거래 가격
    :type price: float
    :param volume: 거래 물량
    :type volume: float
    :return: 
        uuid	주문의 고유 아이디	String
        side	주문 종류	String
        ord_type	주문 방식	String
        price	주문 당시 화폐 가격	NumberString
        state	주문 상태	String
        market	마켓의 유일키	String
        created_at	주문 생성 시간	String
        volume	사용자가 입력한 주문 양	NumberString
        remaining_volume	체결 후 남은 주문 양	NumberString
        reserved_fee	수수료로 예약된 비용	NumberString
        remaining_fee	남은 수수료	NumberString
        paid_fee	사용된 수수료	NumberString
        locked	거래에 사용중인 비용	NumberString
        executed_volume	체결된 양	NumberString
        trades_count	해당 주문에 걸린 체결 수	Integer
    :rtype: _type_
    """
    params = {
    'market': market,
    'side': side,
    'ord_type': ord_type,
    'price': price,
    'volume': volume
    }
    if params['price'] is None:
        del params['price']      
    if params['volume'] is None:
        del params['volume']
    
    headers = payload.encode_payload(params)

    res = requests.post(creds.server_url + '/v1/orders', json=params, headers=headers)
    return {'body':res.json(), 'headers':res.headers}

따라서 post_order를 통해 시장가 매매를 수행하려면 아래와 같이 호출하면 된다.

def market_sell_coin(market, volume):
    return post_order(market, 'ask', 'market', None, volume)

def market_buy_coin(market, price):
    return post_order(market, 'bid', 'price', price, None)

시장가로 주문할때는 기준화폐로 price 정보만 넘겨주면, 해당 price에 맞는 양의 코인을 산다. 거꾸로, 시장가 매도 주문의 경우 내가 가진 코인 중 매매할 코인의 양을 지정만 하면 그 양을 현재가에 맞추어 판다.

먼저 구매 기능을 확인하면 아래와 같다. 총 7000(KRW)어치의 코인을 주문했으니, 이때의 0.0005에 해당하는 3.5(KRW)가 수수료로 예비되어 총 locked는 7003.5가 된 것을 볼 수 있으며, 시장가 매수라는 거래 타입도 확인할 수 있다.

market_buy_coin(str(k2m['비트코인']), 7000)['body']

시장가 매수 결과 값.

해당 주문이 제대로 들어갔는지 확인하기 위해서 매매 홈페이지에 들어가 거래내역을 조회해보면, 체결내역에 상기 거래가(created at 2023-04-17T17:02:04) 정상적으로 조회되는걸 볼 수 있다.

시장가 매수 이력 확인

이제 저 체결수량에 맞추어서 다시 시장가 매도를 수행해보자. (수수료 7원 손해봤다)

market_sell_coin(str(k2m['비트코인']), str(0.00017907))['body']

시장가 매도 주문 결과 값.
시장가 매도 이력 확인

반응형

3. 주문 확인하기

지나간 주문을 취소하거나, 주문한 수량이 모두 체결되었는지 확인하는등의 기능은 온전한 거래를 위한 필수적인 기능이라고 할 수 있다. 따라서 Upbit API에서도 해당 기능을 수행할 수 있게끔 지원하는데 uuid 혹은 거래 ID를 이용한 거래상황 조회 함수이다. 추가적으로, 이전 거래 중 특정 상태('done', 'wait' 등)인 거래들의 리스트 조회와 같은 기능도 제공한다.

def check_single_order_status(uuid=None, identifier=None):
    assert uuid or identifier, 'uuid or identifier must be provided'
    assert not (uuid and identifier), 'uuid and identifier cannot be provided at the same time'
    
    params = {'uuid': uuid} if uuid else {'identifier': identifier}
    headers = payload.encode_payload(params)

    res = requests.get(creds.server_url + '/v1/order', params=params, headers=headers)
    return {'body':res.json(), 'headers':res.headers}

def check_all_orders_by_state(market, state : str = None, states: List[str] = None, page: int = 1, limit: int = 100):
    assert state or states, 'state or states must be provided'
    assert not (state and states), 'state and states cannot be provided at the same time'
    
    params = {'states[]': states} if states else {'state': state}
    
    headers = payload.encode_payload(params)

    res = requests.get(creds.server_url + '/v1/orders', params=params, headers=headers)
    body = res.json()
    if isinstance(body, dict):
        body = [body] if body['market'] == market else []
    elif isinstance(body, list):
        body = [r for r in body if r['market'] == market]
    else:
        body = []
    res = {'body':body, 'headers':res.headers}
    return res

    
def check_all_orders_by_ids(market, uuids: List[str] = None, identifiers : List[str] = None, page: int = 1, limit: int = 100):
    assert uuids or identifiers, 'uuids or identifiers must be provided'
    assert not (uuids and identifiers), 'uuids and identifiers cannot be provided at the same time'
    
    params = {'uuids[]': uuids} if uuids else {'identifiers[]': identifiers}
    
    headers = payload.encode_payload(params)

    res = requests.get(creds.server_url + '/v1/orders', params=params, headers=headers)
    return {'body':res.json(), 'headers':res.headers}

제일 먼저 가장 최근에 수행된 시장가 매도(uuid '0db8ae3d-d209-4a0a-b0b8-e56d8151398a')에 대한 정보를 조회해보면 아래와 같이 수행할 수 있다. 상기 주문 직후 결과에서는 wait이던 거래가 done으로 바뀌어 체결상태로 넘어간걸 확인할 수 있다.

check_single_order_status(uuid='0db8ae3d-d209-4a0a-b0b8-e56d8151398a')['body']

단일 주문 상태 확인하기

지금까지 수행된 모든 done(혹은 'wait' --> 지금은 이외의 거래가 없으므로 빈 리스트) 상태의 거래를 조회하는 것은 아래와 같이 하면 된다.(2022년까지도 거슬러 올라가는 리스트가 나오는걸로 봐서 기록 자체는 거의 영구적인듯 하다)

check_all_orders_by_state(str(k2m['비트코인']),state='done')['body']

모든 'done' 상태의 거래 조회

이때, 유심히 보면 아까의 매수거래의 흔적은 찾아볼 수 없는데, 이는 아래와 같이 매수 거래에 대한 single order status를 조회했을 때 나오는 state 문구를 보면 그 이유를 찾을 수 있다.

check_single_order_status('6443240a-dc6d-444a-b1bc-1e4196c09c7a')['body']

알 수 없는 이유(블록체인상에서 거래가 확정되기 전이라서?)로 done이 아닌 cancel 상태가 된 매수주문

따라서 cancel 상태의 주문들을 모두 조회하면 아래와 같이 정상적으로 나오는 걸 확인할 수 있다.

check_all_orders_by_state(str(k2m['비트코인']),state='cancel')['body']

cancel state에서 보이는 매수주문

4. 주문 취소하기

시장가 주문의 경우에는, 현재가를 기준으로 거래를 시도하는 것이기 때문에 거래량이 절망적이지 않은 한 대부분의 경우에 즉석 거래를 보장한다고도 볼 수 있다. 하지만 지정가 매매의 경우 나보다 싸게 팔거나 비싸게 사는 사람이 있는 경우 거래가 기약없이 지연되기도 하게 된다. 따라서 이때의 유연한 거래를 위해 기존에 걸어두었던 주문을 취소하여 새로운 주문을 넣을 수 있도록 하는 기능이 필요하다.

이를 위해 상기 주문, 주문확인 두 과정을 통해서 확인한 정보를 바탕으로 주문을 취소하는 예시를 아래와 같이 보일 수 있다.

지정가 주문

먼저 거래가 체결되지 않을 극단적인 예시를 위해서 아래와 같이 터무니없는 가격으로 비트코인을 사려는 주문을 넣어보자.

def limit_sell_coin(market, price, volume):
    return post_order(market, 'ask', 'limit', price, volume)

def limit_buy_coin(market, price, volume):
    return post_order(market, 'bid', 'limit', price, volume)
    
limit_buy_coin(str(k2m['비트코인']), price=100, volume=60)
#100원에 비트코인 60개 사기

터무니 없는 거래라도 일단 아래와 같이 등록은 된다.

지정가 거래 주문 결과
홈페이지에서도 확인 가능한 미체결 거래내역

이제 갑자기 비트코인 가격이 현재가(대략 4천만원)에서 급락하여 100원이 된다면 거래가 체결이 되겠지만, 그럴 가능성은 매우 적으므로 해당 거래를 취소해보자. 이는 uuid를 통해서 가능한데, 상기 결과로부터 uuid를 얻어서 취소를 바로 할 수도 있고

cancel_order(uuid='5b02c2a4-a8e7-4efc-b8f8-36672515c762')

혹은, 현재 wait 상태에 있는 모든 거래를 조회해서 전부 취소해버릴 수도 있다. 아래의 코드에서 get_remaining_calls는 header 유틸리티에 선언한 것으로, 공식 문서상에서 규정하고 있는 최대 초당, 분당 요청수 제한에 맞추기 위해 return header에 명시되어있는 잔여 요청수를 파싱하고, 현재 초나 분에 요청이 가능한지 여부를 반환하는 기능을 수행한다.

def cancel_all_waiting(market):
    order_list = check_all_orders_by_state(market, 'wait')
    flag = True
    for o in order_list['body']:
        if flag:
            stat = cancel_order(uuid=o['uuid'])
            print(o['uuid'], stat['body']['state'])
        if not header.is_remaining_calls(stat['headers']):
            time.sleep(0.1)
            flag = False
        else:
            flag = True

이후, 모든 거래가 취소된 것을 거래 페이지에서도 확인할 수 있다.

 

모든 waiting 거래가 취소된 미체결 거래내역 창

상기 4가지 카테고리의 기본 API들로부터 더욱 고도화 된 거래 알고리즘 및 인공지능 훈련환경등을 구성할 수 있다. 이는 앞으로 포스팅할 글들에서 다루도록 하겠다.

728x90
반응형

댓글