Начнем писать трейдинг бота, который будет работать на криптобирже Binance. Бот должен уметь:

  1. торговать самостоятельно, принося какой-то доход

  2. должен быть удобен для создания и обкатывания различных стратегий торговли

  3. тестировать стратегию на исторических данных

Пожалуй, начнем с архитектуры

У нас есть биржа Binance, у которой есть шикарное api. Поэтому архитектура могла бы выглядеть так:

Вызвать пару методов “купи дешевле” и “продай дороже”. Но задача для нас написать такого бота, при котором условный программист-трейдер сможет создавать и тестировать на прибыльность новые стратегии. Поэтому, необходимо отделить логику торговли от всего прочего. А также модулю логики должно быть все равно к какой бирже его подключили: к реальному API или к псевдо-API (для тестирования). С учетом всего этого получилась примерно вот такая архитектура:

Базу выбрал PostgreSQL. Тут нет никакого тайного умысла. Вы можете использовать любую. 

В связи с тем, что каждый модуль стоит внимания, это все не поместится в одну статью. Поэтому я начинаю мини-сериал: "Пишем качественного трейд бота на JS". Поэтому подписывайтесь, устраивайтесь поудобней - начинаем

Сервис для логов

Простой класс, который принимает на вход префикс для логирования и имеет два метода log и error. Эти методы печатают лог с текущим временем и перфиксом:

class LoggerService {
  constructor(prefix) {
    this.logPrefix = prefix
  }

  log(...props) {
    console.log(new Date().toISOString().substr(0, 19), this.logPrefix, ...props)
  }

  error(...props) {
    console.error(new Date().toISOString().substr(0, 19), this.logPrefix, ...props)
  }
}

Теперь подключим биржу

yarn add node-binance-api

Добавим класс BaseApiService. Сделаем в нем инициализацию Binance SDK, а также применим сервис LoggerService. Учитывая мой опыт с Binance могу сразу сказать, что в зависимости от торговой пары мы должны слать цену и обьем с разным количеством знаков после запятой. Все эти настройки для каждой пары можно взять, сделав запрос futuresExchangeInfo(). И написать методы для получения количества знаков после запятой для цены getAssetPricePrecision и объема getAssetQuantityPrecision.

class BaseApiService {
  constructor({ client, secret }) {
    const { log, error } = new Logger('BaseApiService')
    this.log = log
    this.error = error

    this.api = new NodeBinanceApi().options({
      APIKEY: client,
      APISECRET: secret,
      hedgeMode: true,
    })
    this.exchangeInfo = {}
  }

  async init() {
    try {
      this.exchangeInfo = await this.api.futuresExchangeInfo()
    } catch (e) {
      this.error('init error', e)
    }
  }

  getAssetQuantityPrecision(symbol) {
    const { symbols = [] } = this.exchangeInfo
    const s = symbols.find(s => s.symbol === symbol) || { quantityPrecision: 3 }
    return s.quantityPrecision
  }

  getAssetPricePrecision(symbol) {
    const { symbols = [] } = this.exchangeInfo
    const s = symbols.find(s => s.symbol === symbol) || { pricePrecision: 2 }
    return s.pricePrecision
  }
}

Дальше добавляем метод создания ордера, с учетом правильного количества знаков после запятой для цены и обьема:

async futuresOrder(side, symbol, qty, price, params={}) {
  try {
    qty = Number(qty).toFixed(this.getAssetQuantityPrecision(symbol))
    price = Number(price).toFixed(this.getAssetPricePrecision(symbol))
    if (!params.type) {
      params.type = ORDER.TYPE.MARKET
    }
    const res = await this.api.futuresOrder(side, symbol, qty, price || false, params)
    this.log('futuresOrder', res)
    return res
  } catch (e) {
    console.log('futuresOrder error', e)
  }
}

Теперь бот умеет создавать ордера. Научим его слушать события из биржы для того, чтоб он мог отлавливать изменения статуса ордеров. Для этого создадим класс TradeService.

class TradeService {
  constructor({client, secret}) {
    const { log, error } = new LoggerService('TradeService')
    this.log = log
    this.error = error
    this.api = new NodeBinanceApi().options({
      APIKEY: client,
      APISECRET: secret,
      hedgeMode: true,
    })
    this.events = new EventEmitter()
  }

  marginCallCallback = (data) => this.log('marginCallCallback', data)

  accountUpdateCallback = (data) => this.log('accountUpdateCallback', data)

  orderUpdateCallback = (data) => this.emit(data)

  subscribedCallback = (data) => this.log('subscribedCallback', data)

  accountConfigUpdateCallback = (data) => this.log('accountConfigUpdateCallback', data)

  startListening() {
    this.api.websockets.userFutureData(
      this.marginCallCallback,
      this.accountUpdateCallback,
      this.orderUpdateCallback,
      this.subscribedCallback,
      this.accountConfigUpdateCallback,
    )
  }

  subscribe(cb) {
    this.events.on('trade', cb)
  }

  emit = (data) => {
    this.events.emit('trade', data)
  }
}

При помощи метода из SDK this.api.websockets.userFutureData подписываемся на события из биржы. Самой главный колбек для нас this.orderUpdateCallback . Он вызывается каждый раз когда меняется статус у ордера. Ловим это событие и прокидываем через EventEmitter тому, кто на это событие подписался, используя метод subscribe.

Перейдем к базе данных

Для чего она нужна? В базе будем хранить все ордера, а также всю историю торговли бота. Пользователей с их ключами к бирже и балансами. В последствии сможем считать сколько бот принес прибыли/убытка. Тут останавливаться долго не буду. Подключаю sequlize. 

yarn add sequelize-cli -D
yarn add sequelize
npx sequelize-cli init

Добавим docker-compose.yml файл для локальной базы:

version: '3.1'

services:
  db:
    image: 'postgres:12'
    restart: unless-stopped
    volumes:
      - ./volumes/postgresql/data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: example
      POSTGRES_DB: bot
    ports:
      - 5432:5432
    networks:
      - postgres


networks:
  postgres:
    driver: bridge

А также добавляю миграции и модели. User, Order

Продолжение следует.

В следующей статье будем писать ядро, которое соединит все эти части и заставит бота торговать.