伦敦区间突破
这个策略具有其独特的魅力,很多交易员很信任这类策略,它明确基于特定的时间段(比如伦敦市场的开盘时间),而非依赖价格行为或其他指标。这是它与其他许多交易策略的核心区别,也让交易员能清晰知道何时需要密切关注行情——你一天只需看两次盘面,一周只需看四次盘面,其余时间无需守在电脑前。
一、策略核心逻辑
原始策略基于英国伦敦外汇市场开盘时的行情交易,核心理论是:伦敦开盘第1小时的价格行为通常反映当天主要趋势。原因有二:
- 交易量集中:伦敦是全球最大外汇交易市场之一,占全球37%的交易量。其开盘时,亚洲市场未完全闭盘,北美市场即将开盘,形成交易活跃度高峰,大量交易员集中参与,是各国银行外汇交易的密集区。
- 重要消息公布:多数重要财经数据和消息在伦敦交易时段发布,对市场影响重大,易引发大幅波动。
基于以上因素,伦敦开盘后1小时的行情常形成全天趋势,交易员通过观察该时段价格行为,以突破区间上下沿作为交易信号,规则简单明了,执行与回测效率高。
二、策略绩效与心路历程
该策略实盘半年多(2024年7月上线),覆盖加密货币、股指、外汇、期货、黄金等品种。初期表现平平,近期才有起色。前几个月曾想放弃,但因未达下架标准而坚持,最终迎来盈利。
这一过程让我意识到:多数交易员难以盈利的关键,在于无法承受策略的“停滞期”。即使是正收益策略,也会因市场无行情而暂时失效,而许多人在等待盈利的过程中选择放弃。正如老话所说:“不经历风雨怎么见彩虹?阳光总在风雨后,欲戴王冠必承其重。”
三、策略操作细节
1. 核心规则
- 时间区间:设定伦敦开盘后的特定时间段(如第1小时),统计该区间的最高价(上沿)和最低价(下沿),形成“箱体”。
- 突破信号:区间结束后,若价格突破上沿则做多,突破下沿则做空。
- 止损与止盈:
- 止损:设为箱体上下沿点数的倍数(如1倍,多单止损设为箱体下沿,空单止损设为箱体上沿)。
- 止盈:可设(如4倍箱体距离)或不设,我选择较远的止盈位。
- 平仓时间:设定最终平仓时间,无论盈亏,到达时间必须平仓。
2. 过滤规则(可回测调整)
- 周期滤网:仅在周一、周三交易,周二、周四、周五忽略信号(我实测有效,建议自行回测)。
- 信号冲突:若突破后反向再突破(如空单入场后价格反弹至上沿),是否二次开仓需根据品种特性回测决定。
四、策略图示
五、代码实现
cpp
#property strict
#include <Indicators/Trend.mqh>
#include <Util/Util.mqh>
Util util;
enum BREAKOUT_MODE_ENUM {
ONE_SIGNAL, // 单边突破
TWO_SIGNALS, // 双边突破
};
// 不可以做参数优化的参数
input group "固定和输入通用部分";
sinput int MagicNum = 12345; // 魔术编号
sinput int SlType = 1; // 止损方式(1:固定金额{100}美金 2:账户{1}% 3:固定{0.1}手 4:子账户总账户{1}%)
sinput int SlParam = 100; // 止损依据(根据上方【止损方式】的不同,所代表的含义也不同)
input group "参数优化";
input ENUM_TIMEFRAMES TimeFrame = PERIOD_H1; // 时间周期
input int StopLoss =100; // 止损(区域% 0=无) 传入100 则表示止损 区域高度的 100%
input int TakeProfit = 400; // 止盈(区域% 0=无) 传入100 则表示止盈 区域高度的 100%
input int RangeStart = 540; // 区域开始时间点(分钟) 传入540 表示 9点开始
input int RangeDuration = 1260; // 区域持续时间段(分钟) 传入60 表示持续到10点结束
input int RangeClose = 1020; // 区域平仓时间点(分钟,-1=无) 传入1020 表示17点平仓
input BREAKOUT_MODE_ENUM BreakoutMode = TWO_SIGNALS; // 突破模式(1:单边突破 2:双边突破)
input group "星期滤网";
input bool Monday = true; // 星期一是否做单
input bool Tuesday = false; // 星期二是否做单
input bool Wednesday = true; // 星期三是否做单
input bool Thursday = false; // 星期四是否做单
input bool Friday = false; // 星期五是否做单
int barsTotal;
int initBalance;
/*
结构体也是一种特殊的数据类型它是把几个不同类型变量封装为1个自定义得数据类型。它其实很像我们面向对象编程中的对象的概念。
在编程中,当你想在一个变量中保存不同的相关信息时,这种结构就会派上用场。这可以让存储更多的信息,并且可以将这些信息作为一个整体来操作。
用法:
struct 结构体名称{
数据类型1 名称1;
数据类型2 名称2;
...
数据类型n 名称n;
结构体的定义使用关键字struct开始,并以分号;结束。
在大括号{}里面,你可以按照数据类型 名称的格式声明结构体的成员
结构体的成员可以为任何有效的数据类型
然后你可以清楚明确地通过点运算符,来访问这些成员:
RANGE_STRUCT range;
ange.start time =D'2024.01.17 64:29:24';
ange.high =1.3600;
ange.low1.3500;
if(range.high == 1.5)
构造函数在创建一个对象时被自动调用,它通常用于设置成员变量的初始值
如果我们想让对象在创建时具有某些默认的状态,我们就可以在构造函数中显式地设置它
例如,在RANGE STRUCT中的构造函数就设定了所有成员的默认值
*/
/*
构造函数在创建一个对象时被自动调用,它通常用于设置成员变量的初始值
如果我们想让对象在创建时具有某些默认的状态,我们就可以在构造函数中显式地设置它
例如,在RANGE_STRUCT中的构造函数就设定了所有成员的默认值
这章味着,每当我们创建一个新的RANGE STRUCT对象时
这些成员就会自动被设定为指定的值,我们无需手动设置
保证了数据的一致性,改普了代码的可读性。
*/
struct RANGE_STRUCT {
datetime start_time;
datetime end_time;
datetime close_time;
double high;
double low;
bool f_entry;
bool f_high_breakout;
bool f_low_breakout;
RANGE_STRUCT() : start_time(0), end_time(0), close_time(0), high(0), low(DBL_MAX), f_entry(false), f_high_breakout(false), f_low_breakout(false) {};
};
RANGE_STRUCT range;
MqlTick currentTick;
int objKey;
double point;
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
int OnInit() {
// 当用户没有打开参数对应的图表周期,自动帮他打开对应周期
if (MQLInfoInteger(MQL_TESTER)) {
ChartSetInteger(0, CHART_SHOW_GRID, false);
} else {
ChartSetSymbolPeriod(0, _Symbol, TimeFrame);
}
int cntBuy, cntSell;
util.countOpenPositions(MagicNum, cntBuy, cntSell);
// 如果图表参数发生了变更,并且没有任何买单和卖单,那么就重新计算时间区域
if (_UninitReason == REASON_PARAMETERS && cntBuy == 0 && cntSell == 0)
CalculateRange();
objKey = rand();
initBalance = SlParam;
Comment("MagicNum:", MagicNum, " SlType: ", SlType, " SlParam: ", SlParam);
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CheckInputs() {
// 没有止损又没有设置平仓时间
if (RangeClose < 0 && StopLoss == 0) {
Alert("RangeClose<0 && StopLoss ==0 ");
return false;
}
// 统计一天的最高最低价
if (RangeStart < 0 || RangeStart >= 1440) {
Alert("RangeStart<0 || RangeStart>=1440");
return false;
}
if (RangeDuration <= 0 || RangeDuration >= 1440) {
Alert("RangeDuration<=0 || RangeDuration>=1440");
return false;
}
if (RangeClose >= 1440) {
Alert("RangeClose>=1440 or end time == close time");
return false;
}
if (Monday + Tuesday + Wednesday + Thursday + Friday == 0) {
Alert("Monday+Tuesday+Wednesday+Thursday+Friday ==0");
return false;
}
return true;
}
// 计算时间区域
void CalculateRange() {
// 重置数据
range.start_time = 0;
range.end_time = 0;
range.close_time = 0;
range.high = 0.0;
range.low = DBL_MAX;
range.f_entry = false;
range.f_high_breakout = false;
range.f_low_breakout = false;
// 计算区域开始时间, 星期六和星期天区域后移
/*
一天的秒数(24小时*60分钟/小时**60秒/分钟=86400秒)
(currentTick.time%time_cyle)
求的是当前时间自凌晨以来过去的秒数,用当前时间减去这个结果可以得到当天(80:00:80)的时间
tmp.day_of_week获取到这个时间是一周的第几天(1-5表示周一至周五,6表示周六,@表示周日)
当前时间己经过了时间,区域的开始时间|周六周天市场闭市的情况|| 使用的星期过滤器过滤掉了一些不工作的星期
如果以上任意一种情况为真,那么它就将range.start_time向后推迟一天(也就是加上一天的秒数,即time_cycle)
*/
int time_cycle = 86400; // 一天的秒数
range.start_time = currentTick.time - (currentTick.time % time_cycle) + RangeStart * 60;
for (int i = 0; i < 8; i++) {
MqlDateTime tmp;
TimeToStruct(range.start_time, tmp);
int dow = tmp.day_of_week;
if (currentTick.time > range.start_time || dow == 6 || dow == 0 || (dow == 1 && !Monday) || (dow == 2 && !Tuesday) || (dow == 3 && !Wednesday) || (dow == 4 && !Thursday) || (dow == 5 && !Friday)) {
range.start_time += time_cycle;
}
}
// 计算区域结束时间 星期六 和星期天区域后移
range.end_time = range.start_time + RangeDuration * 60;
for (int i = 0; i < 2; i++) {
MqlDateTime tmp;
TimeToStruct(range.end_time, tmp);
int dow = tmp.day_of_week;
if (dow == 6 || dow == 0) {
range.end_time += time_cycle;
}
}
// 计算平仓时间, 星期六和星期天区域后移
if (RangeClose >= 0) {
range.close_time = range.start_time + RangeClose * 60;
for (int i = 0; i < 3; i++) {
MqlDateTime tmp;
TimeToStruct(range.close_time, tmp);
int dow = tmp.day_of_week;
if (range.close_time <= range.end_time || dow == 6 || dow == 0) {
range.close_time += time_cycle;
}
}
}
DrawObjeckts();
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void DrawObjeckts() {
// 开始 结束时间末
/*
图表ID。这里的0表示当前激活的图表,也就是正在查看的图表。
objname:创建的图形对象名称,这个是唯一的,如果有其他对象使用了相同的名称,将会出错。
OBJRECTANGLE:这是图形对象的类型,这里OBJRECTANGLE表示的是矩形。
0:此处的0表示主窗口。在MT5中,图表窗口可以有多个,比如主图窗口,MACD窗口,相对强度指标窗口等等,这里的0表示主图窗口。
range.start_time:矩形对象的开始时间,也就是矩形对象在X轴的开始位置,通常这个是一个时间值。
range.high:表示矩形对象在价格轴(Y轴)上的顶部位置。
range.end_time:矩形对象的结束时间,也就是矩形对象在X轴的结束位置,和开始时间配合确定了矩形的宽度。
range.low:表示矩形对象在价格轴(Y轴)上的底部位置。配合range.high确定了矩形的高度。
*/
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
string objname;
objname = "range time zone " + (string)objKey;
if (range.start_time > 0 && range.end_time > 0) {
// 如果已存在同名的绘图对象,先将其删除
if (ObjectFind(0, objname) != INVALID_HANDLE) {
ObjectDelete(0, objname);
}
// 创建矩形对象
ObjectCreate(0, objname, OBJ_RECTANGLE, 0,
range.start_time, range.high,
range.end_time, range.low);
// 设置相关属性:矩形在背景层,颜色为深海绿,填充模式,无边框
ObjectSetInteger(0, objname, OBJPROP_BACK, true);
ObjectSetInteger(0, objname, OBJPROP_COLOR, clrDarkSeaGreen);
ObjectSetInteger(0, objname, OBJPROP_FILL, true);
ObjectSetInteger(0, objname, OBJPROP_BORDER_TYPE, DRAW_NONE);
}
// 高点
/* NULL:这是图表ID。'NULL'表示要在当前活动图表上创建对象。如果你想在特定图表上创建对象,可以在这里填写特定图表的ID。
objname:这是对象名称。在一个图表上,每个对象都要有唯一的名称,以便于区分和管理。
OBJTREND:对象类型。这里的OBJ_TREND表示要创建的是一个趋势线对象。
0:窗口索引。'0'表示在主图窗口上创建这个趋势线对象。
range.end_time 和 range.high 是趋势线的一端(通常是起点)的时间和价格。range可能是某个数据结构,记录了趋势线的信息。
RangeClose>=0?range.close_time:INT_MAX和range.high 是趋势线的另一端(通常是终点〉的时间和价格。
特别的,如果RangeClese不小于0,那么终点的时间就是range.close_time。
否则,终点的时间将会是INT_MAX,这是一个非常大的值,可能意味着趋势线一直绘制到未来的某个时间。
*/
objname = "range high" + (string)objKey;
if (range.high > 0) {
// 在具有相同高度的范围结束时间到平仓时间之间绘制一条延伸线
objname = objname + "extend";
// 创建趋势线对象(假设需要两个点:开始时间+高点,结束时间+高点)
ObjectCreate(0, objname, OBJ_TREND, 0, range.end_time, range.high, RangeClose >= 0 ? range.close_time : INT_MAX, range.high);
// 设置趋势线属性(示例:红色,实线,线宽2)
ObjectSetString(0, objname, OBJPROP_TOOLTIP, "high of the range \n" + DoubleToString(range.high, digits));
ObjectSetInteger(0, objname, OBJPROP_COLOR, clrDarkSeaGreen);
ObjectSetInteger(0, objname, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, objname, OBJPROP_BACK, true);
// 设置线的样式为点状
ObjectSetInteger(0, objname, OBJPROP_STYLE, STYLE_DOT);
}
objname = "range low" + (string)objKey;
// 如果交易区间的地点小于DBL_MAX(一个非常大的数,表示没有设置低点)
if (range.low < DBL_MAX) {
objname = objname + "extend";
ObjectCreate(0, objname, OBJ_TREND, 0, range.end_time, range.low, RangeClose >= 0 ? range.close_time : INT_MAX, range.low);
// 设置趋势线属性(示例:蓝色,实线,线宽2)
ObjectSetString(0, objname, OBJPROP_TOOLTIP, "low of the range \n" + DoubleToString(range.low, digits));
ObjectSetInteger(0, objname, OBJPROP_COLOR, clrDarkSeaGreen);
ObjectSetInteger(0, objname, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, objname, OBJPROP_BACK, true);
// 设置线的样式为点状
ObjectSetInteger(0, objname, OBJPROP_STYLE, STYLE_DOT);
}
// 平仓时间
/*
NULL:这是图表的ID。为NULL意味着在当前收到报价的图表上创建对象。
如果在特定的图表上创建对象,可以接受图表的特定ID
obiname:这是创建的对象的名称,每个对象必须有一个独一无二的名称,以便于管理和操作。
0BJ_VLINE:这是对象的类型,在这里 0BJVLINE 表示创建的是一个重直线对象。
0:这是窗口的索引,0表示主窗口。
range.close_time:垂直线的横坐标位置,也就是在时间轴上的位置,这里是在平仓的时间位置画这个垂直线。
0:这个参数在 0BVLINE 类型的对象中无意义,因为乖直线在乖直轴(价格轴)上并无起始和结束
*/
objname = "range close" + (string)objKey;
if (range.close_time > 0) {
// 创建垂直线对象
ObjectCreate(0, objname, OBJ_VLINE, 0, range.close_time, 0);
// 设置垂直线属性(示例:红色,实线,线宽2)
ObjectSetString(0, objname, OBJPROP_TOOLTIP, "close of the range \n" + TimeToString(range.close_time, TIME_DATE | TIME_MINUTES));
ObjectSetInteger(0, objname, OBJPROP_COLOR, clrLightPink);
ObjectSetInteger(0, objname, OBJPROP_WIDTH, 2);
ObjectSetInteger(0, objname, OBJPROP_BACK, true);
}
// 刷新图表 , 使新增的绘图对象立即显示出来
ChartRedraw();
}
// 节省内存后 买VPS就可以买小一点
void OnDeinit(const int reason) {
}
void OnTick() {
if (!util.isNewBar(_Symbol, PERIOD_CURRENT, barsTotal))
return;
point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
SymbolInfoTick(_Symbol, currentTick);
if (currentTick.time >= range.start_time && currentTick.time < range.end_time) {
range.f_entry = true;
double high = iHigh(_Symbol, PERIOD_CURRENT, 1);
double low = iLow(_Symbol, PERIOD_CURRENT, 1);
if (high > range.high) {
range.high = high;
DrawObjeckts();
}
if (low < range.low) {
range.low = low;
DrawObjeckts();
}
}
// 出场
// 检查是否达到指定平仓时间
if (RangeClose >= 0 && currentTick.time >= range.close_time)
closeAllPositions(MagicNum);
int cntBuy, cntSell;
util.countOpenPositions(MagicNum, cntBuy, cntSell);
if (((RangeClose >= 0 && currentTick.time >= range.close_time) // 过了平仓时间
|| (range.f_high_breakout && range.f_low_breakout) // 上下沿都被突破
|| (range.end_time == 0) // 区域没有计算
|| (range.end_time != 0 && currentTick.time >= range.end_time && !range.f_entry)) // 区域已经计算但是里面没有价格行为
&& cntBuy == 0 && cntSell == 0) {
objKey = rand();
CalculateRange();
Comment("开始时间: " + (string)range.start_time + "\r\n" +
"结束时间: " + (string)range.end_time + "\r\n" +
"平仓时间: " + (string)range.close_time + "\r\n");
}
CheckBreakouts();
}
// 突破下单
void CheckBreakouts() {
// 联取当前货币对的小数位数
int digits = (int)SymbolInfoInteger(Symbol(), SYMBOL_DIGITS);
// 判断当前时间是否超过区间结束时间,且f_entry为true时存在价格行为
if(currentTick.time >= range.end_time && range.end_time > 0 && range.f_entry) {
// 市价第一次突破区间上沿
if(!range.f_high_breakout && currentTick.ask >= range.high) {
// 确保订单唯一,不同时存在多个买单
int cntBuy, cntSell;
util.countOpenPositions(MagicNum, cntBuy, cntSell);
if(cntBuy != 0) {
range.f_high_breakout = true;
return;
}
// 记录已经突破上沿标记
range.f_high_breakout = true;
// ONE_SIGNAL模式下 如果突破上沿 就认为已经突破下沿
if(BreakoutMode == ONE_SIGNAL) range.f_low_breakout = true;
// 计算止损止盈
double sl = StopLoss == 0 ? 0 : NormalizeDouble(currentTick.bid - ((range.high - range.low) * StopLoss * 0.01), digits);
double tp = TakeProfit == 0 ? 0 : NormalizeDouble(currentTick.bid + ((range.high - range.low) * TakeProfit * 0.01), digits);
// 资金管理计算手数
double slp = SlParam;
if(SlType == 4) slp = util.calculateEachEaBalance(MagicNum, initBalance);
double lotSize = util.calcLots(_Symbol, currentTick.ask, sl, SlType, slp);
// 开买单
trade.SetExpertMagicNumber(MagicNum);
trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, lotSize, currentTick.ask, sl, tp, "LB-" + (string)range.close_time);
}
// 市价突破区间下沿
if(!range.f_low_breakout && currentTick.bid <= range.low) {
int cntBuy, cntSell;
util.countOpenPositions(MagicNum, cntBuy, cntSell);
if(cntSell != 0) {
range.f_low_breakout = true;
return;
}
// 记录已经突破下沿标记
range.f_low_breakout = true;
// 计算止损止盈
double sl = StopLoss == 0 ? 0 : NormalizeDouble(currentTick.ask + ((range.high - range.low) * StopLoss * 0.01), digits);
double tp = TakeProfit == 0 ? 0 : NormalizeDouble(currentTick.ask - ((range.high - range.low) * TakeProfit * 0.01), digits);
// 资金管理计算手数
double slp = SlParam;
if(SlType == 4) slp = util.calculateEachEaBalance(MagicNum, initBalance);
double lotSize = util.calcLots(_Symbol, currentTick.bid, sl, SlType, slp);
// 开卖单
trade.SetExpertMagicNumber(MagicNum);
trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, lotSize, currentTick.bid, sl, tp, "LB-" + (string)range.close_time);
}
}
}
//+------------------------------------------------------------------+
//| 关闭所有仓位 |
//+------------------------------------------------------------------+
void closeAllPositions(int magicNum)
{
int total = PositionsTotal();
for (int i = total - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if (ticket < 0)
{
Print("获取ticket失败");
return;
}
if (!PositionSelectByTicket(ticket))
{
Print("选中ticket失败");
return;
}
long magic;
if (!!PositionGetInteger(POSITION_MAGIC, magic))
{
Print("获取魔术号失败");
return;
}
if (magic == magicNum)
{
// 获取订单备注信息
string comment;
PositionGetString(POSITION_COMMENT, comment);
// 解析备注中的平仓时间 LB-2024.01.01 21:00:00
datetime oldCloseTime = StringToTime(getStr(comment, "-"));
// 如果当前时间未达到平仓时间, 则返回
if (!(currentTick.time >= oldCloseTime))
return;
// 如果达到平仓时间, 打印信息并关闭持仓
Print("平仓时间到达, 开始平仓:", MagicNum, " - ", currentTick.time, " >= ", oldCloseTime);
trade.PositionClose(ticket);
if (trade.ResultRetcode() != TRADE_RETCODE_DONE)
{
Print("平仓失败", ticket, " 异常:", (string)trade.ResultRetcode(), " :", trade.ResultRetcodeDescription());
}
}
}
}
// 通过分割符来分割字符串,并返回分割后的第二个元素
string getStr(string str, string step)
{
// 定义了一个字符串数组result 来保存分割后的结果
string result[];
// 调用StringSplit函数进行字符串的分割
// StringGetCharacter(step,0) 获取的是分割符的第一个字符(通常分割符只有一个字符)
// 然后用这个分割符对字符串str进行分割, 分割后的结果存入result数组,同时返回分割除了几个字符串
int k = StringSplit(str,StringGetCharacter(step,0),result);
// 判断是否至少分割出了一个字符串
if(k>0){
// 返回step"-"后面的内容
return result[1];
}
// 如果没有进入上面的if分支, 那么表示输入的字符串str中没有找到分割符,也就是没有进行分割操作,此时就返回NULL
return NULL;
}