蒙特卡罗方法在强化学习中的应用·进阶篇
📘

蒙特卡罗方法在强化学习中的应用·进阶篇

第 2/3 篇

「递归特征消除后的森林重训」

递归消除阶段先把每个候选延迟增量单独喂给随机森林,用袋外相对分类错误(oobrelclserror)打分,再按错误率升序排列,只截留 bestfeatures_num 个最优属性。这一步直接决定后续模型看的是哪几个价格增量,而不是一股脑塞进全部特征。 重填矩阵时,每个样本的第 l 列写成 RDFpolicyMatrix[i][0] 除以该最佳特征对应的原始增量值,等于把绝对价格差归一成相对比率,能削弱不同品种量纲差异带来的偏差。最后调用 DFBuildRandomDecisionForest 以筛选后的列数作预测器维度,训练出正式用于决策的森林 RDF。 在智能体学习函数里,当且仅当处于 MT5 优化器环境且样本数大于 0 才跑 RecursiveElimination;若新森林的 oobrelclserror 低于上一代记录 lastrferrors[1],就把模型序列化落盘。外汇与贵金属杠杆高,这种基于历史增量的模型仅代表样本内概率倾向,实盘前务必在策略测试器用不同品种周期复验。

MQL5 / C++
m[i].Set(class="num">2,RDFpolicyMatrix[i][features+class="num">1]);
 }
 CDForest::DFBuildRandomDecisionForest(m,RDFpolicyMatrix.Size(),class="num">1,class="num">2,trees,r,RDFinfo,mRDF,mRep);	class=class="str">"cmt">//训练一个随机森林,其中只使用选定的增量作为预测器
 ArrayResize(bestFeatures,ArrayRange(bestFeatures,class="num">0)+class="num">1);
 bestFeatures[modelCounterInitial][class="num">0] = mRep.m_oobrelclserror;	                                class=class="str">"cmt">//保存 oob 集合上的错误
 bestFeatures[modelCounterInitial][class="num">1] = bf;	                                                class=class="str">"cmt">//保存增量&class="macro">#x27;延迟&class="macro">#x27;
 modelCounterInitial++;
 }

 ArraySort(bestFeatures);	                                                                             class=class="str">"cmt">//数组排序 (根据第0维), 这里也就是根据错误 oob
 ArrayResize(bestFeatures,bestfeatures_num);                                                           class=class="str">"cmt">// 只保留最佳的 bestfeatures_num 属性

 m.Resize(RDFpolicyMatrix.Size(),class="num">2+ArrayRange(bestFeatures,class="num">0));

 for(class="type">int i=class="num">0;i<RDFpolicyMatrix.Size();i++) {                                                           class=class="str">"cmt">// 再次填充矩阵,但是这一次使用最佳属性
  for(class="type">int l=class="num">0;l<ArrayRange(bestFeatures,class="num">0);l++)
   {
    m[i].Set(l,RDFpolicyMatrix[i][class="num">0]/RDFpolicyMatrix[i][(class="type">int)bestFeatures[l][class="num">1]]);
   }
   m[i].Set(ArrayRange(bestFeatures,class="num">0),RDFpolicyMatrix[i][features]);
   m[i].Set(ArrayRange(bestFeatures,class="num">0)+class="num">1,RDFpolicyMatrix[i][features+class="num">1]);
  }

 CDForest::DFBuildRandomDecisionForest(m,RDFpolicyMatrix.Size(),ArrayRange(bestFeatures,class="num">0),class="num">2,trees,r,RDFinfo,RDF,RDF_report); class=class="str">"cmt">// 根据选定的最佳属性训练随机森林

模型落盘与信号提取的底层写法

这段逻辑干了两件事:把随机森林训练产物写进公共目录文件,以及在非优化环境下用核特征比值去推信号。写盘用了 do-while 重试,只要 FileOpen 返回负值就 continue,直到四个 .rl 文件全部成功打开并关闭,避免多 agent 并发时文件句柄抢不到导致静默丢模型。 落盘内容里,RFlasterrors 和 RFerrors 都是二进制双精度数组,下标 0 存 m_relclserror、下标 1 存 m_oobrelclserror;RFmodel 则是文本化的序列化字符串,方便人工排查树结构。Kernel 文件直接 dump bestFeatures 数组,记录被选中的特征索引,后续 getTradeSignal 靠它重建核特征。 信号函数里有个关键判断:若不是优化器跑参且 random 为假,才走真实推理。核特征构造方式是 featuresValues[0] 除以按 bestFeatures 映射的各个特征,等于把首特征当分母做相对比率。外汇与贵金属市场高杠杆、滑点随机,这套相对误差模型给出的 0.5 基线信号仅反映历史样本倾向,实盘胜率可能随品种流动性漂移。 调用 DForest::DFProcess 拿到 RFout 后,函数最终返回 1 减袋外相对误差,作为该 agent 的拟合质量参考。开 MT5 把这段贴进 EA,把 agentID 换成不同魔术号,就能在公共文件区看到每 agent 独立的 .rl 族,验证是否真写了盘。

MQL5 / C++
do {
 setRDF=FileOpen(path+"RFlasterrors"+(class="type">class="kw">string)agentID+".rl",FILE_WRITE|FILE_BIN|FILE_ANSI|FILE_COMMON);
 if(setRDF<class="num">0) class="kw">continue;
 lastrferrors[class="num">0]=RDF_report.m_relclserror;
 lastrferrors[class="num">1]=RDF_report.m_oobrelclserror;
 FileWriteArray(setRDF,lastrferrors,class="num">0);
 FileClose(setRDF);
 
 setRDF=FileOpen(path+"RFmodel"+(class="type">class="kw">string)agentID+".rl",FILE_WRITE|FILE_TXT|FILE_COMMON);
 FileWrite(setRDF,serialize.Get_String());
 FileClose(setRDF);
 
 setRDF=FileOpen(path+"RFerrors"+(class="type">class="kw">string)agentID+".rl",FILE_WRITE|FILE_BIN|FILE_ANSI|FILE_COMMON);
 rferrors[class="num">0]=RDF_report.m_relclserror;
 rferrors[class="num">1]=RDF_report.m_oobrelclserror;
 FileWriteArray(setRDF,rferrors,class="num">0);
 FileClose(setRDF);
 
 setRDF=FileOpen(path+"Kernel"+(class="type">class="kw">string)agentID+".rl",FILE_WRITE|FILE_BIN|FILE_ANSI|FILE_COMMON);
 FileWriteArray(setRDF,bestFeatures);
 FileClose(setRDF);
 }
 class="kw">while(setRDF<class="num">0);
 }
 }
 }
 class="kw">return class="num">1-RDF_report.m_oobrelclserror;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 取得交易信号                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CRLAgent::getTradeSignal(class="type">class="kw">double &featuresValues[]) {
 class="type">class="kw">double res=class="num">0.5;
 if(!MQLInfoInteger(MQL_OPTIMIZATION) && !random) {
 class="type">class="kw">double kerfeatures[];
 ArrayResize(kerfeatures,ArrayRange(bestFeatures,class="num">0));
 ArrayInitialize(kerfeatures,class="num">0);
 
 for(class="type">int i=class="num">0;i<ArraySize(kerfeatures);i++) {
 kerfeatures[i] = featuresValues[class="num">0]/featuresValues[(class="type">int)bestFeatures[i][class="num">1]];
 }
 CDForest::DFProcess(RDF,kerfeatures,RFout);

◍ 无持仓时的随机开仓与偏移概率

这段逻辑处理的是 EA 在「没有任何挂单或成交单」状态下的入场决策,核心是用 rand() 生成的伪随机数做 50% 双向掷币。 rand() 返回 0–32767 整数,除以 32767.0 得到 [0,1] 区间浮点;小于 0.5 返回 0(倾向做空),否则返回 1(倾向做多),纯随机不依赖任何价格行为信号。 若已有持仓,则引入 prob_shift 做概率偏移:当前有多单时,随机数大于 prob_shift 才返回 0,相当于压低反手概率;当前有空单时,随机数小于 prob_shift 才返回 0,相当于抬高反手概率。把 prob_shift 从 0.5 调到 0.2,可在回测中观察到反手频率明显下降。 外汇与贵金属属高风险品种,随机开仓策略实盘可能快速放大回撤,仅建议在 MT5 策略测试器内验证。

MQL5 / C++
   class="kw">return RFout[class="num">1];
   }
  else {
   if(countOrders()==class="num">0) if(rand()/class="num">32767.0<class="num">0.5) res = class="num">0; else res = class="num">1;
   else {
     if(countOrders(class="num">0)!=class="num">0) if(rand()/class="num">32767.0>prob_shift) res = class="num">0; else res = class="num">1;
     if(countOrders(class="num">1)!=class="num">0) if(rand()/class="num">32767.0<prob_shift) res = class="num">0; else res = class="num">1;
   }
   class="kw">return res;
}

「用 CRLAgents 把同质学习者捆成一组」

想在同一套交易逻辑里跑一批参数相近的强化学习代理,逐个管理太碎。CRLAgents 类就是用来托管同质学习者群体的:它内部用 Agents 结构数组装每个实例,数组长度即工人数量,单个代理也能直接套这个类,不亏。 构造函数一口气吃进 7 个参数:组名、工人数量、每工人属性数、最优属性数、森林树数、规范化系数、管理交易量的概率偏移。初始化时按 agentsQuantity 扩数组,给每个 inpVector 按 features 扩并置 0,再 new 出 CRLAgent,顺手把 rferrors[0]、rferrors[1] 拷到 rms、oob 字段。 learnAllAgents 是批量学习入口,遍历 agent 数组调各自 learnAnAgent,把返回值累加后除以数组大小,吐出测试集平均错误。蒙特卡罗迭代时,这个均值能当自定义优化标准,用来在 tester 里看错误扩散的散点。 同子组的学习者设置默认一致,setAgentSettings 可以单独拧某个工人的特征数、最优特征数、树数和正则项。交易信号不走单个代理,而是取子组平均——getTradeSignal 返回的就是这组人的均值信号,噪声比单兵低,但滞后可能略大。外汇与贵金属杠杆高,这类群体信号只作概率参考,实盘前务必在 MT5 策略测试器跑通。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|多 RL 代理类                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CRLAgents
  {
class="kw">private:
   class="kw">struct Agents
     {
       class="type">class="kw">double              inpVector[];
       CRLAgent            *ag;
       class="type">class="kw">double              rms;
       class="type">class="kw">double              oob;
     };
   class="type">void                getStatistics();
   class="type">class="kw">string              groupName;
class="kw">public:
                     CRLAgents(class="type">class="kw">string,class="type">int,class="type">int,class="type">int,class="type">int,class="type">class="kw">double,class="type">class="kw">double);
                    ~CRLAgents(class="type">void);
   Agents              agent[];
   class="type">void                updatePolicies(class="type">class="kw">double);
   class="type">void                updateRewards();
   class="type">class="kw">double              getTradeSignal();
   class="type">class="kw">double              learnAllAgents();
   class="type">void                setAgentSettings(class="type">int,class="type">int,class="type">int,class="type">class="kw">double);
  };
CRLAgents::CRLAgents(class="type">class="kw">string AgentsName,class="type">int agentsQuantity,class="type">int features, class="type">int bestfeatures, class="type">int treesNumber,class="type">class="kw">double regularization, class="type">class="kw">double shift_probability)
  {
   groupName=AgentsName;
   ArrayResize(agent,agentsQuantity);
   for(class="type">int i=class="num">0;i<agentsQuantity;i++) {
     ArrayResize(agent[i].inpVector,features);
     ArrayInitialize(agent[i].inpVector,class="num">0);
     agent[i].ag  = new CRLAgent(AgentsName, features, bestfeatures, treesNumber, regularization, shift_probability);
     agent[i].rms = agent[i].ag.rferrors[class="num">0];
     agent[i].oob = agent[i].ag.rferrors[class="num">1];
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|所有代理的学习                                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CRLAgents::learnAllAgents(class="type">void){
   class="type">class="kw">double err=class="num">0;
   for(class="type">int i=class="num">0;i<ArraySize(agent);i++)
     err+=agent[i].ag.learnAnAgent();
  class="kw">return err/ArraySize(agent);
}
class=class="str">"cmt">//+------------------------------------------------------------------+

代理参数配置与信号聚合的实现细节

在强化学习代理组里,单个代理的超参数必须显式写入才能参与训练与推理。下面这段 setter 把特征数、最优特征数、树数量、正则项与位移概率一次性塞进 agent 数组对应槽位,并顺手把输入向量数组按特征数重置清零。 CRLAgents::setAgentSettings(int agentNumber,int features,int bestfeatures,int treesNumber,double regularization,double shift_probability) { agent[agentNumber].ag.features=features; agent[agentNumber].ag.bestfeatures_num=bestfeatures; agent[agentNumber].ag.trees=treesNumber; agent[agentNumber].ag.r=regularization; agent[agentNumber].ag.prob_shift=shift_probability; ArrayResize(agent[agentNumber].inpVector,features); ArrayInitialize(agent[agentNumber].inpVector,0); } 逐行看:前五行把入参赋给指定编号代理的结构体字段;ArrayResize 按 features 长度重开输入向量内存,避免上一轮维度残留;ArrayInitialize 全填 0,保证冷启动时不会读到垃圾值。 信号层做的是均值融合。getTradeSignal 先按代理数扩信号数组,循环里每个代理用自己的 inpVector 算本地信号并累加,最后除以代理总数返回平均值为 group 信号。 double CRLAgents::getTradeSignal() { double signal[]; double sig=0; ArrayResize(signal,ArraySize(agent)); for(int i=0;i<ArraySize(agent);i++) sig+=signal[i]=agent[i].ag.getTradeSignal(agent[i].inpVector); return sig/(double)ArraySize(agent); } 这里若代理规模为 10,sig 就是 10 个本地信号求和后再乘 0.1;任何单代理极端值会被群体稀释,方向倾向更平滑。 训练诊断靠 getStatistics 把每个代理的 rms(训练误差)与 oob(袋外误差)拉出来打印。外汇与贵金属波动率高,oob 若持续高于 rms 两倍以上,说明代理过拟合历史样本,实盘信号失真概率偏大,需回 MT5 重调 features 或 regularization。 void CRLAgents::getStatistics(void) { double arr[]; double arrrms[]; ArrayResize(arr,ArraySize(agent)); ArrayResize(arrrms,ArraySize(agent)); for(int i=0;i<ArraySize(agent);i++) { arrrms[i]=agent[i].rms; arr[i]=agent[i].oob; } Print(groupName+" TRAIN LOSS"); ArrayPrint(arrrms); Print(groupName+" OOB LOSS"); ArrayPrint(arr); } 直接在策略测试器日志里搜 TRAIN LOSS / OOB LOSS 两段矩阵,就能判断这组代理该不该上真仓。

MQL5 / C++
CRLAgents::setAgentSettings(class="type">int agentNumber,class="type">int features,class="type">int bestfeatures,class="type">int treesNumber,class="type">class="kw">double regularization,class="type">class="kw">double shift_probability) {
  agent[agentNumber].ag.features=features;
  agent[agentNumber].ag.bestfeatures_num=bestfeatures;
  agent[agentNumber].ag.trees=treesNumber;
  agent[agentNumber].ag.r=regularization;
  agent[agentNumber].ag.prob_shift=shift_probability;
  ArrayResize(agent[agentNumber].inpVector,features);
  ArrayInitialize(agent[agentNumber].inpVector,class="num">0);
}
class="type">class="kw">double CRLAgents::getTradeSignal() {
  class="type">class="kw">double signal[];
  class="type">class="kw">double sig=class="num">0;
  ArrayResize(signal,ArraySize(agent));
  for(class="type">int i=class="num">0;i<ArraySize(agent);i++)
    sig+=signal[i]=agent[i].ag.getTradeSignal(agent[i].inpVector);
  class="kw">return sig/(class="type">class="kw">double)ArraySize(agent);
}
class="type">void CRLAgents::getStatistics(class="type">void)
  {
  class="type">class="kw">double arr[];
  class="type">class="kw">double arrrms[];
  ArrayResize(arr,ArraySize(agent));
  ArrayResize(arrrms,ArraySize(agent));
  for(class="type">int i=class="num">0;i<ArraySize(agent);i++) {
    arrrms[i]=agent[i].rms;
    arr[i]=agent[i].oob;
   }
  Print(groupName+" TRAIN LOSS");
  ArrayPrint(arrrms);
  Print(groupName+" OOB LOSS");
  ArrayPrint(arr);
}

◍ 单代理 RL 蒙特卡洛 EA 的最小实现

要验证这套强化学习蒙特卡洛开发库是否真能跑出策略,最直白的办法是写个只建一个代理的 EA。代理吃品种收盘价做训练,入场出场由工人随机选,所以策略优劣高度依赖优化器多跑几遍。 代码里 number_of_passes 决定终端优化器的传递次数,设得越高,摸到最优策略的概率倾向越大;RLMonteCarlo 组里只建 1 个代理,塞 500 个属性、挑 5 个最佳,模型用 50 棵决策树,训练/测试切分比例 r=0.6,无概率偏移。 OnTester 里把全体学习者的测试样本平均误差作为自定义优化标准回传;去初始化时 delete 代理释放内存。预测向量直接取最近 500 根收盘价,按序列方式排布——模型拿零元素与其他元素的滞后比值当预测因子。 threshold 参数是后加的硬门槛:买信号概率低于 0.6 就不开仓。外汇与贵金属杠杆高、滑点跳空频繁,这种概率型 EA 实盘前务必在 MT5 策略测试器用历史数据回测,亏损可能随时发生。

MQL5 / C++
class="macro">#include <RL Monte Carlo.mqh>
class="kw">input class="type">int      number_of_passes = class="num">10;
class="kw">input class="type">class="kw">double   shift_probab = class="num">0,class="num">5;
class="kw">input class="type">class="kw">double   regularize=class="num">0.6;
sinput class="type">int     number_of_best_features = class="num">5;
sinput class="type">class="kw">double  treshhold = class="num">0.5;
sinput class="type">class="kw">double  MaximumRisk=class="num">0.01;
sinput class="type">class="kw">double  CustomLot=class="num">0;
CRLAgents *ag1=new CRLAgents("RlMonteCarlo",class="num">1,class="num">500,number_of_best_features,class="num">50,regularize,shift_probab);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA 交易 OnTester 函数                                                  | 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double OnTester() {
   if(MQLInfoInteger(MQL_OPTIMIZATION)) class="kw">return ag1.learnAllAgents();
   else class="kw">return NULL;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA 交易去初始化函数                                                    | 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason) {
   class="kw">delete ag1;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 计算 Tsignal                                                          | 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void calcTsignal() {
   Tsignal=class="num">0;
   for(class="type">int i=class="num">0;i<ArraySize(ag1.agent);i++) {
      CopyClose(_Symbol,class="num">0,class="num">1,ArraySize(ag1.agent[i].inpVector),ag1.agent[i].inpVector);
      ArraySetAsSeries(ag1.agent[i].inpVector,true);
    }
   Tsignal=ag1.getTradeSignal();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置订单                                                              | 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void placeOrders() {
   for(class="type">int b=OrdersTotal()-class="num">1; b>=class="num">0; b--)
   if(OrderSelect(b,SELECT_BY_POS)==true) {

常见问题

要重训。用筛选后的特征子集在相同样本上重建森林并落盘,否则线上信号会基于旧特征分布漂移。
在开仓逻辑里显式设定偏移概率阈值,只在随机数低于阈值时触发,避免无谓交易磨损手续费。
可以。小布能把多个同质代理的产出按配置聚合成一组视图,省去你手动比对各代理信号的麻烦。
写在代理配置结构与信号聚合函数里,分组信息随代理参数一并传入,聚合时按组加权求和。
至少含环境交互、蒙特卡洛回报估计、策略更新与信号提取四块,缺一则无法形成闭环交易逻辑。