这项技术有什么特别之处使得我决定探讨一下呢?
我总是尽量表达我的想法,以便它们对其他交易者有最大的实际应用。我为什么要这么做?这是个好问题。我想和其他交易者分享我的经验。如果有人能利用我的想法,实施一些有趣和有利可图的东西,我会很高兴,不管我从中得到什么。我当然明白,这些想法可能不完全是我的,我可能会重新发明轮子。我认为我们应该分享经验,尽可能富有成效地合作。也许这就是成功的秘诀。孤立地工作是很难取得任何重大成就的。在我看来,模式这个话题对于理解市场的物理特性是非常重要的。你作为交易者的职业生涯可以依赖于对基本思想的理解,即使“作为交易者的职业生涯”这个短语听起来很有趣。我希望我能对研究这些问题的人有所帮助。
关于暴力及其与神经网络的区别
神经网络本质上也是一种暴力。但是它的算法与简单的暴力算法有很大的不同。我将不提供具体的神经网络结构及其元素的细节,但将尝试提供一个一般性的描述。我认为,如果我们坚持某种架构,我们就提前限制了算法的能力。固定的架构是无法弥补的限制。在我们的例子中,神经网络是一种可能策略的架构。因此,神经网络的配置总是与具有网络图的某个文件相对应,这总是指向特定元的集合。它就像一台3D打印机:设置项目参数,打印机就会产生它。因此,神经网络是一种通用代码,没有图纸就没有意义。这就像使用任何高级编程语言,只创建一个空项目,而不利用它的所有功能。结果,空模板什么也不做。神经网络也是如此。与暴力不同,神经网络可以提供几乎无限的策略变化、任意数量的准则和更高的效率。这种方法的唯一缺点是效率在很大程度上取决于代码质量。系统复杂性的增加可能导致程序的资源密集度的增加。结果,我们的策略被转换成一个网络图,这是它的等价物。暴力方法也是如此,但这里我们使用一些简单的数字序列。这个序列比网络图简单得多,它更容易计算,但在效率方面也有限制。下面的方案显示了上述说明。
换句话说,使用暴力方法,我们选择一个与代码交互并产生不同结果的数字序列。但是由于算法是固定的,所以它的灵活性就包含在数字数组中。其长度固定,结构简单。当使用神经网络时,我们搜索一个能给出最佳结果的网络图。在任何情况下,我们搜索特定的字节序列,或最终转换成结果算法的数据。唯一的区别在于它们的能力和复杂性。
我的暴力与优化算法
我在算法中使用了多元泰勒展开。这就是我选择这种方法的原因:我想提供一个算法,将尽可能可变,但非常简单。我不想拘泥于任何具体的公式,因为任何函数最终都可以展开成泰勒级数或傅里叶级数。我认为傅里叶级数不太适合这个目的。此外,我不熟悉多维等价物。这就是为什么我决定使用第一种方法。而且,它的实现要容易得多。一维泰勒级数如下:
Y = Cs[0]+Cs[1]*(x-x0)^1 + Cs[2]*(x-x0) ^2 + ... + Cs[i]*(x-x0)^n
其中幂之前的系数作为从0到n阶的导数。可以通过展开括号将其转换为更简单的形式:
Y = C[0]+C[1]*x^1 + C[2]*x^2 + ... + C[i]*x^n + ...= Sum(0,+infinity)(C[i]x^i)
在这种情况下,我们只有一个变量。这个级数可以模拟任意点附近的连续可微函数。公式中的项越多,就越能准确地描述我们的函数。如果它们的数目等于无穷大,那么这就是我们函数的绝对等价。在这里,我将不展示如何在任何点附近将任何函数展开为泰勒级数,这些信息在任何一本数学书上都有。但是一维变量对我们来说是不够的,因为我们想要使用来自多个柱的数据来增加一般公式的可变性。这就是为什么应该使用多维变量:
Y = Sum(0,+Infinity)( C[i]*Product variant(x1^p1*x2^p2...*xN^pN) )
这个公式的其他变体相当困难。它与一维变量方法具有相同的逻辑。我们必须提供所有可能的偏导数。如果我们限制了这些项的最高幂,那么我们就可以用组合和和来计算这些项的总数。我们的算法将利用最高阶的限制来限制计算资源的消耗。
但这仍然不足以使我们的暴力行动更加方便。最好去掉第一项C[0],让函数对于我们要输入的负值或正值具有最大对称性。另外,一个方便的解决方案是将正值函数值解释为买入信号,将负值函数值解释为卖出信号。理想情况下,增加该信号模的下限应导致预期收益和利润系数的增加,但也将不可避免地导致信号数量的减少。功能越接近这些要求越好。我们将特定烛形的值作为变量输入到函数(Close[i]-Open[i])。
我们现在需要做的是随机生成这些系数的变体,并检查变体在测试器中的行为。当然,没有人会手动迭代这些系数。因此,我们需要一个EA交易,它可以产生这样的变体,同时维护数千个这样的变体,或者一个第三方解决方案实现一些策略测试器的功能。最初,我不得不在MQL4中编写这样一个专家顾问—这个EA随附在文章中,并附有说明,因此每个人都可以使用它。但我将使用另一个应用程序,它是我用C#开发的。不幸的是,很明显我不能免费提供这个应用程序。它的能力远远超出了研究领域。但是我将描述并演示它的所有功能,以便每个知道如何编程的人都可以复制这个应用程序。截图将在本文后面提供,我们将在这里分析操作结果。
下面是应用程序的主要功能。系数数组的搜索分两个阶段进行。第一个阶段只是在加载的报价中搜索数组,这些数组在下一个烛形上产生最大预期收益或最大利润因子。过程的执行与策略测试器类似。事实上,它只是试图找到一个公式,以最大的准确性预测下一个柱的方向。一定数量的最佳结果作为数组变量存储在内存和磁盘中。只能测试部分报价-此处应指定相对于已加载报价文件的百分比。这用于允许在第二阶段丢弃随机值。第二阶段模拟市场订单和平衡曲线-这是为整个载入区域完成的。同时,执行信号幅度的平滑增加,同时搜索更好的质量选项。这个阶段也有各种各样的过滤器,通过使用这些过滤器我们可以得到更平滑的图表。图表越平滑,找到的公式就越好。在完成第二个搜索阶段后,在列表中可以看到一定数量的最佳选项。选择了所需的选项后,可以在第三个选项卡中为 MetaTrader 4 和 MetaTrader 5 生成一个交易机器人。EA是根据一个预编译的模板生成的,在这个模板中,接收到的数字在某些地方指定。
为任务构建简单模板
模板最初是在MQL4中创建的,然后转换为MQL5。该代码适用于这两种平台(类似于上一篇文章中的代码)。我试图提供这种兼容性,以便花更少的时间来适应解决方案。要像在MQL4中那样使用预定义的数组,应该向EA交易中添加一些额外的代码,这在我的前一篇文章中有描述。所以,请查看这篇文章的细节,本文需要这些知识。实际上,这并不困难,任何开发人员都可以实现这种兼容性。让我们从描述机器人生成时自动填充的变量和数组开始。
double C1[] = { %%%CVALUES%%% };//array of coefficients int CNum=%%%CNUMVALUE%%%;//number of candlesticks in the formula int DeepBruteX=%%%DEEPVALUE%%%;//formula depth int DatetimeStart=%%%DATETIMESTART%%%;//start point in time input bool bInvert=%%%INVERT%%%;//inverted trading input double DaysToTrade=%%%DAYS%%%;//number of days into the future to trade
这里C1是我们选择的次数前面的系数数组。CNum是价格表上用于计算多项式值的最近烛形数。接下来是公式的深度,即多维多项式的最大次数。我通常使用1,因为与一维泰勒级数不同,多维泰勒级数的计算复杂度随着阶数的增加而增加,因为系数总数随着阶数的增加而显著增加。为了限制EA操作时间,需要时间上的起始点,因为这样的限制使用关于操作开始位置的信息。inversion(反转)函数用于确保多项式在正确的方向上工作。如果我们把阶系数前面的所有符号都反转,那么多项式本身就不会改变,而只有多项式输出的数字才有不同的符号。这里最重要的部分是系数的比值。如果多项式的负值表示卖出,而正值表示买入,则 inverse = false。如果不是,那么就是 true。因此,我们指示算法“使用带相反符号的多项式的值”。另外,最好将此变量作为输入值,因为我们可能需要能够反转交易以及未来交易的天数。
如果需要使用系数计算数组的大小,可以按以下步骤进行:
int NumCAll=0;//size of the array of coefficients void DeepN(int Nums,int deepC=1)//intermediate fractal { for ( int i=0; i<Nums; i++ ) { if (deepC > 1) { DeepN(Nums,deepC-1); } else { NumCAll++; } } } void CalcDeepN(int Nums,int deepC=1)//to launch calculations { NumCAll=0; for ( int i=0; i<deepC; i++ ) { DeepN(Nums,i+1); } }
中间分形函数计算所有因子的总阶数相同的项数。这样做是为了简单,因为对我们来说,术语的求和顺序并不重要。第二个函数简单地调用循环中的第一个函数,次数与术语类型相同。例如,如果多维级数展开被限制在,比如说,4,那么我们用从1到4的所有自然数调用第一个函数。
计算多项式值的函数几乎相同。但是,在这种情况下,数组是自己生成的,不需要设置其大小。下面是它的样子:
double ValW;//the number where everything is multiplied (and then added to ValStart) uint NumC;//the current number for the coefficient double ValStart;//the number where to add everything void Deep(double &Ci0[],int Nums,int deepC=1,double Val0=1.0)//calculate the sum of one degree { for ( int i=0; i<Nums; i++ ) { if (deepC > 1) { ValW=(Close[i+1]-Open[i+1])*Val0; Deep(Ci0,Nums,deepC-1,ValW); } else { ValStart+=Ci0[NumC]*(Close[i+1]-Open[i+1])*Val0/Point; NumC++; } } } void CalcDeep(double &Ci0[],int Nums,int deepC=1)//calculate the entire polynomial { NumC=0; ValStart=0.0; for ( int i=0; i<deepC; i++ ) { Deep(Ci0,Nums,i+1); } }
所有计算的结果都将添加到ValStart中,也就是说,结果将添加到全局变量中。需要另一个全局变量-ValW,它用于将已有的乘积乘以某个值。在我们的例子中,这是相应柱的移动点数。这个移动可以是上下两种,这是由标志显示的。所以,这些函数有一个非常有趣的结构。他们在内部调用自身,这些调用的数量和结构总是不同的。这是一种调用树。我真的很喜欢使用这些函数,因为它们非常多变。这次我们用一种简单而优雅的方式实现了多维泰勒级数。
对于仅使用一维多项式的情况,也可以实现附加函数。在这种情况下,整个系列大大简化了。它变成了一个系数的总和乘以一阶柱的运动。它们的数量与所用柱的数量相同。这简化了所有的计算。如果次数为1,则使用简化版本。否则,就使用任何次数都统一的方法。
double Val; double PolinomTrade()//optimized polynomial { Val=0; if ( DeepBruteX <= 1 ) { for ( int i=0; i<ArraySize(C1); i++ ) { Val+=C1[i]*(Close[i+1]-Open[i+1])/Point; } return Val; } else { CalcDeep(C1,CNum,DeepBruteX); return ValStart; } }
使用简单变量时,结果将添加到Val变量中。
现在,让我们编写一个新柱出现时将调用的主要方法:
void Trade() { double Value; Value=PolinomTrade(); if ( Value > ValueCloseE) { if ( !bInvert ) { CloseBuyF(); } else { CloseSellF(); } } if ( Value < -ValueCloseE) { if ( !bInvert ) { CloseSellF(); } else { CloseBuyF(); } } if ( double(TimeCurrent()-DatetimeStart)/86400.0 <= DaysToTrade && Value > ValueOpenE && Value <= ValueOpenEMax ) { if ( !bInvert ) SellF(); else BuyF(); } if ( double(TimeCurrent()-DatetimeStart)/86400.0 <= DaysToTrade && Value < -ValueOpenE && Value >= -ValueOpenEMax ) { if ( !bInvert ) BuyF(); else SellF(); } }
这个函数非常简单,您所需要做的就是实现我们所期望的仓位打开和关闭功能。
柱的出现可以按下面的方法来检测:
void CalcTimer() { if ( Time[1] > PrevTimeAlpha ) { if ( PrevTimeAlpha > 0 ) { Trade(); } PrevTimeAlpha=Time[1]; } }
我觉得代码非常简单明了。
我的代码生成的系数是使用上面解释的四个模型创建的。为了方便起见,所有这些系数都在[-1,1]范围内,因为值的比率比值本身更重要。从MQL5程序原型生成这些数字的函数如下所示:
void GenerateC() { double RX; if ( DeepBrute > 1 ) CalcDeepN(CandlesE,DeepBrute); else NumCAll=CandlesE; for ( int j=0; j<VariantsE; j++ ) { ArrayResize(Variants[j].Ci,NumCAll,0); Variants[j].CNum=CandlesE; Variants[j].ANum=NumCAll; Variants[j].DeepBruteX=DeepBrute; RX=MathRand()/32767.0; for ( int i=0; i<Variants[j].ANum; i++ ) { if ( RE == RANDOM_TYPE_1 ) Variants[j].Ci[i]=double(MathRand())/32767.0; if ( RE == RANDOM_TYPE_2 ) { if ( MathRand()/32767.0 >= 0.5 ) { Variants[j].Ci[i]=double(MathRand())/32767.0; } else { Variants[j].Ci[i]=double(-MathRand())/32767.0; } } if ( RE == RANDOM_TYPE_3 ) { if ( MathRand()/32767.0 >= RX ) { if ( MathRand()/32767.0 >= RX+(1.0-RX)/2.0 ) { Variants[j].Ci[i]=double(MathRand())/32767.0; ///Print(Variants[j].Ci[i]); } else { Variants[j].Ci[i]=double(-MathRand())/32767.0; } } else { Variants[j].Ci[i]=0.0; } } if ( RE == RANDOM_TYPE_4 ) { if ( MathRand()/32767.0 >= RX ) { Variants[j].Ci[i]=double(MathRand())/32767.0; } else { Variants[j].Ci[i]=0.0; } } } } }
MQL4和MQL5中的暴力算法原型可以在文章附件中找到。这里我不提供我的交易函数实现,因为目的只是展示如何在模板框架内实现该方法。如果您有兴趣查看整个实现,请查看附件。所有的EA交易和其他必要的材料都可以在本文的附件中找到。我的模板有很多多余的东西,包括不必要的函数或变量,可以通过某种方式进行优化。我个人不介意。如果有什么干扰操作,我就把它移走。对我来说更重要的是现在一切正常。我一直在开发一些东西,所以我没有时间把每一个细节都做到完美。我也认为没有理由将所有过程和变量存储在类中,而这可以提高代码顺序和可读性。模板非常简单。程序将使用的带引号的文件将由一个专门的EA交易生成,该EA根据历史运行,并将柱形图数据写入一个文本文件,该文件的结构便于程序读取。我不会提供这个EA的代码在这里,因为它可以很容易地开发。
使用程序查找和分析模式
我选择了三个市场区域进行分析,它们各有一个月的长度,并相互跟随。EURUSD, M5.
- 第一个时间段: 2020.01.13 - 2020.02.16
- 第二个时间段: 2020.02.13 - 2020.03.15
- 第三个时间段: 2020.03.13 - 2020.04.18
时间间隔已选定,因此最后一天总是星期五。你知道,星期五是本周最后一个交易日。以这种方式选择区间可以让你有整整两天的时间来搜索模式,直到交易所重新开始交易。这是一个小的生存技巧。在我们的例子中,这并不重要,因为我们在测试器中测试EA。我决定在这里描述所发现模式的12种变体。其中六个是最大次数为1的多项式。其他六个的最大次数为2。我觉得这就够了。
我的程序的第一个选项卡如下所示:
它提供了更改数字生成类型的功能,例如,仅正、正和负、正和零、正和负和零。搜索条件在第二个组合框中配置。有两种可能的选择:以点数表示的期望值和我对利润系数公式的模拟。在我的公式中,这个值的范围是-1到+1。此外,它也不存在由于除零错误而无法计算利润系数的情况。
P_Factor=(Profit-Loss)/(Profit+Loss).
然后是多项式的最大次数和用于计算的处理器核数。在文本块中,指定多项式的柱数或计算数,以及我自己发明的处理不对称系数,这与前面的公式非常相似:
D_Asymmetry=|(BuyTrades-SellTrades)|/(BuyTrades+SellTrades).
其值在0到1之间。当我们需要指示程序具有相似数量的买入和卖出信号时,需要使用此过滤器,以避免在全局趋势期间所有交易都在同一方向执行的情况。接下来是所有找到的变量中最好的变量的数量,这些变量应该存储在内存中,以及加载的引用的百分比将用于暴力迭代。这一部分是从最后一根柱开始测量的开放时间,它后面的部分将用于优化。我将不解释其余的指标和变量列表,因为它们足够简单。
第二个选项卡如下所示:
选项卡对应于机器人第一部分的第一个变体。你可以比较我的程序中的图表和测试器的图表,这将在下面介绍。报价单中执行暴力的部分用黄色显示,其他部分用红色显示。我不会在这里提供所有变体的截图-所有这些都可以在附件中找到。
让我们看看标签上有什么。Interval Points — 分解多项式值的区间。当我们在第一个选项卡上使用暴力算法时,它计算变量的主要参数以及该多项式的模极大值。因此,我们知道多项式的值的窗口,可以把这个窗口分成相等的部分,逐渐增加值,试图检测出更强的信号。这在第二个选项卡中完成。此选项卡还具有存储在优化内存中的搜索类型和最佳变体数。进一步的过滤器允许我们过滤掉不符合模式定义的不必要的变体。“线控制”为每个变量启用一个附加的运行,其中它计算图形线与连接图形起点和终点的直线的相对偏差。
Deviation = Max(|Profit[i]-LineProfit[i]|)/EndProfit.
这里,Profit[i]是第i阶平衡曲线的值,LineProfit[i]是直线上的相同值,EndProfit是图表末尾的值。
所有值均以点为单位测量。使用百分比控制是图表中红色部分的百分比(在本例中我没有使用此筛选器)。还有一个过滤器用于过滤最小订单数。