100 个最佳优化递次(第 1 部分)。 开发优化分析器·综合运用
(3/3)· 优化跑了几天只拿到一串统计?本篇把分析器、数据库、演播器串成完整工作流
很多交易者把 EA 优化完就直接拿排名第一的递次上实盘,却没看过它在横盘和趋势下的分别表现。前瞻性测试被误读成利润回吐,滑点承受力一概不知,这类盲区在弱网本上尤其致命。
「用类把EA输入参数存成可比对单元」
在写多策略监控或参数回放工具时,最麻烦的是把 EA 的输入(int / double / string)统一成一种结构再互相比较。下面这个类把单个参数封装成 CDataKeeper 对象,能存三类数据,并对外暴露类型、名称与取值接口。 它用了一个枚举 CoefCompareResult 表达四种比对结果:类型或名字不同、相等、当前小于传入、当前大于传入。注意 double 比较没有用直接 ==,而是走私有方法 compareDouble,用 NormalizeDouble(x-y,10) 把差值精确到 10 位小数后再判正负,避免浮点误差误判。 Compare 方法第一步先比 variable_name 和 variable_type,只有同名且同类型才继续往下走值比较;异名或异型直接返回 Coef_Different。实际在 MT5 里你可以把多组 EA 输入各存一个 CDataKeeper 数组,跑一个循环就能扫出哪几个参数被手动改过。 外汇与贵金属市场杠杆高、滑点随机,任何参数比对脚本只反映历史配置差异,不预示行情方向,接入实盘前请在策略测试器用真实点差验证。
enum CoefCompareResult { Coef_Different,class=class="str">"cmt">// 不同的数据类型或变量名称 Coef_Equal,class=class="str">"cmt">// 变量相等 Coef_Less, class=class="str">"cmt">// 当前变量小于所传递的变量 Coef_More class=class="str">"cmt">// 当前变量超过所传递的变量 }; class=class="str">"cmt">//+---------------------------------------------------------------------+ class=class="str">"cmt">//| 存储一个特定机器人输入的类。 | class=class="str">"cmt">//| 它可以存储以下类型的数据: [class="type">int, class="type">class="kw">double, class="type">class="kw">string] | class=class="str">"cmt">//+---------------------------------------------------------------------+ class CDataKeeper { class="kw">public: CDataKeeper(); class=class="str">"cmt">// 构造函数 CDataKeeper(class="kw">const CDataKeeper&other); class=class="str">"cmt">// 复制构造函数 CDataKeeper(class="type">class="kw">string _variable_name,class="type">int _value); class=class="str">"cmt">// 参数化构造函数 CDataKeeper(class="type">class="kw">string _variable_name,class="type">class="kw">double _value); class=class="str">"cmt">// 参数化构造函数 CDataKeeper(class="type">class="kw">string _variable_name,class="type">class="kw">string _value); class=class="str">"cmt">// 参数化构造函数 CoefCompareResult Compare(CDataKeeper &data); class=class="str">"cmt">// 比较方法 DataTypes getType(){class="kw">return variable_type;}; class=class="str">"cmt">// 获取数据类型 class="type">class="kw">string getName(){class="kw">return variable_name;}; class=class="str">"cmt">// 获取参数名 class="type">class="kw">string valueString(){class="kw">return value_string;}; class=class="str">"cmt">// 获取参数 class="type">int valueInteger(){class="kw">return value_int;}; class=class="str">"cmt">// 获取参数 class="type">class="kw">double valueDouble(){class="kw">return value_double;}; class=class="str">"cmt">// 获取参数 class="type">class="kw">string ToString(); class=class="str">"cmt">// 将任何参数转换为字符串。 如果是一个字符串参数,则从两端给字符串添加单引号 <<&class="macro">#x27;>> class="kw">private: class="type">class="kw">string variable_name,value_string; class=class="str">"cmt">// 变量名和字符串变量 class="type">int value_int; class=class="str">"cmt">// Int 变量 class="type">class="kw">double value_double; class=class="str">"cmt">// Double 变量 DataTypes variable_type; class=class="str">"cmt">// 变量类型 class="type">int compareDouble(class="type">class="kw">double x,class="type">class="kw">double y) class=class="str">"cmt">// 比较 Double 类型,精确到 class="num">10 位小数 { class="type">class="kw">double diff=NormalizeDouble(x-y,class="num">10); if(diff>class="num">0) class="kw">return class="num">1; else if(diff<class="num">0) class="kw">return -class="num">1; else class="kw">return class="num">0; } }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 将当前参数与传递的参数进行比较 | class=class="str">"cmt">//+------------------------------------------------------------------+ CoefCompareResult CDataKeeper::Compare(CDataKeeper &data) { CoefCompareResult ans=Coef_Different; if(StringCompare(this. variable_name,data.getName())==class="num">0 && this.variable_type==data.getType()) class=class="str">"cmt">// 比较名称和类型
◍ 变量比较与自定义评分回调的分派逻辑
这段实现把一个通用比较器按数据类型拆成三条路:整型直接拿 ==、> 做判定,实数走 compareDouble 避免浮点误差,字符串交给 StringCompare。返回的不是布尔,而是 Coef_Equal / Coef_More / Coef_Less 三种系数,方便上层直接加权。 整型分支一行搞定:this.value_int 与 data.valueInteger() 相比,相等给 Coef_Equal,大于给 Coef_More,否则 Coef_Less。实数分支调了两次 compareDouble,返回 0 为等、>0 为多、<0 为少,注意这里重复计算可改成先存局部变量。 下面两个 typedef 把用户评分逻辑抽象成函数指针:customScoring_1 只吃历史成交数组和 isOneLot 标志,customScoring_2 多传一个 CSqliteManager* 用于只读查库。实盘里若想换评分模型,只需重写回调并在 OnInitEvent 里挂上,不用动核心类。 外汇与贵金属波动剧烈、杠杆风险高,这类比较系数仅作策略信号参考,不代表任何方向确定性。
{
class="kw">switch(this.variable_type) class=class="str">"cmt">// 比较数值
{
case Type_INTEGER :
ans=(this.value_int==data.valueInteger() ? Coef_Equal :(this.value_int>data.valueInteger() ? Coef_More : Coef_Less));
break;
case Type_REAL :
ans=(compareDouble(this.value_double,data.valueDouble())==class="num">0 ? Coef_Equal :(compareDouble(this.value_double,data.valueDouble())>class="num">0 ? Coef_More : Coef_Less));
break;
case Type_Text :
ans=(StringCompare(this.value_string,data.valueString())==class="num">0 ? Coef_Equal :(StringCompare(this.value_string,data.valueString())>class="num">0 ? Coef_More : Coef_Less));
break;
}
}
class="kw">return ans;
}
class=class="str">"cmt">//+---------------------------------------------------------------------------------+
class=class="str">"cmt">//| 回调计算用户比率 |
class=class="str">"cmt">//| 计算比率所需的历史数据和历史类型的标志 |
class=class="str">"cmt">//| 作为输入参数传递 |
class=class="str">"cmt">//+---------------------------------------------------------------------------------+
typedef class="type">class="kw">double(*customScoring_1)(class="kw">const DealDetales &history[],class="type">bool isOneLot);
class=class="str">"cmt">//+---------------------------------------------------------------------------------+
class=class="str">"cmt">//| 回调计算用户比率 |
class=class="str">"cmt">//| 连接到数据库(只读),历史记录和所请求的比率类型标志 |
class=class="str">"cmt">//| 作为输入参数传递 |
class=class="str">"cmt">//+---------------------------------------------------------------------------------+
typedef class="type">class="kw">double(*customScoring_2)(CSqliteManager *dbManager,class="kw">const DealDetales &history[],class="type">bool isOneLot);
class=class="str">"cmt">//+---------------------------------------------------------------------------------+
class=class="str">"cmt">//| 保存数据库中的数据并在此之前创建数据库的类 |
class=class="str">"cmt">//+---------------------------------------------------------------------------------+
class CDBWriter
{
class="kw">public:
class=class="str">"cmt">// 为 OnInit 调用重置之一
class="type">void OnInitEvent(class="kw">const class="type">class="kw">string DBPath,class="kw">const CDataKeeper &inputData_array[],customScoring_1 scoringFunction,class="type">class="kw">double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT); class=class="str">"cmt">// 回调 class="num">1回测数据落库的接口与私有成员
这个类把 MT5 策略测试器的每次优化结果写进 SQLite,对外只暴露四个事件钩子。两个 OnInitEvent 重载分别处理「带自定义评分回调」和「无回调且用户比率置零」的初始化,差异仅在是否传入 customScoring_2 函数指针。 OnTesterEvent 在 OnTester 里调用并返回 double,用于把单组参数的综合得分回传给优化器;OnTickEvent 则在 OnTick 中触发,内部会计算最后一根蜡烛的 datetime 边界 DT_Border。 私有段才是落库逻辑的核心。dbManager 持有数据库连接,coef_array 缓存输入参数,r 是无风险比率(填 0 即代表无风险基准),scoring_type 用 [1,2] 标记当前走的回调类型。CreateDB 负责建表与初始化,isForvard 区分历史优化与前瞻优化——这点在交叉验证时决定数据写哪张表。 下面这段是带回调的初始化签名,直接抄进 EA 的 OnInit 就能挂上数据记录器:
class="type">void OnInitEvent(class="kw">const class="type">class="kw">string DBPath,class="kw">const CDataKeeper &inputData_array[],customScoring_2 scoringFunction,class="type">class="kw">double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT); class=class="str">"cmt">// 回调 class="num">2 class="type">void OnInitEvent(class="kw">const class="type">class="kw">string DBPath,class="kw">const CDataKeeper &inputData_array[],class="type">class="kw">double r,ENUM_TIMEFRAMES TF=PERIOD_CURRENT);class=class="str">"cmt">// 没有回调且没有用户比率(等于零) class="type">class="kw">double OnTesterEvent();class=class="str">"cmt">// 在 OnTester 中调用 class="type">void OnTickEvent();class=class="str">"cmt">// 在 OnTick 中调用 class="kw">private: CSqliteManager dbManager; class=class="str">"cmt">// 连接到数据库 CDataKeeper coef_array[]; class=class="str">"cmt">// 输入参数 class="type">class="kw">datetime DT_Border; class=class="str">"cmt">// 最后一根蜡烛的日期(在 OnTickEvent 中计算) class="type">class="kw">double r; class=class="str">"cmt">// 无风险比率 customScoring_1 scoring_1; class=class="str">"cmt">// 回调 customScoring_2 scoring_2; class=class="str">"cmt">// 回调 class="type">int scoring_type; class=class="str">"cmt">// 回调类型 [class="num">1,class="num">2] class="type">class="kw">string DBPath; class=class="str">"cmt">// 指向数据库的路径 class="type">class="kw">double balance; class=class="str">"cmt">// 余额 ENUM_TIMEFRAMES TF; class=class="str">"cmt">// 时间帧 class="type">void CreateDB(class="kw">const class="type">class="kw">string DBPath,class="kw">const CDataKeeper &inputData_array[],class="type">class="kw">double r,ENUM_TIMEFRAMES TF);class=class="str">"cmt">// 创建数据库及其随附的所有内容 class="type">bool isForvard();class=class="str">"cmt">// 定义当前优化类型(历史/前瞻) class="type">void WriteLog(class="type">class="kw">string s,class="type">class="kw">string where);class=class="str">"cmt">// 文件日志条目 class="type">int setParams(class="type">bool IsForvard,CReportCreator *reportCreator,DealDetales &history[],class="type">class="kw">double &customCoef);class=class="str">"cmt">// 填写输入表 class="type">void setBuyAndHold(class="type">bool IsForvard,CReportCreator *reportCreator);class=class="str">"cmt">// 填写买入并持有历史 class="type">bool setTraidingHistory(class="type">bool IsForvard,DealDetales &history[],class="type">int ID);class=class="str">"cmt">// 填写交易历史 class="type">bool setTotalResult(TotalResult &coefData,class="type">bool isOneLot,class="type">long ID,class="type">bool IsForvard,class="type">class="kw">double customCoef);class=class="str">"cmt">// 用比率填写表格 class="type">bool isHistoryItem(class="type">bool IsForvard,DealDetales &item,class="type">int ID); class=class="str">"cmt">// 检查交易历史记录表中是否已存在这些参数 };
「回测落库时的三种初始化重载」
CDBWriter 类用三个 OnInitEvent 重载区分评分逻辑的来源。第一个重载接 scoring_2 函数指针并把 scoring_type 置为 2;第二个接 scoring_1 并置为 1;第三个无自定义评分,scoring_type 直接写 0。三种路径都会先调 CreateDB 把路径、数据数组、相关系数 _r 和周期 _TF 落库,实际跑 EA 时按你接的是哪种评分选对应重载。 OnTesterEvent 是真正在策略测试器每轮结束写库的地方。它先拉全量成交明细,用 CMutexSync 按数据库路径建互斥锁,避免多品种并行测试时 SQLite 写冲突;IsForvard 标记本轮是不是前瞻(样本外)迭代,会影响后续参数与历史是否带前瞻标签。 写库被包在 BeginTransaction / CommitTransaction 里。ParamsType 表用 INSERT OR IGNORE 把 coef_array 里每个参数的名字和类型先存好,再调 setParams 拿回本次运行的 ID(>0 才继续),随后 setTraidingHistory 存成交、setBuyAndHold 只在首次存一次基准。外汇与贵金属回测涉及高杠杆与滑点,样本内高分不代表样本外能复现,开 MT5 接这段类自己跑一轮优化就能验证锁与事务是否按预期串行。
CreateDB(_DBPath,inputData_array,_r,_TF); scoring_2=scoringFunction; scoring_type=class="num">2; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建数据库并连接 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDBWriter::OnInitEvent(class="kw">const class="type">class="kw">string _DBPath,class="kw">const CDataKeeper &inputData_array[],customScoring_1 scoringFunction,class="type">class="kw">double _r,ENUM_TIMEFRAMES _TF) { CreateDB(_DBPath,inputData_array,_r,_TF); scoring_1=scoringFunction; scoring_type=class="num">1; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建数据库并连接 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDBWriter::OnInitEvent(class="kw">const class="type">class="kw">string _DBPath,class="kw">const CDataKeeper &inputData_array[],class="type">class="kw">double _r,ENUM_TIMEFRAMES _TF) { CreateDB(_DBPath,inputData_array,_r,_TF); scoring_type=class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 保存所有数据至数据库并返回 | class=class="str">"cmt">//| 一个自定义比率 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CDBWriter::OnTesterEvent() { DealDetales history[]; CDealHistoryGetter historyGetter; historyGetter.getDealsDetales(history,class="num">0,TimeCurrent()); class=class="str">"cmt">// 获取交易历史 CMutexSync sync; class=class="str">"cmt">// 同步对象 if(!sync.Create(getMutexName(DBPath))) { Print(Symbol()+" MutexSync create ERROR!"); class="kw">return class="num">0; } CMutexLock lock(sync,(DWORD)INFINITE); class=class="str">"cmt">// 将片段锁定在括号内 class="type">bool IsForvard=isForvard(); class=class="str">"cmt">// 找出当前测试器迭代是否为前瞻测试 CReportCreator rc; class="type">class="kw">string Symb[]; rc.Get_Symb(history,Symb); class=class="str">"cmt">// 获取品种列表 rc.Create(history,Symb,balance,r); class=class="str">"cmt">// 创建报告(自动创建“购买并持有”报告) class="type">class="kw">double ans=class="num">0; dbManager.BeginTransaction(); class=class="str">"cmt">// 事务开始 CStatement stmt(dbManager.Create_statement("INSERT OR IGNORE INTO ParamsType VALUES(@ParamName,@ParamType);")); class=class="str">"cmt">// 请求保存 EA 参数类型列表 if(stmt.get()!=NULL) { for(class="type">int i=class="num">0;i<ArraySize(coef_array);i++) { stmt.Parameter(class="num">1,coef_array[i].getName()); stmt.Parameter(class="num">2,(class="type">int)coef_array[i].getType()); stmt.Execute(); class=class="str">"cmt">// 保存参数类型及其名称 } } class="type">int ID=setParams(IsForvard,&rc,history,ans); class=class="str">"cmt">// 保存 EA 参数以及评估比率并获取ID if(ID>class="num">0)class=class="str">"cmt">// 如果 ID > class="num">0, 参数保存成功 { if(setTraidingHistory(IsForvard,history,ID)) class=class="str">"cmt">// 保存交易历史记录并检查是否已保存 { setBuyAndHold(IsForvard,&rc); class=class="str">"cmt">// 保存买入并持有历史记录(仅保存一次 - 在第一次保存期间) dbManager.CommitTransaction(); class=class="str">"cmt">// 确认一笔事务结束 }
◍ 读取层把回测结果从库里捞出来
上面那段事务回滚逻辑收尾后,紧接着是 CDBReader 类,职责很单一:从 SQLite 里把 EA 跑过的东西读出来供面板绘制。 它对外只暴露四个方法:Connect 接库、getBuyAndHold 取买入持有基准曲线、getTraidingHistory 按 ID 取某次 EA 成交明细、getRobotParams 取参数与比率。isForvard 参数区分历史与实时前向样本,实盘外汇/贵金属波动大,前向数据可能和回测分布明显偏离。 私有侧挂了一个 CSqliteManager 实例和 DBPath 字符串,真正执行 SQL 的活都委托给管理器;getParamTypes 只在内部把输入参数类型和名字映射好,不对外。 开 MT5 后把这段类声明贴进 MQ5,配好 sqlite 包装库,就能直接连历史库读成交明细做交叉验证。
else dbManager.RollbackTransaction(); class=class="str">"cmt">// 否则,取消事务 } else dbManager.RollbackTransaction(); class=class="str">"cmt">// 否则,取消事务 class="kw">return ans; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 从数据库中读取数据的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CDBReader { class="kw">public: class="type">void Connect(class="type">class="kw">string DBPath);class=class="str">"cmt">// 连接数据库的方法 class="type">bool getBuyAndHold(BuyAndHoldChart_item &data[],class="type">bool isForvard);class=class="str">"cmt">// 计算买入并持有历史的方法 class="type">bool getTraidingHistory(DealDetales &data[],class="type">long ID,class="type">bool isForvard);class=class="str">"cmt">// 计算 EA 交易历史的方法 class="type">bool getRobotParams(CoefData_item &data[],class="type">bool isForvard);class=class="str">"cmt">// 计算 EA 参数和比率的方法 class="kw">private: CSqliteManager dbManager; class=class="str">"cmt">// 数据库管理器 class="type">class="kw">string DBPath; class=class="str">"cmt">// 数据库的路径 class="type">bool getParamTypes(ParamType_item &data[]);class=class="str">"cmt">// 计算输入类型及其名称。 };
报告生成与递次过滤的双类架构
这个计算模块由两个类支撑:一个是改进版的交易报告生成类,数据计算量比前版翻倍,图形类型也更丰富;另一个是优化递次过滤类,负责对递次范围内的样本排序,并能画出每个优化比率值对应的盈亏交易频率图,以及在优化结束时绘制整体盈亏的正态分布。假设跑了 1000 个优化递次,就会得到 1000 个盈亏结果,我们真正关心的就是这些结果在分布上的偏向。 分布图能直接暴露数值的不对称偏移方向。如果尾部偏厚且分布中心落在利润区,说明大部分优化递次可能盈利;若不对称明显滑向亏损区,则所选参数组合大概率主要产出亏损递次。外汇与贵金属市场杠杆高、跳空频繁,这类分布结论仅代表历史优化样本的统计倾向,实盘仍属高风险。 报告类只在调用 Create 方法时真正跑一次计算,主循环单遍迭代输入参数,在同一次迭代里把后续所需数据全算出来,避免了旧版为画图反复重扫历史。所有比率统计耗时仅数毫秒,之后 Get 开头的方法只是取用已算好的容器数据。 过滤类持有两个全局数组 arr_main 与 arr_result:前者存卸载优化表后的全量数据,后者存初步筛选的 n 个最佳递次。它既管表操作,也按 EA 比率对结果排序。Filtre 方法可按比率值在指定递次范围内排序,Get_Distribution 支持按指定参数名与值抽取子集再画分布,用来评估某个参数对 EA 整体表现的影响程度。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 用于生成交易历史统计的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CReportCreator { class="kw">public: class=class="str">"cmt">//============================================================================================================================================= class=class="str">"cmt">// 计算/重计算: class=class="str">"cmt">//=============================================================================================================================================
「盈亏统计类的多路构造与取数接口」
在 MT5 里做策略复盘,最麻烦的是把成交记录(DealDetales)整理成可画图、可算比率的结构。下面这组重载 Create 就是为了兼容不同输入场景:可以带历史成交、带 Buy&Hold 对照历史、带品种数组和余额,也可以只丢一个风险系数 r 进去让类内部自己拉数据。
六种 Create 重载覆盖的入口组合里,最少参数的 Create(double r=0) 意味着你不传任何成交也能先建壳,后面再喂数据;而最完整的那一版把 balance 和 Symb[] 都显式传入,适合多品种组合账户按净值口径统计。回测时若 Symb 数组长度为 5,类内部会对这 5 个品种分别跑 PL_oneLot 与 PL_total 两套序列。
取数侧全是一组 Get 开头的布尔接口:GetChart 按 ChartType+CalcType 吐出 PLChart_item 数组,GetDistributionChart 拿分布图,GetCoefChart 拿比率图,GetDailyPL 按日聚合盈亏。注意它们都返回 bool,调用失败时不抛异常,得自己判断返回值再读 out 参数。
私有段里 PL_keeper 用三个 PLChart_item 数组(PL_total / PL_oneLot / PL_Indicative)隔离了「总仓位口径」「单手口径」「指示性口径」三种盈亏曲线;DailyPL_keeper 则把平均与绝对、开仓与平仓四种日表捆在一个结构里。外汇与贵金属杠杆高,这类统计只描述历史分布,不预示下一笔盈亏,实盘验证前请先在策略测试器跑通。
class="type">void Create(DealDetales &history[],DealDetales &BH_history[],class="kw">const class="type">class="kw">double balance,class="kw">const class="type">class="kw">string &Symb[],class="type">class="kw">double r); class="type">void Create(DealDetales &history[],DealDetales &BH_history[],class="kw">const class="type">class="kw">string &Symb[],class="type">class="kw">double r); class="type">void Create(DealDetales &history[],class="kw">const class="type">class="kw">string &Symb[],class="kw">const class="type">class="kw">double balance,class="type">class="kw">double r); class="type">void Create(DealDetales &history[],class="type">class="kw">double r); class="type">void Create(class="kw">const class="type">class="kw">string &Symb[],class="type">class="kw">double r); class="type">void Create(class="type">class="kw">double r=class="num">0); class=class="str">"cmt">//============================================================================================================================================= class=class="str">"cmt">// Getters: class=class="str">"cmt">//============================================================================================================================================= class="type">bool GetChart(ChartType chart_type,CalcType calc_type,PLChart_item &out[]); class=class="str">"cmt">// 获取盈亏图 class="type">bool GetDistributionChart(class="type">bool isOneLot,DistributionChart &out); class=class="str">"cmt">// 获取分布图 class="type">bool GetCoefChart(class="type">bool isOneLot,CoefChartType type,CoefChart_item &out[]); class=class="str">"cmt">// 获取比率图 class="type">bool GetDailyPL(DailyPL_calcBy calcBy,DailyPL_calcType calcType,DailyPL &out); class=class="str">"cmt">// 按日获取盈亏图 class="type">bool GetRatioTable(class="type">bool isOneLot,ProfitDrawdownType type,ProfitDrawdown &out); class=class="str">"cmt">// 获得极值点表 class="type">bool GetTotalResult(TotalResult &out); class=class="str">"cmt">// 获取 TotalResult 表 class="type">bool GetPL_detales(PL_detales &out); class=class="str">"cmt">// 获取 PL_detales 表 class="type">void Get_Symb(class="kw">const DealDetales &history[],class="type">class="kw">string &Symb[]); class=class="str">"cmt">// 获取所交易品种的数组 class="type">void Clear(); class=class="str">"cmt">// 清楚统计 class="kw">private: class=class="str">"cmt">//============================================================================================================================================= class=class="str">"cmt">// 私有数据类型: class=class="str">"cmt">//============================================================================================================================================= class=class="str">"cmt">// 盈亏图类型的结构 class="kw">struct PL_keeper { PLChart_item PL_total[]; PLChart_item PL_oneLot[]; PLChart_item PL_Indicative[]; }; class=class="str">"cmt">// 每日盈亏图的类型结构 class="kw">struct DailyPL_keeper { DailyPL avarage_open,avarage_close,absolute_open,absolute_close; }; class=class="str">"cmt">// 极值点表的结构 class="kw">struct RatioTable_keeper {
◍ 回测数据容器的结构定义
在 MT5 策略回测的统计分析模块里,先把交易数据按维度拆成结构体能省掉大量重复计算。下面这段代码定义了利润回撤、连续盈亏计数、辅助计算项以及比率图形容器,实际跑 EA 时可直接照搬。 ProfitDrawdown 结构分了 Total 和 OneLot 两套字段,分别存最大回撤、绝对回撤和百分比回撤。S_dealsCounter 只放 Profit 和 DD 两个 int,用来连续累加盈亏金额;S_dealsInARow 继承它并内嵌一个 Counter 实例,方便统计连胜连亏序列。 CalculationData_item 是核心辅助结构:除了嵌套 dealsCounter,还用动态数组 R_arr[] 记录每笔 R 倍数,并用 double 字段存累计回撤、累计利润、盈亏比 PL,以及按单笔和按 PL 口径的最大回撤/最大利润与对应时间戳。CalculationData 再包一层 total 和 oneLot,加 num_deals 和 isNot_firstDeal 控制首次成交逻辑。 CoefChart_keeper 把夏普、胜率系数、恢复因子、利润因子、Altman Z 分数都按 OneLot 和 Total 分别存成图表序列,方便在小布盯盘里直接拉曲线看策略衰减。CHistoryComparer 类继承 ICustomComparer,重载 Compare 按成交时间排序,避免历史乱序导致回撤算错。 最后一组全局 keeper 实例(PL、DailyPL_data、RatioTable_data 等)配合 balance 和 r 两个 double,分别管初始存款与无风险利率。外汇与贵金属杠杆高,回测容器里的 DD_percent 若超过 30% 应倾向警惕实盘爆仓概率。
ProfitDrawdown Total_max,Total_absolute,Total_percent; ProfitDrawdown OneLot_max,OneLot_absolute,OneLot_percent; }; class=class="str">"cmt">// 用于连续计算利润和损失金额的结构 class="kw">struct S_dealsCounter { class="type">int Profit,DD; }; class="kw">struct S_dealsInARow : class="kw">public S_dealsCounter { S_dealsCounter Counter; }; class=class="str">"cmt">// 用于计算辅助数据的结构 class="kw">struct CalculationData_item { S_dealsInARow dealsCounter; class="type">int R_arr[]; class="type">class="kw">double DD_percent; class="type">class="kw">double Accomulated_DD,Accomulated_Profit; class="type">class="kw">double PL; class="type">class="kw">double Max_DD_forDeal,Max_Profit_forDeal; class="type">class="kw">double Max_DD_byPL,Max_Profit_byPL; class="type">class="kw">datetime DT_Max_DD_byPL,DT_Max_Profit_byPL; class="type">class="kw">datetime DT_Max_DD_forDeal,DT_Max_Profit_forDeal; class="type">int Total_DD_numDeals,Total_Profit_numDeals; }; class="kw">struct CalculationData { CalculationData_item total,oneLot; class="type">int num_deals; class="type">bool isNot_firstDeal; }; class=class="str">"cmt">// 用于创建比率图形的结构 class="kw">struct CoefChart_keeper { CoefChart_item OneLot_ShartRatio_chart[],Total_ShartRatio_chart[]; CoefChart_item OneLot_WinCoef_chart[],Total_WinCoef_chart[]; CoefChart_item OneLot_RecoveryFactor_chart[],Total_RecoveryFactor_chart[]; CoefChart_item OneLot_ProfitFactor_chart[],Total_ProfitFactor_chart[]; CoefChart_item OneLot_AltmanZScore_chart[],Total_AltmanZScore_chart[]; }; class=class="str">"cmt">// 参与按截止日期排序交易历史的类。 class CHistoryComparer : class="kw">public ICustomComparer<DealDetales> { class="kw">public: class="type">int Compare(DealDetales &x,DealDetales &y); }; class=class="str">"cmt">//============================================================================================================================================= class=class="str">"cmt">// Keepers: class=class="str">"cmt">//============================================================================================================================================= CHistoryComparer historyComparer; class=class="str">"cmt">// 比较类 CChartComparer chartComparer; class=class="str">"cmt">// 比较类 class=class="str">"cmt">// 辅助结构 PL_keeper PL,PL_hist,BH,BH_hist; DailyPL_keeper DailyPL_data; RatioTable_keeper RatioTable_data; TotalResult TotalResult_data; PL_detales PL_detales_data; DistributionChart OneLot_PDF_chart,Total_PDF_chart; CoefChart_keeper CoefChart_data; class="type">class="kw">double balance,r; class=class="str">"cmt">// 初始存款和无风险率
回测统计的核心计算接口
在 MT5 自建交易报告分析器时,盈亏与风险类指标通常拆成一堆细粒度函数,而不是塞进一个巨型循环。下面这段声明清单来自一个实盘级回测统计模块,覆盖从单笔成交到分布拟合的全链路。 CGenericSorter sorter; 这一行先挂了一个通用排序器,后面计算分位数、VaR 时大概率要靠它给成交序列排序。CalcPL 与 CalcPLHist 分别输出逐笔盈亏和盈亏直方图数组,参数里都带 CalcType,说明同一套逻辑能切真实手数或标准化手数两种口径。 每日维度由 CalcDailyPL 与 cmpDay 处理,后者接收 ENUM_DAY_OF_WEEK 作为基准日,用来横向比对着哪天容易回撤。盈利因子、恢复因子、胜率三个系数各有 *_chart_calc 版本,输出 CoefChart_item 数组,方便直接丢进子窗口画曲线。 ShartRatio_calc 返回 double,锋锐比率对序列尖峰比夏普更敏感;NormalPDF_chart_calc 配合 PDF_calc 做正态分布拟合,但贵金属跳空时正态假设常失效。VaR 用 quantile、均值、标准差三参估算,外汇与贵金属杠杆高,VaR 只反映历史波动概率,不代表极端行情下的实际亏损上限。 开 MT5 把这批函数签名抄进你的 EA 头文件,先只接 CalcPL + ProfitFactor_chart_calc 跑通一条流水线,比一次性全接更稳。
class=class="str">"cmt">// 排序类 CGenericSorter sorter; class=class="str">"cmt">//============================================================================================================================================= class=class="str">"cmt">// 计算: class=class="str">"cmt">//============================================================================================================================================= class=class="str">"cmt">// 计算盈亏 class="type">void CalcPL(class="kw">const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type); class=class="str">"cmt">// 计算盈亏直方图 class="type">void CalcPLHist(class="kw">const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type); class=class="str">"cmt">// 计算用于绘图的辅助结构 class="type">void CalcData(class="kw">const DealDetales &deal,CalculationData &out,class="type">bool isBH); class="type">void CalcData_item(class="kw">const DealDetales &deal,CalculationData_item &out,class="type">bool isOneLot); class=class="str">"cmt">// 计算每日盈利/亏损 class="type">void CalcDailyPL(DailyPL &out,DailyPL_calcBy calcBy,class="kw">const DealDetales &deal); class="type">void cmpDay(class="kw">const DealDetales &deal,ENUM_DAY_OF_WEEK etalone,PLDrawdown &ans,DailyPL_calcBy calcBy); class="type">void avarageDay(PLDrawdown &day); class=class="str">"cmt">// 比较品种 class="type">bool isSymb(class="kw">const class="type">class="kw">string &Symb[],class="type">class="kw">string symbol); class=class="str">"cmt">// 计算盈利因子 class="type">void ProfitFactor_chart_calc(CoefChart_item &out[],CalculationData &data,class="kw">const DealDetales &deal,class="type">bool isOneLot); class=class="str">"cmt">// 计算恢复因子 class="type">void RecoveryFactor_chart_calc(CoefChart_item &out[],CalculationData &data,class="kw">const DealDetales &deal,class="type">bool isOneLot); class=class="str">"cmt">// 计算胜率 class="type">void WinCoef_chart_calc(CoefChart_item &out[],CalculationData &data,class="kw">const DealDetales &deal,class="type">bool isOneLot); class=class="str">"cmt">// 计算锋锐比率 class="type">class="kw">double ShartRatio_calc(PLChart_item &data[]); class="type">void ShartRatio_chart_calc(CoefChart_item &out[],PLChart_item &data[],class="kw">const DealDetales &deal); class=class="str">"cmt">// 计算分布 class="type">void NormalPDF_chart_calc(DistributionChart &out,PLChart_item &data[]); class="type">class="kw">double PDF_calc(class="type">class="kw">double Mx,class="type">class="kw">double Std,class="type">class="kw">double x); class=class="str">"cmt">// 计算 VaR class="type">class="kw">double VaR(class="type">class="kw">double quantile,class="type">class="kw">double Mx,class="type">class="kw">double Std); class=class="str">"cmt">// 计算 Z 分数
「回测报告类的清算与复制接口」
在 MT5 自建回测报告模块时,CReportCreator 这类封装类通常会把数据生命周期拆成「计算前清场」和「图表数据克隆」两块。下面这段声明给出了明确的函数边界:AltmanZScore_chart_calc 负责把 N/R/W/L 四个系数推进 CoefChart_item 数组,CalcTotalResult 与 CalcPL_detales 则分别填充汇总结构和逐笔盈亏明细。 清算函数群值得注意——Clear_PL_keeper、Clear_DailyPL 一直到 Clear_CoefChart_keeper,覆盖了从日盈亏表到分布图的全部结构体。实盘跑 EA 前若不清这些残留,上一次标的的 XAUUSD 数据可能污染本次 EURUSD 回测,概率上会引发虚假的夏普偏高。 复制接口只有两个:CopyPL 拷盈亏曲线、CopyCoefChart 拷比率曲线,都用了 const 源加 out 目标的标准写法。如果你要在面板里做「当前品种 vs 基准」叠加显示,直接调这两个比手写循环安全。 Create 方法是入口,先 Clear() 再写余额。当传入 _balance<=0 时会用 dealGetter.getBalance 按末笔开仓时间反查,若仍为负则强行置 0;_r 小于 0 也夹到 0。随后 ZeroMemory 两个 CalculationData,再按升序排 history——这一步错了,后面所有统计周期都会偏移。
class="type">void AltmanZScore_chart_calc(CoefChart_item &out[],class="type">class="kw">double N,class="type">class="kw">double R,class="type">class="kw">double W,class="type">class="kw">double L,class="kw">const DealDetales &deal); class=class="str">"cmt">// 计算 TotalResult_item 结构 class="type">void CalcTotalResult(CalculationData &data,class="type">bool isOneLot,TotalResult_item &out); class=class="str">"cmt">// 计算 PL_detales_item 结构 class="type">void CalcPL_detales(CalculationData_item &data,class="type">int deals_num,PL_detales_item &out); class=class="str">"cmt">// 从日期中获取天 ENUM_DAY_OF_WEEK getDay(class="type">class="kw">datetime DT); class=class="str">"cmt">// 清除数据 class="type">void Clear_PL_keeper(PL_keeper &data); class="type">void Clear_DailyPL(DailyPL &data); class="type">void Clear_RatioTable(RatioTable_keeper &data); class="type">void Clear_TotalResult_item(TotalResult_item &data); class="type">void Clear_PL_detales(PL_detales &data); class="type">void Clear_DistributionChart(DistributionChart &data); class="type">void Clear_CoefChart_keeper(CoefChart_keeper &data); class=class="str">"cmt">//============================================================================================================================================= class=class="str">"cmt">// 复制: class=class="str">"cmt">//============================================================================================================================================= class="type">void CopyPL(class="kw">const PLChart_item &src[],PLChart_item &out[]); class=class="str">"cmt">// 复制盈亏图形 class="type">void CopyCoefChart(class="kw">const CoefChart_item &src[],CoefChart_item &out[]); class=class="str">"cmt">// 复制比率图形 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算/重新计算比率 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CReportCreator::Create(DealDetales &history[],DealDetales &BH_history[],class="kw">const class="type">class="kw">double _balance,class="kw">const class="type">class="kw">string &Symb[],class="type">class="kw">double _r) { Clear(); class=class="str">"cmt">// 清除数据 class=class="str">"cmt">// 保存余额 this.balance=_balance; if(this.balance<=class="num">0) { CDealHistoryGetter dealGetter; this.balance=dealGetter.getBalance(history[ArraySize(history)-class="num">1].DT_open); } if(this.balance<class="num">0) this.balance=class="num">0; class=class="str">"cmt">// 保存无风险比率 if(_r<class="num">0) _r=class="num">0; this.r=r; class=class="str">"cmt">// 辅助结构 CalculationData data_H,data_BH; ZeroMemory(data_H); ZeroMemory(data_BH); class=class="str">"cmt">// 排序交易历史 sorter.Method(Sort_Ascending); sorter.Sort<DealDetales>(history,&historyComparer); class=class="str">"cmt">// 通过交易历史循环
◍ 回测数据怎么塞进损益表
这段逻辑干的是把历史成交和买入持有记录分别跑一遍统计,再汇总成可对比的损益矩阵。先按品种过滤 history 与 BH_history,调用 CalcData 填充 data_H 与 data_BH,购买并持有序列还先做了一次排序。 [CODE]for(int i=0;i<ArraySize(history);i++) { if(isSymb(Symb,history[i].symbol)) CalcData(history[i],data_H,false); } // 排序购买并持有历史记录和相应的循环 sorter.Sort<DealDetales>(BH_history,&historyComparer); for(int i=0;i<ArraySize(BH_history);i++) { if(isSymb(Symb,BH_history[i].symbol)) CalcData(BH_history[i],data_BH,true); }[/CODE] 上面代码逐行拆:第1行开循环扫全部历史成交;第3行用 isSymb 比对当前品种,命中才进第4行 CalcData 算逐笔数据(false 表示非固定手数口径);第7行对买入持有序列按比较器排序,避免时间错乱;第9–13行同理过滤 BH_history 并标 true 走单手数归一化统计。 随后对周一至周五的收盘价、开盘价均值各调一次 avarageDay,共10次调用,把 DailyPL_data 里的平均日盈亏补齐。接着直接读 RatioTable_data 各字段——累计利润、累计回撤、单笔最大利润、单笔最大回撤,以及用总盈利笔数除以总成交笔数算出的胜率类比例,这部分没有中间计算,是纯字段搬运。 正态分布用 NormalPDF_chart_calc 对单手和总仓盈亏数组各算一次密度曲线;CalcTotalResult 与 CalcPL_detales 分别填总结果与明细。CalcData_item 内部则按 pl>=0 分流:盈利时 Total_Profit_numDeals 加1、连盈计数加1、连亏清零;亏损时反之,并用 MathMax 保留历史最长连盈连亏。外汇与贵金属回测请记着:样本外失效概率不低,连亏长度只是描述而非预警。
for(class="type">int i=class="num">0;i<ArraySize(history);i++) { if(isSymb(Symb,history[i].symbol)) CalcData(history[i],data_H,class="kw">false); } class=class="str">"cmt">// 排序购买并持有历史记录和相应的循环 sorter.Sort<DealDetales>(BH_history,&historyComparer); for(class="type">int i=class="num">0;i<ArraySize(BH_history);i++) { if(isSymb(Symb,BH_history[i].symbol)) CalcData(BH_history[i],data_BH,true); } class=class="str">"cmt">// 平均每日盈亏(平均类型) avarageDay(DailyPL_data.avarage_close.Mn); avarageDay(DailyPL_data.avarage_close.Tu); avarageDay(DailyPL_data.avarage_close.We); avarageDay(DailyPL_data.avarage_close.Th); avarageDay(DailyPL_data.avarage_close.Fr); avarageDay(DailyPL_data.avarage_open.Mn); avarageDay(DailyPL_data.avarage_open.Tu); avarageDay(DailyPL_data.avarage_open.We); avarageDay(DailyPL_data.avarage_open.Th); avarageDay(DailyPL_data.avarage_open.Fr); class=class="str">"cmt">// 填写损益表 RatioTable_data.data_H.oneLot.Accomulated_Profit; RatioTable_data.data_H.oneLot.Accomulated_DD; RatioTable_data.data_H.oneLot.Max_Profit_forDeal; RatioTable_data.data_H.oneLot.Max_DD_forDeal; RatioTable_data.data_H.oneLot.Total_Profit_numDeals/data_H.num_deals; RatioTable_data.data_H.oneLot.Total_DD_numDeals/data_H.num_deals; RatioTable_data.Total_absolute.Profit=data_H.total.Accomulated_Profit; RatioTable_data.Total_absolute.Drawdown=data_H.total.Accomulated_DD; RatioTable_data.Total_max.Profit=data_H.total.Max_Profit_forDeal; RatioTable_data.Total_max.Drawdown=data_H.total.Max_DD_forDeal; RatioTable_data.Total_percent.Profit=data_H.total.Total_Profit_numDeals/data_H.num_deals; RatioTable_data.Total_percent.Drawdown=data_H.total.Total_DD_numDeals/data_H.num_deals; class=class="str">"cmt">// 计算正态分布 NormalPDF_chart_calc(OneLot_PDF_chart,PL.PL_oneLot); NormalPDF_chart_calc(Total_PDF_chart,PL.PL_total); class=class="str">"cmt">// TotalResult CalcTotalResult(data_H,true,TotalResult_data.oneLot); CalcTotalResult(data_H,class="kw">false,TotalResult_data.total); class=class="str">"cmt">// PL_detales CalcPL_detales(data_H.oneLot,data_H.num_deals,PL_detales_data.oneLot); CalcPL_detales(data_H.total,data_H.num_deals,PL_detales_data.total); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算辅助数据 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CReportCreator::CalcData_item(class="kw">const DealDetales &deal,CalculationData_item &out, class="type">bool isOneLot) { class="type">class="kw">double pl=(isOneLot ? deal.pl_oneLot : deal.pl_forDeal); class=class="str">"cmt">// PL class="type">int n=class="num">0; class=class="str">"cmt">// 损益金额 if(pl>=class="num">0) { out.Total_Profit_numDeals++; n=class="num">1; out.dealsCounter.Counter.DD=class="num">0; out.dealsCounter.Counter.Profit++; } else { out.Total_DD_numDeals++; out.dealsCounter.Counter.DD++; out.dealsCounter.Counter.Profit=class="num">0; } out.dealsCounter.DD=MathMax(out.dealsCounter.DD,out.dealsCounter.Counter.DD); out.dealsCounter.Profit=MathMax(out.dealsCounter.Profit,out.dealsCounter.Counter.Profit); class=class="str">"cmt">// 损益序列 class="type">int s=ArraySize(out.R_arr); if(!(s>class="num">0 && out.R_arr[s-class="num">1]==n)) {
逐笔统计盈亏与回撤的落点
这段逻辑跑在每笔成交闭合之后,把单笔盈亏 pl 累加进总利润 out.PL,同时记录该笔的最大盈利与最大回撤。若 pl 小于当前 Max_DD_forDeal(初始应设 0 或更大值),就刷新最大回撤及其发生时间 DT_Max_DD_forDeal;反之 pl 大于 Max_Profit_forDeal 则更新最大单笔盈利时间。
| 累计层面用 Accomulated_Profit 与 Accomulated_DD 分别承接正负 pl:pl>0 时前者加 pl、后者加 0,否则反过来。基于总权益曲线再算极值——maxPL 取历史高点,DD 在盈利为正时为 PL-maxPL,否则为 -( | PL | +maxPL),据此更新 Max_DD_byPL 与对应日期。 |
|---|
DD_percent 的计算值得在 MT5 里单步验证:balance>0 时分母取 maxPL 与 balance 的较大者,否则仅用 maxPL,避免除零。外汇与贵金属回测中该比率对初始仓位极敏感,样本外可能明显放大。 CParamsFiltre 类负责把优化库卸载后的参数做升序排序缓存,Total() 返回 arr_main 大小,Add() 支持带 ID 与 total_PL 的写入,GetCustomCoef 按 ID 取自定义手数依赖系数,这几个接口是后续筛参的入口。
ArrayResize(out.R_arr,s+class="num">1,s+class="num">1); out.R_arr[s]=n; } out.PL+=pl; class=class="str">"cmt">// Total PL class=class="str">"cmt">// 最大盈利 / 回撤 if(out.Max_DD_forDeal>pl) { out.Max_DD_forDeal=pl; out.DT_Max_DD_forDeal=deal.DT_close; } if(out.Max_Profit_forDeal<pl) { out.Max_Profit_forDeal=pl; out.DT_Max_Profit_forDeal=deal.DT_close; } class=class="str">"cmt">// 累计利润/ 回撤 out.Accomulated_DD+=(pl>class="num">0 ? class="num">0 : pl); out.Accomulated_Profit+=(pl>class="num">0 ? pl : class="num">0); class=class="str">"cmt">// 利润极值点 class="type">class="kw">double maxPL=MathMax(out.Max_Profit_byPL,out.PL); if(compareDouble(maxPL,out.Max_Profit_byPL)==class="num">1class=class="str">"cmt">/* || !isNot_firstDeal*/)class=class="str">"cmt">// yet another check is required for saving the date { out.DT_Max_Profit_byPL=deal.DT_close; out.Max_Profit_byPL=maxPL; } class="type">class="kw">double maxDD=out.Max_DD_byPL; class="type">class="kw">double DD=class="num">0; if(out.PL>class="num">0)DD=out.PL-maxPL; else DD=-(MathAbs(out.PL)+maxPL); maxDD=MathMin(maxDD,DD); if(compareDouble(maxDD,out.Max_DD_byPL)==-class="num">1class=class="str">"cmt">/* || !isNot_firstDeal*/)class=class="str">"cmt">// 另外的检查需要保存日期 { out.Max_DD_byPL=maxDD; out.DT_Max_DD_byPL=deal.DT_close; } out.DD_percent=(balance>class="num">0 ?(MathAbs(DD)/(maxPL>class="num">0 ? maxPL : balance)) :(maxPL>class="num">0 ?(MathAbs(DD)/maxPL) : class="num">0)); } class=class="str">"cmt">//+--------------------------------------------------------------------------+ class=class="str">"cmt">//| 从数据库中卸载它们之后排序优化递次的类 | class=class="str">"cmt">//+--------------------------------------------------------------------------+ class CParamsFiltre { class="kw">public: CParamsFiltre(){sorter.Method(Sort_Ascending);} class=class="str">"cmt">// 默认构造函数 class="type">int Total(){class="kw">return ArraySize(arr_main);}; class=class="str">"cmt">// 卸载参数总数(根据优化数据表) class="type">void Clear(){ArrayFree(arr_main);ArrayFree(arr_result);}; class=class="str">"cmt">// 清除所有数组 class="type">void Add(LotDependency_item &customCoef,CDataKeeper ¶ms[],class="type">long ID,class="type">class="kw">double total_PL,class="type">bool addToResult); class=class="str">"cmt">// 向数组添加新值 class="type">class="kw">double GetCustomCoef(class="type">long ID,class="type">bool isOneLot);class=class="str">"cmt">// 按 ID 获取自定义比率 class="type">void GetParamNames(CArrayString &out);class=class="str">"cmt">// 获取 EA 参数名称 class="type">void Get_UniqueCoef(UniqCoefData_item &data[],class="type">class="kw">string paramName,CArrayString &coefValue); class=class="str">"cmt">// 获得独有的比率
「分布与过滤的底层接口」
这段类声明把回测结果的后处理拆得很干净:公开方法负责按时间区间过滤、重置过滤状态,以及用两个数组拼出分布图。 Filtre() 接收起止字符串和 ID 数组引用,实质是对 arr_result 做排序,把不符合区间的 ID 剔除;ResetFiltre() 则把传入的 ID 数组还原为初始全集,方便反复切条件。 Get_Distribution 有两个重载:一个吃主表标志位直接建分布,另一个按指定字段名和值建分布,给了按单一变量切片的能力。 私有段里 Mx() 和 Std() 分别算 CoefStruct 数组的算术平均与标准差,意味着你能在 MT5 里直接拿这两个值做正态假设检验,但外汇与贵金属品种序列常带厚尾,样本标准差可能低估极端跳空风险。 想验证的话,把这段声明贴进 EA 的统计分析类,接一组真实优化结果跑 Filtre + Get_Distribution,看分布形状再决定阈值。
class="type">void Filtre(class="type">class="kw">string Name,class="type">class="kw">string from,class="type">class="kw">string till,class="type">long &ID_Arr[]);class=class="str">"cmt">// 对 arr_result 数组进行排序 class="type">void ResetFiltre(class="type">long &ID_arr[]);class=class="str">"cmt">// 重置过滤器 class="type">bool Get_Distribution(Chart_item &out[],class="type">bool isMainTable);class=class="str">"cmt">// 通过两个数组创建分布 class="type">bool Get_Distribution(Chart_item &out[],class="type">class="kw">string Name,class="type">class="kw">string value);class=class="str">"cmt">// 按选定数据创建分布 class="kw">private: CGenericSorter sorter; class=class="str">"cmt">// 排序器 CCoefComparer cmp_coef;class=class="str">"cmt">// 比较比率 CChartComparer cmp_chart;class=class="str">"cmt">// 比较图形 class="type">bool selectCoefByName(CDataKeeper &_input[],CDataKeeper &out,class="type">class="kw">string Name);class=class="str">"cmt">// 按名称选择比率 class="type">class="kw">double Mx(CoefStruct &_arr[]);class=class="str">"cmt">// 算术平均值 class="type">class="kw">double Std(CoefStruct &_arr[],class="type">class="kw">double _Mx);class=class="str">"cmt">// 标准偏差 CoefStruct arr_main[]; class=class="str">"cmt">// 优化数据表等效衡 CoefStruct arr_result[];class=class="str">"cmt">// 结果表等效 };
◍ 演播器如何解耦图形层与逻辑层
在 MT5 自建分析工具的架构里,演播器(Presenter)扮演的是图形界面和上层业务逻辑之间的连接器。它用抽象接口 IPresenter 定义回调方法名,具体类 CPresenter 继承该接口并实现全部方法,这样重写演播器模块时不会牵动图形模块或核心逻辑。 接口里一共声明了 18 个回调,覆盖从数据下载、报表生成到各类图表构建的动作。比如 Btn_Update_Click 负责下载数据并重建整个窗体,PL_pressed_3 按结果表全量数据画盈亏图,show_FriquencyChart 控制盈亏频率图显隐。 CPresenter 的构造函数接收 CWindowManager 指针,说明它直接持有窗口管理句柄。若你在 EA 面板里想加一个自定义筛选按钮,只需在接口补一行声明、在类里实现 Filtre_click 类似逻辑,图形端发信号即可,不必改数据库层。 开 MT5 用 MetaEditor 建一个继承 IPresenter 的空类,编译能过就证明你的分层没破环;之后逐步填方法,比把所有点击逻辑写进窗体事件里好维护得多。外汇与贵金属行情波动剧烈,这类工具仅用于历史与实时数据梳理,不构成任何方向暗示。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 演播器接口 | class=class="str">"cmt">//+------------------------------------------------------------------+ interface IPresenter { class="type">void Btn_Update_Click(); class=class="str">"cmt">// 下载数据并构建整个窗体 class="type">void Btn_Load_Click(); class=class="str">"cmt">// 创建报告 class="type">void OptimisationData(class="type">bool isMainTable);class=class="str">"cmt">// 在表中选择优化行 class="type">void Update_PLByDays(); class=class="str">"cmt">// 按天上传损益 class="type">void DaySelect();class=class="str">"cmt">// 从盈亏表中按周内天数选择一天 class="type">void PL_pressed(PLSelected_type type);class=class="str">"cmt">// 按选定历史记录构建盈亏图 class="type">void PL_pressed_2(class="type">bool isRealPL);class=class="str">"cmt">// 构建 "其它图表" 图形 class="type">void SaveToFile_Click();class=class="str">"cmt">// 保存数据文件(到沙箱) class="type">void SaveParam_passed(SaveParam_type type);class=class="str">"cmt">// 选择要写入文件的数据 class="type">void OptimisationParam_selected(); class=class="str">"cmt">// 选择优化参数并填写“优化选择”选项卡 class="type">void CompareTables(class="type">bool isChecked);class=class="str">"cmt">// 通过结果表构建分布(用于与共用(主要)表的关联) class="type">void show_FriquencyChart(class="type">bool isChecked);class=class="str">"cmt">// 显示盈亏频率图 class="type">void FriquencyChart_click();class=class="str">"cmt">// 在比率表中选择一行并构建分布 class="type">void Filtre_click();class=class="str">"cmt">// 按选定条件排序 class="type">void Reset_click();class=class="str">"cmt">// 重置过滤器 class="type">void PL_pressed_3(class="type">bool isRealPL);class=class="str">"cmt">// 按结果表中的所有数据构建盈亏图 class="type">void PL_pressed_4(class="type">bool isRealPL);class=class="str">"cmt">// 构建统计表 class="type">void setChartFlag(class="type">bool isPlot);class=class="str">"cmt">// 从 PL_pressed_3(class="type">bool isRealPL) 方法构建(或不构建)图形的条件; }; class CPresenter : class="kw">public IPresenter { class="kw">public: CPresenter(CWindowManager *_windowManager); class=class="str">"cmt">// 构造函数 class="type">void Btn_Update_Click();class=class="str">"cmt">// 下载数据并构建整个窗体 class="type">void Btn_Load_Click();class=class="str">"cmt">// 创建报告 class="type">void OptimisationData(class="type">bool isMainTable);class=class="str">"cmt">// 在表中选择优化行 class="type">void Update_PLByDays();class=class="str">"cmt">// 按天上传损益 class="type">void PL_pressed(PLSelected_type type);class=class="str">"cmt">// 按选定历史记录构建盈亏图 class="type">void PL_pressed_2(class="type">bool isRealPL);class=class="str">"cmt">// 构建 "其它图表" 图形 class="type">void SaveToFile_Click();class=class="str">"cmt">// 保存数据文件(到沙箱) class="type">void SaveParam_passed(SaveParam_type type);class=class="str">"cmt">// 选择要写入文件的数据 class="type">void OptimisationParam_selected();class=class="str">"cmt">// 选择优化参数并填写“优化选择”选项卡 class="type">void CompareTables(class="type">bool isChecked);class=class="str">"cmt">// 通过结果表构建分布(用于与共用(主要)表的关联) class="type">void show_FriquencyChart(class="type">bool isChecked);class=class="str">"cmt">// 显示盈亏频率图
报表类的私有方法暴露了数据流转细节
这段声明把交易统计面板的底层类结构摊开了:公开部分负责交互回调,私有部分才真正干活。想改 EA 的回测报告生成逻辑,得先看懂 CReportCreator 和 CDBReader 怎么配合。 公开方法里 FriquencyChart_click() 管比率表选行并画分布,PL_pressed_3(bool isRealPL) 按结果表全量数据构建盈亏图,setChartFlag(bool isPlot) 则决定图形是否真的渲染出来——这几个钩子是你接 GUI 事件的主入口。 私有段更关键:loadData() 从数据库拉数据填表,insertDataTo_main_Table() 同时写结果表和“Main”优化递次比率表,curveAdd() 被重载三次分别接 PLChart_item、CoefChart_item、Distribution_item,说明图形层用同一接口吞三种数据结构。 在 MT5 里新建一个继承该类骨架的脚本,把 dbReader 指向你自己的交易历史库,就能复刻这套按周内天数筛选(DaySelect)再重置过滤器(Reset_click)的流水线。外汇与贵金属杠杆高,回测分布再漂亮也只代表历史样本,实盘滑点和点差会吃掉部分边界收益。
class="type">void FriquencyChart_click();class=class="str">"cmt">// 在比率表中选择一行并构建分布 class="type">void Filtre_click();class=class="str">"cmt">// 按选定条件排序 class="type">void PL_pressed_3(class="type">bool isRealPL);class=class="str">"cmt">// 按结果表中的所有数据构建盈亏图 class="type">void PL_pressed_4(class="type">bool isRealPL);class=class="str">"cmt">// 构建统计表 class="type">void DaySelect();class=class="str">"cmt">// 从盈亏表中按周内天数选择一天 class="type">void Reset_click();class=class="str">"cmt">// 重置过滤器 class="type">void setChartFlag(class="type">bool isPlot);class=class="str">"cmt">// 从 PL_pressed_3(class="type">bool isRealPL) 方法构建(或不构建)图形的条件; class="kw">private: CWindowManager *windowManager;class=class="str">"cmt">// 窗口类的引用 CDBReader dbReader;class=class="str">"cmt">// 操纵数据库的类 CReportCreator reportCreator; class=class="str">"cmt">// 处理数据的类 CGenericSorter sorter; class=class="str">"cmt">// 排序类 CoefData_comparer coefComparer; class=class="str">"cmt">// 数据比较类 class="type">void loadData();class=class="str">"cmt">// 从数据库上传数据并填写表 class="type">void insertDataTo_main_Table(class="type">bool isResult,class="kw">const CoefData_item &data[]); class=class="str">"cmt">// 将数据插入结果表和“Main”表(表内为优化递次比率) class="type">void insertRowTo_main_Table(CTable *tb,class="type">int n,class="kw">const CoefData_item &data); class=class="str">"cmt">// 直接将数据插入优化递次表 class="type">void selectChartByID(class="type">long ID,class="type">bool recalc=true);class=class="str">"cmt">// 按 ID 选择图形 class="type">void createReport();class=class="str">"cmt">// 创建报告 class="type">class="kw">string getCorrectPath(class="type">class="kw">string path,class="type">class="kw">string name);class=class="str">"cmt">// 获取文件的正确路径 class="type">bool getPLChart(PLChart_item &data[],class="type">bool isOneLot,class="type">long ID); class="type">bool curveAdd(CGraphic *chart_ptr,class="kw">const PLChart_item &data[],class="type">bool isHist);class=class="str">"cmt">// 将图形添加到其它图表 class="type">bool curveAdd(CGraphic *chart_ptr,class="kw">const CoefChart_item &data[],class="type">class="kw">double borderPoint);class=class="str">"cmt">// 将图形添加到其它图表 class="type">bool curveAdd(CGraphic *chart_ptr,class="kw">const Distribution_item &data);class=class="str">"cmt">// 将图形添加到其它图表 class="type">void setCombobox(CComboBox *cb_ptr,CArrayString &arr,class="type">bool isFirstIndex=true);class=class="str">"cmt">// 设置组合框参数 class="type">void addPDF_line(CGraphic *chart_ptr,class="type">class="kw">double &x[],class="type">class="kw">color clr,class="type">int width,class="type">class="kw">string _name=NULL);class=class="str">"cmt">// 添加分布图的平滑线 class="type">void plotMainPDF();class=class="str">"cmt">// 通过“Main”表构建分布(优化数据) class="type">void updateDT(CDropCalendar *dt_ptr,class="type">class="kw">datetime DT);class=class="str">"cmt">// 更新下拉日历 CParamsFiltre coefKeeper;class=class="str">"cmt">// 排序优化递次(按分布)
「盈亏图与优化表的状态字段怎么摆」
做 MT5 面板时,盈亏图系列和优化递次表共用一套后台状态,直接塞进一个结构体最省事。下面这段声明把标题、按钮触发、选中 ID、数据区间和间隙类型全收在一个作用域里,复制进 EA 头文件就能编译。 _isUpbateClick 这个布尔位很关键:它同时管「按钮按下后重绘」和「从数据库拉数」两个动作,漏掉就会遇到点了没反应的假死。_selectedID 用 long 存,盈利系列染绿、亏损系列染红,前端直接读这个值上色。 _DT_from 与 _DT_till 是 datetime 类型,限定报表拉取窗口;_Gap 存 double,记下点差或滑点模拟的附加间隙。外汇与贵金属波动剧烈、杠杆高风险,这类状态若不清零,跨品种切换时可能把上一次的间隙误带进新回测。 别把正态当圣经:字段顺序不影响逻辑,但把数组 _ID_Arr[] 和标量 _ID 写在同一行只是作者习惯,你自己拆开更利于后期维护。
CArrayString headder; class=class="str">"cmt">// 比率表标题 class="type">bool _isUpbateClick; class=class="str">"cmt">// 按下按钮的更新,以及从数据库加载数据的标志 class="type">long _selectedID; class=class="str">"cmt">// 所有选定盈亏图系列的 ID(如果亏损则为红色,如果盈利则为绿色) class="type">long _ID,_ID_Arr[];class=class="str">"cmt">// 上载数据后为结果表所选择的 ID 数组 class="type">bool _IsForvard_inTables,_IsForvard_inReport; class=class="str">"cmt">// 优化递次表中优化数据类型的标志 class="type">class="kw">datetime _DT_from,_DT_till; class="type">class="kw">double _Gap; class=class="str">"cmt">// 已保存的添加间隙类型(点差/或滑点模拟...) };
◍ 把优化历史接进数据库后怎么挑递次
整套工具链已经能把测试器跑出来的每一轮优化参数落进 SQLite,EA 里挂上 CDBWriter 类就能自动写库,不限于样本里的 2MA_Martin。除了按选定时间段回看全部比率,还能把 Gap 参数调大来模拟滑点,直接观察图形和胜率曲线怎么变形。 最快拿到 100 个能看的递次:把 CDBWriter 接到机器人,像附加的 sample EA 那样设条件过滤器(比如利润因子 >= 1 先剔掉亏损组),Show n params 保持 100 点“更新”,表内就只剩过滤后的前百。 附件里 Experts/OptimisationSelector 下的 ParamsFiltre 负责按比率排序分布,Include/Sqlite3 这套封装了 32/64 位终端用的 Sqlite3 dll,实盘前先在 MT5 用 2MA_Martin 优化数据.sqlite 跑一遍读库逻辑,确认 DLL 权限开了再谈别的。外汇和贵金属品种波动跳空频繁,这种历史优化筛选只代表过去样本,接真金白银前务必用小资金验证。