Ver en GitHub

BB HeikinAshi Entry

Reversión de Bollinger sobre Heikin Ashi, con toma de beneficio ejecutada sobre la mitad de la posición y seguimiento del resto.

Reglas de negociación

Cierre HA: (Open+High+Low+Close)/4. Primera apertura HA: (Open+Close)/2; siguientes: (apertura HA anterior+cierre HA anterior)/2. Los extremos HA incluyen apertura/cierre HA y extremos originales. Bollinger usa cierres HA desde la primera vela archivada, no cierres originales. Doji no tiene color. Contacto: HA low <= lower / HA high >= upper.

Exactamente dos o tres velas HA bajistas consecutivas deben tocar cada una su banda inferior. La siguiente HA alcista debe cerrar estrictamente sobre su banda inferior. Una vela o cuatro o más no sirven. La condición simétrica abre cortos.

Entrada por Volume solo sin posición. Stop inicial en el mínimo/máximo de la vela original terminada anterior, fijado antes de ordenar. R es la distancia positiva entre el precio medio ponderado ejecutado y ese stop. A 1R se cierra la mitad ejecutada a mercado, redondeada hacia abajo por VolumeStep. Si cualquier mitad es inferior a MinVolume, se cierra todo en el objetivo. Una entrada ejecutada más allá del stop se cierra en la siguiente cotización ejecutable.

Solo una ejecución real del objetivo activa el seguimiento del resto por el mínimo/máximo de la última vela original terminada; nunca se afloja el stop. Bid para largos o ask para cortos activan la salida total entre velas, con prioridad del stop sobre el objetivo. Órdenes pendientes bloquean duplicados. No se usan extremos anteriores para activar niveles creados después. Sin objetivo porcentual fijo ni espera oculta.

Las entradas requieren velas terminadas e indicadores preparados. La posición se gestiona hasta su salida; las órdenes de mercado pendientes impiden envíos duplicados.

Bollinger usa la desviación estándar poblacional de la ventana móvil de cierres, incluida la vela terminada actual.

Parámetros

Parameter Default
BollingerLength 20
BollingerWidth 2
CandleType 15m
namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>Heikin Ashi band reversal with a real half-position target and a causal trailing remainder.</summary>
public class BbHeikinAshiEntryStrategy : Strategy
{
	private readonly StrategyParam<int> _bollingerLength;
	private readonly StrategyParam<decimal> _bollingerWidth;
	private readonly StrategyParam<DataType> _candleType;
	private BollingerBands _bands;
	private Order _pending;
	private bool _haInitialized, _targetTaken, _invalidFill;
	private decimal _haOpen, _haClose, _previousLow, _previousHigh;
	private decimal _stop, _target, _entryCost, _entryQuantity;
	private int _redTouches, _greenTouches;

	/// <summary>Initializes the published Heikin Ashi and Bollinger parameters.</summary>
	public BbHeikinAshiEntryStrategy()
	{
		_bollingerLength = Param(nameof(BollingerLength), 20).SetGreaterThanZero().SetOptimize(10, 40, 5)
			.SetDisplay("BollingerLength", "Heikin Ashi close window", "Parameters");
		_bollingerWidth = Param(nameof(BollingerWidth), 2m).SetGreaterThanZero().SetOptimize(1m, 3m, 0.5m)
			.SetDisplay("BollingerWidth", "Bollinger standard deviation multiplier", "Parameters");
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("CandleType", "Finished signal candle interval", "Parameters");
	}

	/// <summary>Heikin Ashi close window.</summary>
	public int BollingerLength
	{
		get => _bollingerLength.Value;
		set => _bollingerLength.Value = value;
	}

	/// <summary>Bollinger standard deviation multiplier.</summary>
	public decimal BollingerWidth
	{
		get => _bollingerWidth.Value;
		set => _bollingerWidth.Value = value;
	}

	/// <summary>Finished signal candle interval.</summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	private bool IsPending => _pending is not null && _pending.State is not (OrderStates.Done or OrderStates.Failed);

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType), (Security, DataType.Level1)];

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_bands = null;
		_pending = null;
		_haInitialized = _targetTaken = _invalidFill = false;
		_haOpen = _haClose = _previousLow = _previousHigh = 0m;
		_stop = _target = _entryCost = _entryQuantity = 0m;
		_redTouches = _greenTouches = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_bands = new BollingerBands { Length = BollingerLength, Width = BollingerWidth };
		Indicators.Add(_bands);
		var candles = SubscribeCandles(CandleType);
		candles.Bind(ProcessCandle).Start();
		var area = CreateChartArea();
		if (area is not null)
		{
			DrawCandles(area, candles);
			DrawIndicator(area, _bands);
			DrawOwnTrades(area);
		}
		foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
		{
			var quote = new Subscription(DataType.Level1, Security);
			quote.MarketData.BuildField = field;
			SubscribeLevel1(quote).Bind(ProcessQuote).Start();
		}
	}

	private void Submit(Sides side, decimal quantity, string reason)
	{
		_pending = CreateOrder(side, 0m, quantity);
		_pending.Type = OrderTypes.Market;
		_pending.Comment = "0555" + reason;
		RegisterOrder(_pending);
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;
		var open = _haInitialized ? (_haOpen + _haClose) / 2m : (candle.OpenPrice + candle.ClosePrice) / 2m;
		var close = (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m;
		_haOpen = open;
		_haClose = close;
		_haInitialized = true;
		var input = new DecimalIndicatorValue(_bands, close, candle.OpenTime) { IsFinal = true };
		var bands = (BollingerBandsValue)_bands.Process(input);
		Sides? signal = null;
		if (_bands.IsFormed && bands.LowBand is decimal lower && bands.UpBand is decimal upper)
		{
			var redValid = _redTouches >= 2 && _redTouches <= 3;
			var greenValid = _greenTouches >= 2 && _greenTouches <= 3;
			if (close > open && close > lower && redValid)
				signal = Sides.Buy;
			else if (close < open && close < upper && greenValid)
				signal = Sides.Sell;
			_redTouches = close < open && Math.Min(candle.LowPrice, Math.Min(open, close)) <= lower ? _redTouches + 1 : 0;
			_greenTouches = close > open && Math.Max(candle.HighPrice, Math.Max(open, close)) >= upper ? _greenTouches + 1 : 0;
		}
		if (Position != 0m && _targetTaken)
			_stop = Position > 0m ? Math.Max(_stop, candle.LowPrice) : Math.Min(_stop, candle.HighPrice);
		var setupStop = signal == Sides.Buy ? _previousLow : _previousHigh;
		_previousLow = candle.LowPrice;
		_previousHigh = candle.HighPrice;
		if (signal is null || Position != 0m || IsPending || !IsFormedAndOnlineAndAllowTrading())
			return;
		_stop = setupStop;
		_entryCost = _entryQuantity = 0m;
		_targetTaken = _invalidFill = false;
		Submit(signal.Value, Volume, "Entry");
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (trade.Order.Comment == "0555Entry")
		{
			_entryCost += trade.Trade.Price * trade.Trade.Volume;
			_entryQuantity += trade.Trade.Volume;
			var entry = _entryCost / _entryQuantity;
			var risk = trade.Order.Side == Sides.Buy ? entry - _stop : _stop - entry;
			_invalidFill = risk <= 0m;
			_target = trade.Order.Side == Sides.Buy ? entry + risk : entry - risk;
		}
		else if (trade.Order.Comment == "0555Target")
		{
			_targetTaken = true;
			if (Position != 0m)
				_stop = Position > 0m ? Math.Max(_stop, _previousLow) : Math.Min(_stop, _previousHigh);
		}
	}

	private void ProcessQuote(Level1ChangeMessage quote)
	{
		if (Position == 0m || _entryQuantity == 0m || IsPending || !IsFormedAndOnlineAndAllowTrading())
			return;
		var field = Position > 0m ? Level1Fields.BestBidPrice : Level1Fields.BestAskPrice;
		if (!quote.Changes.TryGetValue(field, out var raw) || raw is not decimal price || price <= 0m)
			return;
		var side = Position > 0m ? Sides.Sell : Sides.Buy;
		if (_invalidFill || (Position > 0m ? price <= _stop : price >= _stop))
			Submit(side, Math.Abs(Position), "Stop");
		else if (!_targetTaken && (Position > 0m ? price >= _target : price <= _target))
		{
			var quantity = Math.Abs(Position);
			var step = Security.VolumeStep ?? 1m;
			var minimum = Security.MinVolume ?? step;
			var half = Math.Floor(quantity / 2m / step) * step;
			// A sub-lot remainder cannot be managed: take the whole position at 1R instead.
			Submit(side, half >= minimum && quantity - half >= minimum ? half : quantity, "Target");
		}
	}
}