Создание криптотрейдингового бота

Я начал криптоторговлю несколько месяцев назад из-за неприятного инцидента, который оставил меня без работы на два месяца. Глядя на светлую сторону, я подумал, что, возможно, это мой шанс наконец-то узнать о криптографии и торговле для «удовольствия и прибыли». Я начал смотреть видео на YouTube и читать сообщения в блогах, чтобы изучить основы криптографии и торговли. Вот мои лучшие ресурсы:

  1. CoinBureau — это канал YouTube, который предоставляет высококачественный контент, связанный с криптографией.
  2. AltCoin Daily — еще один канал YouTube, который предоставляет ежедневные обновления обо всем, что происходит вокруг крипторынка.
  3. Академия Binance похожа на блог, где вы можете найти контент, касающийся криптографии и торговли.

Через некоторое время я начал торговать вручную с BTC, ETH, ADA и MATIC на основе видео Гая. Не потребовалось много времени, чтобы понять, что торговля — это работа на полный рабочий день, и мне нужно было бы смотреть на множество экранов, чтобы следить за ценами, если я не хочу упустить возможность. Хотя это было приятное хобби, я не хотел менять свою карьеру и становиться трейдером.

Среди этих видео на YouTube это привлекло мое внимание и поставило идею разработки торгового бота в моей голове. Насколько это может быть сложно, верно? Моя первая попытка была катастрофой во всех возможных отношениях. У него была очень плохая производительность, низкая прибыль, супер сложный и очень трудный для тестирования. Он использовал технический анализ крестов 9MA и 24MA (Death и Golden crosses). Это было так плохо, что я почти отпустил эту идею, пока не узнал о сетчатых ботах, о которых эта статья.

Отказ

Прежде чем мы перейдем к фактическому контенту, у меня есть отказ от ответственности. Я не финансовый консультант, и ничто в этой статье не должно рассматриваться как финансовый совет. Всегда делайте свое собственное расследование (DYOI) и инвестируйте только с тем, что вы рискуете потерять.

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

Было бы неплохо иметь базовые знания торговых концепций и терминов, таких как свечи, ордера, позиции, бычий, медвежий и боковой рыноккороткая и длинная позиция, а также технический анализ. Если нет, академия Binance — отличное место для начала. Тем не менее, незнание этого не будет блокирующим для этой статьи.

И последнее, что следует иметь в виду при чтении этой статьи: ради простоты (и чтобы это было как можно короче), многие коды были удалены, а остальные коды являются псевдокодами для объяснения концепции.

Сетчатые боты

Когда дело доходит до платформ, есть много, которые предлагают ограниченные бесплатные торговые боты, например, 3commasPionex и KuCoin. После просмотра этого видео от Гая я решил дать ему шанс и начал с 3Commas. Концепции казались очень простыми и легкими в реализации. Так почему бы не попробовать?

Бэктестинг

Когда вы разрабатываете бота, вы захотите протестировать его, прежде чем вкладывать в него свои деньги. В торговой вселенной это называется бэктестингом. Бэктестинг — это предоставление боту исторических данных и проверка его работы.

Это одна из самых важных концепций, которые следует иметь в виду при принятии дизайнерских решений для вашего бота. Вы должны убедиться, что реализация обеспечивает простой способ выполнения бэктеста. такие вещи, как внедрение зависимостей, чистые функции и детерминированность. Ваш бот всегда должен возвращать один и тот же результат для одного и того же ввода, и он не должен зависеть от времени!

Обмен

Ваш бот должен подключиться к API, чтобы получить последние цены и разместить ордер на покупку или продажу. Вы должны выяснить, с какой биржей вы хотите работать. Он должен предоставить вам хорошую документацию и гибкие API, которые соответствуют вашим требованиям.

Я использую Binance! Однако ни один из моих кодов не является жестким к нему. Поэтому в любой момент я могу переключиться на другую биржу путем рефакторинга класса. Binance предоставляет хорошо документированный API и тестовую сеть для начала работы. Нет необходимости упоминать это видео от Гая.Exchange

export type Balance = Record<string, number>;

export enum OrderSide {
  BUY = "BUY",
  SELL = "SELL",
}

export interface OrderConfig {
  side: OrderSide;
  pair: string;
  amount: number;
  price: number;
}

export class Exchange {
  public async getBalance(): Promise<Balance>;

  public async createOrder(config: OrderConfig);

  public async getAllPrices(): Promise<Record<string, number>>;
}

Псевдокод класса Exchange

Класс реализует три метода:Exchange

  1. getBalance вернет остаток на счете.
  2. createOrder создаст [рыночный] ордер. он должен проверить баланс перед созданием ордера, чтобы предотвратить сбой ордера.
  3. getAllPrices получает последнюю цену для всех пар.

Возможно, вам придется добавить еще несколько методов в отношении биржи, с которой вы решили работать.

Как работает сетчатый бот

Сетчатый бот разбивает диапазон цен на ордера на покупку и продажу. Грид-боты обычно создают ордера на покупку и продажу на линию сетки, когда они начинаются. Однако я предпочитаю исполнять и заказывать после того, как линия покупки или продажи была выполнена.

Пара или символ: Пара — это то, что вы покупаете или продаете, например, BTC / USDT. Символы или пары состоят из двух активов или токенов. первый актив называется (BTC), а вторая часть называется (USDT). Когда вы покупаете пару, вы выигрываете, отдавая . И когда вы продаете, вы выигрываете, продавая . Чтобы все было просто, я выбираю только пары с их

.baseAssetquoteAssetbaseAssetquoteAssetquoteAssetbaseAssetUSDTquoteAsset

Настройка бота сетки начинается с выбора нижней полосы () и верхней полосы. Эти значения выбираются путем просмотра предыдущих рыночных цен. Например, 3Commas использует самую высокую и самую низкую цену за последние 7 дней со смещением 3%. Pionex использует бэктест в течение последних 7 дней для настройки конфигураций при выборе стратегии ИИ.

minPrice

Следующий параметр называется . Это количество сплитов между минимальной и максимальной ценой. С большими s приходит больше сделок, но меньше прибыли (меньше) и наоборот. — разница между двумя последовательными s. Существует компромисс между количеством позиций и суммой прибыли на позицию. Например, если мы рассмотрим BTC, за последние 7 дней разумной кажется нижняя полоса 58281 и верхняя полоса 69344. Если мы разделим этот диапазон на 24 сетки, мы получим 481. Это означает, что если вы покупаете, а цена движется вверх на 481 USDT, то вы получаете прибыль от . Если мы разделим этот диапазон на 12, то цена должна двигаться вверх на 962 USDT, чтобы позиция была закрыта, однако прибыль будет выше.

gridCountgridCountgridCountgridWidthgridWidthgridLinegridWidthtradeAmount x 481tradeAmount x 962

Последней частью конфигурации является создание s. Они могут быть созданы с помощью , и . После генерации линий сетки линия, ближайшая к текущей цене, будет деактивирована. Все строки выше этого будут, а строки ниже будут .gridLineminPricegridWidthgridCountSELLBUY

Если цена движется вниз и пересекает линию, будет исполнен ордер на покупку, эта линия будет деактивирована, и все линии выше этой линии изменятся на активные и . С другой стороны, если цена движется вверх и пересекает линию, будет исполнен ордер на продажу, эта линия будет деактивирована, и все линии ниже этой линии изменятся на активные.BUYSELLSELLBUY

Проверьте этот удивительный GIF, сделанный 3Commas, который разрабатывает каждый шаг бота сетки.

Ниже приведен псевдокод для сетчатого бота.

import { Exchange, OrderSide } from "./Exchange";

export interface BotConfig {
  pair: string;
}

export interface GridLine {
  active: boolean;
  price: number;
  side: OrderSide;
}

export default class GridBot {
  private MAX_QUOTE_ASSET_AMOUNT = 200;

  constructor(public config: BotConfig, private gridLines: GridLine[], private exchange: Exchange) {}

  private manageOrder(side: OrderSide, price: number): Promise<void> {
    const amount = this.MAX_QUOTE_ASSET_AMOUNT / price;
    return this.exchange.createOrder({ pair: this.config.pair, side, amount, price });
  }

  private updateGridLinesAfterDecision(index: number) {
    this.gridLines[index].active = false;

    for (let i = 0, il = this.gridLines.length; i < il; ++i) {
      if (i > index) {
        this.gridLines[i].active = true;
        this.gridLines[i].side = OrderSide.SELL;
      } else if (i < index) {
        this.gridLines[i].active = true;
        this.gridLines[i].side = OrderSide.BUY;
      }
    }
  }

  private decide(lastPrice: number): OrderSide | undefined {
    // check if price is below-min/over-high => disable or recalibrate

    // check buy
    for (let i = 0, il = this.gridLines.length; i < il; ++i) {
      const { active, side, price } = this.gridLines[i];

      if (active && side === OrderSide.BUY && lastPrice <= price) {
        this.updateGridLinesAfterDecision(i);
        return OrderSide.BUY;
      }
    }

    // check sell
    for (let i = this.gridLines.length - 1; i >= 0; --i) {
      const { active, side, price } = this.gridLines[i];

      if (active && side === OrderSide.SELL && lastPrice >= price) {
        this.updateGridLinesAfterDecision(i);
        return OrderSide.SELL;
      }
    }
  }

  public async execute(lastPrice: number) {
    const side = this.decide(lastPrice);
    if (side) {
      await this.manageOrder(side, lastPrice);
    }
  }
}

Метод будет проверять каждую активную линию для ситуации покупки или продажи. Чтобы найти лучшее место для покупки, вы хотите начать с самой низкой цены и двигаться вверх, в то время как для места продажи вы хотите начать с самой высокой цены и двигаться вниз.decidelastPrice

Этот метод также следует учитывать, движется ли цена выше верхней полосы или опускается ниже нижней полосы. Большинство сетевых ботов будут деактивироваться в этих случаях, пока цена не вернется в диапазон или пользователь не вмешается и не изменит конфигурации, например, 3commas. С другой стороны, бот сетки Pionex Infinity добавляет новые линии сетки в верхнюю часть (увеличивая верхнюю полосу), если цена движется выше сетки, но все равно деактивируется, если цены падают ниже сетки.

Вы можете самостоятельно решить, как вы хотите, чтобы ваш бот вел себя в этих случаях, но убедитесь, что вы тщательно протестировали эти решения.

Управление ботами

Теперь, когда у нас есть бот сетки, нам нужен движок для управления нашими экземплярами. Мы не хотим запускать это только для одного символа. Это хорошая часть разработки собственного бота. Используя бесплатные версии сетевых торговых платформ, вы ограничены в количестве сетчатых ботов, которые вы можете запустить.

import { Exchange } from "./Exchange";
import GridBot from "./GridBot";

export default class Engine {
  private bots: Record<string, GridBot> = {};
  private exchange = new Exchange();

  private timer: any;
  private isEngineRunning = false;

  private startEngine() {
    if (this.isEngineRunning) {
      return;
    }
    this.isEngineRunning = true;

    this.timer = setInterval(async () => {
      const prices = await this.exchange.getAllPrices();

      for (const [, bot] of Object.entries(this.bots)) {
        bot.execute(prices[bot.config.pair]).catch();
      }
    }, 60000);
  }

  private stopEngine() {
    clearInterval(this.timer);
    this.isEngineRunning = false;
  }

  private onAfterStopStart() {
    if (Object.keys(this.bots).length) {
      this.startEngine();
    } else {
      this.stopEngine();
    }
  }

  public async startBot(name: string);

  public async stopBot(name: string);
}

Псевдокод для движка бегуна

При запуске бота движок извлекает данные бота из базы данных, создает экземпляр и вызывает метод. Он также будет вызывать метод на каждом интервале (1 минута в этом коде), с последней ценой, полученной с биржи. Для остановки бота движок будет называться , давая боту шанс сохранить свое состояние и изящно остановиться.

bot.startbot.executebot.stop

Бэктестинг сетчатого бота

Механизм бэктестинга будет извлекать исторические данные, необходимые для тестирования бота, и инициировать бота, настроив его. Затем он запустится для всех ботов и вернет окончательную прибыль от этой комбинации.bot.execute

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

BTC,ETH,ADA,LTC,MATIC,...quoteAsset

import GridBot from "../GridBot";
import BackTestExchange from "./BackTestExchange";

export interface BackTestingConfig {
  pair: string;
}

export default class BackTestEngine {
  private bots: GridBot[] = [];
  private exchange = new BackTestExchange();

  constructor(private tokens: string[], private dateRange: string, private interval: string) {
    this.exchange.setBalance({ USDT: 2000 });
  }

  public async runTest() {
    await this.initBots();

    const pairs = this.bots.map((bot) => bot.config.pair);
    const prices = await this.getHistoricalData(pairs);

    for (const batch of prices) {
      for (let i = 0, il = pairs.length; i < il; ++i) {
        const price = batch[pairs[i]];
        await this.bots[i].execute(price);
      }
    }

    return this.calcProfit();
  }

  private async initBots() {
    this.bots = this.tokens.map((token) => new GridBot({ pair: `${token}USDT` }, this.calcGridLines(token), this.exchange));
  }

  private calcProfit(): number;

  private calcGridLines(token: string): GridLine[];

  private async getHistoricalData(pairs: string[]): Promise<Record<string, number>[]>;
}

Движок бэктестирования

Нам также нужно издеваться над классом для бэктестирования. То, как мы реализуем grid bot, позволит нам внедрить этот класс как зависимость (IoC).Exchange

import { Balance, OrderConfig } from "../Exchange";

export default class BackTestExchange {
  private balance: Record<string, number> = {};

  public setBalance(balance: Balance) {
    this.balance = balance;
  }

  public async getBalance() {
    return this.balance;
  }

  public async createOrder(config: OrderConfig);

  public async getAllPrices(): Promise<Record<string, number>>;
}

Макет класса Exchange для тестирования

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

setBalancecreateOrdergetAllPrices

Когда вы сделали достаточно бэктестинга, пришло время протестировать вашего бота в тестовой сети, чтобы увидеть, правильно ли выполняются заказы. Обычно пары недель должно быть достаточно, чтобы убедиться, что он работает так, как ожидалось.

Поиск лучших пар

Когда вы запускаете своих ботов в реальной сети, вы можете заметить, что через некоторое время они не совершают никаких сделок, потому что ваш счет находится вне . Это происходит, когда все ваши пары следуют за одним и тем же бычьим или медвежьим рынком. Например, давайте представим, что у вас есть 1000 USDT в качестве вашего , и вы потратили 200 USDT на каждую сделку. В этом случае первые 5 ботов, которые выполняются движком, потратят все активы, а остальные боты не смогут создать ордер на покупку. Было бы здорово, если бы некоторые из этих пар двигались в противоположных направлениях. Поэтому, когда некоторые боты продают, другие будут покупать и наоборот.

baseAssetquoteAsset

Если у вас огромное количество, то сэкономьте некоторое время и остановитесь прямо здесь. Оставшаяся часть статьи посвящена этому вопросу.

baseAsset

Итак, вот вопрос на миллион долларов: как выбрать лучшую комбинацию пар, чтобы обеспечить максимальную прибыль?

Жадный алгоритм

С помощью жадного алгоритма мы должны рассчитать все возможные комбинации всех пар с разной длиной. Длина — это количество пар для участия в сделках. Если есть 200 пар, может быть комбинация из 40 пар, которая имеет более высокую прибыль, чем просто использование всех этих пар вместе. Поэтому мы должны учитывать все возможные длины, а также все возможные ордера в пределах каждой длины.

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

В математике есть комбинации для выбора предметов из предметов, когда заказ имеет значение. Как я объяснял ранее, имеет другую прибыль, чем когда мы ограничили. Предположим, что у нас есть 20 пар, и мы хотим выяснить лучшую комбинацию с минимальной длиной 10:

n!/(n-r)!rnBTC,ADA,…ADA,BTC,…quoteAsset

Я не собираюсь вычислять это число, но просто чтобы дать вам оценку, давайте рассмотрим только последнее число в этой последовательности, . Если бы у нас была машина, способная выполнить 1000000 бэктестов за одну секунду (что весьма сомнительно), для расчета всех этих комбинаций потребовалось бы 77 146 лет!20!

2,432,902,008,176,640,000 / (1000000*60*60*24*365) = 77,146

Бэктестирование одной комбинации означает вызов метода для каждого бота на каждом интервале с последней ценой. Давайте представим, что у нас есть комбинация из 16 ботов, и мы тестируем с интервалом в 1 минуту в течение последних двух месяцев, что означает работу с одной комбинацией. Нет необходимости упоминать сетевую задержку для извлечения исторических данных, настройки ботов, баз данных и операций ввода-вывода. С моим Macbook Pro (16 ГБ оперативной памяти, чип M1 и SSD-диск) я получаю максимум 20 бэктестов в секунду!

bot.execute60*24*60*16=1,382,400decide

На момент написания этой статьи Binance имеет 1778 пар. Поскольку я использую только пары USDT, это оставляет меня с 356 парами. С помощью некоторых фильтров на , и расчета средней прибыли сетки, мне удается уменьшить это число до 189. Вы делаете математику!

gridWidthminPrice

Генетический алгоритм

Хотя алгоритмы Greedy гарантируют лучшую комбинацию, она не применима для этого случая использования. В этих случаях мы обычно используем генетические алгоритмы, которые обеспечивают достаточно хорошее (если не лучшее) решение в течение разумного периода времени.

Внимание: Вы можете заметить в разных местах моего кода, что (asset) используется вместо или . Поскольку я использую USDT в качестве котировочного актива для всех пар, легко конвертировать токен в пару и наоборот. Имейте это в виду, если вы планируете использовать другие котировочные активы.

tokenpairsymbol

Моделирование

Для моделирования хромосомы я использовал массив строк () для представления пар. может содержать маркер или пустую строку. Чтобы упростить операции кроссовера и мутации, длина всех хромосом одинакова. Вот почему некоторые из них содержат пустые ячейки.

genesgenes[i]genes

Пригодность

Пригодность просто рассчитывается путем проведения бэктеста на всех токенах в хромосоме, соблюдающих их порядок. Это то же самое, что и прибыль.

Начальная численность населения

Одна хромосома генерируется путем случайного помещения маркера или пустой строки в массив. Когда хромосома генерируется, она должна быть уточнена. Уточнение хромосомы гарантирует, что все маркеры уникальны, а количество маркеров больше настраиваемого значения, называемого .genesminGeneSize

Ниже приведен псевдокод класса.

Chromosome

export default class Chromosome {
  private fitness = -1;
  private genes: string[] = [];

  constructor(private tokens: string[], genes?: string[]) {
    this.genes = genes || this.genRandomGenes();
    this.refineGenes();
  }

  private getRandomToken(): string {
    return this.tokens[Math.floor(Math.random() * this.tokens.length)];
  }

  private genRandomGenes(): string[] {
    const genes = [];
    for (let i = 0, il = this.tokens.length; i < il; i++) {
      const setToken = Math.random() > 0.75;
      if (setToken) {
        let token = this.getRandomToken();
        while (genes.includes(token)) {
          token = this.getRandomToken();
        }
        genes.push(token);
      } else {
        genes.push("");
      }
    }
    return genes;
  }

  private refineGenes(): string[];

  public async calcFitness();

  public crossover(theOther: Chromosome): Chromosome[];

  public mutate();
}

Псевдокод для класса хромосом

Кроссовер и мутация

Кроссовер разделит двух родителей пополам, а затем создаст два потомства (одноточечный кроссовер) путем их перекрестного слияния. Первое потомство имеет первую половину первого родителя и вторую половину второго родителя. Второе потомство имеет первую половину второго родителя и вторую половину первого родителя. Эти потомки должны быть проверены, потому что они могут быть пустыми, иметь дублирующие пары, или они могут уже существовать.

После кроссовера (создания следующего поколения) мы должны мутировать некоторые из этих хромосом. Мутация помешает алгоритму сойтись на локальном оптимальном решении. Мутация поменяет местами два случайных индекса друг с другом. Метод будет выбирать хромосомы из популяции и вызывать метод для каждой из этих хромосом.

discovery.mutatemutationRate*populationSizemutate

Отбор

После расчета приспособленности новая популяция будет генерироваться путем объединения предыдущего поколения и нового поколения. Затем эта популяция будет уточнена (для удаления дубликатов) и отсортирована на основе их пригодности (прибыли). Верхняя часть этого списка будет выбрана для следующей итерации.populationSize

Ниже приведен псевдокод для класса, реализующего этот алгоритм.Discovery

import Chromosome from "./Chromosome";

interface DiscoveryConfig {
  iteration: number;
  population: number;
  mutationRate: number;
}

export class Discovery {
  private tokens: string[] = [];

  constructor(private config: DiscoveryConfig) {}

  public async discover() {
    this.tokens = await this.fetchAllTokens();

    let nextGen: Chromosome[] = [];
    for (let i = 1; i <= this.config.iteration; i++) {
      nextGen = await this.generateNextPopulation(nextGen);
    }
  }

  private async generateNextPopulation(prevGen: Chromosome[]): Promise<Chromosome[]> {
    let nextGen = [];
    if (!prevGen.length) {
      nextGen = this.createInitialPopulation();
    } else {
      for (let i = 0, il = prevGen.length - 1; i < il; i += 1) {
        const newGenes = prevGen[i].crossover(prevGen[i + 1]);
        nextGen.push(...newGenes);
      }
    }

    this.mutate(nextGen);
    await this.calcFitness(this.refinePopulation(nextGen));
    return this.sortPopulation(this.refinePopulation(nextGen.concat(prevGen))).slice(0, this.config.population);
  }

  private mutate(population: Chromosome[]) {
    const mutationCount = Math.floor(population.length * this.config.mutationRate);
    for (let i = 0; i < mutationCount; i++) {
      const mutationIndex = Math.floor(Math.random() * population.length);
      population[mutationIndex].mutate();
    }
  }

  private createInitialPopulation(): Chromosome[] {
    const population: Chromosome[] = [];
    while (population.length < this.config.population) {
      population.push(new Chromosome(this.tokens));
    }

    return population;
  }

  private async calcFitness(chromosomes: Chromosome[]);

  private sortPopulation(population: Chromosome[]): Chromosome[];

  private refinePopulation(population: Chromosome[]): Chromosome[];

  private fetchAllTokens(): Promise<string[]>;
}

Запуск этого алгоритма на исторических данных за два месяца (сентябрь и октябрь 2021 года), 3-минутный интервал, 189 пар, 20 итераций, размер популяции 200, скорость мутаций 0,1 и начальный баланс 2000 USDT, занял 82 минуты для завершения и обнаружил следующую комбинацию с прибылью 3503 USDT. Неплохо, правда?

POLY,NU,WAVES,ATA,WTC,XTZ,XRP,VIDT,SOL,AXS,POLS,CTSI,MASK,FTM,CLV,GTC,STRAX,ALPACA,SUSHI,NKN,ILV,QNT,ALGO,GALA

Хотя может быть и лучшая комбинация, учитывая количество времени, потраченного на это, я бы сказал, что 175% прибыли за два месяца, чертовски хорошо. Имейте в виду, что это результат тестирования, и будущий рынок может быть не таким же. Крипторынок очень волатилен.

Источник