在 GitHub 上查看

Altarius RSI 随机指标策略

概述

Altarius RSI 随机指标策略是将 MetaTrader 5 专家顾问 “Altarius RSI Stohastic” 迁移到 StockSharp 高级 API 的版本。策略通过两组随机指标与一个 3 周期 RSI 的组合,捕捉动量在压缩后突然扩张时出现的短期反转。移植后的实现保留了原有的开仓、平仓逻辑,并加入参数化设置、自动风险控制以及自适应仓位管理。

工作原理

  • 主随机指标(15/8/8):作为趋势过滤器。做多要求 %K 位于 50 以下并向上穿越 %D,表示价格在中性偏超卖区域内向上加速;做空条件为镜像逻辑,要求 %K 高于 55 且向下穿越 %D。
  • 次随机指标(10/3/3):衡量 %K 与 %D 的偏离幅度。只有当两者的绝对差值大于 5 点时才允许进场,以确认动量强度。
  • RSI(周期 3):负责平仓。多单在 RSI 超过 60 且主随机 %D 从高位(>70)转头向下时平仓;空单在 RSI 低于 40 且主随机 %D 从低位(<30)向上拐头时平仓。
  • 回撤保护:若浮动盈亏跌破账户权益乘以风险系数 MaximumRisk,策略会立即平掉持仓,模拟原始 EA 中的紧急止损功能。
  • 动态仓位:初始下单量按照账户权益乘以 MaximumRisk 再除以 1000 计算,与 MT5 版本一致。连续亏损会依据 DecreaseFactor 缩减仓位,同时不会低于 MinimumVolume 设定的最小交易量。

参数

参数 说明 默认值
CandleType 订阅的 K 线时间框架。 5 分钟
BaseVolume 当无法获取账户信息时使用的基础下单量。 0.1
MinimumVolume 仓位调整后的最小成交量。 0.1
MaximumRisk 用于头寸调整和回撤平仓的风险系数。 0.1
DecreaseFactor 连续亏损时缩减仓位的分母。 3
PrimaryStochasticLength 主随机指标 %K 的回看周期。 15
PrimaryStochasticKPeriod 主随机 %K 的平滑周期。 8
PrimaryStochasticDPeriod 主随机 %D 的信号周期。 8
SecondaryStochasticLength 次随机指标的回看周期。 10
SecondaryStochasticKPeriod 次随机 %K 的平滑周期。 3
SecondaryStochasticDPeriod 次随机 %D 的信号周期。 3
DifferenceThreshold 次随机 %K 与 %D 的最小差值,用于确认动量。 5
PrimaryBuyLimit 多单开仓时主随机 %K 允许的最大值。 50
PrimarySellLimit 空单开仓时主随机 %K 必须高于的阈值。 55
PrimaryExitUpper 多单平仓所需的主随机 %D 下限。 70
PrimaryExitLower 空单平仓所需的主随机 %D 上限。 30
RsiPeriod RSI 指标回看周期。 3
LongExitRsi 触发多单平仓的 RSI 数值。 60
ShortExitRsi 触发空单平仓的 RSI 数值。 40

交易规则

  1. 入场条件
    • 多单:主随机 %K > %D、主随机 %K < PrimaryBuyLimit,且 |次随机 %K − 次随机 %D| > DifferenceThreshold,并且策略当前为空仓。
    • 空单:主随机 %K < %D、主随机 %K > PrimarySellLimit,且 |次随机 %K − 次随机 %D| > DifferenceThreshold,并且策略当前为空仓。
  2. 离场条件
    • 多单平仓:RSI > LongExitRsi,主随机 %D > PrimaryExitUpper,且当前 %D 低于上一根 K 线的数值。
    • 空单平仓:RSI < ShortExitRsi,主随机 %D < PrimaryExitLower,且当前 %D 高于上一根 K 线的数值。
    • 风险平仓:浮亏绝对值 ≥ MaximumRisk × Portfolio.CurrentValue 时立即平仓。

风险管理

  • 策略启动时调用 StartProtection(),启用 StockSharp 的仓位保护服务。
  • _lossStreak(连续亏损次数)大于 1 时,CalculateTradeVolume() 会使用 DecreaseFactor 缩减仓位。
  • MinimumVolume 防止仓位过小而不符合交易所最小变动要求。

备注

  • 策略设计默认支持对冲账户,与原 MT5 专家顾问保持一致。
  • 可以调整 CandleType 以匹配原本在 MetaTrader 中使用的周期(如 M1、M5 等)。
  • 推荐结合本仓库中的 Backtester 或 StockSharp Designer 验证策略在不同市场数据下的表现。
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>
/// Altarius RSI Stochastic strategy converted from the original MQL implementation.
/// Combines two Stochastic oscillators with RSI exits and adaptive position sizing.
/// </summary>
public class AltariusRsiStochasticStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _minimumVolume;
	private readonly StrategyParam<decimal> _maximumRisk;
	private readonly StrategyParam<decimal> _decreaseFactor;
	private readonly StrategyParam<int> _primaryStochasticLength;
	private readonly StrategyParam<int> _primaryStochasticKPeriod;
	private readonly StrategyParam<int> _primaryStochasticDPeriod;
	private readonly StrategyParam<int> _secondaryStochasticLength;
	private readonly StrategyParam<int> _secondaryStochasticKPeriod;
	private readonly StrategyParam<int> _secondaryStochasticDPeriod;
	private readonly StrategyParam<decimal> _differenceThreshold;
	private readonly StrategyParam<decimal> _primaryBuyLimit;
	private readonly StrategyParam<decimal> _primarySellLimit;
	private readonly StrategyParam<decimal> _primaryExitUpper;
	private readonly StrategyParam<decimal> _primaryExitLower;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _longExitRsi;
	private readonly StrategyParam<decimal> _shortExitRsi;

	private decimal _prevPrimarySignal;
	private bool _hasPrevSignal;
	private decimal _entryPrice;
	private int _positionDirection;
	private int _lossStreak;

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

	/// <summary>
	/// Base volume used when account information is not available.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Minimum allowed trade volume.
	/// </summary>
	public decimal MinimumVolume
	{
		get => _minimumVolume.Value;
		set => _minimumVolume.Value = value;
	}

	/// <summary>
	/// Risk multiplier used for volume sizing and drawdown exit.
	/// </summary>
	public decimal MaximumRisk
	{
		get => _maximumRisk.Value;
		set => _maximumRisk.Value = value;
	}

	/// <summary>
	/// Factor that reduces volume after consecutive losses.
	/// </summary>
	public decimal DecreaseFactor
	{
		get => _decreaseFactor.Value;
		set => _decreaseFactor.Value = value;
	}

	/// <summary>
	/// Period for the primary Stochastic oscillator.
	/// </summary>
	public int PrimaryStochasticLength
	{
		get => _primaryStochasticLength.Value;
		set => _primaryStochasticLength.Value = value;
	}

	/// <summary>
	/// %K smoothing period for the primary Stochastic oscillator.
	/// </summary>
	public int PrimaryStochasticKPeriod
	{
		get => _primaryStochasticKPeriod.Value;
		set => _primaryStochasticKPeriod.Value = value;
	}

	/// <summary>
	/// %D period for the primary Stochastic oscillator.
	/// </summary>
	public int PrimaryStochasticDPeriod
	{
		get => _primaryStochasticDPeriod.Value;
		set => _primaryStochasticDPeriod.Value = value;
	}

	/// <summary>
	/// Period for the secondary Stochastic oscillator.
	/// </summary>
	public int SecondaryStochasticLength
	{
		get => _secondaryStochasticLength.Value;
		set => _secondaryStochasticLength.Value = value;
	}

	/// <summary>
	/// %K smoothing period for the secondary Stochastic oscillator.
	/// </summary>
	public int SecondaryStochasticKPeriod
	{
		get => _secondaryStochasticKPeriod.Value;
		set => _secondaryStochasticKPeriod.Value = value;
	}

	/// <summary>
	/// %D period for the secondary Stochastic oscillator.
	/// </summary>
	public int SecondaryStochasticDPeriod
	{
		get => _secondaryStochasticDPeriod.Value;
		set => _secondaryStochasticDPeriod.Value = value;
	}

	/// <summary>
	/// Minimum gap between %K and %D on the secondary Stochastic to confirm momentum.
	/// </summary>
	public decimal DifferenceThreshold
	{
		get => _differenceThreshold.Value;
		set => _differenceThreshold.Value = value;
	}

	/// <summary>
	/// Upper bound for primary %K during long entries.
	/// </summary>
	public decimal PrimaryBuyLimit
	{
		get => _primaryBuyLimit.Value;
		set => _primaryBuyLimit.Value = value;
	}

	/// <summary>
	/// Lower bound for primary %K during short entries.
	/// </summary>
	public decimal PrimarySellLimit
	{
		get => _primarySellLimit.Value;
		set => _primarySellLimit.Value = value;
	}

	/// <summary>
	/// Minimum primary %D level to trigger long exits.
	/// </summary>
	public decimal PrimaryExitUpper
	{
		get => _primaryExitUpper.Value;
		set => _primaryExitUpper.Value = value;
	}

	/// <summary>
	/// Maximum primary %D level to trigger short exits.
	/// </summary>
	public decimal PrimaryExitLower
	{
		get => _primaryExitLower.Value;
		set => _primaryExitLower.Value = value;
	}

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

	/// <summary>
	/// RSI threshold that closes long positions.
	/// </summary>
	public decimal LongExitRsi
	{
		get => _longExitRsi.Value;
		set => _longExitRsi.Value = value;
	}

	/// <summary>
	/// RSI threshold that closes short positions.
	/// </summary>
	public decimal ShortExitRsi
	{
		get => _shortExitRsi.Value;
		set => _shortExitRsi.Value = value;
	}

	/// <summary>
	/// Initialize parameters for the strategy.
	/// </summary>
	public AltariusRsiStochasticStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used for calculations", "General");

		_baseVolume = Param(nameof(BaseVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Fallback volume when portfolio data is unavailable", "Position Sizing");

		_minimumVolume = Param(nameof(MinimumVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Minimum Volume", "Smallest volume allowed for orders", "Position Sizing");

		_maximumRisk = Param(nameof(MaximumRisk), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Risk Factor", "Risk multiplier used for sizing and drawdown control", "Risk");

		_decreaseFactor = Param(nameof(DecreaseFactor), 3m)
			.SetGreaterThanZero()
			.SetDisplay("Decrease Factor", "Divider applied after losing trades", "Risk");

		_primaryStochasticLength = Param(nameof(PrimaryStochasticLength), 15)
			.SetGreaterThanZero()
			.SetDisplay("Primary %K Length", "Lookback for primary Stochastic", "Primary Stochastic");

		_primaryStochasticKPeriod = Param(nameof(PrimaryStochasticKPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("Primary %K Smoothing", "Smoothing for primary %K", "Primary Stochastic");

		_primaryStochasticDPeriod = Param(nameof(PrimaryStochasticDPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("Primary %D Period", "Signal period for primary Stochastic", "Primary Stochastic");

		_secondaryStochasticLength = Param(nameof(SecondaryStochasticLength), 10)
			.SetGreaterThanZero()
			.SetDisplay("Secondary %K Length", "Lookback for secondary Stochastic", "Secondary Stochastic");

		_secondaryStochasticKPeriod = Param(nameof(SecondaryStochasticKPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Secondary %K Smoothing", "Smoothing for secondary %K", "Secondary Stochastic");

		_secondaryStochasticDPeriod = Param(nameof(SecondaryStochasticDPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("Secondary %D Period", "Signal period for secondary Stochastic", "Secondary Stochastic");

		_differenceThreshold = Param(nameof(DifferenceThreshold), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Signal Gap", "Minimum gap between %K and %D on the fast Stochastic", "Entries");

		_primaryBuyLimit = Param(nameof(PrimaryBuyLimit), 50m)
			.SetDisplay("Primary Buy Cap", "Primary %K must stay below this level for longs", "Entries");

		_primarySellLimit = Param(nameof(PrimarySellLimit), 55m)
			.SetDisplay("Primary Sell Floor", "Primary %K must stay above this level for shorts", "Entries");

		_primaryExitUpper = Param(nameof(PrimaryExitUpper), 70m)
			.SetDisplay("Long Exit %D", "Primary %D threshold that ends long trades", "Exits");

		_primaryExitLower = Param(nameof(PrimaryExitLower), 30m)
			.SetDisplay("Short Exit %D", "Primary %D threshold that ends short trades", "Exits");

		_rsiPeriod = Param(nameof(RsiPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Lookback for RSI filter", "Indicators");

		_longExitRsi = Param(nameof(LongExitRsi), 60m)
			.SetDisplay("RSI Exit Long", "RSI value that closes long positions", "Exits");

		_shortExitRsi = Param(nameof(ShortExitRsi), 40m)
			.SetDisplay("RSI Exit Short", "RSI value that closes short positions", "Exits");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevPrimarySignal = 0m;
		_hasPrevSignal = false;
		_entryPrice = 0m;
		_positionDirection = 0;
		_lossStreak = 0;
	}

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

		var primaryStochastic = new StochasticOscillator
		{
			K = { Length = PrimaryStochasticLength },
			D = { Length = PrimaryStochasticDPeriod },
		};

		var secondaryStochastic = new StochasticOscillator
		{
			K = { Length = SecondaryStochasticLength },
			D = { Length = SecondaryStochasticDPeriod },
		};

		var rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod,
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(primaryStochastic, secondaryStochastic, rsi, ProcessCandle)
			.Start();

		// No protection (TP/SL handled internally).
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue primaryValue, IIndicatorValue secondaryValue, IIndicatorValue rsiValue)
	{
		// Trade only on finished candles to avoid intrabar noise.
		if (candle.State != CandleStates.Finished)
			return;

		if (!primaryValue.IsFinal || !secondaryValue.IsFinal || !rsiValue.IsFinal)
			return;

		var primary = (IStochasticOscillatorValue)primaryValue;
		var secondary = (IStochasticOscillatorValue)secondaryValue;

		if (primary.K is not decimal primaryMain || primary.D is not decimal primarySignal)
			return;

		if (secondary.K is not decimal secondaryMain || secondary.D is not decimal secondarySignal)
			return;

		var rsi = rsiValue.GetValue<decimal>();
		var difference = Math.Abs(secondaryMain - secondarySignal);

		// Emergency drawdown exit replicates the account-level risk guard from MQL.
		if (Position != 0)
		{
			var accountValue = Portfolio?.CurrentValue ?? 0m;
			var riskLimit = accountValue * MaximumRisk;
			if (PnL < 0m && riskLimit > 0m && Math.Abs(PnL) >= riskLimit)
			{
				ClosePosition(candle.ClosePrice);
				UpdatePrimarySignal(primarySignal);
				return;
			}
		}

	var canTrade = IsFormedAndOnlineAndAllowTrading();

	if (Position == 0)
	{
		if (!canTrade)
		{
			UpdatePrimarySignal(primarySignal);
			return;
		}

		var bullishSetup = primaryMain > primarySignal && primaryMain < PrimaryBuyLimit && difference > DifferenceThreshold;
		var bearishSetup = primaryMain < primarySignal && primaryMain > PrimarySellLimit && difference > DifferenceThreshold;

		if (bullishSetup)
		{
			var volume = CalculateTradeVolume();
			if (volume > 0m)
			{
				BuyMarket(volume);
				_entryPrice = candle.ClosePrice;
				_positionDirection = 1;
			}
		}
		else if (bearishSetup)
		{
			var volume = CalculateTradeVolume();
			if (volume > 0m)
			{
				SellMarket(volume);
				_entryPrice = candle.ClosePrice;
				_positionDirection = -1;
			}
		}
	}
	else if (canTrade)
	{
		if (Position > 0)
		{
			var exitSignal = rsi > LongExitRsi && _hasPrevSignal && primarySignal < _prevPrimarySignal && primarySignal > PrimaryExitUpper;
			if (exitSignal)
				ClosePosition(candle.ClosePrice);
		}
		else if (Position < 0)
		{
			var exitSignal = rsi < ShortExitRsi && _hasPrevSignal && primarySignal > _prevPrimarySignal && primarySignal < PrimaryExitLower;
			if (exitSignal)
				ClosePosition(candle.ClosePrice);
		}
	}

	UpdatePrimarySignal(primarySignal);
	}

	private decimal CalculateTradeVolume()
	{
		var volume = BaseVolume;
		var accountValue = Portfolio?.CurrentValue;

		// Derive lot size from account equity similar to the original MQL logic.
		if (accountValue is decimal value && value > 0m)
		{
			var riskVolume = Math.Round(value * MaximumRisk / 1000m, 2, MidpointRounding.AwayFromZero);
			if (riskVolume > 0m)
				volume = riskVolume;
		}

		if (DecreaseFactor > 0m && _lossStreak > 1)
		{
			var reduction = volume * _lossStreak / DecreaseFactor;
			volume = Math.Max(volume - reduction, MinimumVolume);
		}

		if (volume < MinimumVolume)
			volume = MinimumVolume;

		return volume;
	}

	private void ClosePosition(decimal exitPrice)
	{
		var volume = Math.Abs(Position);
		if (volume <= 0m)
		{
			_positionDirection = 0;
			_entryPrice = 0m;
			return;
		}

		var direction = _positionDirection;
		var entryPrice = _entryPrice;

		if (Position > 0)
			SellMarket(volume);
		else
			BuyMarket(volume);

		if (entryPrice > 0m)
		{
			if (direction > 0)
			{
				var profit = exitPrice - entryPrice;
				if (profit < 0m)
					_lossStreak++;
				else if (profit > 0m)
					_lossStreak = 0;
			}
			else if (direction < 0)
			{
				var profit = entryPrice - exitPrice;
				if (profit < 0m)
					_lossStreak++;
				else if (profit > 0m)
					_lossStreak = 0;
			}
		}

		_entryPrice = 0m;
		_positionDirection = 0;
	}

	private void UpdatePrimarySignal(decimal signal)
	{
		_prevPrimarySignal = signal;
		_hasPrevSignal = true;
	}
}