preview
Нейросети в трейдинге: Актер—Режиссёр—Критик (Окончание)

Нейросети в трейдинге: Актер—Режиссёр—Критик (Окончание)

MetaTrader 5Торговые системы | 21 апреля 2025, 07:02
233 1
Dmitriy Gizlyk
Dmitriy Gizlyk

Введение

В предыдущей статье мы познакомились с теоретическими аспектами фреймворка Актер—Режиссёр—Критик (Actor—Director—Critic), который является расширенной версией архитектуры Actor—Critic. Классическая архитектура Actor—Critic стала краеугольным камнем многих успешных RL-алгоритмов. В ней агент разделён на две части: Actor предлагает действия, а Critic оценивает их, исходя из полученного от окружающей среды вознаграждения. Эта связка позволяет постепенно выстраивать стратегию, где действия становятся всё более осмысленными, а оценки — всё более точными. Однако, несмотря на элегантность и эффективность, в реальных задачах, особенно в трейдинге, эта архитектура сталкивается с рядом серьёзных ограничений.

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

Для решения этой фундаментальной проблемы авторы фреймворка Actor—Director—Critic предложили добавить новый компонент — Director, который вносит в систему ещё один канал оценки действий. В отличие от Critic, который выносит непрерывную оценку действия на основе вознаграждения от среды, Director классифицирует действия бинарно: «подходит» или «не подходит» заданной стратегии. Это позволяет агенту быстрее и чётче ориентироваться в пространстве возможных решений.

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

Результатом является синергетическое взаимодействие трёх компонентов: Actor учится выбирать действия, Critic — оценивать их с точки зрения ожидаемого вознаграждения, а Director — чётко указывает, каких действий стоит избегать вовсе. Это создаёт двойную систему обратной связи: непрерывную и бинарную, которая позволяет быстрее отсекать бесперспективные направления и сосредотачиваться на продуктивных стратегиях.

Авторская визуализация фреймворка Actor—Director—Critic представлена ниже.

Авторская визуализация фреймворка Actor-Director-Critic

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

Прежде всего, мы наложили свое видение предложенных подходов на мультиагентный фреймворк HiSSD, рассмотренный ранее. Более того, в нашей реализации Director и Critic обучается на латентных признаках общих навыков Агента — сжатых представлениях, полученных из внутренних слоёв Энкодера состояния окружающей среды, а не на базовых её признаках. Благодаря этому, формируется обобщённое представление о паттернах действий, позволяя оценивать их в контексте глобальной поведенческой логики, а не конкретной рыночной ситуации. Такой подход даёт более стабильную, стратегически осмысленную обратную связь, критически важную в условиях ограниченной информации и высокой неопределённости, как это часто бывает на финансовых рынках.

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

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

На втором этапе начинается тонкая онлайн-настройка моделей. Здесь уже задействуется непосредственное взаимодействие агента с окружающей средой. Actor уточняет свою политику поведения в реальном времени, а Critic и Director продолжают обучение, улучшая качество оценок и классификаций на основе обновлённых данных. Этот этап позволяет адаптировать агента к текущей рыночной ситуации, сохраняя стратегическую направленность, заложенную в офлайн-обучении.

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


Офлайн-обучение

Алгоритм первого этапа обучения (офлайн) реализован в виде советника "…\Experts\ADC\Study.mq5". Основная часть его кода была позаимствована из предыдущего проекта, и в этом нет ничего удивительного. В основу архитектурного решения обучаемых моделей легли наработки из фреймворка HiSSD, с которым мы уже успели познакомиться.

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

Добавление двух дополнительных компонентов (Critic и Director) существенно расширило функциональность системы. Их интеграция потребовала модификации логики программы обучения моделей. В рамках данной статьи детально рассмотрим лишь алгоритм метода Train, в котором организован практически весь процесс непосредственного обучения моделей.

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

void Train(void)
  {
//---
   vector<float> probability = vector<float>::Full(Buffer.Size(), 1.0f / Buffer.Size());
//---
   vector<float> result, target, state;
   matrix<float> fstate = matrix<float>::Zeros(1, NForecast * BarDescr);
   bool Stop = false;
//---
   uint ticks = GetTickCount();

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

for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter += Batch)
  {
   int tr = SampleTrajectory(probability);
   int start = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - NForecast - Batch));
   if(start <= 0)
     {
      iter -= Batch;
      continue;
     }

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

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

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

if(
   !cEncoder.Clear()
   || !cTask.Clear()
   || !cActor.Clear()
   || !cProbability.Clear()
   || !cDirector.Clear()
   || !cCritic.Clear()
)
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
result = vector<float>::Zeros(NActions);

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

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

Если нарушить временную структуру данных, даже частично, то модель может утратить способность распознавать причинно-следственные связи, которые проявляются именно в динамике. А значит, ценность таких обучающих примеров резко снижается.

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

for(int i = start; i < MathMin(Buffer[tr].Total, start + Batch); i++)
  {
   if(!state.Assign(Buffer[tr].States[i].state) ||
      MathAbs(state).Sum() == 0 ||
      !bState.AssignArray(state))
     {
      iter -= Batch + start - i;
      break;
     }
   //---

В теле вложенного цикла мы загружаем из буфера воспроизведения опыта данные описания анализируемого состояния окружающей среды и формируем гармоники временной метки.

bTime.Clear();
double time = (double)Buffer[tr].States[i].account[7];
double x = time / (double)(D'2024.01.01' - D'2023.01.01');
bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
x = time / (double)PeriodSeconds(PERIOD_MN1);
bTime.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
x = time / (double)PeriodSeconds(PERIOD_W1);
bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
x = time / (double)PeriodSeconds(PERIOD_D1);
bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
if(bTime.GetIndex() >= 0)
   bTime.BufferWrite();

После чего, формируем вектор описания состояния счета.

//--- Account
float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0];
float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1];
float profit = float(bState[0] / _Point * (result[0] - result[3]));
bAccount.Clear();
bAccount.Add(1);
bAccount.Add((PrevEquity + profit) / PrevEquity);
bAccount.Add(profit / PrevEquity);
bAccount.Add(MathMax(result[0] - result[3], 0));
bAccount.Add(MathMax(result[3] - result[0], 0));
bAccount.Add((bAccount[3] > 0 ? profit / PrevEquity : 0));
bAccount.Add((bAccount[4] > 0 ? profit / PrevEquity : 0));
bAccount.Add(0);
bAccount.AddArray(GetPointer(bTime));
if(bAccount.GetIndex() >= 0)
   bAccount.BufferWrite();

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

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

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

В частности, мы вынуждены пересчитывать вектор описания состояния счёта, чтобы он соответствовал действиям из «почти идеальной траектории». Без этого Agent будет обучаться на несогласованных данных, где действия и их последствия не соответствуют друг другу. А это неизбежно приведёт к искажению сигналов.

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

//--- Feed Forward
if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!cTask.feedForward((CBufferFloat*)GetPointer(bState), 1, false, GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!cActor.feedForward((CBufferFloat*)GetPointer(bAccount), 1, false, GetPointer(cTask), -1))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!cProbability.feedForward(GetPointer(cEncoder), LatentLayer, (CBufferFloat*)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Здесь стоит обратить внимание, что на данном этапе мы не осуществляем прямой проход моделей оценки действий Агента — Критика и Режиссера. Это связано с использованием подходов «почти идеальной траектории». Но к этому вопросу вернемся немного позже.

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

//--- Look for target
target = vector<float>::Zeros(NActions);
bActions.AssignArray(target);
if(!state.Assign(Buffer[tr].States[i + NForecast].state) ||
   !state.Resize(NForecast * BarDescr) ||
   MathAbs(state).Sum() == 0)
  {
   iter -= Batch + start - i;
   break;
  }
if(!fstate.Resize(1, NForecast * BarDescr) ||
   !fstate.Row(state, 0) ||
   !fstate.Reshape(NForecast, BarDescr))
  {
   iter -= Batch + start - i;
   break;
  }
for(int j = 0; j < NForecast / 2; j++)
  {
   if(!fstate.SwapRows(j, NForecast - j - 1))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }
  }

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

//--- State Encoder
Result.AssignArray(fstate);
if(!cEncoder.backProp(Result, (CBufferFloat*)NULL, NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  } 

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

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

Процесс формирования «почти идеальной» торговой операции был подробно описан в предыдущей работе и я не вижу смысла сейчас его повторять.

Сформированный тензор «почти идеальной» торговой операции служит целевым значением для обучения нашего верхнеуровневого Актера-менеджера.

//--- Actor Policy
if(!cActor.backProp(GetPointer(bActions), (CNet*)GetPointer(cTask), -1))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

На этом этапе мы переходим к важнейшему компоненту обучающей архитектуры — обучению моделей оценки действий Актёра, то есть Критика и Режиссёра. Именно эти элементы отвечают за формирование обратной связи, которая направляет стратегию агента, помогая ему отличать результативные действия от неэффективных.

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

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

И вот здесь подход с «почти идеальной траекторией» даёт нам мощное преимущество. Благодаря наличию эталонного поведения, полученного путём заглядывания в будущее, мы можем сместить фокус оценки с того, что агент реально сделал, на то, что он должен был бы сделать. Иными словами, мы обучаем Критика не просто отличать хорошее от плохого, а ориентироваться на действия, близкие к стратегическому идеалу, заданному в виде «почти идеального» маршрута.

В этой парадигме мы осуществляем прямой проход Критика для оценки «почти идеальной» торговой операции.

//--- Critic
if(!cCritic.feedForward(GetPointer(bActions), 1, false, (CNet*)GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

И сразу выполняем операции обратного прохода с целью минимизации отклонения полученной оценки от рассчитанной по реальным историческим данным.

float reward = float((result[0] - result[3]) * fstate[0, 0] / Point());
Result.Clear();
if(!Result.Add(reward)
   || !cCritic.backProp(Result, (CNet*)GetPointer(cEncoder), LatentLayer)
   || !cEncoder.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer, true)
  )
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Ситуация с обучением Режиссёра в целом схожа с Критиком, однако имеет ряд важных нюансов. Как и любой классификатор, Режиссёр требует сбалансированного набора положительных и отрицательных примеров, на которых он сможет выучить границы между «хорошими» и «плохими» действиями агента.

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

Однако, на одних положительных примерах обучить надёжного классификатора невозможно. Необходим также репрезентативный пул «негативных» решений, которые должны демонстрировать поведение, не соответствующее нашим целям. И вот здесь возникает методологическая сложность: обучающая выборка по своей природе не охватывает всё пространство потенциальных действий — в ней представлены лишь те решения, которые агент действительно принимал в прошлом.

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

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

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

//--- Director
Result.Clear();
if((MathRand() / 32767.0) > 0.5)
   Result.Add(1);
else
  {
   target = vector<float>::Zeros(NActions);
   for(int i = 0; i < NActions; i++)
      target[i] = float(MathRand() / 32767.0);
   bActions.AssignArray(target);
   Result.Add(0);
  }
if(!cDirector.feedForward(GetPointer(bActions), 1, false, (CNet*)GetPointer(cEncoder), LatentLayer)
   || !cDirector.backProp(Result, (CNet*)GetPointer(cEncoder), LatentLayer)
   || !cEncoder.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer, true)
  )
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

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



Онлайн-обучение

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

Одним из ключевых преимуществ онлайн-обучения является непосредственное получение обратной связи от среды. Актёр принимает решение, действие осуществляется и, практически сразу, становится известен его эффект. Это позволяет оперативно корректировать поведение агента, усиливая успешные стратегии и отбрасывая неэффективные.

Однако этот подход не лишён серьёзных ограничений. Главное из них — невозможность заглянуть в будущее, как это делалось при построении «почти идеальной траектории» в офлайн-режиме. В онлайн-обучении агент принимает решения, исходя из текущего состояния и своей стратегии, не имея доступа к «знаниям из будущего». Это кардинально меняет условия обучения и требует перехода к более классической парадигме обучения с подкреплением (Reinforcement Learning).

Здесь обучение строится на основе проб и ошибок, а качество принимаемых решений оценивается, по факту, постфактум. Поэтому возрастает значение оценочных моделей — Критика и Режиссёра, которые выполняют функции внутреннего совета Актёру, направляя его поведение. Однако, в отличие от офлайн-режима, они теперь донастраиваются в ходе самой торговой деятельности, непрерывно адаптируясь к изменяющейся рыночной среде.

Но обо всем по порядку. Алгоритм онлайн-обучения мы построим в рамках советника "…\Experts\ADC\StudyOnline.mq5". Объем статьи ограничен. Поэтому мы остановимся на подробном рассмотрении только метода OnTick. Именно здесь обрабатывается событие поступления нового тика. И в нем же мы реализуем основной алгоритм обучения.

void OnTick()
  {
//---
   if(!IsNewBar())
      return;

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

После закрытия очередного бара, мы запрашиваем в терминале исторические данные на заданную глубину и формируем буферы исходных данных.

   int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, 1), HistoryBars, Rates);
   if(!ArraySetAsSeries(Rates, true))
      return;
//---
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
   Symb.Refresh();
   Symb.RefreshRates();
//---
   float atr = 0;
   for(int b = 0; b < (int)HistoryBars; b++)
     {
      float open = (float)Rates[b].open;
      float rsi = (float)RSI.Main(b);
      float cci = (float)CCI.Main(b);
      atr = (float)ATR.Main(b);
      float macd = (float)MACD.Main(b);
      float sign = (float)MACD.Signal(b);
      if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE ||
         macd == EMPTY_VALUE || sign == EMPTY_VALUE)
         continue;
      //---
      int shift = b * BarDescr;
      sState.state[shift] = (float)(Rates[b].close - open);
      sState.state[shift + 1] = (float)(Rates[b].high - open);
      sState.state[shift + 2] = (float)(Rates[b].low - open);
      sState.state[shift + 3] = (float)(Rates[b].tick_volume / 1000.0f);
      sState.state[shift + 4] = rsi;
      sState.state[shift + 5] = cci;
      sState.state[shift + 6] = atr;
      sState.state[shift + 7] = macd;
      sState.state[shift + 8] = sign;
     }
//---

Тут же загружаем данные о состоянии счета и открытые позиции.

   sState.account[0] = (float)AccountInfoDouble(ACCOUNT_BALANCE);
   sState.account[1] = (float)AccountInfoDouble(ACCOUNT_EQUITY);
//---
   double buy_value = 0, sell_value = 0, buy_profit = 0, sell_profit = 0;
   double position_discount = 0;
   double multiplyer = 1.0 / (60.0 * 60.0 * 10.0);
   int total = PositionsTotal();
   datetime current = TimeCurrent();
   for(int i = 0; i < total; i++)
     {
      if(PositionGetSymbol(i) != Symb.Name())
         continue;
      double profit = PositionGetDouble(POSITION_PROFIT);
      switch((int)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
            buy_value += PositionGetDouble(POSITION_VOLUME);
            buy_profit += profit;
            break;
         case POSITION_TYPE_SELL:
            sell_value += PositionGetDouble(POSITION_VOLUME);
            sell_profit += profit;
            break;
        }
      position_discount += profit - (current - PositionGetInteger(POSITION_TIME)) * multiplyer * MathAbs(profit);
     }
   sState.account[2] = (float)buy_value;
   sState.account[3] = (float)sell_value;
   sState.account[4] = (float)buy_profit;
   sState.account[5] = (float)sell_profit;
   sState.account[6] = (float)position_discount;
   sState.account[7] = (float)Rates[0].time;

После чего, формируем гармоники временной метки.

   bTime.Clear();
   double time = (double)Rates[0].time;
   double x = time / (double)(D'2024.01.01' - D'2023.01.01');
   bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
   x = time / (double)PeriodSeconds(PERIOD_MN1);
   bTime.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
   x = time / (double)PeriodSeconds(PERIOD_W1);
   bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
   x = time / (double)PeriodSeconds(PERIOD_D1);
   bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
   if(bTime.GetIndex() >= 0)
      bTime.BufferWrite();
//---
   bAccount.Clear();
   bAccount.Add((float)((sState.account[0] - PrevBalance) / PrevBalance));
   bAccount.Add((float)(sState.account[1] / PrevBalance));
   bAccount.Add((float)((sState.account[1] - PrevEquity) / PrevEquity));
   bAccount.Add(sState.account[2]);
   bAccount.Add(sState.account[3]);
   bAccount.Add((float)(sState.account[4] / PrevBalance));
   bAccount.Add((float)(sState.account[5] / PrevBalance));
   bAccount.Add((float)(sState.account[6] / PrevBalance));
   bAccount.AddArray(GetPointer(bTime));
//---
   if(bAccount.GetIndex() >= 0)
      if(!bAccount.BufferWrite())
         return;
//---
   bState.AssignArray(sState.state);

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

С другой стороны, на данном этапе мы можем оценить эффективность действий, совершенных Агентом на предыдущей временной отметке. И в наших интересах сделать это до обновления состояния моделей, которые пока ещё сохраняют результаты анализа предшествующего состояния окружающей среды.

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

if(!bFirstRun)
  {
   //--- Target Nets
   if(!cEncoder[1].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL)
      || !cTask[1].feedForward((CBufferFloat*)GetPointer(bState), 1, false,
                                                    (CNet*)GetPointer(cEncoder[1]), LatentLayer)
      || !cActor[1].feedForward((CBufferFloat*)GetPointer(bAccount), 1, false,
                                                                        GetPointer(cTask[1]), -1)
      || !cCritic[2].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer)
      || !cCritic[3].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer)
      || !cCritic[4].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer)
      || !cCritic[5].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer)
     )
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return;
     }

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

Далее мы переходим к обучению Критика. Здесь следует напомнить, что авторы фреймворка ActorDirectorCritic предложили использовать параллельно 2 Критика с двумя целевыми моделями для каждого. Вначале мы формируем целевые значения оценки последних действий Агента, с учетом полученного на данном этапе вознаграждения для первого Критика и осуществляем прямой обратный проход модели.

//--- Critic 1
cCritic[2].getResults(Result);
float reward = Result[0];
cCritic[4].getResults(Result);
reward = (reward + Result[0]) / 2 * DiscFactor + float(sState.account[1] - PrevEquity);
Result.Clear();
if(!Result.Add(reward)
   || !cCritic[0].backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   return;
  }

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

Вторым важным аспектом онлайн-обучения становится вопрос своевременности обновления политики Актёра. Очевидное и технически простое решение — использовать фиксированный счётчик итераций. После заданного количества шагов, стратегия агента обновляется. И надо признать, этот подход вполне оправдан в условиях настоящего онлайн-обучения, где каждое состояние среды уникально и невозможно к нему вернуться.

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

И здесь скрыта потенциальная проблема. Если мы используем наивный подход с жёстким отсчётом итераций, то при каждом повторе обучения, обновления стратегии Актёра будут приходиться на одни и те же состояния среды. Это резко снижает вариативность обучающей выборки, создавая искусственные смещения и мешая агенту выучить устойчивые закономерности.

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

if(cCritic[0].getRecentAverageError() <= cCritic[1].getRecentAverageError() &&
   (MathRand() % ActorUpdate) == 0)
   if(!cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
      || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
     )
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return;
     }

Аналогичные операции повторяем для второго Критика.

//--- Critic 2
cCritic[3].getResults(Result);
reward = Result[0];
cCritic[5].getResults(Result);
reward = (reward + Result[0]) / 2 * DiscFactor + float(sState.account[1] - PrevEquity);
Result.Clear();
if(!Result.Add(reward)
   || !cCritic[1].backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   return;
  }
if(cCritic[0].getRecentAverageError() > cCritic[1].getRecentAverageError() &&
   (MathRand() % ActorUpdate) == 0)
   if(!cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
      || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
     )
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return;
     }

С Режиссером на этот раз все гораздо проще. Прибыльные торговые операции относятся к положительным, а все остальные — к негативным.

//--- Director
Result.Clear();
if((sState.account[1] - PrevEquity) > 0)
   Result.Add(1);
else
   Result.Add(0);
if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
   || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
   || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
  )
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   return;
  }

Тут же мы корректируем параметры прогностической модели по направлению закрытой свечи.

 //--- Probability
 vector<float> target = vector<float>::Zeros(NActions / 3);
 if(sState.state[0] > 0)
    target[0] = 1;
 else
    if(sState.state[0] < 0)
       target[1] = 1;
 if(!Result.AssignArray(target)
    || !cProbability.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
    || !cEncoder[0].backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer)
   )
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    return;
   }
}

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

//--- New state
   if(!cEncoder[0].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL)
      || !cTask[0].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(cEncoder[0]),
                                                                                                   LatentLayer)
      || !cActor[0].feedForward((CBufferFloat*)GetPointer(bAccount), 1, false, (CNet*)GetPointer(cTask[0]), -1)
      || !cProbability.feedForward((CNet*)GetPointer(cEncoder[0]), LatentLayer, (CBufferFloat*)NULL)
      || !cDirector.feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
      || !cCritic[0].feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
      || !cCritic[1].feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
     )
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return;
     }

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

PrevBalance = sState.account[0];
PrevEquity = sState.account[1];

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

   vector<float> temp;
   cActor[0].getResults(temp);
//---
   if(temp.Size() < NActions)
      temp = vector<float>::Zeros(NActions);

Из тензора убираем взаимопоглощающие объемы.

double min_lot = Symb.LotsMin();
double step_lot = Symb.LotsStep();
double stops = (MathMax(Symb.StopsLevel(), 1) + Symb.Spread()) * Symb.Point();
if(temp[0] >= temp[3])
  {
   temp[0] -= temp[3];
   temp[3] = 0;
  }
else
  {
   temp[3] -= temp[0];
   temp[0] = 0;
  }

И переходим к дешифровке результатов, полученных от Актера. Если в них нет объема длинных позиций, то закрываем существующие, которые могли быть открыты ранее.

//--- buy control
   if(temp[0] < min_lot || (temp[1] * MaxTP * Symb.Point()) <= 2 * stops ||
                                 (temp[2] * MaxSL * Symb.Point()) <= stops)
     {
      if(buy_value > 0)
         CloseByDirection(POSITION_TYPE_BUY);
     }

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

else
  {
   double buy_lot = min_lot + MathRound((double)(temp[0] - min_lot) / step_lot) * step_lot;
   double buy_tp = NormalizeDouble(Symb.Ask() + temp[1] * MaxTP * Symb.Point(), Symb.Digits());
   double buy_sl = NormalizeDouble(Symb.Ask() - temp[2] * MaxSL * Symb.Point(), Symb.Digits());

При наличии ранее открытых позиций, осуществляем трейлинг торговых уровней.

if(buy_value > 0)
   TrailPosition(POSITION_TYPE_BUY, buy_sl, buy_tp);

А затем корректируем объем текущей позиции, путем частичного закрытия или добавления недостающего. К последней ситуации можно отнести и открытие новой позиции.

 if(buy_value != buy_lot)
   {
    if((buy_value - buy_lot) >= min_lot)
       ClosePartial(POSITION_TYPE_BUY, buy_value - buy_lot);
    else
       if((buy_lot - buy_value) >= min_lot)
          if(!Trade.Buy(buy_lot - buy_value, Symb.Name(), Symb.Ask(), buy_sl, buy_tp))
             if(Trade.CheckResultRetcode() == 10019)
               {
                Result.Clear();
                Result.Add(0);
                if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
                   || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
                   || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
                  )
                  {
                   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
                   return;
                  }
               }
   }
}

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

Аналогичным образом осуществляется корректировка короткой позиции.

//--- sell control
   if(temp[3] < min_lot || (temp[4] * MaxTP * Symb.Point()) <= 2 * stops ||
                                  (temp[5] * MaxSL * Symb.Point()) <= stops)
     {
      if(sell_value > 0)
         CloseByDirection(POSITION_TYPE_SELL);
     }
   else
     {
      double sell_lot = min_lot + MathRound((double)(temp[3] - min_lot) / step_lot) * step_lot;;
      double sell_tp = NormalizeDouble(Symb.Bid() - temp[4] * MaxTP * Symb.Point(), Symb.Digits());
      double sell_sl = NormalizeDouble(Symb.Bid() + temp[5] * MaxSL * Symb.Point(), Symb.Digits());
      if(sell_value > 0)
         TrailPosition(POSITION_TYPE_SELL, sell_sl, sell_tp);
      if(sell_value != sell_lot)
        {
         if((sell_value - sell_lot) >= min_lot)
            ClosePartial(POSITION_TYPE_SELL, sell_value - sell_lot);
         else
            if((sell_lot - sell_value) >= min_lot)
               if(!Trade.Sell(sell_lot - sell_value, Symb.Name(), Symb.Bid(), sell_sl, sell_tp))
                  if(Trade.CheckResultRetcode() == 10019)
                    {
                     Result.Clear();
                     Result.Add(0);
                     if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
                        || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
                        || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
                       )
                       {
                        PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
                        return;
                       }
                    }
        }
     }

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

   bFirstRun = false;
//---
   if((int(Rates[0].time / PeriodSeconds(TimeFrame)) % TragetUpdate) == 0)
     {
      if(MathRand() / 32767.0 > 0.5)
         cCritic[2].WeightsUpdate(GetPointer(cCritic[0]), tau);
      else
         cCritic[4].WeightsUpdate(GetPointer(cCritic[0]), tau);
      if(MathRand() / 32767.0 > 0.5)
         cCritic[3].WeightsUpdate(GetPointer(cCritic[1]), tau);
      else
         cCritic[5].WeightsUpdate(GetPointer(cCritic[1]), tau);
      cEncoder[1].WeightsUpdate(GetPointer(cEncoder[0]), tau);
      cTask[1].WeightsUpdate(GetPointer(cTask[0]), tau);
      cActor[1].WeightsUpdate(GetPointer(cActor[0]), tau);
     }
   if(PrevBalance < 50)
      ExpertRemove();
  }

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

Полный код советника онлайн-обучения моделей вы можете найти во вложении.



Тестирование

Мы проделали значительный объём работы по адаптации и реализации ключевых идей фреймворка Actor–Director–Critic средствами MQL5, интегрировав его компоненты в архитектуру обучаемых моделей. Была тщательно проработана логика взаимодействия между Актёром, Режиссёром и Критиком, а также реализованы оригинальные подходы к обучению агента. Настало время финального и, пожалуй, самого тревожного этапа — проверки эффективности реализованных решений на реальных исторических данных.

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

Для формирования обучающей выборки использовались случайные проходы агента в тестере стратегий MetaTrader 5, что позволило собрать широкий спектр поведенческих сценариев. В качестве базы были выбраны исторические котировки валютной пары EURUSD на таймфрейме M1 за весь 2024 год.

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

Объективную оценку качества сформированной торговой политики в боевых условиях можно осуществить по результатам тестирования обученных моделей за пределами обучающей выборки. В качестве тестового периода были выбраны исторические данные за ЯнварьМарт 2025 года. Данный временной отрезок не использовался в обучении, что исключает переобучение и придаёт результатам реальную практическую ценность.

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

Результаты тестирования представлены ниже и дают возможность наглядно оценить поведенческую модель агента.

За период тестирования модель совершила 684 торговых операции. Из них 268 было закрыто с прибылью, что составило чуть более 39%. Однако в целом за период тестирования, модель смогла сгенерировать прибыль благодаря тому, что средняя прибыльная сделка почти в 2 раза превышает аналогичный показатель убыточных операций.



Заключение

В рамках данной работы мы познакомились с теоретическими аспектами фреймворка Actor–Director–Critic и реализовали свое видение предложенных подходов средствами MQL5. При этом была проведена его полная интеграция с уже существующей мультиагентной архитектурой. В результате получен модульный, гибкий и эффективно обучаемый агент, способный учитывать не только локальные оценки действий (через Критика), но и стратегический контекст поведенческой логики (через Режиссёра). Такой подход обеспечивает более точную и устойчивую обратную связь для Актёра, позволяя агенту быстрее отбрасывать неэффективные действия и исследовать продуктивные направления в пространстве политик.

Проведённое тестирование подтвердило работоспособность предложенного подхода и показало, что Actor–Director–Critic способен принимать более взвешенные решения, демонстрируя уверенное поведение даже в условиях рыночной неопределённости.

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


Ссылки


Программы, используемые в статье

#ИмяТипОписание
1Research.mq5СоветникСоветник сбора примеров
2ResearchRealORL.mq5
Советник
Советник сбора примеров методом Real-ORL
3Study.mq5СоветникСоветник офлайн обучения моделей
4StudyOnline.mq5
Советник
Советник онлайн обучения моделей
4Test.mq5СоветникСоветник для тестирования модели
5Trajectory.mqhБиблиотека классаСтруктура описания состояния системы и архитектуры моделей
6NeuroNet.mqhБиблиотека классаБиблиотека классов для создания нейронной сети
7NeuroNet.clБиблиотекаБиблиотека кода OpenCL-программы
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Sergey Chalyshev
Sergey Chalyshev | 21 апр. 2025 в 17:58
Безграмотность порождает безграмотность

https://nukadeti.ru/basni/krylov-kvartet

Критерии тренда. Окончание Критерии тренда. Окончание
В этой статье мы рассмотрим особенности применения некоторых критериев тренда на практике. А также сделаем попытку разработать несколько новых критериев. Основное внимание будет уделено эффективности применения этих критериев для анализа рыночных данных и трейдинга.
Самооптимизирующийся советник на языках MQL5 и Python (Часть IV): Стекинг моделей Самооптимизирующийся советник на языках MQL5 и Python (Часть IV): Стекинг моделей
В статье мы продемонстрируем, как можно создавать торговые приложения на базе ИИ, способные учиться на собственных ошибках. Мы рассмотрим технику, известную как стекинг (stacking), при которой мы используем 2 модели для создания 1 прогноза. Первая модель, как правило, является более слабым обучающимся алгоритмом, а вторая - более мощной моделью, которая обучается на результатах более слабого алгоритма. Наша цель — создать ансамбль моделей, чтобы достичь более высокой точности.
От начального до среднего уровня: Массив (II) От начального до среднего уровня: Массив (II)
В этой статье мы рассмотрим, что такое динамический массив и массив статический. И есть ли разница между использованием одного или другого. Или они всегда одинаковы? Когда следует использовать один, а когда другой? А как насчет константных массивов? Для чего они существуют, и какому риску я подвергаюсь, если не инициализирую все значения в массиве? Предположим, что они будут равны нулю.
Угловой анализ ценовых движений: гибридная модель прогнозирования финансовых рынков Угловой анализ ценовых движений: гибридная модель прогнозирования финансовых рынков
Что такое угловой анализ финансовых рынков? Как использовать углы движения цен и машинное обучение для точного прогнозирования с точностью 67? Как совместить регрессионную и классификационную модель с угловыми признаками и получить работающий алгоритм? Причем тут Ганн? Почему углы движения цен являются хорошим признаком для машинного обучения?