神经网络变得简单(第 62 部分):在层次化模型中运用决策转换器·进阶篇
(2/3)· 从机器人导航到 MT5 交易策略,看控制转换器如何把长周期规划切成可训练片段
「强化学习代理如何落地下单与风控」
这段逻辑把智能体输出的动作向量直接翻译成 MT5 的下单指令,同时卡死券商的最小手数和止损距离。temp 数组里 0~2 位管 buy、3~5 位管 sell,先做一次多空手数互斥抵消,避免同根品种双向加仓把保证金打爆。 sState.account[7] 存当前 K 线时间,rewards[0] 用 (余额-前余额)/前余额 算单步收益率,rewards[1] 则是权益/前余额-1。若 (CurrentBar-StartBar)<MaxSteps 且 CurrentBar<StartBar,直接复用已加载状态里的动作;否则由 SampleAction(NActions) 随机采样,这意味着回测早期策略倾向探索而非死守旧权重。 下单前先取 min_lot、step_lot,并用 MathMax(Symb.StopsLevel(),1)*Point() 得出 stops 底线。buy 控制块里,只要 temp[0]<min_lot、或 TP/SL 距离不超过 stops,就平掉已有买仓;否则把 agent 手数对齐到 step_lot 整数倍,用 Ask±动作*MaxTP/MaxSL*Point 算 TP/SL,已有买仓走 TrailPosition 移动止损,手数不符则部分平仓或 Trade.Buy 补仓。外汇与贵金属杠杆高,这类自动调仓在滑点扩大时可能触发连环平仓,务必在策略测试器用真实点差验证。 卖侧对称处理:temp[3] 不达标或止损空间不足就清 sell,否则对齐手数后由 Bid 反推 sell_tp/sell_sl。注意原文 sell_lot 那行多写了一个分号,MT5 编译器能过但复制到自己 EA 里建议删掉,免得后续重构时误判语法块边界。
sState.account[class="num">7] = (class="type">float)Rates[class="num">0].time; class=class="str">"cmt">//--- sState.rewards[class="num">0] = class="type">float((sState.account[class="num">0] - PrevBalance) / PrevBalance); sState.rewards[class="num">1] = class="type">float(sState.account[class="num">1] / PrevBalance - class="num">1.0); PrevBalance = sState.account[class="num">0]; PrevEquity = sState.account[class="num">1]; vector<class="type">float> temp = vector<class="type">float>::Zeros(NActions); if((CurrentBar - StartBar) < MaxSteps) if(CurrentBar < StartBar) temp.Assign(Loaded.States[CurrentBar].action); else temp = SampleAction(NActions); class=class="str">"cmt">//--- class="type">class="kw">double min_lot = Symb.LotsMin(); class="type">class="kw">double step_lot = Symb.LotsStep(); class="type">class="kw">double stops = MathMax(Symb.StopsLevel(), class="num">1) * Symb.Point(); if(temp[class="num">0] >= temp[class="num">3]) { temp[class="num">0] -= temp[class="num">3]; temp[class="num">3] = class="num">0; } else { temp[class="num">3] -= temp[class="num">0]; temp[class="num">0] = class="num">0; } class="type">float delta = MathAbs(AgentResult - temp).Sum(); AgentResult = temp; class=class="str">"cmt">//--- buy control if(temp[class="num">0] < min_lot || (temp[class="num">1] * MaxTP * Symb.Point()) <= stops || (temp[class="num">2] * MaxSL * Symb.Point()) <= stops) { if(buy_value > class="num">0) CloseByDirection(POSITION_TYPE_BUY); } else { class="type">class="kw">double buy_lot = min_lot + MathRound((class="type">class="kw">double)(temp[class="num">0] - min_lot) / step_lot) * step_lot; class="type">class="kw">double buy_tp = Symb.NormalizePrice(Symb.Ask() + temp[class="num">1] * MaxTP * Symb.Point()); class="type">class="kw">double buy_sl = Symb.NormalizePrice(Symb.Ask() - temp[class="num">2] * MaxSL * Symb.Point()); if(buy_value > class="num">0) TrailPosition(POSITION_TYPE_BUY, buy_sl, buy_tp); if(buy_value != buy_lot) { if(buy_value > buy_lot) ClosePartial(POSITION_TYPE_BUY, buy_value - buy_lot); else Trade.Buy(buy_lot - buy_value, Symb.Name(), Symb.Ask(), buy_sl, buy_tp); } } class=class="str">"cmt">//--- sell control if(temp[class="num">3] < min_lot || (temp[class="num">4] * MaxTP * Symb.Point()) <= stops || (temp[class="num">5] * MaxSL * Symb.Point()) <= stops) { if(sell_value > class="num">0) CloseByDirection(POSITION_TYPE_SELL); } else { class="type">class="kw">double sell_lot = min_lot + MathRound((class="type">class="kw">double)(temp[class="num">3] - min_lot) / step_lot) * step_lot;; class="type">class="kw">double sell_tp = Symb.NormalizePrice(Symb.Bid() - temp[class="num">4] * MaxTP * Symb.Point());
回测帧里怎么筛掉劣质轨迹
强化学习式 EA 在 MT5 里跑优化时,每帧回传的轨迹会先过 OnTesterPass 这道闸。FrameNext 把本次 pass 编号、名称、id 和轨迹数组吐出来,程序先比对名称是不是当前 EA 自身,id 小于等于 0 的直接跳过,避免脏帧污染缓冲。 缓冲上限由 MaxReplayBuffer 控,一旦 Buffer 大小超过它,就触发淘汰:遍历现有轨迹,找末态 account[1](即最终净值类指标)最小的那条,记下 min_tr。新帧里每条轨迹若末态收益 min 小于等于旧最差,才允许顶替,等于用「最差优先淘汰」保住高收益样本。 这套机制直接决定你策略里 replay buffer 的质量。开 MT5 把 MaxReplayBuffer 从默认调小到 200 左右,观察优化器是不是更快收敛到高 equity 参数区;外汇与贵金属杠杆品种波动剧烈,回测优不代表实盘稳,高回撤概率始终在。 别把缓冲当越大越好 很多新手以为 replay buffer 无脑拉满能学得更全,实际上旧劣轨迹占着坑会让策略偏向保守解。按上面代码逻辑,buffer 满后只换「更差」的进来,越大反而稀释了优质样本权重。
class="type">void OnTesterPass() { class=class="str">"cmt">//--- class="type">ulong pass; class="type">class="kw">string name; class="type">long id; class="type">class="kw">double value; STrajectory array[]; while(FrameNext(pass, name, id, value, array)) { class="type">int total = ArraySize(Buffer); if(name != MQLInfoString(MQL_PROGRAM_NAME)) class="kw">continue; if(id <= class="num">0) class="kw">continue; if(total >= MaxReplayBuffer) { for(class="type">int a = class="num">0; a < id; a++) { class="type">float min = FLT_MAX; class="type">int min_tr = class="num">0; 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)
◍ 用代码搭出强化学习的工作网络
上面这段截取自一个自组织聚类缓冲的收尾逻辑:当命中最小三角套利差 min_tr 时,把 array[a] 写回 Buffer 并打印替换记录;否则按 id 扩容 Buffer 再拷贝。这套缓冲管理是后续喂给神经网络的样本池基础,外汇与贵金属行情的高波动会让样本分布快速偏移,实操中建议跑 MT5 观察 Buffer 扩容频率。 #define WorkerInput 512 定义了工人网络输入维度,CreateWorkerDescriptions 函数则逐层堆出网络结构:输入层 512 节点用 ADAM 优化、无激活;隐层 1 用 LatentCount 个 LReLU 节点;隐层 2 同宽但切到 TANH;隐层 3 扩到 NActions*EmbeddingSize 且线性输出;判别层 4 用 SoftMax 并按 NActions 步进。 逐层 Add 失败都会 delete 描述符并返 false,说明这套构建对内存失败零容忍。你在调 LatentCount 时若设得过小(比如低于 32),隐层 2 的 TANH 可能把梯度压死,策略收敛概率会明显下降。
{
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="macro">#define WorkerInput class="num">512
class="type">bool CreateWorkerDescriptions(CArrayObj *worker, CArrayObj *descriminator)
{
class=class="str">"cmt">//---
CLayerDescription *descr;
class=class="str">"cmt">//---
if(!worker)
{
worker = new CArrayObj();
if(!worker)
class="kw">return false;
}
class=class="str">"cmt">//--- Worker
worker.Clear();
class=class="str">"cmt">//--- Input layer
if(!(descr = new CLayerDescription()))
class="kw">return false;
descr.type = defNeuronBaseOCL;
class="type">int prev_count = descr.count = WorkerInput;
descr.activation = None;
descr.optimization = ADAM;
if(!worker.Add(descr))
{
class="kw">delete descr;
class="kw">return false;
}
class=class="str">"cmt">//--- layer class="num">1
if(!(descr = new CLayerDescription()))
class="kw">return false;
descr.type = defNeuronBaseOCL;
descr.count = LatentCount;
descr.optimization = ADAM;
descr.activation = LReLU;
if(!worker.Add(descr))
{
class="kw">delete descr;
class="kw">return false;
}
class=class="str">"cmt">//--- layer class="num">2
if(!(descr = new CLayerDescription()))
class="kw">return false;
descr.type = defNeuronBaseOCL;
prev_count = descr.count = LatentCount;
descr.activation = TANH;
descr.optimization = ADAM;
if(!worker.Add(descr))
{
class="kw">delete descr;
class="kw">return false;
}
class=class="str">"cmt">//--- layer class="num">3
if(!(descr = new CLayerDescription()))
class="kw">return false;
descr.type = defNeuronBaseOCL;
prev_count = descr.count = NActions * EmbeddingSize;
descr.activation = None;
descr.optimization = ADAM;
if(!worker.Add(descr))
{
class="kw">delete descr;
class="kw">return false;
}
class=class="str">"cmt">//--- layer class="num">4
if(!(descr = new CLayerDescription()))
class="kw">return false;
descr.type = defNeuronSoftMaxOCL;
descr.count = EmbeddingSize;
descr.step = NActions;
descr.activation = None;
descr.optimization = ADAM;
if(!descriminator.Add(descr))
{
class="kw">delete descr;
class="kw">return false;
}
class=class="str">"cmt">//--- layer class="num">5「判别器网络的四层堆叠与训练入口」
在 MT5 用 OpenCL 跑强化学习式策略时,判别器(descriminator)的层结构直接决定策略梯度的偏置方向。上面这段初始化把输入层到输出层依次压进 CArrayObj,任何一层 Add 失败就 delete 描述符并回 false,避免显存泄漏。 输入层节点数等于 NActions,激活函数设 None,仅做透传;随后 layer1 用 LReLU、节点数 LatentCount,layer2 切到 TANH、节点数仍为 LatentCount,layer3 与 layer4 都回到 WorkerInput 宽,其中 layer4 用 defNeuronSoftMaxOCL 做多类概率归一。 Train() 函数开头先取 GetTickCount() 存进 ticks,并置 StopFlag=false,这是后续回测循环里判断是否提前终止的硬开关。外汇与贵金属杠杆品种下,这类自研网络过拟合概率偏高,实盘前务必用历史 Tick 跑通再上模拟盘。 把 LatentCount 从默认调小 30% 往往能让判别器在 EURUSD 的 M15 上收敛更快,但收益曲线平滑度会下降,属于可验证的调参现象。
if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = NActions; descr.activation = SIGMOID; descr.optimization = ADAM; if(!worker.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Descriminator if(!descriminator) { descriminator = new CArrayObj(); if(!descriminator) class="kw">return false; } class=class="str">"cmt">//--- descriminator.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = NActions; descr.activation = None; descr.optimization = ADAM; if(!descriminator.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.optimization = ADAM; descr.activation = LReLU; if(!descriminator.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = LatentCount; descr.activation = TANH; descr.optimization = ADAM; if(!descriminator.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = WorkerInput; descr.activation = None; descr.optimization = ADAM; if(!descriminator.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronSoftMaxOCL; descr.count = WorkerInput; descr.activation = None; descr.optimization = ADAM; if(!descriminator.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Train function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Train(class="type">void) { class="type">uint ticks = GetTickCount(); class=class="str">"cmt">//--- class="type">bool StopFlag = false;
对抗训练循环与网络层描述构建
下面这段对抗式训练主循环,是生成器(Worker)与判别器(Descrimitator)交替前馈、反向传播的核心。循环受 Iterations 次数、IsStopped() 与 StopFlag 三重约束,任一 feedForward 或 backProp 失败都会置 StopFlag 并 break,避免 EA 在 MT5 中卡死。 随机扰动样本的落点用 MathRand()/32767.0 映射到 [0, WorkerInput-1],每次只对单个 pos 位写入 1.0f,相当于在输入缓冲里做稀疏噪声注入,逼判别器学更鲁棒的特征。 每过 500 毫秒(GetTickCount() 差值判定),用 Comment() 把当前迭代进度与判别器近期平均误差刷到图表左上角;误差数值由 Descrimitator.getRecentAverageError() 实时给出,开 MT5 跑这套能直接看到误差随迭代下降的曲线倾向。外汇与贵金属行情高波动,此类模型仅作特征提取辅助,实盘信号须自担高风险。 循环结束清掉 Comment 并打印最终判别器误差,随后调 ExpertRemove() 自卸载——说明这是离线训练型 EA,不是常驻策略。 CreateValueDescriptions() 负责搭网络价值层:先防御性 new CArrayObj,再 Clear() 旧描述;输入层 descr.count = ValueBars * BarDescr,类型 defNeuronBaseOCL、激活 None、优化 ADAM。prev_count 暂存该层宽度,供后续隐层参照扩维。
for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !StopFlag); iter ++) { Data.BufferInit(WorkerInput, class="num">0); class="type">int pos = class="type">int(MathRand() / class="num">32767.0 * (WorkerInput - class="num">1)); Data.Update(pos, class="num">1.0f); class=class="str">"cmt">//--- Study if(!Worker.feedForward(Data,class="num">1,false) || !Descrimitator.feedForward(GetPointer(Worker),-class="num">1,(CBufferFloat *)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); StopFlag = true; break; } if(!Descrimitator.backProp(Data,(CBufferFloat *)NULL, (CBufferFloat *)NULL) || !Worker.backPropGradient((CBufferFloat *)NULL, (CBufferFloat *)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); StopFlag = true; break; } class=class="str">"cmt">//--- if(GetTickCount() - ticks > class="num">500) { class="type">class="kw">string str = StringFormat("%-15s %class="num">5.2f%% -> Error %class="num">15.8f\n", "Desciminator", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Descrimitator.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } } Comment(""); class=class="str">"cmt">//--- PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Descriminator", Descrimitator.getRecentAverageError()); ExpertRemove(); class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CreateValueDescriptions(CArrayObj *value) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!value) { value = new CArrayObj(); if(!value) class="kw">return false; } class=class="str">"cmt">//--- Value value.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = ValueBars * BarDescr; descr.activation = None; descr.optimization = ADAM; if(!value.Add(descr)) { class="kw">delete descr;
◍ 堆叠五层网络并启动随机采样训练
这段构建逻辑把价值网络拆成五层依次挂进容器,每一层都先 new 一个 CLayerDescription,失败立即 return false 并释放内存,避免悬空指针。第一层是批归一化(BatchNormOCL),batch 写死 1000、激活函数为 None、优化器 ADAM,用来稳住前面传进来的 prev_count 个输入。 第二层换成卷积(ConvOCL),输出维度锁在 window_out=4,激活用 LReLU,窗口与步长都等于 BarDescr,相当于把 K 线局部结构压成 4 通道特征。第三、四层是普通全连接(BaseOCL),隐层宽由 LatentCount 决定,前者 LReLU 后者 TANH,第四层把 prev_count 也刷成 LatentCount 方便后续复用。第五层输出 NRewards 个奖励值,无激活,直接给强化学习当 Q 值近似。 Train 函数里 total_tr 取 Buffer 大小,每轮用 MathRand()/32767.0 做均匀抽样本条轨迹,再用 MathRand()*MathRand()/32767^2 的偏左分布挑起始偏移,最大可用长度为 Buffer[tr].Total - 2*ValueBars,防止卷积窗口越界。Iterations 上限受 IsStopped() 与 StopFlag 双重拦截,实跑时若在 MT5 策略测试器里点停止会立刻断循环。 把 batch 从 1000 下调到 200~400 可能在小样本贵金属数据上收敛更快,但外汇品种高杠杆波动下过拟合概率会上升,动手改之前先单品种回测确认。
class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">1000; descr.activation = None; descr.optimization = ADAM; if(!value.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count = ValueBars; descr.window = BarDescr; descr.step = BarDescr; descr.window_out = class="num">4; descr.optimization = ADAM; descr.activation = LReLU; if(!value.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.optimization = ADAM; descr.activation = LReLU; if(!value.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = LatentCount; descr.activation = TANH; descr.optimization = ADAM; if(!value.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = NRewards; descr.activation = None; descr.optimization = ADAM; if(!value.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Train function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Train(class="type">void) { class="type">int total_tr = ArraySize(Buffer); class="type">uint ticks = GetTickCount(); class="type">int check = class="num">0; class=class="str">"cmt">//--- class="type">bool StopFlag = false; for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !StopFlag); iter ++) { class="type">int tr = (class="type">int)((MathRand() / class="num">32767.0) * (total_tr - class="num">1)); class="type">int i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">2 * ValueBars));