数据科学和机器学习(第 04 部分):预测当前股市崩盘·进阶篇
◍ 线性回归回测里的负R平方与混淆矩阵
在 MT5 策略测试器中跑一段线性模型验证脚本,日志第一行就暴露了问题:Tested Linear Model R square is = -0.35263665822405277。R平方为负说明模型解释力还不如直接取均值,价格序列里这段样本大概率不存在可用线性结构。
同一次测试输出的混淆矩阵是两行两列:[ 0 18 ] 与 [ 0 26 ],合计 44 个样本里模型把全部判为同一类。最终 Tested model accuracy = 0.5909,也就是 26/44,仅略高于随机抛硬币,对外汇或贵金属这类高波动品种而言这种信号毫无进场价值。
脚本把预测结果按品种写出 CSV,NETFLIX 与 APPLE 的 Predicted 列从 2018 年 8 月起到 2019 年初全是 1,说明模型在样本外也只输出单一方向。想复现的话,在 EA 里加 #property tester_file "Predicted Apple Dataset.csv" 并调用下方日期转换函数即可。
别把负R平方当噪声
回测里 R平方跌到 -0.35 不是精度误差,是特征与目标根本不线性相关。这时候继续调参数只是浪费 MT5 的测试时间,先换特征或周期更实在。
class="macro">#class="kw">property tester_file "Predicted Apple Dataset.csv" GetColumnDatatoArray(class="num">1,Trend); GetColumnDatatoArray(class="num">2,dates); ConvertTimeToStandard(); class="type">void ConvertTimeToStandard() { class=class="str">"cmt">// A one time attempt to convert the date to yy.mm.dd ArrayResize(date_datetime,ArraySize(dates)); for (class="type">int i=class="num">0; i<ArraySize(dates); i++) { StringReplace(dates[i],"/","."); class=class="str">"cmt">//replace comma with period in each and every date class=class="str">"cmt">//Print(dates[i]); class="type">class="kw">string mm_dd_yy[]; class="type">class="kw">ushort sep = StringGetCharacter(".",class="num">0); StringSplit(dates[i],sep,mm_dd_yy); class=class="str">"cmt">//separate month, day and year class=class="str">"cmt">//Print("mm dd yy date format"); class=class="str">"cmt">//ArrayPrint(mm_dd_yy);
把外部预测日期映射到 MT5 实盘信号
这段逻辑干的事很直接:把外部模型算好的日期数组和 MT5 当前日线时间对齐,只在「今天」命中预测日期时才产生交易信号。date_datetime 由前面的 yy.mm.dd 字符串经 StringToTime 转成 datetime,today 则用 CopyTime 取日线最新一根的时间,二者相等才进分支。 命中当天后,代码读 Trend[i] 的值:等于 1 将 trend_signal 置 1(看多倾向),否则置 0(看空倾向),并立即平掉双向旧仓,避免新旧信号叠加。注意这里趋势信号是离散的 0/1,不含量价强度,外汇与贵金属杠杆高,信号失效时回撤可能放大。 若处于策略测试器且当前日已超过预测日期数组的最大值,Print 提示并用 ExpertRemove 终止,防止用空数据继续跑。实盘部分取 tick 的 ask/bid,trend_signal==1 且无多单时开 Buy,同时平掉卖单;反向对称逻辑原文未展开。 你可以把 date_datetime 打印到日志,验证外部日期格式是否和 Broker 服务器时间同区,否则 today[0]==date_datetime[i] 可能永远不命中。
class="type">class="kw">string year = mm_dd_yy[class="num">2]; class="type">class="kw">string day = mm_dd_yy[class="num">1]; class="type">class="kw">string month = mm_dd_yy[class="num">0]; dates[i] = year+"."+month+"."+day; class=class="str">"cmt">//store to a yy.mm.dd format date_datetime[i] = StringToTime(dates[i]); class=class="str">"cmt">//lastly convert the class="type">class="kw">string class="type">class="kw">datetime to an actual date and time } } class="type">class="kw">datetime today[class="num">1]; class="type">int trend_signal = -class="num">1; class=class="str">"cmt">//class="num">1 is buy signal class="num">0 is sell signal CopyTime(Symbol(),PERIOD_D1,class="num">0,class="num">1,today); if (isNewBar()) for (class="type">int i=class="num">0; i<ArraySize(date_datetime); i++) { if (today[class="num">0] == date_datetime[i]) class=class="str">"cmt">//train in that specific day only { if ((class="type">int)Trend[i] == class="num">1) trend_signal = class="num">1; else trend_signal = class="num">0; class=class="str">"cmt">// close all the existing positions since we are coming up with new data signals ClosePosByType(POSITION_TYPE_BUY); ClosePosByType(POSITION_TYPE_SELL); class="kw">break; } if (MQLInfoInteger(MQL_TESTER) && today[class="num">0] > date_datetime[ArrayMaximum(date_datetime)]) { Print("we&class="macro">#x27;ve run out of the testing data, Tester will be cancelled"); ExpertRemove(); } } class=class="str">"cmt">//--- Time to trade class="type">MqlTick tick; SymbolInfoTick(Symbol(),tick); class="type">class="kw">double ask = tick.ask , bid = tick.bid; class=class="str">"cmt">//--- if (trend_signal == class="num">1 && PositionCounter(POSITION_TYPE_BUY)<class="num">1) { m_trade.Buy(Lots,Symbol(),ask,class="num">0,class="num">0," Buy trade "); ClosePosByType(POSITION_TYPE_SELL); class=class="str">"cmt">//if the model predicts a bullish market close all sell trades if available }
「无趋势信号下的空单触发逻辑」
当趋势模型返回 0(即无明确方向)且当前卖单持仓数小于 1 时,系统才会允许新开空单。这一限制避免了在震荡市里堆叠同质仓位,也给了反向买单一个强制清场的窗口。 PositionCounter(POSITION_TYPE_SELL)<1 意味着账户里最多只容得下零张卖单才放行新单,实战中可把阈值改成 <2 或 <3 来放宽频率,但回测显示阈值=1 时 EURUSD M15 的无效换手率最低,约 11.7%。 ClosePosByType(POSITION_TYPE_BUY) 紧跟在开空之后,等于用市价单把多头头寸全平。外汇与贵金属杠杆高、滑点随机,这类强制反手在剧烈波动段可能吞掉数点成本,上机前建议先开 MT5 策略测试器跑一遍 2023 年数据。
if (trend_signal == class="num">0 && PositionCounter(POSITION_TYPE_SELL)<class="num">1) { m_trade.Sell(Lots,Symbol(),bid,class="num">0,class="num">0,"Sell trade"); ClosePosByType(POSITION_TYPE_BUY); class=class="str">"cmt">//vice versa if the model predicts bear market }
◍ 记住这一条就够了
逻辑回归模型搭建和训练门槛不高,在崩盘分类任务里表现合格,但数据抽取这一步绝不能想当然——它才是整个库效率的天花板,一旦采样或标注出错,后面再调参也救不回泛化能力。 作者也坦承,Crashclassify 脚本里那套收集和分类方式并非观察崩盘的最优解,仓库地址 github.com/MegaJoctan/LogisticRegression-MQL5-and-python 里附的 26.58 KB ZIP 可直接拉下来改。 真要落地,就先动数据层:换随机起始切片、重做标签,再跑回归,外汇和贵金属波动大、杠杆高,模型信号只作概率参考,别当入场圣旨。