将概率论应用于缺口交易

1 二月 2019, 06:42
Aleksey Nikolayev
0
415

内容

概述

本文延续作者之前文章中开始的话题:在交易中应用概率论和数理统计。 我们将研究运用相应的方法来创建并测试交易策略。

首先,我们将探索这种交易的可能性,即检测来自随机漫游假想的偏差。 事实证明,如果价格表现为零漂移随机漫游(没有方向趋势),那么盈利交易是不可能的。 这为寻找反制这一假想的方法提供了基础。 如果找到一种反制该假想的方法,我们也许会尝试运用它来制定交易策略。

我们还将继续研究我们在之前发表的文章中开始的风险主题。 进而,我们将它们作为 第一第二 参考文章。

由于我们的方法基于概率论,因此建议理解其基础,但这并非强制性的。 重要的是理解概率方法的本质 − 越是系统和经常性地使用它们,得到的结果越可观和明显(由于大数定律)。 当然,它们的应用要充分论证。

有关智能交易系统的一般性注意事项

开发 EA 大致可分为三个阶段:

  1. 产生思路。
  2. 运用各种简化验证思路。
  3. 令思路适配市场现实。

本文将主要探讨第二阶段,令我们能够更加彻底地契合所述主题。 此外,在论坛上针对这个阶段的讨论比其它阶段更频繁。

我们来描述已实现的简化。 我们将 EA 限定在仅交易单一资产。 我们假设资产价格以账户货币表示。 我们将排除非交易操作(如账户中的利息和资金存取),并且不会考虑不同类型的订单(仅按市价进行买卖)。 我们将在执行订单时忽略滑点,且将点差(s)视为固定数值。 我们还假设由 EA 管控我们账户上的所有资金,并且没有其它 EA 参与干扰。

通过所有这些简化,EA 操作结果确定无疑地定义为 v(t) 函数 − 取决于时间的持仓量。 正数值 v(t) 对应买入,而负数值对应卖出。 此外,p(t) (资产价格) 函数和 c0 (初始资金)。 下图展示了可能的 v=v(t) 持仓图表。

持仓样本

买入价格和卖出价格之间的算术平均值。

v(t)p(t) 是分段常数(步进)函数,因为它们的值是一些最小增量步长的倍数。 如果需要更严格的数学定义,则可以认为它们自右侧是连续的,并且在左侧的缺口处有限制。 我们假定 v(t) 缺口点数永远不会匹配 p(t)。 换言之,任意时刻至多改变两者当中其一的数值 − 价格或持仓量,或两者都保持不变。 值得注意的是,价格或交易量可能发生变化的时间点,也是某个最小步长的倍数。

根据这些数据,我们可以找到 c(t) 函数 − 取决于时间的资金数值。 它被定义为我们在 t 时刻平仓情况下由 EA 管控的账户部分的余额值。 由于我们在账户上只有一个 EA,因此该数值与 MetaTrader 5 中定义的账户净值一致。

定义 c(t)t 时刻的变化。 如果此时的交易量和价格没有变化,那么它自然为零。 如果价格变化,资金的增长等于交易量乘以价格增量的乘积。 如果交易量发生变化,则可能有两种选项 − 当绝对持仓量减少时,资金保持不变,当它增长时,资金降低的额度等于点差与交易量变化绝对值的乘积。 换言之,如果一笔持仓部分平仓,净值不会变化,而加仓则会导致净值略有下降。 所以,c(t)t 时刻的资金数值等于 c0=c(0) 之总合,且其所有变化都是从零时刻直至 t

在开发我们的风险理论时(在之前的两篇文章中),我们使用了“交易(deal)”的概念。 这个概念与 MetaTrader 5 中所谓的“成交(deal)”并不完全吻合,更多的对应于那里所谓的“交易(trade)”。 确切地说,它对应于我们所说的简单仓位。 根据我们的定义,一笔简单仓位是由开仓和平仓的时刻决定的。 它的交易量和方向在那些时刻之间保持不变。 下面是一笔简单仓位的样本 v=v(t) 图表。

简单仓位 (交易)

任何仓位(因为它总是分段常数)可以想象为简单仓位的总和。 这种表示可以由无数种方式完成。 下图展示了由两种不同方式表示的单笔仓位,即简单仓位的总和。 初始仓位以蓝色显示,而所有交易以绿色和红色分开显示。 当采用 MetaTrader 5 中的成交概念时,我们还有另一种选择。 这些方法中的每一种都非常合理。

将仓位切分为成交的两种方法

有些 EA,采用这样表示没有多大意义。 例如,可能有些 EA,其仓位递增,之后再递减。 与此同时,还有些 EA,采用这样表示则非常自然。 例如,一笔持仓可以由一系列在时间上不相交的简单仓位组成。 下图包含此类持仓的示例。

不适与相适的仓位将作为交易总合呈现

每笔交易(简单仓位)后的资金相对变化 c1/c0 表示为两个数值 − 盈利能力 a 和风险 r: c1/c0=1+ra。 盈利能力等于交易期间的价格上涨与入场价格和止损之间差值的比率,而风险与交易量成比例,且意味着在止损确切激活的情况下将亏损的资金份额。

所以,替代研究 v(t), p(t)c(t) 时间函数,我们转向分析表征交易顺序的数字序列。 这极大地简化了进阶研究。 特别是,当处理不确定性的概率模型时,我们能避免在有限随机变量集合上应用自我限定随机过程理论。

针对资产价格行为与交易结果,概率论是一种普遍接受的不确定性数学建模方法。 根据这种方法,我们应考虑将 v(t), p(t)c(t) 函数作为一些随机过程的具体实现(轨迹)。 一般而言,这项任务实际上是无法解决的。 主要原因是缺乏准确描述价格行为的契合概率模型。 因此,考虑可能的解决方案的特殊情况是有意义的。 如上所述,在本文中,我们将考虑由 EA 形成的仓位可以恰如其分地表示为一系列简单仓位(交易)。

值得一提的是与 EA 相关的另一个问题 – 参数。 详研究虑它们对于在 EA 开发过程实现一些形式化(标准化)将会很有用。 我们将参数分为三种类型:

  1. 历史参数。 EA 操作期间,可能会在一笔交易至另一笔交易时变化的参数。 这些是指标值,日间时刻,新闻数据,月相等。 一般来说,它们是时间函数,就像价格或持仓量一样。 在应用简化的情况下,我们可将它们视为已知的达成交易时刻的数字序列。 每个特定交易的参数(方向,交易量,止损和止盈)基于历史参数的数值来定义。
  2. 实际参数。 我们简单地称它们为短暂参数。 它们在 EA 开始交易时设置,并且只能在测试和优化 EA 时应用。
  3. 元参数设置 EA 优化算法,例如,自定义优化条件参数。 假设我们想要通过两个条件来优化 EA,尽管仅能逐一来完成。 我们将两个原始条件形成一个新条件,并取它们的一些权重总和。 这些权重作为元参数。

例如,在下面描述的基于缺口的 EA 中,最小缺口是 EA 参数,而每个特定缺口的大小是历史参数。 在这种情况下,元参数可以包括优化条件编号(我们假设条件以某种顺序编号,例如,按照利润优化是#1,而按照回撤优化是#2,等等)。

在本文中,我们将使用与历史参数相关的一个重要简化。 当我们谈论交易的回报分布时,通常可能取决于这些参数。 我们假设这种依赖性是微不足道的。 主要原因是考虑到这种依赖性通常令模型过度复杂化,最终可能导致过度拟合。

交易策略是反制随机漫游假想的一种尝试

我们已经提到缺乏描述价格行为的准确模型。 然而,近似的模型也可能有用。 例如,存在一种众所周知的价格行为模型,视其价格作为零漂移的随机漫游(没有定向趋势)。 该模型称为 随机漫游假想。 根据这一假想,如果我们考虑到点差,任何 EA 的平均利润均为零或小有亏损。

证明不可能在随机漫游中赚钱是相当困难的,因为它需要涉及随机过程理论的复杂数学装置(伊藤演算停止时间, 等等)。 一般来说,它归结为这样的陈述,即在没有趋势的随机漫游交易时,资本是 鞅(martingale)(概率论,不要与赌博系统的 马丁格尔(martingale) 混淆)。 鞅是一个随机过程,其平均值(数学期望)不随时间变化。 在我们的例子中,这意味着任何时候资本数值的数学期望等于其初始值。

因此,我们开始研究交易思路时,应搜索随机漫游的统计数据中的明显价格偏差。 为此,我们将使用概率论和数理统计中的思路,但首先,我们做一些观察:

  • 任何这种解决方案本质上都是概率性的 − 我们的结论总是会有一些非零概率是错误的。
  • 如果我们的方法没有检测到偏差,这并不意味着它们完全没有。 也许,其它一些方法会检测到它们。
  • 统计上明显的偏差并不能保证获得统计上显著的正盈利 − 偏差的存在是必要的,但不是充分条件。

我们来构建一个搜索随机漫游偏差的方法。 为此,我们考虑一些随机变量,我们将依据采用实际价格形成的样本建立经验概率分布。 此外,假设价格行为是随机漫游,我们将构建相同数值的理论概率分布。 比较这些分布,我们将决定反制(或不可能反制)随机漫游假想。

我们lai1构建一个合适数值的示例。 假设在 t0 初始时刻,价格等于 p0。 我们取其它 p1 价格值不等于 p0。一直等到价格抵达数值 p(t1)=p1t1 时刻。 我们在 t0 and t1 时间段中搜寻价格 p2,其距价格 p1 最远。 我们引入数值 K=(p2-p0)/(p0-p1)p1<p0p2p2p0<p1 条件永远有效,因此在任意时间 K0。 下面提供了解释这一思路的图表。 蓝线代表 p0 价位,而它与价格图表的交叉时刻是 t0。 红线代表 p1 价位,它在 t0 之后触及图表的时刻为 t1。 绿线代表 p2 价位,位于距 p1 尽可能远的地方。

p0, p1 和 p2 价位

数值背后的思路很简单。 假设我们在 t0 入场交易。 在价位 p0 卖出,而 p1p1>p0 − 止损。 p2 是止盈的最低可实现价格,而 K 是交易中可实现的最高利润。 实际上,我们在执行交易时并不知道确切的 K 值。 在这种不确定性的概率模型框架内,我们只能谈论知晓的概率分布形态。 假设我们知晓 Fk(x) 概率分布函数,其定义为 K<x 的概率。 假设我们使用某个 pk 价位作为止盈: pk-p0=k(p0-p1)。 在此情况下,Fk(k) 等于触及止损先于止盈的概率。 相应地,1-Fk(k) 等于先激活止盈的概率。 现在让点差等于零。 那么,在止损激活的情况下,盈利能力等于 -1,而在止盈激活的情况下,它等于 k。 这种交易中的数学期望:M=(-1)*Fk(k)+k*(1-Fk(k))=k-(k+1)*Fk(k),如果 Fk(k)=k/(k+1) 其等于零。

如果我们知道方程式 Fk(x),我们甚至可以执行 EA 的初步优化。 例如,我们可以寻找最优的止盈/止损比率,令交易盈利能力的数学期望最大化。 然后我们可以在交易中找到最优风险值。 因此,EA 甚至可以在就绪之前进行优化。 这样可以节省时间,并可令您在早期阶段舍弃明显不合适的思路。

如果我们假设价格表现得像没有趋势的随机漫游,那么 K 数值的分布则由 Fk(x)=Fk0(x) 分布函数设置,其中 Fk0(x)=0 如果 x0 以及 Fk0(x)=x/(x+1) 如果 x>0。 为了更加确定,我们可以假设这里使用的随机漫游是一个零漂移的维纳(Wiener)过程(无趋势)。 正如我们所看到的,如果满足随机漫游假想并且点差等于零,则在任何止盈/止损比率下,盈利能力的数学期望等于零。 在非零点差的情况下,它是负数。

替代 K,我们可以考虑数值 Q=K/(K+1)=(p2-p0)/(p2-p1)K=Q/(1-Q)。 这一数值可表示为止盈与(止损 + 止盈)的比率。 它更方便,因为它取自 [0;1) 间隔内的数值,并且在随机漫游的情况下它具有比 K 更简单的分布(在此间隔内均匀)。

接下来,我们将主要讨论 Q 值。 我们来考察如何构造和应用其经验分布函数 Fq(x)。 假设我们有一个交易思路,我们检查价格历史。 我们有 n 个入场点的集合。 入场价格 p0,i 和止损 p1,i,其中 i=1,...,n,为它们当中的每一个定义。 现在我们应定义这个思路是否具有一定的盈利潜力。 对于每笔交易,我们应搜索尽可能远离止损的价格 p2,i,直到激活时刻。 基于此价格,我们得到了 n 个样本 Qi=(p2,i-p0,i)/(p2,i-p1,i)i=1,...,n。 由该样本构建的经验分布函数由 Fq(x)=m(x)/n 方程定义,其中 m(x) 等于 Qi 样本元素小于 x 的数量。 如果价格行为表现得像没有趋势的随机漫游(零漂移维纳(Wiener) 过程), 数值的 Fq0(x) 分布函数看起来很简单: Fq0(x)=0 如果 x0Fq0(x)=x 如果 0<x1,以及 Fq0(x)=1 如果 x>1

如果 Fq(x) 与随机漫游的理论分布函数 Fq0(x) 有显著差异,我们需要在盈利能力方面检查这种差异的重要性。 如果即使考虑到点差,盈利能力也足够正面,那么是时候选择适当的止盈/止损比率了。 这可以通过最大化盈利预期来实现。 之后,我们可以为每笔交易的风险值选择一个最优值,然后初步测试这一思路。 如果结果很正面,那么继续创建实际的交易 EA 就有意义了。 接下来,我们将尝试在实际操作中展示此算法。

问题升级了 – 如何运用随机漫游进行类似的比较,以便获得更复杂的离场算法。 通常答案与上面研究的状况相同。 主要障碍是随机漫游的利润分布只可在极少数情况下以分析形式获得。 但总是可以使用蒙特卡罗模拟方法得到其经验近似。

缺口交易策略

在分析这一思路之前,我应注意到我们的主要任务是展示分析方法,而不是可盈利的交易策略。 太过专注于利润会令我们陷入琐碎的细节之中,从而分散我们对于整体面貌的注意力。

资产价格是离散的,因此变化总是突发和跳跃式。 这些突发跳跃的大小也许不同。 当它们很大时,它们被称为缺口。 没有明确的界线将缺口与通常的价格变化区分开来。 我们可以自由地设定我们认为合适的边界。

缺口非常适合展示上一节中概述的理论。 它们中的每一个都由两个时间点和资产价格设定。 使用之前介绍的价格表示法,我们假设 p0 是靠后的价格,而 p1 是较早的。 一旦出现缺口,我们就入场交易。 价格 p1 不仅可以视为止损,还可以视为止盈。 这意味着我们可以选择两种系统类型当中的一种 − 希望快速补齐缺口,或者在缺口方向上价格走势拉开更大。 补齐缺口意即价格返回 p1 价位,或突破它。

由于标准形式的缺口在外汇资产中相对较少,因此讨论本文的主题时,论坛管理部门建议作者泛化这一概念。 在定义缺口时,您可以舍弃需求陈述,即仅考虑两个顺序价格之间的差距。 显然,在这种情况下可能存在的缺口数量将变得难以想象地巨大,因此从交易的角度来看,从合理的选择中限制自我是值得的。 例如,作者被建议考察一个美国时段的收盘价与下一个时段的开盘价之间的缺口。

我来解释一下时段的概念是如何形式化的。 它由三个时间间隔设置:期限,长度和顺移。 时段是定期的,长度不超过期限。 任何逐笔报价都属于所有时段,或不属于任何时段(如果其长度严格小于期限,则后者是可能的)。 该顺移是零时间点与其后的第一个时段开始之间的时间间隔。 它应该小于期限。 这个时段的概念比平时稍微宽一些,并能够让我们观察,例如,分钟柱线之间的缺口。 我们会在如下示意图中描绘它。 绿色箭头代表的时间长度定义顺移,红色箭头表示期限,而蓝色箭头表示时段长度。

时段

我们将使用两个略有不同的 EA 来收集与缺口相关的统计数据。 第一个(gaps_reg_stat.mq5)考虑两个顺序逐笔报价间的缺口,而第二个(gaps_ses_stat.mq5)考虑时段间的缺口。 当然,这些 EA 不会在测试模式下进行交易和运行。 建议仅基于真正的逐笔报价上运行第一个,而对于第二个 − 基于分钟的 OHLC 上运行。 EA 代码提供如下。

// gaps_reg_stat.mq5
#define ND 100

input double gmin=0.1;                 // 最小缺口大小: USDJPY - 0.1, EURUSD - 0.001
input string fname="gaps\\stat.txt";   // 统计文件名

struct SGap
  { double p0;
    double p1;
    double p2;
    double p3;
    double s;
    void set(double p_1,double p,double sprd);
    bool brkn();
    void change(double p);
    double gap();
    double Q();
  };

class CGaps
  { SGap gs[];
    int ngs;
    int go[];
    int ngo;
    public:
    void init();
    void add(double p_1,double p,double sprd);
    void change(double p);
    void gs2f(string fn);
  };

CGaps gaps;
MqlTick tick0;
bool is0=false;

void OnTick()
  { MqlTick tick;
    if (!SymbolInfoTick(_Symbol, tick)) return;
    if(is0)
      { double p=(tick.bid+tick.ask)*0.5, p0=(tick0.bid+tick0.ask)*0.5;
        gaps.change(p);
        if(MathAbs(p-p0)>=gmin) gaps.add(p0,p,tick.ask-tick.bid);
      }
    else is0=true;
    tick0=tick;
  }

int OnInit()
  { gaps.init();
     return(INIT_SUCCEEDED);
  }
  
void OnDeinit(const int reason)
  { gaps.gs2f(fname);
  }

void SGap :: set(double p_1,double p,double sprd)
  { p1=p_1; p0=p2=p3=p; s=sprd;
  }

bool SGap :: brkn()
  { return ((p0>p1)&&(p3<=p1))||((p0<p1)&&(p3>=p1));
  }

void SGap :: change(double p)
  { if(brkn()) return;
    if((p0>p1&&p>p2) || (p0<p1&&p<p2)) p2=p;
    p3=p;
  }

double SGap :: gap()
  { return MathAbs(p0-p1);
  }

double SGap :: Q()
  { double q=p2-p1;
    if(q==0.0) return 0.0;
    return (p2-p0)/q;
  }

void CGaps :: init()
  { ngs=ngo=0;
  }

void CGaps :: add(double p_1,double p,double sprd)
  { ++ngs;
    if(ArraySize(gs)<ngs) ArrayResize(gs,ngs,ND);
    gs[ngs-1].set(p_1,p,sprd);
    int i=0;
    for(; i<ngo; ++i) if(go[i]<0) break;
    if(i==ngo)
      {
        ++ngo;
        if(ArraySize(go)<ngo) ArrayResize(go,ngo,ND);
      }
    go[i]=ngs-1;
  }

void CGaps :: change(double p)
  { for(int i=0; i<ngo; ++i)
      { if(go[i]<0) continue;
        gs[go[i]].change(p);
        if(gs[go[i]].brkn()) go[i]=-1;
      }
  }

void CGaps :: gs2f(string fn)
  { int f=FileOpen(fn, FILE_WRITE|FILE_COMMON|FILE_ANSI|FILE_TXT), c;
    for(int i=0;i<ngs;++i)
      { if (gs[i].brkn()) c=1; else c=0;
        FileWriteString(f,(string)gs[i].gap()+" "+(string)gs[i].Q()+" "+(string)c+" "+(string)gs[i].s);
        if(i==ngs-1) break;
        FileWriteString(f,"\n");
      }
    FileClose(f);
  }
// gaps_ses_stat.mq5
#define ND 100

input double gmin=0.001;               // 最小缺口大小: USDJPY - 0.1, EURUSD - 0.001
input uint   mperiod=1;                // 时段期限(分钟)
input uint   mlength=1;                // 时段长度(分钟)
input uint   mbias=0;                  // 第一个时段乖离(分钟)
input string fname="gaps\\stat.txt";   // 统计文件名

struct SGap
  { double p0;
    double p1;
    double p2;
    double p3;
    double s;
    void set(double p_1,double p,double sprd);
    bool brkn();
    void change(double p);
    double gap();
    double Q();
  };

class CGaps
  { SGap gs[];
    int ngs;
    int go[];
    int ngo;
    public:
    void init();
    void add(double p_1,double p,double sprd);
    bool change(double p);
    void gs2f(string fn);
  };

CGaps gaps;
MqlTick tick0;
int ns0=-1;
ulong sbias=mbias*60, speriod=mperiod*60, slength=mlength*60;

void OnTick()
  { MqlTick tick;
    if (!SymbolInfoTick(_Symbol, tick)) return;
    double p=(tick.bid+tick.ask)*0.5;
    gaps.change(p);
    int ns=nsession(tick.time);
    if(ns>=0)
      { double p0=(tick0.bid+tick0.ask)*0.5;
        if(ns0>=0&&ns>ns0&&MathAbs(p-p0)>=gmin) gaps.add(p0,p,tick.ask-tick.bid);
        ns0=ns;
        tick0=tick;
      }
  }

int OnInit()
  { if(speriod==0||slength==0||speriod<slength||speriod<=sbias)
      { Print("wrong session format");
        return(INIT_FAILED);
      }
    gaps.init();
    return(INIT_SUCCEEDED);
  }
  
void OnDeinit(const int reason)
  { gaps.gs2f(fname);
  }

int nsession(datetime t)
  { ulong t0=(ulong)t;
    if(t0<sbias) return -1;
    t0-=sbias;
    if(t0%speriod>slength) return -1;
    return (int)(t0/speriod);
  }

void SGap :: set(double p_1,double p,double sprd)
  { p1=p_1; p0=p2=p3=p; s=sprd;
  }

bool SGap :: brkn()
  { return ((p0>p1)&&(p3<=p1))||((p0<p1)&&(p3>=p1));
  }

void SGap :: change(double p)
  { if(brkn()) return;
    if((p0>p1&&p>p2) || (p0<p1&&p<p2)) p2=p;
    p3=p;
  }

double SGap :: gap()
  { return MathAbs(p0-p1);
  }

double SGap :: Q()
  { double q=p2-p1;
    if(q==0.0) return 0.0;
    return (p2-p0)/q;
  }

void CGaps :: init()
  { ngs=ngo=0;
  }

void CGaps :: add(double p_1,double p,double sprd)
  { ++ngs;
    if(ArraySize(gs)<ngs) ArrayResize(gs,ngs,ND);
    gs[ngs-1].set(p_1,p,sprd);
    int i=0;
    for(; i<ngo; ++i) if(go[i]<0) break;
    if(i==ngo)
      {
        ++ngo;
        if(ArraySize(go)<ngo) ArrayResize(go,ngo,ND);
      }
    go[i]=ngs-1;
  }

bool CGaps :: change(double p)
  { bool chngd=false;
    for(int i=0; i<ngo; ++i)
      { if(go[i]<0) continue;
        gs[go[i]].change(p);
        if(gs[go[i]].brkn()) {go[i]=-1; chngd=true;}
      }
    return chngd;
  }

void CGaps :: gs2f(string fn)
  { int f=FileOpen(fn, FILE_WRITE|FILE_COMMON|FILE_ANSI|FILE_TXT), c;
    for(int i=0;i<ngs;++i)
      { if (gs[i].brkn()) c=1; else c=0;
        FileWriteString(f,(string)gs[i].gap()+" "+(string)gs[i].Q()+" "+(string)c+" "+(string)gs[i].s);
        if(i==ngs-1) break;
        FileWriteString(f,"\n");
      }
    FileClose(f);
  }

EA 非常简单,尽管在 CGaps 类中提及 go[] 数组是有意义的,其中存储未补齐缺口的索引,以便允许加速 EA 工作。

在任何情况下,为每个缺口记录以下数据:绝对缺口值,Q 值,闭合时的数据和缺口的间隙值。 然后,检查 Q 经验分布值与均匀分布值之间的差值,并做出进一步分析的决定。 图形和计算(Kolmogorov 统计计算)方法用于检查差值。 为简单起见,我们将自己限制为 Kolmogorov-Smirnov 检验的 p-value 作为计算的结果。 它取 0 到 1 之间的值,并且越小,样本分布与理论分布一致的可能性就越小。

我们选择了 Kolmogorov-Smirnov(单样本)测试用于数学研究。 主要原因是我们对区分统一收敛度量中的分布函数感兴趣,而非任何积分度量。 在 MQL5 函数库中找不到此测试,因此我不得不使用 R 语言。 值得注意的是,如果样本中存在匹配数字,则该条件的准确性会有所降低(R 给出相应的警告),但仍然可以接受。

如果发现理论分布和经验分布之间存在显著差异,我们应该研究从中赚取利润的可能性。 如果没有明显的差异,那么我们要么放弃这个思路,要么尝试改进它。

如上所述,当形成缺口时,有两种方法可以在 p0 价格处入场交易 − 按缺口方向或其背向。 我们来计算这两种情况的回报期望。 在这样做时,我们考虑将点差恒定,并表示为 s。 绝对缺口值表示为 g,而 g0 表示其 最低值。

  • 按缺口方向入场。 在此情况下,g+s 为止损,而 kg-s 为止盈。 此处 k 是盈利/亏损比率。 盈利数值: -1 在止损激活的情况下,以及 (kg-s)/(g+s) 在止盈激活的情况下。 相应的概率: Fq(q) and 1-Fq(q)。 表达为 k 经由 q: k=k(q)=q/(1-q)。 然后是对盈利能力的数学期望 M, M=Fq(q)*(-1)+(1-Fq(q))*(k(q)g-s)/(g+s)。 仅有 q 值,至于 M 对于所有 gg0 都明显积极,适合我们。 我们需要 q 值,而当 Fq(q) 明显在 g=g0 时低于随机漫游理论值Fq(q)<Fq0(q)
  • 逆向缺口入场。 在此情况下,g-s 为止盈,而 kg+s 为止损。 此处 k 是亏损/盈利比率。 盈利能力值:在止损激活的情况下 -1,而在激活止盈的情况下 (gs)/(kg+s)。 相应的概率: 1-Fq(q)Fq(q)。 表达为 k 经由 q 与前一段中的相同: k=k(q)=q/(1-q)。 对于盈利能力的数学期望 M,我们得到方程式 M=(1-Fq(q))*(-1)+*Fq(q)(g-s)/(k(q)g+s)。 仅有 q 值,至于 M 对于所有 gg0 都明显积极,适合我们。 我们需要 q 值,而当 Fq(q) 明显在 g=g0 时高于随机漫游理论值 Fq(q)>Fq0(q)

收集两个品种的统计数据:

  1. EURUSD
  2. USDJPY

它们当中的每个品种都考虑了以下类型的缺口:

  1. 在连续的逐笔报价之间。
  2. 在分钟柱线之间。
  3. 在交易时段之间。 对于 EURUSD,这是一个美洲时段(芝加哥和纽约合并),而对于 USDJPY,这是一个东京时段。

对于这六个选项中的每一个,都会针对最新的统计数据进行研究:

  1. 200 缺口
  2. 50 缺口

因此,我们有 12 个选项。 它们当中的每个结果如下:

  1. p-value 针对 Kolmogorov 统计
  2. mean spread 在缺口形成时的平均点差
  3. Q 函数值的经验和理论(红线)分布图
  4. M_cont 盈利能力数学期望图表,交易按缺口方向,依据 q,与 M_cont=0 理论线(红色)比较。 此处,q 为止盈 /(止盈+止损)的比率。
  5. M_rvrs 盈利能力数学期望图表,交易按缺口背向,依据 q,与 M_rvrs=0 理论线(红色)比较。 此处,q 为止损 /(止盈+止损)的比率。

下面提供了所有结果选项。

  1. EURUSD, 连续逐笔报价间最后 200 个缺口。 p-value: 3.471654e-07, mean spread: 0.000695

    EURUSD, 连续逐笔报价间最后 200 个缺口。


  2. EURUSD, 连续逐笔报价间最后 50 个缺口。p-value: 0.2428457, mean spread: 0.0005724

    EURUSD, 连续逐笔报价间最后 50 个缺口。

  3. EURUSD, 连续分钟柱线间最后 200 个缺口。 p-value: 8.675995e-06, mean spread: 0.0004352

    EURUSD, 连续分钟柱线间最后 200 个缺口。

  4. EURUSD, 连续分钟柱线间最后 50 个缺口。p-value: 0.0125578, mean spread: 0.000404

    EURUSD, 连续分钟柱线间最后 200 个缺口。

  5. EURUSD, 时段间最后 200 个缺口。p-value: 0.6659917, mean spread: 0.0001323

    EURUSD, 时段间最后 200 个缺口。

  6. EURUSD, 时段间最后 50 个缺口。p-value: 0.08915716, mean spread: 0.0001282

    EURUSD, 时段间最后 50 个缺口。

  7. USDJPY, 连续逐笔报价间最后 200 个缺口。p-value: 2.267454e-06, mean spread: 0.09563

    USDJPY, 连续逐笔报价间最后 200 个缺口

  8. USDJPY, 连续逐笔报价间最后 50 个缺口。p-value: 0.03259067, mean spread: 0.0597

    USDJPY, 连续逐笔报价间最后 50 个缺口。

  9. USDJPY, 连续分钟柱线间最后 200 个缺口。p-value: 0.0003737335, mean spread: 0.05148

    USDJPY, 连续分钟柱线间最后 200 个缺口。

  10. USDJPY, 连续分钟柱线间最后 50 个缺口。p-value: 0.005747542, mean spread: 0.0474

    USDJPY, 连续分钟柱线间最后 50 个缺口。

  11. USDJPY, 时段间最后 200 个缺口。p-value: 0.07743524, mean spread: 0.02023

    USDJPY, 时段间最后 200 个缺口。

  12. USDJPY, 时段间最后 50 个缺口。p-value: 0.009191665, mean spread: 0.0185

    USDJPY, 时段间最后 50 个缺口。

我们可以从这些结果中得出以下结论:

  • 随机漫游的偏差非常显著。
  • 按缺口方向的交易是没有希望的。 向缺口闭合方向交易看起来更为可取,但利润很小(特别是 EURUSD)。
  • 在形成缺口时,与其平均值相比,点差可以增长几倍(对于逐笔报价和分钟柱线之间的缺口,这是真实的)。
  • 按时段交易,来自随机漫游的缺口仅在 1-2 个月的某些时间点内出现,而在一年的期限中,偏差非常小。 乍一看,这是由某个时间点的主流趋势来判断。 显然,补齐缺口在横盘区域更好,但在趋势区域则更糟,尽管这需要进行更详细的分析。 
  • 为了进一步分析,我们应该选择 USDJPY 的分钟柱线缺口。

测试策略并计算每笔成交的最佳风险

基于 USDJPY 分钟柱线之间缺口的系统版本看起来最有希望。 我们在缺口形成时检测到的点差显著增加,这意味着我们应该更加关注其定义。 我们为其指定如下。 我们将考虑的缺口不是为了平均价格,而是为了竞买价(bid)和竞卖价(ask)。 此外,我们将依据入场交易的类型选择其中之一。 这意味着,我们将依据竞卖价定义上行缺口,而下行缺口 — 竞卖价。 缺口闭合也是如此。

我们通过稍微修改我们用于收集时段缺口的统计数据来开发 EA。 主要变化涉及缺口结构。 由于我们在缺口和交易之间存在明确的对应关系,因此交易所需的全部信息(交易量和平仓条件)将存储在此结构中。 为了交易增加了两个函数。 其一是 (pp2v()) 计算每笔交易的交易量,而另一个 (trade()) 保存交易量和持仓量之间的对应关系。 EA 代码 (gaps_ses_test.mq5) 提供如下。

// gaps_ses_test.mq5
#define ND 100

input uint   mperiod=1;                 // 时段期限(分钟)
input uint   mlength=1;                 // 时段长度(分钟)
input uint   mbias=0;                   // 第一个时段乖离(分钟)
input double g0=0.1;                    // 最小缺口大小: USDJPY - 0.1, EURUSD - 0.001
input double q0=0.4;                    // q0=sl/(sl+tp)
input double r=0.01;                    // 交易风险
input double s=0.02;                    // 近似点差
input string fname="gaps\\stat.txt";    // 统计文件名

struct SGap
  { double p0;
    double p1;
    double p2;
    double v;
    int brkn();
    bool up();
    void change(double p);
    double gap();
    double Q();
    double a();
  };

class CGaps
  { SGap gs[];
    int ngs;
    int go[];
    int ngo;
    public:
    void init();
    void add(double p_1,double p);
    bool change(double pbid,double pask);
    double v();
    void gs2f(string fn);
  };

CGaps gaps;
MqlTick tick0;
int ns0=-1;
ulong sbias=mbias*60, speriod=mperiod*60, slength=mlength*60;
double dv=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);

void OnTick()
  { MqlTick tick;
    if (!SymbolInfoTick(_Symbol, tick)) return;
    bool chngd=gaps.change(tick.bid,tick.ask);
    int ns=nsession(tick.time);
    if(ns>=0)
      { if(ns0>=0&&ns>ns0)
          { if(tick0.ask-tick.ask>=g0) {gaps.add(tick0.ask,tick.ask); chngd=true;}
              else if(tick.bid-tick0.bid>=g0) {gaps.add(tick0.bid,tick.bid); chngd=true;}
          }
        ns0=ns;
        tick0=tick;
      }
    
    if(chngd) trade(gaps.v());
  }

int OnInit()
  {
     gaps.init();
     return(INIT_SUCCEEDED);
  }
  
void OnDeinit(const int reason)
  { gaps.gs2f(fname);
  }

int nsession(datetime t)
  { ulong t0=(ulong)t;
    if(t0<sbias) return -1;
    t0-=sbias;
    if(t0%speriod>slength) return -1;
    return (int)(t0/speriod);
  }

double pp2v(double psl, double pen)
  { if(psl==pen) return 0.0;
    double dc, dir=1.0;
    double c0=AccountInfoDouble(ACCOUNT_EQUITY);
    bool ner=true;
    if (psl<pen) ner=OrderCalcProfit(ORDER_TYPE_BUY,_Symbol,dv,pen+s,psl,dc);
      else {ner=OrderCalcProfit(ORDER_TYPE_SELL,_Symbol,dv,pen,psl+s,dc); dir=-1.0;}
    if(!ner) return 0.0;
    return -dir*r*dv*c0/dc;
  }

void trade(double vt)
  { double v0=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
    if(-v0<vt<v0) vt=v0*MathRound(vt/v0);
    double vr=0.0;
    if(PositionSelect(_Symbol))
      { vr=PositionGetDouble(POSITION_VOLUME);
        if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) vr=-vr;
      }
    int vi=(int)((vt-vr)/dv);
    if(vi==0) return;
    MqlTradeRequest request={0};
    MqlTradeResult  result={0};
    request.action=TRADE_ACTION_DEAL;
    request.symbol=_Symbol; 
    if(vi>0)
      { request.volume=vi*dv;
        request.type=ORDER_TYPE_BUY;
      }
      else
        { request.volume=-vi*dv;
          request.type=ORDER_TYPE_SELL;
        }
    if(!OrderSend(request,result)) PrintFormat("OrderSend error %d",GetLastError());
  }

int SGap :: brkn()
  { if(((p0>p1)&&(p2<=p1))||((p0<p1)&&(p2>=p1))) return 1;
    if(Q()>=q0) return -1;
    return 0;
  }

bool SGap :: up()
  { return p0>p1;
  }

void SGap :: change(double p)
  { if(brkn()==0) p2=p;
  }

double SGap :: gap()
  { return MathAbs(p0-p1);
  }

double SGap :: Q()
  { if(p2==p1) return 0.0;
    return (p2-p0)/(p2-p1);
  }

double SGap :: a()
  { double g=gap(), k0=q0/(1-q0);
    return (g-s)/(k0*g+s);
  }

void CGaps :: init()
  { ngs=ngo=0;
  }

void CGaps :: add(double p_1,double p)
  { ++ngs;
    if(ArraySize(gs)<ngs) ArrayResize(gs,ngs,ND);
    gs[ngs-1].p0=gs[ngs-1].p2=p;
    gs[ngs-1].p1=p_1;
    double ps=p+(p-p_1)*q0/(1-q0);
    gs[ngs-1].v=pp2v(ps,p);
    int i=0;
    for(; i<ngo; ++i) if(go[i]<0) break;
    if(i==ngo)
      {
        ++ngo;
        if(ArraySize(go)<ngo) ArrayResize(go,ngo,ND);
      }
    go[i]=ngs-1;
  }

bool CGaps :: change(double pbid,double pask)
  { bool ch=false;
    for(int i=0; i<ngo; ++i)
      { if(go[i]<0) continue;
        if(gs[go[i]].up()) gs[go[i]].change(pbid); else gs[go[i]].change(pask);
        if(gs[go[i]].brkn()!=0) {go[i]=-1; ch=true;}
      }
    return ch;
  }

double CGaps :: v(void)
  { double v=0;
    for(int i=0; i<ngo; ++i) if(go[i]>=0) v+=gs[go[i]].v;
    return v;
  }

void CGaps :: gs2f(string fn)
  { int f=FileOpen(fn, FILE_WRITE|FILE_COMMON|FILE_ANSI|FILE_TXT);
    int na=0, np=0, bk;
    double kt=0.0, pk=0.0;
    for(int i=0;i<ngs;++i)
      { bk=gs[i].brkn();
        if(bk==0) continue;
        ++na; if(bk>0) ++np;
        kt+=gs[i].a();
      }
     if(na>0)
       { kt/=na;
         pk=((double)np)/na;
       }
     FileWriteString(f,"na = "+(string)na+"\n");
     FileWriteString(f,"kt = "+(string)kt+"\n");
     FileWriteString(f,"pk = "+(string)pk);
     FileClose(f);
  }

我们在 2017 年数据上测试 EA,并根据其结果定义 2018 年的交易风险值。 基于 2017 年测试结果的余额/净值图表如下。

2017

在进行风险计算之前,我必须做一些澄清。 首先,我们需要证明判断正确风险级别的必要性。 其次,有必要解释将此理论应用于此目的的优势。

投机交易总是与不确定性有关。 任何交易系统都会有时导致亏损交易。 出于此原因,风险不应太大。 否则,回撤将会过度。 另一方面,市场可能随时变化,盈利系统顷刻变成亏损系统。 因此,系统的“生存期”是有限的,且精准未知。 出于此原因,风险不应太小。 否则,您将无法从您的交易系统中赚取所有可能的利润。

现在我们来考察主要方法(与我们的不同)来定义伴随简要特征的风险:

  • 根据一些“有经验的交易者”的意见,通过指定数值来定义风险。 最流行的数值范围是资金的 0.5-3%。 尽管缺乏正当理由,这种方法仍然足够可取。 其主要缺点是没有为特定系统选择指定风险值的规则。
  • 拉尔夫文斯(Ralph Vince)的“优化 f”方法。 这种方法在理论上是合理的,但它通常会提供不充分的高风险值,可能导致非常大的回撤。
  • 将风险包含在 EA 参数中,并在测试和优化期间定义它。 由于这些参数在很大程度上取决于 EA 的设备以及如何进行优化,因此很难对结果的正当性和充分性有个说法。 其中一个可能的障碍是缺乏对进一步交易结果的不确定性的考证。 重新拟合也是可能的,这也许会导致不合理的风险值增加(前一种方法是一个明显的例子)。 我们提议的方法是合理优化风险的一种方式。

与上述方法不同,我们的方法允许我们获得足够和合理的风险值。 它具有可调节的参数,可根据指定的交易风格进行定制。 我们来描述一下风险计算方法的本质。 我们假设运行我们的交易系统,直到其在指定交易数量内的平均盈利能力低于指定的最低水平,或直到回撤超过同一交易顺序中的指定最高水平。 之后,基于已停止系统进行交易(例如,重新优化其参数)。 选择风险值令系统仍然有盈利概率(以及回撤或盈利能力下降是自然随机波动),该值应不超过指定值。

该方法在先前的文章中有详细描述。 在第二篇文章中,您可以找到用于计算优化风险值的脚本。 此脚本适用于所有按固定比率入场的,并按指定止损和止盈价位离场的交易。 在上述文章中,它名为 bn.mq5

作为测试通关的结果,我们的 EA 将数据写入文本文件,这些会作为风险计算脚本所需参数的一部分。 其余参数要么事先已知,要么通过穷举搜索来选择。 如果事实证明脚本提取的风险值为零,那么我们应该放弃这个交易思路,或者削弱我们的回撤/盈利能力需求(通过改变参数),或者使用更大历史区间的交易数据。 下面是脚本的一部分,其中包含要设置的参数值。

input uint na=26;                     // 该系列中的交易数量
input double kt=1.162698185452029     // 止盈/止损比率
input double pk=0.5769230769230769    // 盈利能力

double G0=0.0;                        // 最低平均盈利能力
double D0=0.9;                        // 最小增量
double dlt=0.17;                      // 显著级别

由于 2017 年的测试结果在交易品质方面并不具有启发性,因此我们的需求相当中性。 我们设定的条件是在 26 笔交易(na=26)内,EA 不亏本(G0=0.0),以及回撤不超过 10%(D0=0.9)。 为了获得非零风险值,我们必须将显著级别设置得相当高 (dlt=0.17)。 事实上,如果它不超过十分之一,那就更好了。 我们不得不令其这么大的事实表明交易结果不佳。 具有这些参数的 EA 不应在此品种的实际交易中使用。 使用指定的参数,脚本会提供以下风险结果: r = 0.014。 您可以在下面找到此风险值于 2018 年的 EA 测试结果。

2018

尽管 EA 在测试期间有所盈利,但在真实交易中它们不太可能保持。 实证品种在交易普通缺口时徒劳无益是显而易见的。 这种缺口非常罕见(随时间变得越来越少)并且尺寸很小。 对泛缺口的更深入考察 − 交易时段之间的价格变化 − 似乎更有潜力。 此外,更为普遍地关注资产的普通缺口是有意义的。

结束语

概率方法非常适合开发和配置 EA。 与此同时,它们绝不会与其它可能的方法冲突。 代之,它通常可以作为其它方法的补充,或引发重新思考。

在本文中,我们没有述及常规 EA 参数优化的主题,而是仅在传递时提到它。 这个领域与概率方法(统计方案理论)之间存在重要联系。 也许,我稍后会详细研究这个问题。

附加文件

您可以在下面找到两个用于收集统计数据的 EA,以及一个用于测试交易的 EA。

 #  名称 类型 
 描述
1 gaps_reg_stat.mq5 EA 收集连续逐笔报价之间的缺口统计
2
gaps_ses_stat.mq5 EA 收集时段之间的缺口统计
3 gaps_ses_test.mq5 EA 利用时段之间的缺口测试交易



本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/5373

附加的文件 |
gaps_reg_stat.mq5 (2.47 KB)
gaps_ses_stat.mq5 (3.16 KB)
gaps_ses_test.mq5 (4.93 KB)
运用 MQL5 和 MQL4 开发品种选择和导航实用程序 运用 MQL5 和 MQL4 开发品种选择和导航实用程序

经验丰富的交易者非常清楚交易中最劳神的事情并非开单和跟踪持仓,而是选择交易品种并寻找入场点。 在本文中,我们将开发一款 EA,可为您简化依据经纪商所提供交易产品搜索入场点的任务。

使用 OpenCL 测试烛形形态 使用 OpenCL 测试烛形形态

这篇文章描述了在"一分钟OHLC"模式下实现 OpenCL 烛形形态测试器的算法。我们还将把它的速度与内建的策略测试器在快速和慢速优化模式下做比较。

如何在 MetaTrader 5 中创建并测试自定义 MOEX(莫斯科证券交易所) 品种 如何在 MetaTrader 5 中创建并测试自定义 MOEX(莫斯科证券交易所) 品种

本文介绍运用 MQL5 语言创建自定义兑换品种。 特别是,它研究使用来自流行的 Finam 网站的兑换报价。 本文中研究的另一个选项是在创建自定义品种时可以使用任意格式的文本文件。 这允许使用任何金融品种和数据源。 创建自定义品种之后,我们可以使用 MetaTrader 5 策略测试器的所有功能来测试兑换品种的交易算法。

利用 MQL5 和 MQL4 实现的选择和导航实用程序:添加"homework"选项卡并保存图形对象 利用 MQL5 和 MQL4 实现的选择和导航实用程序:添加"homework"选项卡并保存图形对象

在本文中,我们打算扩展先前创建的实用程序功能,添加用于选择所需品种的选项卡。 我们还将学习如何保存我们在特定品种图表上创建的图形对象,这样我们就不必再次创建它们。 此外,我们将发掘如何仅使用已操控经指定网站初步遴选的品种。