GitHub で見る

EES Hedger Strategy

Overview

The EES Hedger strategy mirrors the behaviour of the classic MetaTrader expert advisor that automatically hedges positions created by another trading system or by manual traders. Whenever the monitored account opens a position that matches the configured filter, the strategy immediately opens an opposite position with its own parameters. By doing so it neutralises directional exposure while still allowing the original trade to run.

The algorithm is built on the high level StockSharp API. It listens to account trades, opens hedge positions, and manages protective orders through stop-loss, take-profit and trailing stop logic. Trailing management follows the original implementation closely, only advancing the stop when price movement exceeds both the stop distance and the trailing increment.

Parameters

Name Description
HedgeVolume Fixed volume for the hedge order. It does not depend on the external trade size.
StopLossPips Distance in pips for the protective stop-loss of the hedge. Set to zero to skip the initial stop.
TakeProfitPips Distance in pips for the take-profit order. Set to zero to omit the target.
TrailingStopPips Distance in pips used for trailing once price moves favourably.
TrailingStepPips Minimum pip movement required before moving the trailing stop again. Must be positive when trailing is active.
OriginalOrderComment Optional comment filter. Only trades whose comment matches this value (case-insensitive) will be hedged. Leave empty to react to every trade.
HedgerOrderComment Optional comment used to recognise the strategy's own hedge trades. When supplied, trades carrying the same comment are ignored to prevent re-hedging.

Behaviour

  1. Trade detection – the strategy subscribes to NewMyTrade events of the connector. Each trade that comes from the selected security and passes the comment filters is treated as an external entry signal.
  2. Hedge execution – as soon as a qualifying trade is seen, the strategy submits a market order in the opposite direction using HedgeVolume.
  3. Protection setup – after each own fill the algorithm cancels existing protective orders and registers new stop-loss and take-profit orders according to the current average position price.
  4. Trailing stop – every incoming trade tick is used to evaluate the trailing rules. Once the price has moved by at least TrailingStopPips + TrailingStepPips in favour of the hedge, the stop is moved closer to price. For long positions the stop trails below the market, for shorts above it.
  5. Position reset – when the hedge position is fully closed (for example by stop or target), the strategy automatically cancels the remaining protective orders and waits for the next external trade.

Usage Notes

  • The strategy assumes that the account connector reports all account trades, including those generated by other systems.
  • Pip calculation adapts to the instrument price step and multiplies by ten for 3- or 5-digit quotes, imitating the MQL point adjustment.
  • Set OriginalOrderComment to match the comment of the primary system if only specific trades should be mirrored. When hedging manual trades, leave it empty.
  • Ensure that TrailingStepPips remains greater than zero whenever trailing is enabled to avoid premature termination at start-up.
  • Because the hedger always uses a fixed volume, you may wish to tune HedgeVolume so that the hedge covers the average exposure generated by the primary system.
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 System.Globalization;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Mirrors trades by opening an opposite hedge position with trailing stop management.
/// Simplified from the EES Hedger expert advisor.
/// </summary>
public class EesHedgerStrategy : Strategy
{
	private readonly StrategyParam<decimal> _hedgeVolume;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal _pipSize;

	/// <summary>
	/// Hedge position volume.
	/// </summary>
	public decimal HedgeVolume
	{
		get => _hedgeVolume.Value;
		set => _hedgeVolume.Value = value;
	}

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

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

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Minimum step between trailing stop updates in pips.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public EesHedgerStrategy()
	{
		_hedgeVolume = Param(nameof(HedgeVolume), 0.1m)
			.SetDisplay("Hedge Volume", "Volume used for hedge orders", "General");

		_stopLossPips = Param(nameof(StopLossPips), 50)
			.SetDisplay("Stop Loss (pips)", "Stop-loss distance per hedge", "Risk Management");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
			.SetDisplay("Take Profit (pips)", "Take-profit distance per hedge", "Risk Management");

		_trailingStopPips = Param(nameof(TrailingStopPips), 25)
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk Management");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
			.SetDisplay("Trailing Step (pips)", "Minimum trailing stop increment", "Risk Management");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for processing", "General");
	}

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

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_entryPrice = 0m;
		_stopPrice = null;
		_pipSize = 0m;
	}

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

		_pipSize = CalculatePipSize();

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

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

		var price = candle.ClosePrice;

		if (price <= 0m)
			return;

		// Entry: if no position, open based on tick direction
		if (Position == 0 && _entryPrice == 0m)
		{
			var volume = HedgeVolume > 0m ? HedgeVolume : Volume;
			if (volume <= 0m)
				return;

			BuyMarket();
			_entryPrice = price;
			_stopPrice = null;
			return;
		}

		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = price;

		// Check stop loss
		if (Position != 0 && StopLossPips > 0 && _pipSize > 0m)
		{
			var stopDistance = StopLossPips * _pipSize;

			if (Position > 0 && price <= _entryPrice - stopDistance)
			{
				SellMarket();
				_entryPrice = 0m;
				_stopPrice = null;
				return;
			}
			else if (Position < 0 && price >= _entryPrice + stopDistance)
			{
				BuyMarket();
				_entryPrice = 0m;
				_stopPrice = null;
				return;
			}
		}

		// Check take profit
		if (Position != 0 && TakeProfitPips > 0 && _pipSize > 0m)
		{
			var takeDistance = TakeProfitPips * _pipSize;

			if (Position > 0 && price >= _entryPrice + takeDistance)
			{
				SellMarket();
				_entryPrice = 0m;
				_stopPrice = null;
				return;
			}
			else if (Position < 0 && price <= _entryPrice - takeDistance)
			{
				BuyMarket();
				_entryPrice = 0m;
				_stopPrice = null;
				return;
			}
		}

		// Trailing stop
		if (Position != 0 && TrailingStopPips > 0 && _pipSize > 0m)
		{
			var trailingDistance = TrailingStopPips * _pipSize;
			var trailingStep = TrailingStepPips * _pipSize;

			if (Position > 0)
			{
				var newStop = price - trailingDistance;
				if (newStop > _entryPrice && (!_stopPrice.HasValue || newStop > _stopPrice.Value + trailingStep))
					_stopPrice = newStop;

				if (_stopPrice.HasValue && price <= _stopPrice.Value)
				{
					SellMarket();
					_entryPrice = 0m;
					_stopPrice = null;
				}
			}
			else if (Position < 0)
			{
				var newStop = price + trailingDistance;
				if (newStop < _entryPrice && (!_stopPrice.HasValue || newStop < _stopPrice.Value - trailingStep))
					_stopPrice = newStop;

				if (_stopPrice.HasValue && price >= _stopPrice.Value)
				{
					BuyMarket();
					_entryPrice = 0m;
					_stopPrice = null;
				}
			}
		}
	}

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

		var decimals = GetDecimalPlaces(step);
		return decimals == 3 || decimals == 5 ? step * 10m : step;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		var text = Math.Abs(value).ToString(CultureInfo.InvariantCulture);
		var index = text.IndexOf('.');
		return index >= 0 ? text.Length - index - 1 : 0;
	}
}