MQL5 中的范畴论 (第 17 部分):函子与幺半群·进阶篇
「持仓规整里的幺半群运算怎么落代码」
把持仓规模当成一个幺半群来调,核心就是用一个 Operate 函数按指定运算从集合里挑值。原文定义了 6 种运算枚举,但实际只跑得动 4 种——OP_ADD 和 OP_MULTIPLY 要求恒等元 0 和 1 必须常驻集合,而本案例的幺半群做不到这点,所以编号 0 和 1 被弃用,只用 2 到 5(OP_LEAST、OP_MOST、OP_CLOSEST、OP_FURTHEST)。 我们关心的是仓位轻重,不是去预测波动率,所以最后的“动作”幺半群可以换成函子,前四个幺半群照常执行,用来决定指标读数进而指导仓位。输入侧仍用 RSI 加布林带,布林带照例正则化,RSI 输出压到 0–100 区间;这个 0–100 的值虽是前面三个幺半群算出来的,但仍作为第四个输入喂给多层感知器(MLP)。 说白了,一个幺半群就是集合 + 二元运算 + 恒等元,允许你按运算从集合里选一个元素。选好时间帧、回溯区间、应用价格,就得到指标输入,归一化后成为 MLP 的第四路信号。时间帧、回溯、价格、指标的选取步骤前文已给,下面这段是枚举与 Operate 的骨架,以及从各函数取输出的写法。 别把恒等元当摆设 OP_ADD 和 OP_MULTIPLY 被砍不是代码懒,是案例幺半群里塞不进常量 0 和 1;硬用会在 ArrayInitialize 之后挑不到合法 OutputIndex,直接在 MT5 日志里报 Failed to get element。开编辑器把 EOperations 改成只用 2–5,能少踩一堆坑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Enumeration for Monoid Operations | class=class="str">"cmt">//+------------------------------------------------------------------+ enum EOperations { OP_FURTHEST=class="num">5, OP_CLOSEST=class="num">4, OP_MOST=class="num">3, OP_LEAST=class="num">2, OP_MULTIPLY=class="num">1, OP_ADD=class="num">0 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Operate function for executing monoid binary operations | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMoneyCT::Operate(CMonoid<class="type">class="kw">double> &M,EOperations &O,class="type">int IdenityIndex,class="type">int &OutputIndex) { OutputIndex=-class="num">1; class=class="str">"cmt">// class="type">class="kw">double _values[]; ArrayResize(_values,M.Cardinality());ArrayInitialize(_values,class="num">0.0); class=class="str">"cmt">// for(class="type">int i=class="num">0;i<M.Cardinality();i++) { m_element.Let(); if(M.Get(i,m_element)) { if(!m_element.Get(class="num">0,_values[i])) { printf(__FUNCSIG__+" Failed to get class="type">class="kw">double for class="num">1 at: "+IntegerToString(i+class="num">1)); } } else{ printf(__FUNCSIG__+" Failed to get element for class="num">1 at: "+IntegerToString(i+class="num">1)); } } class=class="str">"cmt">// if(O==OP_LEAST) { ... } else if(O==OP_MOST) { ... } else if(O==OP_CLOSEST) { ... } else if(O==OP_FURTHEST) { ... } } ENUM_TIMEFRAMES _timeframe_0=SetTimeframe(m_timeframe,class="num">0); class="type">int _lookback_0=SetLookback(m_lookback,_timeframe_0,class="num">0); ENUM_APPLIED_PRICE _appliedprice_0=SetAppliedprice(m_appliedprice,_timeframe_0,_lookback_0,class="num">0); class="type">class="kw">double _indicator_0=SetIndicator(_timeframe_0,_lookback_0,_appliedprice_0,class="num">0);
逐根柱线训练下的规模协同
把 MLP 放到实盘逻辑里,最反直觉的一点是:本文不在初始化阶段做预训练,而是每来一根新柱线就跑一次 LM 训练。好处是网络始终贴合近期波动,代价是初始权重随机,同一套 EA 参数在不同回测里结果可能差异极大。 为绕开随机性,可盈利的那次运行会把权重落盘,下次启动若隐藏层节点数一致就直接加载为初值。网络读写函数未公开,只能留接口给读者自己补。 代码里真正干活的,是把时间框架、回看长度、应用价格、指标值塞进一行输入矩阵,再拿当前柱的实体振幅与双向点差成本比大小,取较大者作为停止距离标签。_stops 那行把 Ask-Bid 价差乘 2,加上经纪商 StopsLevel 与 FreezeLevel 的点数,外汇和贵金属品种务必留意这部分在高波动时会突然膨胀。 幺半群单独也能估停止距离,MLP 单独也行,但本段展示的是两者集成进同一训练流。想验证协同是否真有效,得另写仅幺半群、仅 MLP 的 EA 做三组对照,源码已给足素材,读者可自行证伪。
m_open.Refresh(-class="num">1); m_high.Refresh(-class="num">1); m_close.Refresh(-class="num">1); CMLPTrain _train; class="type">int _info=class="num">0; CMLPReport _report; CMatrixDouble _xy;_xy.Resize(class="num">1,__INPUTS+__OUTPUTS); _xy[class="num">0].Set(class="num">0,RegularizeTimeframe(_timeframe_1)); _xy[class="num">0].Set(class="num">1,_lookback_1); _xy[class="num">0].Set(class="num">2,RegularizeAppliedprice(_appliedprice_1)); _xy[class="num">0].Set(class="num">3,_indicator_1); class=class="str">"cmt">// class="type">int _x=StartIndex()+class="num">1; class="type">class="kw">double _sl_1=m_high.GetData(_x)-m_low.GetData(_x); if(m_open.GetData(_x)>m_close.GetData(_x)) { _sl_1=m_high.GetData(_x)-m_open.GetData(_x); } class="type">class="kw">double _stops=(class="num">2.0*(m_symbol.Ask()-m_symbol.Bid()))+((m_symbol.StopsLevel()+m_symbol.FreezeLevel())*m_symbol.Point()); _xy[class="num">0].Set(__INPUTS,fmax(_stops,_sl_1)); _train.MLPTrainLM(m_mlp,_xy,class="num">1,m_decay,m_restarts,_info,_report);
◍ 用 BTCUSD 日线验证综合持仓规模
把综合持仓规模方法放到 BTCUSD 上跑,测试区间取 2020-01-01 到 2023-08-01 的日线。模型只用带一个隐藏层的 MLP,除了优化隐藏层权重数量,还要微调决定持仓规模的四个幺半群,给每个幺半群找合适的衡点元素与操作类型。 信号端直接调用 MQL5 标准库的 RSI 智能信号类,不挂尾随止损,take level 与 stop level 都设零,price level 参与优化,系统允许用挂单。 权重初始化是随机的,所以同一组输入在对象函子、态射函子下跑出来的结果不一定能复现。哪怕从之前盈利运行里载入权重,训练在每根新柱线继续,权重也会被改,结果仍会漂移。务实做法是自己写一套记录和读取最佳权重的方法。 对照组的 RSI 信号、无尾随止损完全一致,只把持仓规模换成固定保证金。截短优化只是示意潜力,但倾向显示综合方法的结果更优。外汇与贵金属这类高波动品种同理,持仓规模往往是最后才补的环节,若入场信号本身扎实,规模模块的边际意义可能有限,但一旦动起来对回测曲线影响很大。
「函子仓位系统的几道硬坎」
把幺半群集和策略对齐、再搭出配套的 MLP 训练模式,这条学习曲线很陡。两套东西缺一个都没法落地,开工前得先砸进去大量时间,新手容易卡在第一步。 一分钟 OHLC 和跳价数据对不上的时候,真实账户跑出来的幺半群设置或 MLP 权重基本不可信。价格间隙、证券自身数据不一致,是实盘实现这套思路的主要症结,回测好看不等于实盘能复现。 MLP 的过拟合和泛化是老问题。输入层得塞进「有意义」的数据——比如可信赖的基本面因子,且预期真能影响你要预测的目标,再配合正则化,才有可能压住泛化失败的概率。 参数调优单独拎出来看,已经超出过拟合范畴。研究所耗的 CPU 成本和摸到目标参数的耗时,是实打实的开销,贵金属和外币对的高波动会进一步放大这种资源压力。 编码者还得面对可解释性差的障碍:系统跑顺了想拉资金,投资者往往要求拆开 MLP 讲清楚,网络层一复杂就很难说服人。数据预处理、权重加载、市场制度切换、模型维护都得备好应急方案。 市场制度变化这点,反而给手动交易留了理由。现在系统多在宽历史样本里开发测试,覆盖了多种制度,但裁判仍未终局。
范畴实现落到了仓位尺度上
这套研究把幺半群视角接进了 MQL5,并实打实用范畴语言写了一个可运行的模块。落点不在信号本身,而在持仓规模:例子里用的是 RSI 的入场与离场信号来驱动,但尺寸控制走的是函子与幺半群结构。 值得留意的是,函子在这里只管「规模怎么映射」,入场逻辑、 trailing stop 的摆放与再调整仍由交易系统其它部分决定。也就是说,它把仓位这件事从信号里解耦出来,外汇与贵金属杠杆品种下这种解耦直接影响回撤幅度,属于高风险操作前的必要隔离。 原文也直说,在能把这套思路真正用顺之前还有坑要填。建议开 MT5 新建一个 EA 骨架,先把 RSI 信号接进范畴模块跑历史 tick,验证尺寸输出是否符合你的资金曲线预期,再谈其它。
◍ 把文件摆对地方就能跑起来
要把这套思路落到 MT5 里,先得把头文件归位:'MoneyCT_17_.mqh' 丢进 'MQL5\include\Expert\Money\','ct_9.mqh' 放 include 根目录即可。附带的 ct_17.mq5 是向导拼出来的成品,编译就能跑;文件名带 'control' 的那版用固定保证金管仓位,文件体积 6.7 KB,普通版 9.25 KB。 如果你没碰过向导拼装,得先熟悉怎么用 MQL5 向导把模块凑成 EA——文章里入场就挂了个 RSI 振荡器,且没开尾随止损。这不是什么圣杯,只是条可改的思路,外汇和贵金属波动大、杠杆高,实盘前务必自己调参数验过。 ZIP 里四个文件大小都标好了:ct_9.mqh 占 65.06 KB,MoneyCT_17_.mqh 是 36.29 KB。下下来解压,路径别错,打开 MT5 按自己策略改 RSI 周期,编译通过就能先回测看看。