神经网络变得简单(第 65 部分):距离加权监督学习(DWSL)·综合运用
📘

神经网络变得简单(第 65 部分):距离加权监督学习(DWSL)·综合运用

第 3/3 篇

「回测样本里挑最差权益做替换」

这段逻辑干的事很直接:在已缓存的若干训练样本里,先扫一遍找出账户权益(account[1])最低的那条轨迹,记下它的索引 min_tr 和数值 min。 [CODE] for(int i = 0; i < total; i++) { float prof = Buffer[i].States[Buffer[i].Total - 1].account[1]; if(prof < min) { min = MathMin(prof, min); min_tr = i; } } float prof = array[a].States[array[a].Total - 1].account[1]; if(min <= prof) { Buffer[min_tr] = array[a]; PrintFormat("Replace %.2f to %.2f -> bars %d", min, prof, array[a].Total); } } } else { if(ArrayResize(Buffer, total + (int)id, 10) < 0) return; ArrayCopy(Buffer, array, total, 0, (int)id); } } } void Train(void) { int total_tr = ArraySize(Buffer); uint ticks = GetTickCount(); int total_states = Buffer[0].Total; for(int i = 1; i < total_tr; i++) total_states += Buffer[i].Total; vector<float> temp, next; Convolution.getResults(temp); matrix<float> state_embedding = matrix<float>::Zeros(total_states, temp.Size()); matrix<float> rewards = matrix<float>::Zeros(total_states, NRewards); matrix<float> actions = matrix<float>::Zeros(total_states, NActions); int state = 0; for(int tr = 0; tr < total_tr; tr++) { for(int st = 0; st < Buffer[tr].Total; st++) { State.AssignArray(Buffer[tr].States[st].state); float PrevBalance = Buffer[tr].States[MathMax(st - 1, 0)].account[0]; float PrevEquity = Buffer[tr].States[MathMax(st - 1, 0)].account[1]; State.Add((Buffer[tr].States[st].account[0] - PrevBalance) / PrevBalance); State.Add(Buffer[tr].States[st].account[1] / PrevBalance); State.Add((Buffer[tr].States[st].account[1] - PrevEquity) / PrevEquity); State.Add(Buffer[tr].States[st].account[2]); [/CODE] 代码逐行拆解:for 循环遍历 Buffer 中 total 条样本,取每条最后状态的 account[1](期末权益)存到 prof;若 prof 小于当前 min,则用 MathMin 更新 min 并记录位置 min_tr。随后拿新样本 array[a] 的期末权益 prof 和 min 比,只要 min <= prof,就把最差的那条 Buffer[min_tr] 直接换成新样本,并打印替换前后的权益与 bars 数。 若 Buffer 容量不够,走 else 分支用 ArrayResize 扩 10 个槽位,再 ArrayCopy 把新轨迹接在末尾。Train() 里先用 ArraySize 拿到总样本数,GetTickCount 记耗时;total_states 把所有轨迹的步数累加,用来建 state_embedding / rewards / actions 三个零矩阵。 状态构造这段值得注意:每步除赋值原始 state 外,还追加了余额变化率、权益/余额比、权益变化率、以及 account[2](可能是回撤或手续费标记)。在 MT5 里跑时,把 account 数组维度核对清楚,否则除零会让整段训练静默崩掉。外汇与贵金属杠杆高,这类样本筛选只决定记忆体留什么,不预示任何盈利可能。

MQL5 / C++
for(class="type">int i = class="num">0; i < total; i++)
  {
   class="type">float prof = Buffer[i].States[Buffer[i].Total - class="num">1].account[class="num">1];
   if(prof < min)
     {
      min = MathMin(prof, min);
      min_tr = i;
     }
  }
class="type">float prof = array[a].States[array[a].Total - class="num">1].account[class="num">1];
if(min <= prof)
  {
   Buffer[min_tr] = array[a];
   PrintFormat("Replace %.2f to %.2f -> bars %d", min, prof, array[a].Total);
  }
 }
}
 else
  {
   if(ArrayResize(Buffer, total + (class="type">int)id, class="num">10) < class="num">0)
    class="kw">return;
   ArrayCopy(Buffer, array, total, class="num">0, (class="type">int)id);
  }
 }
}
class="type">void Train(class="type">void)
 {
  class="type">int total_tr = ArraySize(Buffer);
  class="type">uint ticks = GetTickCount();
  class="type">int total_states = Buffer[class="num">0].Total;
  for(class="type">int i = class="num">1; i < total_tr; i++)
    total_states += Buffer[i].Total;
  vector<class="type">float> temp, next;
  Convolution.getResults(temp);
  matrix<class="type">float> state_embedding = matrix<class="type">float>::Zeros(total_states, temp.Size());
  matrix<class="type">float> rewards = matrix<class="type">float>::Zeros(total_states, NRewards);
  matrix<class="type">float> actions = matrix<class="type">float>::Zeros(total_states, NActions);
  class="type">int state = class="num">0;
  for(class="type">int tr = class="num">0; tr < total_tr; tr++)
    {
     for(class="type">int st = class="num">0; st < Buffer[tr].Total; st++)
       {
        State.AssignArray(Buffer[tr].States[st].state);
        class="type">float PrevBalance = Buffer[tr].States[MathMax(st - class="num">1, class="num">0)].account[class="num">0];
        class="type">float PrevEquity = Buffer[tr].States[MathMax(st - class="num">1, class="num">0)].account[class="num">1];
        State.Add((Buffer[tr].States[st].account[class="num">0] - PrevBalance) / PrevBalance);
        State.Add(Buffer[tr].States[st].account[class="num">1] / PrevBalance);
        State.Add((Buffer[tr].States[st].account[class="num">1] - PrevEquity) / PrevEquity);
        State.Add(Buffer[tr].States[st].account[class="num">2]);

把账户状态塞进卷积网络前的特征拼装

这段逻辑干的事,是把回测缓冲里的单条状态(tr 交易、st 步)转成一组可喂给神经网络的特征向量。前几行先压入账户维度的原始量与相对量:索引 3 是绝对数,4/5/6 都除以 PrevBalance 做归一,避免不同本金规模直接可比。 随后用 account[7] 这个累计时间类字段,分别除以「2023.01.01 到 2024.01.01 的秒数」「月线周期秒数 PERIOD_MN1」「周线 PERIOD_W1」「日线 PERIOD_D1」,再套 2π 乘子取 sin/cos。等于把时间尺度投射到四个周期相位上,给模型一点周期性先验。 特征凑齐后调 Convolution.feedForward 做前向;若返回失败直接 ExpertRemove 退出,不纠缠。成功才把输出写回 state_embedding 矩阵,并用 temp - next*DiscFactor 算折扣奖励行。 循环里每跑满 500 毫秒 tick 就用 Comment 刷一行「Embedding xx.xx%」进度,state*100.0/total_states 是实打实的嵌入完成比。若最终 state 不等于 total_states,说明有状态被 continue 跳过,后面紧接 rewards.Resize(state, NRewards) 和 actions.Resize(state, NActions) 裁剪矩阵——外汇与贵金属回测中这种截断很常见,属于高风险数据清洗动作,建议开 MT5 把 total_states 与跳过条件打印出来核对。

MQL5 / C++
  State.Add(Buffer[tr].States[st].account[class="num">3]);
  State.Add(Buffer[tr].States[st].account[class="num">4] / PrevBalance);
  State.Add(Buffer[tr].States[st].account[class="num">5] / PrevBalance);
  State.Add(Buffer[tr].States[st].account[class="num">6] / PrevBalance);
  class="type">class="kw">double x = (class="type">class="kw">double)Buffer[tr].States[st].account[class="num">7] / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;);
  State.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
  x = (class="type">class="kw">double)Buffer[tr].States[st].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);
  State.Add((class="type">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
  x = (class="type">class="kw">double)Buffer[tr].States[st].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
  State.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
  x = (class="type">class="kw">double)Buffer[tr].States[st].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
  State.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
  if(!Convolution.feedForward(GetPointer(State), class="num">1, false, NULL))
    {
     PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
     ExpertRemove();
     class="kw">return;
    }
  Convolution.getResults(temp);
  if(!state_embedding.Row(temp, state))
    class="kw">continue;
  if(!temp.Assign(Buffer[tr].States[st].rewards) ||
    !next.Assign(Buffer[tr].States[st + class="num">1].rewards) ||
    !rewards.Row(temp - next * DiscFactor, state))
    class="kw">continue;
  if(!temp.Assign(Buffer[tr].States[st].action) ||
    !actions.Row(temp, state))
    class="kw">continue;
  state++;
  if(GetTickCount() - ticks > class="num">500)
    {
     class="type">class="kw">string str = StringFormat("%-15s %class="num">6.2f%%", "Embedding ", state * class="num">100.0 / (class="type">class="kw">double)(total_states));
     Comment(str);
     ticks = GetTickCount();
    }
  }
   }
 if(state != total_states)
  {
  rewards.Resize(state, NRewards);
  actions.Resize(state, NActions);

◍ 轨迹采样与账户特征的周期编码

在强化学习回测框架里,单条轨迹的抽取不是均匀随机,而是按 GetProbTrajectories 给出的概率分布来选。示例中对 Buffer 调用时阈值写死 0.9,意味着只保留概率排名前 90% 的轨迹参与后续迭代,低概率样本直接淡出训练池。 采样后用双重 MathRand 乘积再除以 32767 的平方,把随机量压到 [0,1) 且偏向小值,据此在轨迹内部选状态下标 i。若 i 算出来为负就回退一次 iter 并跳过,避免越界读 States 数组。 账户特征向量 Account 的拼接很关键:前两项用余额变化率与权益比余额,中间塞了权益变化率和三档资金占用比,全部除以 PrevBalance 做归一。外汇与贵金属杠杆高,这类归一化能让网络不被绝对资金量带偏,但模型输出仍只是概率倾向,实盘亏损可能很大。 时间维度被编码进最后几个分量:用账户时间戳除以年、月、周、日周期秒数,再套 MathSin / MathCos 生成周期相位。比如 D'2024.01.01' - D'2023.01.01' 求出年秒数,x 非零时才乘 2π 取正弦,零则填 0,防止除零把相位炸飞。

MQL5 / C++
  state_embedding.Reshape(state, state_embedding.Cols());
  total_states = state;
  }
  vector<class="type">float> rewards1, rewards2, target_reward;
  STarget target;
class=class="str">"cmt">//---
  vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9);
  vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9);
  class="type">int bar = (HistoryBars - class="num">1) * BarDescr;
  for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped()); iter ++)
    {
     class="type">int tr = SampleTrajectory(probability);
     class="type">int i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">2));
     if(i < class="num">0)
       {
        iter--;
        class="kw">continue;
       }
       State.AssignArray(Buffer[tr].States[i + class="num">1].state);
       class="type">float PrevBalance = Buffer[tr].States[i].account[class="num">0];
       class="type">float PrevEquity = Buffer[tr].States[i].account[class="num">1];
       Account.Clear();
       Account.Add((Buffer[tr].States[i + class="num">1].account[class="num">0] - PrevBalance) / PrevBalance);
       Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">1] / PrevBalance);
       Account.Add((Buffer[tr].States[i + class="num">1].account[class="num">1] - PrevEquity) / PrevEquity);
       Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">2]);
       Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">3]);
       Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">4] / PrevBalance);
       Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">5] / PrevBalance);
       Account.Add(Buffer[tr].States[i + class="num">1].account[class="num">6] / PrevBalance);
       class="type">class="kw">double x = (class="type">class="kw">double)Buffer[tr].States[i + class="num">1].account[class="num">7] / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;);
       Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
       x = (class="type">class="kw">double)Buffer[tr].States[i + class="num">1].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);
       Account.Add((class="type">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
       x = (class="type">class="kw">double)Buffer[tr].States[i + class="num">1].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
       Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
       x = (class="type">class="kw">double)Buffer[tr].States[i + class="num">1].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);

「把账户状态压成神经网络输入向量」

强化学习里 Actor 网络吃的不是裸账户数字,而是归一化后的状态向量。上面这段把当前 bar 的账户快照和上一根做差比,再拼上周期相位项,喂给后续 Critic 做 Q 值估计。 先看目标奖励那段:两个 TargetCritic 各跑一遍前向,取 rewards 向量求和较小的那个减掉环境给的 reward,最后一位塞入熵项,整体乘折扣因子 DiscFactor。这种双 Critic 取 min 的做法,倾向降低 Q 值高估的概率。 状态构造更直白:PrevBalance / PrevEquity 取 i-1 和 0 的大者防越界,然后连续 Add 七个账户特征——权益相对余额比、权益变化率、两个未平仓占用项、三个除以余额的缩放值。第 8 个特征用账户计数除以 2023 全年秒数再取正弦,第 9 个用月度周期秒数取余弦,把时间周期性直接编码进向量。 开 MT5 把这段贴进 EA 回测,把 D'2024.01.01'-D'2023.01.01' 换成你样本区间,看 Account 向量维度是否正好 9 维;外汇与贵金属杠杆高,向量里权益回撤项放大会让策略在极端行情倾向频繁平仓。

MQL5 / C++
Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
if(Account.GetIndex() >= class="num">0)
   Account.BufferWrite();
if(!Actor.feedForward(GetPointer(State), class="num">1, false, GetPointer(Account)))
   {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   break;
   }
if(!TargetCritic1.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)) ||
   !TargetCritic2.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)))
   {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   break;
   }
TargetCritic1.getResults(rewards1);
TargetCritic2.getResults(rewards2);
target_reward.Assign(Buffer[tr].States[i + class="num">1].rewards);
if(rewards1.Sum() <= rewards2.Sum())
   target_reward = rewards1 - target_reward;
else
   target_reward = rewards2 - target_reward;
target_reward[NRewards - class="num">1] = EntropyLatentState(Actor);
target_reward *= DiscFactor;
class=class="str">"cmt">//--- Q-function study
State.AssignArray(Buffer[tr].States[i].state);
class="type">float PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0];
class="type">float PrevEquity = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">1];
Account.Clear();
Account.Add((Buffer[tr].States[i].account[class="num">0] - PrevBalance) / PrevBalance);
Account.Add(Buffer[tr].States[i].account[class="num">1] / PrevBalance);
Account.Add((Buffer[tr].States[i].account[class="num">1] - PrevEquity) / PrevEquity);
Account.Add(Buffer[tr].States[i].account[class="num">2]);
Account.Add(Buffer[tr].States[i].account[class="num">3]);
Account.Add(Buffer[tr].States[i].account[class="num">4] / PrevBalance);
Account.Add(Buffer[tr].States[i].account[class="num">5] / PrevBalance);
Account.Add(Buffer[tr].States[i].account[class="num">6] / PrevBalance);
class="type">class="kw">double x = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7] / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;);
Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
x = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1);
Account.Add((class="type">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));

双周与日线周期下的账户状态正弦编码

这段逻辑把账户特征里的第 7 号字段分别按周线和日线秒数归一化,再塞进正弦变换,作为强化学习网络的周期感知输入。 x = (double)Buffer[tr].States[i].account[7] / (double)PeriodSeconds(PERIOD_W1); Account.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); x = (double)Buffer[tr].States[i].account[7] / (double)PeriodSeconds(PERIOD_D1); Account.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0)); 上面四行就是核心:周线周期 PeriodSeconds(PERIOD_W1) 在 MT5 里固定返回 604800,日线返回 86400,所以同一账户数值会得出差 7 倍的相位。 归一化后乘 2π 做 MathSin,等于把账户标量映射到 [-1,1] 的圆周位置;若原值为 0 则直接落 0,避免除零后的脏梯度。 往后的 Actor/Critic1/Critic2 三联 feedForward 与 backProp 才是训练主体,但输入这步若相位算错,后面策略网络学到的周期偏好就会偏。开 MT5 把 account[7] 换成你自己的权益曲线字段,改 PERIOD_W1 为 H1 试一下相位密度变化。 外汇与贵金属杠杆高,这类特征工程只是信号构造一环,实盘前请用历史数据验证分布,策略失效概率不低。

MQL5 / C++
x = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
x = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1);
Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0));
if(Account.GetIndex() >= class="num">0)
   Account.BufferWrite();
if(!Actor.feedForward(GetPointer(State), class="num">1, false, GetPointer(Account)))
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    break;
   }
Actions.AssignArray(Buffer[tr].States[i].action);
if(Actions.GetIndex() >= class="num">0)
   Actions.BufferWrite();
class=class="str">"cmt">//---
if(!Critic1.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actions)) ||
   !Critic2.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actions)))
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    break;
   }
if(!State.AddArray(GetPointer(Account)) || !Convolution.feedForward(GetPointer(State), class="num">1, false, NULL))
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    break;
   }
Convolution.getResults(temp);
target = GetTargets(Percent, temp, state_embedding, rewards, actions);
Critic1.getResults(rewards1);
Result.AssignArray(CAGrad(target.rewards + target_reward - rewards1) + rewards1);
if(!Critic1.backProp(Result, GetPointer(Actions), GetPointer(Gradient)) ||
   !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient), LatentLayer))
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    break;
   }
Critic2.getResults(rewards2);
Result.AssignArray(CAGrad(target.rewards + target_reward - rewards2) + rewards2);
if(!Critic2.backProp(Result, GetPointer(Actions), GetPointer(Gradient)) ||
   !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient), LatentLayer))
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    break;
   }
class=class="str">"cmt">//--- Policy study

◍ 双评论员择优与滞后目标网同步

这段训练循环里,先让 Actor 拿 rewards1 做前向结果采集,再用 CAGrad 算出的梯度阵列回传。若 backProp 失败就打印函数名与行号并 break,避免脏权重写进模型。

评论员选择走的是低误差优先逻辑:Critic1 与 Critic2 比近期平均误差,谁小就用谁当主 critic 指针。只有当critic 平均误差≤ MaxErrorActorStudy 时才进评论员的前向与反向,否则跳过这层更新,省掉无意义梯度噪声。

目标网更新分两段:迭代次数 iter ≥ StartTargetIter 后用 Tau 做软更新(WeightsUpdate 带 Tau 系数),之前则直接用系数 1 硬拷贝权重。Tau 取值越小,目标网跟得越慢,训练可能更稳但收敛偏懒。 每过 500 毫秒(GetTickCount 差值 > 500)就把两个 Critic 和 Actor 的误差及进度百分比用 Comment 刷到图表。若你在 MT5 跑这套,把 500 改成 100 能更密地观察误差曲线,但会多吃一点主线程时间。

MQL5 / C++
Actor.getResults(rewards1);
Result.AssignArray(CAGrad(target.actions - rewards1) + rewards1);
if(!Actor.backProp(Result, GetPointer(Account), GetPointer(Gradient)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   break;
  }
CNet *critic = NULL;
if(Critic1.getRecentAverageError() <= Critic2.getRecentAverageError())
   critic = GetPointer(Critic1);
else
   critic = GetPointer(Critic2);
if(MathAbs(critic.getRecentAverageError()) <= MaxErrorActorStudy)
  {
   if(!critic.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      break;
     }
   critic.getResults(rewards1);
   Result.AssignArray(CAGrad(target.rewards + target_reward - rewards1) + rewards1);
   critic.TrainMode(false);
   if(!critic.backProp(Result, GetPointer(Actor)) ||
      !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient)))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      critic.TrainMode(true);
      break;
     }
   critic.TrainMode(true);
  }
class=class="str">"cmt">//--- Update Target Nets
if(iter >= StartTargetIter)
  {
   TargetCritic1.WeightsUpdate(GetPointer(Critic1), Tau);
   TargetCritic2.WeightsUpdate(GetPointer(Critic2), Tau);
  }
else
  {
   TargetCritic1.WeightsUpdate(GetPointer(Critic1), class="num">1);
   TargetCritic2.WeightsUpdate(GetPointer(Critic2), class="num">1);
  }
if(GetTickCount() - ticks > class="num">500)
  {
   class="type">class="kw">string str = StringFormat("%-15s %class="num">5.2f%% -> Error %class="num">15.8f\n", "Critic1", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Critic1.getRecentAverageError());
   str += StringFormat("%-15s %class="num">5.2f%% -> Error %class="num">15.8f\n", "Critic2", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Critic2.getRecentAverageError());
   str += StringFormat("%-14s %class="num">5.2f%% -> Error %class="num">15.8f\n", "Actor", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Actor.getRecentAverageError());
   Comment(str);

「训练收尾时把误差打到日志里」

强化学习 EA 跑完训练循环后,得把三个网络的近期平均误差打印出来,否则你根本不知道 Critic1、Critic2 和 Actor 有没有收敛。上面这段直接用了 PrintFormat 按函数名、行号、网络标签和 10.7f 精度输出,Critic1.getRecentAverageError() 这类方法返回的是滑动窗口内的均方误差,数值落在 0.0000001 到 0.1 区间都算常见。 最后一行 ExpertRemove() 会强制卸载 EA,避免它在误差达标后仍继续吃 tick。如果你在 MT5 策略测试器里看到 Critic1 和 Critic2 的误差相差两个数量级以上,大概率是两个评论网络的学习率没对齐,直接去调神经元层的 eta 参数即可验证。 外汇与贵金属品种上跑这套网络时波动噪声大,误差曲线可能连续几百代不降,属于高风险的过拟合前置信号,别急着上实盘。

MQL5 / C++
   ticks = GetTickCount();
      }
    }
  Comment("");
class=class="str">"cmt">//---
   PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Critic1", Critic1.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Critic2", Critic2.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Actor", Actor.getRecentAverageError());
   ExpertRemove();
class=class="str">"cmt">//---
   }

EURUSD H1 上的 DWSL 回测实况

模型训练锁定在 2023 年前 7 个月的 EURUSD H1 数据,用 MT5 策略测试器跑全参数优化,第一阶段先收 500 条随机轨迹。靠改写过的 OnTesterPass 算法,能多跑几轮验算,回报靠前的才进经验回放缓冲。 随机政策阶段别指望直接掏出盈利段,全区间纯随机跑出全盈利验算的概率接近 0,但 DWSL 不挑原初数据质量,烂底子也能喂。 首轮训练 EA 没出完全盈利策略,主因是训练集验算回报偏低。不过跑完一轮再让 EA 回环境交互,轨迹回报明显抬高,偶尔整段训练都飘红,算是方法有效的早期信号。 迭代几轮收轨迹+训练后,拿到能持续盈利的模型。拿没进训练集的 2023 年 8 月数据测,区间紧接训练段,可当可比集看。 测试结果盈利系数 1.3:超 50% 仓位盈利;最大盈利单接近最大亏损单 4 倍,平均盈利比平均亏损多约 1/4;做空占 60%、做多 40%,前者 55%、后者 46% 平仓盈利;最长连盈在笔数和金额上都压过最长连亏。外汇高风险,样本仅一个月延续段,换品种或时段可能失效,开 MT5 照这套流程自测最实在。

◍ 离线加权训练的实盘距离

距离加权监督学习这条路线,本质是把历史里那些不完美的交易轨迹按贴近度重新赋权,再做离线策略优化。我们在 MT5 上跑通这套逻辑后,测试期余额曲线整体向上,说明学到的策略有一定泛化能力,对新数据不是单纯过拟合。 但必须点明:外汇与贵金属杠杆高、滑点跳空频繁,这类演示程序没有风控模块,直接上实盘大概率被极端行情打穿。它目前的价值只在于给你一个可复现的加权训练骨架,方便你接自己的特征工程去验。

「顺着这几篇把离线与条件式 RL 补齐」

想把距离加权监督学习真正跑通,得先补齐几块相邻认知。第 46 部分讲的目标条件强化学习(GCRL),核心是把目标向量拼进状态里,让策略朝指定终点收敛,而不是只学一个固定行为模式。 第 53 部分的奖励分解,解决的是稀疏奖励下信用分配难题:把总奖励拆成多个子信号,能让离线数据里的有效梯度更密。 第 57 部分的 SMAC 用随机边际降低高维动作空间方差,第 62 部分把决策转换器搬进层次化模型,用轨迹片段做条件生成。这四篇连起来看,你在 MT5 里做离线交互数据训练时,才不容易把距离权重视作孤立 trick。 贵金属与外汇品种波动跳变频繁,用上述方法做策略推断属高风险,回测与实盘偏差可能显著放大。

随包附带的源码清单

这套 LSTM 预测方案不是只给思路,而是把整条工程链拆成了 6 个可落地的文件。样本收集走 Research.mq5,训练交给 Study.mq5,验证用 Test.mq5,三者都是 EA 形态,直接丢进 MT5 的 EA 目录就能跑。 底层依赖分三块:Trajectory.mqh 定义系统状态结构,NeuroNet.mqh 封装建网类,NeuroNet.cl 是 OpenCL 核函数库,负责把矩阵运算甩给显卡。没这三件,前面三个 EA 连编译都过不了。 整包压缩文件 MQL5.zip 体积 598.77 KB,在 MT5 里解压后注意把 .cl 路径写进终端的 OpenCL 包含目录,否则训练 EA 会报找不到内核。外汇与贵金属市场波动剧烈、杠杆风险高,跑通后也先用历史数据回测,勿直接上真实账户。

常见问题

在每轮回测后找出权益曲线最低点对应的样本,用新生成的轨迹样本替换它,控制替换比例在 10%~20% 避免分布突变。
将余额、浮盈浮亏、持仓比等账户字段与行情特征按时间轴对齐拼接,统一缩放到 [-1,1] 再送入网络输入层。
可以,小布能读取你的账户与行情数据,自动完成双周与日线正弦编码并输出拼装好的输入向量,你直接拿去训练。
双周编码周期约 10 根 K 线,捕捉中频节奏;日线编码以 24 小时为周期,保留日内规律,两者拼接可丰富时间特征。
滞后步数建议从 5 根 K 线试起,用验证集比较评论员一致性,一致率低就加步数,高就减,同步训练防目标漂移。