在 GitHub 上查看

DC2008 布林带突破策略

将 Sergey Pavlov (DC2008) 的 MetaTrader 布林带突破专家顾问移植到 StockSharp 高级策略 API。策略在选定的周期上等待完整蜡烛,依据指定的价格源计算布林带,并且只有在当前持仓不亏损时才开仓或反向。

概览

  • 在设定的 CandleType 上计算布林带(支持收盘价、开盘价、最高价、最低价、中位价、典型价、加权价或 OHLC 平均价)。
  • 做多 条件:蜡烛最低价跌破下轨,同时最高价仍低于中轨,表示价格被压制在均线下方后可能反弹。
  • 做空 条件:蜡烛最高价突破上轨,同时最低价仍高于中轨,表示价格被推升到均线上方后可能回落。
  • 原始 MQL 策略在每个 tick 上运行;移植版本在蜡烛完成后判断,保证指标输出稳定且避免未完成数据。
  • 仅当当前仓位的浮动盈亏不为负时才允许开新仓或反向,保持与原始过滤规则一致。

交易流程

指标处理

  1. 订阅所选的蜡烛类型(默认 1 小时)。
  2. AppliedPrice 指定的价格输入布林带指标 (Length = BandsPeriod, Width = BandsDeviation)。
  3. 仅在指标给出有效的上轨、中轨、下轨后才继续判断信号。

信号判定

  • 买入Low < LowerBandHigh < MiddleBand
  • 卖出High > UpperBandLow > MiddleBand

仓位管理

  • 无持仓时,在信号出现后按 Volume 下达市价单建仓。
  • 已有持仓时:
    • 计算蜡烛收盘价下的浮动盈亏 Position * (Close - PositionPrice)
    • 如果浮亏为负,则跳过本根蜡烛的所有操作。
    • 如果浮盈不为负且信号与当前方向相反,则按 Volume + |Position| 发送市价单,实现平仓并反向建仓。
    • 与当前方向相同的信号不会加仓。
  • 策略不设置固定止损或止盈,退出完全依赖满足条件的反向信号。

参数

参数 默认值 说明
BandsPeriod 80 布林带均线与标准差的计算周期,可优化,必须大于 0。
BandsDeviation 3.0 布林带标准差系数,控制带宽,可优化。
AppliedPrice Close 指定指标使用的价格:Close、Open、High、Low、Median、Typical、Weighted 或 Average (OHLC/4)。对应 MetaTrader 的 ENUM_APPLIED_PRICE
CandleType 1 小时 用于分析和下单的蜡烛类型,可替换为 StockSharp 支持的其他数据类型。
Volume(继承) 取决于经纪商 新开仓的数量,反向时会自动加上当前持仓的绝对值。

与原始 MQL EA 的差异

  • Tick 级触发 → 仅在蜡烛收盘后触发。
  • 原代码中的布林带移位参数固定为 0,因此在此实现中保持隐式。
  • MQL 中直接读取持仓浮盈;此版本通过 PositionPrice 与收盘价估算,足以判断正负号。
  • 去除了订单文本注释,仅保留核心交易逻辑。

实现说明

  • 使用 StockSharp 的高层 API:SubscribeCandles().Bind(...)BuyMarketSellMarket 等。
  • 如果 UI 中可创建图表区域,将自动绘制蜡烛、布林带以及策略成交。
  • 每次启动都会重新创建指标,因此修改参数后在下次运行即可生效。
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 breakout strategy inspired by DC2008 implementation.
/// </summary>
public class BollingerBreakoutDc2008Strategy : Strategy
{
	public enum AppliedPriceTypes
	{
		Close,
		Open,
		High,
		Low,
		Median,
		Typical,
		Weighted,
		Average
	}

	private readonly StrategyParam<int> _bandsPeriod;
	private readonly StrategyParam<decimal> _bandsDeviation;
	private readonly StrategyParam<AppliedPriceTypes> _appliedPrice;
	private readonly StrategyParam<DataType> _candleType;

	private BollingerBands _bollinger;
	private decimal _entryPrice;

	public int BandsPeriod
	{
		get => _bandsPeriod.Value;
		set => _bandsPeriod.Value = value;
	}

	public decimal BandsDeviation
	{
		get => _bandsDeviation.Value;
		set => _bandsDeviation.Value = value;
	}

	public AppliedPriceTypes AppliedPrice
	{
		get => _appliedPrice.Value;
		set => _appliedPrice.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public BollingerBreakoutDc2008Strategy()
	{
		_bandsPeriod = Param(nameof(BandsPeriod), 80)
			.SetDisplay("Bands Period", "Number of candles for Bollinger Bands", "Indicators")
			.SetGreaterThanZero()
			
			.SetOptimize(10, 200, 10);

		_bandsDeviation = Param(nameof(BandsDeviation), 3m)
			.SetDisplay("Deviation", "Standard deviation multiplier", "Indicators")
			.SetGreaterThanZero()
			
			.SetOptimize(1m, 5m, 0.5m);

		_appliedPrice = Param(nameof(AppliedPrice), AppliedPriceTypes.Close)
			.SetDisplay("Applied Price", "Candle price source for Bollinger Bands", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to analyze", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, CandleType);
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_bollinger = null;
		_entryPrice = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		// Create Bollinger Bands indicator with the configured parameters.
		_bollinger = new BollingerBands
		{
			Length = BandsPeriod,
			Width = BandsDeviation
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _bollinger);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished || _bollinger is null)
			return;

		// Calculate Bollinger Bands for the selected price source.
		var indicatorValue = _bollinger.Process(new DecimalIndicatorValue(_bollinger, GetAppliedPrice(candle), candle.OpenTime) { IsFinal = true });

		if (!indicatorValue.IsFinal)
			return;

		if (indicatorValue is not BollingerBandsValue bands)
			return;

		if (bands.UpBand is not decimal upper || bands.LowBand is not decimal lower || bands.MovingAverage is not decimal middle)
			return;

		var high = candle.HighPrice;
		var low = candle.LowPrice;
		var close = candle.ClosePrice;

		// Determine breakout conditions based on Bollinger structure.
		var buySignal = low < lower && high < middle;
		var sellSignal = high > upper && low > middle;

		if (!buySignal && !sellSignal)
			return;

		// Compute unrealized profit to mimic original position filter.
		var unrealizedPnL = Position == 0 ? 0m : Position * (close - _entryPrice);

		if (buySignal)
		{
			if (Position == 0)
			{
				// No position open, start a new long.
				BuyMarket();
				_entryPrice = close;
			}
			else
			{
				if (unrealizedPnL < 0m)
					return;

				if (Position < 0)
				{
					// Reverse from short to long while preserving target volume.
					BuyMarket();
					_entryPrice = close;
				}
			}

			return;
		}

		if (sellSignal)
		{
			if (Position == 0)
			{
				// No position open, start a new short.
				SellMarket();
				_entryPrice = close;
			}
			else
			{
				if (unrealizedPnL < 0m)
					return;

				if (Position > 0)
				{
					// Reverse from long to short while preserving target volume.
					SellMarket();
					_entryPrice = close;
				}
			}
		}
	}

	private decimal GetAppliedPrice(ICandleMessage candle)
	{
		return AppliedPrice switch
		{
			AppliedPriceTypes.Close => candle.ClosePrice,
			AppliedPriceTypes.Open => candle.OpenPrice,
			AppliedPriceTypes.High => candle.HighPrice,
			AppliedPriceTypes.Low => candle.LowPrice,
			AppliedPriceTypes.Median => (candle.HighPrice + candle.LowPrice) / 2m,
			AppliedPriceTypes.Typical => (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m,
			AppliedPriceTypes.Weighted => (candle.HighPrice + candle.LowPrice + (2m * candle.ClosePrice)) / 4m,
			AppliedPriceTypes.Average => (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m,
			_ => candle.ClosePrice
		};
	}
}