强化学习中的随机决策森林·进阶篇
(2/3)·从 bootstrap 聚合到自主学习 EA,随机森林在交易系统里到底怎么跑通
「用随机森林当交易代理的大脑」
把 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 就在新矩阵上训并写文件。 订单开口后把状态向量塞进矩阵、用模糊推理结果填输出;订单平仓时若亏损,代理报酬从随机均匀分布里抽,逼它换动作搜最优解,盈利则报酬不动。下面这段是模型检查与载入的核心代码。
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 RDF; <span class="comment">class=class="str">"cmt">//随机森林对象</span> CMatrixDouble RDFpolicyMatrix; <span class="comment">class=class="str">"cmt">//用于随机森林输入和输出的矩阵</span> CDFReport RDF_report; <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() { <span class="keyword">if</span>(clear_model) { <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>); <span class="functions">FileWrite</span>(clearRDF,<span class="number">class="num">0</span>); <span class="functions">FileClose</span>(clearRDF); <span class="functions">ExpertRemove</span>(); <span class="keyword">class="kw">return</span>; } <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>); <span class="keyword">class="type">int</span> ntrees = (<span class="keyword">class="type">int</span>)<span class="functions">FileReadNumber</span>(filehnd); <span class="functions">FileClose</span>(filehnd); <span class="keyword">if</span>(ntrees><span class="number">class="num">0</span>) { random_policy=<span class="macro">class="kw">false</span>; <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>); RDF.m_bufsize=(<span class="keyword">class="type">int</span>)<span class="functions">FileReadNumber</span>(setRDF); <span class="functions">FileClose</span>(setRDF); 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>); RDF.m_nclasses=(<span class="keyword">class="type">int</span>)<span class="functions">FileReadNumber</span>(setRDF); <span class="functions">FileClose</span>(setRDF); 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>); RDF.m_ntrees=(<span class="keyword">class="type">int</span>)<span class="functions">FileReadNumber</span>(setRDF); <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 文件。这样下一次同品种同周期回测能直接加载,避免重复计算。 外汇与贵金属市场高杠杆、高波动,此类模型仅刻画历史样本中的统计关系,实盘信号失效的概率不低,加载旧模型前建议先核对样本区间。
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 策略测试器用历史数据验证阈值稳定性。
{
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;