数据科学和机器学习(第 04 部分):预测当前股市崩盘·进阶篇
📘

数据科学和机器学习(第 04 部分):预测当前股市崩盘·进阶篇

第 2/2 篇

◍ 线性回归回测里的负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 的测试时间,先换特征或周期更实在。

MQL5 / C++
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] 可能永远不命中。

MQL5 / C++
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 年数据。

MQL5 / C++
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 可直接拉下来改。 真要落地,就先动数据层:换随机起始切片、重做标签,再跑回归,外汇和贵金属波动大、杠杆高,模型信号只作概率参考,别当入场圣旨。

常见问题

负R平方说明模型解释力比直接取均值还差,回测区间内预测方向与真实偏离大;可先检查特征滞后与样本外切分,不要直接用于实盘信号。
把预测的崩盘日映射成该品种的时间轴偏移,在对应K线收线后读取信号状态;建议先用历史数据对齐验证再接实盘。
小布可把外部预测结果接入对应品种页,自动标注信号日并推送提醒,你只需核对偏移是否对齐历史。
无趋势段模型容易输出低频误触发,建议叠加波动率过滤或最小持仓间距,贵金属外汇属高风险需严控仓位。
假阳性高意味着空单可能常踩空,回测盈利靠少数大崩盘;实盘前用分段样本看稳定性,勿按单次回测直接跟。