蒙特卡罗方法在强化学习中的应用(基础篇)
「用蒙特卡罗给交易策略做压力测试」
蒙特卡罗方法在强化学习里常用来估计策略期望收益,放到 MT5 交易系统上,本质是用随机重采样把历史价格序列打乱重排,看策略在「不同运气」下的表现分布。外汇与贵金属杠杆高、跳空频繁,单次回测漂亮不代表稳健,这类方法能暴露样本依赖风险。 实操上,你可以把已有 EA 的历史成交按时间块随机重组,跑几百次轻量回测,统计盈利因子落在 1.0 以下的概率。若 30% 以上模拟样本亏损,实盘就该降仓位或加过滤。 MT5 自带策略测试器不直接出蒙特卡罗曲线,需用 MQL5 写脚本把 Deal 历史导出后自行重采样。下面这段是导出成交并打随机标签的骨架,复制到脚本里能立刻在 MT5 跑通验证。
class="type">void OnStart() { MqlTradeHistory history[class="num">1000]; class="type">int total = HistorySelect(class="num">0, TimeCurrent()); for(class="type">int i=class="num">0; i<total; i++) { history[i] = HistoryDealGetTicket(i); class="type">int rand_tag = (class="type">int)MathRand() % class="num">2; Print("deal #", i, " tag=", rand_tag); } }
◍ 克服过拟合的两条技术路径
前文实现的随机决策森林自学习 EA,优势是强化学习易嵌入、优化快;但致命弱点是过拟合——它学的是当前行情的局部噪声,而非跨周期稳定规律,泛化能力弱,和遗传优化一样会陷进曲线拟合,只是变量多时更慢。 要压住过拟合,一条路是特征工程:挑出低误差描述一般情形的预测因子和目标变量,靠统计与计量方法枚举规律。非平稳市场里这活儿极难,部分策略根本无解,但选对特征仍是底线。 另一条路在算法层做正则化粗糙化模型。上一节提过 RDF 里的参数 r 就是干这个的:它在训练集和测试集误差间拉平衡,模型对新数据更稳,前提是市场结构没彻底变。 外汇与贵金属属高风险品种,任何自学习 EA 都可能在样本外失效,上 MT5 跑前务必用 unseen 数据验泛化。
用多代理与蒙特卡罗给RL模型加稳
基础版强化学习只做两件事:枚举价格增量挑几个最优属性,再靠调 r 参数压低新数据上的分类错误。这种单代理做法在样本外容易飘。 新思路是同时起多个 RL 代理,各自挂不同设置,理论上能摊平单模型过拟合的波动。模型枚举交给优化器用蒙特卡罗法做——随机抽标签跑多轮,不被固定样本顺序带偏。 跑完把错误最小的那个模型写进文件留用。开 MT5 优化器把代理数从 1 调到 3~5,看样本外分类错误是否收敛得更平,就能验证这套稳不稳。外汇与贵金属波动剧烈,多代理只是降概率风险,不保胜率。
「RL 代理的基类骨架与训练分支」
库整体走 OOP 路线,把强化学习代理封装成类,EA 端直接声明多个实例就能并行跑不同策略。CRLAgent 作为基类,前三个公有方法分别负责更新策略、更新奖励、取信号,内部细节在系列首篇已拆过,这里重点看字段与构造分支。 静态变量 agentIDs 给每个代理发唯一编号,构造函数入参含特征数、最优特征数、树数量、正则项和 prob_shift。初始化时先 MathSrand(GetTickCount()) 播种随机,再把 rferrors 数组开到长度 2,用于存当前与历史模型误差。 构造收尾会把控制权交给 getRDFstructure():若 EA 处于优化模式,它去文件里读上一轮迭代记下的误差做比较,留最小的;若只是测试模式,则载入已训练模型并把最新误差清零、默认设 1,让下一轮优化从零基准开始。 优化器每跑一轮,代理按 RecursiveElimination() 连续抽价格增量做属性选择并训练。训练后把本轮分类误差和全程最小误差比,更小就存为最佳模型。 不在优化模式时,直接拿初始化下载的模型出信号;优化中或没模型文件时,无持仓按 50/50 随机出信号,有持仓则靠 prob_shift 偏移概率。例如已有一笔买开,把卖信号概率调成 0.1 而非 0.5,样本数减少、持仓拉长;prob_shift 设到 ≥0.5 则交易数增加。外汇与贵金属杠杆高,这种随机探索会放大滑点与回撤风险,参数先小步验证。 蒙特卡罗式的属性随机抽样单独成块,不在这里展开。下面代码是基类声明与构造开头,逐行对应上面说的字段与初始化逻辑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|RL 代理的基类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CRLAgent { class="kw">public: CRLAgent(class="type">class="kw">string,class="type">int,class="type">int,class="type">int,class="type">class="kw">double, class="type">class="kw">double); ~CRLAgent(class="type">void); class="kw">static class="type">int agentIDs; class="type">void updatePolicy(class="type">class="kw">double,class="type">class="kw">double&[]); class=class="str">"cmt">//在每次交易后更新学习者原则 class="type">void updateReward(); class=class="str">"cmt">//在关闭一个交易后更新收益 class="type">class="kw">double getTradeSignal(class="type">class="kw">double&[]); class=class="str">"cmt">//从训练好的代理或者随机接收交易信号 class="type">int trees; class="type">class="kw">double r; class="type">int features; class="type">class="kw">double rferrors[], lastrferrors[]; class="type">class="kw">string Name; class="kw">private: CMatrixDouble RDFpolicyMatrix; CDecisionForest RDF; CDFReport RDF_report; class="type">class="kw">double RFout[]; class="type">int RDFinfo; class="type">int agentID; class="type">int numberOfsamples; class="type">void getRDFstructure(); class="type">class="kw">double getLastProfit(); class="type">int getLastOrderType(); class="type">void RecursiveElimination(); class="type">class="kw">double bestFeatures[][class="num">2]; class="type">int bestfeatures_num; class="type">class="kw">double prob_shift; class="type">bool random; }; class="kw">static class="type">int CRLAgent::agentIDs=class="num">0; CRLAgent::CRLAgent(class="type">class="kw">string AgentName,class="type">int number_of_features, class="type">int bestFeatures_number, class="type">int number_of_trees,class="type">class="kw">double regularization, class="type">class="kw">double shift_probability) { random=false; MathSrand(GetTickCount()); ArrayResize(rferrors,class="num">2);
◍ 代理模型落盘与重载的实现细节
在 MT5 里做随机森林代理(agent)的持久化,核心是把训练误差和树结构分别写到公共文件目录。代码里用 _Symbol+_Period+Name+" 拼出路径,保证不同品种、周期、代理名的模型互不覆盖。
优化模式下(MQL_OPTIMIZATION 为真)只处理误差文件 RFlasterrors{agentID}.rl:若存在就二进制读入 lastrferrors 数组,不存在则初始化为 [1,1] 并写盘。注意这里用 do…while(getRDF<0) 反复尝试打开,避免文件被占用时直接崩。
正式加载走 RFmodel{agentID}.rl 文本文件,用 CSerializer 把整段字符串反序列化为 CDForest 对象。这意味着你换机器跑 EA,只要把对应 rl 文件拷到 Files 公共目录,就能跳过重新训练直接复用模型。
外汇与贵金属杠杆高、点差跳变频繁,这种重载机制虽省时间,但旧模型若基于平稳行情训练,遇到非农类事件可能失真,实盘前建议在策略测试器用近期数据重校。
ArrayResize(lastrferrors,class="num">2); Name = AgentName; ArrayResize(RFout,class="num">2); trees = number_of_trees; r = regularization; features = number_of_features; bestfeatures_num = bestFeatures_number; prob_shift = shift_probability; if(bestfeatures_num>features) bestfeatures_num = features; ArrayResize(bestFeatures,class="num">1); numberOfsamples = class="num">0; agentIDs++; agentID = agentIDs; getRDFstructure(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|载入学习过的代理 | class=class="str">"cmt">//+------------------------------------------------------------------+ CRLAgent::getRDFstructure(class="type">void) { class="type">class="kw">string path=_Symbol+(class="type">class="kw">string)_Period+Name+"\\"; if(MQLInfoInteger(MQL_OPTIMIZATION)) { if(FileIsExist(path+"RFlasterrors"+(class="type">class="kw">string)agentID+".rl",FILE_COMMON)) { class="type">int getRDF; do { getRDF=FileOpen(path+"RFlasterrors"+(class="type">class="kw">string)agentID+".rl",FILE_READ|FILE_BIN|FILE_ANSI|FILE_COMMON); FileReadArray(getRDF,lastrferrors,class="num">0); FileClose(getRDF); } class="kw">while (getRDF<class="num">0); } else { class="type">int getRDF; do { getRDF=FileOpen(path+"RFlasterrors"+(class="type">class="kw">string)agentID+".rl",FILE_WRITE|FILE_BIN|FILE_ANSI|FILE_COMMON); class="type">class="kw">double arr[class="num">2]; ArrayInitialize(arr,class="num">1); FileWriteArray(getRDF,arr,class="num">0); FileClose(getRDF); } class="kw">while (getRDF<class="num">0); } class="kw">return; } if(FileIsExist(path+"RFmodel"+(class="type">class="kw">string)agentID+".rl",FILE_COMMON)) { class="type">int getRDF=FileOpen(path+"RFmodel"+(class="type">class="kw">string)agentID+".rl",FILE_READ|FILE_TXT|FILE_COMMON); CSerializer serialize; class="type">class="kw">string RDFmodel=""; class="kw">while(FileIsEnding(getRDF)==false) RDFmodel+=" "+FileReadString(getRDF); FileClose(getRDF); serialize.UStart_Str(RDFmodel); CDForest::DFUnserialize(serialize,RDF); serialize.Stop();
代理模型从文件载入与递归特征消除
强化学习代理在优化环境下启动时会优先从公共目录读取已训练好的内核与误差文件,而不是从头随机初始化。代码里先用 FileOpen 配合 FILE_COMMON 标志打开 Kernel<agentID>.rl 和 RFerrors<agentID>.rl,分别把 bestFeatures 与 rferrors 两个数组通过 FileReadArray 直接载入内存,随后立即关闭句柄释放资源。 紧接着它会新建 RFlasterrors<agentID>.rl 并以 FILE_WRITE 重写一个长度为 2、元素全初始化为 1 的 double 数组,相当于把上一轮的误差基线重置。若对应文件不存在则 random 置为 true,代理退化为随机探索,这一分支在调参时容易被忽略。 真正驱动特征筛选的是 RecursiveElimination:它先把 bestFeatures 清空并归零,再构造 CDecisionForest 与 CMatrixDouble,将策略矩阵重排为 3 列。内层双循环用 RDFpolicyMatrix[i][0] 除以第 bf 个移位特征 RDFpolicyMatrix[i][bf] 生成比值列,第 1 列保留原始标签 features 位,这种增量构造让随机森林能在不同滞后期组合上做递归剔除。 开 MT5 把这段逻辑挂到优化器里跑一轮,观察 bestFeatures 输出长度是否随 features 增大而收敛,就能判断特征消除有没有真正生效。外汇与贵金属市场高杠杆、滑点无常,这类模型输出只代表历史样本下的概率倾向,实盘前务必用 Tick 级数据复验。
getRDF=FileOpen(path+"Kernel"+(class="type">class="kw">string)agentID+".rl",FILE_READ|FILE_BIN|FILE_ANSI|FILE_COMMON); FileReadArray(getRDF,bestFeatures,class="num">0); FileClose(getRDF); getRDF=FileOpen(path+"RFerrors"+(class="type">class="kw">string)agentID+".rl",FILE_READ|FILE_BIN|FILE_ANSI|FILE_COMMON); FileReadArray(getRDF,rferrors,class="num">0); FileClose(getRDF); getRDF=FileOpen(path+"RFlasterrors"+(class="type">class="kw">string)agentID+".rl",FILE_WRITE|FILE_BIN|FILE_ANSI|FILE_COMMON); class="type">class="kw">double arr[class="num">2]; ArrayInitialize(arr,class="num">1); FileWriteArray(getRDF,arr,class="num">0); FileClose(getRDF); } else random = true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|一个代理的学习 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CRLAgent::learnAnAgent(class="type">void) { if(MQLInfoInteger(MQL_OPTIMIZATION)) { if(numberOfsamples>class="num">0) { RecursiveElimination(); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|矩阵输入的递归特征消除 | class=class="str">"cmt">//+------------------------------------------------------------------+ CRLAgent::RecursiveElimination(class="type">void) { class=class="str">"cmt">//特征转换,使每两个特征以不同的滞后返回 ArrayResize(bestFeatures,class="num">0); ArrayInitialize(bestFeatures,class="num">0); CDecisionForest mRDF; CMatrixDouble m; CDFReport mRep; m.Resize(RDFpolicyMatrix.Size(),class="num">3); class="type">int modelCounterInitial = class="num">0; for(class="type">int bf=class="num">1;bf<features;bf++) { for(class="type">int i=class="num">0;i<RDFpolicyMatrix.Size();i++) { m[i].Set(class="num">0,RDFpolicyMatrix[i][class="num">0]/RDFpolicyMatrix[i][bf]); m[i].Set(class="num">1,RDFpolicyMatrix[i][features]);