Открыть на GitHub

Стратегия Hans123 Trader v2

Hans123 Trader v2 — это адаптация одноимённого эксперта Владимира Карпутова на платформу StockSharp. Стратегия отслеживает выход цены из 80-свечного диапазона и работает через отложенные ордера Buy Stop и Sell Stop. После входа позиции применяются стоп-лосс, тейк-профит и трейлинг-стоп, что полностью повторяет оригинальную логику в MetaTrader.

Основная идея

  • Используются свечи выбранного типа (по умолчанию — часовые).
  • В торговый интервал рассчитываются максимумы и минимумы за последние N свечей (80 по умолчанию).
  • Если цена находится на достаточном расстоянии от текущих Bid/Ask, на уровне максимума размещается Buy Stop, а на уровне минимума — Sell Stop.
  • Количество активных отложенных ордеров ограничено параметром, чтобы избежать избыточной экспозиции.
  • При срабатывании одного ордера противоположные заявки отменяются, а на позицию устанавливаются защитные ордера с возможностью трейлинга.

Управление сделкой

  • Входы: Ордеры выставляются только тогда, когда время свечи попадает в интервал между начальным и конечным часом (границы включительно/исключительно, как в оригинале).
  • Защита позиции: После открытия позиции создаются стоп-лосс и тейк-профит на расстоянии, заданном в пунктах.
  • Трейлинг: При движении цены в прибыль на величину больше трейлинга плюс шаг, стоп-ордер перевыставляется ближе к рынку.
  • Очистка ордеров: Закрытие позиции приводит к отмене защитных ордеров, а новая позиция сразу убирает все противоположные отложенные ордера — аналогично MQL-версии.

Параметры

Параметр Описание
Volume Торговый объём для входов и защитных ордеров.
StopLossPips Расстояние от входа до стоп-лосса в пунктах (0 — без стопа).
TakeProfitPips Расстояние до тейк-профита в пунктах (0 — без тейка).
TrailingStopPips Размер начального трейлинга в пунктах (0 — трейлинг отключён).
TrailingStepPips Минимальный дополнительный профит в пунктах перед очередным переносом стопа. Должен быть > 0 при включённом трейлинге.
StartHour Час начала торговой сессии, с которого разрешено ставить отложенные ордера.
EndHour Час окончания торговой сессии (ордеры не ставятся, если время свечи >= этого часа).
MaxPendingOrders Максимальное количество одновременно активных отложенных ордеров.
BreakoutPeriod Количество свечей в расчёте максимума и минимума.
CandleType Тип обрабатываемых свечей (таймфрейм или иной источник данных).

Дополнительно

  • Размер пункта вычисляется из шага цены инструмента. Для 3- и 5-значных форекс-символов шаг умножается на 10, чтобы соответствовать определению пункта в MQL.
  • Если стакан не доступен, проверка расстояния до рынка выполняется по цене закрытия текущей свечи.
  • При изменении стоп-лосса заявка отменяется и создаётся заново, что соответствует вызову PositionModify в оригинале.
  • В рамках задачи создана только C#-версия стратегии, папка для Python не добавлялась.
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>
/// Hans123 Trader v2 breakout strategy converted from the original MQL expert.
/// Enters on breakout of recent range extremes and manages trailing protection.
/// </summary>
public class Hans123TraderV2Strategy : Strategy
{
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<int> _breakoutPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private Highest _highest;
	private Lowest _lowest;

	private decimal _entryPrice;
	private decimal _pipSize;
	private decimal _stopLossDistance;
	private decimal _takeProfitDistance;
	private decimal _trailingStopDistance;
	private decimal _trailingStepDistance;
	private decimal _highestStopPrice;
	private decimal? _prevBreakoutHigh;
	private decimal? _prevBreakoutLow;

	/// <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>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Trailing step in pips.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Session start hour.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Session end hour.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Lookback length for calculating highs and lows.
	/// </summary>
	public int BreakoutPeriod
	{
		get => _breakoutPeriod.Value;
		set => _breakoutPeriod.Value = value;
	}

	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="Hans123TraderV2Strategy"/>.
	/// </summary>
	public Hans123TraderV2Strategy()
	{
		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Stop distance", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Target distance", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (pips)", "Trailing distance", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Step (pips)", "Extra profit before trailing", "Risk");

		_startHour = Param(nameof(StartHour), 0)
			.SetDisplay("Start Hour", "Session start hour", "Session");

		_endHour = Param(nameof(EndHour), 23)
			.SetDisplay("End Hour", "Session end hour", "Session");

		_breakoutPeriod = Param(nameof(BreakoutPeriod), 10)
			.SetGreaterThanZero()
			.SetDisplay("Breakout Period", "High/low lookback", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Processed candles", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_pipSize = 0m;
		_stopLossDistance = 0m;
		_takeProfitDistance = 0m;
		_trailingStopDistance = 0m;
		_trailingStepDistance = 0m;
		_highest = null;
		_lowest = null;
		_entryPrice = 0m;
		_highestStopPrice = 0m;
		_prevBreakoutHigh = null;
		_prevBreakoutLow = null;
	}

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

		_pipSize = Security?.PriceStep ?? 1m;
		UpdateDistanceCache();

		_highest = new Highest { Length = BreakoutPeriod };
		_lowest = new Lowest { Length = BreakoutPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(_highest, _lowest, ProcessCandle).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void UpdateDistanceCache()
	{
		_stopLossDistance = StopLossPips * _pipSize;
		_takeProfitDistance = TakeProfitPips * _pipSize;
		_trailingStopDistance = TrailingStopPips * _pipSize;
		_trailingStepDistance = TrailingStepPips * _pipSize;
	}

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

		if (!_highest.IsFormed || !_lowest.IsFormed)
			return;

		// Manage existing position: trailing stop and SL/TP
		if (Position != 0)
		{
			ManagePosition(candle);
			return;
		}

		// Session filter
		var hour = candle.OpenTime.TimeOfDay.Hours;
		if (hour < StartHour || hour >= EndHour)
		{
			_prevBreakoutHigh = breakoutHigh;
			_prevBreakoutLow = breakoutLow;
			return;
		}

		// Breakout entry: buy when price breaks above previous bar's high, sell when below previous bar's low
		if (_prevBreakoutHigh.HasValue && _prevBreakoutLow.HasValue)
		{
			if (candle.HighPrice > _prevBreakoutHigh.Value)
			{
				BuyMarket();
				_entryPrice = candle.ClosePrice;
				_highestStopPrice = 0m;
			}
			else if (candle.LowPrice < _prevBreakoutLow.Value)
			{
				SellMarket();
				_entryPrice = candle.ClosePrice;
				_highestStopPrice = 0m;
			}
		}

		_prevBreakoutHigh = breakoutHigh;
		_prevBreakoutLow = breakoutLow;
	}

	private void ManagePosition(ICandleMessage candle)
	{
		var price = candle.ClosePrice;

		if (Position > 0)
		{
			// Check stop loss
			if (_stopLossDistance > 0m && candle.LowPrice <= _entryPrice - _stopLossDistance)
			{
				SellMarket(Position);
				return;
			}

			// Check take profit
			if (_takeProfitDistance > 0m && candle.HighPrice >= _entryPrice + _takeProfitDistance)
			{
				SellMarket(Position);
				return;
			}

			// Trailing stop
			if (_trailingStopDistance > 0m)
			{
				var moveFromEntry = price - _entryPrice;
				if (moveFromEntry > _trailingStopDistance + _trailingStepDistance)
				{
					var newStop = price - _trailingStopDistance;
					if (newStop > _highestStopPrice + _trailingStepDistance)
						_highestStopPrice = newStop;

					if (_highestStopPrice > 0m && candle.LowPrice <= _highestStopPrice)
					{
						SellMarket(Position);
						return;
					}
				}
			}
		}
		else if (Position < 0)
		{
			var vol = Math.Abs(Position);

			// Check stop loss
			if (_stopLossDistance > 0m && candle.HighPrice >= _entryPrice + _stopLossDistance)
			{
				BuyMarket(vol);
				return;
			}

			// Check take profit
			if (_takeProfitDistance > 0m && candle.LowPrice <= _entryPrice - _takeProfitDistance)
			{
				BuyMarket(vol);
				return;
			}

			// Trailing stop
			if (_trailingStopDistance > 0m)
			{
				var moveFromEntry = _entryPrice - price;
				if (moveFromEntry > _trailingStopDistance + _trailingStepDistance)
				{
					var newStop = price + _trailingStopDistance;
					if (_highestStopPrice == 0m || newStop < _highestStopPrice - _trailingStepDistance)
						_highestStopPrice = newStop;

					if (_highestStopPrice > 0m && candle.HighPrice >= _highestStopPrice)
					{
						BuyMarket(vol);
						return;
					}
				}
			}
		}
	}
}