Нейросети в трейдинге: От единственного прогноза к пространству рыночных сценариев (кодовая книга сценариев)
Введение
Финансовый рынок редко оставляет модели единственный очевидный путь. После одного и того же состояния импульс может продолжиться, перейти в коррекцию или затухнуть в боковом движении. Каждый вариант способен выглядеть правдоподобно в момент принятия решения, хотя фактически реализуется только один.
Обычная прогностическая модель вынуждена свести эту развилку к одной траектории. Когда похожие состояния в обучающей выборке получают разные продолжения, минимизация средней ошибки подталкивает модель к компромиссу. Продолжение тренда и глубокая коррекция превращаются в умеренное движение, а резкий импульс и консолидация — в сглаженную линию. Формально ошибка может оставаться небольшой, но такой прогноз нередко не соответствует ни одному реальному рыночному режиму.
Для торговли потеря альтернатив особенно чувствительна. Недостаточно знать предполагаемую цену через несколько баров. Важно понимать путь к ней: возможную глубину встречного движения, продолжительность импульса, границу отмены исходной гипотезы и диапазон, в котором прогноз ещё сохраняет смысл. Одна усреднённая линия скрывает эту структуру и заставляет торговую политику восстанавливать её косвенно.
Для практикующего трейдера различие принципиально. Две траектории могут завершаться примерно на одном ценовом уровне, но требовать совершенно разных действий. Одна ведёт к цели почти без отката, другая сначала проходит заметное движение против позиции. В первом случае допустим короткий защитный уровень, во втором такой же стоп превратит верную идею в убыточную сделку ещё до реализации основного движения. Поэтому торговому контуру важна не только конечная точка прогноза, но и вся форма предполагаемого пути.
Не менее важна устойчивость плана во времени. Если модель каждый бар строит новый единственный прогноз, торговая политика может постоянно менять направление, переносить уровни выхода и пересматривать допустимый риск. Набор сценариев позволяет держать несколько заранее сформированных гипотез и оценивать, какая из них подтверждается по мере поступления новых данных. Это снижает вероятность того, что обычный рыночный шум будет принят за полный слом исходного замысла.
Естественным решением становится переход от единственного прогноза к пространству сценариев. Однако простое добавление нескольких выходных голов не создаёт осмысленного разнообразия. При одинаковом обучающем сигнале они быстро начинают копировать друг друга. Жёсткое разнесение прогнозов ведёт к другой крайности: часть траекторий может стать необычной только ради различий и потерять связь с данными. Следовательно, сценарии должны специализироваться на разных продолжениях рынка, но оставаться реалистичными.
При этом сценарное пространство не должно превращаться в коллекцию произвольных линий. Полезны не любые отличия, а только те, которые отражают реальные развилки в данных. Если рынок допускает продолжение импульса и коррекцию, модель должна удержать оба варианта. Если же исторически похожие состояния почти всегда развивались одинаково, искусственно добавлять противоположные траектории бессмысленно.
Для обучения нужен механизм специализации. Он должен определять, какие сценарии лучше объяснили реализованное будущее, и направлять в них основной градиент; остальные варианты не должны получать усредняющее обновление на каждом примере. Именно здесь возникают сценарные прототипы, разреженная ответственность и отдельный роутер априорных вероятностей.
Эта постановка развивает архитектурную идею ORION. В исходной работе авторы решают задачу автономного вождения и связывают семантическое пространство рассуждений с числовым пространством действий. QT-Former агрегирует текущую сцену и долгосрочный исторический контекст, языковая модель формирует состояние планирования, а генеративный планировщик раскрывает его в мультимодальные траектории.
В авторском ORION мультимодальность появляется на уровне планирования движения автомобиля. Несколько траекторий позволяют сохранить разные допустимые продолжения сцены, а планировщик связывает высокоуровневое рассуждение с конкретной последовательностью действий. Для рынка этот принцип особенно близок: наблюдаемая ситуация также не определяет единственного будущего, а решение должно учитывать не только наиболее вероятный вариант, но и соседние риски.
Вместе с тем мы не переносим QT-Former, языковую модель и автомобильный планировщик буквально. Рыночная адаптация использует собственное латентное представление, собственную память и отдельный торговый контур. От исходной работы сохраняется архитектурная граница между пониманием ситуации и генерацией действий. Кодовая книга сценариев, разреженный выбор Top‑K и передача всего набора прогнозов Актору развивают эту идею применительно к финансовым временным рядам.
Мы переносим в торговлю не автомобильную архитектуру буквально, а её основной принцип: представление ситуации не должно сразу превращаться в единственное действие. Между анализом рынка и торговой операцией появляется сценарный уровень. Он отвечает на вопрос, как может развиваться рынок, тогда как последующий Актор решает, что делать с учётом состояния счёта, открытых позиций и риска. Сценарные прототипы, разреженная ответственность и отдельная торговая политика относятся уже к нашей адаптации, а не к авторскому ORION.
В предыдущей статье мы сформулировали архитектуру такого прогноза и создали её вычислительный фундамент. Были определены роли сценарных прототипов, общего генератора, роутера априорных вероятностей, блока оценки неопределённости и разреженного распределения ответственности. Также мы описали, как эти компоненты должны взаимодействовать при формировании и обучении нескольких вариантов будущего.
Практическая часть работы была ограничена созданием двух OpenCL‑кернелов, которые реализовали отдельные вычислительные операции: подготовку сценарных траекторий и расчёт обучающих оценок. Был заложен вычислительный фундамент. В этой работе мы переходим к его архитектурному оформлению: создаём объект кодовой книги, организуем внутреннее состояние сценариев и объединяем отдельные компоненты в класс, который можно встроить в общую структуру нейронной сети.

Банк рыночных сценариев
Начнём работу с построения объекта CScenarioCodebook. Он не формирует торговый сигнал и не выбирает сценарий-победитель. Его задача — хранить устойчивый набор прототипов, вокруг которых генератор строит прогнозные траектории.
Условно кодовую книгу можно представить как каталог типовых движений. Один прототип со временем может закрепиться за продолжением импульса, другой — за коррекцией, третий — за переходом в диапазон. Эти роли не задаются вручную. Модель выделяет их в латентном пространстве по мере обучения, поэтому каждый сценарий получает собственный вектор и собственную статистику использования.
Важно, что прототип не является готовым шаблоном в привычном техническом анализе. Мы не задаём ему форму. Это вектор в скрытом пространстве, который описывает целую прогнозную траекторию: последовательность состояний на заданном горизонте и, при необходимости, несколько связанных переменных. Его смысл возникает только в процессе обучения.
Поэтому сценарии не следует заранее подписывать как рост, падение или боковое движение. Один прототип может специализироваться на продолжении импульса при высокой волатильности, другой — на похожем направлении, но с глубоким промежуточным откатом. Для торгового решения такое различие не менее важно, чем сам знак будущего изменения цены.
class CScenarioCodebook : public CObject { protected: uint iScenarios; uint iDimension; COpenCLMy *OpenCL; CBufferFloat cPrototypes; CBufferFloat cEMASums; CBufferFloat cEMACounts; CBufferFloat cUsage; CBufferFloat cInactive; CBufferFloat cInactivityAge; public: CScenarioCodebook(void) : iScenarios(0), iDimension(0), OpenCL(NULL) {}; ~CScenarioCodebook(void) { OpenCL = NULL; }; //--- bool Init(uint scenarios, uint dimension, COpenCLMy *open_cl); bool Save(const int file_handle); bool Load(const int file_handle, COpenCLMy *open_cl); bool ValidateContract(uint scenarios, uint dimension); void SetOpenCL(COpenCLMy *open_cl); uint Scenarios(void) const { return iScenarios; } uint Dimension(void) const { return iDimension; } CBufferFloat *GetPrototypes(void) { return GetPointer(cPrototypes); } CBufferFloat *GetEMASums(void) { return GetPointer(cEMASums); } CBufferFloat *GetEMACounts(void) { return GetPointer(cEMACounts); } CBufferFloat *GetUsage(void) { return GetPointer(cUsage); } CBufferFloat *GetInactive(void) { return GetPointer(cInactive); } CBufferFloat *GetInactivityAge(void) { return GetPointer(cInactivityAge); } };
Параметр iScenarios задаёт число прототипов, а iDimension — размер каждого из них. Слишком маленькая кодовая книга объединит разные режимы. Слишком большая создаст много редко используемых сценариев, которым сложно накопить статистику. Поэтому размер словаря должен соответствовать разнообразию данных, а не стремлению получить как можно больше вариантов.
Подбор числа сценариев становится компромиссом между детализацией и устойчивостью. Если прототипов мало, один центр будет вынужден обслуживать несколько разных форм движения и снова начнёт их усреднять. Если их слишком много, часть кодовой книги останется без устойчивых назначений. Такие сценарии будут зависеть от случайной инициализации сильнее, чем от данных. Поэтому количество прототипов следует оценивать вместе с энтропией их использования и числом неактивных элементов, а не выбирать из принципа чем больше, тем лучше.
Основные значения хранятся в cPrototypes. Рядом расположены EMA‑суммы и счётчики, статистика использования, признаки активности и возраст бездействия. Методы сохранения, загрузки и проверки контракта позволяют восстановить кодовую книгу вместе с её накопленным состоянием, а не только с текущими векторами.
Внутренняя память создаётся в методе инициализации. Здесь проверяются размеры, выделяются буферы и задаётся начальное состояние всех сценариев.
bool CScenarioCodebook::Init(uint scenarios, uint dimension, COpenCLMy *open_cl) { if(scenarios == 0 || dimension == 0) ReturnFalse; iScenarios = scenarios; iDimension = dimension; OpenCL = open_cl; const int total = int(iScenarios * iDimension); if(!cPrototypes.BufferInit(total, 0) || !cEMASums.BufferInit(total, 0) || !cEMACounts.BufferInit(iScenarios, 0) || !cUsage.BufferInit(iScenarios, 0) || !cInactive.BufferInit(iScenarios, 1) || !cInactivityAge.BufferInit(iScenarios, 0)) ReturnFalse;
Буфер cPrototypes содержит непрерывные участки памяти для всех сценариев. cEMASums и cEMACounts поддерживают их плавное обновление методом экспоненциального скользящего среднего. Вместо резкого переноса прототипа к одному новому наблюдению модель накапливает статистику и постепенно смещает центр соответствующей группы.
Такой режим естественен для рынка. Один ценовой выброс не должен немедленно менять представление о типичном сценарии. Если сходные движения повторяются, их влияние накапливается и прототип адаптируется. EMA отсекает единичный шум, но сохраняет реакцию на устойчивое изменение данных.
Коэффициент затухания определяет инерцию этой памяти. При большом значении отдельное наблюдение влияет на прототип слабо, зато его форма остаётся устойчивой. Более быстрое обновление помогает адаптироваться к смене режима, но повышает риск следовать за шумом. Здесь проявляется знакомая трейдеру дилемма: слишком медленная модель опаздывает, слишком быстрая начинает принимать каждое колебание за новую закономерность.
Буфер cUsage отражает востребованность сценариев. cInactive и cInactivityAge позволяют отличить временно неиспользуемый прототип от фактически выпавшего из работы. Это важно при смене рыночного режима: сценарий, полезный в тренде, может долго не получать назначений во время консолидации, а затем снова стать актуальным.
Статистика активности нужна не только для наблюдения. Она позволяет управлять ограниченной ёмкостью кодовой книги. Если прототип длительное время не получает назначений, он перестаёт описывать текущую выборку и фактически занимает место впустую. Возраст бездействия даёт возможность обнаружить такой сценарий и позднее вернуть его в рабочую область пространства.
При этом временная пассивность сама по себе не считается ошибкой. На рынке отдельные режимы могут исчезать на сотни баров, а затем возвращаться. Поэтому система хранит не один бинарный флаг, а историю бездействия. Это позволяет отличить редкий, но полезный сценарий от прототипа, который окончательно потерял связь с данными.
После выделения памяти прототипы получают начальные значения. Для этого используется независимая детерминированная последовательность, которая не изменяет глобальное состояние генератора случайных чисел терминала.
//--- independent deterministic prototypes without changing the terminal RNG for(int i = 0; i < total; i++) { xor128; const float value = float((double(rnd_w) / UINT_MAX) * 2.0 - 1.0); if(!cPrototypes.Update(i, value) || !cEMASums.Update(i, value)) ReturnFalse; }
Каждый элемент инициализируется в диапазоне от −1 до 1 и одновременно записывается в cPrototypes и cEMASums. Текущее положение прототипа сразу согласуется с накопленной статистикой. Разные начальные значения также разрушают симметрию: сценарии стартуют из разных точек и не обязаны повторять один и тот же путь с первой итерации.
for(uint k = 0; k < iScenarios; k++) if(!cEMACounts.Update(k, 1.0f)) ReturnFalse; SetOpenCL(OpenCL); //--- return true; }
EMA‑счётчики устанавливаются равными единице. Это исключает деление на ноль и позволяет считать случайный прототип первым условным наблюдением собственной группы. Затем буферы подключаются к OpenCL‑устройству.
После инициализации кодовая книга должна оставаться согласованной не только в памяти, но и при сохранении модели. Поэтому объект записывает в файл прототипы, EMA‑суммы, счётчики, статистику использования и признаки активности. При загрузке восстанавливается весь накопленный контекст. Если сохранить только текущие векторы, модель забудет, насколько надёжен каждый сценарий и как долго он оставался без назначений.
Дополнительные проверки сопоставляют заявленное число сценариев и размерность с фактическими размерами буферов. Отдельно контролируется порядок индексов. Один и тот же номер должен указывать на соответствующие прототип, EMA‑статистику, счётчик использования и возраст бездействия. Такое правило кажется техническим, но его нарушение смешало бы историю разных сценариев и сделало дальнейшее обучение бессмысленным.
Случайная инициализация сама по себе ещё не гарантирует специализацию. За неё отвечает последующее распределение ответственности. Кодовая книга лишь создаёт необходимую основу: хранит геометрию сценарного пространства, накапливает статистику и отслеживает варианты, которые перестали использоваться.
В результате CScenarioCodebook становится долговременной памятью сценарного модуля. Тяжёлые вычисления выполняют OpenCL‑кернелы, а объект поддерживает согласованное состояние прототипов и предоставляет его остальным компонентам модели.
Слой прогноза сценариев
Кодовая книга хранит варианты будущего, но не адаптирует их к текущему рынку. Для этого нужен слой, который сопоставит анализируемое состояние с каждым прототипом, построит траекторию, оценит её априорную вероятность и укажет допустимый масштаб ошибки. Эту задачу решает CNeuronScenarioForecast.
class CNeuronScenarioForecast : public CNeuronBaseOCL { protected: uint iVariables; uint iScenarios; uint iTopK; uint iHorizon; uint iDimension; uint iLastAssignment; bool bLastTargetAccepted; bool bLastControlValid; //--- CScenarioCodebook cCodebook; CNeuronBaseOCL cGeneratorInput; CNeuronBaseOCL cRouterInput; CNeuronBaseOCL cConfidenceInput; CNeuronConvOCL cGenerator; CNeuronConvOCL cRouter; CNeuronConvOCL cConfidenceHead; CNeuronSoftMaxOCL cRouterSoftmax; CNeuronBaseOCL cResponsibilityScores; CNeuronSparseSoftMax cTopKSelector; //--- CBufferFloat cConfidence; CBufferFloat cLogConfidence; CBufferFloat cLogProbabilities; CBufferFloat cResponsibilities; CBufferFloat cTarget; CBufferFloat cHardLoss; CBufferFloat cBatchControl; CBufferFloat cRecovery; CBufferFloat cForwardValidity; CBufferFloat cDiagnosticPartials; CBufferFloat cEpochDiagnostics; CBufferFloat cDiagnosticScratch; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; //--- bool ValidateLaunchConfiguration(COpenCLMy *open_cl, uint scenarios, uint horizon, uint latent, uint variables); bool BindResponsibilitiesKernel(CBufferFloat *target_source, CBufferFloat *observation_by_scenario); bool UpdateCodebook(float decay = 0.99f); public: CNeuronScenarioForecast(void) : iVariables(0), iScenarios(0), iTopK(0), iHorizon(0), iDimension(0), iLastAssignment(0), bLastTargetAccepted(false), bLastControlValid(false) {}; ~CNeuronScenarioForecast(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint scenarios = 21, uint top_k = 3, uint horizon = 12, uint latent = 16, ENUM_OPTIMIZATION optimization_type = ADAM, uint batch = 1, uint variables = 1); //--- bool BuildResponsibilities(CBufferFloat *target); bool NormalizeObservationGradient(CBufferFloat *observation); bool ResetEpochDiagnostics(void); bool CommitBatchDiagnostics(CBufferFloat *observation_by_scenario); bool BuildEpochCodebookDiagnostics(void); bool ReadEpochDiagnostics(double &mix_loss, double &router_loss, double &trajectory_loss, double &confidence_loss, double &latent_error, double &observation_error, double &valid_batches, double &invalid_batches, double &usage_entropy, double &pairwise_codebook, double &inactive_count); //--- virtual int Type(void) const override { return defNeuronScenarioForecast; } virtual bool Save(const int file_handle) override; virtual bool Load(const int file_handle) override; bool Load(const int file_handle, COpenCLMy *open_cl); virtual void SetOpenCL(COpenCLMy *obj) override; virtual void TrainMode(bool flag) override; virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual bool Clear(void) override; //--- uint Variables(void) const { return iVariables; } uint Scenarios(void) const { return iScenarios; } uint TopK(void) const { return iTopK; } uint Horizon(void) const { return iHorizon; } uint Dimension(void) const { return iDimension; } //--- CBufferFloat *GetZ(void) { return getOutput(); } CBufferFloat *GetU(void) { return GetPointer(cConfidence); } CBufferFloat *GetLogU(void) { return GetPointer(cLogConfidence); } CBufferFloat *GetPi(void) { return cRouterSoftmax.getOutput(); } CBufferFloat *GetLogPi(void) { return GetPointer(cLogProbabilities); } CBufferFloat *GetResponsibilities(void) { return GetPointer(cResponsibilities); } //--- CScenarioCodebook *GetCodebook(void) { return GetPointer(cCodebook); } CNeuronConvOCL *GetGenerator(void) { return GetPointer(cGenerator); } CNeuronConvOCL *GetRouter(void) { return GetPointer(cRouter); } CNeuronConvOCL *GetConfidenceHead(void) { return GetPointer(cConfidenceHead); } CNeuronSoftMaxOCL *GetRouterSoftmax(void) { return GetPointer(cRouterSoftmax); } CNeuronSparseSoftMax *GetTopKSelector(void) { return GetPointer(cTopKSelector); } //--- ulong ContractSignature(void); bool ExportDetached(CBufferFloat *z, CBufferFloat *u, CBufferFloat *log_u, CBufferFloat *pi, CBufferFloat *log_pi); uint TrainableWeights(void); uint LastAssignment(void) const { return iLastAssignment; } bool LastTargetAccepted(void) const { return bLastTargetAccepted; } bool LastBatchControlValid(void) const { return bLastControlValid; } uint ActiveTrajectories(void); uint DenseRouterGradientCount(void); };
Геометрию выхода задают пять параметров. iScenarios определяет число вариантов, iHorizon — длину прогноза, iDimension — размер латентного состояния на шаге, а iVariables позволяет работать с несколькими связанными последовательностями. Параметр iTopK указывает, сколько наиболее подходящих сценариев получат активную ответственность на одном обучающем примере.
Внутри слоя работают три обучаемые ветви. Генератор строит траектории, роутер оценивает относительную уместность сценариев, а блок неопределённости определяет масштаб ожидаемой ошибки. Softmax переводит выход роутера в распределение вероятностей, а разреженный селектор оставляет для обучения траекторий только Top‑K вариантов.
Генератор получает текущее состояние вместе с конкретным прототипом. Он не создаёт траекторию с нуля, а уточняет базовую форму сценария под наблюдаемый рынок. Благодаря этому знания о динамике разделяются между всеми вариантами, тогда как различия сохраняются в кодовой книге.
Роутер решает другую задачу. Он не знает фактического будущего и не может оценивать точность уже случившегося прогноза. Его выход показывает, насколько каждый сценарий уместен в текущем контексте до реализации следующего движения. Именно поэтому вероятности роутера позднее можно передать торговому Актору.
Ветвь неопределённости дополняет траекторию масштабом ожидаемой ошибки. Два одинаково направленных прогноза могут иметь разную практическую ценность: один описывает узкий и устойчивый путь, другой допускает широкий диапазон колебаний. Без этой оценки торговая политика снова была бы вынуждена самостоятельно угадывать, насколько строго следует воспринимать форму сценария.
Такое разделение ролей защищает модель от двух крайностей. Общий генератор использует знания, накопленные на всей выборке, но разные прототипы задают ему разные условия. Разреженная ответственность не позволяет всем сценариям постоянно двигаться к одной цели, а плотный роутер учится сравнивать весь набор вариантов.
При обучении эти ветви получают разные сигналы. Генератор должен специализировать несколько лучших сценариев на реализованной траектории. Роутер, напротив, сравнивает весь набор и учится перераспределять вероятность между всеми вариантами. Блок неопределённости связывает заявленную ширину прогноза с фактической ошибкой. Если смешать эти задачи в одной голове, модель легко найдёт формальный, но бесполезный компромисс.
Создание слоя начинается с проверки вычислительного контракта.
bool CNeuronScenarioForecast::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint scenarios, uint top_k, uint horizon, uint dimension, ENUM_OPTIMIZATION optimization_type, uint batch, uint variables) { #define ORION_FORECAST_INIT_STAGE(EXPR,STAGE) if(!(EXPR)) { PrintFormat("%s line %d: %s", __FUNCTION__, __LINE__, STAGE); return false; } if(top_k == 0 || top_k > scenarios || !ValidateLaunchConfiguration(open_cl, scenarios, horizon, dimension, variables)) { PrintFormat("%s: invalid launch contract S=%u TopK=%u H=%u D=%u V=%u", __FUNCTION__, scenarios, top_k, horizon, dimension, variables); return false; }
Сначала контролируются логические ограничения: Top‑K должен быть положительным и не превышать число сценариев. Затем ValidateLaunchConfiguration сопоставляет конфигурацию с возможностями конкретного OpenCL‑устройства. Проверяются размеры рабочих групп, ограничения отдельных кернелов и объёмы будущих буферов. Это позволяет отклонить чрезмерную конфигурацию до выделения памяти и запуска обучения.
iVariables = variables; iScenarios = scenarios; iTopK = top_k; iHorizon = horizon; iDimension = dimension; iLastAssignment = 0; bLastTargetAccepted = false; bLastControlValid = false;
После проверки параметры сохраняются, а служебные флаги сбрасываются. Они позднее покажут, была ли последняя цель принята и удалось ли получить корректную управляющую информацию от устройства.
ORION_FORECAST_INIT_STAGE(CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl,
iScenarios * iVariables * iHorizon * iDimension,
optimization_type, batch), "base output");
Родительский класс создаёт основной выходной тензор размером сценарии × переменные × горизонт × латентная размерность. В нём хранится не одна линия, а полный набор прогнозных траекторий.
ORION_FORECAST_INIT_STAGE(cConfidence.BufferInit(iScenarios * iVariables * iHorizon, 1.0f), "confidence BufferInit"); ORION_FORECAST_INIT_STAGE(cConfidence.BufferCreate(OpenCL), "confidence BufferCreate");
Буфер cConfidence содержит положительный масштаб неопределённости для каждого сценария, переменной и шага горизонта. Начальное значение 1.0 задаёт нейтральный масштаб до того, как ветвь уверенности начнёт адаптироваться.
ORION_FORECAST_INIT_STAGE(cLogConfidence.BufferInit(iScenarios * iVariables * iHorizon,
MathLog(1.000001f)), "log-confidence BufferInit");
ORION_FORECAST_INIT_STAGE(cLogConfidence.BufferCreate(OpenCL), "log-confidence BufferCreate");
cLogConfidence хранит логарифмы тех же величин. Логарифмическая форма используется в вероятностной функции потерь и избавляет от повторных вычислений внутри кернелов.
ORION_FORECAST_INIT_STAGE(cLogProbabilities.BufferInit(iScenarios, MathLog(1.0f / iScenarios)), "log-probabilities BufferInit"); ORION_FORECAST_INIT_STAGE(cLogProbabilities.BufferCreate(OpenCL), "log-probabilities BufferCreate");
cLogProbabilities начинается с равномерного распределения: до анализа контекста ни один сценарий не получает преимущества.
ORION_FORECAST_INIT_STAGE(cResponsibilities.BufferInit(iScenarios, 0), "responsibilities BufferInit"); ORION_FORECAST_INIT_STAGE(cResponsibilities.BufferCreate(OpenCL), "responsibilities BufferCreate");
cResponsibilities хранит обучающее распределение ответственности, которое строится уже после появления фактической траектории. Это не то же самое, что априорная вероятность роутера: вероятность доступна во время прогноза, ответственность используется для обучения по реализованному будущему.
ORION_FORECAST_INIT_STAGE(cTarget.BufferInit(iVariables * iHorizon * iDimension, 0), "target BufferInit"); ORION_FORECAST_INIT_STAGE(cTarget.BufferCreate(OpenCL), "target BufferCreate");
Фактическая траектория помещается в cTarget. Её размер не зависит от числа сценариев: одно реализованное будущее сопоставляется со всем набором прогнозов.
ORION_FORECAST_INIT_STAGE(cHardLoss.BufferInit(iScenarios, 0) && cHardLoss.BufferCreate(OpenCL), "hard-loss buffer"); ORION_FORECAST_INIT_STAGE(cBatchControl.BufferInit(2, 0) && cBatchControl.BufferCreate(OpenCL), "batch-control buffer"); ORION_FORECAST_INIT_STAGE(cRecovery.BufferInit(iScenarios, 0) && cRecovery.BufferCreate(OpenCL), "recovery buffer"); ORION_FORECAST_INIT_STAGE(cForwardValidity.BufferInit(iScenarios, 0) && cForwardValidity.BufferCreate(OpenCL), "forward-validity buffer"); ORION_FORECAST_INIT_STAGE(cDiagnosticPartials.BufferInit(3 * iScenarios, 0) && cDiagnosticPartials.BufferCreate(OpenCL), "diagnostic-partials buffer"); ORION_FORECAST_INIT_STAGE(cEpochDiagnostics.BufferInit(11, 0) && cEpochDiagnostics.BufferCreate(OpenCL), "epoch-diagnostics buffer"); ORION_FORECAST_INIT_STAGE(cDiagnosticScratch.BufferInit((iScenarios * (iScenarios - 1) / 2 > iScenarios ? iScenarios * (iScenarios - 1) / 2 : iScenarios), 0) && cDiagnosticScratch.BufferCreate(OpenCL), "diagnostic-scratch buffer");
Остальные буферы обслуживают жёсткое назначение, проверку прямого прохода, восстановление неактивных прототипов и диагностику. Такой контроль нужен потому, что одной средней ошибки недостаточно. Сценарная модель может снизить её, схлопнув все варианты в один, поэтому дополнительно отслеживаются использование кодовой книги, расстояния между прототипами и число неактивных сценариев.
Диагностические показатели особенно важны именно для многосценарной модели. Обычная функция потерь может снижаться даже тогда, когда все прототипы постепенно сходятся к одной средней траектории. Поэтому дополнительно оцениваются энтропия использования, попарное расстояние между элементами кодовой книги и число неактивных сценариев. Эти метрики показывают, действительно ли модель использует доступное пространство будущих вариантов.
ORION_FORECAST_INIT_STAGE(cCodebook.Init(iScenarios, iVariables * iHorizon * iDimension, OpenCL), "codebook");
Затем создаётся кодовая книга. Размер одного прототипа охватывает все переменные, весь горизонт и латентные признаки. Поэтому прототип описывает не отдельную точку, а целую форму возможного продолжения.
ORION_FORECAST_INIT_STAGE(cGeneratorInput.Init(0, myIndex + 1, OpenCL, iScenarios * iVariables * iHorizon * 2 * iDimension, optimization, iBatch), "generator input"); ORION_FORECAST_INIT_STAGE(cRouterInput.Init(0, myIndex + 2, OpenCL, iScenarios * 2 * iVariables * iDimension, optimization, iBatch), "router input"); ORION_FORECAST_INIT_STAGE(cConfidenceInput.Init(0, myIndex + 3, OpenCL, iScenarios * iVariables * iHorizon * 2 * iDimension, optimization, iBatch), "confidence input");
Три слоя подготовки данных готовят условия для генератора, роутера и ветви неопределённости. Исходное состояние рынка общее, но задачи различаются: генератору нужна пошаговая адаптация прототипа, роутеру — компактное сравнение сценариев, а блоку неопределённости — оценка надёжности каждой точки траектории.
ORION_FORECAST_INIT_STAGE(cGenerator.Init(0, myIndex + 4, OpenCL, 2 * iDimension, 2 * iDimension, iDimension, iScenarios * iVariables * iHorizon, optimization, iBatch), "generator"); ORION_FORECAST_INIT_STAGE(cRouter.Init(0, myIndex + 5, OpenCL, 2 * iVariables * iDimension, 2 * iVariables * iDimension, 1, iScenarios, optimization, iBatch), "router"); ORION_FORECAST_INIT_STAGE(cConfidenceHead.Init(0, myIndex + 6, OpenCL, 2 * iDimension, 2 * iDimension, 1, iScenarios * iVariables * iHorizon, optimization, iBatch), "confidence head");
Вычислительные ветви реализованы свёрточными слоями. Здесь свёртка не связана с изображениями: общий набор весов применяется к однотипным элементам сценарного тензора. Это позволяет разделить знания между всеми вариантами, не создавая отдельную нейронную сеть для каждого прототипа.
Общий набор весов здесь играет роль дисциплинирующего ограничения. Все сценарии учатся на общей рыночной статистике и не превращаются в двадцать одну независимую модель с разным качеством. В то же время прототипы создают разные условия для одного и того же генератора. Это позволяет получить разнообразие без линейного роста числа обучаемых параметров.
ORION_FORECAST_INIT_STAGE(cRouterSoftmax.Init(0, myIndex + 7, OpenCL, iScenarios, optimization, iBatch), "router softmax");
cRouterSoftmax нормирует оценки роутера. На выходе получаем априорные вероятности сценариев, сумма которых равна единице.
ORION_FORECAST_INIT_STAGE(cResponsibilityScores.Init(0, myIndex + 8, OpenCL, iScenarios, optimization, iBatch) , "responsibility scores"); ORION_FORECAST_INIT_STAGE(cTopKSelector.Init(0, myIndex + 9, OpenCL, 1, iScenarios, iTopK, optimization, iBatch), "TopK selector");
cResponsibilityScores принимает оценки соответствия фактическому будущему, а cTopKSelector оставляет несколько лучших вариантов. Сценарии за пределами выбранного множества не получают слабый усредняющий градиент и сохраняют возможность специализироваться на других режимах.
SetActivationFunction(None); cGeneratorInput.SetActivationFunction(None); cRouterInput.SetActivationFunction(None); cConfidenceInput.SetActivationFunction(None); cGenerator.SetActivationFunction(None); cRouter.SetActivationFunction(None); cConfidenceHead.SetActivationFunction(SoftPlus); #undef ORION_FORECAST_INIT_STAGE return true; }
Большинство компонентов работают без дополнительной активации. Для ветви неопределённости используется SoftPlus, поскольку масштаб ошибки должен оставаться положительным, но не должен расти так резко, как при экспоненциальном преобразовании.
После инициализации слой имеет все необходимые буферы и обучаемые ветви. Следующий ключевой алгоритм — прямой проход, в котором текущее состояние разворачивается в пространство сценариев.
bool CNeuronScenarioForecast::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!OpenCL || !NeuronOCL || NeuronOCL.Neurons() != int(iVariables * iDimension) || !NeuronOCL.getOutput() || NeuronOCL.getOutputIndex() < 0) ReturnFalse;
Метод начинает с проверки указателя на OpenCL‑контекст, входного слоя и размеров данных. Ошибка обнаруживается до запуска кернела, где её было бы значительно сложнее локализовать.
uint global_work_offset[3] = {0, 0, 0}; uint global_work_size[3] = {iDimension, iVariables, iScenarios}; uint local_work_size[3] = {iDimension, iVariables, 1};
Вычисления распределяются по трём осям: латентным признакам, переменным и сценариям. Такая геометрия позволяет обрабатывать все варианты будущего параллельно.
const int kernel = def_k_ScenarioForecastForward; setBuffer(kernel, def_k_sff_input, NeuronOCL.getOutputIndex()) setBuffer(kernel, def_k_sff_prototypes, cCodebook.GetPrototypes().GetIndex()) setBuffer(kernel, def_k_sff_generator_input, cGeneratorInput.getOutputIndex()) setBuffer(kernel, def_k_sff_router_input, cRouterInput.getOutputIndex()) setBuffer(kernel, def_k_sff_confidence_input, cConfidenceInput.getOutputIndex()) setBuffer(kernel, def_k_sff_generator_output, cGenerator.getOutputIndex()) setBuffer(kernel, def_k_sff_router_output, cRouter.getOutputIndex()) setBuffer(kernel, def_k_sff_confidence_output, cConfidenceHead.getOutputIndex()) setBuffer(kernel, def_k_sff_z, Output.GetIndex()) setBuffer(kernel, def_k_sff_u, cConfidence.GetIndex()) setBuffer(kernel, def_k_sff_pi, cRouterSoftmax.getOutputIndex()) setArgument(kernel, def_k_sff_scenarios, (int)iScenarios) setArgument(kernel, def_k_sff_horizon, (int)iHorizon) setArgument(kernel, def_k_sff_dimension, (int)iDimension) setArgument(kernel, def_k_sff_phase, (int)0) setBuffer(kernel, def_k_sff_log_pi, cLogProbabilities.GetIndex()) setBuffer(kernel, def_k_sff_log_u, cLogConfidence.GetIndex()) setArgument(kernel, def_k_sff_variables, (int)iVariables) setBuffer(kernel, def_k_sff_forward_validity, cForwardValidity.GetIndex())
С кернелом связываются входное состояние, прототипы, промежуточные представления трёх ветвей и итоговые тензоры. Передаются также вероятности, логарифмы, масштабы неопределённости и буфер проверки корректности.
CBufferFloat *generator_weights = cGenerator.GetWeightsConv(); CBufferFloat *router_weights = cRouter.GetWeightsConv(); CBufferFloat *confidence_weights = cConfidenceHead.GetWeightsConv(); if(!generator_weights || !router_weights || !confidence_weights || generator_weights.GetIndex() < 0 || router_weights.GetIndex() < 0 || confidence_weights.GetIndex() < 0 || generator_weights.Total() <= 0 || router_weights.Total() <= 0 || confidence_weights.Total() <= 0) ReturnFalse; setBuffer(kernel, def_k_sff_generator_weights, generator_weights.GetIndex()) setBuffer(kernel, def_k_sff_router_weights, router_weights.GetIndex()) setBuffer(kernel, def_k_sff_confidence_weights, confidence_weights.GetIndex()) setArgument(kernel, def_k_sff_generator_weights_count, (int)generator_weights.Total()) setArgument(kernel, def_k_sff_router_weights_count, (int)router_weights.Total()) setArgument(kernel, def_k_sff_confidence_weights_count, (int)confidence_weights.Total()) kernelExecuteLoc(kernel, global_work_offset, global_work_size, local_work_size)
Отдельно проверяются веса генератора, роутера и блока неопределённости. На фазе 0 кернел объединяет текущее состояние с каждым прототипом и формирует специализированные входы для внутренних слоёв. Именно здесь одно наблюдаемое состояние раскрывается в несколько условных продолжений.
if(!cGenerator.FeedForward(cGeneratorInput.AsObject()) ||
!cRouter.FeedForward(cRouterInput.AsObject()) ||
!cRouterSoftmax.FeedForward(cRouter.AsObject()) ||
!cConfidenceHead.FeedForward(cConfidenceInput.AsObject()))
ReturnFalse;
Затем выполняются обучаемые ветви. Генератор строит траектории, роутер формирует оценки сценариев, Softmax превращает их в вероятности, а блок неопределённости задаёт положительный масштаб ожидаемой ошибки.
setArgument(kernel, def_k_sff_phase, (int)1) kernelExecuteLoc(kernel, global_work_offset, global_work_size, local_work_size) return true; }
На фазе 1 кернел собирает результаты в итоговое представление, рассчитывает логарифмические величины и проверяет значения на NaN и бесконечности. Все крупные тензоры остаются в памяти OpenCL‑устройства, поэтому между этапами не требуется перенос данных на CPU.
На выходе слой предоставляет три связанные характеристики: сценарные траектории Z, масштабы неопределённости U и априорные вероятности π. Вместе они описывают не только возможное направление, но и форму движения, относительную уместность вариантов и допустимый диапазон отклонений.
Эти величины нужно рассматривать совместно. Высокая априорная вероятность ещё не означает, что траектория узкая и надёжная. Низкая вероятность также не делает сценарий бесполезным: он может описывать редкое, но рискованное продолжение, которое важно учитывать при выборе стоп-уровня или объёма позиции. Поэтому Актору следует передавать не только лидирующий вариант, а всё сценарное пространство.
Например, два наиболее вероятных сценария могут предполагать рост, но различаться глубиной предварительной коррекции. Для направления сделки они выглядят одинаково. Для выбора момента входа и допустимого риска — совершенно по-разному. Именно такие различия теряются в одном усреднённом прогнозе и сохраняются в многосценарном представлении.
В приложенном коде работа слоя на этом не заканчивается. Реализованы: построение разреженных ответственностей, жёсткое назначение прототипа, EMA‑обновление кодовой книги и восстановление неактивных сценариев. Также добавлены нормализация градиентов, обратное распространение, диагностика, сохранение и загрузка состояния. В статье мы не разбираем каждый метод построчно: такой объём скрыл бы основную логику за техническими деталями. Это ограничение касается только объёма текста. Реализация уже содержит полный рабочий цикл слоя и может использоваться в составе модели.
Для практикующего трейдера результат можно представить как набор согласованных рыночных планов. Каждый вариант содержит последовательность будущих состояний, собственную вероятность и оценку допустимого отклонения. Это ещё не торговый приказ, но последующему Актору уже не придётся принимать решение по одной усреднённой линии.
Заключение
В этой статье мы продолжили адаптацию фреймворка ORION к финансовым рынкам и решили задачу организации устойчивого пространства сценариев. Объект CScenarioCodebook хранит прототипы возможных траекторий, накапливает EMA‑статистику, отслеживает их использование и поддерживает жизненный цикл неактивных вариантов. Тем самым сценарии получили собственную долговременную память вместо набора внешних параметров.
На основе кодовой книги построен слой CNeuronScenarioForecast. Он объединяет общий генератор, роутер и ветвь оценки неопределённости, проверяет вычислительный контракт и формирует сценарный прогноз на стороне OpenCL. Полный обучающий контур реализован в приложенных файлах, а в статье мы сосредоточились на его основных алгоритмах — структуре объектов, инициализации и прямом проходе.
В результате проблема, обозначенная во введении, получила архитектурное и программное решение: модель больше не обязана сжимать несколько правдоподобных продолжений в одну усреднённую траекторию. Она формирует пространство вариантов с априорными вероятностями и масштабами неопределённости. В следующей статье мы перейдём к передаче всего сценарного пространства торговому Актору.
Ссылки
- ORION: A Holistic End-to-End Autonomous Driving Framework by Vision-Language Instructed Action Generation
- Другие статьи серии
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 1 | Study.mq5 | Советник | Советник офлайн-обучения моделей |
| 2 | StudyOnline.mq5 | Советник | Советник онлайн-обучения моделей |
| 3 | StudyStateEncoder.mq5 | Советник | Советник офлайн-обучения прогнозной модели |
| 4 | Test.mq5 | Советник | Советник для тестирования модели |
| 5 | Trajectory.mqh | Библиотека класса | Структура описания состояния системы и архитектуры моделей |
| 6 | NeuroNet.mqh | Библиотека класса | Библиотека классов для создания нейронной сети |
| 7 | NeuroNet.cl | Библиотека | Библиотека кода OpenCL‑программы |
Проект представлен на forge.mql5.io/dng.
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Ассоциативные правила и теория вероятностей на Форекс: Фильтр сигналов без нейросетей и без магии
Создание пользовательских индикаторов в MQL5 (Часть 9): Футпринт-график с отслеживанием объёма по ценовым уровням
Создание пользовательских индикаторов в MQL5 (Часть 10): Дополнение футпринт-графика информационным блоком настроений по объёму по каждому бару
Теория графов: Применение алгоритма эвристического поиска A* в торговле
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования