在 GitHub 上查看

Improve MA & RSI Hedge 策略

本策略将 MetaTrader 的 “Improve” EA 迁移到 StockSharp 平台。它同时交易两个品种:在策略中选择的主品种以及用于对冲的品种。交易方向由主品种上的两条平滑移动平均线(SMMA)以及 RSI 指标共同决定。对冲腿与主腿保持同方向,从而构建协同的双品种头寸,目标是在相关资产同步运动时捕捉收益,同时降低单一品种的风险。

策略逻辑

  • 在主品种上计算两条平滑移动平均线(SMMA),可分别设置快线与慢线周期。
  • 计算同周期的 RSI,并监控超卖/超买水平。
  • 当慢线位于快线上方且 RSI 低于或等于超卖阈值时,同时在两个品种上开多仓。
  • 当慢线位于快线下方且 RSI 高于或等于超买阈值时,同时在两个品种上开空仓。
  • 持仓一直保留,直到两个品种的合计浮动利润达到设定的货币目标,然后通过市价单同时平掉两条腿。

算法保存每个品种最新的收盘价,并使用入场价与当前价的差值估算总体利润。由于没有内置止损,如果价格未触及盈利目标,仓位可能会长期持有。

参数

参数 说明
Volume 主腿与对冲腿的下单数量。
Profit Target 两条腿共享的盈利目标(货币单位)。达到后立刻平仓。
Hedge Security 与主品种同时交易的对冲品种。
Fast MA 快速 SMMA 的周期(默认 8)。
Slow MA 慢速 SMMA 的周期(默认 21),必须大于快速周期。
RSI Period RSI 的计算长度(默认 21)。
Oversold RSI 的超卖阈值,满足条件并配合均线关系触发做多(默认 30)。
Overbought RSI 的超买阈值,满足条件并配合均线关系触发做空(默认 70)。
Candle Type 计算所用的蜡烛周期。默认 1 小时,可按需求调整。

使用指标

  • 平滑移动平均线(SMMA):用于判定趋势方向(快/慢两条)。
  • 相对强弱指标(RSI):用于识别超卖与超买区域。

入场与出场规则

  1. 做多
    • 慢速 SMMA 高于快速 SMMA。
    • RSI ≤ 超卖阈值。
    • 在主品种和对冲品种上同时发送买入市价单。
  2. 做空
    • 慢速 SMMA 低于快速 SMMA。
    • RSI ≥ 超买阈值。
    • 在两个品种上同时发送卖出市价单。
  3. 平仓
    • (主腿利润 + 对冲腿利润) ≥ Profit Target 时,通过市价单同时平掉所有仓位。
    • 策略不包含额外的止损或追踪止损,如需风险控制应另外实现。

使用建议

  • 启动策略前必须指定主品种和对冲品种,否则策略会抛出异常。
  • 合计利润基于蜡烛收盘价估算,实际成交可能受滑点和执行速度影响。
  • 建议用于高度相关的品种组合(如相关货币对或同类期货),预期出现同步走势。
  • 实盘时应结合额外的资金管理或组合风险控制,因为策略仅依赖虚拟盈利目标作为退出机制。
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 StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Dual smoothed moving average and RSI hedge strategy converted from Improve.mq5.
/// </summary>
public class ImproveMaRsiHedgeStrategy : Strategy
{
	private readonly StrategyParam<decimal> _profitTarget;
	private readonly StrategyParam<Security> _hedgeSecurity;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _oversoldLevel;
	private readonly StrategyParam<decimal> _overboughtLevel;
	private readonly StrategyParam<DataType> _candleType;

	private SmoothedMovingAverage _fastMa = null!;
	private SmoothedMovingAverage _slowMa = null!;
	private RelativeStrengthIndex _rsi = null!;

	private decimal _baseLastClose;
	private decimal _hedgeLastClose;
	private decimal _baseEntryPrice;
	private decimal _hedgeEntryPrice;
	private bool _hasBaseClose;
	private bool _hasHedgeClose;
	private int _pairDirection;


	/// <summary>
	/// Profit target across both legs expressed in money.
	/// </summary>
	public decimal ProfitTarget
	{
		get => _profitTarget.Value;
		set => _profitTarget.Value = value;
	}

	/// <summary>
	/// Second instrument traded alongside the primary security.
	/// </summary>
	public Security HedgeSecurity
	{
		get => _hedgeSecurity.Value;
		set => _hedgeSecurity.Value = value;
	}

	/// <summary>
	/// Smoothed moving average period for the fast line.
	/// </summary>
	public int FastMaPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Smoothed moving average period for the slow line.
	/// </summary>
	public int SlowMaPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// RSI calculation length.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// RSI oversold threshold.
	/// </summary>
	public decimal OversoldLevel
	{
		get => _oversoldLevel.Value;
		set => _oversoldLevel.Value = value;
	}

	/// <summary>
	/// RSI overbought threshold.
	/// </summary>
	public decimal OverboughtLevel
	{
		get => _overboughtLevel.Value;
		set => _overboughtLevel.Value = value;
	}

	/// <summary>
	/// Type of candles used for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="ImproveMaRsiHedgeStrategy"/> class.
	/// </summary>
	public ImproveMaRsiHedgeStrategy()
	{

		_profitTarget = Param(nameof(ProfitTarget), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Profit Target", "Combined profit target across both legs", "Risk")
			;

		_hedgeSecurity = Param<Security>(nameof(HedgeSecurity))
			.SetDisplay("Hedge Security", "Secondary instrument to trade", "General");

		_fastPeriod = Param(nameof(FastMaPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("Fast MA", "Fast smoothed MA period", "Indicators")
			;

		_slowPeriod = Param(nameof(SlowMaPeriod), 21)
			.SetGreaterThanZero()
			.SetDisplay("Slow MA", "Slow smoothed MA period", "Indicators")
			;

		_rsiPeriod = Param(nameof(RsiPeriod), 21)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Length of the RSI", "Indicators")
			;

		_oversoldLevel = Param(nameof(OversoldLevel), 30m)
			.SetDisplay("Oversold", "RSI oversold threshold", "Indicators")
			;

		_overboughtLevel = Param(nameof(OverboughtLevel), 70m)
			.SetDisplay("Overbought", "RSI overbought threshold", "Indicators")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for calculations", "Data");
	}

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

		if (HedgeSecurity != null)
			yield return (HedgeSecurity, CandleType);
	}

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

		_fastMa = null!;
		_slowMa = null!;
		_rsi = null!;
		_baseLastClose = 0m;
		_hedgeLastClose = 0m;
		_baseEntryPrice = 0m;
		_hedgeEntryPrice = 0m;
		_hasBaseClose = false;
		_hasHedgeClose = false;
		_pairDirection = 0;
	}

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

		if (Security == null)
			throw new InvalidOperationException("Primary security must be specified.");

		if (HedgeSecurity == null)
			throw new InvalidOperationException("Hedge security must be specified.");

		if (FastMaPeriod >= SlowMaPeriod)
			throw new InvalidOperationException("Fast MA period must be less than slow MA period.");

		_fastMa = new SmoothedMovingAverage { Length = FastMaPeriod };
		_slowMa = new SmoothedMovingAverage { Length = SlowMaPeriod };
		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

		var baseSubscription = SubscribeCandles(CandleType);
		baseSubscription
			.Bind(_fastMa, _slowMa, _rsi, ProcessBaseCandle)
			.Start();

		var hedgeSubscription = SubscribeCandles(CandleType, false, HedgeSecurity);
		hedgeSubscription
			.Bind(ProcessHedgeCandle)
			.Start();
	}

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

		_baseLastClose = candle.ClosePrice;
		_hasBaseClose = true;

		CheckProfitTarget();

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (!_fastMa.IsFormed || !_slowMa.IsFormed || !_rsi.IsFormed)
			return;

		if (_pairDirection != 0)
			return;

		if (!_hasHedgeClose)
			return;

		if (slowValue > fastValue && rsiValue <= OversoldLevel)
		{
			OpenPair(1);
		}
		else if (slowValue < fastValue && rsiValue >= OverboughtLevel)
		{
			OpenPair(-1);
		}
	}

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

		_hedgeLastClose = candle.ClosePrice;
		_hasHedgeClose = true;

		CheckProfitTarget();
	}

	private void OpenPair(int direction)
	{
		if (direction == 0)
			return;

		var basePos = GetPositionValue(Security, Portfolio) ?? 0m;
		var hedgePos = GetPositionValue(HedgeSecurity, Portfolio) ?? 0m;

		if (basePos != 0m || hedgePos != 0m)
			return;

		var volume = Volume;

		if (direction > 0)
		{
			BuyMarket(volume, Security);
			BuyMarket(volume, HedgeSecurity);
		}
		else
		{
			SellMarket(volume, Security);
			SellMarket(volume, HedgeSecurity);
		}

		_pairDirection = direction;
		_baseEntryPrice = _baseLastClose;
		_hedgeEntryPrice = _hedgeLastClose;
	}

	private void CheckProfitTarget()
	{
		if (_pairDirection == 0 || !_hasBaseClose || !_hasHedgeClose)
			return;

		var baseProfit = _pairDirection > 0
			? (_baseLastClose - _baseEntryPrice) * Volume
			: (_baseEntryPrice - _baseLastClose) * Volume;

		var hedgeProfit = _pairDirection > 0
			? (_hedgeLastClose - _hedgeEntryPrice) * Volume
			: (_hedgeEntryPrice - _hedgeLastClose) * Volume;

		var totalProfit = baseProfit + hedgeProfit;

		if (totalProfit >= ProfitTarget)
		{
			ClosePair();
		}
	}

	private void ClosePair()
	{
		var basePos = GetPositionValue(Security, Portfolio) ?? 0m;
		if (basePos > 0)
		{
			SellMarket(basePos, Security);
		}
		else if (basePos < 0)
		{
			BuyMarket(-basePos, Security);
		}

		var hedgePos = GetPositionValue(HedgeSecurity, Portfolio) ?? 0m;
		if (hedgePos > 0)
		{
			SellMarket(hedgePos, HedgeSecurity);
		}
		else if (hedgePos < 0)
		{
			BuyMarket(-hedgePos, HedgeSecurity);
		}

		_pairDirection = 0;
		_baseEntryPrice = 0m;
		_hedgeEntryPrice = 0m;
	}
}