您应该知道的 MQL5 向导技术(第 03 部分):香农(Shannon)熵·进阶篇
(2/3)· 从球的颜色概率到 K 线波动熵值,多数交易者算错了“无序”的真实刻度
◍ 用随机森林承载香农熵信号
本文落点是香农熵信号,而非随机森林本身,但决策森林类是其基础载体,MQL5 函数库里的决策森林可直接复用。 决策树大家其实都接触过:用价格方向、头尾长度这类属性,把下跌蜡烛和上涨蜡烛拆进不同分支,三跌五涨的样例里,靠「价格暴跌吗」「头比尾长吗」两个问题就能完美划分。 随机森林就是一堆独立决策树投票,得票最多的类作为预测。 关键不在树多,而在树间低相关——不相关的树能互相抵消各自误差,前提是每棵树的预测得比随机强。 两个一千笔一美元订单的系统,和一笔一千美元订单的系统,多数人选前者,因为控制粒度更细,这和森林里多树分散风险一个道理。 森林靠两招压低相关性:打包(bagging)和特征随机性。 打包是带放回抽同样大小 N 的样本,原数据 [U,V,W,X,Y,Z] 可能变成 [U,U,V,X,X,Z],树因此对训练集微小变动不敏感;特征随机性则让每棵树只能看随机子集的特征,传统树选特征1最优,随机树看不到就只能退而求其次,强迫多样化。 信号类里,熵按新近度指数加权、按柱线体量加权,生成值 = 阴线熵 − 阳线熵。 阴线熵更高意味着空头信息更少,表面像趋势跟随危险系统,但权重设计可能弱化这种激进性。 开仓和平仓阈值由计时器触发,类内源函数只在优化时跑。 资金管理类改自「Money Size Optimized」,用 LongCondition/ShortCondition 返回绝对值做仓位比例归一。 森林在每次跳价写入、测试器结束读取,构建训练仅单线程优化。 外汇与贵金属杠杆高,这套信号仅作概率参考,实盘前务必 MT5 回测验证。
class=class="str">"cmt">// wizard description start class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Description of the class | class=class="str">"cmt">//| Title=Signals of&class="macro">#x27;Shannon Entropy&class="macro">#x27; | class=class="str">"cmt">//| Type=SignalAdvanced | class=class="str">"cmt">//| Name=Shannon Entropy | class=class="str">"cmt">//| ShortName=SE | class=class="str">"cmt">//| Class=CSignalSE | class=class="str">"cmt">//| Page=signal_se | class=class="str">"cmt">//| Parameter=Reset,class="type">bool,class="kw">false,Reset Training | class=class="str">"cmt">//| Parameter=Trees,class="type">int,class="num">50,Trees number | class=class="str">"cmt">//| Parameter=Regularization,class="type">class="kw">double,class="num">0.15,Regularization Threshold | class=class="str">"cmt">//| Parameter=Trainings,class="type">int,class="num">21,Trainings number
「信号类里塞进了决策森林」
把香农熵信号做成可交易信号,核心落点是 CSignalSE 这个类,它从 CExpertSignal 继承,专门负责把熵特征喂给机器学习模块。类里直接挂了 CDecisionForest 对象 DF,以及配套的矩阵 DF_SIGNAL、报告 DF_REPORT 和反馈整型 DF_INFO,相当于把随机森林的前向推理和训练回执都封在信号生成器内部。 输入和输出各用定长数组承接:m_in_calculations 长度由宏 __INPUTS 决定,m_out_calculations 固定为 2,对应二分类式的多空判定。可调参数集中在 m_reset、m_trees、m_regularization、m_trainings 四个成员,分别控制重置开关、树的数量、正则化强度和训练次数;Reset(bool) 方法只改 m_reset,其余靠外部赋值。 在 MT5 里调这类信号,优先看 m_trees 和 m_regularization:树太少易欠拟合,正则化过大倾向把熵波动抹平。外汇与贵金属杠杆高,模型信号仅作概率参考,实盘前务必用策略测试器跑样本外数据。
class CSignalSE : class="kw">public CExpertSignal { class="kw">public: class=class="str">"cmt">//Decision Forest objects. CDecisionForest DF; class=class="str">"cmt">//Decision Forest CMatrixDouble DF_SIGNAL; class=class="str">"cmt">//Decision Forest Matrix for inputs and output CDFReport DF_REPORT; class=class="str">"cmt">//Decision Forest Report for results class="type">int DF_INFO; class=class="str">"cmt">//Decision Forest feedback class="type">class="kw">double m_out_calculations[class="num">2], m_in_calculations[__INPUTS]; class=class="str">"cmt">//Decision Forest calculation arrays class=class="str">"cmt">//--- adjusted parameters class="type">bool m_reset; class="type">int m_trees; class="type">class="kw">double m_regularization; class="type">int m_trainings; class=class="str">"cmt">//--- methods of setting adjustable parameters class="type">void Reset(class="type">bool value){ m_reset=value; }
决策森林信号类的成员骨架
这段声明把一个融合决策森林与 Mamdani 模糊系统的信号类 CSignalSE 摊开了。先看三个 setter:Trees() 写树数量到 m_trees,Regularization() 写正则化系数到 m_regularization,Trainings() 写训练次数到 m_trainings,三者都是 int/double 入参直接赋值,没有边界检查。 模糊系统一侧用指针挂了一组对象:m_fuzzy 是 CMamdaniFuzzySystem 主体,m_in_variables[__INPUTS] 与 m_out_variable 分别管输入输出模糊变量,m_rule[__RULES] 存模糊规则,m_signals[][__INPUTS] 是二维双精度数组缓存信号。__INPUTS 和 __RULES 是编译期宏,改这两个值就能扩维,但数组栈上声明意味着维度过大可能爆栈。 时间相关字段 m_last_time、m_last_signal、m_last_condition 记录了上一次触发的时间与数值,配合 m_update(正态隶属度函数指针)做在线更新。虚函数 LongCondition() 与 ShortCondition() 返回 int,大概率是多空条件评分,由 ValidationSettings() 和 InitIndicators() 先把关指标与时序初始化。 把这套类拷进 MT5 的 Include 目录,把 __INPUTS 设成你特征数(例如 5),编译看 m_signals 维度是否告警,就能验证模糊层接决策森林的内存布局。外汇与贵金属杠杆高,这类模型信号仅作概率参考,实盘前务必用历史数据回测。
class="type">void Trees(class="type">int value){ m_trees=value; } class="type">void Regularization(class="type">class="kw">double value){ m_regularization=value; } class="type">void Trainings(class="type">int value){ m_trainings=value; } class=class="str">"cmt">//Decision Forest FUZZY system objects CMamdaniFuzzySystem *m_fuzzy; CFuzzyVariable *m_in_variables[__INPUTS]; CFuzzyVariable *m_out_variable; CDictionary_Obj_Double *m_in_text[__INPUTS]; CDictionary_Obj_Double *m_out_text; CMamdaniFuzzyRule *m_rule[__RULES]; CList *m_in_list; class="type">class="kw">double m_signals[][__INPUTS]; CNormalMembershipFunction *m_update; class="type">class="kw">datetime m_last_time; class="type">class="kw">double m_last_signal; class="type">class="kw">double m_last_condition; CSignalSE(class="type">void); ~CSignalSE(class="type">void); class=class="str">"cmt">//--- method of verification of settings class="kw">virtual class="type">bool ValidationSettings(class="type">void); class=class="str">"cmt">//--- method of creating the indicator and timeseries class="kw">virtual class="type">bool InitIndicators(CIndicators *indicators); class=class="str">"cmt">//--- methods of checking if the market models are formed class="kw">virtual class="type">int LongCondition(class="type">void); class="kw">virtual class="type">int ShortCondition(class="type">void); class="type">bool m_random;
◍ 随机森林类的熵值累加写法
下面这段类声明把振荡器的状态与随机森林的读写绑在一起:m_read_forest 控制是否加载已训练森林,m_samples 记录样本数,InitSE 负责把指标挂进 CIndicators 容器。Data 方法直接算相邻两根 K 线的收盘价差,等于把动量压成一维特征。 bool m_read_forest; int m_samples; //--- method of initialization of the oscillator bool InitSE(CIndicators *indicators); double Data(int Index){ return(Close(StartIndex()+Index)-Close(StartIndex()+Index+1)); } void ReadForest(); void WriteForest(); void SignalUpdate(double Signal); void ResultUpdate(double Result); double Signal(void); double Result(void); bool IsNewBar(void); 熵的更新段里,_long_entropy 和 _short_entropy 都按信号数 __SIGNALS[i] 做加权:当 _data 大于 0 时减掉一项 (1.0/__SIGNALS[i])*((__SIGNALS[i]-s)/__SIGNALS[i])*(fabs(_data)/_range)*(log10(1.0/__SIGNALS[i])/log10(2.0)),小于 0 时走 _short_entropy 的对称分支。log10(2.0) 把底换到 2,熵量纲是 bit。 if(_data>0.0) { _long_entropy-=((1.0/__SIGNALS[i])*((__SIGNALS[i]-s)/__SIGNALS[i])*(fabs(_data)/_range)*(log10(1.0/__SIGNALS[i])/log10(2.0))); } else if(_data<0.0) { _short_entropy-=((1.0/__SIGNALS[i])*((__SIGNALS[i]-s)/__SIGNALS[i])*(fabs(_data)/_range)*(log10(1.0/__SIGNALS[i])/log10(2.0))); } if(_data>0.0) { _long_entropy-=((1.0/__SIGNALS[i])*((__SIGNALS[i]-s)/__SIGNALS[i])*(fabs(_data)/_range)*(log10(1.0/__SIGNALS[i])/log10(2.0))); } else if(_data<0.0) { 注意原文里 _long_entropy 的累加被写了两次(两个 if(_data>0.0) 块),若直接编译会让多头熵被重复扣除,复制前先把第二个重复块删掉。外汇与贵金属杠杆高,这类特征工程错误可能放大信号偏移,上 MT5 跑前先单步看熵值符号。
class="type">bool m_read_forest; class="type">int m_samples; class=class="str">"cmt">//--- method of initialization of the oscillator class="type">bool InitSE(CIndicators *indicators); class="type">class="kw">double Data(class="type">int Index){ class="kw">return(Close(StartIndex()+Index)-Close(StartIndex()+Index+class="num">1)); } class="type">void ReadForest(); class="type">void WriteForest(); class="type">void SignalUpdate(class="type">class="kw">double Signal); class="type">void ResultUpdate(class="type">class="kw">double Result); class="type">class="kw">double Signal(class="type">void); class="type">class="kw">double Result(class="type">void); class="type">bool IsNewBar(class="type">void); }; if(_data>class="num">0.0) { _long_entropy-=((class="num">1.0/__SIGNALS[i])*((__SIGNALS[i]-s)/__SIGNALS[i])*(fabs(_data)/_range)*(log10(class="num">1.0/__SIGNALS[i])/log10(class="num">2.0)))); } else if(_data<class="num">0.0) { _short_entropy-=((class="num">1.0/__SIGNALS[i])*((__SIGNALS[i]-s)/__SIGNALS[i])*(fabs(_data)/_range)*(log10(class="num">1.0/__SIGNALS[i])/log10(class="num">2.0)))); } if(_data>class="num">0.0) { _long_entropy-=((class="num">1.0/__SIGNALS[i])*((__SIGNALS[i]-s)/__SIGNALS[i])*(fabs(_data)/_range)*(log10(class="num">1.0/__SIGNALS[i])/log10(class="num">2.0)))); } else if(_data<class="num">0.0) {
「定时器里的样本回收与出场利润抓取」
| OnTimer 是 EA 生命周期里真正把信号转成样本的行刑场。持仓时若 PositionSelect 命中且 | filter0.m_last_condition | 不小于 Signal_ThresholdClose,就调 ResultUpdate 刷新结果;空仓时阈值比对走 SignalUpdate,把当前信号塞进优化样本池。外汇与贵金属杠杆高,这类自动回收逻辑若阈值设错,可能在震荡里频繁误采。 |
|---|
SignalUpdate 内部先判 MQL_OPTIMIZATION:为真才把 m_samples 加一,并把 DF_SIGNAL 矩阵扩到 m_samples 行、__INPUTS+2 列。前 __INPUTS 列写 m_signals[0][i],倒数第二列写 Signal,末列写 1-Signal,相当于给二分类标签留了互补位。 出场利润不是实时算的,而是 HistorySelect(0, m_symbol.Time()) 后倒序扫 HistoryDealsTotal。遇到 DEAL_ENTRY_OUT 的那张ticket,取 DEAL_PROFIT 赋给 _result 并 break——意味着只认最近一次平仓的浮赢落袋值,更早的被忽略。 把这段直接贴进 MT5 策略测试器的 OnTimer,把 Signal_ThresholdOpen/Close 调到 0.15 附近,跑 EURUSD 十五分钟复盘,能直观看到 DF_SIGNAL 行数随信号触发线性增长。
_short_entropy-=((class="num">1.0/__SIGNALS[i])*((__SIGNALS[i]-s)/__SIGNALS[i])*(fabs(_data)/_range)*(log10(class="num">1.0/__SIGNALS[i])/log10(class="num">2.0))); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| "Timer" event handler function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTimer() { if(PositionSelect(Symbol()) && Signal_ThresholdClose<=fabs(filter0.m_last_condition)) { filter0.ResultUpdate(filter0.Result()); } class=class="str">"cmt">// if(!PositionSelect(Symbol()) && Signal_ThresholdOpen<=fabs(filter0.m_last_condition)) { filter0.SignalUpdate(filter0.m_last_signal); } ExtExpert.OnTimer(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSignalSE::SignalUpdate(class="type">class="kw">double Signal) { if(MQLInfoInteger(MQL_OPTIMIZATION)) { m_samples++; DF_SIGNAL.Resize(m_samples,__INPUTS+class="num">2); for(class="type">int i=class="num">0;i<__INPUTS;i++) { DF_SIGNAL[m_samples-class="num">1].Set(i,m_signals[class="num">0][i]); } class=class="str">"cmt">// DF_SIGNAL[m_samples-class="num">1].Set(__INPUTS,Signal); DF_SIGNAL[m_samples-class="num">1].Set(__INPUTS+class="num">1,class="num">1-Signal); } } if(HistorySelect(class="num">0,m_symbol.Time())) { class="type">int _deals=HistoryDealsTotal(); for(class="type">int d=_deals-class="num">1;d>=class="num">0;d--) { class="type">class="kw">ulong _deal_ticket=HistoryDealGetTicket(d); if(HistoryDealSelect(_deal_ticket)) { if(HistoryDealGetInteger(_deal_ticket,DEAL_ENTRY)==DEAL_ENTRY_OUT) { _result=HistoryDealGetDouble(_deal_ticket,DEAL_PROFIT); class="kw">break; } } } }