您应当知道的 MQL5 向导技术(第 47 部分):配合时态差异的强化学习·综合运用
「强化信号类的网络与Q表装配」
这段实现把交易决策拆成买/卖两套独立策略:CSignalTD 构造函数里先给 RL 结构定下 3 个动作(买、卖、观望)与 3 种环境状态(多头、空头、震荡),并分别 new 出 QL_BUY 与 QL_SELL 两个 Q-learning 实例。 策略网络部分,POLICY_NETWORK_BUY 和 POLICY_NETWORK_SELL 都按相同拓扑搭建:首层 AddDenseLayer(9, AF_SIGMOID, 3) 表示 9 个输入神经元、Sigmoid 激活、上一隐层 3 个节点;第二层 AddDenseLayer(3, AF_SOFTMAX) 输出 3 维概率分布。两套网络均用 Init(0.0004, LOSS_BCE) 设定学习率 0.0004 与二分类交叉熵损失。 CNeuralNetwork 的层扩容逻辑靠 ArrayResize + new CLayer 完成:若 LastNeurons 非零则按该值加权重,否则连到前一层激活向量大小。析构时遍历 layers 逐个 delete,避免 MT5 终端内存泄漏。 直接在 MT5 里把 m_scale 从 5 改成 10,输入向量维度会随之翻倍,策略网络首层需同步改 9 为 19 才能编译通过——这是验证拓扑耦合的最快办法。外汇与贵金属杠杆高,此类信号仅作概率参考,实盘须自担风险。
{ ArrayResize(layers, layers.Size() + class="num">1);
layers[layers.Size() - class="num">1] = new CLayer(Neurons, AF);
if(LastNeurons != class="num">0)
{ layers[layers.Size() - class="num">1].AddWeights(LastNeurons);
}
else if(layers.Size() - class="num">1 > class="num">0)
{ layers[layers.Size() - class="num">1].AddWeights(layers[layers.Size() - class="num">2].activations.Size());
}
};
class="type">void Init(class="type">class="kw">double LearningRate, ENUM_LOSS_FUNCTION LF)
{ m_loss = LF;
m_learning_rate = LearningRate;
};
vector Forward(vector& Data);
class="type">void Backward(vector& LabelAnswer);
class="type">void CNeuralNetwork(){};
class="type">void ~CNeuralNetwork()
{ if(layers.Size() > class="num">0)
{ for(class="type">int i = class="num">0; i < class="type">int(layers.Size()); i++)
{ class="kw">delete layers[i];
}
}
};
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CSignalTD::CSignalTD(class="type">void) : m_scale(class="num">5),
m_use_markov(true),
m_policy(true)
{
class=class="str">"cmt">//--- initialization of class="kw">protected data
m_used_series = USE_SERIES_OPEN + USE_SERIES_HIGH + USE_SERIES_LOW + USE_SERIES_CLOSE + USE_SERIES_SPREAD + USE_SERIES_TIME;
class=class="str">"cmt">//
RL.actions = class="num">3;class=class="str">"cmt">//buy, sell, do nothing
RL.environments = class="num">3;class=class="str">"cmt">//bullish, bearish, flat
RL.use_markov = m_use_markov;
RL.epsilon = m_epsilon;
QL_BUY = new Cql(RL);
QL_SELL = new Cql(RL);
class=class="str">"cmt">//
POLICY_NETWORK_BUY.AddDenseLayer(class="num">9, AF_SIGMOID, class="num">3);
POLICY_NETWORK_BUY.AddDenseLayer(class="num">3, AF_SOFTMAX);
POLICY_NETWORK_BUY.Init(class="num">0.0004,LOSS_BCE);
class=class="str">"cmt">//
POLICY_NETWORK_SELL.AddDenseLayer(class="num">9, AF_SIGMOID, class="num">3);
POLICY_NETWORK_SELL.AddDenseLayer(class="num">3, AF_SOFTMAX);
POLICY_NETWORK_SELL.Init(class="num">0.0004,LOSS_BCE);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSignalTD::GetOutput(Cql *QL, CNeuralNetwork &PN)
{ class="type">int _td_act = class="num">1;
vector _in, _in_row, _in_row_old, _in_col, _in_col_old;
if
(
_in_row.Init(m_scale) &&强化学习环境的差分采样与策略更新
这段逻辑在 MT5 里干的事,是把当前 K 线与上一根做差分,喂给一个 Q-learning 加感知机的混合模型。注意 CopyRates 的偏移参数:行向量取 shift=0,列向量旧集取 shift=m_scale,这意味着列方向拿的是更早一段窗口,用来构造状态空间的横截面特征。 _in_row 与 _in_row_old 都要求 Size()==m_scale+1,否则整段跳过。差分后 Resize(m_scale) 把长度收掉一帧,避免时间维和状态维错位。QL.Environment 吃的是行、列差分向量和全零期望向量 _in_e,随后用末端元素 int(_in_e[m_scale-1]) 定位马尔可夫状态坐标 (_row,_col)。 奖励不是裸价格变动:_reward_float 取末端差分,再除以 _in_row.Max() 与 Min() 构成的区间做归一,QL.GetReward 输出才进策略表。m_policy 开关决定走 OnPolicy 还是 OffPolicy 更新,两套写法都在同一函数内分支,回测时切这个 bool 就能比两种学习范式的信号分布。 标签构造很硬:末端差分大于 0 标 [1,0,0] 看多,小于 0 标 [0,0,1] 看空,等于 0 标 [0,1,0] 横盘。PN.Backward(_label) 反向传播后,再用 policy_history 第 0 行跑前向,取三输出最大下标赋给 _td_act。外汇与贵金属杠杆高,这套信号只代表模型倾向,实盘前请在策略测试器用 2020—2023 年 H1 数据跑一遍确认过拟合程度。
_in_row.CopyRates(m_symbol.Name(), m_period, class="num">8, class="num">0, m_scale+class="num">1) && _in_row.Size() == m_scale+class="num">1 && _in_row_old.Init(m_scale) && _in_row_old.CopyRates(m_symbol.Name(), m_period, class="num">8, class="num">1, m_scale+class="num">1) && _in_row_old.Size() == m_scale+class="num">1 && _in_col.Init(m_scale) && _in_col.CopyRates(m_symbol.Name(), m_period, class="num">8, class="num">0, m_scale+class="num">1) && _in_col.Size() == m_scale+class="num">1 && _in_col_old.Init(m_scale) && _in_col_old.CopyRates(m_symbol.Name(), m_period, class="num">8, m_scale, m_scale+class="num">1) && _in_col_old.Size() == m_scale+class="num">1 ) { _in_row -= _in_row_old; _in_col -= _in_col_old; _in_row.Resize(m_scale); _in_col.Resize(m_scale); vector _in_e; _in_e.Init(m_scale); QL.Environment(_in_row, _in_col, _in_e); class="type">int _row = class="num">0, _col = class="num">0; QL.SetMarkov(class="type">int(_in_e[m_scale - class="num">1]), _row, _col); class="type">class="kw">double _reward_float = _in_row[m_scale - class="num">1]; class="type">class="kw">double _reward_max = _in_row.Max(); class="type">class="kw">double _reward_min = _in_row.Min(); class="type">class="kw">double _reward = QL.GetReward(_reward_max, _reward_min, _reward_float); if(m_policy) { QL.SetOnPolicy(_reward, _in_e); } else if(!m_policy) { QL.SetOffPolicy(_reward, _in_e); } PN.Forward(QL.policy_history.Row(class="num">1)); vector _label; _label.Init(class="num">3); _label.Fill(class="num">0.0); if(_in_row[m_scale-class="num">1] > class="num">0.0) { _label[class="num">0] = class="num">1.0; } else if(_in_row[m_scale-class="num">1] < class="num">0.0) { _label[class="num">2] = class="num">1.0; } else if(_in_row[m_scale-class="num">1] == class="num">0.0) { _label[class="num">1] = class="num">1.0; } PN.Backward(_label); vector _td_output = PN.Forward(QL.policy_history.Row(class="num">0)); if(_td_output[class="num">0] >= _td_output[class="num">1] && _td_output[class="num">0] >= _td_output[class="num">2]) { _td_act = class="num">0; } else if(_td_output[class="num">2] >= _td_output[class="num">0] && _td_output[class="num">2] >= _td_output[class="num">1]) { _td_act = class="num">2; } } class="kw">return(_td_act); }
◍ TD 里被忽略的两个敏感参数
前一轮测试只动了 epsilon、马尔可夫权重开关和政策更新开关,长/短条件里的开收盘价阈值、止盈、以点计的开仓阈值也都属于常规非 TD 参数。真正藏在 TD 和强化学习内部的 alpha、gamma 当时被直接按 0.1 与 0.5 写死,没有参与寻优。 这两个值直接决定政策更新的步幅与远期奖励折扣,对信号类整体表现高度敏感,预设值未必落在合适区间。 实现时还略过了政策网络本身的设定:固定用了 3-9-3 结构,各层激活函数和学习率都是常量。实际调参中,单层激活函数或学习率任一变动,甚至全部一起改,都可能显著改变自定义信号类的产出与执行效率。 开 MT5 把 CTrade 信号类里的 alpha、gamma 从 0.1/0.5 起做网格扫描,外汇与贵金属杠杆高、滑点大,回测结论仅代表历史概率,实盘需控仓验证。
「探索衰减之外:让 epsilon 真正活起来」
前面几节把时态差分(TD)强化学习在 MT5 里的落地跑通了,但有一个暗坑没细说:多数示例里 epsilon 随步数单调衰减,隐含假设是模型越往后越不需要探索。可实盘环境里贵金属和外汇的高波动特性,会让这个假设提前失效——价格结构切换时,衰减到谷底的 agent 可能连新形态都采不到样。 更合理的做法不是只减 epsilon,而是让它可变:比如随近期回撤幅度或波动率上调。我们在之前一篇动态学习率文章里验证过,针对 epsilon 做自适应比固定衰减更能保住强化学习的‘根源’。你在 wz_47.mq5 里改几行就能试:把衰减项换成波动率归一后的系数。 外汇与贵金属杠杆高、滑点跳空频繁,任何自适应策略都只是概率占优,不是免死金牌。开 MT5 把附带的 SignalWZ_47.mqh 接上实盘模拟,观察 epsilon 不降反升的时段,往往对应着行情 regime 切换。