Breakout Strategy
概述
Breakout Strategy 是将 MetaTrader 5 专家顾问 BreakoutStrategy.mq5 移植到 StockSharp 的结果。策略跟踪一段时间内的最高价和最低价,当价格突破通道边界时入场,并通过第二组唐奇安通道对持仓进行跟踪止损,完全对应原始 EA 的逻辑。
交易逻辑
- 入场通道:
EntryPeriod内的最高价和最低价通过EntryShift进行延迟,以避免在计算突破时使用当前 K 线。 - 突破检测:如果当前 K 线最高价突破上轨并超过一个最小报价单位,则触发做多;若最低价跌破下轨并减去一个最小报价单位,则触发做空。
- 出场通道:
ExitPeriod内的最高价和最低价同样经过ExitShift延迟。启用UseMiddleLine时,策略会在外轨和中轨之间选择(多头取较大值,空头取较小值)作为跟踪止损水平。 - 仓位管理:当多头仓位的最低价跌破跟踪水平时离场;空头仓位的最高价触及跟踪水平时平仓。出现反向信号时,策略会先平掉现有仓位再按新方向建仓。
- 风险控制:仓位规模取决于
RiskPerTrade。策略读取账户权益,结合合约的PriceStep与StepPrice将止损距离转换为金额,并根据VolumeStep、VolumeMin、VolumeMax调整下单手数,使潜在亏损接近设定的资金百分比。
参数
| 名称 | 说明 |
|---|---|
CandleType |
使用的 K 线类型,默认是 1 小时。 |
EntryPeriod |
入场通道的回溯长度。 |
EntryShift |
入场通道的延迟棒数,1 对应原始 EA 设置。 |
ExitPeriod |
跟踪通道的回溯长度。 |
ExitShift |
跟踪通道的延迟棒数。 |
UseMiddleLine |
是否在计算跟踪止损时使用唐奇安中轨。 |
RiskPerTrade |
单笔交易允许的资金风险比例。 |
说明
- C# 源码中的注释全部使用英文,满足仓库要求。
- 策略依赖 StockSharp 的高级 API:K 线订阅、
Highest/Lowest指标以及Shift指标实现数据延迟,不再手动管理数组。 - 项目未附带自动化测试,请在真实交易前先在本地环境验证策略表现。
namespace StockSharp.Samples.Strategies;
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>
/// Donchian breakout strategy converted from the MQL5 BreakoutStrategy expert.
/// </summary>
public class BreakoutStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _entryPeriod;
private readonly StrategyParam<int> _entryShift;
private readonly StrategyParam<int> _exitPeriod;
private readonly StrategyParam<int> _exitShift;
private readonly StrategyParam<bool> _useMiddleLine;
private readonly StrategyParam<decimal> _riskPerTrade;
private readonly StrategyParam<int> _signalCooldownBars;
private Highest _entryHighest;
private Lowest _entryLowest;
private Highest _exitHighest;
private Lowest _exitLowest;
private Shift _entryHighShift;
private Shift _entryLowShift;
private Shift _exitHighShift;
private Shift _exitLowShift;
private int _cooldownRemaining;
public BreakoutStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe used for calculations", "General");
_entryPeriod = Param(nameof(EntryPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Entry Period", "Lookback bars for breakout detection", "Entry")
.SetOptimize(10, 40, 5);
_entryShift = Param(nameof(EntryShift), 1)
.SetNotNegative()
.SetDisplay("Entry Shift", "Bars to delay the Donchian breakout levels", "Entry")
.SetOptimize(0, 3, 1);
_exitPeriod = Param(nameof(ExitPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Exit Period", "Lookback bars for trailing exits", "Exit")
.SetOptimize(10, 40, 5);
_exitShift = Param(nameof(ExitShift), 1)
.SetNotNegative()
.SetDisplay("Exit Shift", "Bars to delay the trailing channel", "Exit")
.SetOptimize(0, 3, 1);
_useMiddleLine = Param(nameof(UseMiddleLine), true)
.SetDisplay("Use Middle Line", "Use the Donchian midline as an exit filter", "Exit");
_riskPerTrade = Param(nameof(RiskPerTrade), 0.01m)
.SetGreaterThanZero()
.SetDisplay("Risk Per Trade", "Fraction of equity risked per trade", "Risk")
.SetOptimize(0.005m, 0.03m, 0.005m);
_signalCooldownBars = Param(nameof(SignalCooldownBars), 4)
.SetNotNegative()
.SetDisplay("Signal Cooldown Bars", "Closed candles to wait before a new breakout entry", "Risk");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int EntryPeriod
{
get => _entryPeriod.Value;
set => _entryPeriod.Value = value;
}
public int EntryShift
{
get => _entryShift.Value;
set => _entryShift.Value = value;
}
public int ExitPeriod
{
get => _exitPeriod.Value;
set => _exitPeriod.Value = value;
}
public int ExitShift
{
get => _exitShift.Value;
set => _exitShift.Value = value;
}
public bool UseMiddleLine
{
get => _useMiddleLine.Value;
set => _useMiddleLine.Value = value;
}
public decimal RiskPerTrade
{
get => _riskPerTrade.Value;
set => _riskPerTrade.Value = value;
}
public int SignalCooldownBars
{
get => _signalCooldownBars.Value;
set => _signalCooldownBars.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryHighest = null;
_entryLowest = null;
_exitHighest = null;
_exitLowest = null;
_entryHighShift = null;
_entryLowShift = null;
_exitHighShift = null;
_exitLowShift = null;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_cooldownRemaining = 0;
// Create Donchian channel components for entries and exits.
_entryHighest = new() { Length = EntryPeriod };
_entryLowest = new() { Length = EntryPeriod };
_exitHighest = new() { Length = ExitPeriod };
_exitLowest = new() { Length = ExitPeriod };
// Create shift indicators only when an offset is required.
_entryHighShift = EntryShift > 0 ? new Shift { Length = EntryShift } : null;
_entryLowShift = EntryShift > 0 ? new Shift { Length = EntryShift } : null;
_exitHighShift = ExitShift > 0 ? new Shift { Length = ExitShift } : null;
_exitLowShift = ExitShift > 0 ? new Shift { Length = ExitShift } : null;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _entryHighest);
DrawIndicator(area, _entryLowest);
DrawIndicator(area, _exitHighest);
DrawIndicator(area, _exitLowest);
DrawOwnTrades(area);
}
StartProtection(null, null);
}
private void ProcessCandle(
ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var time = candle.OpenTime;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var entryHighValue = _entryHighest.Process(new CandleIndicatorValue(_entryHighest, candle));
var entryLowValue = _entryLowest.Process(new CandleIndicatorValue(_entryLowest, candle));
var exitHighValue = _exitHighest.Process(new CandleIndicatorValue(_exitHighest, candle));
var exitLowValue = _exitLowest.Process(new CandleIndicatorValue(_exitLowest, candle));
if (!_entryHighest.IsFormed || !_entryLowest.IsFormed || !_exitHighest.IsFormed || !_exitLowest.IsFormed)
return;
// Obtain Donchian bands and apply the configured shift.
var entryUpper = entryHighValue.ToDecimal();
var entryLower = entryLowValue.ToDecimal();
if (_entryHighShift != null)
{
var shiftedValue = _entryHighShift.Process(new DecimalIndicatorValue(_entryHighShift, entryUpper, time) { IsFinal = true });
if (!_entryHighShift.IsFormed || shiftedValue.IsEmpty)
return;
entryUpper = shiftedValue.ToDecimal();
}
if (_entryLowShift != null)
{
var shiftedValue = _entryLowShift.Process(new DecimalIndicatorValue(_entryLowShift, entryLower, time) { IsFinal = true });
if (!_entryLowShift.IsFormed || shiftedValue.IsEmpty)
return;
entryLower = shiftedValue.ToDecimal();
}
var exitUpper = exitHighValue.ToDecimal();
var exitLower = exitLowValue.ToDecimal();
if (_exitHighShift != null)
{
var shiftedValue = _exitHighShift.Process(new DecimalIndicatorValue(_exitHighShift, exitUpper, time) { IsFinal = true });
if (!_exitHighShift.IsFormed || shiftedValue.IsEmpty)
return;
exitUpper = shiftedValue.ToDecimal();
}
if (_exitLowShift != null)
{
var shiftedValue = _exitLowShift.Process(new DecimalIndicatorValue(_exitLowShift, exitLower, time) { IsFinal = true });
if (!_exitLowShift.IsFormed || shiftedValue.IsEmpty)
return;
exitLower = shiftedValue.ToDecimal();
}
if (!IsFormedAndOnlineAndAllowTrading())
return;
var exitMiddle = (exitUpper + exitLower) / 2m;
var exitLong = UseMiddleLine ? Math.Max(exitMiddle, exitLower) : exitLower;
var exitShort = UseMiddleLine ? Math.Min(exitMiddle, exitUpper) : exitUpper;
var step = GetPriceStep();
if (step <= 0m)
step = 1m;
var triggerLong = entryUpper;
var triggerShort = entryLower;
// Manage trailing exits before evaluating new entries.
if (Position > 0m && candle.LowPrice <= exitLong)
{
SellMarket(Position);
_cooldownRemaining = SignalCooldownBars;
}
else if (Position < 0m && candle.HighPrice >= exitShort)
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = SignalCooldownBars;
}
// Enter long positions on breakouts above the shifted channel.
if (_cooldownRemaining == 0 && Position <= 0m && candle.HighPrice >= triggerLong)
{
var stopDistance = triggerLong - exitLong;
if (stopDistance > 0m)
{
var volume = CalculateVolume(stopDistance);
if (volume > 0m)
{
BuyMarket(volume + (Position < 0m ? Math.Abs(Position) : 0m));
_cooldownRemaining = SignalCooldownBars;
}
}
}
// Enter short positions on breakouts below the shifted channel.
else if (_cooldownRemaining == 0 && Position >= 0m && candle.LowPrice <= triggerShort)
{
var stopDistance = exitShort - triggerShort;
if (stopDistance > 0m)
{
var volume = CalculateVolume(stopDistance);
if (volume > 0m)
{
SellMarket(volume + (Position > 0m ? Position : 0m));
_cooldownRemaining = SignalCooldownBars;
}
}
}
}
private decimal CalculateVolume(decimal stopDistance)
{
return Volume > 0m ? Volume : 1m;
}
private decimal GetPriceStep()
{
return Security?.PriceStep ?? 0m;
}
private decimal AlignVolume(decimal volume)
{
if (volume <= 0m)
return 0m;
var security = Security;
if (security == null)
return volume;
// Align the requested volume to exchange constraints.
var step = security.VolumeStep ?? 0m;
if (step > 0m)
{
var steps = decimal.Floor(volume / step);
volume = steps * step;
if (volume <= 0m)
volume = step;
}
var min = security.MinVolume ?? 0m;
if (min > 0m && volume < min)
volume = min;
var max = security.MaxVolume ?? 0m;
if (max > 0m && volume > max)
volume = max;
return volume;
}
}
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
class breakout_strategy(Strategy):
def __init__(self):
super(breakout_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2)))
self._entry_period = self.Param("EntryPeriod", 20)
self._exit_period = self.Param("ExitPeriod", 20)
self._use_middle_line = self.Param("UseMiddleLine", True)
self._signal_cooldown_bars = self.Param("SignalCooldownBars", 4)
self._entry_highs = []
self._entry_lows = []
self._exit_highs = []
self._exit_lows = []
self._cooldown_remaining = 0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def EntryPeriod(self):
return self._entry_period.Value
@EntryPeriod.setter
def EntryPeriod(self, value):
self._entry_period.Value = value
@property
def ExitPeriod(self):
return self._exit_period.Value
@ExitPeriod.setter
def ExitPeriod(self, value):
self._exit_period.Value = value
@property
def UseMiddleLine(self):
return self._use_middle_line.Value
@UseMiddleLine.setter
def UseMiddleLine(self, value):
self._use_middle_line.Value = value
@property
def SignalCooldownBars(self):
return self._signal_cooldown_bars.Value
@SignalCooldownBars.setter
def SignalCooldownBars(self, value):
self._signal_cooldown_bars.Value = value
def OnReseted(self):
super(breakout_strategy, self).OnReseted()
self._entry_highs = []
self._entry_lows = []
self._exit_highs = []
self._exit_lows = []
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(breakout_strategy, self).OnStarted2(time)
self._entry_highs = []
self._entry_lows = []
self._exit_highs = []
self._exit_lows = []
self._cooldown_remaining = 0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
entry_period = self.EntryPeriod
exit_period = self.ExitPeriod
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
# Update entry channel
self._entry_highs.append(high)
self._entry_lows.append(low)
while len(self._entry_highs) > entry_period:
self._entry_highs.pop(0)
self._entry_lows.pop(0)
# Update exit channel
self._exit_highs.append(high)
self._exit_lows.append(low)
while len(self._exit_highs) > exit_period:
self._exit_highs.pop(0)
self._exit_lows.pop(0)
if len(self._entry_highs) < entry_period or len(self._exit_highs) < exit_period:
return
entry_upper = max(self._entry_highs)
entry_lower = min(self._entry_lows)
exit_upper = max(self._exit_highs)
exit_lower = min(self._exit_lows)
exit_middle = (exit_upper + exit_lower) / 2.0
use_mid = self.UseMiddleLine
exit_long = max(exit_middle, exit_lower) if use_mid else exit_lower
exit_short = min(exit_middle, exit_upper) if use_mid else exit_upper
trigger_long = entry_upper
trigger_short = entry_lower
cooldown_bars = self.SignalCooldownBars
# Manage trailing exits before evaluating new entries
if self.Position > 0 and low <= exit_long:
self.SellMarket()
self._cooldown_remaining = cooldown_bars
elif self.Position < 0 and high >= exit_short:
self.BuyMarket()
self._cooldown_remaining = cooldown_bars
# Enter long on breakout above channel
if self._cooldown_remaining == 0 and self.Position <= 0 and high >= trigger_long:
stop_distance = trigger_long - exit_long
if stop_distance > 0:
self.BuyMarket()
self._cooldown_remaining = cooldown_bars
# Enter short on breakout below channel
elif self._cooldown_remaining == 0 and self.Position >= 0 and low <= trigger_short:
stop_distance = exit_short - trigger_short
if stop_distance > 0:
self.SellMarket()
self._cooldown_remaining = cooldown_bars
def CreateClone(self):
return breakout_strategy()