在 GitHub 上查看

Doubler 对冲跟踪策略

概述

Doubler 对冲跟踪策略 是将 MetaTrader 5 智能交易系统 Doubler.mq5 迁移到 StockSharp 高阶 API 的版本。策略在没有持仓时立即同时开出一笔多单和一笔空单,并为两个方向分别维护止损、止盈与移动止损。所有风险控制均用“点(pip)”进行配置,并在内部根据交易品种的价格精度自动换算成价格步长,完全复刻原始 EA 的行为。

与普通方向性策略不同,本策略在任意时刻最多同时持有一多一空两个仓位。每个仓位按自身的保护规则独立退出,当两个方向都平仓后,下一次收到 Level1 行情就会再次建立新的对冲组合,从而持续保持双向敞口。

主要特点

  • 自动建立对冲:在没有未完成仓位且没有挂单时,同时发送一笔买单和一笔卖单,成交量均为 OrderVolume
  • 点值化风险控制:止损、止盈、移动止损均以点数配置。程序通过 PriceStepDecimals 自动转换为绝对价格距离(3/5 位报价会乘以 10,与 MT5 的 pip 定义一致)。
  • 每条腿独立移动止损:多头使用最优买价、空头使用最优卖价。只有当浮盈大于等于 TrailingStopPips + TrailingStepPips 时才将止损推进 TrailingStopPips,完全保留原 EA 的触发逻辑。
  • 成交量合法性校验:在下单前验证是否满足 MinVolumeMaxVolumeVolumeStep,若非法则抛出异常,避免向交易所发送无效报单。
  • 可选日志输出LogTradeDetails 参数开启后,会记录建仓、平仓以及移动止损的详细信息,便于回测和实盘排查。

参数说明

参数 描述 默认值 备注
OrderVolume 每条腿的下单数量。 1 必须符合交易所的最小/最大交易量与步长。
StopLossPips 止损距离(点)。 150 设为 0 表示不使用止损。
TakeProfitPips 止盈距离(点)。 300 设为 0 表示不使用止盈。
TrailingStopPips 移动止损距离(点)。 5 大于 0 时要求 TrailingStepPips 也大于 0。
TrailingStepPips 推进移动止损所需的额外点数。 5 防止止损过于频繁移动。
LogTradeDetails 是否记录详细日志。 false 适合调试或验证阶段使用。

交易逻辑

建仓

  1. 订阅 Level1 行情,获取最优买卖价。
  2. _longPosition_shortPosition 均为空且没有挂起的入场订单时,同时发送买入与卖出市价单,数量均为 OrderVolume
  3. 成交后保存入场价格,计算初始止损/止盈,并初始化移动止损跟踪变量。

风险管理

  • 止损:若 StopLossPips 大于 0,则初始止损设置在入场价±StopLossPips×pip 值的位置;为 0 时禁用止损。
  • 止盈:同样在入场价±TakeProfitPips×pip 值处设置止盈;为 0 时禁用止盈。
  • 下单前校验NormalizeVolume 会检测下单数量是否符合最小、最大及步长要求,如不符合直接抛出异常。

移动止损

  1. 当价格朝有利方向运行超过 TrailingStopPips + TrailingStepPips 时,尝试将止损推进到 当前价格 ± TrailingStopPips
  2. 只有当新止损比旧止损至少接近 TrailingStepPips,或旧止损尚未设置时,才会执行调整。
  3. 多头使用最优买价作为参考,空头使用最优卖价,保证平仓价格贴近真实成交价。

平仓

  • 当任一腿触及止损、移动止损或止盈时,立刻发送反向市价单平仓,并清理该腿的内部状态。
  • 两条腿全部平仓后,下一个 Level1 事件会重新建立对冲仓位。

数据需求

  • Level1 最优买/卖价:用于确定入场价、检测止盈止损以及移动止损。
  • 无需订阅 K 线或逐笔成交,策略完全基于 Level1 数据工作。

转换说明

  • pip 距离会乘以 PriceStep 得到绝对价格差。如果品种的小数位数为 3 或 5,则自动额外乘以 10,与原 EA 的 pip 定义一致。
  • 使用 StockSharp 高阶 API(RegisterOrderStartProtectionSubscribeLevel1),不依赖任何底层 Connector 调用。
  • 内部维护 PositionState 对象来追踪多、空两条腿,即便账户以净头寸形式展示,也能完整复刻对冲逻辑。
  • 该策略文件独立于测试工程,不需要修改仓库中的测试代码。
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Doubler strategy using double EMA confirmation with trailing stop management.
/// Enters long when both fast and medium EMAs are above slow EMA.
/// Enters short when both fast and medium EMAs are below slow EMA.
/// </summary>
public class DoublerStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _medPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _med;
	private ExponentialMovingAverage _slow;

	private decimal _entryPrice;
	private int _cooldown;

	/// <summary>
	/// Fast EMA period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Medium EMA period.
	/// </summary>
	public int MedPeriod
	{
		get => _medPeriod.Value;
		set => _medPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public DoublerStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Fast Period", "Fast EMA period", "Indicator");

		_medPeriod = Param(nameof(MedPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("Medium Period", "Medium EMA period", "Indicator");

		_slowPeriod = Param(nameof(SlowPeriod), 200)
			.SetGreaterThanZero()
			.SetDisplay("Slow Period", "Slow EMA period", "Indicator");

		_stopLossPoints = Param(nameof(StopLossPoints), 150)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 300)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

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

		_fast = null;
		_med = null;
		_slow = null;
		_entryPrice = 0;
		_cooldown = 0;
	}

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

		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_med = new ExponentialMovingAverage { Length = MedPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };

		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _med, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal medValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_fast.IsFormed || !_med.IsFormed || !_slow.IsFormed)
			return;

		if (_cooldown > 0)
		{
			_cooldown--;
			return;
		}

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		// Check SL/TP
		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step)
			{
				SellMarket();
				_entryPrice = 0;
				_cooldown = 100;
				return;
			}

			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step)
			{
				SellMarket();
				_entryPrice = 0;
				_cooldown = 100;
				return;
			}
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step)
			{
				BuyMarket();
				_entryPrice = 0;
				_cooldown = 100;
				return;
			}

			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step)
			{
				BuyMarket();
				_entryPrice = 0;
				_cooldown = 100;
				return;
			}
		}

		// Double confirmation: both fast and med above slow for long
		if (fastValue > slowValue && medValue > slowValue && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();

			BuyMarket();
			_entryPrice = close;
			_cooldown = 100;
		}
		// Double confirmation: both fast and med below slow for short
		else if (fastValue < slowValue && medValue < slowValue && Position >= 0)
		{
			if (Position > 0)
				SellMarket();

			SellMarket();
			_entryPrice = close;
			_cooldown = 100;
		}
	}
}