Теперь, когда мы разобрались, что такое биржа биткоинов, отложенные ордера и API, пришло время написать своего собственного бота.
Бот будет выполнять рутинную работу за вас – он будет мониторить состояние биржи, отслеживать текущий курс, создавать ордера на покупку по выгодному курсу, и, после их выполнения, продавать купленную валюту.
Бот будет работать на вашем компьютере, подключаться к бирже под вашей учетной записью, все, что вам потребуется – стабильный интернет, наличие некоторой суммы на балансе биржи, ну и установленный интерпретатор Python (о том, как его установить, я писал в этой статье).
Если сделка на покупку не реализуется в течении какого-то времени (у меня это три минуты) бот отменяет ордер и создает новый, с новым курсом.
Если сделка на покупку прошла, то бот создает ордер на продажу, и держит этот ордер до тех пор, пока он не будет целиком исполнен.
Бот берет среднюю цену по рынку за некоторый период (из-за ограничений exmo, за последние 100 сделок, на других биржах я действовал по другому), и создает ордера на покупку с указанной наценкой – т.е. ниже текущей цены рынка, после чего создает ордера на покупку – опять же с указанной наценкой – получается выше цены рынка. В сумму продаж/покупок закладывается комиссия биржи и, таким образом, нивелируется. Совершая сделки, бот отдает бирже требуемый ею кусок, но прибыль для владельца бота остается неизменной.
Бот отслеживает частичное исполнение ордера – он не будет создавать новых ордеров, пока предыдущий не был полностью исполнен или отменен. Если предыдущий ордер был исполнен частично, бот будет ждать завершения всех сделок по этому ордеру.
Бота можно останавливать и запускать в любой момент и с разных компьютеров – он при запуске проверит состояние ордеров, баланса и так далее – нет нужды бояться того, что при перезапуске потеряются ордера, деньги или что-то еще.
Бот неоднократно протестирован в различных режимах – и с локального компьютера, и в качестве серверного процесса, и мультирежиме – торговле одновременно несколькими валютными парами (в текущем примере мультирежима нет, но можно запускать несколько экземпляров бота с разными настройками – они будут работать параллельно).
В рамках подготовки этой статьи (и отладки алгоритма), я играл на сумму 1 доллар 49 центов – и вот какие он сделки совершал (читать снизу вверх):
Если проанализировать доход/расход, то бот принес 3.5 цента за день – при том, что я играл на полтора доллара – это 2.4% со вклада в день.
Не стоит спешить экстраполировать эту сумму на ежемесячный доход, так как на биржах есть периоды как бешеной активности, так и долгого застоя курса. Тем не менее бот алгоритм спроектирован так, что бы не терять деньги на торгах. Если даже график изменился не в лучшую сторону, и продать по выгодной цене не выходит, бот будет ждать позитивного изменения графика вечно.
Так как бот учебный, некоторые вещи упрощены и убраны из кода – нет стоп-лоссов и тейк-профитов, курс берется по последним 100 сделкам, которые возвращает эта биржа, и есть вероятность, что бот купит на пике, и потом долго не сможет продать (тут придется либо ждать, либо продать по курсу рынка, выбор за человеком).
Так же в примере используется торговля по одной валютной паре, хотя вполне возможно изменить алгоритм для торговли несколькими парами параллельно.
Так же для упрощения вес не пишется в локальную базу данных, а делается запрос к API. С одной стороны, это хорошо для бота, так как информация всегда приходит актуальная, с другой стороны – плохо, так как эта биржа ограничивает количество API запросов до 180 в минуту. Код, который написан здесь, будет работать без проблем, но если вы запустите параллельно несколько экземпляров бота, с разными валютными парами, например, вполне можете наткнуться на это ограничение.
Для наглядности составлена блок-схема алгоритма работы – полностью транслировать её в текст я смысла не вижу, поясню основные принципы.
Бот играет на сумму которую вы указали – в данном случае для примера выбрана сумма 10 долларов США. На эту сумму бот старается купить биткойнов по курсу, чуть ниже текущего курса рынка. Если в течении некоторого времени (три минуты в примере) купить не получается, этот ордер на покупку отменяется, и создается новый, чуть ниже текущей цены уже на этот момент времени.
Если же ордер на покупку исполняется, то бот создает отложенный ордер на продажу этой валюты – он старается продать купленную валюту, и получить за это условные 10 долларов + желаемую наценку.
И в том и в другом случае, в отложенные ордера вносится поправка на комиссию биржи – сделка планируется таким образом, что бы, при успешном завершении, биржа смогла взять свой процент, и что бы это никаким образом не сказалось на благополучии игрока.
Все, что бот зарабатывает, не тратится – бот играет на указанную сумму, а полученный излишек просто копится на балансе.
Вот блок-схема работы:
1. Регистрируйтесь на бирже (если еще этого не сделали):
2. Перейдите в Account-settings-API, нажмите “Generate and save”, и получите ключ и подпись:
3. Установите интерпретатор Python 3.4 и выше (описано в этой статье)
4. Создайте файл с названием exmo.py и скопируйте туда код, указанный ниже
5. В коде, в строках 11 и 13, укажите ключи API, полученные в шаге 2
6. В строке 24 укажите сумму, на которую будет играть бот - CAN_SPEND = 1.45 – сейчас указано 1.45 доллара.
7. На балансе не должно быть currency_1 - например, если играете на паре BTC_USD, то BTC заранее переведите в доллары или в другую валюту, а то продаст в минус.
8. Сохраните и запускайте (F5) – бот начнет работать.
Вы можете его запустить, даже если на бирже сейчас нет денег – бот вас предупредит, и просто ничего не купит. Но, конечно, для успешной работы нужно, что бы деньги были :) На 11.04.2017 минимальная сумма на балансе должна составлять примерно 1.5 доллара – это примерно равно минимальной сумме сделки на бирже, 0.001 Btc.
import urllib, urllib.request, http.client import time import json import sys # эти модули нужны для генерации подписи API import hmac, hashlib # ключи API, которые предоставила exmo API_KEY = '' # обратите внимание, что добавлена 'b' перед строкой API_SECRET = b'' # Тонкая настройка CURRENCY_1 = 'BTC' CURRENCY_2 = 'USD' CURRENT_PAIR = CURRENCY_1 + '_' + CURRENCY_2 ORDER_LIFE_TIME = 3 # через сколько минут отменять неисполненный ордер на покупку CURRENCY_1 STOCK_FEE = 0.002 # Комиссия, которую берет биржа (0.002 = 0.2%) AVG_PRICE_PERIOD = 15 # За какой период брать среднюю цену (мин) CAN_SPEND = 10 # Сколько тратить CURRENCY_2 каждый раз при покупке CURRENCY_1 PROFIT_MARKUP = 0.001 # Какой навар нужен с каждой сделки? (0.001 = 0.1%) DEBUG = True # True - выводить отладочную информацию, False - писать как можно меньше STOCK_TIME_OFFSET = 0 # Если расходится время биржи с текущим # Запросить с биржи лимиты и использовать данные в работе PAIR_LIMITS = {} with urllib.request.urlopen("https://api.exmo.com/v1.1/pair_settings") as url: pairs_settings = json.loads(url.read().decode()) if CURRENT_PAIR in pairs_settings: PAIR_LIMITS = pairs_settings[CURRENT_PAIR] else: print("Не удалось найти настройки пары", CURRENT_PAIR, "в ответе от биржи", pairs_settings) sys.exit(1) CURRENCY_1_MIN_QUANTITY = float(PAIR_LIMITS["min_quantity"]) # минимальная сумма ставки - берется из https://api.exmo.com/v1/pair_settings/ PRICE_PRECISION = int(PAIR_LIMITS["price_precision"]) # базовые настройки API_URL = 'api.exmo.com' API_VERSION = 'v1' # Свой класс исключений class ScriptError(Exception): pass class ScriptQuitCondition(Exception): pass # все обращения к API проходят через эту функцию def call_api(api_method, http_method="POST", **kwargs): # Составляем словарь {ключ:значение} для отправки на биржу # пока что в нём {'nonce':123172368123} payload = {'nonce': int(round(time.time()*1000))} # Если в ф-цию переданы параметры в формате ключ:значение if kwargs: # добавляем каждый параметр в словарь payload # Получится {'nonce':123172368123, 'param1':'val1', 'param2':'val2'} payload.update(kwargs) # Переводим словарь payload в строку, в формат для отправки через GET/POST и т.п. payload = urllib.parse.urlencode(payload) # Из строки payload получаем "подпись", хешируем с помощью секретного ключа API # sing - получаемый ключ, который будет отправлен на биржу для проверки H = hmac.new(key=API_SECRET, digestmod=hashlib.sha512) H.update(payload.encode('utf-8')) sign = H.hexdigest() # Формируем заголовки request для отправки запроса на биржу. # Передается публичный ключ API и подпись, полученная с помощью hmac headers = {"Content-type": "application/x-www-form-urlencoded", "Key":API_KEY, "Sign":sign} # Создаем подключение к бирже, если в течении 60 сек не удалось подключиться, обрыв соединения conn = http.client.HTTPSConnection(API_URL, timeout=60) # После установления связи, запрашиваем переданный адрес # В заголовке запроса уходят headers, в теле - payload conn.request(http_method, "/"+API_VERSION + "/" + api_method, payload, headers) # Получаем ответ с биржи и читаем его в переменную response response = conn.getresponse().read() # Закрываем подключение conn.close() try: # Полученный ответ переводим в строку UTF, и пытаемся преобразовать из текста в объект Python obj = json.loads(response.decode('utf-8')) # Смотрим, есть ли в полученном объекте ключ "error" if 'error' in obj and obj['error']: # Если есть, выдать ошибку, код дальше выполняться не будет raise ScriptError(obj['error']) # Вернуть полученный объект как результат работы ф-ции return obj except ValueError: # Если не удалось перевести полученный ответ (вернулся не JSON) raise ScriptError('Ошибка анализа возвращаемых данных, получена строка', response) # Реализация алгоритма def main_flow(): try: # Получаем список активных ордеров try: opened_orders = call_api('user_open_orders')[CURRENCY_1 + '_' + CURRENCY_2] except KeyError: if DEBUG: print('Открытых ордеров нет') opened_orders = [] 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']) try: order_history = call_api('order_trades', order_id=order['order_id']) # по ордеру уже есть частичное выполнение, выход raise ScriptQuitCondition('Выход, продолжаем надеяться докупить валюту по тому курсу, по которому уже купили часть') except ScriptError as e: if 'Error 50304' in str(e): if DEBUG: print('Частично исполненных ордеров нет') time_passed = time.time() + STOCK_TIME_OFFSET*60*60 - int(order['created']) if time_passed > ORDER_LIFE_TIME * 60: # Ордер уже давно висит, никому не нужен, отменяем call_api('order_cancel', order_id=order['order_id']) raise ScriptQuitCondition('Отменяем ордер -за ' + str(ORDER_LIFE_TIME) + ' минут не удалось купить '+ str(CURRENCY_1)) else: raise ScriptQuitCondition('Выход, продолжаем надеяться купить валюту по указанному ранее курсу, со времени создания ордера прошло %s секунд' % str(time_passed)) else: raise ScriptQuitCondition(str(e)) else: # Открытых ордеров нет balances = call_api('user_info')['balances'] if float(balances[CURRENCY_1]) >= CURRENCY_1_MIN_QUANTITY: # Есть ли в наличии CURRENCY_1, которую можно продать? """ Высчитываем курс для продажи. Нам надо продать всю валюту, которую купили, на сумму, за которую купили + немного навара и минус комиссия биржи При этом важный момент, что валюты у нас меньше, чем купили - бирже ушла комиссия 0.00134345 1.5045 """ wanna_get = CAN_SPEND + CAN_SPEND * (STOCK_FEE+PROFIT_MARKUP) # сколько хотим получить за наше кол-во print('sell', balances[CURRENCY_1], wanna_get, (wanna_get/float(balances[CURRENCY_1]))) new_order = call_api( 'order_create', pair=CURRENT_PAIR, quantity = balances[CURRENCY_1], price= "{price:0.{prec}f}".format(prec=PRICE_PRECISION, price=wanna_get/float(balances[CURRENCY_1])), type='sell' ) print(new_order) if DEBUG: print('Создан ордер на продажу', CURRENCY_1, new_order['order_id']) else: # CURRENCY_1 нет, надо докупить # Достаточно ли денег на балансе в валюте CURRENCY_2 (Баланс >= CAN_SPEND) if float(balances[CURRENCY_2]) >= CAN_SPEND: # Узнать среднюю цену за AVG_PRICE_PERIOD, по которой продают CURRENCY_1 """ Exmo не предоставляет такого метода в API, но предоставляет другие, к которым можно попробовать привязаться. У них есть метод required_total, который позволяет подсчитать курс, но, во-первых, похоже он берет текущую рыночную цену (а мне нужна в динамике), а во-вторых алгоритм расчета скрыт и может измениться в любой момент. Сейчас я вижу два пути - либо смотреть текущие открытые ордера, либо последние совершенные сделки. Оба варианта мне не слишком нравятся, но завершенные сделки покажут реальные цены по которым продавали/покупали, а открытые ордера покажут цены, по которым только собираются продать/купить - т.е. завышенные и заниженные. Так что берем информацию из завершенных сделок. """ deals = call_api('trades', pair=CURRENT_PAIR) prices = [] for deal in deals[CURRENT_PAIR]: time_passed = time.time() + STOCK_TIME_OFFSET*60*60 - int(deal['date']) if time_passed < AVG_PRICE_PERIOD*60: prices.append(float(deal['price'])) try: avg_price = sum(prices)/len(prices) """ Посчитать, сколько валюты CURRENCY_1 можно купить. На сумму CAN_SPEND за минусом STOCK_FEE, и с учетом PROFIT_MARKUP ( = ниже средней цены рынка, с учетом комиссии и желаемого профита) """ # купить больше, потому что биржа потом заберет кусок my_need_price = avg_price - avg_price * (STOCK_FEE+PROFIT_MARKUP) my_amount = CAN_SPEND/my_need_price print('buy', my_amount, my_need_price) # Допускается ли покупка такого кол-ва валюты (т.е. не нарушается минимальная сумма сделки) if my_amount >= CURRENCY_1_MIN_QUANTITY: new_order = call_api( 'order_create', pair=CURRENT_PAIR, quantity = my_amount, price="{price:0.{prec}f}".format(prec=PRICE_PRECISION, price=my_need_price), type='buy' ) print(new_order) if DEBUG: print('Создан ордер на покупку', new_order['order_id']) else: # мы можем купить слишком мало на нашу сумму raise ScriptQuitCondition('Выход, сумма для торгов (CAN_SPEND) меньше минимально разрешенной биржей') except ZeroDivisionError: print('Не удается вычислить среднюю цену', prices) else: raise ScriptQuitCondition('Выход, не хватает денег') except ScriptError as e: print(e) except ScriptQuitCondition as e: if DEBUG: print(e) pass except Exception as e: print("!!!!",e) try: balances = call_api('user_info')['balances'] alt_balance = float(balances[CURRENCY_1]) poss_profit = (CAN_SPEND*(1+STOCK_FEE) + CAN_SPEND * PROFIT_MARKUP) / (1 - STOCK_FEE) if float(balances[CURRENCY_1]) > 0: decision = input(""" У вас на балансе есть {amount:0.8f} {curr1} Вы действительно хотите, что бы бот продал все это по курсу {rate:0.8f}, выручив {wanna_get:0.8f} {curr2}? Введите Д/Y или Н/N """.format( amount=alt_balance, curr1=CURRENCY_1, curr2=CURRENCY_2, wanna_get=poss_profit, rate=poss_profit/alt_balance )) if decision in ('N','n','Н','н'): print("Тогда избавьтесь от {curr} (как вариант создайте ордер с ними по другой паре) и перезапустите бота".format(curr=CURRENCY_1)) sys.exit(0) except Exception as e: print(str(e)) while(True): main_flow() time.sleep(1)
Строки 16 и 17 обозначают валютную пару. В данном примере это BTC_USD, но вы можете поменять на любую другую.
Строка 19 - CURRENCY_1_MIN_QUANTITY = 0.001. Это минимальная ставка, которая допускается на бирже. Для разных валют она разная, и, вообще, стоило бы получать её автоматически через API запрос. Но это усложнит код, поэтому я указал её как константу. Тем не менее, если вы планируете торговать другой валютой, вам следует поменять это значение, иначе торговля может затрудниться.
Строка 21 - ORDER_LIFE_TIME = 3. Если ордер на покупку не сыграл, то через сколько минут отменить его и создать новый, с новой ценой, более приближенной к текущим реалиям.
Строка 22 - STOCK_FEE = 0.002. Комиссия биржи за совершенную сделку. Непохоже, что бы она когда-то менялась, но, тем не менее, вы, при необходимости, сможете поменять её здесь если понадобится.
Строка 23 - AVG_PRICE_PERIOD = 90. Бот, в идеале, смотрит сделки за последние 90 минут, что бы узнать среднюю цену, в данной реализации он получает список совершенных сделок, и берет те из них, кто моложе 90 минут. Другой вопрос, что биржа не возвращает больше 100 записей, так что в данном случае число 90 сильно завышено.
Строка 24 - CAN_SPEND = 1.45. Важный параметр – сумма денег, которую вы доверяете боту для игры. В данном случае – 1 доллар 45 центов. Это удобно в том случае, когда бот играет на одну валютную пару, а вы – на другую, ну и еще гарантирует, что бот не проиграет всё, что нажито. В общем, чем больше эта сумма, тем больше денег он может заработать.
Строка 25 - PROFIT_MARKUP = 0.001. Это сумма наценки, которую вы хотите получить. В данном случае – это 0.1% от ставки. Чем больше это число, тем больше вы заработаете, но и курс будет раздуваться больше – т.е. вам придется дольше ждать исполнения сделки. Допускается дальнейшее дробление – например, число 0.00111 подходит. Если указать ноль, то бот будет работать вхолостую, обогащая биржу. Вы при этом, терять и зарабатывать не будете.
В строке 26 указано DEBUG = True. С этим параметром будет очень «разговорчивым», он будет комментировать каждое свое действие. Когда вам это надоест, советую вместо True написать False – тогда бот будет писать только по делу.
Так же не помешало бы в код добавить обработку некоторых исключительных ситуаций, перевести на ООП и так далее – но я не вижу смысла усложнять учебный код. Тот, кто заинтересуется, сможет сделать всё это и сам. Ну, или не делать, а просто пользоваться ботом как он есть :)
Надеюсь, этот бот будет для вас полезен – и буду признателен обратной связи. Расскажите, каких результатов вы добились при использовании, с какими трудностями столкнулись и какие моменты показались вам непонятными.
Желаю вам стабильных, хороших заработков!