请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化策略 帖子:3353079 新帖:39

形态搜索的暴力强推方式(第四部分):最小功能

谎言梦发表于:6 月 9 日 23:00回复(1)
在本文中,我将带来产品的新版本,其中拥有最少的功能,且能够提取可供交易的操作设置。 这些变化首要瞄准提升易用性,并改进最低准入门槛。 主要目标仍然是吸引尽可能多的用户进行设定搜索和市场调查。 我将提供尽可能多的有用信息,来取代讨论交易细节。 我将尝试提供一些易于理解的,如何运用这种方法的解释,讲述其优缺点,以及研究实际应用的前景。

  • 新版本的变化
  • 首次演示和新概念
  • 基于修订的傅立叶级数的新多项式
  • 从内部实现有关软件
  • 对抗点差噪声和结算点差的新机制
  • 短线阶段的手数变化
  • 依据全局历史操作变化
  • 暴力强推数学
  • 历史数据黏合和过度拟合
  • 用法建议
  • 结束语
  • 该系列之前文章的链接


新版本的变化

如同上一篇文章,该程序在功能和可用性方面得到了可观的改进。 以前的版本非常蹩脚,存在各种疏漏和错误。 此版本包含大量的修改。 大量修订工作令智能交易系统模板和软件与时俱进。 修改清单:

  1. 重新设计的界面
  2. 为暴力强推机制添加了另一个多项式(基于修订的傅立叶级数)
  3. 生成随机数的增强机制
  4. 方法应朝便利和易用的概念扩展
  5. 为超短线阶段增加了手数变化机制
  6. 增加点差计算机制
  7. 添加了减少点差噪声的机制
  8. 故障修复

大多数先前计划的修改均已实现。 进一步的算法修正不会太多了。


首次演示和新概念

在创建 EA 时,我意识到始终创建 EA 名称,并处理大量设置极具挑战性。 有时,名称可能会重叠,而设置可能会混淆。 此问题的解决方案是智能交易系统接收设置。 程序可以生成通常的 txt 格式的配置文件,智能交易系统会读取它。 这种方式可以加快此解决方案的工作速度,并令其更简单、更易于理解。 解决方案现在看起来像这样:

外汇侍应生用法


当然,生成机器人的程序版本依然可用。 但本文奉献出一套专门为 MetaTrader 4 和 MetaTrader 5 终端发明的新概念 — 它允许普通用户尽可能简单快速地开始运用该解决方案。 该解决方案最便利的部分是在 MetaTrader 4 和 MetaTrader 5 中操作设置的方式相同。

我只展示新界面的一部分,因为它太大了,会占用文章的太多篇幅。 这是第一个选项卡。

新的侍应生界面


所有元素都切分到相应的部分,从而便于导航。 如果未使用的元素毫无存在意义,则它们将被封印。

我已创作了一个特殊的视频来演示程序操作。



为什么不能通过普通的设置文件来传递设置? 原因在于设置文件不能包含数组,而此方法会用到数组作为输入,且其长度是浮动的。 在这种情况下,最便利的解决方案是使用普通的文本文件。


基于修订的傅立叶级数的新多项式

许多熟悉机器学习的人士出于不同目的,会主动在他们的算法里运用傅立叶级数。 创建傅立叶级数最初是为了将函数分解为 [-π;π] 范围内的级数。 我们需要知道的是如何将函数分解为一个序列,以及如此分解是否必要。 此外,了解变量替换方法的特殊性很重要,因为分解可以在不同的区间上进行,而不限于 [-π; π]。 所有这一切都需要良好的数学知识,以及了解在交易里运用它是否有哪些目的。

傅里叶级数的一般观点如下:

以这种形式,该多项式只是为了以更方便的形式呈现交易形态,以及假设价格是波动过程的前提下,尝试预测价格走势。 该假设似乎经过验证,但这种形式不太适用于暴力强推,故此,我们需要从根本上改变方法的整体概念。 取而代之的是,我们需要把多项式进行变换,如此即可应用此方法。 其变化会有很多。 不过,我建议执行以下操作,以使令公式保持接近傅立叶级数,且无需把公式用于其预期目的:

第一次变换

此处没有任何变化,除了该系列拥有更多的自由度:它的周期在正负间浮动。 此外,现在项式的数量有限。 这是因为数组 C[] 包含系数,我们会有效地组合这些系数,来查找合适的公式。 这种系数的数量是有限的。 这个序列不能是无限的,所以我们必须将它限定为 “m” 根柱线。此外,我删除了第一项以便保持对称性 - 公式值在 “+” 和 “-” 范围内产生的信号应该最对称。 但是这种方式只能用于依据 1 根柱线选择一个函数! 我们必须确保公式中的所有柱线值都可用。 甚至,一根柱状有 6 个参数,而不是 1 个。 这 6 个参数在本系列的第二篇文章中已有所探讨。 在此,我们不得不牺牲一根柱线的处理精度,以便考虑所有其余柱线。 理想情况下,该数额应包含在另一个当中。 但我不想令多项式复杂化,故此我们现在将采用其最简单的版本:

最终的多项式

实际上,一维函数已经变成了多维。 但这并不意味着该多项式可以描述所选多维超立方体中的任意多维函数。 无论如何,该函数提供了另一个多维函数族,泰勒级数无法正确涵盖的其他规则均可由其描述。 这提供了在同一数据样本上查找更佳形态的更多机会。 

这个函数的代码看起来像这样:

if ( Method == "FOURIER" )
   {
      for ( int i=0; i<CNum; i++ )
         {
         Val+=C1[iterator]*MathSin(C1[iterator+1]*(Close[i+1]-Open[i+1])/_Point)+C1[iterator+2]*MathCos(C1[iterator+3]*(Close[i+1]-Open[i+1])/_Point);
         iterator+=4;
         }

      for ( int i=0; i<CNum; i++ )
         {
         Val+=C1[iterator]*MathSin(C1[iterator+1]*(High[i+1]-Open[i+1])/_Point)+C1[iterator+2]*MathCos(C1[iterator+3]*(High[i+1]-Open[i+1])/_Point);
         iterator+=4;
         }

      for ( int i=0; i<CNum; i++ )
         {
         Val+=C1[iterator]*MathSin(C1[iterator+1]*(Open[i+1]-Low[i+1])/_Point)+C1[iterator+2]*MathCos(C1[iterator+3]*(Open[i+1]-Low[i+1])/_Point);
         iterator+=4;
         }

      for ( int i=0; i<CNum; i++ )
         {
         Val+=C1[iterator]*MathSin(C1[iterator+1]*(High[i+1]-Close[i+1])/_Point)+C1[iterator+2]*MathCos(C1[iterator+3]*(High[i+1]-Close[i+1])/_Point);
         iterator+=4;
         }

      for ( int i=0; i<CNum; i++ )
         {
         Val+=C1[iterator]*MathSin(C1[iterator+1]*(Close[i+1]-Low[i+1])/_Point)+C1[iterator+2]*MathCos(C1[iterator+3]*(Close[i+1]-Low[i+1])/_Point);
         iterator+=4;
         }         

   return Val;
   }

并非整个函数,而只是它的一部分实现了多项式。 尽管很简单,但即使这样的公式也能适应市场。

我用这种方法针对去年的历史数据进行了非常快速的暴力强推,来展现这个公式是能操作的。 然而,我们需要找出它的操作是否有效。 实际上,我还没有经此公式找到一个真正有效的解决方案。 但我认为这是时间和计算能力的问题。 我在初始版本上花费了大量时间。 这是我在最后的历史年度中针对 USDJPY M15 设法达到的结果:

傅里叶方法


我不喜欢这个公式的地方在于它在点差噪声抑制方面非常不稳定。 也许这与该方法框架内的谐波函数的细节有关。 亦或,我没有完全正确地归纳公式。 确保启用第二个选项卡中的 “Spread Control” 选项。 这在优化期间会禁用点差噪声抑制机制,并产生相当好的变体。 大概,这个公式十分“温文尔雅”。 尽管如此,它仍然能够发现相当不错的变体。


从内部实现有关软件

这些问题在我之前的文章中只是略有涉及。 我决定揭开这部分的面纱,从内部展示它是如何操作的。 最有趣和简单的部分是为公式生成系数。 针对这部分的解释可有助您理解系数是如何生成的:

public void GenerateC(Tester CoreWorker)
   {
   double RX;
   TYPE_RANDOM RT;
   RX = RandomX.NextDouble();
   if (RandomType == TYPE_RANDOM.RANDOM_TYPE_R) RT = (TYPE_RANDOM)RandomX.Next(0, Enum.GetValues(typeof(TYPE_RANDOM)).Length-1);
   else RT = RandomType;

   for (int i = 0; i < CoreWorker.Variant.ANum; i++)
      {
      if (RT == TYPE_RANDOM.RANDOM_TYPE_0) 
         {
         if (i > 0) CoreWorker.Variant.Ci[i] = CoreWorker.Variant.Ci[i-1]*RandomX.NextDouble();
         else CoreWorker.Variant.Ci[0]=1.0;
         }
      if (RT == TYPE_RANDOM.RANDOM_TYPE_5)
         {
         if (RandomX.NextDouble() >= 0.5)
            {
            if (i > 0) CoreWorker.Variant.Ci[i] = CoreWorker.Variant.Ci[i - 1] * RandomX.NextDouble();
            else CoreWorker.Variant.Ci[0] = 1.0;
            }
         else
            {
            if (i > 0) CoreWorker.Variant.Ci[i] = CoreWorker.Variant.Ci[i - 1] * (-RandomX.NextDouble());
            else CoreWorker.Variant.Ci[0] = -1.0;
            }
         }
      if (RT == TYPE_RANDOM.RANDOM_TYPE_1) CoreWorker.Variant.Ci[i] = RandomX.NextDouble();
      if (RT == TYPE_RANDOM.RANDOM_TYPE_2)
         {
         if (RandomX.NextDouble() >= 0.5) CoreWorker.Variant.Ci[i] = RandomX.NextDouble();
         else CoreWorker.Variant.Ci[i] = -RandomX.NextDouble();
         }
      if (RT == TYPE_RANDOM.RANDOM_TYPE_3)
         {
         if (RandomX.NextDouble() >= RX)
            {
            if (RandomX.NextDouble() >= RX + (1.0 - RX) / 2.0) CoreWorker.Variant.Ci[i] = RandomX.NextDouble();
            else CoreWorker.Variant.Ci[i] = -RandomX.NextDouble();
            }
         else CoreWorker.Variant.Ci[i] = 0.0;
         }
      if (RT == TYPE_RANDOM.RANDOM_TYPE_4)
         {
         if (RandomX.NextDouble() >= RX) CoreWorker.Variant.Ci[i] = RandomX.NextDouble();
         else CoreWorker.Variant.Ci[i] = 0.0;
         }
      }
   }

这相当简单:有若干种固定的随机数生成类型,并且有一种通用类型可以一次性实现所有内容。 每种生成类型都已在实践中经历过测试。 事实证明,一般的生成类型 “RANDOM_TYPE_R” 展现出最大的效率。 由于不同金融产品和时间帧上的报价性质差异,固定类型并不总能给出结果。 从视觉上看,在大多数情况下,这些差异不可见,但机器可以看到一切。 尽管在某些时间帧上一些固定类型可以提供更多拥有最高质量的信号。 我已注意这些,例如,在 NZDUSD H1 上,使采用 RANDOM_TYPE_4 时结果质量急剧上升,这意味着“只有零和正数”。 这也许是肉眼无法看到的隐藏波浪过程,给出的清晰暗示。 我想更详细地探索不同的金融产品,但这很难单独完成。


对抗点差噪声和结算点差的新机制

如前一篇文章所述,点差扭曲了价格数据,因此已发现的形态大多会因点差而成为假象。 点差是所有策略的最大敌人,因为大多数策略无法提供足够的数学期望来覆盖点差。 即时依据实盘账户,您也不应该被一个月甚至一年的的回测,或交易统计的正回报值所愚弄,因为这个数据样本规模太小,故无法评估未来的表现。 有单独的一类交易策略和自动交易系统称为“夜间剥头皮”。 这些机器人在有限的时间内捕捉微薄的利润。 经纪商正在积极打击此类系统,所用手法即是在午夜后扩大点差。 点差若设置在这样的水平上,会导致大多数策略都变得无利可图。

对于大多数经纪商来说,有一个值几乎相同:

  • 点差 = (要价 - 出价) / 品种点值
  • 中间价 = ( 要价 + 出价 ) / 2

该价格以绿色高亮显示。 这是订单簿的中部。 订单簿通常相对于该价格排列,并且前后两个价格均与该价格的距离相等。 事实上,如果我们看一下订单簿的经典定义,这个价格没有任何意义,因为没有该价位的交易订单。 即使我们假设所有经纪商都有自己的点差,该价格对于几乎所有经纪商来说都是一样的。 此处是一个示意图:

点差

上方的示意图展示了两个随机选择经纪商的价格序列。 总是有一个“要价”和一个“出价”,它们象征着买入和卖出的价格。 两个价格范围的黑线相同。 这个价格可以很容易地计算出来,正如我上面所展示的。 很重要的是,该数值实际上并不取决于特定经纪商的点差放宽或收窄,因为相对于给定的价格,所有变化几乎都是均匀的。

下方的示意图展示了来自不同经纪商的报价实际发生时的真实场景。 问题是,即使这个平均价格在不同的数据流中也是有差别的。 我不知道真实原因。 即便我知道,这对交易也很难有帮助。 我在实践对冲交易时发现了这个事实,其中所有这些细微差别都极端重要。 相对于我们的方法,仅有的重点:

  • MidPrice1=f(t)
  • MidPrice2=MidPrice1-D
  • MidPrice1 '(t) =  MidPrice2 '(t)

换言之,两个价格序列的平均价格(如果表示为时间函数)具有相同的导数,因为这些函数仅在于常数 “D” 不同。 由于我们的多项式不使用价格,而是它们的差值,因此所有数值都将是这些平均价格函数的导数的泛函。 由于这些导数对于所有经纪商都是相同的,我们可以据此预期这些设置对于不同的经纪商来说均有效。 对于一种替代情况,所发现的设置,依据真实即时报价能够成功回测,亦或用于其他经纪商的可能性极低。 而上述概念避免了此类问题。

为了实现这个机制,我不得不针对所有元素进行相应的修改。 首先,在编写报价文件时,必须记录所有重要柱线点位的点差。 这些是 Open[]、Close[]、High[]、Low[] 的点位。 点差将用于调整数值,从而获得要价,因为柱线基于出价。 现在,编写报价的 EA 基于即时报价,而非柱线。 现在,记录这些柱线的函数如下所示:

void WriteBar()
   {
   FileWriteString(Handle0x,"\r\n");
   FileWriteString(Handle0x,DoubleToString(Close[1],8)+"\r\n");
   FileWriteString(Handle0x,DoubleToString(Open[1],8)+"\r\n");
   FileWriteString(Handle0x,DoubleToString(High[1],8)+"\r\n");
   FileWriteString(Handle0x,DoubleToString(Low[1],8)+"\r\n");         
   FileWriteString(Handle0x,IntegerToString(int(Time[1]))+"\r\n");
   FileWriteString(Handle0x,IntegerToString(PrevSpread)+"\r\n");
   FileWriteString(Handle0x,IntegerToString(CurrentSpread)+"\r\n");
   FileWriteString(Handle0x,IntegerToString(PrevHighSpread)+"\r\n");
   FileWriteString(Handle0x,IntegerToString(PrevLowSpread)+"\r\n");   
   MqlDateTime T;
   TimeToStruct(Time[1],T);
   FileWriteString(Handle0x,IntegerToString(int(T.hour))+"\r\n");
   FileWriteString(Handle0x,IntegerToString(int(T.min))+"\r\n");
   FileWriteString(Handle0x,IntegerToString(int(T.day_of_week))+"\r\n");         
   }      

四条线以绿色高亮显示 - 这些线记录了柱线的所有四个点位的点差。 在以前的版本中,这些数值没有被记录下来,在计算时也未考虑在内。 这些数据可以很容易地获得和记录。 以下简单的基于即时报价的函数,用于获取最高价和最低价的点差:

void RecalcHighLowSpreads()
   {
   if ( Close[0] > LastHigh )
      {
      LastHigh=Close[0];
      HighSpread=int(SymbolInfoInteger(_Symbol,SYMBOL_SPREAD));
      }
   if ( Close[0] < LastLow )
      {
      LastLow=Close[0];
      LowSpread=int(SymbolInfoInteger(_Symbol,SYMBOL_SPREAD));
      }      
   }

此函数仅探测当前正在形成柱线的最高点和最低点的点差。 当新柱线出现时,当前柱线被视为完全成型,其数据将写入文件。 此函数与另一个基于柱线的函数协同操作:

bool bNewBar()
   {
   ArraySetAsSeries(Close,false);                        
   ArraySetAsSeries(Open,false);                           
   ArraySetAsSeries(High,false);                        
   ArraySetAsSeries(Low,false);                              
   CopyOpen(_Symbol,_Period,0,2,Open);
   CopyClose(_Symbol,_Period,0,2,Close);
   CopyHigh(_Symbol,_Period,0,2,High);
   CopyLow(_Symbol,_Period,0,2,Low);
   ArraySetAsSeries(Close,true);                        
   ArraySetAsSeries(Open,true);                           
   ArraySetAsSeries(High,true);                        
   ArraySetAsSeries(Low,true);                                 
   if ( Time0 < Time[1] )
      {
      if (Time0 != 0)
         {
         Time0=Time[1];
         PrevHighSpread=HighSpread;
         PrevLowSpread=LowSpread;         
         PrevSpread=CurrentSpread;
         CurrentSpread=int(SymbolInfoInteger(_Symbol,SYMBOL_SPREAD));
         HighSpread=CurrentSpread;
         LowSpread=CurrentSpread;         
         return true;
         }
      else
         {
         Time0=Time[1];
         return false;
         }
      }
   else return false;
   }

该函数既是断言又是重要的逻辑元素,在其中会最终检测所有四个点差。 它在程序内部的实现均类似。 它在 OnTick 处理程序中的操作非常简单:

RecalcHighLowSpreads();
if ( bNewBar()) WriteBar();

报价文件将包含以下数据:

柱线结构

程序中同样实现了一个平均价格数组:

OpenX[1]=Open[1]+(double(PrevSpread)/2.0)*_Point;
CloseX[1]=Close[1]+(double(Spread)/2.0)*_Point;
HighX[1]=High[1]+(double(PrevHighSpread)/2.0)*_Point;
LowX[1]=Low[1]+(double(PrevLowSpread)/2.0)*_Point;

也许看起来这种方式能够用来实现点差噪声抑制。 然而,问题在于它需要收集一定数量的即时报价(时间帧越高,每根柱线需要的即时报价越多)。 收集大量即时报价需要很多时间。 甚而,柱线不存储要价或点差值,这就是我们采用出价进行计算的原因。

作为一个附加选项,我添加了一个机制,进行优化时,在结果里考虑点差。 测试表明,这种机制是可选的,但只有在足够的计算力下,它才可以给出十分良好的结果。 仅当价差未超出所需数值时,算法才需要点数来处理开单和平单。 当前版本将点差记录在柱线数据里,因此该数值可控,从而允许我们计算排除了点差后的真实测试值。


短线阶段的手数变化

手数变化(变体之一)不仅可用于短线阶段,也可用于长线阶段。 有两种风险/手数管理机制。

  • 手数递增
  • 手数递减

第一种机制应该用于逆势交易,当我们期望信号很快就会翻转。 第二个选项只在已知信号稳定,且会持续很长时间的情况下才有效。 您可以使用任何控制函数。 我采用了最简单的函数 — 线性函数。 换言之,手数会随时间推移而变化。 我们看看实践中该机制的操作。

这将使用 USDJPY M15 报价来完成。 在这种情况下,我用 MQL4 版本的机器人进行演示,因为机器人在基于即时报价回测时会失败,如同它在交易时点差点数增加。 我想证明在较低时间帧内采用精良的暴力强推方法,能够在等同于暴力强推周期的时间内提供更好的前瞻验证结果。 鉴于我的计算能力有限,这项操作不是很广泛。 但如此结果足以展现出相当长的前瞻验证操作周期,在这些前瞻验证阶段采用了两种手数管理机制。 我们在搜索区间中从找到的变化开始演示。 时间间隔等于一年:

USDJPY M15 暴力强推历史周期

此处的数学期望是略高于 12 个点(这次没有点差,这对我们来说并不重要,因为我们这次忽略点差)。 我们看看盈利因子。 一年的前瞻验证测试如下所示:

1 年至未来

尽管只在一年时间内执行搜索,不过它能连续工作不少于一年。 在实践中,这意味着如果您有一台强力计算机,您能够分析一两周内所有低点差的主要货币对,然后选择其中最佳的,并可运用形态至少一年。 不要忘记确保系统能通过 MetaTrader 5 中的实时报价回测。 为了收集更多证据,最好加入一个小型团队,在若干台计算机上分析数据,从而收集结果并编制统计数据。

现在我们来看一下前瞻验证测试 - 开始时有一个非常大的回撤,这通常出现在我们基于较小的时间周期(例如一年)查找形态的时候。 无论如何,即使是这种回撤,也可以用于您自己的目的。 在上图中该间隔以红框标示。 此处的 EA 操作时间在设置中被限制为 50 个交易日(周六、日不计算在内)。 此外,该信号已被逆转,可扭亏为盈。 这样做是为了在回撤后截断图形部分,因为它会在逆转后变成负数。 回测结果如下:

逆转 + 固定手数 50 天至未来

注意盈利因子。 我们打算提高它。 您永远不会真正知道是否会发生逆转,以及逆转会走多远,但它通常会因暴力强推阶段的发生而经历相当一大段回滚走势。 从最小到最大应用手数线性递增,我们的回测会得到这样增加的盈利因子:

逆转 + 递增手数 50 天至未来

现在,我们看看逆转机制,它在一年的前瞻验证测试中以绿色框标示。 这部分展示了一段较大的上升部分,然后形态逆转。 在这种状况下,我们将采用手数递减。 机器人的交易设置会持续到到该阶段结束。 首先,我们采用用固定手数测试它,如此稍后就能进行结果比较:

绿框是固定手数

现在,我们启用随时间推移手数递减的机制。 这也会导致盈利因子递增,而图形变得更平滑,且最后没有回撤:

GreenBox + 手数递减

许多市场操盘手运用这些技术。 如果在正确的时间和位置运用,它们既可提高盈利能力,亦可减少亏损。 但在实战中它会更复杂。 无论如何,这些机制在我的智能交易系统程序中均已提供,因此您可以在需要时启用和禁用它们。


依据全局历史操作变化

我认为许多读者会有兴趣查看和检查一些操作设置变体,从中找出至少能够通过基于即时报价历史的全局回测设置。 我已找到了一些这样的设置。 由于计算能力有限,又因为只有我独自执行暴力强推,故搜索花了相当长的时间。 尽管如此,我还是发现了一些变体。 此处就是它们了:

USDCAD H1 2010-2020

USDJPY H1 2017-2021

EURUSD H1 2010-2021

这些设置附带于后,因此您可以根据需要进行测试。 您也可以尝试找到自己的设置,并在模拟账户上回测它们。 自这个程序版本开始,任何人都可以尝试这种方法。


暴力强推数学

本章节需要详尽的解释,从而帮助用户理解底层的计算原理。 我们从程序的第一个选项卡开始,它是如何操作以及如何解释其结果呢。

第一个选项卡中的暴力强推

事实上,任何暴力强推算法中发生的一切都与概率论有关,因为我们总是有某种模型和迭代。 此处的迭代是一个分析当前策略变体的完整循环。 一个完整的循环可以由一个或多个测试组成,具体取决于具体的方法。 测试和其他操作的数量并不重要,因为一切都可以归类为一次迭代。 一次迭代可被视为成功或不成功,这取决于对结果的需求。 宽泛的定量值变化可作为衡量优良结果的标准,而这应取决于分析方法。

该算法总会输出一个或多个符合我们需求的变体。 我们可以指令算法在内存中存储多少结果。 所有其他符合要求但无法容纳的结果将被丢弃。 无论我们的暴力强推算法有多少步骤,这个过程总是会发生。 否则,我们会浪费太多时间去处理无甚意义的低质量数据。 按此方式,不会丢失单个变体,但会降低搜索速度。 采用哪种方式最终由您定夺。

现在我们进入正题。 任何搜索过程的结果最终都归结为根据伯努利方案进行独立测试的过程(前提是算法是固定的)。 这一切都取决于获得优良变体的可能性。 对于固定算法,此概率始终是固定的。 在我们的例子中,这个概率取决于以下数值:

  • 样本尺寸
  • 算法变体
  • 接近基准
  • 对最终结果的严格要求

以此观点,根据伯努利公式,所获结果的数量和质量随着迭代次数的增加而增长。 然而,不要忘记这是一个纯粹的概率过程! 也就是说,无法明确预测您会得到哪一组结果。 只能计算出找到期望结果的概率

  • Pk - 迭代会依照指定需求产生操作变体的概率(此概率可能因需求而异)
  • C(n,m) - “n” 到 “m” 组合的数量
  • Pa=Sum(m0...m...n)[C(n,m)*Pow(Pk ,m)*Pow(1-Pk ,n-m)] - 在 n 次迭代后我们至少有 m0 主要变体满足我们需求的概率
  • m0 - 满意原型的最小数量
  • Pa — 从 “n” 次迭代中获得至少 “m0” 或更多变体的概率
  • n — 搜索操作原型的最大可用循环数量(我们准备等待结果的最长时间)

循环数量也可以用时间来表示:从第一个选项卡中的计数器中获取暴力强推的速度,以及您准备花在处理当前数据上的时间:

  • Sh - 速度,每小时迭代次数
  • T - 我们准备等待的时间(以小时为单位)
  • n = Sh*T

与此类似,可以根据某些品质需求来计算发现变体的概率。 上述公式允许查找落在“偏差”过滤器之下的变体,这是结果呈线性的要求。 如果未启用该过滤器,则每次迭代都将会视为成功,并且总会有变体。 找到的变体将按品质得分进行排序。 取决于所需的品质,“Ps” 值将是品质函数值取模。 我们要求的品质越高,这个函数的值就越低:

  • Ps - 依照某些附加品质需求找到结果的概率
  • q - 要求的品质
  • qMax - 最高的可用品质
  • Ps = Ps(|q|) = K * Px(|q|) , q <= qMax
  • K = Pk - 该系数参考了获得某些随机变体的概率(基于品质的变体从此变体中选择)
  • Ps ' (|q|) < 0
  • Lim (q-->qMax) [  Ps(|q|) ] = 0

该函数的一阶导数为负,表示随着需求的增加,满足它的概率趋于零。 当 “q” 趋向于最大可用值时,该函数的值趋向于 “0”,因为这是一个概率。 如果 “q” 大于最大值,则该函数没有意义,因为所选算法不能每个都具有高品质。 该函数遵循随机变量 “q” 的概率密度函数。 下图显示了 Ps(q) 和随机变量 P(q) 的概率密度,以及其他重要量值:

变种

基于这些插图:

  • Integral(q0,qMax) [P(q)] = Integral(-qMax,-q0) [P(q)] =  K*Px(|q|) = Ps(|q|)  - 这是含有 |q| 的变体在当前迭代期间在 q0 和 qMax 之间找到变体的概率。
  • Integral(q1,q2) [P(q)] - 在 q1 和 q2 之间迭代得到品质结果值的概率(这是如何解释随机变量分布函数的示例)

如此,我们所需的品质越高,我们需要花费的时间就越多,找到的变体就越少。 此外,任何方法的品质值都有上限,这取决于我们分析的数据,以及我们的方法完善程度。

第二个选项卡中的优化

第二个选项卡中的优化过程与主要搜索过程略有不同。 无论如何,它仍然采用迭代,以及得到的满足我们需求的变体的概率。 此选项卡含有更多过滤器,相应地,获得良好结果的可能性较低。 然而,由于第二个选项卡改进了已处理的变体,因此在第一个选项卡中找到的选项越好,则在第二个选项卡上获得的结果就越好。 与某个变体对应的最终公式已得到改进,它在某些方面与伯努利方程有些相似。 我们感兴趣的是得到至少有一个变体会落入我们的过滤器的概率。 解释如下:

  • Py = Sum(1...m...n)[ Sum(0... i ... C(n,m)-1) {  Product(0 .. j .. i-1 )[Pk[j]) * Product(i .. j .. m) [1 - Pk[j]] } ] - 获得至少一种满足过滤器要求的变体的概率
  • Pk [i] - 获得满足第二个选项卡中过滤器要求的变体的概率
  • n - 分割优化间隔(第二个选项卡中的间隔点值)

优化的执行方式与 MetaTrader 4 和 MetaTrader 5 优化器完全相同,不过我们仅优化一个参数,即买入或卖出信号。 优化步骤是基于我们把优化间隔(Interval Points)所切分的为多少部分而自动计算的。 正在优化的最高数值是在第一个选项卡中的搜索过程中计算得到的。 在第一个选项卡中的处理完成后,我们知道优化数值的波动范围。 所以,在第二个选项卡中,我们只需设置网格精度来切分这个间隔。 该变体在第二个选项卡中占据一个槽位,只要达到更好的品质就会被更新。 

再一次,获得某些符合品质要求的变体的概率,与上述的那个具有类似的分布函数。 这意味着我们能够采用略作调整的相同公式:

  • Integral(q0,qMax) [P(q)] = Integral(-qMax,-q0) [P(q)] =  K*Px(|q|) = Pz(|q|)  - 在当前迭代期间,这是在 q0 和 qMax 之间找到含有 |q| 变体的概率。
  • K = Py

这里唯一的区别是系数 “K”,它等于早前获得的新概率。 获得所需品质变体的概率非常低,但我们在第一个选项卡中已有了很多这样的变体,所以我们获得的变体越多,则越好。 甚而,在第一个选项卡中产生的变体越多,在第二个选项卡中就能得到更好的变体。 计算过程都类似。 不幸的是,伯努利公式在此不适用,但可用之前研究的构造作为替代。 在这种情况下,一个变体的优化可解释为单独的迭代。 因此,总迭代次数将等于迭代编号。 我们至少需要一个满足我们需求的变体,以表示前面的公式是完美的。 此处 Pk 替换为 Pz,Pz 由 Pz[j](|q|) 函数族确定,因为每个优化变体都有单独的此类函数。

  • Pb = Sum(1...m...n)[ Sum(0... i ... C(n,m)-1) {  Product(0.. j .. i-1 )[Pz[j]) * Product(i.. j .. m) [1 - Pz[j]] } ]
  • n - 在第一个选项卡中找到的变体的数量

所以,您暴力强推的时间越长,您获得的品质就越好。 然而,不要忘记每个参数都会影响概率和结果。 采用合理设置,来避免过度资源消耗。 现代计算机性能强劲,但不要忘记合理的设置和详知处理细节可以提升计算效率很多倍。


历史数据黏合和过度拟合

许多自动交易系统的问题在于它们过度训练,及历史拟合。 创建一个令人印象深刻的系统,展现每月高达 1000% 的结果,这是有可能的。 但是这样的系统在现实中是行不通的。

交易系统的输入参数越多,EA 逻辑的可变性越大,这样的 EA 对历史的黏合就越强。 问题是我们有一个将报价转换为另一种数据格式的简单处理过程。 总有正向和逆向转换函数可以提供数据转换处理。 它可与加密和解密相比。 例如,WinRar 存档就是一个加密的例子。 在我们的任务场景中,加密算法是优化过程和交易逻辑表述的组合。 优化器中进行足够数量的回测,和一种确定的灵活逻辑可以创造奇迹。 在这种情况下,交易逻辑充当解码器,基于过去的读数对未来价格进行解码。

不幸的是,所有 EA 交易都在某种程度上黏合历史。 不过,还有一个逻辑部分应该在未来保留一些功能。 这样的算法极难获得。 我们不知道特定算法进行公平预测时的最大能力,因此我们无法判定过度训练的边界。 我们需要这样一种算法,能以尽可能高的概率预测下一根烛条走势。 此处,价格数据压缩程度越强,算法越可靠。 我们以函数 sin(w*t) 为例。 我们知道该函数对应于无限多个点 [X[i],Y[i]] — 它是一个无限长度的数据数组,被压缩为一个简短的正弦函数。 这是一种理想的数据压缩。 这种压缩在现实中是不可能的,一些数据种类我们总会受限压缩比。 该系数越高,公式定义的质量就越高。

我的方法采用固定数量的可变数据。 尽管如此,与任何其他方法一样,过度拟合也是可能的。 避免历史过度训练的唯一方法是增加压缩比。 这只能通过增加所分析的历史部分的规模来实现。 还有第二种途径 — 减少公式中所分析的柱线数量(Bars To Equation)。 最好采用第一种方法,因为通过减少公式中的柱线数量,我们拉低了 “qMax” 的上限,取代增加它。 汇总一下,最好基于大量样本训练,使用足够多的 “Bars To equation”;但同时必须牢记,过度增加这个数值会降低暴力强推速度,不可避免地会产生较高风险率的过度历史拟合。


用法建议

在测试期间,我辨别了一些配置主程序 Awaiter.exe 的重要细节。 此处是其中最重要的:

  1. 一旦您在所有选项卡中完成所需设置后,请确保保存它们 (按钮 Save Settings)
  2. Spread Control 可在第二个选项卡中启用
  3. 若通过 HistoryWriter EA 生成报价,使用尽可能的大量样本(至少 10 年的历史)
  4. 更多变体可以保存在第一个选项卡中,1000 个似乎足矣 ( Variants Memory )
  5. 不要在优化选项卡中设置一个很大的 Interval Points 值(20-100 应该就足够了)
  6. 如果您希望获得优良设置,其可通过真实即时报价回测,则不需要大量的变体订单 (Min Orders)
  7. 您应该控制变体搜索速度(如果您的暴力强推运行了很长时间,依然未找到变体,您可能应该更改设置)
  8. 为了获得稳定的结果,请将 Deviation 设置在 “0.1 - 0.2” 范围内;0.1 是最好的选择
  9. 若在优化选项卡中采用 FOURIER 方程时,启用 "Spread Control" 选项(该公式对点差噪声极其敏感)


结束语

请勿把此解决方案捧为圣杯。 它只是一个工具。 很难在 MetaTrader 4 和 MetaTrader 5 终端中,难以实现无需额外编程或优化即可使用的高效且用户友好的解决方案。 本期解决方案就是这样了,且它已经可用于一般用途了。 我希望

这个方法能对您很所帮助。 当然,它仍有很大的改进空间,但笼统地说,它只是行情研究和交易的操作工具。 进一步的结果取决于计算能力,而不是改进。 无论如何,我认为将来会有一些改进。

我已汇总了需要花费很多时间但尚未实现的思路。 其中之一就是基于最著名的震荡指标和价格指标(例如布林带或移动平均线)构建逻辑多项式。 但这个概念有点复杂。 我想实现一些更有用的思路,而非只是“在指标交叉点交易”。 我也希望本文能为读者提供一些新的和有用的信息。


该系列之前文章的链接

  • 形态搜索的暴力强推方式(第三部分):新视野
  • 形态搜索的暴力强推方式(第二部分):沉浸
  • 形态搜索的暴力强推方式

全部回复

0/140

量化课程

    移动端课程