布林带多头/空头分层策略
概述
该策略是 MetaTrader 专家顾问 Bollinger Bands N positions 的 StockSharp 移植版本。策略监控收盘价与布林带上下轨之间的关系,只要行情在完成的 K 线收盘价位于通道之外,就会触发入场信号。为了重现原始专家顾问的仓位管理逻辑,策略会限制总敞口、设置固定止损/止盈,并在盈利达到指定幅度后启动移动止损。
交易逻辑
- 订阅设定的蜡烛类型,并按配置的周期和标准差倍数计算布林带。
- 每根完整 K 线到来时,策略首先检查当前持仓是否需要离场:
- 多头仓位在触及固定止损、固定止盈或跌破移动止损时退出。
- 空头仓位使用对称的判断逻辑。
- 如果本根 K 线没有触发离场,并且策略允许交易,则评估入场信号:
- 当收盘价高于上轨时,策略会平掉任何现有的空头敞口,并在未超过仓位上限的前提下按配置的手数建立新的多头仓位。
- 当收盘价低于下轨时,策略会平掉任何现有的多头敞口,并按相同方式开立新的空头仓位。
- 一旦盈利超过“移动止损距离 + 移动步长”,移动止损开始生效。止损价保持在价格后方“移动止损距离”处,并且只有当盈利进一步增加至少一个“移动步长”时才会继续向盈利方向推进。
仓位管理
- Max Positions 参数以
MaxPositions × Volume的形式定义允许的最大净仓。由于 StockSharp 采用净持仓模式,策略在任何时刻只能持有一个净方向的仓位,因此该参数实际上用于防止净仓位超过指定阈值后继续加仓。 - 止损与止盈距离以点(pip)为单位,通过交易品种的
PriceStep转换为价格偏移。如果交易品种使用小数点后一位的点值,需要相应调整参数数值。 - 启用移动止损时必须同时设置正的移动距离与移动步长;将移动距离设为 0 会关闭移动止损模块。
参数
| 参数 | 说明 | 默认值 |
|---|---|---|
Volume |
每次进场使用的手数。 | 0.1 |
MaxPositions |
允许的最大净仓量,按 Volume 的倍数表示。 |
9 |
BollingerPeriod |
布林带移动平均的周期。 | 20 |
BollingerWidth |
布林带标准差倍数。 | 2 |
StopLossPips |
固定止损距离(点)。 | 50 |
TakeProfitPips |
固定止盈距离(点)。 | 50 |
TrailingStopPips |
移动止损距离(点),为 0 时禁用移动止损。 |
5 |
TrailingStepPips |
移动止损每次推进所需的最小盈利增量(点)。 | 5 |
CandleType |
用于计算布林带的蜡烛类型或时间框架。 | 1 分钟时间框架 |
与 MQL5 原版的差异
- 原版专家顾问运行在 MetaTrader 的锁仓模式,可以同时持有多头和空头仓位。本移植版在 StockSharp 的净持仓模式下运行,因此在入场之前会先平掉相反方向的敞口;
MaxPositions参数也随之变为对净仓位绝对值的限制。 - 止损与止盈(包括移动止损)在策略内部模拟,而非以附加委托的形式发送到服务器。这种做法符合原版 EA 的移动止损逻辑,但意味着平仓会在下一根完成的 K 线上执行。
- 启用移动止损且步长为零时,策略在启动阶段会抛出异常,复现原始 EA 在初始化时的参数校验。
使用提示
- 根据品种合约规模和点值调整
Volume、MaxPositions以及风险参数。 - 确认交易品种提供有效的
PriceStep。如果返回值为 0,策略会退化为使用1,这可能不适用于所有市场。 - 建议等待布林带周期完成(即指标充分预热)后再允许策略进场,以避免基于不完整的数据做出决策。
- 修改移动止损配置时留意日志中的提示信息,确保步长与距离的组合有效。
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>
/// Bollinger Bands breakout strategy translated from the MQL5 version with N-position control.
/// Opens positions when price closes outside the Bollinger envelope and manages exits via fixed and trailing stops.
/// </summary>
public class BollingerBandsNPositionsStrategy : Strategy
{
private readonly StrategyParam<decimal> _volumeTolerance;
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerWidth;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<decimal> _trailingStepPips;
private readonly StrategyParam<DataType> _candleType;
private decimal? _longEntryPrice;
private decimal? _shortEntryPrice;
private decimal? _longTrailingStop;
private decimal? _shortTrailingStop;
/// <summary>
/// Maximum allowed net position expressed as multiples of <see cref="Volume"/>.
/// </summary>
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
/// <summary>
/// Bollinger Bands period.
/// </summary>
public int BollingerPeriod
{
get => _bollingerPeriod.Value;
set => _bollingerPeriod.Value = value;
}
/// <summary>
/// Bollinger Bands width multiplier.
/// </summary>
public decimal BollingerWidth
{
get => _bollingerWidth.Value;
set => _bollingerWidth.Value = value;
}
/// <summary>
/// Stop-loss distance in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing-stop distance in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Trailing-step increment in pips.
/// </summary>
public decimal TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Net position magnitude treated as flat.
/// </summary>
public decimal VolumeTolerance
{
get => _volumeTolerance.Value;
set => _volumeTolerance.Value = value;
}
/// <summary>
/// Candle type used for signal calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes <see cref="BollingerBandsNPositionsStrategy"/>.
/// </summary>
public BollingerBandsNPositionsStrategy()
{
_maxPositions = Param(nameof(MaxPositions), 9)
.SetGreaterThanZero()
.SetDisplay("Max Positions", "Net position limit in multiples of Volume", "Risk");
_bollingerPeriod = Param(nameof(BollingerPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Bollinger Period", "Moving average length", "Indicators");
_bollingerWidth = Param(nameof(BollingerWidth), 2m)
.SetGreaterThanZero()
.SetDisplay("Bollinger Width", "Standard deviation multiplier", "Indicators");
_stopLossPips = Param(nameof(StopLossPips), 50m)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 5m)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing-stop distance in pips", "Risk");
_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Trailing adjustment increment", "Risk");
_volumeTolerance = Param(nameof(VolumeTolerance), 0.00000001m)
.SetNotNegative()
.SetDisplay("Volume Tolerance", "Minimum net position magnitude treated as flat", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Source candles", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetLongState();
ResetShortState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
throw new InvalidOperationException("Trailing step must be greater than zero when trailing stop is enabled.");
var bollinger = new BollingerBands
{
Length = BollingerPeriod,
Width = BollingerWidth
};
var subscription = SubscribeCandles(CandleType);
subscription.BindEx(bollinger, ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue bbValue)
{
if (candle.State != CandleStates.Finished)
return;
var bb = bbValue as IBollingerBandsValue;
var upper = bb?.UpBand ?? 0m;
var lower = bb?.LowBand ?? 0m;
if (HandleActivePosition(candle))
return;
if (!IsFormed)
return;
if (TryEnterLong(candle, upper))
return;
TryEnterShort(candle, lower);
}
private bool HandleActivePosition(ICandleMessage candle)
{
if (Position > VolumeTolerance)
return ManageLong(candle);
if (Position < -VolumeTolerance)
return ManageShort(candle);
if (_longEntryPrice.HasValue || _shortEntryPrice.HasValue)
{
ResetLongState();
ResetShortState();
}
return false;
}
private bool ManageLong(ICandleMessage candle)
{
if (_longEntryPrice is null)
_longEntryPrice = candle.ClosePrice;
var entry = _longEntryPrice.Value;
var step = GetPriceStep();
if (StopLossPips > 0m)
{
var stopLevel = entry - StopLossPips * step;
if (candle.LowPrice <= stopLevel)
{
SellMarket();
ResetLongState();
return true;
}
}
if (TakeProfitPips > 0m)
{
var targetLevel = entry + TakeProfitPips * step;
if (candle.HighPrice >= targetLevel)
{
SellMarket();
ResetLongState();
return true;
}
}
if (TrailingStopPips > 0m && TrailingStepPips > 0m)
{
var trailingDistance = TrailingStopPips * step;
var trailingStep = TrailingStepPips * step;
var activationDistance = trailingDistance + trailingStep;
if (candle.ClosePrice - entry > activationDistance)
{
var candidate = candle.ClosePrice - trailingDistance;
if (_longTrailingStop is null || candidate - _longTrailingStop.Value > trailingStep)
_longTrailingStop = candidate;
}
if (_longTrailingStop is decimal trailing && candle.LowPrice <= trailing)
{
SellMarket();
ResetLongState();
return true;
}
}
return false;
}
private bool ManageShort(ICandleMessage candle)
{
if (_shortEntryPrice is null)
_shortEntryPrice = candle.ClosePrice;
var entry = _shortEntryPrice.Value;
var step = GetPriceStep();
if (StopLossPips > 0m)
{
var stopLevel = entry + StopLossPips * step;
if (candle.HighPrice >= stopLevel)
{
BuyMarket();
ResetShortState();
return true;
}
}
if (TakeProfitPips > 0m)
{
var targetLevel = entry - TakeProfitPips * step;
if (candle.LowPrice <= targetLevel)
{
BuyMarket();
ResetShortState();
return true;
}
}
if (TrailingStopPips > 0m && TrailingStepPips > 0m)
{
var trailingDistance = TrailingStopPips * step;
var trailingStep = TrailingStepPips * step;
var activationDistance = trailingDistance + trailingStep;
if (entry - candle.ClosePrice > activationDistance)
{
var candidate = candle.ClosePrice + trailingDistance;
if (_shortTrailingStop is null || _shortTrailingStop.Value - candidate > trailingStep)
_shortTrailingStop = candidate;
}
if (_shortTrailingStop is decimal trailing && candle.HighPrice >= trailing)
{
BuyMarket();
ResetShortState();
return true;
}
}
return false;
}
private bool TryEnterLong(ICandleMessage candle, decimal upper)
{
if (candle.ClosePrice <= upper)
return false;
if (!HasCapacity())
return false;
if (Position < -VolumeTolerance)
{
BuyMarket();
ResetShortState();
return true;
}
if (Position > VolumeTolerance)
{
SellMarket();
ResetLongState();
return true;
}
BuyMarket();
_longEntryPrice = candle.ClosePrice;
_longTrailingStop = null;
ResetShortState();
return true;
}
private bool TryEnterShort(ICandleMessage candle, decimal lower)
{
if (candle.ClosePrice >= lower)
return false;
if (!HasCapacity())
return false;
if (Position > VolumeTolerance)
{
SellMarket();
ResetLongState();
return true;
}
if (Position < -VolumeTolerance)
{
BuyMarket();
ResetShortState();
return true;
}
SellMarket();
_shortEntryPrice = candle.ClosePrice;
_shortTrailingStop = null;
ResetLongState();
return true;
}
private bool HasCapacity()
{
if (Volume <= 0m || MaxPositions <= 0)
return false;
var limitVolume = MaxPositions * Volume;
return Math.Abs(Position) < limitVolume - VolumeTolerance;
}
private decimal GetPriceStep()
{
var step = Security?.PriceStep ?? 0m;
return step <= 0m ? 1m : step;
}
private void ResetLongState()
{
_longEntryPrice = null;
_longTrailingStop = null;
}
private void ResetShortState()
{
_shortEntryPrice = null;
_shortTrailingStop = null;
}
}
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 BollingerBands
class bollinger_bands_n_positions_strategy(Strategy):
"""BB breakout strategy with N-position control, SL/TP and trailing stop."""
def __init__(self):
super(bollinger_bands_n_positions_strategy, self).__init__()
self._max_positions = self.Param("MaxPositions", 9) \
.SetGreaterThanZero() \
.SetDisplay("Max Positions", "Net position limit in multiples of Volume", "Risk")
self._bb_period = self.Param("BollingerPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Period", "Moving average length", "Indicators")
self._bb_width = self.Param("BollingerWidth", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Bollinger Width", "Standard deviation multiplier", "Indicators")
self._sl_pips = self.Param("StopLossPips", 50.0) \
.SetNotNegative() \
.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk")
self._tp_pips = self.Param("TakeProfitPips", 50.0) \
.SetNotNegative() \
.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk")
self._trail_pips = self.Param("TrailingStopPips", 5.0) \
.SetNotNegative() \
.SetDisplay("Trailing Stop (pips)", "Trailing-stop distance in pips", "Risk")
self._trail_step_pips = self.Param("TrailingStepPips", 5.0) \
.SetNotNegative() \
.SetDisplay("Trailing Step (pips)", "Trailing adjustment increment", "Risk")
self._vol_tol = self.Param("VolumeTolerance", 0.00000001) \
.SetNotNegative() \
.SetDisplay("Volume Tolerance", "Minimum net position magnitude treated as flat", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Source candles", "General")
self._long_entry = None
self._short_entry = None
self._long_trail = None
self._short_trail = None
@property
def MaxPositions(self):
return self._max_positions.Value
@property
def BollingerPeriod(self):
return self._bb_period.Value
@property
def BollingerWidth(self):
return self._bb_width.Value
@property
def StopLossPips(self):
return self._sl_pips.Value
@property
def TakeProfitPips(self):
return self._tp_pips.Value
@property
def TrailingStopPips(self):
return self._trail_pips.Value
@property
def TrailingStepPips(self):
return self._trail_step_pips.Value
@property
def VolumeTolerance(self):
return self._vol_tol.Value
@property
def CandleType(self):
return self._candle_type.Value
def _step(self):
sec = self.Security
s = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
return s
def OnStarted2(self, time):
super(bollinger_bands_n_positions_strategy, self).OnStarted2(time)
bb = BollingerBands()
bb.Length = self.BollingerPeriod
bb.Width = self.BollingerWidth
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(bb, self.process_candle).Start()
def process_candle(self, candle, bb_val):
if candle.State != CandleStates.Finished:
return
upper = float(bb_val.UpBand) if bb_val.UpBand is not None else 0.0
lower = float(bb_val.LowBand) if bb_val.LowBand is not None else 0.0
if self._handle_active(candle):
return
if not self.IsFormed:
return
self._try_long(candle, upper)
self._try_short(candle, lower)
def _handle_active(self, candle):
tol = float(self.VolumeTolerance)
if self.Position > tol:
return self._manage_long(candle)
if self.Position < -tol:
return self._manage_short(candle)
if self._long_entry is not None or self._short_entry is not None:
self._reset_long()
self._reset_short()
return False
def _manage_long(self, candle):
if self._long_entry is None:
self._long_entry = float(candle.ClosePrice)
entry = self._long_entry
step = self._step()
sl = float(self.StopLossPips)
tp = float(self.TakeProfitPips)
ts = float(self.TrailingStopPips)
tstp = float(self.TrailingStepPips)
if sl > 0:
if float(candle.LowPrice) <= entry - sl * step:
self.SellMarket()
self._reset_long()
return True
if tp > 0:
if float(candle.HighPrice) >= entry + tp * step:
self.SellMarket()
self._reset_long()
return True
if ts > 0 and tstp > 0:
td = ts * step
tstep = tstp * step
act = td + tstep
if float(candle.ClosePrice) - entry > act:
cand = float(candle.ClosePrice) - td
if self._long_trail is None or cand - self._long_trail > tstep:
self._long_trail = cand
if self._long_trail is not None and float(candle.LowPrice) <= self._long_trail:
self.SellMarket()
self._reset_long()
return True
return False
def _manage_short(self, candle):
if self._short_entry is None:
self._short_entry = float(candle.ClosePrice)
entry = self._short_entry
step = self._step()
sl = float(self.StopLossPips)
tp = float(self.TakeProfitPips)
ts = float(self.TrailingStopPips)
tstp = float(self.TrailingStepPips)
if sl > 0:
if float(candle.HighPrice) >= entry + sl * step:
self.BuyMarket()
self._reset_short()
return True
if tp > 0:
if float(candle.LowPrice) <= entry - tp * step:
self.BuyMarket()
self._reset_short()
return True
if ts > 0 and tstp > 0:
td = ts * step
tstep = tstp * step
act = td + tstep
if entry - float(candle.ClosePrice) > act:
cand = float(candle.ClosePrice) + td
if self._short_trail is None or self._short_trail - cand > tstep:
self._short_trail = cand
if self._short_trail is not None and float(candle.HighPrice) >= self._short_trail:
self.BuyMarket()
self._reset_short()
return True
return False
def _try_long(self, candle, upper):
if float(candle.ClosePrice) <= upper:
return
if not self._has_capacity():
return
tol = float(self.VolumeTolerance)
if self.Position < -tol:
self.BuyMarket()
self._reset_short()
return
if self.Position > tol:
self.SellMarket()
self._reset_long()
return
self.BuyMarket()
self._long_entry = float(candle.ClosePrice)
self._long_trail = None
self._reset_short()
def _try_short(self, candle, lower):
if float(candle.ClosePrice) >= lower:
return
if not self._has_capacity():
return
tol = float(self.VolumeTolerance)
if self.Position > tol:
self.SellMarket()
self._reset_long()
return
if self.Position < -tol:
self.BuyMarket()
self._reset_short()
return
self.SellMarket()
self._short_entry = float(candle.ClosePrice)
self._short_trail = None
self._reset_long()
def _has_capacity(self):
if self.Volume <= 0 or self.MaxPositions <= 0:
return False
limit = self.MaxPositions * self.Volume
return abs(self.Position) < limit - float(self.VolumeTolerance)
def _reset_long(self):
self._long_entry = None
self._long_trail = None
def _reset_short(self):
self._short_entry = None
self._short_trail = None
def OnReseted(self):
super(bollinger_bands_n_positions_strategy, self).OnReseted()
self._reset_long()
self._reset_short()
def CreateClone(self):
return bollinger_bands_n_positions_strategy()