在 GitHub 上查看

EES 对冲策略

概述

EES 对冲策略复刻了经典的 MetaTrader 智能交易程序,通过自动在账户中开立与原始交易方向相反的持仓来进行风险对冲。只要监控到符合过滤条件的外部交易(来自其他自动化策略或人工操作),本策略就会立即按照自身参数开仓,从而抵消净敞口,同时保留原始仓位的运行空间。

策略基于 StockSharp 的高级 API 编写:订阅账户成交、发送对冲单,并通过止损、止盈及追踪止损来管理防护订单。追踪止损的实现严格遵循原始 MQL 版本,只有当价格变动超过止损距离与追踪步长之和时才会移动止损价位。

参数

参数 说明
HedgeVolume 对冲单的固定手数,不会根据外部成交的数量自动调整。
StopLossPips 对冲仓位初始止损的点数距离。为 0 表示不设置初始止损。
TakeProfitPips 对冲仓位止盈的点数距离。为 0 表示不放置止盈。
TrailingStopPips 追踪止损的距离,表示与当前价格保持的点差。
TrailingStepPips 每次更新追踪止损所需的最小价格位移。启用追踪功能时必须为正值。
OriginalOrderComment 可选的订单注释过滤器,仅对注释与此值匹配(忽略大小写)的交易执行对冲。留空表示对所有交易生效。
HedgerOrderComment 可选注释,用于识别策略自身的对冲单。当成交注释与该值相同时时将忽略,避免重复对冲。

行为流程

  1. 成交监听:策略订阅连接器的 NewMyTrade 事件,对每一笔属于目标证券且满足过滤条件的成交视为外部进场信号。
  2. 对冲下单:当检测到有效成交时,立即使用 HedgeVolume 以市价在相反方向开仓。
  3. 防护设置:每次自身成交后都会撤销现有的防护单,并根据当前持仓均价重新挂出止损与止盈订单。
  4. 追踪止损:利用收到的逐笔成交数据评估追踪条件;当浮盈超过 TrailingStopPips + TrailingStepPips 时,向有利方向移动止损价。多头止损跟随在价格下方,空头则在价格上方。
  5. 仓位归零:当对冲仓位被完全平仓(触发止损或止盈)后,会自动撤销剩余的防护订单并等待下一笔外部信号。

使用建议

  • 请确保连接器能够返回所有账户成交,包括由其他程序生成的订单。
  • 点值计算参考品种的最小价格跳动,对于 3 位或 5 位报价会自动乘以 10,以模拟 MQL 中对 Point 的调整。
  • 若只希望对特定策略的交易进行对冲,可设置 OriginalOrderComment 与其订单注释一致;对人工交易对冲时可保持为空。
  • 启用追踪止损时务必保证 TrailingStepPips 大于零,否则策略会在启动阶段直接终止。
  • 由于对冲手数固定,建议结合主策略的平均持仓规模调节 HedgeVolume,以获得最佳的风险覆盖效果。
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;
	}
}