//+------------------------------------------------------------------+ //| Study.mq5 | //| ORION direct-transition twin Actor-Critic study | //+------------------------------------------------------------------+ #property copyright "Copyright DNGĀ®" #property link "https://www.mql5.com/ru/users/dng" #property version "1.00" #define Study #include "Trajectory.mqh" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ input datetime Start = D'2024.01.01'; input datetime End = D'2026.01.01'; input int Iterations = 1000000; input int EpisodeBars = 2 * StackSize; input double MinBalance = 50.0; input int UpdatePolicy = 5; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ CORIONNet Actor; CORIONNet Q1; CORIONNet Q2; CBufferFloat State; CBufferFloat TimeState; CBufferFloat Account; CBufferFloat NextAccount; CBufferFloat CurrentAction; CBufferFloat TeacherAction; CBufferFloat RandomAction; CBufferFloat CriticInput; CBufferFloat ScalarTarget; CBufferFloat ActorObjective; ulong TeacherTransitions = 0; ulong TeacherCriticTransitions = 0; ulong RandomCriticTransitions = 0; double TeacherRewardSum = 0; ulong PolicyTransitions = 0; ulong PolicySkippedInvalid = 0; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool LoadOrCreateORIONPolicies(void) { return ORIONLoadOrCreatePolicySet(Actor, Q1, Q2, true); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool SaveORIONPolicies(void) { //--- Forecast artifacts are immutable throughout Actor-Critic study. This //--- bounded lifecycle check avoids hashing three large files every batch. return ORIONSavePolicySet(Actor, Q1, Q2); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool PrepareHistory(int &first_position, int &last_position) { int start = iBarShift(Symb.Name(), TimeFrame, Start); int end = iBarShift(Symb.Name(), TimeFrame, End); int bars = CopyRates(Symb.Name(), TimeFrame, 0, start, Rates); if(bars <= 0 || !RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars)) ReturnFalse; int wait = -1; bool calculated = false; do { calculated = (RSI.BarsCalculated() >= bars && CCI.BarsCalculated() >= bars && ATR.BarsCalculated() >= bars && MACD.BarsCalculated() >= bars); Sleep(100); wait++; } while(!calculated && wait < 100); if(!calculated) ReturnFalse; RSI.Refresh(); CCI.Refresh(); ATR.Refresh(); MACD.Refresh(); if(!ArraySetAsSeries(Rates, true)) ReturnFalse; first_position = end + 1; //--- CreateBuffers(position,...,forecast) forms its state from //--- position+NForecast and needs HistoryBars bars behind it. position is //--- therefore the end of the realized future window, not its first bar. last_position = start - HistoryBars - NForecast; return (last_position > first_position); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool PolicyBackward(void) { //--- PolicyBackward builds Q+1 in this buffer. ScalarTarget must remain the //--- immutable TD target consumed by both Critic backward passes. return PolicyBackward(Actor, Q1, GetPointer(ActorObjective), GetPointer(ORIONMarket), -1); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool TrainTransition(const int position, const int iteration, bool &terminal) { terminal = false; const bool trace_td = (iteration < 8 || iteration % 10000 == 0); if(!ORIONForwardForecast(position, GetPointer(State), GetPointer(TimeState))) { Print("TrainTransition stage=current_forecast"); ReturnFalse; } if(!Actor.feedForward(GetPointer(Account), 1, false, GetPointer(ORIONMarket), -1)) { Print("TrainTransition stage=actor_forward"); ReturnFalse; } if(!ReadAction(Actor, GetPointer(CurrentAction))) { Print("TrainTransition stage=read_action"); ReturnFalse; } const double balance = MathMax(0.0, double(Account[0]) * EtalonBalance); const double buy_lot = MathMax(0.0, double(CurrentAction[0] - CurrentAction[3])); const double sell_lot = MathMax(0.0, double(CurrentAction[3] - CurrentAction[0])); if(trace_td) PrintFormat("ORION action iteration=%d balance=%.2f buy_lot=%.8f buy_tp=%.8f buy_sl=%.8f sell_lot=%.8f sell_tp=%.8f sell_sl=%.8f", iteration, balance, buy_lot, double(CurrentAction[1]), double(CurrentAction[2]), sell_lot, double(CurrentAction[4]), double(CurrentAction[5])); double reward = 0; if(!AdvanceAccount(GetPointer(Account), GetPointer(CurrentAction), position, MinBalance, GetPointer(NextAccount), reward, terminal)) { Print("TrainTransition stage=advance_account"); ReturnFalse; } const double target_value = reward; if(trace_td) PrintFormat("ORION return iteration=%d reward=%.8f target=%.8f balance=%.2f equity=%.2f " "floating_buy=%.8f floating_sell=%.8f next_buy=%.8f next_sell=%.8f terminal=%s", iteration, reward, target_value, double(NextAccount[0])*EtalonBalance, double(NextAccount[2])*balance, double(NextAccount[6])*MathMax(double(NextAccount[0])*EtalonBalance, 1.0), double(NextAccount[7])*MathMax(double(NextAccount[0])*EtalonBalance, 1.0), double(NextAccount[4]), double(NextAccount[5]), (terminal ? "true" : "false")); if(!MathIsValidNumber(target_value) || !ScalarTarget.BufferInit(1, 0) || !ScalarTarget.Update(0, float(target_value))) { Print("TrainTransition stage=scalar_target"); ReturnFalse; } CNeuronBaseOCL *context_layer = Actor.Layer(0); CNeuronBaseOCL *actor_layer = Actor.Layer(3); if(!context_layer || !actor_layer) { Print("TrainTransition stage=actor_layer"); ReturnFalse; } //--- Use the live device activation, not the CPU account object. if(!BuildCriticInput(context_layer.getOutput(), actor_layer.getOutput(), GetPointer(CriticInput))) { Print("TrainTransition stage=critic_input"); ReturnFalse; } if(!Q1.feedForward(GetPointer(CriticInput), 1, false, GetPointer(ORIONMarket), -1)) { Print("TrainTransition stage=q1_forward"); ReturnFalse; } if(!Q2.feedForward(GetPointer(CriticInput), 1, false, GetPointer(ORIONMarket), -1)) { Print("TrainTransition stage=q2_forward"); ReturnFalse; } if(trace_td) { CNeuronBaseOCL *q1_layer = Q1.Layer(3); CNeuronBaseOCL *q2_layer = Q2.Layer(3); CBufferFloat *q1_output = (q1_layer ? q1_layer.getOutput() : NULL); CBufferFloat *q2_output = (q2_layer ? q2_layer.getOutput() : NULL); if(!q1_output || !q2_output || q1_output.GetIndex() < 0 || q2_output.GetIndex() < 0 || q1_output.Total() != 1 || q2_output.Total() != 1 || !q1_output.BufferRead() || !q2_output.BufferRead()) { Print("TrainTransition stage=current_q_read"); ReturnFalse; } PrintFormat("ORION Q iteration=%d q1=%.8f q2=%.8f td_q1=%.8f td_q2=%.8f", iteration, double(q1_output[0]), double(q2_output[0]), target_value - double(q1_output[0]), target_value - double(q2_output[0])); } //--- A critic gradient has no valid direction on the discontinuous no-trade //--- plateau. Keep learning Q there, but let only the realized-future teacher //--- move Actor back into the executable action manifold. const bool executable_action = (IsExecutableOrder(buy_lot, CurrentAction[1], CurrentAction[2]) || IsExecutableOrder(sell_lot, CurrentAction[4], CurrentAction[5])); if(iteration > 0 && UpdatePolicy > 0 && iteration % UpdatePolicy == 0) { if(executable_action) { if(!PolicyBackward()) { Print("TrainTransition stage=policy_backward"); ReturnFalse; } PolicyTransitions++; } else PolicySkippedInvalid++; } if(!Q1.backProp(GetPointer(ScalarTarget), GetPointer(ORIONMarket), -1) || !Q2.backProp(GetPointer(ScalarTarget), GetPointer(ORIONMarket), -1)) { Print("TrainTransition stage=critic_backward"); ReturnFalse; } //--- The teacher uses realized future bars, not one selected Forecast scenario. //--- It is deliberately a second Critic-only sample: Q1/Q2 are stateless, //--- whereas Actor has already performed its only forward for this state. double teacher_reward = 0; if(!BuildTeacherAction(position, GetPointer(Account), GetPointer(TeacherAction), teacher_reward)) { Print("TrainTransition stage=teacher_action"); ReturnFalse; } if(teacher_reward > 0) { if(trace_td) PrintFormat("ORION teacher iteration=%d reward=%.8f buy_lot=%.8f sell_lot=%.8f", iteration, teacher_reward, MathMax(0.0, double(TeacherAction[0] - TeacherAction[3])), MathMax(0.0, double(TeacherAction[3] - TeacherAction[0]))); //--- Supervised Actor update reuses the current activation; it never advances //--- the history stack or invokes a second Actor forward. if(!Actor.backProp(GetPointer(TeacherAction), GetPointer(ORIONMarket), -1)) { Print("TrainTransition stage=teacher_actor_backward"); ReturnFalse; } TeacherTransitions++; TeacherRewardSum += teacher_reward; } //--- Critic receives every oracle action, including a losing one. Only Actor //--- is restricted to profitable oracle supervision above. if(!ScalarTarget.BufferInit(1, 0) || !ScalarTarget.Update(0, float(teacher_reward)) || !BuildCriticInput(context_layer.getOutput(), GetPointer(TeacherAction), GetPointer(CriticInput))) { Print("TrainTransition stage=teacher_critic_input"); ReturnFalse; } if(!Q1.feedForward(GetPointer(CriticInput), 1, false, GetPointer(ORIONMarket), -1) || !Q2.feedForward(GetPointer(CriticInput), 1, false, GetPointer(ORIONMarket), -1)) { Print("TrainTransition stage=teacher_critic_forward"); ReturnFalse; } if(!Q1.backProp(GetPointer(ScalarTarget), GetPointer(ORIONMarket), -1) || !Q2.backProp(GetPointer(ScalarTarget), GetPointer(ORIONMarket), -1)) { Print("TrainTransition stage=teacher_critic_backward"); ReturnFalse; } TeacherCriticTransitions++; //--- A second critic-only sample is uniformly random over executable lots and //--- stop distances. It never runs Actor backward or advances policy history. double random_reward = 0; if(!BuildRandomAction(position, GetPointer(Account), GetPointer(RandomAction), random_reward)) { Print("TrainTransition stage=random_action"); ReturnFalse; } if(random_reward > 0) { if(!Actor.backProp(GetPointer(RandomAction), GetPointer(ORIONMarket), -1)) { Print("TrainTransition stage=random_actor_backward"); ReturnFalse; } } if(!ScalarTarget.BufferInit(1, 0) || !ScalarTarget.Update(0, float(random_reward)) || !BuildCriticInput(context_layer.getOutput(), GetPointer(RandomAction), GetPointer(CriticInput))) { Print("TrainTransition stage=random_critic_input"); ReturnFalse; } if(!Q1.feedForward(GetPointer(CriticInput), 1, false, GetPointer(ORIONMarket), -1) || !Q2.feedForward(GetPointer(CriticInput), 1, false, GetPointer(ORIONMarket), -1)) { Print("TrainTransition stage=random_critic_forward"); ReturnFalse; } if(!Q1.backProp(GetPointer(ScalarTarget), GetPointer(ORIONMarket), -1) || !Q2.backProp(GetPointer(ScalarTarget), GetPointer(ORIONMarket), -1)) { Print("TrainTransition stage=random_critic_backward"); ReturnFalse; } RandomCriticTransitions++; //--- Only the three declared Forecast trainable buffers are read here. The //--- complete persistent Codebook is checked at checkpoint/lifecycle bounds. if(!ORIONVerifyFrozenWeightsExact()) { Print("TrainTransition stage=frozen_forecast_check"); ReturnFalse; } return true; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void TrainORIONActorCritic(void) { int first = 0, last = 0; if(!PrepareHistory(first, last)) { PrintFormat("%s -> %d history unavailable", __FUNCTION__, __LINE__); return; } uint shown = GetTickCount(); int completed_iterations = 0; int failed_iteration = -1; int failed_position = -1; int position = last; int episode = 0; TeacherTransitions = 0; TeacherCriticTransitions = 0; RandomCriticTransitions = 0; TeacherRewardSum = 0; PolicyTransitions = 0; PolicySkippedInvalid = 0; for(int iteration = 0; iteration < Iterations && !IsStopped(); iteration++) { if(episode == 0) { if(!ORIONForwardForecast(position, GetPointer(State), GetPointer(TimeState))) break; const vector sampled = SampleAccount(GetPointer(State), Rates[position].time, EtalonBalance, MinBalance); if(sampled.Size() != AccountDescr || !Account.AssignArray(sampled) || (Account.GetIndex() >= 0 && !Account.BufferWrite()) || !Actor.Clear()) break; } bool terminal = false; if(!TrainTransition(position, iteration, terminal)) { failed_iteration = iteration; failed_position = position; break; } completed_iterations++; if((NextAccount.GetIndex() >= 0 && !NextAccount.BufferRead()) || !Account.AssignArray(GetPointer(NextAccount)) || (Account.GetIndex() >= 0 && !Account.BufferWrite())) { failed_iteration = iteration; failed_position = position; break; } position--; episode++; if(position < first || terminal || episode >= MathMax(1, EpisodeBars)) { if(position < first) position = last; episode = 0; } if(GetTickCount() - shown > 500) { const double q1_rmse = MathSqrt(MathMax(0.0, double(Q1.getRecentAverageError()))) / MathMax(EtalonBalance, 1.0); const double q2_rmse = MathSqrt(MathMax(0.0, double(Q2.getRecentAverageError()))) / MathMax(EtalonBalance, 1.0); const double teacher_mean = (TeacherTransitions > 0 ? TeacherRewardSum / double(TeacherTransitions) : 0.0); Comment(StringFormat("ORION AC %6.2f%% Q1 rRMSE %.8f Q2 rRMSE %.8f teacherA %I64u teacherQ %I64u randomQ %I64u R %.2f policy %I64u skip %I64u", 100.0 * iteration / MathMax(Iterations, 1), q1_rmse, q2_rmse, TeacherTransitions, TeacherCriticTransitions, RandomCriticTransitions, teacher_mean, PolicyTransitions, PolicySkippedInvalid)); shown = GetTickCount(); } } Comment(""); if(completed_iterations != Iterations) { if(failed_iteration >= 0) PrintFormat("%s -> %d publication aborted: TrainTransition failed iteration=%d position=%d completed=%d/%d", __FUNCTION__, __LINE__, failed_iteration, failed_position, completed_iterations, Iterations); else PrintFormat("%s -> %d publication aborted: stop requested completed=%d/%d", __FUNCTION__, __LINE__, completed_iterations, Iterations); ExpertRemove(); return; } if(!SaveORIONPolicies()) PrintFormat("%s -> %d save failed", __FUNCTION__, __LINE__); ExpertRemove(); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ int OnInit() { ResetLastError(); if(!ORIONInitIndicators() || !ORIONLoadForecastInference() || !LoadOrCreateORIONPolicies() || !ORIONVerifyFrozenForecastExact()) { PrintFormat("ORION Actor-Critic initialization failed at line %d error=%d", __LINE__, GetLastError()); return INIT_FAILED; } return (EventChartCustom(ChartID(), 1, 0, 0, "Init") ? INIT_SUCCEEDED : INIT_FAILED); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Only a completed explicit training loop persists all six artifacts and //--- writes the hash manifest last. Deinit must not create a mixed generation. if(ORIONFrozenBaselineReady && ORIONForecast != NULL && !ORIONVerifyFrozenForecastExact()) PrintFormat("%s -> %d forecast mutation", __FUNCTION__, __LINE__); ORIONForecast = NULL; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == 1001) TrainORIONActorCritic(); } //+------------------------------------------------------------------+