Breakthrough BB 策略
概述
Breakthrough BB 策略将 MetaTrader 专家顾问 Breakthrough_BB 迁移到 StockSharp 高级 API。系统结合布林带与简单移动平均线(SMA),用于捕捉价格在通道边缘压缩后的突破行情。所有信号均基于已完成的 K 线生成,从而忠实再现原始 MQL5 逻辑。
交易逻辑
- 趋势过滤器: 使用可配置周期的 SMA 来判断趋势方向。策略比较当前 SMA 数值与四根 K 线前的 SMA 数值。数值上升时允许做多,下降时允许做空。
- 布林带突破: 策略关注四根 K 线前的收盘价与当前布林带上下轨的关系。如果价格从轨道内部移动到轨道外部,则视为有效突破。
- 单一持仓模式: 系统任意时刻仅持有一笔仓位。若已有持仓,会先按照离场规则平仓后再评估新的进场机会。
入场条件
做多
- 四根 K 线前的收盘价低于当时的布林带上轨。
- 最新收盘价高于当前布林带上轨。
- 最新 SMA 数值大于四根 K 线前的 SMA 数值(趋势向上)。
- 当前无持仓。
做空
- 四根 K 线前的收盘价高于当时的布林带下轨。
- 最新收盘价低于当前布林带下轨。
- 最新 SMA 数值小于四根 K 线前的 SMA 数值(趋势向下)。
- 当前无持仓。
当满足相应条件时,策略会按照参数 Volume 定义的数量发送市价单。
离场规则
- 多头离场: 若持有多单且最新收盘价跌破布林带中轨,则立即以市价卖出平仓。
- 空头离场: 若持有空单且最新收盘价突破布林带中轨,则立即以市价买入回补。
上述规则与原始 MQL5 程序保持一致,确保价格重新回到中轨附近时及时退出。
指标
- 简单移动平均线 (SMA): 提供趋势判断,并用于比较四根 K 线之间的斜率变化。
- 布林带: 提供上轨、中轨、下轨,既用于识别突破,也用于确定出场信号。
参数
| 名称 | 说明 | 默认值 | 可优化 |
|---|---|---|---|
MaPeriod |
用于趋势过滤的 SMA 周期。 | 9 |
✔ |
BandsPeriod |
布林带的计算周期。 | 28 |
✔ |
Deviation |
布林带的标准差倍数。 | 1.6 |
✔ |
Volume |
每次下单的数量(按合约或手数)。 | 1 |
✔ |
CandleType |
策略使用的 K 线类型。 | 1 小时时间框架 | ✖ |
所有参数均通过 StrategyParam 暴露,便于在界面中调整或在优化器中批量搜索。
数据要求
- 适用于任何能够提供所选
CandleTypeK 线数据的市场品种。 - 仅在 K 线收盘后评估信号,忽略未完成的 K 线以保证确定性。
- 默认使用 1 小时 K 线,可根据数据源改为其他周期。
其他说明
- 策略未直接访问指标历史,而是维护了一个四根 K 线的滚动缓冲区来保存收盘价和 SMA,用于满足项目关于高阶 API 的要求。
- 原版程序未设置止损或止盈,因此这里也未启用。如需风险控制,可进一步调用
StartProtection。 - 策略使用市价单执行,建议选择流动性充足的品种以降低滑点风险。
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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Breakout strategy that trades Bollinger Bands breakouts with a moving average trend filter.
/// </summary>
public class BreakthroughBbStrategy : Strategy
{
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<int> _bandsPeriod;
private readonly StrategyParam<decimal> _deviation;
private readonly StrategyParam<DataType> _candleType;
private SimpleMovingAverage _sma;
private BollingerBands _bollingerBands;
private decimal? _closeLag0;
private decimal? _closeLag1;
private decimal? _closeLag2;
private decimal? _closeLag3;
private decimal? _maLag0;
private decimal? _maLag1;
private decimal? _maLag2;
private decimal? _maLag3;
/// <summary>
/// Moving average period that defines the long term trend.
/// </summary>
public int MaPeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Bollinger Bands lookback period.
/// </summary>
public int BandsPeriod
{
get => _bandsPeriod.Value;
set => _bandsPeriod.Value = value;
}
/// <summary>
/// Bollinger Bands width measured in standard deviations.
/// </summary>
public decimal Deviation
{
get => _deviation.Value;
set => _deviation.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="BreakthroughBbStrategy"/>.
/// </summary>
public BreakthroughBbStrategy()
{
_maPeriod = Param(nameof(MaPeriod), 9)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Simple moving average length", "Parameters")
;
_bandsPeriod = Param(nameof(BandsPeriod), 28)
.SetGreaterThanZero()
.SetDisplay("Bands Period", "Bollinger Bands lookback", "Parameters")
;
_deviation = Param(nameof(Deviation), 1.6m)
.SetGreaterThanZero()
.SetDisplay("Deviation", "Bollinger Bands width in deviations", "Parameters")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Candle series processed by the strategy", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_sma = default;
_bollingerBands = default;
_closeLag0 = null;
_closeLag1 = null;
_closeLag2 = null;
_closeLag3 = null;
_maLag0 = null;
_maLag1 = null;
_maLag2 = null;
_maLag3 = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_sma = new SimpleMovingAverage { Length = MaPeriod };
_bollingerBands = new BollingerBands
{
Length = BandsPeriod,
Width = Deviation
};
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_sma, _bollingerBands, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue smaIndValue, IIndicatorValue bbValue)
{
if (candle.State != CandleStates.Finished)
return;
var smaValue = smaIndValue.IsFormed ? smaIndValue.ToDecimal() : 0m;
var bb = bbValue as IBollingerBandsValue;
var middleBand = bb?.MovingAverage ?? 0m;
var upperBand = bb?.UpBand ?? 0m;
var lowerBand = bb?.LowBand ?? 0m;
var close = candle.ClosePrice;
var maPrev4 = _maLag2;
var closePrev4 = _closeLag2;
if (_sma is null || _bollingerBands is null)
{
UpdateHistory(close, smaValue);
return;
}
if (!_sma.IsFormed || !_bollingerBands.IsFormed)
{
UpdateHistory(close, smaValue);
return;
}
if (Position > 0 && close < middleBand)
{
SellMarket();
UpdateHistory(close, smaValue);
return;
}
if (Position < 0 && close > middleBand)
{
BuyMarket();
UpdateHistory(close, smaValue);
return;
}
if (maPrev4 is null || closePrev4 is null)
{
UpdateHistory(close, smaValue);
return;
}
if (Position == 0)
{
if (closePrev4.Value < upperBand && close > upperBand && smaValue > maPrev4.Value)
{
BuyMarket();
UpdateHistory(close, smaValue);
return;
}
if (closePrev4.Value > lowerBand && close < lowerBand && smaValue < maPrev4.Value)
{
SellMarket();
UpdateHistory(close, smaValue);
return;
}
}
UpdateHistory(close, smaValue);
}
private void UpdateHistory(decimal close, decimal maValue)
{
_maLag3 = _maLag2;
_maLag2 = _maLag1;
_maLag1 = _maLag0;
_maLag0 = maValue;
_closeLag3 = _closeLag2;
_closeLag2 = _closeLag1;
_closeLag1 = _closeLag0;
_closeLag0 = close;
}
}
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.Indicators import SimpleMovingAverage, BollingerBands
from StockSharp.Algo.Strategies import Strategy
class breakthrough_bb_strategy(Strategy):
def __init__(self):
super(breakthrough_bb_strategy, self).__init__()
self._ma_period = self.Param("MaPeriod", 9)
self._bands_period = self.Param("BandsPeriod", 28)
self._deviation = self.Param("Deviation", 1.6)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._close_lag0 = None
self._close_lag1 = None
self._close_lag2 = None
self._close_lag3 = None
self._ma_lag0 = None
self._ma_lag1 = None
self._ma_lag2 = None
self._ma_lag3 = None
@property
def MaPeriod(self):
return self._ma_period.Value
@MaPeriod.setter
def MaPeriod(self, value):
self._ma_period.Value = value
@property
def BandsPeriod(self):
return self._bands_period.Value
@BandsPeriod.setter
def BandsPeriod(self, value):
self._bands_period.Value = value
@property
def Deviation(self):
return self._deviation.Value
@Deviation.setter
def Deviation(self, value):
self._deviation.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(breakthrough_bb_strategy, self).OnStarted2(time)
self._sma = SimpleMovingAverage()
self._sma.Length = self.MaPeriod
self._bollinger = BollingerBands()
self._bollinger.Length = self.BandsPeriod
self._bollinger.Width = self.Deviation
self._close_lag0 = None
self._close_lag1 = None
self._close_lag2 = None
self._close_lag3 = None
self._ma_lag0 = None
self._ma_lag1 = None
self._ma_lag2 = None
self._ma_lag3 = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(self._sma, self._bollinger, self.ProcessCandle).Start()
def ProcessCandle(self, candle, sma_ind_value, bb_value):
if candle.State != CandleStates.Finished:
return
sma_val = float(sma_ind_value) if sma_ind_value.IsFormed else 0.0
mid_band = float(bb_value.MovingAverage) if bb_value.MovingAverage is not None else 0.0
upper = float(bb_value.UpBand) if bb_value.UpBand is not None else 0.0
lower = float(bb_value.LowBand) if bb_value.LowBand is not None else 0.0
close = float(candle.ClosePrice)
if not self._sma.IsFormed or not self._bollinger.IsFormed:
self._update_history(close, sma_val)
return
# Exit on middle band cross
if self.Position > 0 and close < mid_band:
self.SellMarket()
self._update_history(close, sma_val)
return
if self.Position < 0 and close > mid_band:
self.BuyMarket()
self._update_history(close, sma_val)
return
ma_prev4 = self._ma_lag2
close_prev4 = self._close_lag2
if ma_prev4 is None or close_prev4 is None:
self._update_history(close, sma_val)
return
if self.Position == 0:
if close_prev4 < upper and close > upper and sma_val > ma_prev4:
self.BuyMarket()
self._update_history(close, sma_val)
return
if close_prev4 > lower and close < lower and sma_val < ma_prev4:
self.SellMarket()
self._update_history(close, sma_val)
return
self._update_history(close, sma_val)
def _update_history(self, close, ma_value):
self._ma_lag3 = self._ma_lag2
self._ma_lag2 = self._ma_lag1
self._ma_lag1 = self._ma_lag0
self._ma_lag0 = ma_value
self._close_lag3 = self._close_lag2
self._close_lag2 = self._close_lag1
self._close_lag1 = self._close_lag0
self._close_lag0 = close
def OnReseted(self):
super(breakthrough_bb_strategy, self).OnReseted()
self._close_lag0 = None
self._close_lag1 = None
self._close_lag2 = None
self._close_lag3 = None
self._ma_lag0 = None
self._ma_lag1 = None
self._ma_lag2 = None
self._ma_lag3 = None
def CreateClone(self):
return breakthrough_bb_strategy()