在 GitHub 上查看

Zone Recovery Area 策略

概述

Zone Recovery Area Strategy 是将 MetaTrader 专家顾问 “Zone Recovery Area”(MQL/20266)完整移植到 StockSharp 高级 API 的版本。策略保留了原有的区间恢复(zone recovery)对冲逻辑,并将主要参数全部公开,方便在不修改代码的情况下调优。开仓后,系统会围绕基准价格交替建立多、空头寸,当价格离开或重新进入预设区域时加仓,以期逐步弥补浮动亏损并最终以盈利关闭整个组合。

主要特性:

  • 使用快、慢两条简单移动平均线(SMA)结合月度 MACD(12/26/9)作为趋势过滤器。
  • 实现 zone recovery 对冲机制:第一笔交易确定基准价,后续对冲单在价格穿越区域边界或回到基准价时触发。
  • 支持三种盈利退出方式:绝对金额、账户百分比、追踪止盈。
  • 每一步加仓体量可按倍数递增(类马丁策略)或按固定增量增加。

数据与指标

  • 主级别 K 线: 用户自定义的入场与管理周期,默认 30 分钟。
  • 月度 K 线: 若无原生月线,可由更小周期合成,用于计算 MACD。
  • 指标:
    • 主周期上的两条 SMA。
    • 月度 MACD(含信号线)。

交易流程

  1. 趋势确认
    • 等待两条 SMA 与月度 MACD 均形成有效数值。
    • 多头条件: 前一根 K 线中快 SMA 低于慢 SMA,且 MACD 主线高于信号线。
    • 空头条件: 前一根 K 线中快 SMA 高于慢 SMA,且 MACD 主线低于信号线。
  2. 启动恢复周期
    • 出现多头(空头)信号时,以 InitialVolume 开多(空)头寸,并记录成交价为基准价。
    • 清空内部计数器与盈利跟踪变量,开始新的恢复周期。
  3. 恢复引擎
    • 计算两个关键价位:恢复区间ZoneRecoveryPips)与盈利目标TakeProfitPips)。
    • 周期运行过程中,每根完结的 K 线都需要检查:
      • 价格到达盈利目标时立即平掉所有净头寸,结束周期;
      • 若达到金额或百分比目标,或追踪止盈触发,也会立即平仓;
      • 否则判断是否需要新对冲单:
        • 多头周期:跌破 base - zone 时加空单,重新站回基准价时加多单;
        • 空头周期:突破 base + zone 时加多单,回落至基准价以下时加空单。
      • 多、空方向自动交替;每笔对冲单的手数根据设置自动放大或累加。
    • MaxTrades 控制单个周期内的最大交易次数。
  4. 盈利管理
    • UseMoneyTakeProfit:未实现利润达到设定金额时结束周期。
    • UsePercentTakeProfit:未实现利润达到账户价值的一定百分比时结束周期。
    • EnableTrailing:利润超过 TrailingStartProfit 后记录峰值,若回撤超过 TrailingDrawdown 即平仓。

策略使用 StockSharp 的高阶下单函数 BuyMarket/SellMarket,无需直接处理底层订单对象。

参数说明

参数 默认值 说明
CandleType 30 分钟 入场与管理所用的主周期。
MonthlyCandleType 30 天 计算月度 MACD 的周期。
FastMaLength 20 快速 SMA 的周期。
SlowMaLength 200 慢速 SMA 的周期。
TakeProfitPips 150 从基准价起算的总体盈利目标。
ZoneRecoveryPips 50 恢复区间半宽度。
InitialVolume 1 周期第一笔订单的手数。
UseVolumeMultiplier true 是否按倍数放大后续订单。
VolumeMultiplier 2 使用倍增模式时的乘数。
VolumeIncrement 0.5 使用加法模式时的增量。
MaxTrades 6 单个恢复周期内的最大订单数。
UseMoneyTakeProfit false 启用金额止盈。
MoneyTakeProfit 40 金额止盈目标。
UsePercentTakeProfit false 启用百分比止盈。
PercentTakeProfit 5 百分比止盈目标。
EnableTrailing true 启用追踪止盈。
TrailingStartProfit 40 追踪止盈启动阈值。
TrailingDrawdown 10 允许的利润回吐。

点值转换: TakeProfitPipsZoneRecoveryPips 会根据标的的 PriceStep 转换为价格偏移,请确保证券提供正确的最小价位与步长价值。

使用建议

  1. 在 Designer/API/Runner 中加载策略,并指定交易品种与投资组合。
  2. 根据标的波动率与风险承受能力调整各项参数。
  3. 确保历史数据长度足够,便于 SMA 与 MACD 完成预热。
  4. 密切关注保证金占用,倍增模式下仓位会迅速扩大。
  5. 先在回测或模拟环境验证,再考虑实盘运行。

风险提示

  • 区间恢复/马丁策略在强趋势行情中可能累积巨额头寸,必须使用 MaxTrades 和合理的参数限制风险。
  • StockSharp 采用净持仓模型,策略会根据 PriceStep/StepPrice 计算组合盈亏,建议与券商数据进行核对。
  • 金额与百分比止盈依赖投资组合估值,回测时请确认 BeginValueCurrentValue 等字段有效。
  • 策略未设置硬止损,如有需要应在账户层面增加其他风控措施。

文件说明

  • CS/ZoneRecoveryAreaStrategy.cs — 策略实现。
  • README.md — 英文说明。
  • README_ru.md — 俄文说明。
  • README_zh.md — 中文说明(本文件)。
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>
/// Zone recovery hedging strategy converted from MetaTrader expert advisor.
/// The strategy alternates buy and sell positions around a base price to recover drawdowns.
/// </summary>
public class ZoneRecoveryAreaStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<DataType> _monthlyCandleType;
	private readonly StrategyParam<int> _fastMaLength;
	private readonly StrategyParam<int> _slowMaLength;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _zoneRecoveryPips;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<bool> _useVolumeMultiplier;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<decimal> _volumeIncrement;
	private readonly StrategyParam<int> _maxTrades;
	private readonly StrategyParam<bool> _useMoneyTakeProfit;
	private readonly StrategyParam<decimal> _moneyTakeProfit;
	private readonly StrategyParam<bool> _usePercentTakeProfit;
	private readonly StrategyParam<decimal> _percentTakeProfit;
	private readonly StrategyParam<bool> _enableTrailing;
	private readonly StrategyParam<decimal> _trailingStartProfit;
	private readonly StrategyParam<decimal> _trailingDrawdown;

	private SimpleMovingAverage _fastMa = null!;
	private SimpleMovingAverage _slowMa = null!;
	private MovingAverageConvergenceDivergenceSignal _monthlyMacd = null!;

	private decimal _prevFast;
	private decimal _prevSlow;
	private bool _maInitialized;
	private bool _macdReady;
	private decimal _macdMain;
	private decimal _macdSignal;
	private bool _isLongCycle;
	private decimal _cycleBasePrice;
	private int _nextStepIndex;
	private decimal _peakCycleProfit;

	private readonly List<TradeStep> _steps = new();

	/// <summary>
	/// Initializes a new instance of <see cref="ZoneRecoveryAreaStrategy"/>.
	/// </summary>
	public ZoneRecoveryAreaStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Entry Candle", "Timeframe used for entries", "General");

		_monthlyCandleType = Param(nameof(MonthlyCandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Monthly Candle", "Timeframe used for MACD filter", "General");

		_fastMaLength = Param(nameof(FastMaLength), 20)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA", "Fast moving average period", "Trend Filter");

		_slowMaLength = Param(nameof(SlowMaLength), 200)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA", "Slow moving average period", "Trend Filter");

		_takeProfitPips = Param(nameof(TakeProfitPips), 150m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Distance to close the cycle in profit", "Risk Management");

		_zoneRecoveryPips = Param(nameof(ZoneRecoveryPips), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Zone Width (pips)", "Distance that triggers hedging trades", "Risk Management");

		_initialVolume = Param(nameof(InitialVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Initial Volume", "Volume of the first trade", "Position Sizing");

		_useVolumeMultiplier = Param(nameof(UseVolumeMultiplier), true)
			.SetDisplay("Use Multiplier", "If true the next trades multiply the previous volume", "Position Sizing");

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Multiplier", "Factor applied when increasing volume", "Position Sizing");

		_volumeIncrement = Param(nameof(VolumeIncrement), 0.5m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Increment", "Additional volume when multiplier is disabled", "Position Sizing");

		_maxTrades = Param(nameof(MaxTrades), 6)
			.SetGreaterThanZero()
			.SetDisplay("Max Trades", "Maximum number of trades in one cycle", "Risk Management");

		_useMoneyTakeProfit = Param(nameof(UseMoneyTakeProfit), false)
			.SetDisplay("Money Take Profit", "Enable profit target in account currency", "Risk Management");

		_moneyTakeProfit = Param(nameof(MoneyTakeProfit), 40m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit $", "Target profit in account currency", "Risk Management");

		_usePercentTakeProfit = Param(nameof(UsePercentTakeProfit), false)
			.SetDisplay("Percent Take Profit", "Enable profit target based on account balance", "Risk Management");

		_percentTakeProfit = Param(nameof(PercentTakeProfit), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit %", "Target profit as a percentage of balance", "Risk Management");

		_enableTrailing = Param(nameof(EnableTrailing), true)
			.SetDisplay("Trailing", "Enable trailing profit lock", "Risk Management");

		_trailingStartProfit = Param(nameof(TrailingStartProfit), 40m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Start", "Profit required before trailing starts", "Risk Management");

		_trailingDrawdown = Param(nameof(TrailingDrawdown), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Step", "Maximum profit giveback before exit", "Risk Management");
	}

	/// <summary>
	/// Working candle type for entries.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Monthly candle type used for the MACD filter.
	/// </summary>
	public DataType MonthlyCandleType
	{
		get => _monthlyCandleType.Value;
		set => _monthlyCandleType.Value = value;
	}

	/// <summary>
	/// Fast moving average period.
	/// </summary>
	public int FastMaLength
	{
		get => _fastMaLength.Value;
		set => _fastMaLength.Value = value;
	}

	/// <summary>
	/// Slow moving average period.
	/// </summary>
	public int SlowMaLength
	{
		get => _slowMaLength.Value;
		set => _slowMaLength.Value = value;
	}

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

	/// <summary>
	/// Zone width in pips for opening hedging trades.
	/// </summary>
	public decimal ZoneRecoveryPips
	{
		get => _zoneRecoveryPips.Value;
		set => _zoneRecoveryPips.Value = value;
	}

	/// <summary>
	/// Volume of the first trade in a cycle.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Use multiplicative volume scaling.
	/// </summary>
	public bool UseVolumeMultiplier
	{
		get => _useVolumeMultiplier.Value;
		set => _useVolumeMultiplier.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the previous volume.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	/// <summary>
	/// Additional volume added when multiplier is disabled.
	/// </summary>
	public decimal VolumeIncrement
	{
		get => _volumeIncrement.Value;
		set => _volumeIncrement.Value = value;
	}

	/// <summary>
	/// Maximum number of trades per recovery cycle.
	/// </summary>
	public int MaxTrades
	{
		get => _maxTrades.Value;
		set => _maxTrades.Value = value;
	}

	/// <summary>
	/// Enable profit target in account currency.
	/// </summary>
	public bool UseMoneyTakeProfit
	{
		get => _useMoneyTakeProfit.Value;
		set => _useMoneyTakeProfit.Value = value;
	}

	/// <summary>
	/// Profit target in account currency.
	/// </summary>
	public decimal MoneyTakeProfit
	{
		get => _moneyTakeProfit.Value;
		set => _moneyTakeProfit.Value = value;
	}

	/// <summary>
	/// Enable profit target based on account percentage.
	/// </summary>
	public bool UsePercentTakeProfit
	{
		get => _usePercentTakeProfit.Value;
		set => _usePercentTakeProfit.Value = value;
	}

	/// <summary>
	/// Profit target as a percentage of account balance.
	/// </summary>
	public decimal PercentTakeProfit
	{
		get => _percentTakeProfit.Value;
		set => _percentTakeProfit.Value = value;
	}

	/// <summary>
	/// Enable trailing profit lock.
	/// </summary>
	public bool EnableTrailing
	{
		get => _enableTrailing.Value;
		set => _enableTrailing.Value = value;
	}

	/// <summary>
	/// Profit level where trailing begins.
	/// </summary>
	public decimal TrailingStartProfit
	{
		get => _trailingStartProfit.Value;
		set => _trailingStartProfit.Value = value;
	}

	/// <summary>
	/// Allowed drawdown from the peak profit before closing.
	/// </summary>
	public decimal TrailingDrawdown
	{
		get => _trailingDrawdown.Value;
		set => _trailingDrawdown.Value = value;
	}

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

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

		_steps.Clear();
		_prevFast = 0m;
		_prevSlow = 0m;
		_maInitialized = false;
		_macdReady = false;
		_macdMain = 0m;
		_macdSignal = 0m;
		_isLongCycle = false;
		_cycleBasePrice = 0m;
		_nextStepIndex = 0;
		_peakCycleProfit = 0m;
	}

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

		_fastMa = new SimpleMovingAverage { Length = FastMaLength };
		_slowMa = new SimpleMovingAverage { Length = SlowMaLength };
		_monthlyMacd = new MovingAverageConvergenceDivergenceSignal
		{
			Macd =
			{
				ShortMa = { Length = 12 },
				LongMa = { Length = 26 }
			},
			SignalMa = { Length = 9 }
		};

		var mainSubscription = SubscribeCandles(CandleType);
		mainSubscription
			.Bind(_fastMa, _slowMa, ProcessMainCandle)
			.Start();

		var monthlySubscription = SubscribeCandles(MonthlyCandleType);
		monthlySubscription
			.Bind(ProcessMonthlyCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, mainSubscription);
			DrawIndicator(area, _fastMa);
			DrawIndicator(area, _slowMa);
			DrawOwnTrades(area);

			// MACD is manually processed so cannot be drawn via DrawIndicator
		}
	}

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

		var macdResult = _monthlyMacd.Process(candle);
		if (macdResult.IsEmpty || !_monthlyMacd.IsFormed)
			return;

		var macd = (MovingAverageConvergenceDivergenceSignalValue)macdResult;
		if (macd.Macd is not decimal macdLine || macd.Signal is not decimal signalLine)
			return;

		_macdMain = macdLine;
		_macdSignal = signalLine;
		_macdReady = true;
	}

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

		if (!_fastMa.IsFormed || !_slowMa.IsFormed || !_macdReady)
		{
			_prevFast = fastValue;
			_prevSlow = slowValue;
			return;
		}

		if (!_maInitialized)
		{
			_prevFast = fastValue;
			_prevSlow = slowValue;
			_maInitialized = true;
			return;
		}

		if (_steps.Count > 0)
		{
			HandleExistingCycle(candle.ClosePrice);
		}
		else
		{
			TryStartCycle(candle.ClosePrice);
		}

		_prevFast = fastValue;
		_prevSlow = slowValue;
	}

	private void TryStartCycle(decimal price)
	{
		var macdBullish = _macdMain > _macdSignal;
		var macdBearish = _macdMain < _macdSignal;

		var bullishSetup = _prevFast < _prevSlow && macdBullish;
		var bearishSetup = _prevFast > _prevSlow && macdBearish;

		if (bullishSetup)
		{
			StartCycle(true, price);
		}
		else if (bearishSetup)
		{
			StartCycle(false, price);
		}
	}

	private void StartCycle(bool isLong, decimal price)
	{
		if (InitialVolume <= 0m)
			return;

		_steps.Clear();
		_isLongCycle = isLong;
		_cycleBasePrice = price;
		_nextStepIndex = 1;
		_peakCycleProfit = 0m;

		ExecuteOrder(isLong, InitialVolume, price);
	}

	private void HandleExistingCycle(decimal price)
	{
		var takeProfitOffset = GetPriceOffset(TakeProfitPips);
		if (takeProfitOffset > 0m)
		{
			if (_isLongCycle && price >= _cycleBasePrice + takeProfitOffset)
			{
				CloseCycle();
				return;
			}

			if (!_isLongCycle && price <= _cycleBasePrice - takeProfitOffset)
			{
				CloseCycle();
				return;
			}
		}

		var cycleProfit = CalculateCycleProfit(price);

		if (UseMoneyTakeProfit && MoneyTakeProfit > 0m && cycleProfit >= MoneyTakeProfit)
		{
			CloseCycle();
			return;
		}

		if (UsePercentTakeProfit && PercentTakeProfit > 0m && TryGetPercentTarget(out var percentTarget) && cycleProfit >= percentTarget)
		{
			CloseCycle();
			return;
		}

		if (EnableTrailing && TrailingStartProfit > 0m && TrailingDrawdown > 0m)
		{
			if (cycleProfit >= TrailingStartProfit)
			{
				_peakCycleProfit = Math.Max(_peakCycleProfit, cycleProfit);
			}

			if (_peakCycleProfit > 0m && cycleProfit <= _peakCycleProfit - TrailingDrawdown)
			{
				CloseCycle();
				return;
			}
		}
		else
		{
			_peakCycleProfit = 0m;
		}

		if (_steps.Count >= MaxTrades)
			return;

		if (!ShouldOpenNextTrade(price))
			return;

		var nextIsBuy = GetNextDirection();
		var volume = GetNextVolume();

		ExecuteOrder(nextIsBuy, volume, price);
		_nextStepIndex++;
	}

	private bool ShouldOpenNextTrade(decimal price)
	{
		var zoneOffset = GetPriceOffset(ZoneRecoveryPips);
		if (zoneOffset <= 0m)
			return false;

		var nextIsBuy = GetNextDirection();

		if (_isLongCycle)
		{
			if (nextIsBuy)
				return price >= _cycleBasePrice;

			return price <= _cycleBasePrice - zoneOffset;
		}

		if (nextIsBuy)
			return price >= _cycleBasePrice + zoneOffset;

		return price <= _cycleBasePrice;
	}

	private bool GetNextDirection()
	{
		var isOddStep = _nextStepIndex % 2 == 1;
		if (_isLongCycle)
			return !isOddStep;

		return isOddStep;
	}

	private decimal GetNextVolume()
	{
		if (_steps.Count == 0)
			return InitialVolume;

		var lastVolume = _steps[^1].Volume;
		decimal nextVolume;

		if (UseVolumeMultiplier)
		{
			nextVolume = lastVolume * VolumeMultiplier;
		}
		else
		{
			nextVolume = lastVolume + VolumeIncrement;
		}

		return nextVolume <= 0m ? InitialVolume : decimal.Round(nextVolume, 6);
	}

	private decimal CalculateCycleProfit(decimal price)
	{
		if (_steps.Count == 0 || Security == null)
			return 0m;

		var priceStep = Security.PriceStep ?? 0m;
		var stepPrice = Security.PriceStep ?? 0m;

		if (priceStep <= 0m || stepPrice <= 0m)
			return 0m;

		decimal pnl = 0m;
		foreach (var step in _steps)
		{
			var diff = price - step.Price;
			var stepsCount = diff / priceStep;
			var direction = step.IsBuy ? 1m : -1m;
			pnl += stepsCount * stepPrice * step.Volume * direction;
		}

		return pnl;
	}

	private bool TryGetPercentTarget(out decimal target)
	{
		target = 0m;
		if (Portfolio == null)
			return false;

		var balance = Portfolio.CurrentValue ?? Portfolio.BeginValue ?? 0m;
		if (balance <= 0m)
			return false;

		target = balance * PercentTakeProfit / 100m;
		return true;
	}

	private decimal GetPriceOffset(decimal pips)
	{
		if (Security == null)
			return 0m;

		var priceStep = Security.PriceStep ?? 0m;
		return priceStep <= 0m ? 0m : pips * priceStep;
	}

	private void ExecuteOrder(bool isBuy, decimal volume, decimal price)
	{
		if (volume <= 0m)
			return;

		if (isBuy)
		{
			BuyMarket(volume);
		}
		else
		{
			SellMarket(volume);
		}

		_steps.Add(new TradeStep(isBuy, price, volume));
	}

	private void CloseCycle()
	{
		if (Position > 0m)
		{
			SellMarket(Position);
		}
		else if (Position < 0m)
		{
			BuyMarket(Math.Abs(Position));
		}

		_steps.Clear();
		_nextStepIndex = 0;
		_cycleBasePrice = 0m;
		_peakCycleProfit = 0m;
	}

	private sealed record TradeStep(bool IsBuy, decimal Price, decimal Volume);
}