Продолжаем развлекаться - соорудим бота для биржи Bittrex.com.
Регистрацию я расписывать не буду, она довольно тривиальна, а вот с подключением через API «есть нюанс» (с). Дело в том, что биржа параноидальна серьезно относится к безопасности пользователей и сохранности их средств, так что приготовьтесь к многочисленным подтверждениям того, что вы это вы. Так вот.
В меню выбираем 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 биттрекса расписаны тут. Изменим немного функцию из бота для эксмо, и попробуем что-то сделать.
Я закинул на баланс немного 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%.
Зависших пар (купленных на пике) - не обнаружено.
Продолжаем наблюдение..