在 GitHub 上查看

布林带多仓位 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;
	}
}