MQL5 中的范畴论 (第 16 部分):多层感知器函子·进阶篇
(2/3)· 从尾随止损到信号生成,把函子推进到神经网络架构的实战边界
「把标普500读数塞进范畴论的框里」
在信号文件里,把标普500指数值画成图,本质上就是搭了一个范畴:图上的每个数据点是一个对象,点与点之间的连线是态射。一个对象除了我们盯着的数值,还带着发布日期、预期共识、MT5日历里的其他字段——这些在财经日历范畴里共同组成一个集合对象。 想用函子去预处理历史财经日历,策略测试器本身连不上 MetaQuotes 服务器,只能走第三方落地。我们的做法是先用脚本把数据导成 csv,再在测试器里读回来;这次读的对象是智能信号类实例,不是上一篇文章里的尾随类。 因为同时处理了交叉对象和交叉态射两个函子,脚本会写出两个文件:前缀 true 的是函子交叉对象,前缀 false 的是交叉态射。这两个文件随文附在末尾,可直接拿去在 MT5 里复跑验证。
用函子把两层感知器接进行情范畴
把函子当成多层感知器来用,比之前在范畴之间做线性或二次映射更往前走了一步。函子不只是把两个范畴的对象交叉对应,还强制交叉映射各自的态射——知道协域里的对象就能反推态射,反之亦然,所以这一架构里实际要处理的是范畴间的两个感知器。 财经数据范畴每次喂四个数据点(基于前文假设),输入层固定四个节点;隐藏层节点数是一个可优化参数,默认取 7;输出层只给一个值,即标普 500 指数变化的预测。权重、偏差和激活函数决定了前馈怎么算,这三块建议先补基础再读代码。 Alglib 的函数库在 MT5 的 Include\Math 目录下可直接调用,感知器是标准三层结构。下面这段训练函数的骨架,展示了怎么用 MLPCreate1 按「输入数 / 隐藏数 / 输出数」建网,并优先读取已训练权重: 别把隐藏层 7 当成金标准 隐藏层节点数在回测里对过拟合敏感,外汇与贵金属行情噪声大、高风险,默认 7 只是起点;建议用walk-forward在 EURUSD 或 XAUUSD 的 1H 上从 3 到 12 扫一遍,看哪档泛化误差最低再定。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function to train Perceptron. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CSignalCT::Train(CMultilayerPerceptron &MLP) { CMLPBase _base; CMLPTrain _train; if(!ReadPerceptron(m_training_profit)) { _base.MLPCreate1(__INPUTS,m_hidden,__OUTPUTS,MLP); m_training_profit=class="num">0.0; } else { printf(__FUNCSIG__+" read perceptron, with profit: "+DoubleToString(m_training_profit)); } ... class="kw">return(class="kw">false); }
◍ 用 Levenberg-Marquardt 喂数据训网络
历史财经日历数据走 Levenberg-Marquardt 算法训练,前向和反向验算都交给 AlgLib 函数处理。每次新柱线或定时器触发时,矩阵从共用目录的 csv 抓每行四个数据点当历史,喂给网络去算权重和背离。 TrainingLoad 负责把 XY 输入矩阵填满。代码里它会拼出 '_s_'+货币对+周期+对象数的 csv 文件名,用 FileOpen 以共享读方式打开,逐行按逗号切分;只有切出 6 段的那一行才被采纳,第一段转成时间,后面四个槽位塞进维度为 4 的经济数据向量。 唯一可优化参数是隐藏层权重数量,范围 5 到 12。每次优化验算结束,仅当本次准则优于已写入文件的旧准则,才把权重落盘到共用目录;下次初始化时若盘上有文件,初始权重就从那读,而不是从头随机。 反向传播和梯度下降都由 AlgLib 的 CMLPTrain 类里 MLPTrainLM 接管。下面这段是训练主调和数据装载的原样代码,注意 _info>0 才代表收敛成功返回 true。
class="type">int _info=class="num">0; CMatrixDouble _xy; CMLPReport _report; TrainingLoad(m_training_stop,_xy,m_training_points,m_testing_points); class=class="str">"cmt">// if(m_training_points>class="num">0) { _train.MLPTrainLM(MLP,_xy,m_training_points,m_decay,m_restarts,_info,_report); if(_info>class="num">0){ class="kw">return(true); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function Get Training Points and Initialize Training Matrix. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSignalCT::TrainingLoad(class="type">class="kw">datetime Date,CMatrixDouble &XY,class="type">int &TrainingPoints,class="type">int &TestingPoints) { TrainingPoints=class="num">0; TestingPoints=class="num">0; ResetLastError(); class="type">class="kw">string _file="_s_"+m_currency+"_"+m_symbol.Name()+"_"+EnumToString(m_period)+"_"+class="type">class="kw">string(m_objects)+".csv"; class="type">int _handle=FileOpen(_file,FILE_SHARE_READ|FILE_ANSI|FILE_COMMON,"\n",CP_ACP); if(_handle!=INVALID_HANDLE) { class="type">class="kw">string _line=""; class="type">int _line_length=class="num">0; class="kw">while(!FileIsLineEnding(_handle)) { class=class="str">"cmt">//--- find out how many characters are used for writing the line _line_length=FileReadInteger(_handle,INT_VALUE); class=class="str">"cmt">//--- read the line _line=FileReadString(_handle,_line_length); class="type">class="kw">string _values[]; class="type">class="kw">ushort _separator=StringGetCharacter(",",class="num">0); if(StringSplit(_line,_separator,_values)==class="num">6) { class="type">class="kw">datetime _date=StringToTime(_values[class="num">0]); _d_economic.Let(); _d_economic.Cardinality(class="num">4); class=class="str">"cmt">//printf(__FUNCSIG__+" initializing for: "+TimeToString(Date)+" at: "+TimeToString(_date));
「按时间切分训练集与测试集」
这段逻辑干的事很直接:读历史数据文件时,拿每条记录的日期和预设的 Date 做比较,早于它的进训练池,晚于或等于它的进测试池。TrainingPoints 和 TestingPoints 两个计数器实时累加,互不重叠。 当 _date<Date 成立,TrainingPoints 先自增 1,随后 XY.Resize 把二维数组行数扩到当前训练样本数,列宽固定为 __INPUTS+__OUTPUTS。紧接着两层赋值:前 __INPUTS 列从 _values 第 2 个起逐个转 double 写进去,最后一列(偏移 __INPUTS)取 _values[__INPUTS+1] 作为输出标签。 否则分支只做 TestingPoints++,不碰 XY 矩阵,意味着测试样本后续另存或另算。文件读完 FileClose 收尾;若 FileOpen 失败,printf 打出函数签名加错误码,方便在 MT5 Experts 日志里定位。外汇与贵金属数据回测本身高风险,切分比例失当会让样本外表现出现偏误。 把 Date 设成你品种实盘数据的 70% 位置,跑一遍这段代码,看 TrainingPoints 与 TestingPoints 的比值是否落在 7:3 附近,就能验证切分没写反。
if(_date<Date) { TrainingPoints++; class=class="str">"cmt">// XY.Resize(TrainingPoints,__INPUTS+__OUTPUTS); for(class="type">int i=class="num">0;i<__INPUTS;i++) { XY[TrainingPoints-class="num">1].Set(i,StringToDouble(_values[i+class="num">1])); } class=class="str">"cmt">// XY[TrainingPoints-class="num">1].Set(__INPUTS,StringToDouble(_values[__INPUTS+class="num">1])); } else { TestingPoints++; } } } FileClose(_handle); } else { printf(__FUNCSIG__+" failed to load file. Err: "+IntegerToString(GetLastError())); } }