在 GitHub 上查看

Boll Trade Breakout 策略

该策略将 MetaTrader 上的 BollTrade EA 移植到 StockSharp 平台,通过布林带突破信号进行交易,并提供可配置 的点值缓冲与可选的资金规模管理。策略仅在 K 线收盘后作出决策,并使用固定的止损与止盈价位管理仓位。

核心逻辑

  • 订阅所选的主图时间框架,根据参数计算布林带(周期与标准差)。
  • 在上轨和下轨外侧再加上 Band Offset 指定的点数缓冲,减少虚假突破。
  • 当收盘价低于下轨减去缓冲时开多单;当收盘价高于上轨加上缓冲时开空单。
  • 始终只持有一个方向的仓位,新的信号会等待当前交易结束后再执行。

仓位管理

  • 开仓后立即根据点数参数计算止损与止盈水平,在每根完成的 K 线上检查最高/最低价是否触及目标,一旦触发 则通过市价单平仓。
  • 如果启用 Scale Volume,策略会按照账户权益与初始权益的比例调整下次下单的手数,最大不超过 500 手,以 保持与原始 EA 相同的风险限制。
  • 点值根据 PriceStep 自动推导。对于价格步长非常小的外汇品种,会将步长乘以 10,以得到与 MetaTrader 相同的标准点。

参数说明

参数 说明 默认值
Candle Type 用于生成信号的 K 线类型/周期。 15 分钟 K 线
Bollinger Period 布林带计算周期。 4
Bollinger Deviation 布林带宽度系数。 2
Band Offset 在触发信号前需要突破的额外点数。 3
Take Profit (pips) 止盈距离(点)。 3
Stop Loss (pips) 止损距离(点)。 20
Base Volume 未启用规模管理时的基础手数。 1
Scale Volume 是否根据账户权益自动调整手数。 开启

使用建议

  • 适用于外汇、差价合约等以点为主要波动单位的市场;若在期货或股票上使用,请确保正确配置品种的 PriceStep
  • 策略仅基于收盘价,因此盘中突破后回落的情况不会立即触发交易。
  • 固定的止损/止盈需要根据所选品种的波动性进行调整,以避免过度紧或过度宽松。
  • 原版 EA 使用服务器端的止损与止盈,本移植版本通过检查 K 线的最高价和最低价来模拟相同的保护逻辑。

文件结构

  • CS/BollTradeStrategy.cs —— C# 策略实现。
  • README.md —— 英文说明。
  • README_ru.md —— 俄文说明。
  • README_zh.md —— 中文说明(当前文件)。

按要求暂未提供 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 with optional balance-based position sizing.
/// </summary>
public class BollTradeStrategy : Strategy
{
	private readonly StrategyParam<decimal> _maxVolume;

	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _bandOffset;
	private readonly StrategyParam<int> _bollingerPeriod;
	private readonly StrategyParam<decimal> _bollingerDeviation;
	private readonly StrategyParam<decimal> _lots;
	private readonly StrategyParam<bool> _lotIncrease;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _lotBaseline;
	private decimal _pipSize;
	private BollingerBands _bollinger;
	private decimal? _longStop;
	private decimal? _longTarget;
	private decimal? _shortStop;
	private decimal? _shortTarget;

	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	public decimal BollingerDistance
	{
		get => _bandOffset.Value;
		set => _bandOffset.Value = value;
	}

	public int BollingerPeriod
	{
		get => _bollingerPeriod.Value;
		set => _bollingerPeriod.Value = value;
	}

	public decimal BollingerDeviation
	{
		get => _bollingerDeviation.Value;
		set => _bollingerDeviation.Value = value;
	}

	public decimal Lots
	{
		get => _lots.Value;
		set => _lots.Value = value;
	}

	public bool LotIncrease
	{
		get => _lotIncrease.Value;
		set => _lotIncrease.Value = value;
	}

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

	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	public BollTradeStrategy()
	{
		_maxVolume = Param(nameof(MaxVolume), 500m)
			.SetGreaterThanZero()
			.SetDisplay("Max Volume", "Upper bound for scaled volume", "Money Management");

		_takeProfit = Param(nameof(TakeProfit), 3m)
		.SetNotNegative()
		.SetDisplay("Take Profit (pips)", "Distance to take profit expressed in pip units.", "Orders")
		;

		_stopLoss = Param(nameof(StopLoss), 20m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (pips)", "Distance to stop loss expressed in pip units.", "Orders")
		;

		_bandOffset = Param(nameof(BollingerDistance), 0m)
		.SetNotNegative()
		.SetDisplay("Band Offset", "Extra pip offset beyond Bollinger Bands.", "Signals")
		;

		_bollingerPeriod = Param(nameof(BollingerPeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("Bollinger Period", "Length of the Bollinger Bands.", "Signals")
		;

		_bollingerDeviation = Param(nameof(BollingerDeviation), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Bollinger Deviation", "Width multiplier of the Bollinger Bands.", "Signals")
		;

		_lots = Param(nameof(Lots), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Base Volume", "Default trade volume in lots.", "Money Management");

		_lotIncrease = Param(nameof(LotIncrease), true)
		.SetDisplay("Scale Volume", "Increase volume with balance growth.", "Money Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Primary timeframe for signals.", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_lotBaseline = 0m;
		_pipSize = 0m;
		_bollinger = null!;
		ResetStops();
	}

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

		Volume = Lots;

		_pipSize = CalculatePipSize();
		_lotBaseline = 0m;

		if (LotIncrease && Lots > 0m)
		{
			var balance = Portfolio?.CurrentValue ?? 0m;

			if (balance > 0m)
			_lotBaseline = balance / Lots;
		}

		var bollinger = new BollingerBands
		{
			Length = BollingerPeriod,
			Width = BollingerDeviation
		};

		var subscription = SubscribeCandles(CandleType);

		subscription
		.BindEx(bollinger, ProcessCandle)
		.Start();

		_bollinger = bollinger;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;

		if (step <= 0m)
		step = 1m;

		if (step < 0.01m)
		step *= 10m;

		return step;
	}

	private decimal CalculateVolume()
	{
		var baseVolume = Lots;

		if (!LotIncrease || _lotBaseline <= 0m)
		return baseVolume;

		var balance = Portfolio?.CurrentValue ?? 0m;

		if (balance <= 0m)
		return baseVolume;

		var scaled = baseVolume * (balance / _lotBaseline);

		return Math.Min(scaled, MaxVolume);
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue value)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (value is not BollingerBandsValue bollingerValue)
			return;

		if (bollingerValue.MovingAverage is not decimal middleBand ||
			bollingerValue.UpBand is not decimal upperBand ||
			bollingerValue.LowBand is not decimal lowerBand)
			return;

		var offset = _pipSize * BollingerDistance;
		var upperThreshold = upperBand + offset;
		var lowerThreshold = lowerBand - offset;

		var shouldBuy = candle.ClosePrice < lowerThreshold;
		var shouldSell = candle.ClosePrice > upperThreshold;

		if (Position == 0)
		{
			if (shouldBuy)
			{
				EnterLong(candle);
			}
			else if (shouldSell)
			{
				EnterShort(candle);
			}

			return;
		}

		if (Position > 0)
		{
			// Close long positions when stop loss or take profit levels are hit.
			if ((_longStop.HasValue && candle.LowPrice <= _longStop.Value) ||
				(_longTarget.HasValue && candle.HighPrice >= _longTarget.Value))
			{
				SellMarket();
				ResetStops();
			}
		}
		else if (Position < 0)
		{
			// Close short positions when stop loss or take profit levels are hit.
			if ((_shortStop.HasValue && candle.HighPrice >= _shortStop.Value) ||
				(_shortTarget.HasValue && candle.LowPrice <= _shortTarget.Value))
			{
				BuyMarket();
				ResetStops();
			}
		}
	}

	private void EnterLong(ICandleMessage candle)
	{
		var volume = CalculateVolume();

		if (volume <= 0m)
			return;

		BuyMarket(volume);

		// Store exit targets for the newly opened long trade.
		_longStop = StopLoss > 0m ? candle.ClosePrice - _pipSize * StopLoss : null;
		_longTarget = TakeProfit > 0m ? candle.ClosePrice + _pipSize * TakeProfit : null;
		_shortStop = null;
		_shortTarget = null;
	}

	private void EnterShort(ICandleMessage candle)
	{
		var volume = CalculateVolume();

		if (volume <= 0m)
			return;

		SellMarket(volume);

		// Store exit targets for the newly opened short trade.
		_shortStop = StopLoss > 0m ? candle.ClosePrice + _pipSize * StopLoss : null;
		_shortTarget = TakeProfit > 0m ? candle.ClosePrice - _pipSize * TakeProfit : null;
		_longStop = null;
		_longTarget = null;
	}

	private void ResetStops()
	{
		// Clear cached exit levels after a position is closed.
		_longStop = null;
		_longTarget = null;
		_shortStop = null;
		_shortTarget = null;
	}
}