Как стать автором
Обновить

Как написать пассивный доход: Пишем качественного трейд бота на JS (часть 2)

Время на прочтение5 мин
Количество просмотров17K

Эта статья является логическим продолжением предыдущей. Кто не читал - прошу ознакомиться тут.

Итак продолжим. Быстренько допишем еще пару вспомогательных сервисов и перейдем к базовой стратегии.

Чтобы запросы к бирже не превышали допустимых лимитов в секунду - добавим сервис асинхронной очереди.

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 как бот будет создавать и закрывать ордера в онлайн режиме

Текущий исходный код

Теги:
Хабы:
Всего голосов 9: ↑3 и ↓6-3
Комментарии16

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань