神经网络变得轻松(第二十九部分):优势扮演者-评价者算法·进阶篇
「训练循环里的取样与状态拼装」
这段 Train 函数负责把历史行情喂给模型做离线训练。它先用 TimeCurrent 拿到当前时间,按 StudyPeriod 回退年份拉取 CopyRates 数据;若年份减到 0 以下则兜底成 1900,实盘里基本碰不到,但回测跨世纪数据时会救你一命。 缓冲区resize和ArraySetAsSeries失败就直接ExpertRemove退出,说明这套逻辑对内存连续性很敏感。RSI、CCI、ATR、MACD四个指标Refresh后,total被掐掉 HistoryBars+SessionSize+2 个柱,留给随机游走的偏移空间。 迭代里用MathRandomNormal(0,1)取标准正态再fabs夹到[0,1],乘total加SessionSize得到shift。这里正态只是拿来打乱起点,不隐含任何价格分布假设。 内层batch循环逐个拼CBufferFloat状态:取r=i+HistoryBars这根柱往前HistoryBars根的开盘价、时分结构和四大指标值。若r超出bars就delete跳过,避免越界读Rates。外汇和贵金属波动大,这类训练对外汇/贵金属属高风险操作,参数不当可能过拟合。
class="kw">return(INIT_SUCCEEDED); } class="type">void Train(class="type">void) { class=class="str">"cmt">//--- class="type">MqlDateTime start_time; TimeCurrent(start_time); start_time.year -= StudyPeriod; if(start_time.year <= class="num">0) start_time.year = class="num">1900; class="type">class="kw">datetime st_time = StructToTime(start_time); class="type">int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates); if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars)) { ExpertRemove(); class="kw">return; } if(!ArraySetAsSeries(Rates, true)) { ExpertRemove(); class="kw">return; } class=class="str">"cmt">//--- RSI.Refresh(); CCI.Refresh(); ATR.Refresh(); MACD.Refresh(); class="type">int total = bars - (class="type">int)(HistoryBars + SessionSize+class="num">2); class=class="str">"cmt">//--- CBufferFloat* State; class="type">class="kw">float loss = class="num">0; class="type">uint count = class="num">0; for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped()); iter ++) { class="type">int error_code; class="type">int shift = (class="type">int)(fmin(fabs(Math::MathRandomNormal(class="num">0, class="num">1, error_code)), class="num">1) * (total) + SessionSize); States.Clear(); for(class="type">int batch = class="num">0; batch < SessionSize; batch++) { class="type">int i = shift - batch; State = new CBufferFloat(); if(!State) { ExpertRemove(); class="kw">return; } class="type">int r = i + (class="type">int)HistoryBars; if(r > bars) { class="kw">delete State; class="kw">continue; } for(class="type">int b = class="num">0; b < (class="type">int)HistoryBars; b++) { class="type">int bar_t = r - b; class="type">class="kw">float open = (class="type">class="kw">float)Rates[bar_t].open; TimeToStruct(Rates[bar_t].time, sTime); class="type">class="kw">float rsi = (class="type">class="kw">float)RSI.Main(bar_t); class="type">class="kw">float cci = (class="type">class="kw">float)CCI.Main(bar_t); class="type">class="kw">float atr = (class="type">class="kw">float)ATR.Main(bar_t); class="type">class="kw">float macd = (class="type">class="kw">float)MACD.Main(bar_t); class="type">class="kw">float sign = (class="type">class="kw">float)MACD.Signal(bar_t);
特征拼装与强化信号判定
这段逻辑在做两件事:先剔除指标缺值导致的脏数据,再把单根 K 线的价格位移、时空标签和五大指标压进一个状态向量。任一指标返回 EMPTY_VALUE 就直接 delete State 并跳过,避免把空值喂给后续网络。 状态向量固定 12 维:收盘减开盘、最高减开盘、最低减开盘、tick_volume/1000、小时、星期几、月份,以及 rsi、cci、atr、macd、sign 共 11 项再加一项凑满。若 State.Total() 小于 HistoryBars*12,说明历史样本没攒够,continue 重来。 喂完 Actor.feedForward(State,12,true) 后取结果,GetAction 拿到离散动作。动作 0 代表偏空:若上一根实际收益 reward(收减开)为负,乘 -20 放大惩罚;若为正则只乘 1。外汇与贵金属波动剧烈,这种不对称奖励只是训练倾向,实盘信号可能失效,务必先在 MT5 策略测试器验证。 别把 EMPTY_VALUE 当小事 指标在初期几根或离线时段常返回 EMPTY_VALUE,不拦截就会让状态维度错位,feedForward 直接崩。开 EA 时把 HistoryBars 设小一点,能更快暴露这类边界。
if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE) { class="kw">delete State; class="kw">continue; } class=class="str">"cmt">//--- if(!State.Add((class="type">class="kw">float)Rates[bar_t].close - open) || !State.Add((class="type">class="kw">float)Rates[bar_t].high - open) || !State.Add((class="type">class="kw">float)Rates[bar_t].low - open) || !State.Add((class="type">class="kw">float)Rates[bar_t].tick_volume / class="num">1000.0f) || !State.Add(sTime.hour) || !State.Add(sTime.day_of_week) || !State.Add(sTime.mon) || !State.Add(rsi) || !State.Add(cci) || !State.Add(atr) || !State.Add(macd) || !State.Add(sign)) { class="kw">delete State; class="kw">break; } } if(IsStopped()) { class="kw">delete State; ExpertRemove(); class="kw">return; } if(State.Total() < (class="type">int)HistoryBars * class="num">12) { class="kw">delete State; class="kw">continue; } if(!Actor.feedForward(GetPointer(State), class="num">12, true)) { class="kw">delete State; ExpertRemove(); class="kw">return; } Actor.getResults(TempData); class="type">int action = GetAction(TempData); if(action < class="num">0) { class="kw">delete State; ExpertRemove(); class="kw">return; } class="type">class="kw">double reward = Rates[i - class="num">1].close - Rates[i - class="num">1].open; class="kw">switch(action) { case class="num">0: if(reward < class="num">0) reward *= -class="num">20; else reward *= class="num">1; class="kw">break;
◍ 强化学习里的奖励惩罚与折扣回传
这段逻辑处理的是 agent 在每一 batch 动作后的奖励重标定。case 1 里若原始 reward 为正,直接乘 -20 翻转成重惩罚;若已为负则只乘 -1,说明对「错误方向盈利」的容忍度极低,模型倾向把这类样本视为严重误导。 default 分支按 batch 序号区分:首步(batch==0)用 fabs 取负绝对值强制惩罚;非首步则看上一步动作——动作 0 翻转符号、动作 1 维持、其余取负绝对值。这种嵌套 switch 实质是在用历史动作约束当前步的信用分配。 底部向量运算把 SessionSize 长度的奖励做累积折扣:rewards 先按折扣因子幂次加权再 CumSum,最后用最大绝对值归一化。loss 采用滑动平均,前 9 次用 count 加权、第 10 次起固定分母 10,意味着早期梯度噪声被逐步稀释。 开 MT5 把 DiscountFactor 从默认改到 0.95 对比 0.99,能直接看到归一化后 rewards 尾部权重差异;外汇与贵金属杠杆高,这类自奖励回路若惩罚系数设错,回测曲线可能虚假平滑。
case class="num">1: if(reward > class="num">0) reward *= -class="num">20; else reward *= -class="num">1; class="kw">break; class="kw">default: if(batch == class="num">0) reward = -fabs(reward); else { class="kw">switch((class="type">int)vActions[batch - class="num">1]) { case class="num">0: reward *= -class="num">1; class="kw">break; case class="num">1: class="kw">break; class="kw">default: reward = -fabs(reward); class="kw">break; } } class="kw">break; } if(!States.Add(State)) { class="kw">delete State; ExpertRemove(); class="kw">return; } vActions[batch] = (class="type">class="kw">float)action; vRewards[SessionSize - batch - class="num">1] = (class="type">class="kw">float)reward; vProbs[SessionSize - batch - class="num">1] = TempData.At(action); class=class="str">"cmt">//--- } vectorf rewards = vectorf::Full(SessionSize, class="num">1); rewards = MathAbs(rewards.CumSum() - SessionSize); rewards = (vRewards * MathPow(vectorf::Full(SessionSize, DiscountFactor), rewards)).CumSum(); rewards = rewards / fmax(rewards.Max(), fabs(rewards.Min())); loss = (fmin(count, class="num">9) * loss + (rewards * MathLog(vProbs) * (-class="num">1)).Sum() / SessionSize) / fmin(count + class="num">1, class="num">10); count++; class="type">class="kw">float total_reward = vRewards.Sum(); if(BestLoss >= loss) {
「回放缓冲区里的反向传播闭环」
这段逻辑跑在训练会话的末尾,先把 Actor 与 Critic 的权重落盘,文件名拼了 .nnw 后缀,并写入当前 loss 与最近平均误差,时间戳取 Rates[shift - SessionSize].time。若任一侧 Save 失败直接 return,BestLoss 也只在成功保存后才被更新为当前 loss,这意味着中断的训练不会污染“最优”记录。 紧接着是一个从 SessionSize-1 倒序到 0 的批循环,逐帧把历史状态喂给两个网络做前馈。任何一次 feedForward 返回 false,立即 ExpertRemove 并退出,实盘里这往往对应矩阵维度不匹配或状态向量越界。 Critic 先取结果里下标 0~3 的最大值当作价值估计 value,再把对应动作位置的奖励写回 TempData;随后 Critic.backProp 用带奖励的标签更新自己。Actor 的更新标签则是「奖励减 value」的优势量,由 TempData.BufferInit(Actions,0) 重置后写入再 backProp。整套流程每轮迭代结束会 PrintFormat 打出累计奖励与 loss,五位小数精度足够观察梯度是否塌缩。 把这段代码直接贴进 MT5 的 EA 训练函数,把 SessionSize 调到 128 对比 512,你可能看到 loss 收敛速度差出 2~3 倍,但过大会话也更容易在 feedForward 阶段触发 ExpertRemove。外汇与贵金属杠杆高,这类强化学习权重仅作概率参考,实盘前务必用历史数据重放验证。
if(!Actor.Save(ACTOR + ".nnw", loss, class="num">0, class="num">0, Rates[shift - SessionSize].time, class="kw">false) || !Critic.Save(CRITIC + ".nnw", Critic.getRecentAverageError(), class="num">0, class="num">0, Rates[shift - SessionSize].time, class="kw">false)) class="kw">return; BestLoss = loss; } for(class="type">int batch = SessionSize - class="num">1; batch >= class="num">0; batch--) { State = States.At(batch); if(!Actor.feedForward(State) || !Critic.feedForward(State)) { ExpertRemove(); class="kw">return; } Critic.getResults(TempData); class="type">class="kw">float value = TempData.At(TempData.Maximum(class="num">0, class="num">3)); if(!TempData.Update((class="type">int)vActions[batch], rewards[SessionSize - batch - class="num">1])) { ExpertRemove(); class="kw">return; } if(!Critic.backProp(TempData)) { ExpertRemove(); class="kw">return; } if(!TempData.BufferInit(Actions, class="num">0) || !TempData.Update((class="type">int)vActions[batch], rewards[SessionSize - batch - class="num">1] - value)) { ExpertRemove(); class="kw">return; } if(!Actor.backProp(TempData)) { ExpertRemove(); class="kw">return; } } PrintFormat("Iteration %d, Cummulative reward %.5f, loss %.5f", iter, total_reward, loss); } Comment(""); class=class="str">"cmt">//--- ExpertRemove(); }
用最小手数验证训练后的策略模型
模型在 EURUSD H1 周期、过去两年历史数据上做额外训练,指标走默认参数,训练超参和前几篇基本一致。训完直接把策略模型丢进策略测试器跑 REINFORCE-test.mq5,这 EA 的算法上一节讲过,全部用固定最小手数、无止损无止盈,纯粹看模型本身怎么动作。 测试样本完全在训练集之外,这点很关键——说明系统构建逻辑是自洽的,不是过拟合出来的漂亮曲线。余额图走得很平顺,盈利因子 2.20,盈利单占比 56% 以上,平均盈利比平均亏损高 70%。 图表上能看清一件事:亏的单子跑得飞快,赚的单子愿意拿一会儿,所有单子都在新 K 线开盘触发,好几个几乎踩在分形反转 K 线的开盘价上。 外汇和贵金属杠杆高、滑点跳空频发,这套 EA 严禁上实盘。测试区间太短,又没有资金管理和风控模块,裸奔止损止盈在真实账户里是找死,它只配当模型演示器。