Бот для Wex.nz

Выкладываю реализацию бота для wex.nz.

Суть его - если ничего не куплено по паре, то выставляет ордер на покупку, отслеживает, если ордер не сработал, отменяет через указанное время, если сработал - выставляет ордер на продажу с профитом, ждет исполнения до победного. Если продажа сработала, начинает все сначала.

Важно: на балансе не должно быть первой валюты! Т.е. если играете на пару ltc_usd, то избавьтесь от всех ltc - купите на них доллары или там мороженное, не знаю, главное, что бы на балансе был ноль! Тогда все будет хорошо, и никаких неприятных сюрпризов не случится.

Для определения цены покупки берет записи из стакана (параметр OFFERS_AMOUNT), и берет из них среднюю. Если указать OFFERS_AMOUNT = 1 то будет брать по текущей лучшей цене.

О запуске - для тех, кто недавно присоединился:

1. Нужно скачать питон версии 3.6+ с официального сайта 

2. После установки запустить командную строку (cmd) вбить туда pip install requests + Enter

3. Код, выложенный на сайте, скопировать в блокнот и сохранить (например wex_bot.py)

3. Получить на Wex ключи API и прописать в файл бота (поставьте галочки trade & info на бирже, при создании ключа).

4. В настройках указать на какую пару хотите играть и на какую сумму

5. Запустить бота (в командной строке введите python путь_к_файлу_wex_bot.py)

Настройка

В коде бота нужно прописать API ключи, вот тут, буква b'' во второй строчке нужна, просто вставьте апи-ключи в кавычки

# Вписываем свои ключи
API_KEY = 'TW0......NB' 
API_SECRET = b'191....02'

Пропишите пару, на которую собираетесь играть, в данном случае у меня пара eur_usd, поэтому на балансе изначально должно быть 0 евро

# Тонкая настройка
CURRENCY_1 = 'eur' 
CURRENCY_2 = 'usd'

Остальное вроде бы прописано, 

CAN_SPEND - это сколько CURRENCY_2 тратить (долларов в данном случае)

ORDER_LIFE_TIME  - через сколько минут отменять buy, если он не сработал

PROFIT_MARKUP - какой навар желаем с каждого раунда купли/продажи


Реклама:


ORDER_LIFE_TIME = 3 # через сколько минут отменять неисполненный ордер на покупку CURRENCY_1
STOCK_FEE = 0.002 # Комиссия, которую берет биржа (0.002 = 0.2%)
OFFERS_AMOUNT = 1 # Сколько предложений из стакана берем для расчета средней цены
CAN_SPEND = 5 # Сколько тратить CURRENCY_2 каждый раз при покупке CURRENCY_1 (5$ в моем случае)
PROFIT_MARKUP = 0.001 # Какой навар нужен с каждой сделки? (0.001 = 0.1%)
DEBUG = True # True - выводить отладочную информацию, False - писать как можно меньше

Непосредственно код:

import os
import json
import requests
import urllib, http.client
import hmac, hashlib
import time

# Вписываем свои ключи
API_KEY = 'TW.....NB' 
API_SECRET = b'191....e02'

# Тонкая настройка
CURRENCY_1 = 'eur' 
CURRENCY_2 = 'usd'

ORDER_LIFE_TIME = 3 # через сколько минут отменять неисполненный ордер на покупку CURRENCY_1
STOCK_FEE = 0.002 # Комиссия, которую берет биржа (0.002 = 0.2%)
OFFERS_AMOUNT = 1 # Сколько предложений из стакана берем для расчета средней цены
CAN_SPEND = 5 # Сколько тратить CURRENCY_2 каждый раз при покупке CURRENCY_1 (5$ в моем случае)
PROFIT_MARKUP = 0.001 # Какой навар нужен с каждой сделки? (0.001 = 0.1%)
DEBUG = True # True - выводить отладочную информацию, False - писать как можно меньше

CURR_PAIR = CURRENCY_1.lower() + "_" + CURRENCY_2.lower()
"""
    Каждый новый запрос к серверу должен содержать увеличенное число в диапазоне 1-2147483646
    Поэтому храним число в файле поблизости, каждый раз обновляя его
"""
nonce_file = "./nonce"
if not os.path.exists(nonce_file):
    with open(nonce_file, "w") as out:
        out.write('1')

# Будем перехватывать все сообщения об ошибках с биржи
class ScriptError(Exception):
    pass
class ScriptQuitCondition(Exception):
    pass
        
def call_api(**kwargs):

    # При каждом обращении к торговому API увеличиваем счетчик nonce на единицу
    with open(nonce_file, 'r+') as inp:
        nonce = int(inp.read())
        inp.seek(0)
        inp.write(str(nonce+1))
        inp.truncate()

    payload = {'nonce': nonce}

    if kwargs:
        payload.update(kwargs)
    payload =  urllib.parse.urlencode(payload)

    H = hmac.new(key=API_SECRET, digestmod=hashlib.sha512)
    H.update(payload.encode('utf-8'))
    sign = H.hexdigest()
    
    headers = {"Content-type": "application/x-www-form-urlencoded",
           "Key":API_KEY,
           "Sign":sign}
    conn = http.client.HTTPSConnection("wex.nz", timeout=60)
    conn.request("POST", "/tapi/", payload, headers)
    response = conn.getresponse().read()
    
    conn.close()

    try:
        obj = json.loads(response.decode('utf-8'))

        if 'error' in obj and obj['error']:
            print(response.decode('utf-8'))
            raise ScriptError(obj['error'])
        return obj
    except ValueError:
        raise ScriptError('Ошибка анализа возвращаемых данных, получена строка', response)

# Узнаем лимиты по парам
pair_settings = json.loads(requests.get("https://wex.nz/api/3/info").text)['pairs']

# Реализация алгоритма
def main_flow():
    
    try:
        # Получаем список активных ордеров
        opened_orders = []
        try:
            wex_orders = call_api(method="ActiveOrders", pair=CURR_PAIR)['return']
            
            for order in wex_orders:
                o = wex_orders[order]
                o['order_id']=order
                opened_orders.append(o)

        except ScriptError:
            pass
        except KeyError:
            if DEBUG:
                print('Открытых ордеров нет')

    
        sell_orders = []
        # Есть ли неисполненные ордера на продажу CURRENCY_1?
        for order in opened_orders:
            if order['type'] == 'sell':
                # Есть неисполненные ордера на продажу CURRENCY_1, выход
                raise ScriptQuitCondition('Выход, ждем пока не исполнятся/закроются все ордера на продажу (один ордер может быть разбит биржей на несколько и исполняться частями)')
            else:
                # Запоминаем ордера на покупку CURRENCY_1
                sell_orders.append(order)
                
        # Проверяем, есть ли открытые ордера на покупку CURRENCY_1
        if sell_orders: # открытые ордера есть
            for order in sell_orders:
                # Проверяем, есть ли частично исполненные
                if DEBUG:
                    print('Проверяем, что происходит с отложенным ордером', order['order_id'])
                # Получаем состояние ордера, если он еще не исполнен, отменяем
                order_status = call_api(method="OrderInfo", order_id=order['order_id'])['return'][str(order['order_id'])]['status']

                time_passed = time.time()  - int(order['timestamp_created'])

                if time_passed > ORDER_LIFE_TIME * 60:
                    # Ордер уже давно висит, никому не нужен, отменяем
                    call_api(method="CancelOrder", order_id=order['order_id']) 
                    raise ScriptQuitCondition('Отменяем ордер: за ' + str(ORDER_LIFE_TIME) + ' минут не удалось купить '+ str(CURRENCY_1))
                else:
                    raise ScriptQuitCondition('Выход, продолжаем надеяться купить валюту по указанному ранее курсу, со времени создания ордера прошло %s секунд' % str(time_passed))
                   
        else: # Открытых ордеров нет
            balances = call_api(method="getInfo")['return']['funds']
            if float(balances.get(CURRENCY_1, 0)) > 0: # Есть ли в наличии CURRENCY_1, которую можно продать?
                """
                    Высчитываем курс для продажи.
                    Нам надо продать всю валюту, которую купили, на сумму, за которую купили + немного навара и минус комиссия биржи
                    При этом важный момент, что валюты у нас меньше, чем купили - бирже ушла комиссия
                    0.00134345 1.5045
                    Поэтому курс продажи может получиться довольно высоким
                """
                wanna_get = (CAN_SPEND + CAN_SPEND * PROFIT_MARKUP)/(1-STOCK_FEE)  # сколько хотим получить за наше кол-во
                print('sell', balances[CURRENCY_1], wanna_get, (wanna_get/float(balances[CURRENCY_1])))
                new_order = call_api(
                    method="Trade",
                    pair=CURR_PAIR,
                    type="sell",
                    rate=round(wanna_get/float(balances[CURRENCY_1]),pair_settings[CURR_PAIR]['decimal_places']),
                    amount=round(balances[CURRENCY_1],8)
                )['return']
               
                print(new_order)
                if DEBUG:
                    print('Создан ордер на продажу', CURRENCY_1, new_order['order_id'])
            else:
                # CURRENCY_1 нет, надо докупить
                # Достаточно ли денег на балансе в валюте CURRENCY_2 (Баланс >= CAN_SPEND)
                if float(balances.get(CURRENCY_2, 0)) >= CAN_SPEND:
                    # Получаем информацию по предложениям из стакана
                    offers = json.loads(requests.get("https://wex.nz/api/3/depth/"+CURR_PAIR+"?limit="+str(OFFERS_AMOUNT)).text)[CURR_PAIR]
                    prices = [bid[0] for bid in offers['bids']]                    
                    try:        
                        avg_price = sum(prices)/len(prices)
                        """
                            Посчитать, сколько валюты CURRENCY_1 можно купить на сумму CAN_SPEND 
                        """
                        # Купить как есть, потом продать с учетом комиссии
                        my_need_price = avg_price
                        my_amount = CAN_SPEND/my_need_price
                        
                        print('buy: кол-во {amount:0.8f}, курс: {rate:0.8f}'.format(amount=my_amount, rate=my_need_price))
                        new_order = call_api(method="Trade", pair=CURR_PAIR, type="buy", rate=round(my_need_price,8), amount=round(my_amount,8))['return']
                       
                        print(new_order)
                        if DEBUG:
                            print('Создан ордер на покупку', new_order['order_id'])
                            
                        
                    except ZeroDivisionError:
                        print('Не удается вычислить среднюю цену', prices)
                else:
                    raise ScriptQuitCondition('Выход, не хватает денег')
        
    except ScriptError as e:
        print('ScriptError', e)
    except ScriptQuitCondition as e:
        print('ScriptQuitCondition', e)
    except Exception as e:
        print("!!!!",e)

while(True):
    main_flow()
    time.sleep(1)

Заключение

Бот проверен, покупку/продажу/отмену создает, в минус не уходит (хотя может купить на пике и соответственно притормозиться и ждать роста). Если будут замечены какие-то косяки в работе, пишите, поправим.

Вот как в процессе тестирования я заработал один цент с 10 долларов (выставлял 0.1% прибыльности):

Кому интересно, как устроено API у Wex и какие там есть методы, можете пройтись по этим ссылкам - официальная документация (https://wex.nz/api/3/docs#info, https://wex.nz/tapi/docs#Trade), а так же посмотреть примеры в статье про Yobit - у них одинаковое API. 

Так что всем удачи и хороших заработков!

Тэги: