在 GitHub 上查看

Hpcs Inter6 RSI 策略

概述

Hpcs Inter6 RSI 策略将 MetaTrader 专家顾问 _HPCS_Inter6_MT4_EA_V01_WE 移植到 StockSharp 高级 API。算法观察可配置周期的相对强弱指标(RSI),关注围绕经典 70/30 阈值的快速反转。当 RSI 向上穿越 70 时策略转为空头,当 RSI 向下穿越 30 时策略转为多头。每次入场都会立即设置对称的止盈和止损,距离以点(pip)表示。

数据与指标

  • K线来源:用户可配置的时间框架(默认 1 小时)。
  • 指标:可配置周期的相对强弱指标(默认 14),通过 StockSharp 指标绑定流水线重新计算。

入场逻辑

  1. 策略只在 K 线收盘后工作,避免使用未完成的数据。
  2. 每根收盘 K 线比较新的 RSI 数值与前一根的数值。
  3. 做空条件:若 RSI 刚从下方穿越 UpperLevel(默认 70),策略以市价卖出。若存在多头头寸,会先平掉多头,再建立与配置交易量相等的净空头。
  4. 做多条件:若 RSI 刚从上方穿越 LowerLevel(默认 30),策略以市价买入。若存在空头头寸,会先回补空头,再建立与配置交易量相等的净多头。
  5. 每根 K 线最多触发一次信号;同一根 K 线上的重复信号会被忽略,以匹配原始 EA 使用 K 线时间戳的保护机制。

离场逻辑

  • 每次入场都会设置相同距离的止盈和止损。
  • 持有多头时,如果 K 线最高价触及止盈或最低价触及止损,立即平仓。
  • 持有空头时,如果 K 线最低价触及止盈或最高价触及止损,立即平仓。
  • 空仓时会清除所有保护价位。

点距会根据交易标的的最小报价单位进行换算。若标的使用三位或五位小数,算法会将距离乘以 10,以符合 MetaTrader 中对 1 点(pip)的定义。

参数

参数 默认值 说明
CandleType 1 小时时间框架 提供 RSI 计算所需的 K 线。
RsiLength 14 RSI 的回溯周期。
UpperLevel 70 RSI 向上穿越该值时触发做空。
LowerLevel 30 RSI 向下穿越该值时触发做多。
TradeVolume 1 市价单的下单手数,反向持仓会在入场前平掉。
OffsetInPips 10 止盈和止损距入场价的距离(单位:点)。

所有参数均通过 StrategyParam 暴露,可在 StockSharp 中执行优化。

说明

  • 策略使用 K 线的最高价与最低价来模拟止盈止损的触发,贴近 MetaTrader 的固定价位行为。
  • 不会挂出挂单,所有交易均通过市价单完成。
  • 如果界面中存在图表区域,策略会自动绘制价格与 RSI 指标,方便监控。
using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Port of the MetaTrader strategy _HPCS_Inter6_MT4_EA_V01_WE.
/// Trades RSI reversals at the 70/30 levels with symmetric fixed targets and stops.
/// </summary>
public class HpcsInter6RsiStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rsiLength;
	private readonly StrategyParam<decimal> _upperLevel;
	private readonly StrategyParam<decimal> _lowerLevel;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<decimal> _offsetInPips;
	private readonly StrategyParam<int> _signalCooldownCandles;

	private RelativeStrengthIndex _rsi;
	private decimal? _previousRsi;
	private DateTimeOffset? _lastSignalTime;
	private decimal? _targetPrice;
	private decimal? _stopPrice;
	private bool _isLongPosition;
	private int _candlesSinceTrade;

	/// <summary>
	/// Candle type used for the RSI evaluation.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// RSI lookback length.
	/// </summary>
	public int RsiLength
	{
		get => _rsiLength.Value;
		set => _rsiLength.Value = value;
	}

	/// <summary>
	/// Upper RSI level that triggers short entries when crossed from below.
	/// </summary>
	public decimal UpperLevel
	{
		get => _upperLevel.Value;
		set => _upperLevel.Value = value;
	}

	/// <summary>
	/// Lower RSI level that triggers long entries when crossed from above.
	/// </summary>
	public decimal LowerLevel
	{
		get => _lowerLevel.Value;
		set => _lowerLevel.Value = value;
	}

	/// <summary>
	/// Market order volume used for entries.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Target and stop distance expressed in pips.
	/// </summary>
	public decimal OffsetInPips
	{
		get => _offsetInPips.Value;
		set => _offsetInPips.Value = value;
	}

	public int SignalCooldownCandles
	{
		get => _signalCooldownCandles.Value;
		set => _signalCooldownCandles.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="HpcsInter6RsiStrategy"/> class.
	/// </summary>
	public HpcsInter6RsiStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for RSI evaluation", "General");

		_rsiLength = Param(nameof(RsiLength), 7)
			.SetGreaterThanZero()
			.SetDisplay("RSI Length", "Lookback period for RSI", "Parameters")

			.SetOptimize(5, 40, 1);

		_upperLevel = Param(nameof(UpperLevel), 65m)
			.SetDisplay("Upper RSI", "Upper RSI level for shorts", "Parameters")

			.SetOptimize(60m, 90m, 5m);

		_lowerLevel = Param(nameof(LowerLevel), 35m)
			.SetDisplay("Lower RSI", "Lower RSI level for longs", "Parameters")

			.SetOptimize(10m, 40m, 5m);

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Order volume for entries", "Trading")
			
			.SetOptimize(0.1m, 5m, 0.1m);

		_offsetInPips = Param(nameof(OffsetInPips), 30m)
			.SetGreaterThanZero()
			.SetDisplay("Offset (pips)", "Target and stop distance in pips", "Risk")
			
			.SetOptimize(5m, 30m, 5m);

		_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 4)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait between entries", "Trading");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_rsi = null;
		_previousRsi = null;
		_lastSignalTime = null;
		_targetPrice = null;
		_stopPrice = null;
		_isLongPosition = false;
		_candlesSinceTrade = SignalCooldownCandles;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_previousRsi = null;
		_lastSignalTime = null;
		_targetPrice = null;
		_stopPrice = null;
		_isLongPosition = false;
		_candlesSinceTrade = SignalCooldownCandles;

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiLength
		};

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(_rsi, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _rsi);
		}
	}

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

		if (!_rsi.IsFormed)
			return;

		if (_candlesSinceTrade < SignalCooldownCandles)
			_candlesSinceTrade++;

		UpdateActivePositionTargets(candle);

		var previousRsi = _previousRsi;
		_previousRsi = rsiValue;

		if (previousRsi is null)
			return;

		var candleTime = candle.OpenTime;

		if (_lastSignalTime.HasValue && _lastSignalTime.Value == candleTime)
			return;

		if (_candlesSinceTrade >= SignalCooldownCandles && TryEnterShort(candle, rsiValue, previousRsi.Value))
		{
			_lastSignalTime = candleTime;
			_candlesSinceTrade = 0;
			return;
		}

		if (_candlesSinceTrade >= SignalCooldownCandles && TryEnterLong(candle, rsiValue, previousRsi.Value))
		{
			_lastSignalTime = candleTime;
			_candlesSinceTrade = 0;
		}
	}

	private void UpdateActivePositionTargets(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (!_isLongPosition)
			{
				_targetPrice = null;
				_stopPrice = null;
				return;
			}

			var shouldExit = (_targetPrice.HasValue && candle.HighPrice >= _targetPrice.Value)
				|| (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value);

			if (shouldExit)
			{
				SellMarket(Math.Abs(Position));
				_targetPrice = null;
				_stopPrice = null;
			}
		}
		else if (Position < 0)
		{
			if (_isLongPosition)
			{
				_targetPrice = null;
				_stopPrice = null;
				return;
			}

			var shouldExit = (_targetPrice.HasValue && candle.LowPrice <= _targetPrice.Value)
				|| (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value);

			if (shouldExit)
			{
				BuyMarket(Math.Abs(Position));
				_targetPrice = null;
				_stopPrice = null;
			}
		}
		else
		{
			_targetPrice = null;
			_stopPrice = null;
			_isLongPosition = false;
		}
	}

	private bool TryEnterShort(ICandleMessage candle, decimal currentRsi, decimal previousRsi)
	{
		if (!(currentRsi > UpperLevel && previousRsi <= UpperLevel))
			return false;

		var volume = TradeVolume;
		if (volume <= 0m)
			return false;

		if (Position > 0)
		{
			volume += Math.Abs(Position);
		}

		SellMarket(volume);

		var offset = CalculateOffset();
		if (offset > 0m)
		{
			var entryPrice = candle.ClosePrice;
			_targetPrice = entryPrice - offset;
			_stopPrice = entryPrice + offset;
			_isLongPosition = false;
		}
		else
		{
			_targetPrice = null;
			_stopPrice = null;
			_isLongPosition = false;
		}

		return true;
	}

	private bool TryEnterLong(ICandleMessage candle, decimal currentRsi, decimal previousRsi)
	{
		if (!(currentRsi < LowerLevel && previousRsi >= LowerLevel))
			return false;

		var volume = TradeVolume;
		if (volume <= 0m)
			return false;

		if (Position < 0)
		{
			volume += Math.Abs(Position);
		}

		BuyMarket(volume);

		var offset = CalculateOffset();
		if (offset > 0m)
		{
			var entryPrice = candle.ClosePrice;
			_targetPrice = entryPrice + offset;
			_stopPrice = entryPrice - offset;
			_isLongPosition = true;
		}
		else
		{
			_targetPrice = null;
			_stopPrice = null;
			_isLongPosition = true;
		}

		return true;
	}

	private decimal CalculateOffset()
	{
		var priceStep = Security?.PriceStep ?? 0.01m;
		if (priceStep <= 0m)
			priceStep = 0.01m;

		var decimals = Security?.Decimals ?? 0;
		var factor = decimals is 3 or 5 ? 10m : 1m;

		return OffsetInPips * priceStep * factor;
	}
}