
简单均值回归交易策略
概述
均值回归是一种逆势交易,交易者预估价格将返回到某种形式的均衡点位,通常依据均值或其它向心趋势统计值来衡量。 本文讨论一个非常简单的均值回归交易策略。
均值回归快速介绍
行情通常以无规律的周期波动。 这意味着,当我们查看图表时,我们往往会看到上升、下降和相对平坦的阶段。 交易和投资的关键是能够判定这些阶段的变化,这些阶段也称为行情制度。
均值回归可以采用移动平均线的形式,如果行情离它太远,它可能会回退到其区域附近。
什么是均值回归?
均值回归是一个金融术语,即假设资产价格随时间推移,趋向聚拢到平均价格。
使用均值回归作为时机策略涉及辨别证券的交易范围,以及利用量化方法计算平均价格。 均值回归是一种现象,在大量的金融时序数据中均有所表现,来自价格数据、收益数据、和账面数值。
当现行价格低于过去的平均价格时,可认为该证对买方具有吸引力,并预估价格会上涨。 当现行价格高于过去的平均价格时,则预估行情价格会下跌。 换言之,预计自平均价格的偏差将退回至平均值。 这些知识可当作多交易策略的基石。
股票报告服务通常提供周期为 50 天和 100 天的移动平均线。 虽然报告服务提供了平均值,但仍然需要辨别所研究期间的最高价和最低价。
与图表相比,均值回归似乎是一种更科学地选择股票买卖点的方法,因为精确的数值衍生自历史数据,可识别买入/卖出价位,而非试图使用图表(图表,也称为技术分析)来解释价格走势,尽管新兴的尝试是利用 RSI 指标和平均真实范围(ATR)来捕捉这种系统形态。
许多资产类型,甚至汇率,都观测到均值回归;不过,这个过程也许会持续数年,因此对短线投资者并无价值。
均值回归应当展现出一种对称形式,因为一只股票高于或低于其历史平均水平的频率大致相仿。
历史均值回归模型不会收容证券价格的全部实际行为。 例如,也许会出现新信息,永久性影响标的股票的长期估值。 在破产的情况下,它也许会完全停止交易,永远无法恢复到曾经的历史平均水平。
在金融领域中,“均值回归”一词与统计学中的“回归或回归均值”的含义略有不同。杰里米·西格尔(Jeremy Siegel)使用“均值回报率”一词来描述一般原则,即一个金融时序,其中“在短期内回报可能非常不稳定,但从长期来看非常稳定”。按量化来讲,平均年回报率的标准差下降速度快于持有期的倒数,这意味着该过程不是随机游走,而是在低回报率区间之后迎来高回报的补偿期,例如季节性商业。
下图勾勒该示例。
但是我们如何衡量“太远”呢? 我们尝试一种非常简单的方式,仅基于价格相对于移动平均线的位置。
设计策略
现在,我们取行情与其 200-周期移动平均线的间距,据其得到一个 50-周期的常规化均值,我们准备遵照以下交易信号编码:
- 每当常规化指数自等于 100 后回落,而当前收盘价低于 5-周期前的收盘价,且低于 200-周期移动平均线时,就会生成做多(买入)信号。
- 每当常规化指数自等于 100 后回落,而当前收盘价高于 5-周期前的收盘价,且高于 200-周期移动平均线时,就会生成做空(卖出)信号。
如此,在给定条件下,或许这并非最简单的策略,但尽管如此,它还是非常直观和直接的。 信号功能如下:
自上一章节,我们已有明确的目标来开始设计策略:
- 每当行情远低于其移动平均线时,就会生成做多(买入)信号,寄希望于它回退到较高的均值。
- 每当行情远高于其移动平均线时,就会生成做空(卖出)信号,寄希望于它回退到较低的均值。
我将对策略进行一些修改,尝试在策略作用不佳时(当净值走低)获得更好的结果。 我将修改这个策略:
- 取代查看最后的收盘价,我会查看最高价和最低价的走高,以便尝试筛选更好的开单。
开单不会太频繁。
修改后的代码如下:
if(previousValue==100) { if(Normalizado<100 && array_ma[0]>tick.bid && rates[5].high < rates[1].low ) { Print("Open Order Buy"); Alert(" Buying"); Orden="Buy"; sl=NormalizeDouble(tick.ask - ptsl*_Point,_Digits); tp=NormalizeDouble(tick.bid + pttp*_Point,_Digits); //trade.Buy(get_lot(tick.bid),_Symbol,tick.bid,sl,tp); trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,get_lot(tick.bid),tick.bid,sl,tp,"Buy"); } if(Normalizado<100 && array_ma[0]<tick.ask && rates[5].low > rates[1].high ) { Print("Open Order Sell"); Alert(" Selling"); Orden="Sell"; sl=NormalizeDouble(tick.bid + ptsl*_Point,_Digits); tp=NormalizeDouble(tick.ask - pttp*_Point,_Digits); //trade.Sell(get_lot_s(tick.ask),_Symbol,tick.ask,sl,tp); trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,get_lot(tick.ask),tick.ask,sl,tp,"Sell"); } }
因之这些修改,策略获胜较少,但看起来更稳定(止损 = 4000 pts)。
EA 必须要优化才能获得更好的结果。 您还可以更改代码。 您可以看到策略在止损为 300 持续 30 分钟的示例。 只需根据所用的品种和周期搜索最拟合的。
该图形看似更稳定,止损为 300 点,但止损为 4000 点时更有利可图。 我们应该针对止损进行优化,以便找到 30 分钟的平衡点。 但是这份工作我会留给你。
代码
这是输入
input ENUM_LOT_TYPE inp_lot_type = LOT_TYPE_FIX; // type of lot input double inp_lot_fix = 0.01; // fix lot input double inp_lot_risk = 0.01; input bool InpPrintLog = false; // Print log ulong Expert_MagicNumber =66777; // bool Expert_EveryTick =false; // input ENUM_TIMEFRAMES my_timeframe=PERIOD_CURRENT; // Timeframe int handle_iMA; input int Inp_MA_ma_period = 200; // MA: averaging period input int Inp_MA_ma_shift = 5; // MA: horizontal shift input ENUM_MA_METHOD Inp_MA_ma_method = MODE_SMA; // MA: smoothing type input ENUM_APPLIED_PRICE Inp_MA_applied_price = PRICE_CLOSE; // MA: type of price int shift = 49; // loockback normalization input int ptsl = 350; // points for stoploss input int pttp = 5000; // points for takeprofit
第一行设置所要用的手数类型,即固定手数。 下一行设置固定手数的大小,后跟风险手数的大小。 下一行设置一个布尔值,指示是否要打印日志。 下一行设置智能系统的魔幻数字。 下一行设置一个布尔值,指示智能系统是否要在每次跳价时执行。 下一行为智能系统设置时间帧 接下来的几行设置移动平均线指标的参数,例如平均周期、水平偏移、平滑类型、和价格类型。 下一行设置回溯常规化的移位。 接下来的两行设置止损和止盈的点数。
在 OnInit() 中:
int OnInit() { //--- handle_iMA=iMA(_Symbol,my_timeframe,Inp_MA_ma_period,Inp_MA_ma_shift, Inp_MA_ma_method,Inp_MA_applied_price); // Initialize the variable here if needed previousValue = 0.0; //--- return(INIT_SUCCEEDED); }
此代码是用 MQL5 语言编写的,用于初始化变量。 代码第一行为 iMA 函数创建一个句柄,其用于计算给定品种在指定时间帧内的移动平均线。 iMA 函数的参数是为了给输入变量 Inp_MA_ma_period、Inp_MA_ma_shift、Inp_MA_ma_method、和 Inp_MA_applied_price 设置初值。 代码第二行初始化变量 previousValue 为 0.0。 代码最后一行返回值 INIT_SUCCEEDED,表示初始化成功。
在 OnTick() 中:
MqlTick tick; double last_price = tick.ask; SymbolInfoTick(_Symbol,tick);
以及
if(SymbolInfoTick(_Symbol,tick)) last=tick.last; double Last = NormalizeDouble(last,_Digits);
此代码是用 MQL5 语言编写的,比较交易品种的当前要价(ask)与最后成交价(last)。 第一行创建一个名为 'tick' 的变量,类型为 MqlTick。 第二行将最后成交价存储在变量 “last_price” 之中。 第三行提取变量 “_Symbol” 中指定的交易品种的跳价信息,并将其存储在变量 “tick” 之中。 第四行检查当前要价是否大于变量 “last_price” 中存储的最后成交价。 如果是,则完成一些事情。
此代码计算给定交易品种的点差百分比。 它首先使用 SymbolInfoTick() 函数提取交易品种的最后成交价。 然后,将最后的成交价常规化到 _Digits 参数指定的小数位。 如果常规化的最后成交价大于 0,则提取并常规化交易品种的要价(ask)和出价(bid)。 点差的计算方法是从常规化要价(ask)中减去常规化出价(bid)。 然后将点差除以交易品种的点值(使用 Pow() 函数计算),就得到以点数为单位的点差。 最后,以点数为单位的点差除以常规化最后成交价,再乘以 100 得到点差百分比。 如果点差百分比小于或等于 Max_Spread 参数,则会采取一些动作。
对于 MA,我们将这样用它:
handle_iMA=iMA(_Symbol,my_timeframe,Inp_MA_ma_period,Inp_MA_ma_shift, Inp_MA_ma_method,Inp_MA_applied_price);
//--- double array_ma[]; ArraySetAsSeries(array_ma,true); int start_pos=0,count=3; if(!iGetArray(handle_iMA,0,start_pos,count,array_ma)) return;
string text=""; for(int i=0; i<count; i++) text=text+IntegerToString(i)+": "+DoubleToString(array_ma[i],Digits()+1)+"\n"; //--- Comment(text);
bool iGetArray(const int handle,const int buffer,const int start_pos, const int count,double &arr_buffer[]) { bool result=true; if(!ArrayIsDynamic(arr_buffer)) { //if(InpPrintLog) PrintFormat("ERROR! EA: %s, FUNCTION: %s, this a no dynamic array!",__FILE__,__FUNCTION__); return(false); } ArrayFree(arr_buffer); //--- reset error code ResetLastError(); //--- fill a part of the iBands array with values from the indicator buffer int copied=CopyBuffer(handle,buffer,start_pos,count,arr_buffer); if(copied!=count) { //--- if the copying fails, tell the error code //if(InpPrintLog) PrintFormat("ERROR! EA: %s, FUNCTION: %s, amount to copy: %d, copied: %d, error code %d", __FILE__,__FUNCTION__,count,copied,GetLastError()); //--- quit with zero result - it means that the indicator is considered as not calculated return(false); } return(result); }此代码计算和显示给定时间帧内给定品种的移动平均线(MA)。 iMA() 函数计算 MA,iGetArray() 函数从指标缓冲区中提取 MA 数值。 ArraySetAsSeries() 函数设置数组作为序列,IntegerToString() 和 DoubleToString() 函数将数组值转换为字符串。 最后,Comment() 函数在图表注释中显示 MA 数值。
为了在开单时不出现交易量错误,我们将使用以下方法:
//<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< double get_lot(double price) { if(inp_lot_type==LOT_TYPE_FIX) return(normalize_lot(inp_lot_fix)); double one_lot_margin; if(!OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,1.0,price,one_lot_margin)) return(inp_lot_fix); return(normalize_lot((AccountInfoDouble(ACCOUNT_BALANCE)*(inp_lot_risk/100))/ one_lot_margin)); } //+------------------------------------------------------------------+ //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< double normalize_lot(double lt) { double lot_step = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP); lt = MathFloor(lt / lot_step) * lot_step; double lot_minimum = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); lt = MathMax(lt, lot_minimum); return(lt); }
此代码计算买入订单的手数。 第一个函数 get_lot() 以价格作为参数,并返回手数。 手数大小由手数类型决定,手数类型可以是固定的,也可以基于风险百分比。 如果手数类型是固定的,则该函数返回常规化的手数大小。 如果手数类型基于风险百分比,则该函数将计算一手的保证金,根据余额和风险百分比计算手数,并返回常规化的手数大小。 第二个函数 normalize_lot() 以手数作为参数,并返回常规化的手数大小。 标准化手数的计算方法是将手数大小除以交易量步长,然后乘以交易量步长。 然后将常规化的手数与最小手数进行比较,并返回两者的最大值。
开单时我们要用到它
trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,get_lot(tick.bid),tick.bid,sl,tp,"Buy");
此代码是用 MQL5 编写的,用于在市场上开新仓。 第一个参数是要交易的资产品种。 第二个参数是订单类型,在本例中为做多订单。 第三个参数是手数,它是调用 get_lot() 函数和当前出价计算得出的。 第四个参数是当前出价(bid)。 第五个和第六个参数分别是止损和止盈水平。 最后一个参数是将会添加到订单中的注释。
根据初始策略,开始将是这样的,只是针对高低点改变收盘条件(以获得更稳健的结果)。
if(0<=Normalizado<=100 ) { //------------------------------------------------------------------------------ if(previousValue==100) { if(Normalizado<100 && array_ma[0]>tick.bid && rates[5].high < rates[1].low ) { Print("Open Order Buy"); Alert(" Buying"); Orden="Buy"; sl=NormalizeDouble(tick.ask - ptsl*_Point,_Digits); tp=NormalizeDouble(tick.bid + pttp*_Point,_Digits); //trade.Buy(get_lot(tick.bid),_Symbol,tick.bid,sl,tp); trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,get_lot(tick.bid),tick.bid,sl,tp,"Buy"); } if(Normalizado<100 && array_ma[0]<tick.ask && rates[5].low > rates[1].high ) { Print("Open Order Sell"); Alert(" Selling"); Orden="Sell"; sl=NormalizeDouble(tick.bid + ptsl*_Point,_Digits); tp=NormalizeDouble(tick.ask - pttp*_Point,_Digits); //trade.Sell(get_lot_s(tick.ask),_Symbol,tick.ask,sl,tp); trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,get_lot(tick.ask),tick.ask,sl,tp,"Sell"); } } } previousValue = Normalizado; if(Orden=="Sell" && rates[0].low <array_ma[0]) { trade.PositionClose(_Symbol,5); Print("cerro sell"); return; } if(Orden=="Buy" && rates[0].high >array_ma[0]) { trade.PositionClose(_Symbol,5); Print("cerro buy"); return; } }
此代码是用 MQL5 语言编写的,用于在外汇市场中开仓和平仓。 该代码首先检查变量 “Normalizado” 的值是否介于 0 和 100 之间。 如果是,则检查 “Normalizado” 的先前值是否为 100。 如果是,则检查 “Normalizado” 的值是否小于 100,数组 array_ma[0] 的值是否大于当前买入价,以及最后 5 个汇率的最高价是否小于第一个汇率的最低价。 如果满足所有这些条件,则代码将打印 “Open Order Buy”,提醒用户它正在买入,将 “Orden” 变量设置为 “Buy”,设置止损和止盈水平,并按指定参数开立买单。
如果 “Normalizado” 的值小于 100,并且 array_ma[0] 的值小于当前要价,并且最后 5 个汇率的最低价大于第一个汇率的最高价,则代码打印 “Open Order Sell”,提醒用户它正在卖出,将 “Orden” 变量设置为 “Sell”, 设置止损和止盈水平,并按指定参数开立卖单。 此后,代码将 “Normalizado” 的先前值设置为当前值。 最后,代码检查 “Orden” 变量是否设置为 “Sell”,以及当前汇率的低点是否小于 array_ma[0] 的值。 如果满足这些条件,则代码将卖单平仓,并打印 “cerro sell”。 类似地,如果 “Orden” 变量设置为 “Buy”,并且当前汇率的最高点大于 array_ma[0] 值,则代码将买单平仓,并打印 “cerro buy”。
结束语
该策略必须经过优化才能应用于市场,但其本意是提出一种思考市场分析的均值回归方式。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/12830





