Ver no GitHub

Estratégia de Setas Doji

Conceito

A estratégia de Setas Doji converte o expert advisor original "Doji Arrows" do MetaTrader para a API de alto nível do StockSharp. A ideia é aguardar uma vela doji genuína e então operar um rompimento do seu range. Uma vela doji representa indecisão, portanto, um fechamento acima da máxima do doji indica força altista enquanto um fechamento abaixo da mínima do doji indica controle baixista.

  1. A estratégia processa apenas velas completadas da assinatura CandleType configurada.
  2. A vela anterior é analisada para determinar se é um doji. A vela é classificada como doji quando a diferença absoluta entre a abertura e o fechamento é menor ou igual a DojiBodyPoints multiplicado pelo passo de preço do instrumento. Se o parâmetro for definido como 0, um único passo de preço é usado como tolerância, o que corresponde à verificação de igualdade estrita na versão MQL5.
  3. Quando a próxima vela fecha acima da máxima do doji, a estratégia envia uma ordem de compra a mercado. Quando a próxima vela fecha abaixo da mínima do doji, uma ordem de venda a mercado é emitida. Posições opostas existentes são niveladas automaticamente pelo volume da ordem de mercado.

Esta sequência espelha o expert advisor original que reagia uma vez na abertura de cada nova barra.

Gerenciamento de Risco

A implementação convertida mantém o comportamento protetor do script MQL:

  • Stop loss: StopLossPoints controla quão longe, em passos de preço, o stop loss inicial é colocado do preço de entrada. Defina como zero para desabilitar o stop fixo.
  • Take profit: TakeProfitPoints define a distância até o objetivo de lucro em passos de preço. Defina como zero para pular o objetivo.
  • Trailing stop: TrailingStopPoints e TrailingStepPoints reproduzem o mecanismo de trailing. Uma vez que a operação ganha mais de TrailingStopPoints + TrailingStepPoints, o nível de stop é puxado para TrailingStopPoints do último fechamento (fechamento mais alto para comprado, fechamento mais baixo para vendido). O trailing é opcional e ativa apenas quando TrailingStopPoints é maior que zero.

Stops e objetivos são avaliados em cada vela terminada. Quando qualquer nível é violado (usando a máxima/mínima da vela), a estratégia sai da posição com uma ordem de mercado e reinicia o estado de proteção.

Parâmetros

Parâmetro Padrão Descrição
StopLossPoints 30 Distância do stop loss inicial em passos de preço.
TakeProfitPoints 90 Distância do take profit em passos de preço.
TrailingStopPoints 15 Distância usada pelo trailing stop em passos de preço.
TrailingStepPoints 5 Lucro adicional necessário antes de ajustar o trailing stop, em passos de preço.
DojiBodyPoints 1 Tamanho máximo permitido do corpo da vela anterior em passos de preço para tratá-la como um doji. 0 usa um passo de preço como tolerância.
CandleType 1 hora Tipo de vela assinada para geração de sinais.

Notas de Implementação

  • A estratégia assina velas através de SubscribeCandles(CandleType).Bind(ProcessCandle) e mantém apenas a última vela completada na memória.
  • O passo de preço do instrumento é obtido via Security?.PriceStep. Quando não disponível, um valor de fallback de 1 é usado para que a estratégia possa operar igualmente com dados sintéticos ou históricos.
  • Os níveis protetores são recalculados após cada entrada, e a lógica de trailing pode criar um stop mesmo quando o stop loss fixo está desabilitado (correspondendo ao comportamento MQL onde o trailing stop podia começar de zero).
  • Todas as ações são executadas com ordens de mercado para manter alinhamento com o advisor original que dependia de execução imediata no mercado.

Dicas de Uso

  1. Configure as propriedades Security, Portfolio e Volume antes de iniciar a estratégia.
  2. Ajuste os parâmetros baseados em pontos de acordo com o instrumento operado. Para instrumentos cotados com pips fracionários, aumente os valores para corresponder ao tamanho do tick do broker.
  3. Combine a estratégia com controles de risco ou módulos de análise do StockSharp se for necessário dimensionamento de posição mais avançado, pois a conversão mantém a lógica de volume fixo do código original.
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>
/// Doji breakout strategy with optional fixed and trailing protection.
/// </summary>
public class DojiArrowsStrategy : Strategy
{
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<decimal> _dojiBodyPoints;
	private readonly StrategyParam<DataType> _candleType;

	private bool _hasPreviousCandle;
	private decimal _prevOpen;
	private decimal _prevClose;
	private decimal _prevHigh;
	private decimal _prevLow;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;

	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	public decimal DojiBodyPoints
	{
		get => _dojiBodyPoints.Value;
		set => _dojiBodyPoints.Value = value;
	}

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

	public DojiArrowsStrategy()
	{
		_stopLossPoints = Param(nameof(StopLossPoints), 30m)
			.SetNotNegative()
			.SetDisplay("Stop Loss Points", "Stop loss distance in price steps.", "Risk")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 90m)
			.SetNotNegative()
			.SetDisplay("Take Profit Points", "Take profit distance in price steps.", "Risk")
			;

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 15m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop Points", "Trailing distance in price steps.", "Risk")
			;

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Step Points", "Minimum profit before the trailing stop moves.", "Risk")
			;

		_dojiBodyPoints = Param(nameof(DojiBodyPoints), 1m)
			.SetNotNegative()
			.SetDisplay("Doji Body Points", "Maximum difference between open and close to treat the candle as a doji.", "Pattern")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used for signal generation.", "General");
	}

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

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

		_hasPreviousCandle = false;
		_prevOpen = 0m;
		_prevClose = 0m;
		_prevHigh = 0m;
		_prevLow = 0m;
		ResetProtection();
	}

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

		SubscribeCandles(CandleType)
			.Bind(ProcessCandle)
			.Start();

		StartProtection(
			takeProfit: new Unit(2, UnitTypes.Percent),
			stopLoss: new Unit(1, UnitTypes.Percent)
		);
	}

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

		ManageActivePosition(candle);

		if (!_hasPreviousCandle)
		{
			CachePreviousCandle(candle);
			return;
		}

		var step = Security?.PriceStep ?? 1m;
		var tolerance = DojiBodyPoints <= 0m ? step : DojiBodyPoints * step;
		var bodySize = Math.Abs(_prevOpen - _prevClose);
		var isDoji = bodySize <= tolerance;

		var breakoutUp = isDoji && candle.ClosePrice > _prevHigh;
		var breakoutDown = isDoji && candle.ClosePrice < _prevLow;

		if (breakoutUp && Position == 0)
		{
			BuyMarket();
		}
		else if (breakoutDown && Position == 0)
		{
			SellMarket();
		}

		CachePreviousCandle(candle);
	}

	private void ManageActivePosition(ICandleMessage candle)
	{
		if (Position == 0)
			return;

		var step = Security?.PriceStep ?? 1m;
		var trailingDistance = TrailingStopPoints > 0m ? TrailingStopPoints * step : 0m;
		var trailingStep = TrailingStepPoints > 0m ? TrailingStepPoints * step : 0m;

		if (Position > 0)
		{
			if (trailingDistance > 0m && _entryPrice.HasValue)
			{
				var gain = candle.ClosePrice - _entryPrice.Value;

				if (gain > trailingDistance + trailingStep)
				{
					var newStop = candle.ClosePrice - trailingDistance;

					if (!_stopPrice.HasValue || newStop > _stopPrice.Value)
						_stopPrice = newStop;
				}
			}

			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetProtection();
				return;
			}

			if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetProtection();
				return;
			}
		}
		else if (Position < 0)
		{
			if (trailingDistance > 0m && _entryPrice.HasValue)
			{
				var gain = _entryPrice.Value - candle.ClosePrice;

				if (gain > trailingDistance + trailingStep)
				{
					var newStop = candle.ClosePrice + trailingDistance;

					if (!_stopPrice.HasValue || newStop < _stopPrice.Value)
						_stopPrice = newStop;
				}
			}

			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetProtection();
				return;
			}

			if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetProtection();
				return;
			}
		}
	}

	private void InitializeProtection(decimal price, bool isLong, decimal step)
	{
		_entryPrice = price;

		if (StopLossPoints > 0m)
		{
			var offset = StopLossPoints * step;
			_stopPrice = isLong ? price - offset : price + offset;
		}
		else
		{
			_stopPrice = null;
		}

		if (TakeProfitPoints > 0m)
		{
			var offset = TakeProfitPoints * step;
			_takePrice = isLong ? price + offset : price - offset;
		}
		else
		{
			_takePrice = null;
		}
	}

	private void ResetProtection()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
	}

	private void CachePreviousCandle(ICandleMessage candle)
	{
		_prevOpen = candle.OpenPrice;
		_prevClose = candle.ClosePrice;
		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
		_hasPreviousCandle = true;
	}
}