Открыть на GitHub

Стратегия Doji Arrows

Концепция

Стратегия Doji Arrows представляет собой адаптацию одноимённого советника MetaTrader под высокоуровневый API StockSharp. Алгоритм ожидает формирования классической свечи «доджи» и затем торгует пробой её диапазона. Доджи свидетельствует о балансе сил; закрытие следующей свечи выше максимума доджи трактуется как захват инициативы покупателями, а закрытие ниже минимума — как сигнал к продажам.

  1. Обрабатываются только завершённые свечи выбранного таймфрейма CandleType.
  2. Предыдущая свеча проверяется на соответствие критериям доджи. Она считается доджи, если модуль разницы между ценой открытия и закрытия не превышает DojiBodyPoints, умноженные на шаг цены инструмента. При значении параметра 0 используется один шаг цены, что воспроизводит строгую проверку на равенство из версии MQL5.
  3. При закрытии следующей свечи выше максимума доджи отправляется рыночная заявка на покупку. При закрытии ниже минимума — рыночная заявка на продажу. Если открыта противоположная позиция, объём рыночного ордера автоматически её закрывает и разворачивает при необходимости.

Такое поведение повторяет логику исходного советника, который принимал решения один раз на старте нового бара.

Управление рисками

Перенос включает защитные механизмы оригинала:

  • Стоп-лосс. Параметр StopLossPoints задаёт расстояние начального стопа в шагах цены от точки входа. Значение 0 отключает фиксированный стоп.
  • Тейк-профит. Параметр TakeProfitPoints определяет расстояние до цели по прибыли в шагах цены. Значение 0 убирает тейк-профит.
  • Трейлинг-стоп. Пара TrailingStopPoints и TrailingStepPoints воспроизводит алгоритм подтягивания стопа. После того как прибыль превысит сумму TrailingStopPoints + TrailingStepPoints, стоп переносится на расстояние TrailingStopPoints от последней цены закрытия (для длинной позиции — от максимума, для короткой — от минимума). Трейлинг активируется только при положительном TrailingStopPoints.

На каждой завершённой свече проверяется, были ли достигнуты стоп или цель. При пробое уровня по максимуму/минимуму свечи позиция закрывается рыночным ордером, после чего защитные параметры сбрасываются.

Параметры

Параметр Значение по умолчанию Описание
StopLossPoints 30 Расстояние начального стоп-лосса в шагах цены.
TakeProfitPoints 90 Расстояние тейк-профита в шагах цены.
TrailingStopPoints 15 Шаг трейлинг-стопа в шагах цены.
TrailingStepPoints 5 Дополнительная прибыль, необходимая перед срабатыванием трейлинга, в шагах цены.
DojiBodyPoints 1 Максимальный размер тела предыдущей свечи в шагах цены, допускающий признание её доджи. Значение 0 использует один шаг цены в качестве допуска.
CandleType 1 час Таймфрейм свечей, применяемый для сигналов.

Особенности реализации

  • Подписка на свечи осуществляется через SubscribeCandles(CandleType).Bind(ProcessCandle), в памяти хранится только последняя завершённая свеча.
  • Шаг цены берётся из Security?.PriceStep. Если брокер или источник данных его не предоставляет, используется запасное значение 1, что позволяет стратегии работать и на синтетических инструментах.
  • Защитные уровни пересчитываются после каждого входа; трейлинг-стоп способен выставить стоп даже при отключённом фиксированном стоп-лоссе, как и в MQL-версии.
  • Все операции выполняются рыночными ордерами, что соответствует исходному советнику с немедленным исполнением.

Рекомендации по применению

  1. Перед запуском укажите Security, Portfolio и Volume стратегии.
  2. Подберите значения параметров в шагах цены под конкретный инструмент, особенно если он торгуется с дробными пунктами.
  3. При необходимости расширенного мани-менеджмента добавьте модули риск-контроля StockSharp — конвертация сохраняет фиксированный объём сделок из оригинала.
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;
	}
}