Отказ от ответственности: эта статья предназначена только для информационных целей. Он не предназначен для инвестиционных советов. Обратитесь к должным образом лицензированным профессионалам для инвестиционного совета.

Введение

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

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

1. Окружающая среда. Можно рассматривать его как функцию, где входными параметрами являются действия, а выходными параметрами — состояние и награда. Например, рынок криптоторговли может быть средой. Эта среда на каждом временном шаге получает действия от трейдеров и создает новое состояние (новые цены на криптовалюту) и вознаграждение для трейдеров (их прибыль или убыток).

2. Агент. Основная часть агента — это функция, которая получает состояния (а иногда и награды) и производит действия. В настоящее время глубокая нейронная сеть является наиболее популярным выбором для аппроксимации этой функции.

Алгоритм обучения с подкреплением оптимизирует функцию (глубокую нейронную сеть) с ограничением максимального вознаграждения. Другими словами, алгоритм обучения с подкреплением учит агента совершать действия, которые приносят больше вознаграждения. Давайте реализуем пример бота-трейдера с обучением с подкреплением на Python. Код можно запустить сразу здесь.

Настраивать

Наряду со стандартной библиотекой Python и популярными пакетами для анализа, обработки и визуализации данных (Numpy, Pandas, Matplotlib, Seaborn) мы будем использовать некоторые пакеты Python, специфичные для обучения с подкреплением:
Gym — это стандартный API для среды обучения с подкреплением.
- RLlib — это библиотека с открытым исходным кодом для обучения с подкреплением.

Дата подготовки

Торговая среда требует некоторых данных для представления состояния рынка. Состояние должно быть достаточно информативным, чтобы позволить агенту совершить разумное действие. Трейдеры широко используют данные свечей для принятия решений, поэтому они должны соответствовать и нашим потребностям. Я скачал данные 1-минутных свечей с Binance для пары BTC-USDT. BTC — самая популярная криптовалюта — биткойн. USDT — самая популярная стабильная монета, которая примерно эквивалентна доллару США. Здесь доступен хороший скрипт на Python для загрузки данных с Binance. Вероятно, данных минутной свечи недостаточно для принятия правильных решений, так как она не содержит никаких данных о предыдущих временных шагах. Таким образом, мы можем сделать данные свечей для других периодов из имеющихся у нас данных. Также нам необходимо нормализовать данные, здесь мы возьмем короткий период для обучения (с 22-01-31 по 22-03-01 после подготовки данных), поэтому можно использовать среднее значение и стандартное отклонение для этого периода для нормализация данных.

Некоторые глобальные константы

PERIODS = ['3min', '5min', '15min', '30min', '1h', '2h', '4h', 
           '6h', '8h', '12h', '1d', '3d', '7d', '30d']
START_DATE = '2022-01-01'
INITIAL_DATE = '2022-01-31'
FINISH_DATE = '2022-03-01'

Скачать данные 1-минутных свечей

os.makedirs('data', exist_ok=True)
urllib.request.urlretrieve(
    'https://github.com/shmyak-ai/cryp-bot-demo/raw/main/data/BTC-USDT.parquet', 
    'data/BTC-USDT.parquet')
df = pd.read_parquet('data/BTC-USDT.parquet')

Нормализация данных

df_train = df[START_DATE:FINISH_DATE][['open', 'high', 'low', 'close']].copy()
df_train[['open', 'high', 'low', 'close']] = df_train[['open', 'high', 'low', 'close']] - df_train[['open', 'high', 'low', 'close']].mean().mean()
df_train[['open', 'high', 'low', 'close']] = df_train[['open', 'high', 'low', 'close']] / df_train[['open', 'high', 'low', 'close']].std().mean()
df_train['price'] = df[START_DATE:FINISH_DATE]['close']

Рассчитывайте данные свечей других периодов и сохраняйте только действительные данные.

for period in PERIODS:
    df_train['open' + period] =
        df_train['open'].rolling(period).agg(lambda rows: rows[0])
    df_train['high' + period] = 
        df_train['high'].rolling(period).max()
    df_train['low' + period] = 
        df_train['low'].rolling(period).min()
df_train = df_train[INITIAL_DATE:]

Среда

Давайте реализуем среду Gym, которую может использовать RLlib. Готовая к RLlib среда состоит из трех основных частей: конструктора, метода сброса среды и метода шага.

  • Конструктор должен определять области действия и наблюдения. Для пространства действий мы выбираем непрерывный интервал [-1, 1], где -1 означает продать все до базового денежного эквивалента, а 1 означает купить актив до базового денежного эквивалента. Таким образом, агент сможет выбрать сумму актива, которую он хочет купить или продать. В качестве базовых денежных средств мы будем использовать 10000 долларов США, а торговым активом является биткойн. Так, например, действие -0,5 означает продажу биткойнов эквивалентом 5000 долларов США. Пространство наблюдений — это количество записей в массиве наблюдений за временной шаг, который состоит из данных подготовленных свечей, плюс 2 записи для сумм usdt и биткойнов.
  • Сброс должен вернуть исходное состояние среды. Реальный рынок криптовалюты работает непрерывно в течение нескольких дней, месяцев и т. д. Но мы ограничим «эпизод» нашей среды обучения с подкреплением до 2000 шагов. 1440 минут равны суткам, поэтому 2000 шагов — это примерно полтора дня.
    Агент научится получать как можно больше вознаграждений за этот период.
    После каждого сброса мы случайным образом выбираем начальный шаг, а затем среда будет продолжаться в течение 2000 шагов до конца. Исходное состояние среды — это массив наблюдений для начального шага.
  • Шаг должен возвращать текущее состояние, вознаграждение на текущем шаге и состояние среды. Текущее состояние — это массив наблюдений для текущего шага. Вознаграждение — это разница между текущим общим активом и общим активом предыдущего шага. Общий актив равен сумме эквивалентов долларов США и биткойнов долларов США для текущей ступенчатой ​​цены. Среда создается на шаге 2000, как упоминалось ранее.
def prepare_dict(df):
    price_array = df['price'].to_numpy(
                      dtype=np.float32)[:, np.newaxis]
    df = df.drop(columns=['price'])
    obs_array = df.to_numpy(dtype=np.float32)
    data_dictionary = {'price_array': price_array, 
                       'observations': obs_array}
    return data_dictionary
class CryptoEnv(gym.Env):
    def __init__(self, config: EnvContext):
        self._price_array = config['price_array']
        self._observations = config['observations']
        self._base_cash = config['initial_capital']
        self._cash_usd = None
        self._stocks_usd = None
        self._stocks = None
        self._total_asset = None
        self._initial_total_asset = None
        self._time_step = None
        self._initial_step = None
        self._max_steps = config['max_steps']
        self._final_step = None
        self._upper_bound_step = 
            self._price_array.shape[0] - self._max_steps - 1
        self._gamma = config['gamma']
        self._gamma_return = None
        self._action_dim = self._price_array.shape[1]
        # buy or sell up to the base cash equivalent(usd)
        self.action_space = spaces.Box(
            low=-1.0, high=1.0,
            shape=(self._action_dim,),
            dtype=np.float32)
        # cash + stocks + observations
        self._state_dim = 2 + self._observations.shape[1]
        self.observation_space = spaces.Box(
            low=-5.0, high=5.0, 
            shape=(self._state_dim,), 
            dtype=np.float32)
        self._state = None
        self._episode_ended = None
    def reset(self):
        self._time_step = self._initial_step = random.randint(
            0, self._upper_bound_step)
        self._final_step = self._initial_step + self._max_steps
        self._cash_usd = random.random() * self._base_cash
        self._stocks_usd = random.random() * self._base_cash
        self._stocks = self._stocks_usd / 
            self._price_array[self._time_step][0]
        self._total_asset = self._initial_total_asset = 
            self._cash_usd + self._stocks_usd
        self._gamma_return = 0.0
        self._state = self._get_state()
        self._episode_ended = False
        return self._state
    def step(self, action):
        self._time_step += 1
        price = self._price_array[self._time_step][0]
        self._stocks_usd = self._stocks * price
        if action[0] < 0 and price > 0:  # sell
            sell_shares_usd = min(self._base_cash * -action[0],
                                  self._stocks_usd)
            self._stocks_usd -= sell_shares_usd
            self._cash_usd += sell_shares_usd
        elif action[0] > 0 and price > 0:  # buy
            money_to_spend = min(self._base_cash * action[0],
                                 self._cash_usd)
            self._stocks_usd += money_to_spend
            self._cash_usd -= money_to_spend
            self._stocks = self._stocks_usd / price
        self._episode_ended = self._time_step == self._final_step
        self._state = self._get_state()
        next_total_asset = self._cash_usd + self._stocks_usd
        reward = (next_total_asset - self._total_asset) / 
            self._base_cash
        self._total_asset = next_total_asset
        self._gamma_return = self._gamma_return * self._gamma + 
            reward
        if self._episode_ended:
            reward = self._gamma_return
            return self._state, reward, True, self._get_info()
        else:
            return self._state, reward, False, self._get_info()
    def _get_state(self):
        state = np.hstack((
            (self._cash_usd - self._base_cash) / self._base_cash,
            (self._stocks_usd - self._base_cash) / self._base_cash))
        observation = self._observations[self._time_step]
        state = np.hstack((state, observation)).astype(np.float32)
        return state
    def _get_info(self):
        return {"Initial step": self._initial_step,
                "Final step": self._final_step,
                "Initial total asset": self._initial_total_asset,
                "Final total asset": self._total_asset,
                "Gamma return": self._gamma_return}

Обучение агента

Мы будем использовать алгоритм RLlib PPO для обучения с подкреплением. Этот алгоритм является надежным выбором для многих задач. В конфигурации мы должны указать среду, которую мы реализовали ранее, и указать некоторые другие параметры, которые вы можете увидеть ниже. Всего обучение будет продолжаться в течение 200 итераций.

Одна итерация PPO состоит из двух этапов: накопление опыта и обучение. Перед запуском RLlib PPO инициализирует агент обучения с подкреплением с плотной (256, 256) нейронной сетью. Нейронная сеть принимает состояние (свечные наблюдения плюс количество долларов США и биткойнов для текущего шага) в качестве входных данных и выводит, грубо говоря, 2 величины: действие (значение от -1 до 1) и значение, которое оценивает сумму будущие награды.

  • Во время сбора опыта алгоритм будет собирать «траектории опыта», используя нейронную сеть в качестве актора (в нашем случае трейдера) в предоставленной среде. Эти траектории будут примерно состоять из действий, вознаграждений и состояний для каждого шага траектории. В следующей конфигурации за сбор опыта будет отвечать один процесс, он будет отбирать из 100 сред параллельно целые эпизоды (таким образом, одна траектория будет содержать 2000 шагов) за фазу. Будет доступно 100 * 2000 = 200000 шагов на итерацию для обучения.
  • На этапе обучения алгоритм PPO будет использовать собранные траектории опыта для построения функции потерь и обновления весов в нейронной сети (трейдер).

Алгоритм также разделит пакет (200 000 шагов) на более мелкие мини-пакеты (20 000 в нашем случае).

Конфигурация RLlib PPO:

stop_iters = 200
n_workers = 1
n_envs_per_worker = 100
r_fragment_length = 2000
train_batch_size = n_workers * n_envs_per_worker * r_fragment_length
sgd_minibatch_size = int(train_batch_size / 10)
train_dict = prepare_dict(df_train)
config = {
    "env": CryptoEnv,
    "env_config": {
        "price_array": train_dict['price_array'],
        "observations": train_dict['observations'],
        "initial_capital": 1e4,
        "gamma": 0.99,
        "max_steps": r_fragment_length,
    },
    "num_gpus": 1,
    "model": {
        "vf_share_layers": False,
    },
    "num_workers": n_workers,
    "num_envs_per_worker": n_envs_per_worker,
    "rollout_fragment_length": r_fragment_length,
    "train_batch_size": train_batch_size,
    "sgd_minibatch_size": sgd_minibatch_size,
    "batch_mode": "complete_episodes",
    "framework": "tf",
}

Обучение:

ppo_config = ppo.DEFAULT_CONFIG.copy()
ppo_config.update(config)
ppo_config["lr"] = 1e-5
trainer = ppo.PPOTrainer(config=ppo_config, env=CryptoEnv)
for iter in range(stop_iters):
    result = trainer.train()
    print(f"Iteration {iter}.")

Полученные результаты

Проверим обученного агента на всей обучающей выборке.

config["env_config"]["max_steps"] = df_train.shape[0] - 1
env = CryptoEnv(config["env_config"])
actions = []
total_assets = []
rewards = []
observation = env.reset()
while True:
    action = trainer.compute_action(observation)
    observation, reward, done, info = env.step(action)
    rewards.append(reward)
    total_assets.append(info["Final total asset"])
    actions.append(action)
    if done:
        break
init_step, final_step = info["Initial step"], info["Final step"]

За весь период цена биткойна выросла с 38 000 долларов США до 44 000 долларов США (примерно на 16 процентов). Общий актив, полученный агентом, увеличивается с 4500 долларов США до 7500 долларов США (около 65 процентов). Неплохо для крошечной сети с очень простой подготовкой данных и стандартными гиперпараметрами, обученными за столь короткое время. Самое ценное то, что этот пример показывает, что обучение с подкреплением является работоспособным подходом для реализации полностью автоматизированных торговых ботов.