English Deutsch 日本語
preview
Возможности Мастера MQL5, которые вам нужно знать (Часть 62): Использование паттернов ADX и CCI с обучением с подкреплением TRPO

Возможности Мастера MQL5, которые вам нужно знать (Часть 62): Использование паттернов ADX и CCI с обучением с подкреплением TRPO

MetaTrader 5Торговые системы |
50 0
Stephen Njuki
Stephen Njuki

Введение

Мы продолжаем рассматривать, как технические индикаторы, отслеживающие различные этапы движения цены, могут быть объединены в системах машинного обучения. В предыдущей статье мы рассмотрели, как обучение с учителем в многослойном персептроне (MLP) закладывает основу для прогнозирования динамики цен. Мы называем входные данные многослойного перцептрона признаками, а выходные данные прогноза — состояниями. Исходя из того, как мы определили наши признаки в предыдущей статье, что несколько отличалось от нашего подхода в статьях 57–60, мы стремились к более непрерывному входному вектору, в отличие от дискретного варианта, который мы использовали ранее. Наиболее убедительно аргументировать переход к непрерывным данным и регрессии и отказ от дискретных данных и классификации, пожалуй, лучше всего, если мы рассмотрим тенденции в области искусственного интеллекта.

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

А затем, осенью 2023 года, OpenAI представила свой первый публичный GPT, и всё изменилось. Разработка сетей трансформеров и GPT не произошла в одночасье, поскольку первые перцептроны были разработаны в конце 60-х годов, но можно с уверенностью сказать, что запуск ChatGPT стал важной вехой. С широким распространением моделей обработки больших языковых данных стало совершенно очевидно, что токенизация, векторное представление слов и, конечно же, самовнимание являются важнейшими компонентами, позволяющими моделям масштабироваться в соответствии с их возможностями обработки информации. Больше никаких условных выражений. Именно на этом фоне, используя токенизацию и векторное представление слов для обеспечения максимальной непрерывности входных данных сети, мы также сделали входные данные нашего многослойного перцептрона с обучением с учителем «более непрерывными».

В частности, наш второй признак, признак-1, представлен в MLP из Python следующим образом:

def feature_1(adx_df, cci_df):
    """
    Creates a modified 3D signal array with:
    1. ADX > 25 (1 when above 25, else 0)
    2. CCI crosses from below 0 to above +50 (1 when condition met, else 0)
    3. CCI crosses from above 0 to below -50 (1 when condition met, else 0)
    """
    # Initialize empty array with 3 dimensions
    feature = np.zeros((len(adx_df), 5))
    
    # Dimension 1: ADX above 25 (continuous, not just crossover)
    feature[:, 0] = (adx_df['adx'] > 25).astype(int)
    
    # Dimension 2: CCI crosses from <0 to >+50
    feature[:, 1] = (cci_df['cci'] > 50).astype(int)
    feature[:, 2] = (cci_df['cci'].shift(1) < 0).astype(int)
    
    # Dimension 3: CCI crosses from >0 to <-50
    feature[:, 3] = (cci_df['cci'] < -50).astype(int)
    feature[:, 4] = (cci_df['cci'].shift(1) > 0).astype(int)
    
    # Set first row to 0 (no previous values to compare)
    feature[0, :] = 0
    
    return feature

Если бы мы придерживались метода, который использовали для статей с 57 по 60, то обработка прошла бы так:

def feature_1(adx_df, cci_df):
    """
    """
    # Initialize empty array with 3 dimensions and same length as input
    feature = np.zeros((len(dem_df), 3))
    
    # Dimension 1:
    feature[:, 0] = (adx_df['adx'] > 25).astype(int)
    feature[:, 1] = ((cci_df['cci'] > 50) &
                     (cci_df['cci'].shift(1) < 0)).astype(int)
    feature[:, 2] = ((cci_df['cci'] < -50) &
                     (cci_df['cci'].shift(1) > 0)).astype(int)
    
    # Set first row to 0 (no previous values to compare)
    feature[0, :] = 0
    
    return feature

Этот подход, как правило, классифицирует сигналы в соответствии с типичными ожидаемыми паттернами бычьего и медвежьего тренда, поскольку второй элемент в выходном векторе отражает исключительно характеристики бычьего сигнала. Третий элемент отражает исключительно медвежьи признаки. Придерживаясь уже определенных паттернов, определенных как бычьи или медвежьи, этот подход носит скорее классификационный и, следовательно, дискретный характер. При этом, согласно результатам наших испытаний, только 3 из 10 протестированных моделей прошли форвард-тестирование с 01.01.2024 по 01.01.2025 при тестировании/обучении, проведенном с 01.01.2020 по 01.01.2024. Использовался символ EURUSD, временной интервал — дневной.

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

Мы не делаем ничего подобного, и в этом может заключаться объяснение. Поэтому читатели могут свободно изменять и тестировать различные форматы входных данных, поскольку весь исходный код MQL5 прилагается. Со своей стороны, мы будем придерживаться этого подхода и посмотрим, каких результатов добьемся с помощью обучения с подкреплением.

Irein


Обучение с подкреплением

Мы развиваем модель обучения с учителем, описанную в нашей предыдущей статье, вводя действия и вознаграждения. Напомним, что в качестве входных данных для нашего многослойного перцептрона (MLP) использовались признаки, а в качестве выходных — состояния (прогнозируемые изменения цен). Действия на этом этапе описывают, как нам нужно действовать, когда мы знаем, что прогнозирует наш многослойный перцептрон (MLP). Например, если прогнозируется падение цен, мы можем установить лимитный ордер на продажу, стоп-ордер на продажу или совершить мгновенную продажу по рынку. Развитие и обучение сети политики (policy network) может помочь усовершенствовать это решение.

Обычно в сценарии, подобном приведенному выше примеру с выполнением различных типов ордеров на продажу, размер выходного вектора для сетевой политики будет соответствовать количеству возможных действий. В этом случае это количество будет равно 3 для 3 вариантов: лимитный ордер, стоп-ордер и рыночный ордер. Обучение в этих условиях должны привести к различиям в результатах, и читатель может свободно изучить этот вопрос. Со своей стороны, мы по-прежнему рассматриваем действия как одномерный вектор, который, по сути, является копией выходного вектора состояний из многослойного перцептрона, описанного в предыдущей статье. Для чего мы этого делаем? Это служит подтверждением долгосрочного или краткосрочного прогноза, сделанного сетью обучения с учителем.

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


Оптимизация политики доверенных регионов

Оптимизация политики доверенных регионов (Trust Region Policy Optimization, TRPO) - алгоритм обучения с подкреплением, цель которого — улучшение политики. Это достигается итеративным путем обновления весов и смещений сетевой политики с одновременным поддержанием их в пределах "доверенного региона" (trust region) текущей политики.

Ключевыми компонентами, участвующими в реализации этого проекта, являются сеть политики, доверенный регион и дивергенция Кульбака-Лейблера (KL-дивергенция). Сеть политики (policy network) — это нейронная сеть, которая представляет выбор действий путем сопоставления состояний с вероятностным распределением возможных действий. Доверенный регион — это ограничение изменения политики на каждой итерации. Это гарантирует, что новая политика не будет слишком сильно отличаться от старой, тем самым избегая нестабильности. Наконец, KL-дивергенция измеряет разницу между прогнозируемым и достоверным распределениями вероятностей. По сути, она определяет ограничение доверенного региона.

Процесс обучения включает в себя: сбор данных, в идеале партиями траекторий, с использованием текущей политики; оценку функции преимущества для каждой пары состояние-действие в собранных данных, чтобы понять, насколько лучше действие по сравнению со средним действием; формулировку задачи оптимизации для поиска подходящих весов и смещений сети политики и сети значений, которые максимизируют вознаграждение при соблюдении ограничений KL-дивергенции, поддерживающих дивергенцию между новой и старой политиками в пределах определенного порога; решение задачи оптимизации с помощью таких методов, как градиентный спуск; и, наконец, обновление весов сети политики и сети значений.

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

teqn

где:

  • θ - новые параметры политики.
  • θold - старые параметры политики до обновления.
  • πθ(a∣s) - вероятность действия a при новой политике πθ.
  • πθold(a∣s) - вероятность действия a при старой политике πθold.
  • Aπθold(s,a) - функция преимущества, оценивающая, насколько действие a лучше, чем среднее действие в состоянии s.
  • ρθold (s) - распределение посещений состояния в соответствии со старой политикой.
  • DKL - дивергенция Кульбака-Лейблера (KL), измеряющая разницу между старой и новой политикой.
  • δ - ограничение доверительного региона (небольшое положительное значение).


Сеть политики

Мы реализуем наши сети политики и значений на Python так:

class PolicyNetwork(nn.Module):
    def __init__(self, state_dim, action_dim, hidden_size=64, discrete=False):
        super(PolicyNetwork, self).__init__()
        self.discrete = discrete
        
        self.fc1 = nn.Linear(state_dim, hidden_size)
        self.fc2 = nn.Linear(hidden_size, hidden_size)

        self.export_mode = False
        
        if self.discrete:
            self.fc3 = nn.Linear(hidden_size, action_dim)
        else:
            self.mean = nn.Linear(hidden_size, action_dim)
            self.log_std = nn.Parameter(torch.zeros(action_dim))
    
    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        
        if self.discrete:
            action_probs = F.softmax(self.fc3(x), dim=-1)
            dist = Categorical(action_probs)
        else:
            mean = self.mean(x)
            std = torch.exp(self.log_std)
            
            if self.export_mode:
                return mean, std  # return raw tensors
            
            cov_mat = torch.diag_embed(std).unsqueeze(dim=0)
    
            dist = MultivariateNormal(mean, cov_mat)
        
        return dist

Сеть политики наследует свойства от nn.module, что делает его сетевым модулем PyTorch. Функция принимает следующие параметры: state-dimension — размер входного пространства состояний; action-dimension — размер пространства действий; hidden-size — размер скрытых слоев, по умолчанию равный 64. В качестве входных данных мы также принимаем логический флаг с меткой discrete, который определяет, является ли пространство действий дискретным или непрерывным. Этот флаг в конечном итоге определяет структуру выходного слоя, а также тип используемого распределения.

Такая конфигурация делает нашу сеть универсальной в обработке как дискретных, так и непрерывных пространств действий в средах обучения с подкреплением. Таким образом, в таких играх, как CartPole, можно использовать discrete, равный True, в то время как в случае с трейдинге или даже в робототехнике этот флаг будет равен False. В TRPO сеть политик определяет политику агента, которая представляет собой сопоставление состояний с действиями. Таким образом, гибкость в работе с различными типами пространств действий может оказаться важной для широкого применения. Трейдерам может потребоваться ограничить свои действия тремя типами ордеров: лимитными, стоп-ордерами и рыночными ордерами, как указано выше, и в этом случае параметру discrete будет присвоено значение True. 

При реализации этого убедиться, что измерение состояния и измерение действия соответствуют спецификациям, установленным в среде или используемых наборах данных. Дискретный флаг также должен соответствовать типу пространства действий в данной среде. Размер скрытого слоя, равный 64, является регулируемым гиперпараметром, увеличение которого или даже добавление большего количества скрытых слоев может потребоваться в зависимости от сложности используемой среды/набора данных.

Архитектура сети включает 2 полностью связанных линейных слоя, где fc1 отображает входное состояние на скрытый слой, а fc2 — на другой скрытый слой того же размера. Export Mode - это флаг, используемый для управления тем, возвращает ли сеть необработанные тензоры для экспорта или распределение для обучения/выборки. 

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

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

Наконец, в зависимости от того, работает ли наша сеть в дискретном режиме или нет, наш выходной слой будет содержать одну или две нейронные сети. Для дискретных пространств выходные данные линейного слоя fc3 представляют собой логиты каждого действия. Если же параметр discrete равен false, то мы будем иметь дело с непрерывными пространствами, в этом случае каждая из двух сетей выдаст отдельный вектор. Во-первых, это среднее значение гауссова распределения для каждого измерения действия. Во-вторых, у нас есть вектор логарифмического стандартного отклонения гауссова распределения для каждого измерения действия.

Это важно, потому что бифуркация позволяет моделировать различные типы пространства действий. Опция discrete-on выводит распределение вероятностей по заданному числу действий. В варианте с непрерывным или дискретным отключением на выходе получается многомерное гауссово распределение. Это просто два вектора, где один из них представляет собой ориентировочное среднее значение и, следовательно, вес для каждого действия; другой вектор логарифмических распределений представляет собой логарифмическое распределение или показатель достоверности для каждого из средних прогнозов.

В TRPO распределение политики используется для выборки действий и вычисления логарифмических вероятностей для обновлений градиента политики. Таким образом, выбор между дискретным и непрерывным режимами влияет на количество необходимых вычислений и эффективность сети. Стандартное отклонение логарифмических вероятностей — это обучаемый параметр, позволяющий сети адаптировать уровень исследования (дисперсию) во время обучения. Это важно для поддержания баланса между исследованием и эксплуатацией.

Для дискретных действий размерность действия должна соответствовать количеству возможных действий. Мы будем использовать одномерный подход, поскольку это непрерывная переменная, но к дискретным вариантам действий мы вернемся в последующих статьях. Однако для наших текущих непрерывных действий всегда крайне важно тщательно инициализировать log_std. Начиная с Torch.zeros(action_dim), начальные стандартные отклонения равны exp(0)=1, что может быть слишком широким или слишком узким значением в зависимости от масштаба действия. Необходимо внедрить метод масштабирования, специфичный для конкретной среды.

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

Прямой проход к сети политики выполняет общую обработку, при которой входное состояние x проходит через fc1 и fc2 с применением активаций ReLU для добавления нелинейности. В случае, если действия дискретны, то выходные данные функции fc3 обрабатываются функцией softmax для получения вероятностей действий. Если же данные непрерывны, то среднее значение вычисляется с помощью слоя среднего значения; стандартное отклонение получается как exp(log_std), чтобы гарантировать его положительность; если export_mode установлено в true, то стандартные отклонения возвращаются в виде необработанных тензоров. Если export_mode равен false, то будет выполнено построение диагональной ковариационной матрицы (cov_mat) и создание многомерного нормального распределения для выборки логарифмов.

Форвард-тестирование определяет способ, которым политика сопоставляет состояния с распределениями действий, и это является основой процесса принятия решений агентом обучения с подкреплением. В TRPO распределение политики: отбирает действия во время взаимодействия с окружающей средой, вычисляет логарифмические вероятности для целевой функции градиента политики, оценивает ограничение доверительного региона. Использование категориальных и многомерных нормальных распределений обеспечивает совместимость со стандартными библиотеками RL, такими как torch.distributions в PyTorch. Опция export-mode обеспечивает практическое применение, поскольку используются необработанные выходные данные, которые затем могут быть обработаны по мере необходимости.

В дискретном случае функция softmax гарантирует, что сумма вероятностей равна 1. В логитах часто наблюдается численная нестабильность, поэтому при необходимости следует проводить мониторинг на наличие значений NaN и использовать функцию Torch.Clamp. Для непрерывных действий диагональная матрица cov-mat предполагает независимые измерения действий. Если же действия коррелированы, то следует применить полную ковариационную матрицу. Это приведет к увеличению вычислительных затрат. В TRPO логарифмические вероятности политики должны вычисляться эффективно и точно, поскольку они используются на этапах сопряженного градиента и линейного поиска.


Сеть значений

Реализуем сеть значений так:

class ValueNetwork(nn.Module):
    def __init__(self, state_dim, hidden_size=64):
        super(ValueNetwork, self).__init__()
        self.fc1 = nn.Linear(state_dim, hidden_size)
        self.fc2 = nn.Linear(hidden_size, hidden_size)
        self.fc3 = nn.Linear(hidden_size, 1)
    
    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

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


Агент TRPO

Реализуем наш класс агента TRPO на Python следующим образом:

class TRPO_Agent:
    def __init__(self, state_dim, action_dim, discrete=False, 
                 hidden_size=64, lr_v=0.001, gamma=0.99, 
                 delta=0.01, lambda_=0.97, max_kl=0.01, cg_damping=0.1, 
                 cg_iters=10, device='cpu'):
        
        self.policy = PolicyNetwork(state_dim, action_dim, hidden_size, discrete).to(device)
        self.value_net = ValueNetwork(state_dim, hidden_size).to(device)
        self.value_optimizer = optim.Adam(self.value_net.parameters(), lr=lr_v)
        
        self.gamma = gamma
        self.delta = delta
        self.lambda_ = lambda_
        self.max_kl = max_kl
        self.cg_damping = cg_damping
        self.cg_iters = cg_iters
        
        self.discrete = discrete
        self.device = device
        self.state_dim = state_dim
    
    def get_action(self, state):
        # Convert state to tensor and add batch dimension
        state = torch.FloatTensor(state).unsqueeze(0).to(self.device)
        
        # Get action distribution from policy
        dist = self.policy(state)
        
        # Sample action from distribution
        action = dist.sample()
        
        # Get log probability BEFORE converting to numpy/item
        log_prob = dist.log_prob(action)
        
        # Convert action to appropriate format
        if self.discrete:
            action = action.item()  # For discrete actions
        else:
            action = action.detach().cpu().numpy()[0]  # For continuous actions
        
        # Clip continuous actions to [-1, 1] range (optional for discrete)
        if not self.discrete:
            action = np.clip(action, -1, 1)
        
        return action, log_prob
        
    def update_value_net(self, states, targets):
        # Convert inputs to proper tensor format
        if torch.is_tensor(states):
            states = states.detach().cpu().numpy()
        if torch.is_tensor(targets):
            targets = targets.detach().cpu().numpy()
        
        states = np.array(states, dtype=np.float32)
        targets = np.array(targets, dtype=np.float32)
        
        # Ensure proper shapes
        if len(states.shape) == 1:
            states = np.expand_dims(states, 0)
        if len(targets.shape) == 0:
            targets = np.expand_dims(targets, 0)
        
        states_tensor = torch.FloatTensor(states).to(self.device)
        targets_tensor = torch.FloatTensor(targets).to(self.device)
        
        # Forward pass
        self.value_optimizer.zero_grad()
        values = self.value_net(states_tensor)
        
        # Ensure matching shapes for loss calculation
        values = values.view(-1)
        targets_tensor = targets_tensor.view(-1)
        
        loss = F.mse_loss(values, targets_tensor)
        loss.backward()
        self.value_optimizer.step()

    def update_policy(self, states, actions, old_log_probs, advantages):
        # Handle tensor conversion safely
        def safe_convert(x):
            if torch.is_tensor(x):
                return x.detach().cpu().numpy()
            return np.array(x, dtype=np.float32)
        
        states = safe_convert(states)
        actions = safe_convert(actions)
        old_log_probs = safe_convert(old_log_probs)
        advantages = safe_convert(advantages)
        
        # Convert to tensors with proper shapes
        states_tensor = torch.FloatTensor(states).to(self.device)
        actions_tensor = torch.FloatTensor(actions).to(self.device)
        old_log_probs_tensor = torch.FloatTensor(old_log_probs).to(self.device)
        advantages_tensor = torch.FloatTensor(advantages).to(self.device)
        
        # Get old distribution
        with torch.no_grad():
            old_dist = self.policy(states_tensor)
        
        # Compute gradient of surrogate loss
        def get_loss():
            dist = self.policy(states_tensor)
            if self.discrete:
                log_probs = dist.log_prob(actions_tensor.long())
            else:
                log_probs = dist.log_prob(actions_tensor)
            return -self.surrogate_loss(log_probs, old_log_probs_tensor, advantages_tensor)
        
        # Rest of the TRPO update remains the same...
        loss = get_loss()
        grads = torch.autograd.grad(loss, self.policy.parameters(), create_graph=True)
        flat_grad = torch.cat([grad.view(-1) for grad in grads]).detach()
        
        step_dir = self.conjugate_gradient(states_tensor, old_dist, flat_grad, nsteps=self.cg_iters)
        
        shs = 0.5 * torch.dot(step_dir, self.hessian_vector_product(states_tensor, old_dist, step_dir))
        step_size = torch.sqrt(self.max_kl / (shs + 1e-8))
        full_step = step_size * step_dir
        
        old_params = torch.cat([param.view(-1) for param in self.policy.parameters()])
        
        def line_search():
            for alpha in [0.5**x for x in range(10)]:
                new_params = old_params + alpha * full_step
                self.set_policy_params(new_params)
                
                with torch.no_grad():
                    new_dist = self.policy(states_tensor)
                    new_loss = get_loss()
                    kl = self.kl_divergence(old_dist, new_dist)
                
                if kl <= self.max_kl and new_loss < loss:
                    return True
            return False
        
        if not line_search():
            self.set_policy_params(old_params)
    
    def set_policy_params(self, flat_params):
        prev_idx = 0
        for param in self.policy.parameters():
            flat_size = param.numel()
            param.data.copy_(flat_params[prev_idx:prev_idx + flat_size].view(param.size()))
            prev_idx += flat_size
    
    def compute_advantages(self, rewards, values, dones):
        advantages = np.zeros_like(rewards)
        last_advantage = 0
        
        for t in reversed(range(len(rewards))):
            if dones[t]:
                delta = rewards[t] - values[t]
                last_advantage = delta
            else:
                delta = rewards[t] + self.gamma * values[t+1] - values[t]
                last_advantage = delta + self.gamma * self.lambda_ * last_advantage
            advantages[t] = last_advantage
        
        advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
        return advantages
    
    def surrogate_loss(self, new_probs, old_probs, advantages):
        ratio = torch.exp(new_probs - old_probs)
        return torch.mean(ratio * advantages)
    
    def kl_divergence(self, old_dist, new_dist):
        if self.discrete:
            return torch.mean(torch.sum(old_dist.probs * (torch.log(old_dist.probs) - torch.log(new_dist.probs)), dim=1))
        else:
            return torch.distributions.kl.kl_divergence(old_dist, new_dist).mean()
    
    def hessian_vector_product(self, states, old_dist, vector):
        kl = self.kl_divergence(old_dist, self.policy(states))
        
        # First compute gradient of KL
        grads = torch.autograd.grad(kl, self.policy.parameters(), create_graph=True)
        flat_grad_kl = torch.cat([grad.view(-1) for grad in grads])
        
        # Compute gradient of (grad_KL * vector)
        grad_vector_product = torch.sum(flat_grad_kl * vector)
        grad_grad = torch.autograd.grad(grad_vector_product, self.policy.parameters(), retain_graph=True)
        flat_grad_grad = torch.cat([grad.contiguous().view(-1) for grad in grad_grad])
        
        return flat_grad_grad + self.cg_damping * vector
    
    def conjugate_gradient(self, states, old_dist, b, nsteps=10, residual_tol=1e-10):
        x = torch.zeros_like(b)
        r = b.clone()
        p = b.clone()
        rdotr = torch.dot(r, r)
        
        for i in range(nsteps):
            Avp = self.hessian_vector_product(states, old_dist, p)
            alpha = rdotr / torch.dot(p, Avp)
            x += alpha * p
            r -= alpha * Avp
            new_rdotr = torch.dot(r, r)
            if new_rdotr < residual_tol:
                break
            beta = new_rdotr / rdotr
            p = r + beta * p
            rdotr = new_rdotr
        
        return x

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

Функция def __init__() запускает агент TRPO с сетями политик и значений, оптимизаторами и гиперпараметрами для оптимизации доверительной области TRPO. Входные параметры включают гиперпараметры, некоторые из которых специфичны для TRPO, например, max_kl и cg_damping, другие — для обучения с подкреплением, например, gamma и lambda. При настройке этих гиперпараметров разумными являются значения по умолчанию max_kl=0.01, cg_damping=0.1 и lambda=0.97, однако они зависят от среды/набора данных.

Для более сложных сред, таких как многомерные наборы данных, для более строгих ограничений имеет смысл использовать меньшее значение max_kl, равное примерно 0,005, или большее значение cg_iters, равное примерно 20. Выбор оптимизатора Adam для сети значений является стандартным, однако сеть политик зависит от пользовательского обновления TRPO без использования оптимизатора. Также целесообразно убедиться, что скорость обучения нейронной сети достаточно мала для обеспечения большей стабильности обучения сети.

Функция get преобразует входное состояние в тензор PyTorch. Состояние обрабатывается в сети политик для получения распределения действий, из этого распределения выбирается действие для вычисления его логарифмической вероятности, и, наконец, действие преобразуется в формат, совместимый с окружающей средой. Этот формат представляет собой скаляр для дискретных действий и усеченный массив NumPy для непрерывных действий. 

Эта функция представляет собой интерфейс агента с окружающей средой, поскольку она позволяет выбирать действия на основе политики. Логарифмическая вероятность имеет решающее значение для градиентных вычислений TRPO, поскольку она используется в качестве суррогатной функции потерь для оценки эффективности политики. Ограничение непрерывных действий диапазоном [-1,1] может гарантировать, что выходные данные сети политик будут совместимы с тем, что среда ожидает в отношении ограниченных действий. При непрерывном действии необходимо контролировать стандартное отклонение, чтобы избежать вырожденных распределений. Используемый метод пакетной обработки предполагает входные данные в виде одного состояния, однако в векторизованных или многомерных состояниях его можно расширить для более эффективной обработки состояний.

Обновление сети значений преобразует входные состояния в Q-значение прогнозов сети политики, то есть в вознаграждение. Он вычисляет среднеквадратичную ошибку потерь по отношению к целевым значениям и обновляет сеть значений путем обратного распространения ошибки с помощью оптимизатора Adam. Точные оценки стоимости уменьшают вариативность градиентов политики и, следовательно, повышают стабильность TRPO. Функция потерь MSE гарантирует, что сеть создания ценности научится прогнозировать ожидаемую дисконтированную доходность и, таким образом, будет соответствовать цели обучения с подкреплением. Мы используем целевые значения, полученные методом временной разницы, для вычисления целевых значений при обучении сети значений. Эти расчеты должны быть выполнены с высокой точностью, поскольку неточные оценки могут привести к нестабильным обновлениям политики.

В качестве функции потерь используется стандартная среднеквадратичная ошибка (MSE), однако для большей устойчивости к выбросам в условиях высокой дисперсии/наборах данных можно также рассмотреть функцию потерь Хубера. Логика коррекции формы также представляется достаточно эффективной, однако в ситуациях с большими массивами данных она может оказаться проблематичной. Для этого может потребоваться предварительная оптимизация форм входных данных, чтобы обеспечить их предварительную обработку с использованием правильных форм. Кроме того, ограничение градиента может быть реализовано с помощью таких модулей, как Torch.nn.utils.clip_grd_norm_, чтобы ограничить чрезмерно большие обновления и тем самым стабилизировать обучение сети.

Функция обновления политики преобразует состояния, действия, старые логарифмические вероятности и вознаграждения в тензоры соответствующей формы. Она также вычисляет старое распределение политики для расчета KL-дивергенции и определяет суррогатную функцию потерь, которая является способом измерения ожидаемого вознаграждения при новой политике по сравнению со старой. Кроме того, функция обновления политики вычисляет градиент политики; использует метод сопряженных градиентов для определения направления поиска; и определяет размер шага на основе ограничения области доверия 'max-kl'. Она выполняет линейный поиск, который гарантирует, что новая политика удовлетворяет ограничениям KL-дивергенции, а также улучшает функцию потерь или возвращается к старым параметрам, если поиск не удается. 

Во многом это и есть суть TRPO, поскольку она реализует оптимизацию доверительной области, которая уравновешивает улучшение политики со стабильностью. Функция потерь, используемая в качестве суррогатной, оценивает целевую функцию градиента политики, а ограничение KL-дивергенции гарантирует отсутствие значительных изменений в политике, которые могут ухудшить производительность. Иными словами, метод сопряженных градиентов эффективно определяет направление поиска, а линейный поиск обеспечивает надежные обновления.

В TRPO параметр max-kl имеет решающее значение. Слишком малое значение, например, ниже 0,005, может чрезмерно ограничивать обновления, что может привести к очень медленному обучению. И наоборот, слишком большое значение, например, выше 0,05, может привести к дестабилизирующим обновлениям. TRPO стремится избежать именно этого. Параметр cg-iters (количество итераций сопряженного градиента) должен иметь достаточный размер для обеспечения сходимости. Для проверки точности решения необходимо также отслеживать остаточные значения.

Функция установки параметров политики обновляет параметры сети политики, создавая копию значений из сглаженного вектора, а затем изменяя их форму в соответствии с размером каждого параметра. Это позволяет обновлять пользовательские параметры TRPO, которые вычисляются как плоский вектор во время поиска линии сопряженного градиента. Таким образом, эта функция гарантирует, что после каждого обновления сетевая политика будет отражать оптимизированные параметры.

Расчет вознаграждений, который в коде называется compute-advantage, определяет вознаграждения с использованием обобщенной оценки преимущества (Generalized Advantage Estimation, GAE). Это включает в себя вычисление ошибки временной разницы для каждого временного шага. Сочетание ошибок TD и параметра lambda_ помогает сбалансировать смещение и дисперсию. Кроме того, в конце каждого эпизода обнуляется вознаграждение/преимущество, отслеживаемое параметром dones[t], и эти вознаграждения нормализуются таким образом, чтобы иметь нулевое среднее значение и единичную дисперсию.

Функция суррогатных потерь вычисляет суррогатные потери как ожидаемое значение отношения вероятностей πnew(a∣s)/πold(a∣s), умноженное на преимущества. Функция суррогатной потери оценивает целевую функцию градиента политики, измеряя, как изменения политики влияют на ожидаемые вознаграждения. В TRPO эта потеря максимизируется и, следовательно, нивелируется в get-loss в пределах ограничения доверительного региона.

Функция KL-дивергенции определяет величину расхождения между старыми и новыми распределениями политик. Когда действия дискретны, используется аналитическая формула категориальных распределений. Для непрерывных действий PyTorch использует многомерные нормальные распределения. Эти измерения помогают обеспечить соблюдение ограничения доверительного региона TRPO.

Функция векторного произведения Гессиана (Hessian vector product function), как следует из названия, вычисляет произведение Гессе, используемое для KL-дивергенции в методе сопряженных градиентов. В ходе вычислений вычисляется градиент KL-дивергенции, он умножается на входной вектор, а затем вычисляется градиент второго порядка. Это добавляет затухание (damping) для повышения численной устойчивости. Аппроксимируя действие матрицы информации Фишера на вектор, мы обеспечиваем эффективное вычисление направления поиска в TRPO. Затухание обеспечивает положительную определенность матрицы Гессе и сходимость сопряженного градиента.

Наконец, функция сопряженных градиентов реализует метод решения уравнения Hx = g, где H — матрица Фишера, аппроксимированная функцией hessian_vector_product, а g — градиент политики. Она итеративно уточняет свое решение x или направление поиска до тех пор, пока не будет достигнута сходимость или не будет выполнено n шагов итераций.



Тестовые запуски

Если мы выполним форвард-тестирование только для 3 типов признаков, которые удалось протестировать в предыдущей статье (признаки 2, 3 и 4), мы получим отчеты, представленные ниже. Протестируем валютную пару EURUSD в период с 01.01.2020 по 01.01.2025. Обучение проводилось на языке Python с использованием данных, охватывающих 80% этого периода, то есть с 01.01.2020 по 01.01.2024. 

r2

c2


r3

c3

r4

c4


Если учесть, что рассматриваемый период охватывает только 2024 год, то, похоже, удалось осуществить переход только по паттернам 2 и 3. Как всегда, здесь играет роль множество факторов, и перед использованием любого кода/материала, представленного в этих статьях, всегда рекомендуется провести независимую проверку. Для сборки в Мастере MQL5 и использования советников, подобных тому, что использовался в приведенных выше тестах, необходимо использовать приложенные файлы. Новички могут найти руководства здесь и здесь.


Заключение

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

В последующей статье, посвященной анализу этих индикаторных паттернов, предполагалось рассмотреть обучение на основе вывода (inference). В данном случае мы применяем вывод как средство обобщения и "архивирования" того, что изучается в рамках обучения с учителем и обучения с подкреплением. Иллюстрации этого подхода приведены в статье. Однако я решил оставить применение вывода на усмотрение читателя и вернуться к более простому формату статьи, в которой попеременно будут представлены некоторые идеи машинного обучения. 

Имя Описание
wz_62.mq5 Созданный Мастером советник, в заголовок которого включены использованные файлы
SignalWZ_62.mqh Файл класса пользовательских сигналов
61_2.onnx ONNX-модель обучения с учителем признака 2
61_3.onnx ONNX-модель обучения с учителем признака 3
61_4.onnx ONNX-модель обучения с учителем признака 4
62_policy_2.onnx Актер обучения с подкреплением признака 2
62_policy_3.onnx Актер обучения с подкреплением признака 3
62_policy_4.onnx Актер обучения с подкреплением признака 4
62_value_2.onnx Критик обучения с подкреплением признака 2
62_value_3.onnx Критик обучения с подкреплением признака 3
62_value_4.onnx Критик обучения с подкреплением признака 4

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17938

Прикрепленные файлы |
Experts.zip (1.57 KB)
MQL5.zip (835.64 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Нейросети в трейдинге: Гибридные модели прогнозирования с управляемой смесью распределений (Основные компоненты) Нейросети в трейдинге: Гибридные модели прогнозирования с управляемой смесью распределений (Основные компоненты)
В статье представлена практическая реализация модуля адаптивного прогнозирования, объединяющего подходы Lattice и Tail-Aware моделирования для финансовых временных рядов. Читатель увидит, как система адаптивно выбирает архетипы рынка, оценивает релевантность экспертов и формирует взвешенные прогнозные распределения с учётом тяжёлых хвостов и локальных экстремумов.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Создание и форвардное тестирование автономного LLM агента для трейдинга с SEAL Создание и форвардное тестирование автономного LLM агента для трейдинга с SEAL
Гибридная архитектура на базе Llama 3.2 и SEAL тестируется на восьми валютных парах (M15) с форвардной изоляцией данных и контролем утечки информации. Методология объединяет adversarial self-play, curriculum learning и балансировку классов для стабильного обучения. Эксперименты подтверждают разрыв между точностью прогноза и реальной доходностью, что дает читателю практические ориентиры по проверке стратегий и корректной оценке их обобщающей способности.