布林带多仓位 v2 策略
概述
本策略复刻 Vladimir Karputov 的 "Bollinger Bands N positions v2" 智能交易顾问。策略仅在蜡烛图收盘后运行,比较收盘价与布林带上下轨的位置。在迁移到 StockSharp 时保留了原策略的加仓、风险控制以及移动止损逻辑,同时符合平台的净持仓体系。
交易逻辑
- 在所选蜡烛序列上计算布林带指标,周期与偏差均可配置。
- 当蜡烛收盘价高于上轨时,策略先平掉所有空头仓位,再按设定的手数加仓多头(最多累积到 Max Positions 次)。
- 当蜡烛收盘价低于下轨时,策略先平掉所有多头仓位,再按设定手数加仓空头(同样受最大次数限制)。
- 每次加仓的交易量固定,由 Volume 参数控制,因此仓位会按等量阶梯扩展。
- 策略跟踪当前方向的平均持仓价格,用于统一计算止损、止盈和移动止损的触发点。
风险管理
- 止损与止盈距离以“点”(pip)为单位输入。程序会将其乘以品种的最小价格变动(PriceStep)转换为绝对价格偏移;若品种精度为 3 或 5 位小数,则额外乘以 10,以模拟 MetaTrader 中的五位定价处理。
- 移动止损距离与移动步长同样以点数表示。只有当价格从平均入场价向有利方向移动超过
TrailingStop + TrailingStep点后,策略才会将止损价格上移/下移指定的 trailing stop 距离,并保持额外的步长缓冲,避免过于频繁地修改订单。 - 保护性止损在策略内部模拟:一旦收盘蜡烛触及止损或止盈水平,将通过市价单立即平仓全部头寸。
参数
| 参数 | 说明 |
|---|---|
| Bollinger Period | 计算布林带的移动平均周期。 |
| Bollinger Deviation | 布林带的标准差倍数。 |
| Max Positions | 同方向最多允许的累计加仓次数。 |
| Volume | 每次入场的下单量。 |
| Stop Loss (pips) | 止损距离(点),0 表示不启用。 |
| Take Profit (pips) | 止盈距离(点),0 表示不启用。 |
| Trailing Stop (pips) | 移动止损距离(点),0 表示不启用。 |
| Trailing Step (pips) | 再次移动止损前需要的额外盈利点数;启用移动止损时必须为正值。 |
| Candle Type | 用于计算的蜡烛类型或时间框架。 |
实现细节
- 使用高层 API 的蜡烛订阅与
BindEx绑定指标,符合 StockSharp 的开发规范。 - 仅处理已完成的蜡烛,保持与原 MT5 脚本 "new bar" 逻辑一致。
- 由于 StockSharp 采用净持仓模式,策略在切换方向前会先平掉相反方向的仓位,再执行新的加仓。
- 当启用移动止损时会强制检查步长必须大于零,与原版专家顾问的安全限制一致。
- 本次仅提供 C# 版本,暂未包含 Python 实现。
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 that can pyramid entries and applies pip-based risk management.
/// </summary>
public class BollingerBandsNPositionsV2Strategy : Strategy
{
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerDeviation;
private readonly StrategyParam<int> _maxPositions;
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 _pipValue;
private decimal _stopLossDistance;
private decimal _takeProfitDistance;
private decimal _trailingStopDistance;
private decimal _trailingStepDistance;
private decimal _longEntryPrice;
private decimal _shortEntryPrice;
private int _longEntryCount;
private int _shortEntryCount;
private BollingerBands _bollinger = null!;
private decimal? _longStopPrice;
private decimal? _longTakeProfitPrice;
private decimal? _shortStopPrice;
private decimal? _shortTakeProfitPrice;
/// <summary>
/// Bollinger Bands period.
/// </summary>
public int BollingerPeriod
{
get => _bollingerPeriod.Value;
set => _bollingerPeriod.Value = value;
}
/// <summary>
/// Bollinger Bands standard deviation multiplier.
/// </summary>
public decimal BollingerDeviation
{
get => _bollingerDeviation.Value;
set => _bollingerDeviation.Value = value;
}
/// <summary>
/// Maximum stacked entries per direction.
/// </summary>
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
/// <summary>
/// Stop loss distance expressed in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance expressed in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance expressed in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Additional profit in pips required before trailing stop is moved again.
/// </summary>
public decimal TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.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="BollingerBandsNPositionsV2Strategy"/>.
/// </summary>
public BollingerBandsNPositionsV2Strategy()
{
_bollingerPeriod = Param(nameof(BollingerPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Bollinger Period", "Period used for Bollinger Bands.", "Indicators")
;
_bollingerDeviation = Param(nameof(BollingerDeviation), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier for Bollinger Bands.", "Indicators")
;
_maxPositions = Param(nameof(MaxPositions), 1)
.SetGreaterThanZero()
.SetDisplay("Max Positions", "Maximum number of stacked entries per direction.", "Trading");
_stopLossPips = Param(nameof(StopLossPips), 30m)
.SetNotNegative()
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips.", "Risk")
;
_takeProfitPips = Param(nameof(TakeProfitPips), 60m)
.SetNotNegative()
.SetDisplay("Take Profit (pips)", "Profit target distance in pips.", "Risk")
;
_trailingStopPips = Param(nameof(TrailingStopPips), 5m)
.SetNotNegative()
.SetDisplay("Trailing Stop (pips)", "Trailing stop offset in pips.", "Risk")
;
_trailingStepPips = Param(nameof(TrailingStepPips), 1m)
.SetNotNegative()
.SetDisplay("Trailing Step (pips)", "Extra profit in pips before trailing stop is adjusted.", "Risk")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for Bollinger analysis.", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pipValue = 0m;
_stopLossDistance = 0m;
_takeProfitDistance = 0m;
_trailingStopDistance = 0m;
_trailingStepDistance = 0m;
ResetLongState();
ResetShortState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
throw new InvalidOperationException("Trailing step must be positive when trailing stop is enabled.");
_pipValue = CalculatePipValue();
UpdateRiskDistances();
_bollinger = new BollingerBands
{
Length = BollingerPeriod,
Width = BollingerDeviation
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
IIndicatorValue indicatorValue;
try
{
indicatorValue = _bollinger.Process(candle);
}
catch (IndexOutOfRangeException)
{
return;
}
if (indicatorValue.IsEmpty || !_bollinger.IsFormed)
return;
UpdateRiskDistances();
var value = (BollingerBandsValue)indicatorValue;
if (value.UpBand is not decimal upper || value.LowBand is not decimal lower)
return;
HandleRiskManagement(candle);
if (candle.ClosePrice > upper)
{
TryEnterLong(candle);
return;
}
if (candle.ClosePrice < lower)
{
TryEnterShort(candle);
}
}
private void HandleRiskManagement(ICandleMessage candle)
{
if (_longEntryCount > 0 && Position > 0)
{
if (_longTakeProfitPrice is decimal takeProfit && candle.HighPrice >= takeProfit)
{
SellMarket(Position);
ResetLongState();
return;
}
if (_longStopPrice is decimal stopLoss && candle.LowPrice <= stopLoss)
{
SellMarket(Position);
ResetLongState();
return;
}
UpdateLongTrailing(candle);
}
else if (Position <= 0)
{
ResetLongState();
}
if (_shortEntryCount > 0 && Position < 0)
{
var positionVolume = Math.Abs(Position);
if (_shortTakeProfitPrice is decimal takeProfit && candle.LowPrice <= takeProfit)
{
BuyMarket(positionVolume);
ResetShortState();
return;
}
if (_shortStopPrice is decimal stopLoss && candle.HighPrice >= stopLoss)
{
BuyMarket(positionVolume);
ResetShortState();
return;
}
UpdateShortTrailing(candle);
}
else if (Position >= 0)
{
ResetShortState();
}
}
private void TryEnterLong(ICandleMessage candle)
{
if (_longEntryCount >= MaxPositions)
return;
if (Position < 0)
{
var closeVolume = Math.Abs(Position);
if (closeVolume > 0)
{
BuyMarket(closeVolume);
ResetShortState();
}
}
var tradeVolume = Volume;
if (tradeVolume <= 0)
return;
var existingVolume = _longEntryCount * tradeVolume;
BuyMarket(tradeVolume);
var entryPrice = candle.ClosePrice;
var newVolume = existingVolume + tradeVolume;
_longEntryPrice = existingVolume <= 0 ? entryPrice : ((_longEntryPrice * existingVolume) + entryPrice * tradeVolume) / newVolume;
_longEntryCount++;
_longStopPrice = StopLossPips > 0m ? _longEntryPrice - _stopLossDistance : null;
_longTakeProfitPrice = TakeProfitPips > 0m ? _longEntryPrice + _takeProfitDistance : null;
}
private void TryEnterShort(ICandleMessage candle)
{
if (_shortEntryCount >= MaxPositions)
return;
if (Position > 0)
{
var closeVolume = Position;
if (closeVolume > 0)
{
SellMarket(closeVolume);
ResetLongState();
}
}
var tradeVolume = Volume;
if (tradeVolume <= 0)
return;
var existingVolume = _shortEntryCount * tradeVolume;
SellMarket(tradeVolume);
var entryPrice = candle.ClosePrice;
var newVolume = existingVolume + tradeVolume;
_shortEntryPrice = existingVolume <= 0 ? entryPrice : ((_shortEntryPrice * existingVolume) + entryPrice * tradeVolume) / newVolume;
_shortEntryCount++;
_shortStopPrice = StopLossPips > 0m ? _shortEntryPrice + _stopLossDistance : null;
_shortTakeProfitPrice = TakeProfitPips > 0m ? _shortEntryPrice - _takeProfitDistance : null;
}
private void UpdateLongTrailing(ICandleMessage candle)
{
if (_trailingStopDistance <= 0m)
return;
var moveFromEntry = candle.ClosePrice - _longEntryPrice;
if (moveFromEntry <= _trailingStopDistance + _trailingStepDistance)
return;
var newStop = candle.ClosePrice - _trailingStopDistance;
if (_longStopPrice is not decimal currentStop || newStop > currentStop + _trailingStepDistance)
_longStopPrice = newStop;
}
private void UpdateShortTrailing(ICandleMessage candle)
{
if (_trailingStopDistance <= 0m)
return;
var moveFromEntry = _shortEntryPrice - candle.ClosePrice;
if (moveFromEntry <= _trailingStopDistance + _trailingStepDistance)
return;
var newStop = candle.ClosePrice + _trailingStopDistance;
if (_shortStopPrice is not decimal currentStop || newStop < currentStop - _trailingStepDistance)
_shortStopPrice = newStop;
}
private void ResetLongState()
{
_longEntryPrice = 0m;
_longEntryCount = 0;
_longStopPrice = null;
_longTakeProfitPrice = null;
}
private void ResetShortState()
{
_shortEntryPrice = 0m;
_shortEntryCount = 0;
_shortStopPrice = null;
_shortTakeProfitPrice = null;
}
private void UpdateRiskDistances()
{
_stopLossDistance = StopLossPips > 0m ? StopLossPips * _pipValue : 0m;
_takeProfitDistance = TakeProfitPips > 0m ? TakeProfitPips * _pipValue : 0m;
_trailingStopDistance = TrailingStopPips > 0m ? TrailingStopPips * _pipValue : 0m;
_trailingStepDistance = TrailingStepPips > 0m ? TrailingStepPips * _pipValue : 0m;
}
private decimal CalculatePipValue()
{
var security = Security;
if (security == null)
return 1m;
var step = security.PriceStep ?? 0m;
if (step <= 0m)
return 1m;
var decimals = CountDecimals(step);
if (decimals == 3 || decimals == 5)
return step * 10m;
return step;
}
private static int CountDecimals(decimal value)
{
value = Math.Abs(value);
var decimals = 0;
while (value != Math.Truncate(value) && decimals < 10)
{
value *= 10m;
decimals++;
}
return decimals;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import BollingerBands, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
class bollinger_bands_n_positions_v2_strategy(Strategy):
def __init__(self):
super(bollinger_bands_n_positions_v2_strategy, self).__init__()
self._bollinger_period = self.Param("BollingerPeriod", 20) \
.SetDisplay("Bollinger Period", "Period used for Bollinger Bands.", "Indicators")
self._bollinger_deviation = self.Param("BollingerDeviation", 1.5) \
.SetDisplay("Bollinger Deviation", "Standard deviation multiplier for Bollinger Bands.", "Indicators")
self._max_positions = self.Param("MaxPositions", 1) \
.SetDisplay("Max Positions", "Maximum number of stacked entries per direction.", "Trading")
self._stop_loss_pips = self.Param("StopLossPips", 30.0) \
.SetDisplay("Stop Loss (pips)", "Protective stop distance in pips.", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 60.0) \
.SetDisplay("Take Profit (pips)", "Profit target distance in pips.", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 5.0) \
.SetDisplay("Trailing Stop (pips)", "Trailing stop offset in pips.", "Risk")
self._trailing_step_pips = self.Param("TrailingStepPips", 1.0) \
.SetDisplay("Trailing Step (pips)", "Extra profit in pips before trailing stop is adjusted.", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Timeframe used for Bollinger analysis.", "General")
self._pip_value = 0.0
self._stop_loss_distance = 0.0
self._take_profit_distance = 0.0
self._trailing_stop_distance = 0.0
self._trailing_step_distance = 0.0
self._long_entry_price = 0.0
self._short_entry_price = 0.0
self._long_entry_count = 0
self._short_entry_count = 0
self._long_stop_price = None
self._long_take_profit_price = None
self._short_stop_price = None
self._short_take_profit_price = None
self._bollinger = None
@property
def bollinger_period(self):
return self._bollinger_period.Value
@property
def bollinger_deviation(self):
return self._bollinger_deviation.Value
@property
def max_positions(self):
return self._max_positions.Value
@property
def stop_loss_pips(self):
return self._stop_loss_pips.Value
@property
def take_profit_pips(self):
return self._take_profit_pips.Value
@property
def trailing_stop_pips(self):
return self._trailing_stop_pips.Value
@property
def trailing_step_pips(self):
return self._trailing_step_pips.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(bollinger_bands_n_positions_v2_strategy, self).OnReseted()
self._pip_value = 0.0
self._stop_loss_distance = 0.0
self._take_profit_distance = 0.0
self._trailing_stop_distance = 0.0
self._trailing_step_distance = 0.0
self._reset_long_state()
self._reset_short_state()
def OnStarted2(self, time):
super(bollinger_bands_n_positions_v2_strategy, self).OnStarted2(time)
self._pip_value = self._calculate_pip_value()
self._update_risk_distances()
self._bollinger = BollingerBands()
self._bollinger.Length = self.bollinger_period
self._bollinger.Width = self.bollinger_deviation
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
civ = CandleIndicatorValue(self._bollinger, candle)
civ.IsFinal = True
indicator_value = self._bollinger.Process(civ)
if indicator_value.IsEmpty or not self._bollinger.IsFormed:
return
self._update_risk_distances()
upper = indicator_value.UpBand
lower = indicator_value.LowBand
if upper is None or lower is None:
return
upper = float(upper)
lower = float(lower)
self._handle_risk_management(candle)
if float(candle.ClosePrice) > upper:
self._try_enter_long(candle)
return
if float(candle.ClosePrice) < lower:
self._try_enter_short(candle)
def _handle_risk_management(self, candle):
if self._long_entry_count > 0 and self.Position > 0:
if self._long_take_profit_price is not None and float(candle.HighPrice) >= self._long_take_profit_price:
self.SellMarket()
self._reset_long_state()
return
if self._long_stop_price is not None and float(candle.LowPrice) <= self._long_stop_price:
self.SellMarket()
self._reset_long_state()
return
self._update_long_trailing(candle)
elif self.Position <= 0:
self._reset_long_state()
if self._short_entry_count > 0 and self.Position < 0:
if self._short_take_profit_price is not None and float(candle.LowPrice) <= self._short_take_profit_price:
self.BuyMarket()
self._reset_short_state()
return
if self._short_stop_price is not None and float(candle.HighPrice) >= self._short_stop_price:
self.BuyMarket()
self._reset_short_state()
return
self._update_short_trailing(candle)
elif self.Position >= 0:
self._reset_short_state()
def _try_enter_long(self, candle):
if self._long_entry_count >= self.max_positions:
return
if self.Position < 0:
self.BuyMarket()
self._reset_short_state()
self.BuyMarket()
entry_price = float(candle.ClosePrice)
self._long_entry_count += 1
self._long_entry_price = entry_price
if self.stop_loss_pips > 0:
self._long_stop_price = self._long_entry_price - self._stop_loss_distance
if self.take_profit_pips > 0:
self._long_take_profit_price = self._long_entry_price + self._take_profit_distance
def _try_enter_short(self, candle):
if self._short_entry_count >= self.max_positions:
return
if self.Position > 0:
self.SellMarket()
self._reset_long_state()
self.SellMarket()
entry_price = float(candle.ClosePrice)
self._short_entry_count += 1
self._short_entry_price = entry_price
if self.stop_loss_pips > 0:
self._short_stop_price = self._short_entry_price + self._stop_loss_distance
if self.take_profit_pips > 0:
self._short_take_profit_price = self._short_entry_price - self._take_profit_distance
def _update_long_trailing(self, candle):
if self._trailing_stop_distance <= 0:
return
move_from_entry = float(candle.ClosePrice) - self._long_entry_price
if move_from_entry <= self._trailing_stop_distance + self._trailing_step_distance:
return
new_stop = float(candle.ClosePrice) - self._trailing_stop_distance
if self._long_stop_price is None or new_stop > self._long_stop_price + self._trailing_step_distance:
self._long_stop_price = new_stop
def _update_short_trailing(self, candle):
if self._trailing_stop_distance <= 0:
return
move_from_entry = self._short_entry_price - float(candle.ClosePrice)
if move_from_entry <= self._trailing_stop_distance + self._trailing_step_distance:
return
new_stop = float(candle.ClosePrice) + self._trailing_stop_distance
if self._short_stop_price is None or new_stop < self._short_stop_price - self._trailing_step_distance:
self._short_stop_price = new_stop
def _reset_long_state(self):
self._long_entry_price = 0.0
self._long_entry_count = 0
self._long_stop_price = None
self._long_take_profit_price = None
def _reset_short_state(self):
self._short_entry_price = 0.0
self._short_entry_count = 0
self._short_stop_price = None
self._short_take_profit_price = None
def _update_risk_distances(self):
self._stop_loss_distance = self.stop_loss_pips * self._pip_value if self.stop_loss_pips > 0 else 0.0
self._take_profit_distance = self.take_profit_pips * self._pip_value if self.take_profit_pips > 0 else 0.0
self._trailing_stop_distance = self.trailing_stop_pips * self._pip_value if self.trailing_stop_pips > 0 else 0.0
self._trailing_step_distance = self.trailing_step_pips * self._pip_value if self.trailing_step_pips > 0 else 0.0
def _calculate_pip_value(self):
security = self.Security
if security is None:
return 1.0
step = security.PriceStep
if step is None or float(step) <= 0:
return 1.0
step_val = float(step)
decimals = self._count_decimals(step_val)
if decimals == 3 or decimals == 5:
return step_val * 10.0
return step_val
@staticmethod
def _count_decimals(value):
value = abs(value)
decimals = 0
while value != int(value) and decimals < 10:
value *= 10
decimals += 1
return decimals
def CreateClone(self):
return bollinger_bands_n_positions_v2_strategy()