Эта статья является логическим продолжением предыдущей. Кто не читал - прошу ознакомиться тут.
Итак продолжим. Быстренько допишем еще пару вспомогательных сервисов и перейдем к базовой стратегии.
Чтобы запросы к бирже не превышали допустимых лимитов в секунду - добавим сервис асинхронной очереди.
const MAX_QUERY_PER_SECOND = 4;
class ApiQueueService {
constructor() {
this.queue = [];
this.executeHistory = [];
this.isRun = false;
}
executeInQueue(method, ...data) {
return new Promise((resolve, reject) => {
this.addToQueue(method, data, ({ err, data }) => {
if (err) {
return reject(err);
}
return resolve(data);
});
});
}
addToQueue(method, data, cb) {
this.queue.push({ method, data, cb });
this.run();
}
run(force = false) {
if (this.isRun && !force) {
return;
}
this.isRun = true;
if (this.isBusy()) {
const timer = setTimeout(() => {
clearTimeout(timer);
this.run(true);
}, 100);
return;
}
const q = this.getFirst();
if (!q) {
this.isRun = false;
return;
}
this.execute(q).finally(() => {
this.removeFirst();
if (this.queue.length === 0) {
this.isRun = false;
return;
}
this.run(true);
});
}
getFirst() {
return this.queue && this.queue[0];
}
removeFirst() {
this.queue.splice(0, 1);
}
isBusy() {
return this.getCountQueryInLastSecond() >= MAX_QUERY_PER_SECOND;
}
getCountQueryInLastSecond() {
const currentTime = +new Date();
this.executeHistory = this.executeHistory.filter((d) => d.time > (currentTime - 1000));
return this.executeHistory.length;
}
execute(q) {
this.executeHistory.push({ time: +new Date() });
return q.method(...(q.data || [])).then((data) => {
q.cb({ data });
}).catch((err) => {
q.cb({ err });
});
}
}
module.exports = new ApiQueueService();
Делаем из него синглтон при помощи module.exports = new ApiQueueService();
и запустим небольшой тест очереди:
const apiQueue = require('./services/copy/ApiQueueService');
const apiQueue2 = require('./services/copy/ApiQueueService');
const apiQueue3 = require('./services/copy/ApiQueueService');
const wait = (t) => new Promise((resolve) => setTimeout(() => resolve(), t));
const testAsync = async (d) => {
const { t, ...other } = d;
await wait(t);
return other;
};
(async () => {
for (let i = 0; i < 100; i++) {
apiQueue.executeInQueue(testAsync, { t: 100, index: i, fromFile: 1 }).then((d) => console.log(d));
apiQueue2.executeInQueue(testAsync, { t: 100, index: i, fromFile: 2 }).then((d) => console.log(d));
apiQueue3.executeInQueue(testAsync, { t: 100, index: i, fromFile: 3 }).then((d) => console.log(d));
}
})();
Отлично. Работает как надо. Также нам надо следить за статусом наших ордеров. Для этого установим сокет соединение с биржей по API ключам и будем ловить ивенты обновления ордеров. Бинанс дает нам слушать такие события через сокет:
this.api.websockets.userFutureData(
this.marginCallCallback,
this.accountUpdateCallback,
this.orderUpdateCallback,
this.subscribedCallback,
this.accountConfigUpdateCallback,
)
Испольуем только одно событие orderUpdateCallback
. Как только будет приходить событие - проверяем ордер. Если его статус в базе отличается от того, что пришел в событии - обновляем статус и сохраняем комиссию.
checkOrder = async (data)=>{
if (data && data.order) {
try {
const { order } = data
if (order && order.orderId && order.orderStatus && order.executionType === 'TRADE') {
const o = await orderProvider.getOrder(order)
if (o && o.status !== order.orderStatus) {
o.status = order.orderStatus
await orderProvider.updateOrderCommission(o, order)
}
}
} catch (e) {
this.error('executionTRADE error', e)
}
}
}
Теперь напишем базовый сервис, который будет управлять стратегией торговли. Стратегия должна иметь несколько состояний WAIT_ENTRY_POINT
,IN_PROGRESS
, COMPLETED
. И в зависимости от состояний должна выполнять разные действия:
WAIT_ENTRY_POINT
- стратегия анализирует свечи, патерны, индикаторы (зависит от торговой стратегии) и выставляет ордера для входа в лонг/шорт. Пока стратегия в этом режиме, он должен переодически повторять эту процедуру, корректирую точку входа (отменить старые ордера и добавлять новые)IN_PROGRESS
- в этом состоянии стратегия уже в "позиции" (ордера для входа в лонг/шорт сработали). Теперь ей нужно определится с уровнями фиксирования позиции, а также ордером стоп лосс. Также дополнительная логика при срабатывании ордера фиксирования позицииCOMPLETED
- в это состояние стратегия переходит только когда позиция полностью зафиксирована. Идет просчет прибыли/убытка и запускается новая стратегия со статусомWAIT_ENTRY_POINT
Получился вот такой базовый класс. По определенному событию, стратегия будет проверятся методом checkStrategy
и в зависимости от статуса выполняется нужная функция. Complete
у всех стратегий одинаковый, а вот wait
и progress
- это самая важная часть, это логика работы нашего бота. И поэтому методы wait
и progress
определяются в каждой конкретной стратегии.
class Strategy extends BaseApiService{
constructor(params) {
const { symbol, user, positionSide } = params
super(user.binanceApiKey, user.binanceApiSecret)
this.symbol = symbol
this.user = user
this.positionSide = positionSide
}
async init() {
await this.loadStrategy()
await this.checkStrategy()
}
async checkStrategy() {
await this.loadStrategy()
if (this.strategy.status === STRATEGY.STATUS.WAIT_ENTRY_POINT) {
await this.wait()
} else if (this.strategy.status === STRATEGY.STATUS.IN_PROGRESS) {
await this.progress()
} else if (this.strategy.status === STRATEGY.STATUS.COMPLETED) {
await this.complete()
}
}
async wait() {
// this should be implemented in parent strategy class
}
async progress() {
// this should be implemented in parent strategy class
}
async cancelAllOrders() {
if (this.strategy.orders && Array.isArray(this.strategy.orders)) {
const prs = []
for (const order of this.strategy.orders) {
if (order && order.orderId &&
![ORDER.STATUS.FILLED, ORDER.STATUS.CANCELED, ORDER.STATUS.EXPIRED].includes(order.status)) {
prs.push(this.cancelDBOrder(order))
}
}
if (prs.length > 0) {
await Promise.all(prs)
}
}
}
async addOrderToStrategy(order) {
return orderProvider.createOrder({
...order,
userId: this.user.id,
strategyId: this.strategy.id,
})
}
async complete() {
this.strategy.status = STRATEGY.STATUS.COMPLETED
await this.cancelAllOrders()
await this.strategy.save()
await this.loadStrategy()
}
async loadStrategy() {
this.strategy = await strategyProvider.getCurrentStrategy({
symbol: this.symbol,
userId: this.user.id,
positionSide: this.positionSide,
})
if (!(this.strategy && this.strategy.id)) {
this.strategy = await strategyProvider.create({
symbol: this.symbol,
userId: this.user.id,
positionSide: this.positionSide,
status: STRATEGY.STATUS.WAIT_ENTRY_POINT,
})
}
}
}
Итак мы написали основу трейд бота. В следующей статье реалзиуем первую стратегию. И посмотрим в кабинете binance как бот будет создавать и закрывать ордера в онлайн режиме
Комментарии (16)
egoserg
15.07.2021 15:19Очень интересно.
Я просто потратил минимум год времени.
Перепробовал кучу разных паттернов и комбинацию паттернов.
Но так и не смог сделать нормального бота.
Иногда он фармил + 30% а иногда все сливал.
Суммарно доход составлял 0%
Знаю ребят: которые прикручивали и нейронные сети, но и там баз результатов.
Очень хочется посмотреть на вашу реализациюtommy_lee
15.07.2021 19:22+1А те, кто просто купил и держал с марта, сделали +90% к депозиту. Ну и зачем было мучаться?)
MyraJKee
15.07.2021 23:03Думаю нужно быть семи пядей во лбу, чтобы стабильно, на длинной дистанции, зарабатывать таким способом.
surVrus
16.07.2021 00:32Как написать ... качественного трейд бота на JS
Никак. Ни на JS, ни на любом ином языке.
Успех на бирже - случайность.
Прогнозирование случайных величин невозможно.
tommy_lee
16.07.2021 08:17Цена - случайная величина?
surVrus
18.07.2021 20:38Да. Если это более-менее развитый рынок - то цена строго случайная величина.
Если же рынок слабо развит, мало агентов, или он регулируется - то цена почти случайная величина. И только в редких случаях какого-то специфического рынка цена может быть иногда не случайной величиной.
Тут ведь как: при вводе фактора времени в модель цена может быть в какой-то момент зависимой от массы параметров. А потом - от иной массы параметров. Или стать случайной. Поэтому корректно считать цену и случайной (в 90% времени) и не случайной (10% времени). Проблема анализа цен в том, что аналитики используют обычно одно-двух параметрические линейные модели первого порядка к сложным явлениям, которые такими моделями можно описать только на очень коротком отрезке времени. Или вообще нельзя применять такие модели к этому явлению.
sgkryvenko
17.07.2021 14:14Также как трейдеры зарабатывают на рынке, можно написать алгоритм, перенеся свой подход к торговле в код. Нет программы или идеи, которую невозможно запрограммировать.
okosynskyi Автор
17.07.2021 16:59согласен. я поэтому и хочу создать инструмент для этого. чтоб любой программист трейдер мог накидать свою стратегию (идею), протестировать ее на исторических данных и, если все ок - использовать. Рынок меняется, значит надо скорректировать стратегию. Проверяем и запускаем опять.
surVrus
18.07.2021 20:45Нет программы или идеи, которую невозможно запрограммировать.
Есть. Например, предсказание курса валюты или курса акций. Пробовать можно. Даже написать какую-то программу тоже можно. И она будет даже иногда предсказывать. Но результат ее работы в автоматическом режиме всегда будет негативный.
Причина: есть явления, которые не детерминированные по своей природе. Построить более менее точные модели таких явлений невозможно.
Есть явления в какой-то степени детерминированные. Их модели создать можно.
Самое сложное - отличить одни от других.
surVrus
18.07.2021 20:51Также как трейдеры зарабатывают на рынке, можно написать алгоритм, перенеся свой подход к торговле в код.
Все так. Вот только если бы сами трейдеры смогли корректно сформулировать свои методы, если бы их заработки не были случайными, и если считать, что "везение" и заработок на этом рынке - результат именно действий трейдера. Тогда да, можно попробовать запрограммировать действия трейдеров. Но пока это никому не удалось.
По этой теме рекомендую читать именно трейдеров, того же Талеба. Или Сороса. Ну или самим окунуться в этот бизнес. На большие суммы играть. Тогда многое станет понятнее: решения трейдера обычно очень нелинейно зависят от какой-то группы параметров. И эта зависимость не всегда может быть описана даже самим трейдером.
glestwid
18.07.2021 23:03И эта зависимость не всегда может быть описана даже самим трейдером.
Из чего напрашивается вывод что вся эта писанина под названием "технический анализ", а особенно примкнувшие к ней инфоцыгане - что-то вроде астрологии и ее попыток описывать астрономические явления.
Den_from_Beerulovo
24.08.2021 11:40Написать бота вполне возможно. Мне эта тема очень интересна. Давайте обсуждать код и качество реализации идеи, а не саму идею.
addewyd
Пассивный доход? Скорее, это способ проиграть машину, квартиру и почку. Конечно, если вы сами будете пользоваться описанным ПО. Другое дело — его продавать. Но это уже как-то мошенничеством попахивает