Ver en GitHub

Estrategia de bandas

Descripción general

Esta estrategia transfiere el asesor experto MetaTrader 5 Bands.mq5 al API de alto nivel de StockSharp. Espera una vela terminada que perfora las bandas Bollinger desde el exterior hacia el canal y solo abre una posición cuando el canal Donchian se configura Confirma que la pendiente de la banda ha sido estable durante un número configurable de barras. Los múltiplos del rango verdadero promedio (ATR) reproducen el ori distancias iniciales de stop-loss y take-profit, mientras que un rastreador de regresión opcional imprime el coeficiente de determinación de la curva de acciones (R cuadrado) cada 100 operaciones, reflejando el resultado de diagnóstico de la versión MQL.

Lógica comercial

  1. Suscríbase a un flujo de velas único y calcule Bollinger Bandas, un Donchian Canal y ATR con los mismos períodos que el MetaT. robot de radar.
  2. Cuando no haya ninguna posición abierta, inspeccione la vela anterior completada:
    • Ingrese en largo si esa vela se abrió por debajo de la banda inferior Bollinger y cerró por encima de ella, y la banda inferior Donchian no ha disminuido. ined durante más de ConfirmationPeriod barras.
    • Entre en corto si la vela se abrió por encima de la banda superior Bollinger y cerró por debajo de ella, y la banda superior Donchian no ha subido. es para más de ConfirmationPeriod barras.
  3. Cuando exista una posición, salga si se cruza el límite final Donchian (utilizando el cierre anterior) o si la base ATR Se violan los niveles d proteccion intrabar.
  4. Cada operación ejecutada almacena el capital de la cartera actual e imprime la métrica R cuadrado de regresión lineal después de cada bloque de 100 operaciones. Una pendiente negativa produce un R cuadrado negativo al igual que el asesor experto original.

Gestión de riesgos

  • Las órdenes de entrada siempre se envían al mercado con el TradeVolume definido por el usuario.
  • Los niveles de protección se recrean en el código (en lugar de utilizar órdenes pendientes) comparando los máximos y mínimos de las velas con el ATR mu. tiples.
  • Cuando se activa el stop-loss o el take-profit, la estrategia cierra toda la posición con una orden de mercado y restablece la protección. en niveles.

Parámetros

Parámetro Descripción
TradeVolume Volumen neto (en lotes) para cada orden de mercado.
CandleType Tipo de datos de vela/período de tiempo utilizado para todos los indicadores.
BollingerPeriod Número de velas utilizadas por las Bollinger Bandas.
BollingerDeviation Multiplicador de desviación estándar aplicado a las Bollinger Bandas.
DonchianPeriod Longitud del canal Donchian utilizado como filtro de tendencias.
ConfirmationPeriod Recuento mínimo de barras consecutivas que deben mantener la pendiente Donchian no decreciente (larga) ni no creciente (corta).
AtrPeriod Período del Rango Verdadero Promedio utilizado para la gestión de riesgos.
StopAtrMultiplier ATR múltiplo que define la distancia del stop-loss.
TakeAtrMultiplier ATR múltiplo que define la distancia de toma de ganancias.

Notas

  • La verificación de pendiente Donchian se implementa como un contador rodante en lugar de copiar los buffers del indicador, lo que mantiene el StockSharp versión eficiente y al mismo tiempo coincide con el comportamiento del EA original.
  • Todos los comentarios y diagnósticos se proporcionan en inglés según lo exigen las directrices del proyecto.
  • Los ayudantes de administración de dinero del código MetaTrader no se reproducen; la implementación de StockSharp se basa en TradeVolume parámetro para dimensionar la posición.
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 confirmed by Donchian channel slope and ATR-based risk management.
/// </summary>
public class BandsStrategy : Strategy
{
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _bollingerPeriod;
	private readonly StrategyParam<decimal> _bollingerDeviation;
	private readonly StrategyParam<int> _donchianPeriod;
	private readonly StrategyParam<int> _confirmationPeriod;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _stopAtrMultiplier;
	private readonly StrategyParam<decimal> _takeAtrMultiplier;

	private decimal? _prevOpen;
	private decimal? _prevClose;
	private decimal? _prevLowerBand;
	private decimal? _prevUpperBand;
	private decimal? _prevDonchLower;
	private decimal? _prevDonchUpper;
	private decimal? _prevAtr;

	private int _lowerTrendLength;
	private int _upperTrendLength;

	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;

	private int _equitySamples;
	private decimal _sumIndices;
	private decimal _sumEquity;
	private decimal _sumIndexEquity;
	private decimal _sumIndexSquared;
	private decimal _sumEquitySquared;

	/// <summary>
	/// Initializes a new instance of <see cref="BandsStrategy"/>.
	/// </summary>
	public BandsStrategy()
	{
		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Net volume in lots sent with every order", "Trading")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used for indicator calculations", "Market Data");

		_bollingerPeriod = Param(nameof(BollingerPeriod), 100)
			.SetGreaterThanZero()
			.SetDisplay("Bollinger Period", "Number of candles used for the Bollinger Bands", "Indicators")
			;

		_bollingerDeviation = Param(nameof(BollingerDeviation), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Bollinger Deviation", "Standard deviation multiplier for the Bollinger Bands", "Indicators")
			;

		_donchianPeriod = Param(nameof(DonchianPeriod), 100)
			.SetGreaterThanZero()
			.SetDisplay("Donchian Period", "Donchian Channel length used as trend filter", "Indicators")
			;

		_confirmationPeriod = Param(nameof(ConfirmationPeriod), 100)
			.SetGreaterThanZero()
			.SetDisplay("Slope Confirmation", "Minimum number of bars that must keep the Donchian slope intact", "Indicators")
			;

		_atrPeriod = Param(nameof(AtrPeriod), 21)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "Length of the Average True Range used for stops", "Indicators")
			;

		_stopAtrMultiplier = Param(nameof(StopAtrMultiplier), 4m)
			.SetGreaterThanZero()
			.SetDisplay("Stop ATR Multiplier", "How many ATRs below/above the entry to place the stop", "Risk")
			;

		_takeAtrMultiplier = Param(nameof(TakeAtrMultiplier), 4m)
			.SetGreaterThanZero()
			.SetDisplay("Take ATR Multiplier", "How many ATRs below/above the entry to place the target", "Risk")
			;
	}

	/// <summary>
	/// Trade volume in lots.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Candle type used for analysis.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Period of the Bollinger Bands.
	/// </summary>
	public int BollingerPeriod
	{
		get => _bollingerPeriod.Value;
		set => _bollingerPeriod.Value = value;
	}

	/// <summary>
	/// Deviation multiplier of the Bollinger Bands.
	/// </summary>
	public decimal BollingerDeviation
	{
		get => _bollingerDeviation.Value;
		set => _bollingerDeviation.Value = value;
	}

	/// <summary>
	/// Period of the Donchian Channel.
	/// </summary>
	public int DonchianPeriod
	{
		get => _donchianPeriod.Value;
		set => _donchianPeriod.Value = value;
	}

	/// <summary>
	/// Number of consecutive bars required to confirm the Donchian slope.
	/// </summary>
	public int ConfirmationPeriod
	{
		get => _confirmationPeriod.Value;
		set => _confirmationPeriod.Value = value;
	}

	/// <summary>
	/// Period of the Average True Range indicator.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in ATR multiples.
	/// </summary>
	public decimal StopAtrMultiplier
	{
		get => _stopAtrMultiplier.Value;
		set => _stopAtrMultiplier.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in ATR multiples.
	/// </summary>
	public decimal TakeAtrMultiplier
	{
		get => _takeAtrMultiplier.Value;
		set => _takeAtrMultiplier.Value = value;
	}

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

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

		_prevOpen = null;
		_prevClose = null;
		_prevLowerBand = null;
		_prevUpperBand = null;
		_prevDonchLower = null;
		_prevDonchUpper = null;
		_prevAtr = null;
		_lowerTrendLength = 0;
		_upperTrendLength = 0;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_equitySamples = 0;
		_sumIndices = 0m;
		_sumEquity = 0m;
		_sumIndexEquity = 0m;
		_sumIndexSquared = 0m;
		_sumEquitySquared = 0m;
	}

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

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

		var atr = new AverageTrueRange
		{
			Length = AtrPeriod
		};

		var donchian = new DonchianChannels
		{
			Length = DonchianPeriod
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(bollinger, atr, donchian, ProcessCandle)
			.Start();
	}

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

		if (!bollingerVal.IsFormed || !atrVal.IsFormed || !donchianVal.IsFormed)
			return;

		var bollingerComplex = (ComplexIndicatorValue<BollingerBands>)bollingerVal;
		var middle = bollingerComplex.InnerValues.ElementAt(0).Value.GetValue<decimal>();
		var upper = bollingerComplex.InnerValues.ElementAt(1).Value.GetValue<decimal>();
		var lower = bollingerComplex.InnerValues.ElementAt(2).Value.GetValue<decimal>();
		var atrValue = atrVal.GetValue<decimal>();
		var donchianComplex = (ComplexIndicatorValue<DonchianChannels>)donchianVal;
		var donchUpper = donchianComplex.InnerValues.ElementAt(0).Value.GetValue<decimal>();
		var donchLower = donchianComplex.InnerValues.ElementAt(1).Value.GetValue<decimal>();

		var lowerTrendLength = CalculateLowerTrendLength(donchLower);
		var upperTrendLength = CalculateUpperTrendLength(donchUpper);

		if (!_prevOpen.HasValue)
		{
			CachePreviousValues(candle, lower, upper, donchLower, donchUpper, atrValue, lowerTrendLength, upperTrendLength);
			return;
		}

		var previousOpen = _prevOpen.Value;
		var previousClose = _prevClose!.Value;
		var previousLowerBand = _prevLowerBand!.Value;
		var previousUpperBand = _prevUpperBand!.Value;
		var previousDonchLower = _prevDonchLower!.Value;
		var previousDonchUpper = _prevDonchUpper!.Value;
		var atrForStops = _prevAtr ?? atrValue;

		if (Position == 0m)
		{
			if (previousOpen < previousLowerBand && previousClose > previousLowerBand && lowerTrendLength > ConfirmationPeriod)
			{
				OpenLong(candle.ClosePrice, atrForStops);
			}
			else if (previousOpen > previousUpperBand && previousClose < previousUpperBand && upperTrendLength > ConfirmationPeriod)
			{
				OpenShort(candle.ClosePrice, atrForStops);
			}
		}
		else if (Position > 0m)
		{
			var exitVolume = Position;
			var stopTriggered = _stopLossPrice is decimal stop && candle.LowPrice <= stop;
			var takeTriggered = _takeProfitPrice is decimal take && candle.HighPrice >= take;

			if (stopTriggered || takeTriggered || previousClose > previousDonchUpper || previousClose < previousDonchLower)
			{
				SellMarket(exitVolume);
				ClearProtection();
			}
		}
		else if (Position < 0m)
		{
			var exitVolume = Math.Abs(Position);
			var stopTriggered = _stopLossPrice is decimal stop && candle.HighPrice >= stop;
			var takeTriggered = _takeProfitPrice is decimal take && candle.LowPrice <= take;

			if (stopTriggered || takeTriggered || previousClose < previousDonchLower || previousClose > previousDonchUpper)
			{
				BuyMarket(exitVolume);
				ClearProtection();
			}
		}

		CachePreviousValues(candle, lower, upper, donchLower, donchUpper, atrValue, lowerTrendLength, upperTrendLength);
	}

	private int CalculateLowerTrendLength(decimal currentLower)
	{
		if (_prevDonchLower is decimal prevLower)
		{
			return currentLower >= prevLower ? _lowerTrendLength + 1 : 1;
		}

		return 1;
	}

	private int CalculateUpperTrendLength(decimal currentUpper)
	{
		if (_prevDonchUpper is decimal prevUpper)
		{
			return currentUpper <= prevUpper ? _upperTrendLength + 1 : 1;
		}

		return 1;
	}

	private void CachePreviousValues(ICandleMessage candle, decimal lower, decimal upper, decimal donchLower, decimal donchUpper, decimal atrValue, int lowerTrendLength, int upperTrendLength)
	{
		_prevOpen = candle.OpenPrice;
		_prevClose = candle.ClosePrice;
		_prevLowerBand = lower;
		_prevUpperBand = upper;
		_prevDonchLower = donchLower;
		_prevDonchUpper = donchUpper;
		_prevAtr = atrValue;

		_lowerTrendLength = lowerTrendLength;
		_upperTrendLength = upperTrendLength;
	}

	private void OpenLong(decimal entryPrice, decimal atrValue)
	{
		var volume = TradeVolume;
		if (volume <= 0m)
			return;

		BuyMarket(volume);
		AssignProtection(entryPrice, atrValue, true);
	}

	private void OpenShort(decimal entryPrice, decimal atrValue)
	{
		var volume = TradeVolume;
		if (volume <= 0m)
			return;

		SellMarket(volume);
		AssignProtection(entryPrice, atrValue, false);
	}

	private void AssignProtection(decimal entryPrice, decimal atrValue, bool isLong)
	{
		if (atrValue <= 0m)
		{
			ClearProtection();
			return;
		}

		var stopDistance = atrValue * StopAtrMultiplier;
		var takeDistance = atrValue * TakeAtrMultiplier;

		if (isLong)
		{
			_stopLossPrice = entryPrice - stopDistance;
			_takeProfitPrice = entryPrice + takeDistance;
		}
		else
		{
			_stopLossPrice = entryPrice + stopDistance;
			_takeProfitPrice = entryPrice - takeDistance;
		}
	}

	private void ClearProtection()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		var portfolio = Portfolio;
		if (portfolio == null)
			return;

		UpdateEquityStatistics(portfolio.CurrentValue ?? 0m);
	}

	private void UpdateEquityStatistics(decimal equity)
	{
		var index = (decimal)_equitySamples;
		_sumIndices += index;
		_sumEquity += equity;
		_sumIndexEquity += index * equity;
		_sumIndexSquared += index * index;
		_sumEquitySquared += equity * equity;
		_equitySamples++;

		if (_equitySamples % 100 != 0)
			return;

		var n = (decimal)_equitySamples;
		if (n <= 1m)
			return;

		var denominator = n * _sumIndexSquared - _sumIndices * _sumIndices;
		if (denominator == 0m)
			return;

		var slope = (n * _sumIndexEquity - _sumIndices * _sumEquity) / denominator;
		var mean = _sumEquity / n;
		var ssTotal = _sumEquitySquared - n * mean * mean;

		if (ssTotal == 0m)
		{
			LogInfo("Equity R-squared: 1.0000");
			return;
		}

		var regressionComponent = slope * (_sumIndexEquity - (_sumIndices / n) * _sumEquity);
		var rSquared = regressionComponent / ssTotal;

		if (slope < 0m)
			rSquared = -rSquared;

		LogInfo($"Equity R-squared: {rSquared:F4}");
	}
}