Бот для 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, указываем на странице цифры из приложения, и указываем права доступа – я не стал включать вывод средств, ибо вроде бы незачем. Но вот описание:

  • Read Info – Можно получать информацию о балансе и других свойствах аккаунта
  • Withdraw – Вывод средств на указанный вами адрес. Эта опция может быть полезной для проведения арбитража или переводе средств в cold storage.
  • Trade Limit – Позволяет выставлять отложенные ордера по лимиту
  • Trade Market – Позволяет выставлять отложенные ордера по рынку. ПРЕДУПРЕЖДЕНИЕ (так на сайте написано): Не включайте эту опцию, если точно не уверены, что делаете. Покупки по рынку чрезвычайно опасны!

В общем я включил первые две опции. Насчет предупреждения не понял, т.к. в 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%.

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

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


Это статья из цикла "Популярно о бирже"
Все статьи цикла:

Последнее изменение:


Крипто-кошельки для помощи и благодарности проекту:

Bitcoin адрес проекта: [[address]]

Перевод на сумму [[value]] BTC получен. Спасибо!.
[[error]]

Ethereum адрес проекта: [[address]]



Комментарии
07.11.2017 08:11:04
Ув. Андрей, спасибо вам за ваш труд и вашу помощь в освоении автоматизации торговли. Скажите пожалуйста, а для биржи bitfinex планируется написание бота?
ПроголосоватьПроголосовать
0 0
08.11.2017 15:21:00
День добрый ! Для двухфакторки использую FreeOTP Authenticator вместо Google Authenticator , на всякий случай делаю скрин ключа и сохраняю на флешку.

Также интересует бот для биржи WEX.nz (бывшая реанимированная BTCe)
Заранее благодарен !
ПроголосоватьПроголосовать
0 0
08.11.2017 15:42:14
Добрый день.
Вообще пока не планировал ботов для других бирж, но подумаю. За FreeOTP спасибо)
ПроголосоватьПроголосовать
0 0
08.11.2017 17:11:33
На здоровье )
С нетерпением буду ждать робота для WEX, если что )
ПроголосоватьПроголосовать
0 0
11.11.2017 13:46:59
Traceback (most recent call last):
File "C:\Users\Admin\Downloads\bittrex_macd.py", line 68, in
chart_data = get_ticks()
File "C:\Users\Admin\Downloads\bittrex_macd.py", line 39, in get_ticks
res = requests.get("https://bittrex.com/Api/v2.0/pub/market/GetTicks?marketName=" + MARKET + "&tickInterval=fiveMin")
NameError: name 'MARKET' is not defined

имя 'рынок' не определен ?
ПроголосоватьПроголосовать
0 0
12.11.2017 15:21:35
Точно, точно. Обновил статью
ПроголосоватьПроголосовать
0 0
14.11.2017 09:57:30
Андрей подскажите как с Bittrex.com выгрузить в Excel историю торгов.
ПроголосоватьПроголосовать
0 0
14.11.2017 19:37:42
Заходите в раздел Orders - https://bittrex.com/History, там кнопка "Load All" со стрелочкой. Нажимаете, разгадываете капчу и получаете Excel-файл со всеми торгами
ПроголосоватьПроголосовать
0 0
14.11.2017 21:46:38
Это я понял а как скрипто python вытащить в эксель?
ПроголосоватьПроголосовать
0 0
14.11.2017 23:02:07
1. Берем код из первого примера
2. В конце добавляем

trades = call_api(method='/account/getorderhistory')['result']

import os
from openpyxl import Workbook
# Создадим Excel файл
wb = Workbook()
ws = wb.active

for trade in trades:

ws.append(
[
trade['OrderUuid'],
trade['Exchange'],
trade['OrderType'],
trade['Price']
]
)

wb.save(os.path.dirname(os.path.abspath(__file__)) + "/bittrex_excel.xlsx")

3. Запускаем. Рядом с ботом появляется эксель. Дополнительные поля можно добавить, описаны тут https://bittrex.com/home/api, метод /account/getorderhistory

ПроголосоватьПроголосовать
0 0
15.11.2017 14:18:16
Андрей, подскажи пожалуйста как подобрать наиболее подходящую пару для торгов данным ботом на Bittrex?
ПроголосоватьПроголосовать
0 0
17.11.2017 13:03:47
Если именно этим ботом - то я бы рекомендовал бы разбить депозит на небольшие суммы и торговать сразу на максимально возможное количество пар. Не так часто меняется macd, особенно на непопулярных парах, так что по каждой паре количество сделок в день будет небольшим. Соответственно, чем больше пар - тем больше сделок. Если вложить все в одну пару, она может уйти в даун на несколько часов или дней, а так пока одна пара ждет другие работают.
ПроголосоватьПроголосовать
0 0
19.11.2017 22:37:55
Доброго времени суток, Андрей, робот работает исправно, большое спасибо. Но столкнулся с такой проблемой, после покупки ботом крипты, я решил изменить параметры кода и продать валюту в ручную (т.к. думал что с новыми параметрами он не исполнит ордера созданные со старыми параметрами). Но бот все еще берет из БД неисполненные ордера которых нет и пытается создать ордер на продажу. Можно ли обнулить эту БД чтобы начать с чистого листа или что можно сделать?
ПроголосоватьПроголосовать
0 0
20.11.2017 00:12:52
Можно удалить файл local.db из папки с ботом, он начнет вообще все с чистого листа.
Можно внутри базы удалить записи по конкретным ордерам, но наверное вам не так нужна история, её можно и с биржи скачать.
ПроголосоватьПроголосовать
0 0
Пожалуйста, авторизуйтесь, что бы оставить свой комментарий