Сразу скажу, в этой статье не будут описаны алгоритмы торговли, хотя они есть и приносят прибыль. Моя цель показать вам удобный инструмент для создания ботов и поделится результатом пятилетней работы. Если кому-то не интересны боты, то здесь есть одно техническое решение, которое как вишенка на торте может многих заинтересовать, аналог dbms_pipe в ORACLE, реализованный на чистом pgsql, ну а также много других моих технических решений. Это не основная моя работа, скорее хобби. Сначала был просто интерес написать алгоритм, который гарантировал если не плюс, то хотя бы сохранение средств на падающем рынке, про растущий рынок говорить не интересно там и так все хорошо. По образованию я не экономист, а советский айтишник, и в качестве своих невидимых соперников рассматривал волков с Уолт стрит. Для меня это был вызов сродни изобретению вечного двигателя, но с возможностью реализации. В разработке алгоритма торговли мне сильно помогла книга Эдвина Лефевра "Воспоминания биржевого спекулянта". Итак, приступим.
Требование к боту
Бот должен зарабатывать денег больше чем банковские вклады, то есть больше чем 5% в валюте. Сразу скажу это достигнуто, бот работает уже 3 года и доход даже на падающем рынке не опускался ниже 35%. В хороший год достигал 100%. Алгоритмы здесь рассматривать не буду. Так же понимаю, что возможны и убытки и математически доказать, что всегда будет прибыль невозможно, и я всегда готов что будут убытки.
. Легкость разработки алгоритмов и прогон этих алгоритмов на исторических данных. Я постараюсь вас убедить в этой статье, что лучше pgsql вы не найдете. Главное инструмент должен позволять вам сосредотачиваться именно на алгоритме, а не программировании.
Надежность бота, он должен работать 24 на 7. Это реализовано, падает бинанс но не бот.
Производительность, сейчас я торгую 350 парами криптовалют в паре к USDT и никаких проблем с производительностью, нет. Раз в минуту я опрашиваю биржу по этим парам и в течении 30 секунд все данные по всем парам обработаны. Запас прочности еще есть.
Легкость администрирования. Вся реализация написана на pgsql, достаточно делать импорт экспорт. Проблем с администрированием нет.
Почему Postgres
Удобный пакет pgsql-http который позволяет общаться с внешним миром из сохраненных процедур.
Быстрая и удобная работа с json.
Большой набор математический функций.
Удобный пакет dblink позволяющий запускать много процессов postgres.
Sql прекрасный язык для обработки данных и синхронизации работы с данными, это важно когда вдруг два или три бота решат в один момент купить или продать свой актив.
Удобный IDE в виде pgadmin, с любого рабочего места можно ничего не устанавливая через браузер, работать с кодом.
Я знаю и люблю postgres как в прочем и oracle, но oracle за любовь денег хочет.
Как пример выше сказанного привожу запрос который показывает объём в стакане бинанса на покупку с отклонением в 3% вниз и в верх.
WITH depth AS (SELECT (content::jsonb)->'asks' a, (content::jsonb)->'bids' b FROM
http_get('https://api.binance.com/api/v1/depth?symbol=BTCUSDT&limit=5000'))
SELECT
(SELECT va FROM (
SELECT (j->>0)::float8 pa, sum((j->>1)::float8) OVER (ORDER BY j->>0) va FROM
jsonb_array_elements(a) j) t WHERE pa <= (a#>>'{0,0}')::float8 * 1.03 ORDER BY va DESC LIMIT 1) va,
(SELECT vb FROM (
SELECT (j->>0)::float8 pb, sum((j->>1)::float8) OVER (ORDER BY j->>0 DESC) vb FROM
jsonb_array_elements(b) j) t WHERE pb >= (b#>>'{0,0}')::float8 * 0.97 ORDER BY vb DESC LIMIT 1) vb
FROM depth
А теперь попробуйте все то же самое написать на другом языке. Конечно получится, но думаю будет не так изящно.
Общая архитектура системы.
Система состоит из следующих модулей
Основная программа, написана на pgsql.
Боты, запускаемые с определенным интервалов времени, боты делятся на служебные и боты для торговли. Боты для торговли разделяются на боты, торгующие в LONG и боты, торгующие в SHORT. Служебные боты — это бот для работы с телеграмм и бот, который может определять общую стратегию торговли для всех пар, в данный момент я его не использую, поскольку не смог ответить на вопрос, на до ли связывать между собой результаты торгов разными парами. Сейчас они у меня торгуются не зависимо. Это как пример игры в казино вы играете за разными столами, и если в сумме вы выиграйте, прекращать играть за всеми столами или продолжать играть на всех столах дальше. Эмулятор показал мне что лучше торговать не зависимо от результата торговли в других парах, но я не уверен, что это правильно. Боты реализованы на pgsql.
Бот выполняющий асинхронные команды, которые получает из pipe, реализация на pgsql.
Программа для посылки сигнала останов, основной программе, реализация на pgsql.
Командные файлы операционной системы для запуска и останова основной программы, написаны на bash.
Есть список ботов, которые надо запускать через определенные интервалы времени. Бот — это сохраненная процедура на pgsql. Боты запускаются выполнятся параллельно, каждый в своем процессе postgres. Боты делятся на два класса торговые и служебные. Торговые боты одинаковые, но можно реализовать и разные, каждый торгует своей парой криптовалют. Служебных ботов несколько, бот для работы с телеграмм, бот для выполнения асинхронных операций (эмуляция автономных транзакций), такие как логирование или вывод сообщений в телеграмм и прочие вспомогательные боты. Главная программа запускает боты в цикле, с определенным интервалом времени. Можно было это сделать более правильно так что бы каждый бот возвращал время, когда его надо запустить, но это я отложил на будущее.
CREATE TABLE crypto_bot2.bots
(
key text primary key,
name text ,
params jsonb,
is_run boolean,
comment text
)
В таблице bots содержится список ботов, которые будут запускаться. Key – это уникальный ключ бота, для торговых ботов он означает имя торговой пары, name имя бота, имя сохраненной процедуры, params – это переменные бота с которыми он работает, is_run флаг который показывает бот надо запускать или нет, и последние поле — это комментарий. Вот как верхняя выглядит часть данной таблицы. В таблице содержатся как служебные боты, так и боты для торговли. Боты для торговли делятся на торгующие в Long и Short. Для торговли в Short я использую монеты DOWN которые предоставляет binance, но в будущем предполагаю работать с маржинальной торговлей, поскольку возможностей больше. Вот как выглядит таблица в базе.
Теперь часть кода, которая запускает эти боты, дальше будет приведен полный код главной процедуры.
bot_cmd = CASE WHEN is_restart THEN 'restart' ELSE 'start' END;
LOOP
wt = clock_timestamp();
SELECT * INTO st FROM crypto_bot2.bot_state t ;
IF (st.status = 'STOP') THEN
bot_cmd = 'stop';
ELSE IF (clock_timestamp() - start_time > restart_interval) THEN
SELECT count(*) INTO wi FROM crypto_bot2.bots WHERE params->>'buy_price' IS NOT NULL;
IF (wi = 0 OR clock_timestamp() - start_time > restart_interval * 5) THEN
bot_cmd = 'pre_restart';
END IF;
END IF; END IF;
FOR bot IN (SELECT * FROM crypto_bot2.bots WHERE is_run) LOOP
dblnk = dblinks->bot.key;
IF (dblnk IS NULL) THEN
dblnk = 'dblink_' || bot.key;
PERFORM crypto_bot2.create_dblink(dblnk);
PERFORM dblink_exec(dblnk, format('CALL crypto_bot2.init_bot(''%s'')', dblnk));
dblinks = CASE WHEN dblinks IS NULL THEN hstore(bot.key, dblnk)
ELSE dblinks || hstore(bot.key, dblnk) END;
END IF;
IF (dblink_is_busy(dblnk) = 0) THEN
PERFORM crypto_bot2.clear_dblink(dblnk);
PERFORM dblink_send_query(dblnk, format('SELECT crypto_bot2.%s(''%s'', ''%s'')', bot.name, bot.key,
bot_cmd));
END IF;
END LOOP;
EXIT WHEN bot_cmd IN ('stop', 'pre_restart');
wi = req_time - EXTRACT(epoch FROM(clock_timestamp() - wt));
IF (wi > 0) THEN
PERFORM pg_sleep(wi);
END IF;
bot_cmd = 'work';
END LOOP;
FOR bot IN (SELECT * FROM each(dblinks)) LOOP
PERFORM dblink_disconnect(bot.value);
END LOOP;
Как вы можете видеть я использую асинхронный запуск через dblink_send_query, в качестве параметра боту передаётся его key. Данный цикл выполняется раз в 5 секунд. Теперь посмотрим на стартовую часть, когда бота.
BEGIN
SELECT params INTO p_params FROM crypto_bot2.bots WHERE key=p_pair;
save_params = p_params;
last_call = current_setting(last_call_key, true)::timestamptz;
IF (last_call IS NULL OR (clock_timestamp() - last_call) > '55S'::interval
OR clock_timestamp() - date_trunc('minute', clock_timestamp()) < '5S'::interval) THEN
Вначале бот получает свои параметры из таблицы bots и затем смотрит, когда его последний раз вызывали и если прошло меньше 55 секунд, то прекращает работать, бот для телеграмма запускается каждые 5 секунд и проверяет не прислали команда от хозяина или не надо ли что сообщить хозяину. В конце работы бота, бот сохраняет параметры в таблице bots что бы использовать их при новом вызове.
IF (save_params != p_params) THEN
UPDATE crypto_bot2.bots SET params = p_params WHERE key=p_pair;
END IF;
Сохранение параметров в таблице сделано по двум причинам, первое в postgresql нет пакетов где можно держать удобно сессионные переменные и второе в случае перезагрузки бота сессионные переменные нас не спасут. Прошу прощения если не сумел донести основную идею, но думаю если вы сумеете прочесть до конца, то все прояснится.
Пакет pgsql-http
https://github.com/pramsey/pgsql-http
Это основное расширение, который я использую для общения с внешним миром. Я сделал небольшую обертку для обработки ошибок сети. Хочу поблагодарить автора данного расширения. Обертка для выполнения https запросов, в случае ошибки пробуем повторить запрос .
CREATE OR REPLACE FUNCTION crypto_bot2.https_get(
url text,
params hstore DEFAULT ''::hstore,
OUT status integer,
OUT content text)
RETURNS record
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
r record;
error text;
BEGIN
url = url || CASE WHEN array_length(akeys(params), 1) > 0 THEN '?' ELSE '' END;
FOR r IN SELECT key, value FROM each(params) LOOP
url = url || '&' || r.key || '=' || urlencode(r.value);
END LOOP;
BEGIN
SELECT h.status, h.content INTO status, content FROM http_get(url) h;
IF (status != 200) THEN
PERFORM pg_sleep(1);
SELECT h.status, h.content INTO status, content FROM http_get(url) h;
END IF;
EXCEPTION WHEN OTHERS THEN
PERFORM pg_sleep(1);
SELECT h.status, h.content INTO status, content FROM http_get(url) h;
END;
IF (status != 200) THEN
RAISE EXCEPTION USING HINT = url, ERRCODE = 'BW002', MESSAGE = 'Http status =' || status;
END IF;
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS error = MESSAGE_TEXT;
RAISE EXCEPTION USING HINT = url, ERRCODE = 'BW002', MESSAGE = error;
END;
$BODY$;
Подключаем телеграмм
Выше я немного рассказал о удобном пакете pgsql-http. На нем держится все взаимодействие с внешним миром. Для управления ботом я выбрал мессенджер телеграмм. Поскольку он обладает самым удобным API. Я не буду описывать шаги создания бота в телеграмме, в документации к нему все написано, просто покажу процедуры для взаимодействия с сервером телеграмма.
CREATE OR REPLACE FUNCTION crypto_bot2.tlg_call_server(
method text,
params hstore DEFAULT ''::hstore)
RETURNS json
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
url text = 'https://api.telegram.org/botXXXX:YYYYYYYY/';
j json;
http_status int;
sql_state text;
sql_msg text;
sql_hint text;
BEGIN
url = url || method;
SELECT status, content::json INTO http_status, j FROM crypto_bot2.https_get(url, params);
RETURN j;
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS
sql_state = RETURNED_SQLSTATE,
sql_msg = MESSAGE_TEXT,
sql_hint = PG_EXCEPTION_HINT;
PERFORM crypto_bot2.tlg_error('%s %s %s', sql_state, sql_msg, sql_hint);
RETURN null;
END;
$BODY$;
Процедура для отсылки сообщения выглядит так
CREATE OR REPLACE FUNCTION crypto_bot2.tlg_send_msg(
chat_id text,
msg text,
VARIADIC param text[] DEFAULT NULL::text[])
RETURNS json
LANGUAGE 'plpgsql'
AS $BODY$
BEGIN
RETURN crypto_bot2.tlg_call_server('sendMessage',
hstore(array['chat_id', chat_id::text, 'text',
format(msg, VARIADIC param)]));
END;
$BODY$;
Бот для работы с телеграмм.
CREATE OR REPLACE FUNCTION2 crypto_bot2.tlg_bot(key text, cmd text)
RETURNS integer
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
u_id int8;
params hstore ;
msgs json;
m json;
jt json;
user_id int8;
send_text text;
send_ids int8[];
s text;
sql_state text;
sql_msg text;
sql_hint text;
st record;
r record;
pr float8;
btc float8;
usd float8;
c int;
BEGIN
IF (cmd = 'start') THEN
LOOP
msgs = crypto_bot2.tlg_call_server('getUpdates') j;
EXIT WHEN msgs->>'ok' = 'true';
END LOOP;
FOR m IN SELECT json_array_elements(msgs->'result') LOOP
u_id = m->>'update_id';
END LOOP;
DELETE FROM crypto_bot2.tlg_state;
INSERT INTO crypto_bot2.tlg_state(tlg_error_on, tlg_warning_on, tlg_log_on, tlg_update_id)
VALUES(true, false, true, u_id);
DELETE FROM crypto_bot2.tlg_messages;
RETURN 0;
END IF;
SELECT * INTO st FROM crypto_bot2.tlg_state;
u_id = st.tlg_update_id + 1;
params = (CASE WHEN (u_id IS NOT NULL) THEN 'offset=>"' || u_id || '"' ELSE '' END)::hstore ||
'timeout=>4'::hstore;
msgs = crypto_bot2.tlg_call_server('getUpdates', params);
u_id = null;
IF (msgs->>'ok' = 'true') THEN
FOR m IN SELECT json_array_elements(msgs->'result') LOOP
u_id = (m->>'update_id')::int8;
user_id = coalesce(m#>>'{message,from,id}',m#>>'{callback_query,from,id}')::int8;
IF (user_id != crypto_bot2.tlg_my_id()::int4) THEN
jt = crypto_bot2.tlg_send_msg(user_id::text, '%s %s не мешай работать, а то хозяину пожалуюсь',
m#>>'{message,from,first_name}', m#>>'{message,from,last_name}');
jt = crypto_bot2.tlg_send_msg(crypto_bot2.tlg_my_id(), '%s %s лезет не туда ',
m#>>'{message,from,first_name}', m#>>'{message,from,last_name}');
ELSE
SELECT * INTO r FROM crypto_bot2.binance_state2 bs;
CASE coalesce(m#>>'{message,text}', '%@sell_command%&')
WHEN '%@sell_command%&' THEN
-- обработка команд
WHEN '/status' THEN
WHEN '/error' THEN
UPDATE crypto_bot2.tlg_state SET tlg_error_on = NOT tlg_error_on;
s = CASE WHEN NOT st.tlg_error_on
THEN 'Сообщения об ошибках включены' ELSE 'Сообщения об ошибках выключены' END;
WHEN '/war' THEN
WHEN '/risk' THEN
WHEN '/add_sup' THEN
…..
WHEN '/trade_stop' THEN
UPDATE crypto_bot2.binance_state2 SET trade_status = 'STOP';
PERFORM crypto_bot2.log('Торговля остановлена');
s = 'Спасибо хозяин, отдыхаю';
WHEN '/trade_start' THEN
UPDATE crypto_bot2.binance_state2 SET is_sell = false, trade_status = 'RUN';
PERFORM crypto_bot2.log('Торговля продолжена');
s = 'Пошел копать';
WHEN '/pgadmin_start' THEN
s = 'Pgadmin стартовал';
COPY (SELECT 'password') TO PROGRAM 'sudo -S systemctl start httpd';
WHEN '/pgadmin_stop' THEN
s = 'Pgadmin остановлен';
COPY (SELECT 'password') TO PROGRAM 'sudo -S systemctl stop httpd';
ELSE
s = 'Ой, хозяин не отвлекай';
END CASE;
IF (s IS NOT NULL) THEN
jt = crypto_bot2.tlg_send_msg(user_id::text, s);
END IF;
END IF;
END LOOP;
END IF;
IF (u_id IS NOT NULL) THEN
UPDATE crypto_bot2.tlg_state SET tlg_update_id = u_id;
END IF;
SELECT array_agg(id), string_agg(msg, chr(10)) INTO send_ids, send_text FROM crypto_bot2.tlg_messages;
IF (send_text IS NOT NULL) THEN
FOR i IN 1.. (length(send_text)/2048 + 1) LOOP
jt = crypto_bot2.tlg_send2_msg(substring(send_text, (i - 1) * 2048, 2048));
IF (jt->>'ok' != 'true') THEN
EXIT;
END IF;
END LOOP;
IF (jt->>'ok' = 'true') THEN
DELETE FROM crypto_bot2.tlg_messages WHERE id IN (SELECT * FROM unnest(send_ids));
END IF;
END IF;
RETURN 0;
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS
sql_state = RETURNED_SQLSTATE,
sql_msg = MESSAGE_TEXT,
sql_hint = PG_EXCEPTION_HINT;
PERFORM crypto_bot2.tlg_error('%s %s %s', sql_state, sql_msg, sql_hint);
RETURN -1;
END;
$BODY$;
Бот запускается раз в 5 секунд и проверяет новые сообщения от телеграмм, а так же отправляет сообщения от торговых ботов.
DBMS_PIPE средствами pgsql
В ORACLE есть удобная вещь, это автономные транзакции. Для логирования в таблицу они просто необходимы, иначе в случае ошибки произойдет rollback, и вы ничего не увидите. Вначале я использовал dblink, запускал отдельный процесс, для эмуляции автономной транзакции, но это было не очень красиво, хотелось что-то более изящного. В результате был написан dbms_pipe для postgres.
Создаем fifo mkfifo crypto_bot_pipe.
Создаем процедуру для чтения из pipe .
CREATE OR REPLACE PROCEDURE crypto_bot2.async_cmd_bot()
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
cmd text;
rc record;
sql_state text;
sql_msg text;
sql_hint text;
BEGIN
CREATE TEMP TABLE IF NOT EXISTS msg_table(msg text);
LOOP
COPY msg_table FROM '/var/lib/pgsql/crypto_bot_pipe';
FOR rc IN (SELECT * FROM msg_table) LOOP
cmd = rc.msg;
IF (cmd = 'stop') THEN
EXIT;
ELSE
BEGIN
EXECUTE cmd;
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS
sql_state = RETURNED_SQLSTATE,
sql_msg = MESSAGE_TEXT,
sql_hint = PG_EXCEPTION_HINT;
-- обработка ошибки
END;
COMMIT;
END IF;
END LOOP;
TRUNCATE msg_table;
EXIT WHEN cmd ='stop';
END LOOP;
END;
$BODY$;
Используем команду copy для чтения из pipe в таблицу и затем проходим по этой таблице и выполняем sql запросы и так до тех пор, пока не получим команду stop. Обязательно поставте внутри процедуры commit иначе ничего не уведите в других сессиях. Сейчас покажу как это использовать.
Создаем процедуру для записи в pipe.
CREATE OR REPLACE FUNCTION crypto_bot2.send_async_cmd(cmd text)
RETURNS integer
LANGUAGE 'plpgsql'
AS $BODY$
BEGIN
EXECUTE format('COPY (SELECT %s) TO PROGRAM ''cat >>/var/lib/pgsql/crypto_bot_pipe''', quote_literal(cmd));
RETURN 0;
END;
$BODY$;
Как видим все очень просто, но это позволяет создать некий аналог автономных транзакций, что мне очень помогло. Это решение до сих пор мне очень нравится.
Выполнение системных команд из pgsql
В процессе разработки бота, мне захотелось запускать и останавливать pgadmin с помощью бота телеграмм, для этого надо было выполнять системные команды из pgsql. Сделано это было следующим образом
COPY (SELECT 'root password') TO PROGRAM 'sudo -S systemctl start httpd';
Логирование действий бота
Система логирование подразумевает фиксацию ошибок бота и его действий, например, по покупки или продажи криптовалюты. Логирование обязательно должно быть асинхронным. Например, в случае продажи криптовалюты мы хотим вывести сообщение в телеграмм, а сервер телеграмма не доступен в этот момент. Если делать это синхронно, то мы должны зависнуть в данной точке и ждать доступности телеграмма, что для нас не приемлемо. Мы записываем сообщение в таблицу используя эмуляцию механизма автономных транзакций, описанную выше и затем срабатывает триггер на таблице логирование и происходит отсылка сообщений в телеграмм. Таким образом мы нигде не зависаем и гарантировано получим уведомление, а также в случае ролбэка в боте, в таблице логера останется запись. Главное не отсылать сообщение об ошибки самого телеграмма в телеграмм.
CREATE TABLE crypto_bot2.log
(
id BIGSERIAL PRIMARY KEY,
l_date timestamp with time zone,
l_type text,
msg text,
CONSTRAINT log_pkey PRIMARY KEY (id)
);
CREATE TRIGGER tlg_send_log
AFTER INSERT
ON crypto_bot2.log
FOR EACH ROW
WHEN (new.l_type <> 'TLG_ERROR'::text)
EXECUTE PROCEDURE crypto_bot2.tlg_send_log();
CREATE FUNCTION crypto_bot2.tlg_send_log()
RETURNS trigger
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
st record;
BEGIN
SELECT * INTO st FROM crypto_bot2.tlg_state;
IF ((st.tlg_error_on AND NEW.l_type = 'ERROR') OR (st.tlg_log_on AND NEW.l_type = 'LOG') OR
(st.tlg_warning_on AND NEW.l_type = 'WARNING')) THEN
INSERT INTO crypto_bot2.tlg_messages(msg) VALUES(NEW.msg);
END IF;
RETURN NULL;
END;
$BODY$;
В функции tlg_send_log сообщения необходимые для отправки в телеграмм помещаются в таблицу tlg_messages, при очередном запуске телеграмм бота они будут отосланы в телеграмм.
А вот функция логирования.
CREATE OR REPLACE FUNCTION crypto_bot2.log_commit(
msg text,
VARIADIC param text[] DEFAULT NULL::text[])
RETURNS integer
LANGUAGE 'plpgsql'
AS $BODY$
BEGIN
PERFORM crypto_bot2.send_async_cmd('SELECT crypto_bot2.log(' ||
quote_literal(format(msg, VARIADIC param)) || ')');
RETURN 0;
END;
$BODY$;
CREATE OR REPLACE FUNCTION crypto_bot2.log(
msg text,
VARIADIC param text[] DEFAULT NULL::text[])
RETURNS integer
LANGUAGE 'plpgsql'
AS $BODY$
BEGIN
INSERT INTO crypto_bot2.log(l_date, l_type, msg) VALUES (clock_timestamp(), 'LOG', format(msg, VARIADIC param));
RETURN 0;
END;
$BODY$;
Еще раз вкратце как работает логирование, код у вас перед глазами.
Вызывается функция log_commit, эта функция через pipe посылает команду в другой сессии выполнить функцию log, которая просто вставит сообщение в таблицу log, но это в другой сессии и сразу закомитится. Так что если основная сессия даст сбой log у нас останется. На таблице log у нас сработает тригер который проверит надо ли отсылать данное сообщение в телеграмм и если надо то положит сообщение в таблицу tlg_messages, и когда запустится, мой телеграмм бот он отошлет это сообщение мне. Свои дети и свои идеи самое лучшее, мне кажется это хорошее решение. Вот кусок кода из телеграм бота который отсылает сообщения
SELECT array_agg(id), string_agg(msg, chr(10)) INTO send_ids, send_text FROM crypto_bot2.tlg_messages;
IF (send_text IS NOT NULL) THEN
FOR i IN 1.. (length(send_text)/2048 + 1) LOOP
jt = crypto_bot2.tlg_send2_msg(substring(send_text, (i - 1) * 2048, 2048));
IF (jt->>'ok' != 'true') THEN
EXIT;
END IF;
END LOOP;
IF (jt->>'ok' = 'true') THEN
DELETE FROM crypto_bot2.tlg_messages WHERE id IN (SELECT * FROM unnest(send_ids));
END IF;
END IF;
Если произойдет сбой в отправке, то сообщения не будут удалены из таблицы tls_messages и будут повторно отправлены позже.
Драйвер для доступа к бирже Binance.
Я начинал торговать на wex, затем на kraken и сейчас перебрался на binance. Писал драйверы для всех этих бирж, а так же для тинькоф когда хотел торговать акциями. Здесь пример драйвера для доступа к binance. Так же при чтении данных с binance, биржа не любит когда ее часто опрашивают, учитывая что я торгую 350 пар, то есть делаю 350 запросов в минуту, то надо учитывать просьбы биржи, когда просят замедлить опросы. Для этого написал отдельную маленькую процедуру для опроса биржи. Готов предоставить имеющиеся драйверы для кракена, тинькова, или разработать для других бирж если кого-то заинтересует.
CREATE OR REPLACE FUNCTION crypto_bot2.binance_call_trade_server(
path text,
param text DEFAULT NULL::text,
http_method text DEFAULT 'GET'::text)
RETURNS jsonb
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
url text = 'https://api.binance.com/api/v3';
j text;
key text = 'KEYXXXXXX';
sign text = 'signYYYYYYY';
p text;
u text;
signature text;
req http_request;
key_header http_header;
http_status integer;
error text;
BEGIN
key_header.field = 'X-MBX-APIKEY';
key_header.value = key;
p = CASE WHEN param IS NULL THEN 'timestamp=' ELSE param || '×tamp=' END ||
round((extract(epoch from clock_timestamp())*1000)::numeric,0);
signature = encode(hmac(p,sign,'sha256'),'hex');
p = p || '&signature=' || signature;
req.method = http_method;
req.headers = array[key_header];
u = url || path;
IF (lower(http_method) = 'get') THEN
req.uri = u || '?' || p;
ELSE IF (lower(http_method) = 'post') THEN
req.uri = u;
req.content_type = 'application/x-www-form-urlencoded';
req.content = p;
END IF; END IF;
BEGIN
SELECT status, content INTO http_status, j FROM http(req);
IF (http_status NOT IN (200, 400)) THEN
PERFORM pg_sleep(1);
SELECT status, content INTO http_status, j FROM http(req);
END IF;
IF (http_status != 200) THEN
RAISE EXCEPTION USING HINT = format('%s?%s', u, p::text), ERRCODE = 'BW002', MESSAGE = j;
END IF;
EXCEPTION WHEN OTHERS THEN
PERFORM pg_sleep(1);
SELECT status, content INTO http_status, j FROM http(req);
END;
IF (http_status != 200) THEN
RAISE EXCEPTION USING HINT = format('%s?%s', u, p::text), ERRCODE = 'BW002', MESSAGE = j;
END IF;
RETURN j::jsonb;
END;
$BODY$;
В случае ошибки пытаюсь повторить запрос если нет, то возвращаю ошибку и бот попробует все повторить заново чуть позже.
CREATE OR REPLACE FUNCTION crypto_bot2.bhttps_get(
url text,
params hstore DEFAULT ''::hstore,
OUT status integer,
OUT content text)
RETURNS record
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
r record;
error text;
BEGIN
url = url || CASE WHEN array_length(akeys(params), 1) > 0 THEN '?' ELSE '' END;
FOR r IN SELECT key, value FROM each(params) LOOP
url = url || '&' || r.key || '=' || urlencode(r.value);
END LOOP;
SELECT h.status, h.content INTO status, content FROM http_get(url) h;
-- Бинанс иногда просит притормозить с запросами
IF (status = 429) THEN
PERFORM pg_sleep(10);
END IF;
IF (status = 418) THEN
PERFORM pg_sleep(30);
END IF;
IF (status = 403) THEN
PERFORM pg_sleep(120);
END IF;
IF (status != 200) THEN
RAISE EXCEPTION USING HINT = url, ERRCODE = 'BW002', MESSAGE = 'Http status =' || status;
END IF;
EXCEPTION WHEN OTHERS THEN
GET STACKED DIAGNOSTICS error = MESSAGE_TEXT;
RAISE EXCEPTION USING HINT = url, ERRCODE = 'BW002', MESSAGE = error;
END;
$BODY$;
Бот для торговли
Код бота для торговли я не привожу. Отмечу только главные вещи, на которые следует обратить внимание если вы будете его реализовывать. Решение о покупке мой бот принимает раз в минуту, опрашивая биржу, затем если бот купил крипту я опрашиваю биржу каждые 5 секунд для принятия решения о продаже, одновременно у меня бывает куплено от 0 до 8 различных крипто валют. Опрос каждые 5 секунд это эмуляция стоп ордера, конечно если биржа упадет и у меня на руках будет криптовалюта, то придется продать ее с убытком, но пока такого не было, и стратегия предусматривает продажу до момента обвала. Хотя такой риск есть, и я его понимаю. Стоп ордера не ставлю, поскольку стратегия не предусматривает продажи по определенной цене, а продажа и покупка происходит при совпадении ряда факторов. Опрашивать биржу чаще раза в минуту если у вас на руках нет купленной криптовалюты не стоит, биржа вас быстро забанит особенно если вы торгуете большим количеством коинов, а принимать решение на основе частых опросов точно не стоит. Никогда не стоит вкладывать все деньги в один коин, и обязательно бот должен фиксировать убытки и не надеется, что все будет хорошо. Число сделок, которые мне не приносят убыток в среднем 72%, но это не значит, что все, что без убытка это прибыль, примерно половина сделок из этих 72% закрывается без прибыли. Самое интересное что при прогоне на акциях этот алгоритм так же показал процент сделок без убытка в 75%, что в пределах погрешности. Меня это обнадежило. Так же я сделал управление ботом, которое позволяет мне продать купленные коины в любой момент, главное у меня нет кнопок их купить, потому что бот при покупке коинов всегда знает в какой момент он будет фиксировать убыток, ну а прибыль при желании я могу и сам зафиксировать, это иногда полезно на падающем рынке. На растущем рынке лучше все доверить технике, у которой нет ни жадности, ни нервов, ни надежды.
Запуск основной программы
Основная программа запускается и останавливается через командный файл. В centos 7 была известная проблема утечки памяти в библиотеки curl в случае https соединения, данную библиотеку использует пакет pgsql-http. Поэтому приходилось перезапускать бота раз в сутки. Сейчас в centos 8 проблемы нет и бот может работать без перезагрузки, но я оставил данное решение, поскольку в этом случае можно отключить автоматический vacuum в постгрессе и делать его в ручную, что может повысить производительность, но это мысли о будущем. Так же при старте компьютера проверяется был ли бот запущен и если был, то он запускается снова. Было два раза, когда сервер у провайдера перезапускался и это меня выручало. Тут все просто. Запуск и останов осуществляются следующими командами.
Файл cryptostart.sh
#!/bin/bash
if [ -f crypto_start ]; then
psql -c 'select crypto_bot2.start(true)'
else
touch crypto_start
psql -c 'select crypto_bot2.start(false)'
fi
while [ -f crypto_start ]; do
psql -c 'select crypto_bot2.start(true)'
done
Файл cryptostop.sh
#!/bin/bash
rm -f crypto_start
psql -c 'select crypto_bot2.stop()'
Файл cryptorestart.sh
#!/bin/bash
if [ -f crypto_start ]; then
while [ -f crypto_start ]; do
psql -c 'select crypto_bot2.start(true)'
done
fi
Запуск выполняется командой
nohup ./cryptostart.sh &
Файл cryptorestart.sh положил в rc.local
sudo -i -u postgres bash -c 'nohup /var/lib/pgsql/cryptorestart.sh &'
Основная программа
CREATE OR REPLACE FUNCTION crypto_bot2.start(is_restart boolean DEFAULT false)
RETURNS integer
LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
st record;
wt timestamptz;
start_time timestamptz;
wi int;
dblinks hstore;
bot_cmd text;
bot record;
dblnk text;
restart_interval interval = '2D'::interval;
req_time int = 5;
async_bot text = 'dblink_ASYNC_BOT';
BEGIN
SELECT * INTO st FROM crypto_bot2.bot_state;
IF (st.status = 'RUN' AND NOT is_restart) THEN
RETURN -1;
END IF;
-- Запускаем чтение из pipe
PERFORM crypto_bot2.create_dblink(async_bot);
PERFORM dblink_send_query(async_bot, 'CALL crypto_bot2.async_cmd_bot()');
IF (is_restart) THEN
PERFORM crypto_bot2.warning_commit('Перегрузился');
ELSE
PERFORM crypto_bot2.send_async_cmd('DELETE FROM crypto_bot2.bot_state;
INSERT INTO crypto_bot2.bot_state(status, start_time) VALUES(''RUN'', now());');
PERFORM crypto_bot2.tlg_send_msg(crypto_bot2.tlg_my_id(),'Хозяин, пошел работать');
PERFORM crypto_bot2.log_commit('Старт');
END IF;
start_time = now();
bot_cmd = CASE WHEN is_restart THEN 'restart' ELSE 'start' END;
LOOP
wt = clock_timestamp();
SELECT * INTO st FROM crypto_bot2.bot_state t ;
IF (st.status = 'STOP') THEN
bot_cmd = 'stop';
ELSE IF (clock_timestamp() - start_time > restart_interval) THEN
SELECT count(*) INTO wi FROM crypto_bot2.bots WHERE params->>'buy_price' IS NOT NULL;
IF (wi = 0 OR clock_timestamp() - start_time > restart_interval * 5) THEN
bot_cmd = 'pre_restart';
END IF;
END IF; END IF;
FOR bot IN (SELECT * FROM crypto_bot2.bots WHERE is_run) LOOP
dblnk = dblinks->bot.key;
IF (dblnk IS NULL) THEN
dblnk = 'dblink_' || bot.key;
PERFORM crypto_bot2.create_dblink(dblnk);
PERFORM dblink_exec(dblnk, format('CALL crypto_bot2.init_bot(''%s'')', dblnk));
dblinks = CASE WHEN dblinks IS NULL THEN hstore(bot.key, dblnk)
ELSE dblinks || hstore(bot.key, dblnk) END;
END IF;
IF (dblink_is_busy(dblnk) = 0) THEN
PERFORM crypto_bot2.clear_dblink(dblnk);
PERFORM dblink_send_query(dblnk, format('SELECT crypto_bot2.%s(''%s'', ''%s'')', bot.name, bot.key,
bot_cmd));
END IF;
END LOOP;
EXIT WHEN bot_cmd IN ('stop', 'pre_restart');
wi = req_time - EXTRACT(epoch FROM(clock_timestamp() - wt));
IF (wi > 0) THEN
PERFORM pg_sleep(wi);
END IF;
bot_cmd = 'work';
END LOOP;
FOR bot IN (SELECT * FROM each(dblinks)) LOOP
PERFORM dblink_disconnect(bot.value);
END LOOP;
IF (bot_cmd = 'stop') THEN
PERFORM crypto_bot2.log_commit('Бот остановлен');
END IF;
-- Останавливаем чтение из pipe
PERFORM crypto_bot2.send_async_cmd('stop');
DELETE FROM crypto_bot2.log WHERE l_date < clock_timestamp() - '7D'::interval AND l_type != 'LOG';
-- Удаляем данные для бинанса их слишком много
DELETE FROM crypto_bot2.binance_trades WHERE t_date < clock_timestamp() - '1D'::interval;
DELETE FROM crypto_bot2.binance_data WHERE t_date < clock_timestamp() - '60D'::interval;
PERFORM dblink_disconnect(async_bot);
IF (bot_cmd = 'stop') THEN
PERFORM crypto_bot2.tlg_send_msg(crypto_bot2.tlg_my_id(), 'Хозяин, я закончил работать');
END IF;
RETURN 0;
END;
$BODY$;
CREATE OR REPLACE FUNCTION crypto_bot2.stop()
RETURNS integer
LANGUAGE 'plpgsql'
AS $BODY$
BEGIN
UPDATE crypto_bot2.bot_state SET status = 'STOP';
RETURN 0;
END;
$BODY$;
Таблица bot_state
CREATE TABLE crypto_bot2.bot_state
(
status text ,
start_time timestamp with time zone
)
Комментариев к основной программе у меня нет, она приведена целиком. Маленький комментарий, боту запрещено перегружаться если у нас куплена крипта. Если будут вопросы готов ответить.
Настройки Postgres
Пришлось немного повозится с конфигурированием базы, поскольку одновременно запускается около 400 процессов постгреса и обрабатывается от 5000 до 50000 операций купли продажи на бирже, в зависимости от поведения рынка. Еще все это надо обсчитать и принять решение. Меня приятно удивило что со всем этим постгрес легко справляется на недорогом облачном хостинге за 14 евро. Параметры компьютера 4 ядра, 8Гб памяти и 160Гб диск. Использую 13 версию постгрес. Здесь привожу только те параметры, которые менял.
max_connections = 1000
shared_buffers = 1024MB
max_files_per_process = 1500
wal_buffers = 32MB
commit_delay = 100000
Вот как выглядит мой дашбоард с 350 процессами постгресс.
Эмуляция работы боты
Я сохраняю все данные собранные в процессе работа бота в таблице, эти данные агрегированы и сохранены все произведенные вычисления, чтобы не повторять вычисления снова. Всегда можно запустить эмулятор, который проходит по этим данным и показывает теоретическое поведение бота. В большинстве случаев оно совпадает с реальным, особенно для маленьких сумм. При больших суммах бот всегда смотрит корзину и покупает так что бы не продавить корзину больше заданной величины, у меня это 0.015%. Это важно для альтов с маленькой капитализацией. В результате получается расхождение с эмулятором так как состояние корзины в каждый момент времени не сохранить. Так же сейчас я стал перед покупкой смотреть корзину и решение зависит так же от ее состояния, увеличило количество удачный сделок на 5%-10%, это так же не может быть обработано эмулятором.
Эмулятор — это, наверное, самая важная часть бота, я начинал работу именно с него. Сначала я скачал все исторические данные с биржи kraken и изобретал алгоритмы торговли. То, что бот написан целиком на pqsql позволяет сосредоточится именно на алгоритмах, а не на программировании, учитывая, что все необходимые математические функции прекрасно реализованы в postgresql и так же замечательно реализовано работа с json, ну и sql отличная вещь. Когда у меня получились рабочие алгоритмы я перешел к реальной торговле. Еще раз убеждаюсь, что выбор правильного инструмента — это очень важно.
Обеспечение отказоустойчивости
Я арендую виртуальную машину у hetzner.de, и меня все устраивает, единственное я советую для торговли на binance размещать компьютер в дата центре в Германии, а не в Финляндии. Почему-то сетевых ошибок при доступе к binance из Финляндии гораздо больше чем из Германии. Однажды возникла мысль, если денег будет под управлением будет много как обеспечить надежность в случае обрыва соединения с биржей. Сразу скажу я еще не реализовал данную схему, поскольку на яхту пока не заработал. Однако продумал как это сделать. Объясню только схему. Есть два компьютера установленные в разных дата центрах, желательно на разных континентах. На них работает наша торговая система, одна в рабочем режиме, другая в резерве. Когда рабочая система падает, резервная система переходит в рабочий режим. В электронике есть такой забавный прием watchdog - аппаратно реализованная схема контроля над зависанием системы. Представляет собой таймер, который периодически сбрасывается контролируемой системой. Если сброса не произошло в течение некоторого интервала времени, происходит принудительная перезагрузка системы. В качестве таймера будем использовать ордер, на бирже который заведомо не может выполнится. Например, купить биток за рубль. Мы будем ставить этот ордер раз в 5 минут и убирать и если наша основная система зависнет и, не уберет ордер в течении определенного времени, то управление будет передано резервной системе, и она станет основной. Суть идеи использовать ордер биржи для обеспечения отказоустойчивости.
Выводы
Всегда есть риск потерять деньги, например, биржа закроется или криптовалюту запретят. Для игры на понижения я использую монеты DOWN которые есть на binance. Правильнее оформлять шорт ордера, тогда вариантов монет намного больше, но пока я до этого не дошел. Я собирался перейти на торговлю акциями, прогонял тесты на исторических данных по акциям и результат так же был положительным, от 25% до 50% процентов в год, но хорошего апи для торговли не было, а сейчас и подавно вариантов торговать в России нет. Главное отличие алгоритма торговли акциями от алгоритма торговли криптой это временной интервал на основе которого вы принимаете решение, он больше в случае торговли акциями поскольку они менее волотильны, а также торговля криптой идет круглосуточно, а при торговли акциями надо ориентироваться на американские торги. Сама идея алгоритма что для крипты что для акций одинакова. Главное, чтобы бот фиксировал убытки и не зависал в надежде что все будет хорошо. Еще дам совет, в алгоритме должно быть как можно меньше коэффициентов, а если они есть то типа таких 0.5, 0.25, 1./3. Никогда не подгоняйте коэффициенты к историческим данным. Лучшая проверка алгоритма если у вас совершенно новая идея и вы ее прогнали на исторических данных желательно за большой срок и на разных парах криптовалют или акций и у вас сразу получился на выходе плюс. У меня в телеграмме есть канал куда бот пишет свои действия, можете заглянуть в него, не призываю копировать его решения, но если интересно можете поглядеть https://t.me/criptozhenya. И еще совет не торгуйте руками, я на этом погорел, на LUNE, хотел много и сразу. Код в git я не выкладывал, но, если кого заинтересует могу вычистить торговых ботов и сделать экспорт приложения.
Главный вывод: можно делать торговых ботов, которые торгуют в плюс, их делать лучше самому и не верить тем, кто предлагает платные сигналы. Мой телеграмм для связи если возникли вопросы https://t.me/antokols .
Комментарии (11)
RekGRpth
05.07.2022 12:28код можно немного упростить, используя pg_jobmon для асинхронных логов и/или pg_task для асинхронных задач
kuza2000
05.07.2022 12:33+1Стоп ордера не ставлю, поскольку стратегия не предусматривает продажи по определенной цене, а продажа и покупка происходит при совпадении ряда факторов.
Тоже пришел к выводу, что для бота стопы не нужны, если они не входят в стратегию. Бот всегда принимает решение объективно. А вот для торговли руками - стопы нужны всегда, так как объективность принятия решения человеком при убыточной сделке нарушается. Очень сильно нарушается. Это особенности принятия решения человеком.
Flmstr
05.07.2022 16:51Интересная статья =)
А можете посоветовать литературу по торговым алгоритмам
"Воспоминания биржевого спекулянта" уже заказал, в пути)Format-X22
05.07.2022 21:59И добавьте как минимум года три опыта и не час в день и тогда КПД станет больше единицы. Вероятно.
rPman
05.07.2022 23:00Во время разработки алгоритма ты проводил много симуляций, как именно на sql ты это делал? по шагам, по каждой минуте в базе запускал скрипт, запускающий твой алгоритм или как то сумел это сделать агрегировано, запуская запрос, определяющий нужно ли совершать сделку, сразу для всей истории?
bigono
06.07.2022 09:44для себя пришёл к выводу писать лучше на том языке который хорошо знаешь и используешь регулярно.
главное торговая стратегия и бэктесты. для бэктестов нужна база.
можно хотя бы подсказку на что основывается торговая стратегия ?
SatCat
06.07.2022 09:47Хорошая статья! на какой впс это крутится - в смысле сколько cpu\ram\disk требуется?
sleeper141
07.07.2022 10:09Интересная инфа про отказоустойчивость, для размещения дата центра в Германии. Есть, же обратные сделки для игры на понижения, здесь непоняточка. Да и вообще не ясно, если бот заоюрабатывет зачем об этом требещать на всю округу...
kuza2000
Плюсую, статья интересная и работа заслуживает уважения. Только вот постгри здесь точно не нужен)
Здесь не нужна база данных, вообще. Данные торгов даже на минутках легко помещаются в памяти. А для синхронизации ботов лучше использовать другие средства.
Но если нравится - то почему бы нет))
vassabi
ИМХО пострес тут не для чего-то, а "потому что автор в него умеет".
Раз у него получается - то почему бы и не использовать.
bigono
В защиту автора.
Тот кто разрабатывал торговых ботов знает что всё начинается с ТС, потом бэктесты.
для бэктестов нужно хранить исторические данные. Возможно дополнить предварительно рассчитанными индикаторами. Вполне нормально проверить тысячи ТС (это без подбора коэффициентов) что бы найти прибыльную.
потом для реальной торговли проще оставить туже систему где проходят тесты.
т.к. после запуска работающей - лучше продолжить тесты и поиск дополнительных ТС.