在 GitHub 上查看

逆向反应策略

概述

逆向反应策略是一套均值回归系统,灵感来自 MetaTrader 上的 "IREA" 专家顾问。它关注异常巨大的单根 K 线波动,并预期下一根 K 线会出现反向修正。策略通过最近几根 K 线的价格变化计算出动态置信阈值,只有当波动超过该阈值并且仍处在用户设定的范围内时才会下单。策略在任何时刻只持有一个仓位。

交易逻辑

  1. 逆向反应指标 —— 对每根收盘完成的 K 线,策略计算开收盘价差,并将其绝对值送入长度为 MaPeriod 的简单移动平均。得到的平均值乘以 Coefficient,即得到类似原始指标动态置信区间(DCL)的阈值。
  2. 信号过滤 —— 最新一根 K 线的绝对开收盘差必须大于动态阈值,同时大于 MinCriteriaPoints * PriceStep 并小于 MaxCriteriaPoints * PriceStep。如果上一根 K 线已经满足同样条件,则忽略当前信号,这与原始 EA 的处理方式一致。
  3. 方向选择 —— 若价差为负(阴线),策略预期价格向上反弹并开多;若价差为正(阳线),策略预期向下回调并开空。只有在没有持仓时才会开新仓。
  4. 风险管理 —— 开仓后策略会持续监控后续 K 线。当价格触及按点数换算的止损或止盈价位(使用品种的 PriceStep 转换),策略立即通过市价单平仓。同时调用 StartProtection() 以启用 StockSharp 的保护机制。

参数

参数 说明
StopLossPoints 止损距离(点),会乘以 PriceStep 转为实际价格。
TakeProfitPoints 止盈距离(点)。
TradeVolume 每笔市价单使用的交易量。
SlippagePoints 与 MQL 版本一致的滑点信息,目前不会应用到订单。
MinCriteriaPoints 有效信号所需的最小开收盘差(点)。
MaxCriteriaPoints 有效信号允许的最大开收盘差(点)。
Coefficient 动态置信阈值的倍率。
MaPeriod 指标内部的移动平均长度,必须不少于 3。
CandleType 处理的 K 线周期(默认 1 小时)。

使用建议

  • 确保交易品种提供有效的 PriceStep。若缺失,策略会退化为 1.0 的步长,这可能导致阈值失真。
  • 根据所选周期的波动性调整 MinCriteriaPointsMaxCriteriaPoints。窗口过窄会过滤掉大部分信号,过宽则可能允许极端行情进入。
  • 默认的 Coefficient 为 1.618,对应原始指标使用的黄金比例缩放。数值越大,需要的异常 K 线越极端。
  • 策略在下一根 K 线触发止损或止盈时,通过市价单平仓,实际成交价格可能与标记的价位略有差异。必要时可结合更细粒度数据测试。
  • 策略一次仅持有一个仓位,会等待当前交易结束后才响应下一次信号。

备注

  • 在真实交易之前务必先使用历史数据回测。原始 EA 针对外汇市场设计,在其他品种上可能需要重新调参。
  • SlippagePoints 参数保留用于说明,与 MetaTrader 的写法保持一致,但在 StockSharp 中默认不直接使用。
  • 请确保 MaPeriod 不小于 3;原始实现中禁止更小的数值,以避免阈值过于敏感。
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>
/// Strategy that reacts to large single-bar moves expecting a mean reversion.
/// </summary>
public class InverseReactionStrategy : Strategy
{
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _coefficient;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _absChanges = new();

	public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public decimal TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
	public decimal Coefficient { get => _coefficient.Value; set => _coefficient.Value = value; }
	public int MaPeriod { get => _maPeriod.Value; set => _maPeriod.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public InverseReactionStrategy()
	{
		_stopLossPoints = Param(nameof(StopLossPoints), 1000m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss", "Stop-loss distance in points", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 250m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit", "Take-profit distance in points", "Risk");

		_coefficient = Param(nameof(Coefficient), 1.618m)
			.SetGreaterThanZero()
			.SetDisplay("Coefficient", "Confidence coefficient", "Signal");

		_maPeriod = Param(nameof(MaPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Moving average length", "Signal");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe", "General");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_absChanges.Clear();
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m) step = 1m;

		StartProtection(
			takeProfit: new Unit(TakeProfitPoints * step, UnitTypes.Absolute),
			stopLoss: new Unit(StopLossPoints * step, UnitTypes.Absolute),
			useMarketOrders: true);

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

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

		var change = candle.ClosePrice - candle.OpenPrice;
		var absChange = Math.Abs(change);

		_absChanges.Add(absChange);
		if (_absChanges.Count > MaPeriod)
			_absChanges.RemoveAt(0);

		if (_absChanges.Count < MaPeriod)
			return;

		var avg = 0m;
		for (int i = 0; i < _absChanges.Count; i++)
			avg += _absChanges[i];
		avg /= _absChanges.Count;

		var threshold = avg * Coefficient;

		if (Position != 0)
			return;

		if (absChange > threshold && absChange > 0m)
		{
			if (change < 0m)
			{
				// Large bearish bar => expect reversion upward
				BuyMarket();
			}
			else
			{
				// Large bullish bar => expect reversion downward
				SellMarket();
			}
		}
	}
}