Бот для Bittrex - продолжаем продолжать

Продолжаем развлекаться - соорудим бота для биржи Bittrex.com.

Регистрацию я расписывать не буду, она довольно тривиальна, а вот с подключением через API «есть нюанс» (с). Дело в том, что биржа параноидальна серьезно относится к безопасности пользователей и сохранности их средств, так что приготовьтесь к многочисленным подтверждениям того, что вы это вы. Так вот.

Подключаем API на Bittrex.com

В меню выбираем Settings – API Keys, и с грустью тоской смотрим на надпись:

To use API Keys, you must have Two-Factor Authentication enabled.

Для использования API необходимо подключить двуфакторную авторизацию. Круто, ну давайте включим. Там же, в Settings, переходим в раздел Two-Factor Authentication и с еще большей болью в сердце видим, что Bittrex использует только и исключительно Google Authenticator.

Пару слов о Google Authenticator: Первый раз я столкнулся с Google Authenticator, когда подключал двуфакторную авторизацию на btc-e.nz. Тогда я подумал, ух ты, как здорово, ставишь приложение на телефон и авторизуешься когда тебе надо, можно симки менять.. Прошло несколько лет, мой телефон давно сломался, я давно купил новый, и вот мне потребовалось зайти на эту биржу и авторизоваться под старым акканутом.. И разверзся ад! Сначала Google меня допрашивал, я ли это, требовал всевозможных подтверждений, гонял по кругу, не верил мне.. Потом вроде заработало. Я попытался авторизоваться на бирже, но теперь биржа начала ругаться на то, что код неправильный. Я сдался, удалил приложение, и пошел делать то, что хотел, на другую биржу. Через несколько месяцев владельца btc-e арестовали, а биржу на месяц прикрыли, потом вроде как перекупили, так что вся эта канитель по сути спасла меня от потери денег. Но Google Authenticator мне все равно с тех пор не нравится.

Так вот. Для двуфакторной авторизации нам нужно установить на телефон Google Authenticator. Это официальное приложение, его можно найти в Play Market и iTunes. После установки приложение попросит вас добавить аккаунт.

Отсканируйте в приложении QR-код с сайта, введите цифры с приложения на сайт, потом перейдите на почту и подтвердите, что вы действительно решили подключить авторизацию. После этого можно работать с ключами API.

Возвращаемся в Settings – API Keys, запускаем Google Authenticator, указываем на странице цифры из приложения, и указываем права доступа – я не стал включать вывод средств, ибо вроде бы незачем. Но вот описание:

В общем я включил первые две опции. Насчет предупреждения не понял, т.к. в API нет метода, что бы торговать по рынку. Ну да ладно.

После выставления настроек вводим новые цифры из Google Authenticator, нажимаем Update Keys и ура! Получилось.

Совет – сразу эти ключи сохраните куда-нибудь. Тут строго относятся к безопасности, если забудете, придется новые создавать.

Проверяем работу через API

Методы для работы API биттрекса расписаны тут. Изменим немного функцию из бота для эксмо, и попробуем что-то сделать.

Я закинул на баланс немного LTC и создал ордер на продажу по завышенной цене. Проверим, видно ли это через API запросы:

import time
import json

Реклама:


import urllib, http.client import hmac, hashlib API_KEY = '...' # обратите внимание, что добавлена 'b' перед строкой API_SECRET = b'...' API_URL = 'bittrex.com' API_VERSION = 'v1.1' # Свой класс исключений class ScriptError(Exception): pass # все обращения к API проходят через эту функцию def call_api(**kwargs): http_method = kwargs.get('http_method') if kwargs.get('http_method', '') else 'POST' method = kwargs.get('method') nonce = str(int(round(time.time()))) payload = { 'nonce': nonce } if kwargs: payload.update(kwargs) uri = "https://" + API_URL + "/api/" + API_VERSION + method + '?apikey=' + API_KEY + '&nonce=' + nonce uri += urllib.parse.urlencode(payload) payload = urllib.parse.urlencode(payload) H = hmac.new(key=API_SECRET, digestmod=hashlib.sha512) H.update(uri.encode()) sign = H.hexdigest() apisign = hmac.new(API_SECRET, uri.encode(), hashlib.sha512).hexdigest() headers = {"Content-type": "application/x-www-form-urlencoded", "Key": API_KEY, "apisign": apisign} conn = http.client.HTTPSConnection(API_URL, timeout=60) conn.request(http_method, uri, payload, headers) response = conn.getresponse().read() conn.close() try: obj = json.loads(response.decode('utf-8')) if 'error' in obj and obj['error']: raise ScriptError(obj['error']) return obj except json.decoder.JSONDecodeError: raise ScriptError('Ошибка анализа возвращаемых данных, получена строка', response) print(call_api(method='/account/getbalances')) print(call_api(method='/market/getopenorders', market='USDT-LTC'))

На выходе получаем:

Баланс: {'success': True, 'message': '', 'result': [{'Currency': 'LTC', 'Balance': 0.48107406, 'Available': 0.47107406, 'Pending': 0.0, 'CryptoAddress': 'LMGJrSr37DYpZGiWEpLzTEz7my6PUu48KP'}]}

Открытые ордера: {'success': True, 'message': '', 'result': [{'Uuid': None, 'OrderUuid': '07dd349c-936f-426e-be6a-c3ad6d6d0bf4', 'Exchange': 'USDT-LTC', 'OrderType': 'LIMIT_SELL', 'Quantity': 0.01, 'QuantityRemaining': 0.01, 'Limit': 56.90798125, 'CommissionPaid': 0.0, 'Price': 0.0, 'PricePerUnit': None, 'Opened': '2017-11-05T08:47:53.18', 'Closed': None, 'CancelInitiated': False, 'ImmediateOrCancel': False, 'IsConditional': False, 'Condition': 'NONE', 'ConditionTarget': None}]}

Отлично, всё работает, можно начинать делать бота.

Получаем данные для индикаторов

В официальном API нет метода, который позволил бы получить значения для свечей. Тем не менее, существует неофициальный API v2.0, который используется самим биттрексом, и о котором они никому ничего не рассказывают. Я обнаружил это, изучая запросы между моим браузером и биржей в панели разработчика (F12 - Network), позже, погуглив, обнаружил что люди уже повытаскивали все запросы и даже их расписали. Эту информацию вы можете найти в гугле, мне же нужна одна функция – получение данных свечей.

 Вот как выглядит запрос:

https://bittrex.com/Api/v2.0/pub/market/GetTicks?marketName=USDT-LTC&tickInterval=fiveMin

Если требуется интервал по полчаса, то fiveMin меняем на thirtyMin, и т.п.

У этих данных, впрочем, есть недостаток – они возвращают данные за прошедшие периоды.. Т.е. если строить по минутам, то отставание составит 3 минуты, если по 5 минут, то как минимум 5 минут и т.п. Для оперативного принятия решений этого мало, поэтому недостающий набор данных я планирую брать с публичного метода getmarkethistory:

https://bittrex.com/api/v1.1/public/getmarkethistory?market=USDT-LTC.

Т.е. последние несколько минут торгов я буду брать с истории торгов, и дополнять ими то, что недополучил вначале – так я соберу наиболее полную картину. Более того, как раз последние несколько секунд и минут могут как раз оказаться решающими. Напишем код, проверим:

import time
import json
import requests
from datetime import datetime

MARKET = 'USDT-BTC'

def get_ticks():
    chart_data = {}
    # Получаем готовые данные свечей
    res = requests.get("https://bittrex.com/Api/v2.0/pub/market/GetTicks?marketName=" + MARKET + "&tickInterval=fiveMin")
    for item in json.loads(res.text)['result']:
        try:
            dt_obj = datetime.strptime(item['T'], '%Y-%m-%dT%H:%M:%S.%f')
        except ValueError:
            dt_obj = datetime.strptime(item['T'], '%Y-%m-%dT%H:%M:%S')
        ts = int(time.mktime(dt_obj.timetuple()))
        if not ts in chart_data:
            chart_data[ts] = {'open': float(item['O']), 'close': float(item['C']), 'high': float(item['H']), 'low': float(item['L'])}

    # Добираем недостающее
    res = requests.get("https://bittrex.com/api/v1.1/public/getmarkethistory?market=" + MARKET)
    for trade in reversed(json.loads(res.text)['result']):
        try:
            dt_obj = datetime.strptime(trade['TimeStamp'], '%Y-%m-%dT%H:%M:%S.%f')
        except ValueError:
            dt_obj = datetime.strptime(item['T'], '%Y-%m-%dT%H:%M:%S')
            
        ts = int((time.mktime(dt_obj.timetuple())/300))*300 # округляем до 5 минут
        if not ts in chart_data:
            chart_data[ts] = {'open': 0, 'close': 0, 'high': 0,'low': 0}

        chart_data[ts]['close'] = float(trade['Price'])

        if not chart_data[ts]['open']:
            chart_data[ts]['open'] = float(trade['Price'])

        if not chart_data[ts]['high'] or chart_data[ts]['high'] < float(trade['Price']):
            chart_data[ts]['high'] = float(trade['Price'])

        if not chart_data[ts]['low'] or chart_data[ts]['low'] > float(trade['Price']):
            chart_data[ts]['low'] = float(trade['Price'])

    return chart_data

# Выведем результат, посмотрим - работаем по времени биржи
chart_data = get_ticks()
for item in sorted(chart_data):
    print(datetime.fromtimestamp(item),chart_data[item])

Результат:

2017-11-05 11:10:00 {'open': 54.75500015, 'close': 54.75500015, 'high': 54.75500015, 'low': 54.75500015}
2017-11-05 11:15:00 {'open': 54.87745003, 'close': 54.89744999, 'high': 54.89744999, 'low': 54.87745003}
2017-11-05 11:20:00 {'open': 54.87755003, 'close': 55.03567601, 'high': 55.03567601, 'low': 54.87755003}
2017-11-05 11:25:00 {'open': 55.03567601, 'close': 55.0, 'high': 55.03567601, 'low': 55.0}
2017-11-05 11:30:00 {'open': 55.00668, 'close': 55.0, 'high': 55.00668, 'low': 55.0}
2017-11-05 11:35:00 {'open': 55.0, 'close': 55.0, 'high': 55.0, 'low': 55.0}
2017-11-05 11:40:00 {'open': 55.0, 'close': 55.0, 'high': 55.0, 'low': 55.0}
2017-11-05 11:45:00 {'open': 55.0, 'close': 55.0, 'high': 55.0, 'low': 55.0}
2017-11-05 11:50:00 {'open': 54.8975, 'close': 54.79500025, 'high': 55.0, 'low': 54.79500025}
2017-11-05 11:55:00 {'open': 54.915338, 'close': 54.79500001, 'high': 54.915338, 'low': 54.79500001}

Для анализа этих данных мы будем использовать MACD, как и в предыдущей статье, потому что, во-первых, это сэкономит время и размер статьи, а во-вторых, потому что мы будем в этот раз использовать значения по другому.

Итак, принцип алгоритма расписан здесь, сделаем небольшую функцию, которая будет возвращать нам тренд и совет, пора или не пора торговать. Я подразумеваю, что тренд растет (growing),  если это либо бык в активной фазе, либо медведь в затухающей. Код выглядит вот так:

def get_macd_advice(chart_data):
    macd, macdsignal, macdhist = talib.MACD(numpy.asarray([chart_data[item]['close'] for item in sorted(chart_data)]),
                                            fastperiod=12, slowperiod=26, signalperiod=9)
    idx = numpy.argwhere(numpy.diff(numpy.sign(macd - macdsignal)) != 0).reshape(-1) + 0

    trand = 'BULL' if macd[-1] > macdsignal[-1] else 'BEAR'

    max_v = 0

    activity_time = False
    growing = False

    for offset, elem in enumerate(macdhist):

        growing = False

        curr_v = macd[offset] - macdsignal[offset]
        if abs(curr_v) > abs(max_v):
            max_v = curr_v
        perc = curr_v / max_v

        if ((macd[offset] > macdsignal[offset] and perc * 100 > BULL_PERC)  # восходящий тренд
            or (
                macd[offset] < macdsignal[offset] and perc * 100 < (100 - BEAR_PERC)
            )
            ):
            activity_time = True

            growing = True

        if offset in idx and not numpy.isnan(elem):
            # тренд изменился
            max_v = curr_v = 0  # обнуляем пик спреда между линиями

    return ({'trand':trand, 'growing':growing})

Соответственно, при работе мы будем вызывать эту функцию, передавать ей полученные ранее данные и узнавать, пора ли покупать/продавать и т.п.

Стратегия

В этот раз предлагаю поступить так – бот будет закупать на падении графика, когда MACD все уже падает, но тренд уже начал разворот, и будет продавать купленное на росте тренда, пока он все еще растет, но уже начинает спад.

Таким образом, он будет стараться купить дешевле, но продать как можно дороже, а если курс вырастет недостаточно и пойдет на спад, то будет выставлять по минимально допустимой цене (с учетом комиссии при покупке, при продаже и требуемого навара).

И это, вроде бы и относительно безопасно, но, как выяснилось, работает оочень долго – сначала бот ждет покупки на спаде, потом ждет роста. С другой стороны, значительно уменьшается шанс залезть на самый пик и никогда ничего не продать.

Но, тем не менее, предусмотрена опция отключения такого поведения - USE_MACD = True заставит бота думать, USE_MACD = False включит ковбойский режим. Если режим MACD включен, управлять настройками задумчивости можно через переменные BEAR_PERC и BULL_PERC (подробнее рассмотрено тут) – выставляйте каждую от 0 и до 101 и смотрите, как лучше.

В общем-то вот код, скачивайте, пользуйтесь, может еще какие-то идеи придут. Удачи вам)

P.S. Если вы раньше не запускали ботов с моего сайта, то знайте, вам нужно установить Python с модулями (описано тут) и ta-lib (описано тут). О всяких других аспектах, таких как работа с API или комиссиями, читайте в статьях цикла, ссылки под этой статьёй. 

Статистика по результатам тестирования

Через 6 суток непрерывной работы бота я снял статистику - выкачал историю сделок с биттрекса, и посчитал чистый доход за вычетом комиссий.

Играл на 8 пар, каждой выделял по 2 доллара. Из-за политики биржи, еще 0.25% прибавлялись к платежам, так что каждый ордер на покупку обходился примерно в 2,00499762 доллара.

Профитность ставил 0.1% с каждой сделки.

В итоге за 6 дней было совершено 144 сделки (72 цикла покупки/продажи), прибыль с каждой составила большей частью 0.1%, в некоторых случаях больше (иногда на пару порядков больше).

Итоговый доход с 16 долларов, заряженных в игру, составил 1,23 - или 7.6%.

Зависших пар (купленных на пике) - не обнаружено.

Продолжаем наблюдение..

Тэги: