Открыть на GitHub

Стратегия Boll Trade Breakout

Эта стратегия переносит эксперта BollTrade из MetaTrader в StockSharp. Она торгует пробои полос Боллинджера с настраиваемым буфером (в пунктах) и при необходимости масштабирует объём сделок в зависимости от баланса. Все решения о входе принимаются только по закрытым свечам, а позиции сопровождаются фиксированными уровнями стоп-лосса и тейк-профита.

Идея

  • Подписывается на выбранный таймфрейм и рассчитывает полосы Боллинджера с заданным периодом и отклонением.
  • Добавляет дополнительный отступ (Band Offset) поверх верхней и нижней полос, чтобы отфильтровать ложные импульсы.
  • Открывает покупку, когда свеча закрывается ниже нижней полосы минус заданный отступ.
  • Открывает продажу, когда свеча закрывается выше верхней полосы плюс отступ.
  • Одновременно может быть открыта только одна позиция; пока активная сделка не завершена, новые входы не рассматриваются.

Управление сделкой

  • После открытия позиции сразу вычисляются уровни стоп-лосса и тейк-профита (в пунктах) и проверяются на каждой закрытой свече. При достижении любого из уровней позиция закрывается рыночной заявкой.
  • Параметр Scale Volume включает динамическое изменение объёма: базовое значение рассчитывается из стартового баланса и размера лота, что полностью повторяет оригинальную реализацию в MQL. Максимальный объём ограничен 500 лотами для контроля риска.
  • Размер пункта вычисляется по PriceStep инструмента. Если шаг цены очень мал (как на форекс), он умножается на 10, чтобы получить стандартный пункт, аналогично тому, как это делалось в MetaTrader.

Параметры

Название Описание Значение по умолчанию
Candle Type Таймфрейм для расчёта сигналов. Свечи 15 минут
Bollinger Period Количество баров в расчёте полос Боллинджера. 4
Bollinger Deviation Коэффициент ширины полос Боллинджера. 2
Band Offset Дополнительный буфер в пунктах перед подтверждением сигнала. 3
Take Profit (pips) Дистанция до тейк-профита в пунктах. 3
Stop Loss (pips) Дистанция до стоп-лосса в пунктах. 20
Base Volume Базовый размер позиции в лотах при отключённом масштабировании. 1
Scale Volume Масштабирование объёма в зависимости от баланса. Включено

Рекомендации

  • Наиболее логичное применение — валютные пары и CFD, где понятие «пункт» естественно. Однако стратегия также работает на фьючерсах и акциях, если корректно задан PriceStep инструмента.
  • В расчёте участвуют только завершённые свечи, поэтому внутрибарабанные ложные пробои, которые откатывают до закрытия, игнорируются.
  • Параметры стопа и тейка фиксированы, поэтому их следует адаптировать под волатильность выбранного инструмента и таймфрейм.
  • В MetaTrader защита реализована через серверные ордера. В StockSharp те же уровни контролируются внутри стратегии путём проверки экстремумов свечей.

Содержимое

  • 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;
	}
}