神经网络变得简单(第 70 部分):封闭式政策改进运算器(CFPI)·综合运用
◍ 训练循环里的状态采样与账户差分
在 EA 的 Train 函数里,外层用 for(iter<Iterations && !IsStopped()) 控制总迭代次数,内层按 BatchSize 攒一批样本。每一批先建一个 BatchSize×4 的零矩阵 mBatch,随后逐条抽取轨迹与状态点。
轨迹由 SampleTrajectory(probability) 按 0.9 置信概率分布选出;状态偏移 i 用 MathRand()*MathRand()/Pow(32767,2) 做平方倾斜采样,再乘 (Buffer[tr].Total-2)。若 i<0 则本次 b-- 重抽,避免越界。
真正喂给网络的是账户变化率而非绝对值:PrevBalance 取 i-1 处余额,Account.Add() 写入 (当前余额-前余额)/前余额。这样模型看到的是归一化收益梯度,对外汇与贵金属这种高杠杆品种,训练对绝对资金量不敏感,但实盘仍属高风险,参数偏移可能让回测收益迅速衰减。
开 MT5 把 BatchSize 从默认改到 64 或 128,观察 GetTickCount() 返回的 ticks 耗时变化,能直接判断你显卡的 OpenCL 吞吐瓶颈。
class="type">void Train(class="type">void) { class=class="str">"cmt">//--- vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9); class=class="str">"cmt">//--- vector<class="type">float> rewards, rewards1, rewards2, target_reward; vector<class="type">float> action, action_beta; class="type">float Improve = class="num">0; class="type">int bar = (HistoryBars - class="num">1) * BarDescr; class="type">uint ticks = GetTickCount(); class=class="str">"cmt">//--- for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped()); iter ++) { matrix<class="type">float> mBatch = matrix<class="type">float>::Zeros(BatchSize, class="num">4); for(class="type">int b = class="num">0; b < BatchSize; b++) { 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) { b--; class="kw">continue; } class=class="str">"cmt">//--- State 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);
「把账户状态塞进特征向量」
这段逻辑干的事很直接:把每一帧的账户快照转成一组归一化数值,喂给后面的编码器。账户[1]是权益,先除以 PrevBalance 得到余额占比,再单独算 (权益-PrevEquity)/PrevEquity 的权益回撤率;账户[2]~[4] 原样入栈,账户[4]~[6] 都除以 PrevBalance 做缩放。 时间维度用了四组三角函数:以 2023.01.01 到 2024.01.01 的秒差(31536000 秒)为年周期,月用 PeriodSeconds(PERIOD_MN1)、周用 PERIOD_W1、日用 PERIOD_D1,分别取 sin/cos。x 为 0 时直接给 0,避免除零炸网络。 特征拼满后 Account.GetIndex()>=0 才 BufferWrite(),否则不落盘。随后 StateEncoder.feedForward 做状态嵌入,Actor 出动作,Critic1/Critic2 各算一路价值,任一 forward 失败就 PrintFormat 打函数名加行号并 break。外汇与贵金属杠杆高,这类特征若直接接实盘策略,回测漂移可能很大,建议先开 MT5 用历史数据跑一遍 Account.Add 的维度确认。
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 time = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7]; class="type">class="kw">double x = time / (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 = time / (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 = time / (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 = time / (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(); class=class="str">"cmt">//--- State embedding if(!StateEncoder.feedForward(GetPointer(State), class="num">1, false, GetPointer(Account))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">break; } class=class="str">"cmt">//--- Action if(!Actor.feedForward(GetPointer(StateEncoder), -class="num">1, NULL, class="num">1)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">break; } class=class="str">"cmt">//--- Cost if(!Critic1.feedForward(GetPointer(StateEncoder), -class="num">1, GetPointer(Actor)) || !Critic2.feedForward(GetPointer(StateEncoder), -class="num">1, GetPointer(Actor))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">break; } Critic1.getResults(rewards1);
从经验池挑样本喂给策略网络
强化学习回放阶段,代码先把 Critic 与 Actor 的本次结果取回,再把相邻三步的状态动作与奖励对齐:当前步 action、下一步 rewards、下下步 target_reward,构成时序差分需要的标签链。 批矩阵 mBatch 每行塞了 4 个数:轨迹号、步号、优势项(两路奖励较小值减去折扣后的目标差)、动作与参考动作的 L2 距离。优势项里 DiscFactor 是折扣系数,动作距离用 MathSqrt(MathPow(...)) 逐元素平方求和再开方。 选样本时先对动作列取 0.68 分位数 quant,做一组只保留低于分位数样本的权重裁剪,再用奖励绝对值乘权重除以动作得到最终 weights。ArgMax 挑出最重要的一条经验,sign 按该条奖励正负定方向,Improve 用滑动平均(iter 计数)缓步更新。 账户特征构造段把余额权益变化率、点差占用等 8 维归一化进 Account 向量,还额外用 2023→2024 年的秒数做年周期 sin 项、用月线/周线秒数做 cos 项,把时间季节性直接编进状态。外汇与贵金属杠杆品种用这套特征训练策略,回测过拟合概率高,实盘前务必在 MT5 策略测试器跑多品种验证。
Critic2.getResults(rewards2); Actor.getResults(action); action_beta.Assign(Buffer[tr].States[i].action); rewards.Assign(Buffer[tr].States[i + class="num">1].rewards); target_reward.Assign(Buffer[tr].States[i + class="num">2].rewards); class=class="str">"cmt">//--- Collect mBatch[b, class="num">0] = class="type">float(tr); mBatch[b, class="num">1] = class="type">float(i); mBatch[b, class="num">2] = MathMin(rewards1.Sum(), rewards2.Sum()) - (rewards - target_reward * DiscFactor).Sum(); mBatch[b, class="num">3] = MathSqrt(MathPow(action - action_beta, class="num">2).Sum()); } action = mBatch.Col(class="num">3); class="type">float quant = action.Quantile(class="num">0.68); vector<class="type">float> weights = action - quant - FLT_EPSILON; weights.Clip(weights.Min(), class="num">0); weights = weights / weights; weights.ReplaceNan(class="num">0); rewards = mBatch.Col(class="num">2); weights = MathAbs(rewards) * weights / action; class="type">class="kw">ulong pos = weights.ArgMax(); class="type">int sign = (rewards[pos] >= class="num">0 ? class="num">1 : -class="num">1); Improve = (Improve * iter + weights[pos]) / (iter + class="num">1); class="type">int tr = class="type">int(mBatch[pos, class="num">0]); class="type">int i = class="type">int(mBatch[pos, class="num">1]); class=class="str">"cmt">//--- Policy 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 time = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7]; class="type">class="kw">double x = time / (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 = time / (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 = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1);
◍ 双评论家网络下的奖励回传写法
这段逻辑跑在强化学习训练循环里,核心是用两个 Critic 网络(Critic1、Critic2)做保守 Q 值估计,再选较小的那个去反传梯度。外汇与贵金属行情具有高杠杆与跳空风险,这类模型仅用于离线回测,实盘直接套用可能放大回撤。 先看账户特征构造:把时间换算成日线周期数 x = time / PeriodSeconds(PERIOD_D1),再用 MathSin(2*M_PI*x) 生成周期正弦量写入 Account 缓冲。两次 Add 调用说明状态里塞了重复的周期相位特征,调参时若想降维,可只留一次。 StateEncoder 先吃 State 和 Account 做前向,失败就 PrintFormat 打函数行号并 break;Actor 与两个 Critic 的前向若返回 false 同样中断。这里没有任何异常兜底,意味着任一层维度不匹配会直接停训,开 MT5 跑前务必确认各网络输入输出层定义一致。 奖励侧用 rewards - target_reward * DiscFactor 算 TD 残差,DiscFactor 即折扣因子,典型取值 0.9~0.99。哪个 Critic 的 Sum() 小就选它做 backProp,双评论家取小能缓解 Q 值高估,但训练步数可能多 20%~30%。 循环里每累计 500ms(GetTickCount()-ticks>500)就打印一次 Mean Improvement 进度,iter/Iterations 给出百分比。想看收敛速度,直接把 500 改成 100 能在 MT5 Experts 日志里拿到更细的曲线。
Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0)); x = time / (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)); class=class="str">"cmt">//--- State if(Account.GetIndex() >= class="num">0) Account.BufferWrite(); if(!StateEncoder.feedForward(GetPointer(State), class="num">1, false, GetPointer(Account))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">break; } class=class="str">"cmt">//--- Action if(!Actor.feedForward(GetPointer(StateEncoder), -class="num">1, NULL, class="num">1)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">break; } class=class="str">"cmt">//--- Cost if(!Critic1.feedForward(GetPointer(StateEncoder), -class="num">1, GetPointer(Actor)) || !Critic2.feedForward(GetPointer(StateEncoder), -class="num">1, GetPointer(Actor))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">break; } Critic1.getResults(rewards1); Critic2.getResults(rewards2); class=class="str">"cmt">//--- rewards.Assign(Buffer[tr].States[i + class="num">1].rewards); target_reward.Assign(Buffer[tr].States[i + class="num">2].rewards); rewards = rewards - target_reward * DiscFactor; CNet *critic = NULL; if(rewards1.Sum() <= rewards2.Sum()) { Result.AssignArray(CAGrad((rewards1 - rewards)*sign) + rewards1); critic = GetPointer(Critic1); } else { Result.AssignArray(CAGrad((rewards2 - rewards)*sign) + rewards2); critic = GetPointer(Critic2); } if(!critic.backProp(Result, GetPointer(Actor), -class="num">1) || !Actor.backPropGradient((CBufferFloat *)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">break; } if(GetTickCount() - ticks > class="num">500) { class="type">class="kw">string str = StringFormat("%-15s %class="num">5.2f%% -> %class="num">15.8f\n", "Mean Improvement", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Improve);
「用计时与均值差给优化结果打分」
这段收尾代码把每次 tick 的耗时通过 GetTickCount 记录下来,并在循环结束后用 Comment("") 清空图表上的临时文字,避免干扰后续观察。 PrintFormat 把函数名、行号、Mean Improvement 及 Improve 数值按 %-15s %10.7f 格式打到日志,精度到小数点后 7 位,方便横向比对不同参数下的均值改进量。 最后调用 ExpertRemove 让 EA 跑完这组测量后自动退出,不留在图表上占用资源;外汇与贵金属市场高波动,这类微基准只反映本地计算开销,不等于实盘优势。
Comment(str); ticks = GetTickCount(); } } Comment(""); class=class="str">"cmt">//--- PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Mean Improvement", Improve); ExpertRemove(); class=class="str">"cmt">//--- }
EURUSD H1 上的 CFPI 从头训练与测试表现
这一节把前面实现的封闭式策略改进(CFPI)真正跑了一遍。训练与测试都用的 EURUSD H1 历史数据,训练取 2023 年前 7 个月,测试用 2023 年 8 月,所有指标保持默认参数,没有动源数据结构,只改了模型架构,所以旧模型不能直接复用,整个学习是“从头开始”的。 状态编码器和评论者先用 Experts\CFPI\StudyCritic.mq5 训,训练集是 500 条轨迹、每条 3591 个环境状态,合计近 180 万个状态-行动-奖励样本;评论者主训练跑满 100 万次迭代,理论上几乎能覆盖每一秒状态。扮演者政策在 Experts\CFPI\Study.mq5 里训,按 256 状态的数据包做 1 万次迭代,累计看了超 250 万个状态,比训练集还大。 头几次验算就能看到余额图里有可盈利段;额外收轨迹做到 200 次验算时,有 3 次完整跑完且获利。这倾向是随机初始化碰巧不差,也可能是方法外的因素叠加,但后续迭代里验算的平均盈利和盈利因子确有抬升迹象。 最终得到的扮演者政策在训练集和 8 月测试集上都盈利。测试期初有一段回撤,之后余额增长较均匀。整段测试做了 125 笔交易,45.6% 胜率,最高/平均盈利比相应亏损指标高约 50%,盈利因子 1.23。外汇与贵金属杠杆品种高风险,这套结果仅代表样本内与邻近样本外表现,换周期或品种可能明显衰减,建议开 MT5 用同数据重跑验证。
◍ 把封闭式政策改进搬进自己的策略
封闭式政策改进(CFPI)的核心价值,不在于跑出一条和示范轨迹一模一样的曲线,而是换一个方向去优化模型:不再死磕复刻最优轨迹,只挑效率最高的区段训练,把算力从噪声现象里省出来。原文实测里,这套思路即便做了数学上的简化改动,回测仍给出正向经验与可用结果,说明落地门槛没想象中高。 额外计算成本确实存在,但总体训练开支反而可能下降——因为你不再为「完全重复最佳轨迹」买单。对做外汇、贵金属量化的人,这种取舍在高波动品种上尤其值得试,毕竟这类市场噪声极多、过拟合风险高。 想验证就开 MT5 用自己历史 Tick 跑一遍 CFPI 变体,先拿 EURUSD 的 M15 小样本测,看策略是否在效率区段集中收敛。神经网络相关方法用于实盘前务必小资金验证,外汇与贵金属交易杠杆风险极高。
「随包附带的八个程序文件」
这套 LSTM 优化方案不是只给了一段示意代码,作者把整套训练与验证链路拆成了 8 个可独立加载的文件,全部打在 MQL5.zip(735.49 KB)里。 最底层是 Trajectory.mqh 和 NeuroNet.mqh 两个类库,前者定义系统状态结构,后者负责搭神经网络;再往下还有 NeuroNet.cl,是跑在显卡上的 OpenCL 算子库,显存够的话训练速度会明显不同。 往上四层都是 EA:Research.mq5 只做样本收集,ResearchRealORL.mq5 用 Real-ORL 方法补样本,Study.mq5 训 Actor、StudyCritic.mq5 训 Critics,最后 Test.mq5 把模型拉到历史行情里回测。 开 MT5 把 zip 解到 MQL5 目录后,先挂 Research 类 EA 跑几天收集轨迹,再按顺序训网络,不然直接跑 Test 会因为状态文件为空而没输出。外汇和贵金属行情高波动、杠杆风险大,任何模型都只是概率优势,实盘前务必用策略测试器自己跑一遍。