强化学习中的随机决策森林·进阶篇
🌲

强化学习中的随机决策森林·进阶篇

(2/3)·从 bootstrap 聚合到自主学习 EA,随机森林在交易系统里到底怎么跑通

新手友好 第 2/3 篇
很多交易者把随机森林当成黑箱直接套历史数据,却没注意 37% 袋外样本才是检验泛化真正的试金石。把树剪枝去掉换速度,代价是单树过拟合,靠投票抵消方差这条路在行情突变时仍可能失灵。先搞清楚 bagging 怎么选样本,再谈策略可信度。

「用随机森林当交易代理的大脑」

把 AI 交易者看成和环境(市场)互动的代理:在时间 t 处于状态 S_t,执行动作 A_t,随后进入 S_{t+1} 并按动作成败拿到回报 R_t。这一交互可重复 t+n 次,直到一局学习情节结束,再迭代多局来改进行为。 经典强化学习用状态-动作矩阵存经验,状态集一大,矩阵就爆炸。原文方案直接用随机森林替代表格,扮演代理的“大脑”来记忆与近似策略。 代理的随机参数化策略 Π_θ 是 (s,a) 对集合,θ 为各状态的参数向量;最优策略即该任务下的最优动作集。这属于连续未知状态、无模型的 actor-critic 思路,外汇与贵金属波动连续、滑点随机,实盘验证前务必认清高风险。 Sutton 与 Barto 的《强化学习:简介》覆盖了更多方法族,但 MT5 上落地的关键仍是先把状态与回报定义成可回测的数字。

用随机森林替代理替模糊系统选高斯中心

上一节把 Mamdani 模糊推理的高斯隶属函数做了优化,但那个版本有个硬伤:高斯中心在所有行情环境下都锁死,不随三个振荡器指标值变化。现在让代理自己动起来——根据当前状态向量在 [0;1] 区间里自动挑“中性”项的高斯位置,逼近最优策略。 具体落地是在 EA 全局变量里挂一个指向成员函数的独立指针,学习过程中随时改参数不掉链子。随机森林和矩阵类负责喂数据:输入是 3 个振荡器值组成的状态向量,输出是更新后的高斯中心。 两个输入参数直接暴露给优化器:树的数量 number_of_trees 默认 50,正则化分量 regularization 默认 0.63。调这两个值可能改善模型在贵金属与外汇品种上的拟合表现,但外汇/贵金属杠杆交易高风险,回测优不等于实盘能复现。 checkBeforeLearn 这个函数管模型状态:如果当前周期已经训过随机森林(读 RDFNtrees 文件里的 ntrees>0),就载入缓冲区大小、类别数、树数接着用;否则 random_policy 保持 true,用随机值初始化策略先瞎做。训练放在策略测试器优化模式,每跑完一次 pass 就在新矩阵上训并写文件。 订单开口后把状态向量塞进矩阵、用模糊推理结果填输出;订单平仓时若亏损,代理报酬从随机均匀分布里抽,逼它换动作搜最优解,盈利则报酬不动。下面这段是模型检查与载入的核心代码。

MQL5 / C++
CNormalMembershipFunction *updateNeutral=<span class="keyword">new</span> CNormalMembershipFunction(<span class="number">class="num">0.5</span>,<span class="number">class="num">0.2</span>);
<span class="comment">class=class="str">"cmt">//RDF 系统. 我们在这里创建所有的 RF 对象.</span>
CDecisionForest&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RDF;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//随机森林对象</span>
CMatrixDouble&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RDFpolicyMatrix;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//用于随机森林输入和输出的矩阵</span>
CDFReport&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RDF_report;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//RF 在这个对象中返回错误,这样我们可以检查它</span>
<span class="keyword">sinput</span> <span class="keyword">class="type">int</span> number_of_trees=<span class="number">class="num">50</span>;
<span class="keyword">sinput</span> <span class="keyword">class="type">class="kw">double</span> regularization=<span class="number">class="num">0.63</span>;
<span class="keyword">class="type">void</span> checkBeforeLearn()
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">if</span>(clear_model)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> clearRDF=<span class="functions">FileOpen</span>("RDFNtrees"+<span class="predefines">_Symbol</span>+(<span class="keyword">class="type">class="kw">string</span>)<span class="predefines">_Period</span>+".txt",<span class="macro">FILE_READ</span>|<span class="macro">FILE_WRITE</span>|<span class="macro">FILE_CSV</span>|<span class="macro">FILE_ANSI</span>|<span class="macro">FILE_COMMON</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">FileWrite</span>(clearRDF,<span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">FileClose</span>(clearRDF);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ExpertRemove</span>();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>;
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="keyword">class="type">int</span> filehnd=<span class="functions">FileOpen</span>("RDFNtrees"+ <span class="predefines">_Symbol</span> + (<span class="keyword">class="type">class="kw">string</span>)<span class="predefines">_Period</span> +".txt",<span class="macro">FILE_READ</span>|<span class="macro">FILE_WRITE</span>|<span class="macro">FILE_CSV</span>|<span class="macro">FILE_ANSI</span>|<span class="macro">FILE_COMMON</span>);
&nbsp;&nbsp; <span class="keyword">class="type">int</span> ntrees =&nbsp;&nbsp;(<span class="keyword">class="type">int</span>)<span class="functions">FileReadNumber</span>(filehnd);
&nbsp;&nbsp; <span class="functions">FileClose</span>(filehnd);
&nbsp;&nbsp; <span class="keyword">if</span>(ntrees&gt;<span class="number">class="num">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;random_policy=<span class="macro">class="kw">false</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> setRDF=<span class="functions">FileOpen</span>("RDFBufsize"+<span class="predefines">_Symbol</span>+(<span class="keyword">class="type">class="kw">string</span>)<span class="predefines">_Period</span>+".txt",<span class="macro">FILE_READ</span>|<span class="macro">FILE_WRITE</span>|<span class="macro">FILE_CSV</span>|<span class="macro">FILE_ANSI</span>|<span class="macro">FILE_COMMON</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RDF.m_bufsize=(<span class="keyword">class="type">int</span>)<span class="functions">FileReadNumber</span>(setRDF);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">FileClose</span>(setRDF);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setRDF=<span class="functions">FileOpen</span>("RDFNclasses"+<span class="predefines">_Symbol</span>+(<span class="keyword">class="type">class="kw">string</span>)<span class="predefines">_Period</span>+".txt",<span class="macro">FILE_READ</span>|<span class="macro">FILE_WRITE</span>|<span class="macro">FILE_CSV</span>|<span class="macro">FILE_ANSI</span>|<span class="macro">FILE_COMMON</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RDF.m_nclasses=(<span class="keyword">class="type">int</span>)<span class="functions">FileReadNumber</span>(setRDF);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">FileClose</span>(setRDF);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;setRDF=<span class="functions">FileOpen</span>("RDFNtrees"+<span class="predefines">_Symbol</span>+(<span class="keyword">class="type">class="kw">string</span>)<span class="predefines">_Period</span>+".txt",<span class="macro">FILE_READ</span>|<span class="macro">FILE_WRITE</span>|<span class="macro">FILE_CSV</span>|<span class="macro">FILE_ANSI</span>|<span class="macro">FILE_COMMON</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RDF.m_ntrees=(<span class="keyword">class="type">int</span>)<span class="functions">FileReadNumber</span>(setRDF);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">FileClose</span>(setRDF);

◍ 随机森林模型的存取与回测落盘

在 EA 初始化阶段,若检测到本地已存在对应品种与周期的模型文件,会先读回变量数:从 RDFNvars+_Symbol+_Period.txt 中读取 m_nvars,再关闭句柄;随后以二进制模式打开 RDFMtrees 文件,用 FileReadArray 把树结构直接灌进 RDF.m_trees,省去重新训练的开销。 OnTester 只在优化模式下干活。当样本数大于 0 时调用 CDForest::DFBuildRandomDecisionForest,传入特征矩阵、样本数、3 个特征随机子集、1 类输出、树数量与正则项,构建随机决策森林。 构建完后把核心参数分文件落盘:m_bufsize、m_nclasses、m_ntrees、m_nvars 写进各自的 CSV 文本,m_trees 则以二进制写入 RDFMtrees 文件。这样下一次同品种同周期回测能直接加载,避免重复计算。 外汇与贵金属市场高杠杆、高波动,此类模型仅刻画历史样本中的统计关系,实盘信号失效的概率不低,加载旧模型前建议先核对样本区间。

MQL5 / C++
setRDF=FileOpen("RDFNvars"+_Symbol+(class="type">class="kw">string)_Period+".txt",FILE_READ|FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_COMMON);
RDF.m_nvars=(class="type">int)FileReadNumber(setRDF);
FileClose(setRDF);
setRDF=FileOpen("RDFMtrees"+_Symbol+(class="type">class="kw">string)_Period+".txt",FILE_READ|FILE_WRITE|FILE_BIN|FILE_ANSI|FILE_COMMON);
FileReadArray(setRDF,RDF.m_trees);
FileClose(setRDF);
 }
 else Print("Starting new learn");
 checked_for_learn=true;
}
class="type">class="kw">double OnTester()
 {
  if(clear_model) class="kw">return class="num">0;
  if(MQLInfoInteger(MQL_OPTIMIZATION)==true)
   {
    if(numberOfsamples>class="num">0)
     {
      CDForest::DFBuildRandomDecisionForest(RDFpolicyMatrix,numberOfsamples,class="num">3,class="num">1,number_of_trees,regularization,RDFinfo,RDF,RDF_report);
     }
    class="type">int filehnd=FileOpen("RDFBufsize"+_Symbol+(class="type">class="kw">string)_Period+".txt",FILE_READ|FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_COMMON);
    FileWrite(filehnd,RDF.m_bufsize);
    FileClose(filehnd);
    filehnd=FileOpen("RDFNclasses"+_Symbol+(class="type">class="kw">string)_Period+".txt",FILE_READ|FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_COMMON);
    FileWrite(filehnd,RDF.m_nclasses);
    FileClose(filehnd);
    filehnd=FileOpen("RDFNtrees"+_Symbol+(class="type">class="kw">string)_Period+".txt",FILE_READ|FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_COMMON);
    FileWrite(filehnd,RDF.m_ntrees);
    FileClose(filehnd);
    filehnd=FileOpen("RDFNvars"+_Symbol+(class="type">class="kw">string)_Period+".txt",FILE_READ|FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_COMMON);
    FileWrite(filehnd,RDF.m_nvars);
    FileClose(filehnd);
    filehnd=FileOpen("RDFMtrees"+_Symbol+(class="type">class="kw">string)_Period+".txt",FILE_READ|FILE_WRITE|FILE_BIN|FILE_ANSI|FILE_COMMON);
    FileWriteArray(filehnd,RDF.m_trees);
    FileClose(filehnd);
   }
  class="kw">return class="num">0;
 }
class="type">void updatePolicy(class="type">class="kw">double action)
 {
  if(MQLInfoInteger(MQL_OPTIMIZATION)==true)

「用强化学习思路改写持仓动作」

这段逻辑把每一笔样本塞进一个 4 列矩阵:前三列存三个特征数组的首值,第四列存动作标签,样本数随 numberOfsamples 递增动态扩容。 在优化器里跑时,若上一笔利润为负数,就把该样本的动作列改写为 [0,1) 均匀随机数,等于让亏损样本的策略权重被随机扰动,避免策略过早固化。 下单函数 PlaceOrders 以 ts 为信号阈值:持多单且 ts≥0.5 时先平仓并刷新奖励;若 ts>0.6 则反手开空,手数走 LotsOptimized(),成交成功后写入新策略样本。外汇与贵金属杠杆高,这类自学习开关在实盘可能频繁反转,务必先在 MT5 策略测试器用历史数据验证阈值稳定性。

MQL5 / C++
  {
      numberOfsamples++;
      RDFpolicyMatrix.Resize(numberOfsamples,class="num">4);
      RDFpolicyMatrix[numberOfsamples-class="num">1].Set(class="num">0,arr1[class="num">0]);
      RDFpolicyMatrix[numberOfsamples-class="num">1].Set(class="num">1,arr2[class="num">0]);
      RDFpolicyMatrix[numberOfsamples-class="num">1].Set(class="num">2,arr3[class="num">0]);
      RDFpolicyMatrix[numberOfsamples-class="num">1].Set(class="num">3,action);
   }
}\nvoid updateReward()
  {
   if(MQLInfoInteger(MQL_OPTIMIZATION)==true)
     {
       class="type">int unierr;
       if(getLAstProfit()<class="num">0) RDFpolicyMatrix[numberOfsamples-class="num">1].Set(class="num">3,MathRandomUniform(class="num">0,class="num">1,unierr));
     }
  }
class="type">void PlaceOrders(class="type">class="kw">double ts)
  {
   if(CountOrders(class="num">0)!=class="num">0 || CountOrders(class="num">1)!=class="num">0)
     {
       for(class="type">int b=OrdersTotal()-class="num">1; b>=class="num">0; b--)
        if(OrderSelect(b,SELECT_BY_POS)==true)
          if(OrderSymbol()==_Symbol && OrderMagicNumber()==OrderMagic)
            class="kw">switch(OrderType())
             {
               case OP_BUY:
                if(ts>=class="num">0.5)
                 if(OrderClose(OrderTicket(),OrderLots(),OrderClosePrice(),class="num">0,Red))
                  {
                   updateReward();
                   if(ts>class="num">0.6)
                    {
                     lots=LotsOptimized();
                     if(OrderSend(Symbol(),OP_SELL,lots,SymbolInfoDouble(_Symbol,SYMBOL_BID),class="num">0,class="num">0,class="num">0,NULL,OrderMagic,Red)>class="num">0)
                      {
                       updatePolicy(ts);
                      };
                    }
                  }
                class="kw">break;
让小布替你跑这套样本切分
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到随机森林类的信号置信分布,把重复劳动交给小布,你专注决策。

常见问题

有放回抽取同等数量对象时,单轮未被抽中的概率约为 37%,因此约 63% 原始样本会进入训练,剩余用作袋外测试评估泛化能力。
代理以持仓状态为环境反馈,用随机森林近似价值函数或策略概率,每步依据树投票结果选择开平动作,属于模型无关的近似方案。
可以,小布盯盘品种页已内置信号置信与样本外衰减视图,你粘贴 EA 输出日志就能看到森林投票的稳定性变化。
经验上回归取 n/3 个特征,分类取 √n 个,这种去相关设计让不同树看到不同属性子集,降低整体方差。
外汇贵金属属高风险市场,单树不剪枝易记噪声,靠多树投票缓解但突变行情仍可能失效,信号仅作概率参考。