在 GitHub 上查看
早鸟区间突破策略
概述
早鸟区间突破策略是 MetaTrader 4 专家顾问 earlyBird1 的 C# 版本。策略在交易日开盘前统计指定时段的最高价和最低价,使用 14 周期 RSI(基于开盘价)判断做多或做空方向,并在常规交易时段开启后捕捉首次突破。该移植版本完整保留了原始 EA 的每日单向一次交易限制、基于波动率的跟踪止损以及收盘平仓逻辑。
策略逻辑
区间构建
- 时间窗口 – 区间在
Range Start Hour 与 Range End Hour 之间计算(会按照夏令时/冬令时规则调整时差)。所有与该时间段有交集的 K 线都会扩展区间高低点。
- 入场缓冲 – 在区间高点上方与低点下方各叠加一个可配置的点差,复刻 MT4 脚本中
±2/Fakt 的突破缓冲处理。
- 每日重置 – 每个新交易日第一根已完成的 K 线会重置区间、入场触发价以及当日交易计数。
方向过滤
- 基于开盘价的 RSI – RSI 指标以蜡烛开盘价为输入,对应 MT4 中
iRSI(..., PRICE_OPEN) 的实现。
- 方向选择 – 当 RSI 大于 50 时,仅启用多头触发;当 RSI 小于等于 50 时,仅启用空头触发。这样确保每根 K 线只会激活一个方向,与原 EA 保持一致。
入场规则
- 交易时段 – 只有在工作日且处于
Session Start 与 Session End 之间、并且区间已经计算完成后,策略才允许入场。
- 每日单次限制 – 一旦当日已经开过一笔多单或空单,对应方向当天不再重复入场,完全复刻 MT4 中的历史成交计数逻辑。
- 对冲开关 – 启用
Allow Hedging 后,策略会在下单时自动加量以平掉相反持仓并立即翻向;禁用时若仍持仓则跳过入场,直到仓位归零。
出场规则
- 固定止盈止损 – 止盈和止损距离以点数表示。实际止盈距离会被止损距离和区间宽度共同约束,复现 MQL 程序中的
MathMin 判定。
- 波动率跟踪 – 当当前 K 线实体大于 16 根历史 K 线平均波动乘以
Trailing Risk,且浮盈达到 Trailing Trigger 点时,止损上调到当前价减去原始止损距离,止盈同步收紧为触发距离的一半,与原 EA 的 OrderModify 行为一致。
- 日内收盘 – 到达
Closing Hour 后,盈利仓位立即平仓;浮亏仓位将止盈价调整到开仓价,等待价格回到保本位离场,呼应 MT4 的日内收盘处理。
参数
- Auto Trading – 自动交易开关。
- Allow Hedging – 允许在已有仓位的情况下翻向交易。
- Trade Direction – 方向限制:
0 为双向、1 为仅做多、2 为仅做空。
- Volume – 市价单下单量。
- Take Profit (pips) – 止盈距离上限,最终距离受止损与区间宽度限制。
- Stop Loss (pips) – 固定止损距离。
- Trailing Trigger (pips) – 启用跟踪逻辑所需的最低浮盈。
- Trailing Risk – 乘以 16 根历史波动的系数,用于判断是否进入高波动模式。
- Entry Buffer (pips) – 计算突破价时附加的点差缓冲。
- Session Start Hour / Minute – 可交易时段的起始时间(DST 调整前的图表时间)。
- Session End Hour – 可交易时段的结束时间。
- Closing Hour – 日内强制收盘时间。
- Range Start Hour / Range End Hour – 用于构建突破区间的时间范围。
- Summer Time Start / Winter Time Start – 夏令时与冬令时切换所在的年内日数,用于复刻原脚本的
Sommerzeit/Winterzeit 逻辑。
- RSI Length – RSI 周期数(默认 14)。
- Candle Type – 主驱动时间周期(默认 15 分钟 K 线)。
其他说明
- 点值按当前价格大小自动切换(价格 ≥ 10 取 0.01,否则取 0.0001),与 MT4 脚本中的
Fakt 计算完全一致。
- 跟踪所用的平均波动长度为 16 根已完成 K 线,不包含当前 K 线,与原策略一致。
- StockSharp 使用净头寸模型,若开启对冲,策略会通过加减仓的方式模拟双向持仓效果。
- 本仓库仅提供 C# 版本,不包含 Python 实现。
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Early Bird Range Breakout: N-bar high/low breakout with EMA filter and ATR stops.
/// </summary>
public class EarlyBirdRangeBreakoutStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaLength;
private readonly StrategyParam<int> _atrLength;
private decimal _prevHigh;
private decimal _prevLow;
private decimal _entryPrice;
public EarlyBirdRangeBreakoutStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
.SetDisplay("Candle Type", "Timeframe.", "General");
_emaLength = Param(nameof(EmaLength), 20)
.SetDisplay("EMA Length", "Trend filter.", "Indicators");
_atrLength = Param(nameof(AtrLength), 14)
.SetDisplay("ATR Length", "ATR period.", "Indicators");
}
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int EmaLength { get => _emaLength.Value; set => _emaLength.Value = value; }
public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevHigh = 0; _prevLow = 0; _entryPrice = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevHigh = 0; _prevLow = 0; _entryPrice = 0;
var ema = new ExponentialMovingAverage { Length = EmaLength };
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ema, atr, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null) { DrawCandles(area, subscription); DrawIndicator(area, ema); DrawOwnTrades(area); }
}
private void ProcessCandle(ICandleMessage candle, decimal emaVal, decimal atrVal)
{
if (candle.State != CandleStates.Finished) return;
var close = candle.ClosePrice;
if (_prevHigh == 0 || atrVal <= 0) { _prevHigh = candle.HighPrice; _prevLow = candle.LowPrice; return; }
if (Position > 0)
{
if (close <= _entryPrice - atrVal * 1.5m || close >= _entryPrice + atrVal * 2.5m) { SellMarket(); _entryPrice = 0; }
}
else if (Position < 0)
{
if (close >= _entryPrice + atrVal * 1.5m || close <= _entryPrice - atrVal * 2.5m) { BuyMarket(); _entryPrice = 0; }
}
if (Position == 0)
{
if (close > _prevHigh && close > emaVal) { _entryPrice = close; BuyMarket(); }
else if (close < _prevLow && close < emaVal) { _entryPrice = close; SellMarket(); }
}
_prevHigh = candle.HighPrice; _prevLow = candle.LowPrice;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import ExponentialMovingAverage, AverageTrueRange
class early_bird_range_breakout_strategy(Strategy):
def __init__(self):
super(early_bird_range_breakout_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(8))) \
.SetDisplay("Candle Type", "Timeframe.", "General")
self._ema_length = self.Param("EmaLength", 20) \
.SetDisplay("EMA Length", "Trend filter.", "Indicators")
self._atr_length = self.Param("AtrLength", 14) \
.SetDisplay("ATR Length", "ATR period.", "Indicators")
self._prev_high = 0.0
self._prev_low = 0.0
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@property
def EmaLength(self):
return self._ema_length.Value
@property
def AtrLength(self):
return self._atr_length.Value
def OnStarted2(self, time):
super(early_bird_range_breakout_strategy, self).OnStarted2(time)
self._prev_high = 0.0
self._prev_low = 0.0
self._entry_price = 0.0
self._ema = ExponentialMovingAverage()
self._ema.Length = self.EmaLength
self._atr = AverageTrueRange()
self._atr.Length = self.AtrLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._ema, self._atr, self.ProcessCandle).Start()
def ProcessCandle(self, candle, ema_val, atr_val):
if candle.State != CandleStates.Finished:
return
ev = float(ema_val)
av = float(atr_val)
close = float(candle.ClosePrice)
if self._prev_high == 0 or av <= 0:
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
return
if self.Position > 0:
if close <= self._entry_price - av * 1.5 or close >= self._entry_price + av * 2.5:
self.SellMarket()
self._entry_price = 0.0
elif self.Position < 0:
if close >= self._entry_price + av * 1.5 or close <= self._entry_price - av * 2.5:
self.BuyMarket()
self._entry_price = 0.0
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
return
if self.Position == 0:
if close > self._prev_high and close > ev:
self._entry_price = close
self.BuyMarket()
elif close < self._prev_low and close < ev:
self._entry_price = close
self.SellMarket()
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
def OnReseted(self):
super(early_bird_range_breakout_strategy, self).OnReseted()
self._prev_high = 0.0
self._prev_low = 0.0
self._entry_price = 0.0
def CreateClone(self):
return early_bird_range_breakout_strategy()