GitHub で見る

Doji Trader Strategy

The strategy replicates the core logic of the classic Doji Trader expert advisor. It monitors completed candles for compact-bodied doji patterns and waits for a breakout close beyond the doji range to enter the market in the breakout direction.

Trading Logic

  1. Only finished candles are processed. The default timeframe is 1 hour, but it can be adjusted through the CandleType parameter.
  2. Trading is allowed only when the closing time of the latest candle falls within the configurable session window [StartHour, EndHour) measured in exchange time.
  3. The algorithm keeps the three most recent finished candles in memory. The candle that just closed is compared against the two candles that came before it (-2 and -3).
  4. A candle counts as a doji when the absolute difference between its open and close is lower than MaximumDojiHeight * pip, where the pip value is derived from the security price step (3- or 5-digit quotes are automatically scaled by ×10).
  5. If the newest candle closes above the high of the most recent qualifying doji, the strategy opens (or flips into) a long position. If it closes below the doji low, it opens a short position. No trade is placed when price remains inside the doji range.
  6. The position size is taken from the strategy Volume property. When a reversal signal appears, the algorithm sends enough volume to close the previous position and establish the desired exposure in the new direction so only one net position stays open.

Risk Management

  • Stop-loss and take-profit distances are configured in pips through StopLossPips and TakeProfitPips. Setting a value to zero disables the corresponding protective order.
  • StartProtection is launched once at startup and uses market orders for exits so the behaviour mirrors the MQL implementation that closed and reopened positions directly.

Parameters

Name Description Default
CandleType Timeframe of processed candles. 1 hour time frame
StartHour Inclusive opening hour of the trading window. 8
EndHour Exclusive closing hour of the trading window. 17
MaximumDojiHeight Maximum body height (in pips) for a candle to be treated as a doji. 1
StopLossPips Protective stop distance in pips. 50
TakeProfitPips Profit target distance in pips. 50

Additional Notes

  • The strategy assumes the platform account uses netted positions. If your feed provides fractional pip steps (5-digit or 3-digit quotes), the pip value is multiplied by 10 to match traditional pip measurements.
  • Set the desired lot size in the Volume property before running the strategy.
  • No additional indicators are required; the logic depends only on raw candle data.
  • There is no Python port yet per request, only the C# implementation.
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy inspired by the Doji Trader Expert Advisor.
/// Looks for a recent doji candle and trades when the next candle closes beyond the doji range.
/// </summary>
public class DojiTraderStrategy : Strategy
{
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<decimal> _maximumDojiHeight;
	private readonly StrategyParam<DataType> _candleType;

	private ICandleMessage _previousCandle;
	private ICandleMessage _twoAgoCandle;
	private ICandleMessage _threeAgoCandle;
	private decimal _pipSize;

	/// <summary>
	/// Stop-loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// First trading hour (inclusive) using exchange time.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Last trading hour (exclusive) using exchange time.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Maximum body height for a candle to be considered a doji (in pips).
	/// </summary>
	public decimal MaximumDojiHeight
	{
		get => _maximumDojiHeight.Value;
		set => _maximumDojiHeight.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="DojiTraderStrategy"/>.
	/// </summary>
	public DojiTraderStrategy()
	{
		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetDisplay("Stop Loss", "Stop-loss distance in pips", "Protection")
			.SetRange(0m, 500m);

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetDisplay("Take Profit", "Take-profit distance in pips", "Protection")
			.SetRange(0m, 500m);

		_startHour = Param(nameof(StartHour), 8)
			.SetDisplay("Start Hour", "Hour when trading becomes active", "Session")
			.SetRange(0, 23);

		_endHour = Param(nameof(EndHour), 17)
			.SetDisplay("End Hour", "Hour when trading stops (exclusive)", "Session")
			.SetRange(1, 24);

		_maximumDojiHeight = Param(nameof(MaximumDojiHeight), 1m)
			.SetDisplay("Doji Height", "Maximum doji body height in pips", "Pattern")
			.SetRange(0.1m, 20m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for doji detection", "General");
	}

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

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

		_previousCandle = null;
		_twoAgoCandle = null;
		_threeAgoCandle = null;
		_pipSize = 0m;
	}

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

		_pipSize = CalculatePipSize();

		// Configure stop-loss and take-profit protection once at start.
		var takeProfitUnit = TakeProfitPips > 0m ? new Unit(TakeProfitPips * _pipSize, UnitTypes.Absolute) : default;
		var stopLossUnit = StopLossPips > 0m ? new Unit(StopLossPips * _pipSize, UnitTypes.Absolute) : default;

		if (takeProfitUnit != default || stopLossUnit != default)
		{
			StartProtection(takeProfitUnit, stopLossUnit, useMarketOrders: true);
		}
		else
		{
			StartProtection(null, null);
		}

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Process only finished candles.
		if (candle.State != CandleStates.Finished)
			return;

		// Skip trading outside the configured session window.
		var nextHour = candle.CloseTime.Hour;
		if (nextHour < StartHour || nextHour >= EndHour)
		{
			ShiftHistory(candle);
			return;
		}

		// We need at least three completed candles for pattern detection.
		if (_twoAgoCandle is null)
		{
			ShiftHistory(candle);
			return;
		}

		var pipSize = _pipSize > 0m ? _pipSize : (_pipSize = CalculatePipSize());
		var dojiHeight = MaximumDojiHeight * pipSize;

		var dojiHigh = 0m;
		var dojiLow = 0m;

		// Check the two candles before the current close for the most recent doji.
		if (IsDoji(_twoAgoCandle, dojiHeight))
		{
			dojiHigh = _twoAgoCandle.HighPrice;
			dojiLow = _twoAgoCandle.LowPrice;
		}
		else if (_threeAgoCandle is not null && IsDoji(_threeAgoCandle, dojiHeight))
		{
			dojiHigh = _threeAgoCandle.HighPrice;
			dojiLow = _threeAgoCandle.LowPrice;
		}
		else
		{
			ShiftHistory(candle);
			return;
		}

		var direction = 0;

		// Long signal when the latest candle closes above the doji range.
		if (candle.ClosePrice > dojiHigh)
		{
			direction = 1;
		}
		// Short signal when the latest candle closes below the doji range.
		else if (candle.ClosePrice < dojiLow)
		{
			direction = -1;
		}

		if (direction != 0 && Volume > 0m)
		{
			if (direction > 0)
			{
				// Buy enough volume to cover a short position and establish the target long size.
				var volume = Volume + Math.Max(0m, -Position);
				if (volume > 0m)
					BuyMarket(volume);
			}
			else
			{
				// Sell enough volume to cover a long position and establish the target short size.
				var volume = Volume + Math.Max(0m, Position);
				if (volume > 0m)
					SellMarket(volume);
			}
		}

		ShiftHistory(candle);
	}

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

		var digits = 0;
		var value = step;

		while (value < 1m && digits < 10)
		{
			value *= 10m;
			digits++;
		}

		var multiplier = (digits == 3 || digits == 5) ? 10m : 1m;
		return step * multiplier;
	}

	private static bool IsDoji(ICandleMessage candle, decimal threshold)
	{
		var body = Math.Abs(candle.OpenPrice - candle.ClosePrice);
		return body <= threshold;
	}

	private void ShiftHistory(ICandleMessage candle)
	{
		// Maintain the three most recent completed candles for doji detection.
		_threeAgoCandle = _twoAgoCandle;
		_twoAgoCandle = _previousCandle;
		_previousCandle = candle;
	}
}