在 GitHub 上查看

RM Stochastic Band 策略

概述

RM Stochastic Band Strategy 是 MetaTrader 专家顾问 EA RM Stochastic Band(作者 Ronny Maheza)的 StockSharp 高层 API 版本。策略同时监控三个不同时框上的随机指标,并且仅当三个时框的 %K 值同时指示超买或超卖时才开仓。进场之后,使用位于最高时框上的平均真实波幅(ATR)来计算止损和止盈,完全再现原始 EA 的波动率管理方法。此外,实现了可配置的最小资金阈值和自适应的点差过滤器。

核心逻辑

  1. 多时框确认

    • 基础时框(默认 1 分钟)产生主要信号。
    • 中级和高级时框(默认 5 分钟、15 分钟)必须与基础信号方向一致。
    • 仅当三个时框的 %K 同时低于超卖阈值时买入;当三个时框的 %K 同时高于超买阈值时卖出。
  2. ATR 波动率止损/止盈

    • ATR 在最高时框(默认 15 分钟)上计算。
    • 止损 = 入场价 ± ATR * StopLossMultiplier
    • 止盈 = 入场价 ± ATR * TakeProfitMultiplier
    • 在基础时框的已完成蜡烛上检查价格触及情况并市价离场。
  3. 执行与风控过滤

    • 根据 Level-1 的最佳买卖价估算点差;如果当前点差超过标准上限,则使用更宽松的“分型账户”上限,与原 EA 的逻辑一致。
    • 当投资组合价值低于 MinMargin 时暂停交易。
    • 同一时间仅允许一笔持仓,且存在活动委托时不会开新仓。

指标与订阅

指标 时框 用途
随机指标 (Stochastic Oscillator) 基础时框 (默认 1 分钟) 产生主要信号,仅使用 %K。
随机指标 中级时框 (默认 5 分钟) 确认信号方向。
随机指标 高级时框 (默认 15 分钟) 长周期确认。
平均真实波幅 (ATR) 高级时框 (默认 15 分钟) 计算止损和止盈距离。

策略还订阅 Level-1 行情以获取最佳买卖价,确保点差过滤器能够工作。

入场规则

  • 做多:三个时框的 %K 值均低于 OversoldLevel。策略以 OrderVolume 的数量市价买入,并记录 ATR 计算出的止损/止盈。
  • 做空:三个时框的 %K 值均高于 OverboughtLevel。策略以相同数量市价卖出。

出场规则

  • 止损:多单在价格低点触及 入场价 - ATR * StopLossMultiplier 时平仓;空单在高点触及 入场价 + ATR * StopLossMultiplier 时平仓。
  • 止盈:多单在高点触及 入场价 + ATR * TakeProfitMultiplier 时平仓;空单在低点触及 入场价 - ATR * TakeProfitMultiplier 时平仓。
  • 每次平仓后都会清空内部的止损/止盈缓存,等待下一次信号重新计算。

参数

参数 说明 默认值
OrderVolume 每次市价订单的成交量。 0.1
StochasticLength %K 的回溯长度。 5
StochasticSmoothing %K 的平滑参数。 3
StochasticSignalLength %D 的长度。 3
AtrPeriod 在高时框计算 ATR 的周期。 14
StopLossMultiplier ATR 止损倍数。 1.5
TakeProfitMultiplier ATR 止盈倍数。 3.0
MinMargin 允许交易的最小投资组合价值。 100
MaxSpreadStandard 标准账户允许的最大点差。 3
MaxSpreadCent 当标准上限被突破时使用的备用点差上限。 10
OversoldLevel 判定超卖的 %K 阈值。 20
OverboughtLevel 判定超买的 %K 阈值。 80
BaseCandleType 基础时框(默认 1 分钟 K 线)。 1 分钟
MidCandleType 中级确认时框。 5 分钟
HighCandleType 高级确认 + ATR 时框。 15 分钟

所有参数均支持与原始 EA 相同的优化范围。

实现细节

  • 指标值通过 SubscribeCandles(...).BindEx(...) 获取,完全遵循 AGENTS.md 中的高层 API 要求,未直接访问内部缓存。
  • 点差依据 Level-1 数据实时计算;若行情源缺少买卖报价,策略将保持待机状态,避免在不可靠的市场条件下下单。
  • 头寸管理完全采用市价单,与原 EA 的做法保持一致。
  • 原 MQL 代码虽定义了保本、追踪等输入参数,但并未实现相关逻辑,因此移植版本也不包含这些功能。

使用建议

  1. 在接入策略之前确认数据源提供 Level-1 行情,否则点差过滤会阻止交易。
  2. 根据标的资产的波动率调整随机指标阈值与 ATR 倍数。
  3. 在回测或优化时可尝试不同的时框组合,以适配与原始 M1/M5/M15 结构不同的市场周期。
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>
/// Multi-timeframe stochastic oscillator strategy with ATR-based stop-loss and take-profit management.
/// Emulates the logic of the "EA RM Stochastic Band" MetaTrader expert advisor.
/// </summary>
public class RmStochasticBandStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _stochasticLength;
	private readonly StrategyParam<int> _stochasticSmoothing;
	private readonly StrategyParam<int> _stochasticSignalLength;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _stopLossMultiplier;
	private readonly StrategyParam<decimal> _takeProfitMultiplier;
	private readonly StrategyParam<decimal> _minMargin;
	private readonly StrategyParam<decimal> _maxSpreadStandard;
	private readonly StrategyParam<decimal> _maxSpreadCent;
	private readonly StrategyParam<decimal> _oversoldLevel;
	private readonly StrategyParam<decimal> _overboughtLevel;
	private readonly StrategyParam<DataType> _baseCandleType;
	private readonly StrategyParam<DataType> _midCandleType;
	private readonly StrategyParam<DataType> _highCandleType;

	private decimal? _stochM1;
	private decimal? _stochM5;
	private decimal? _stochM15;
	private decimal? _atrValue;
	private decimal? _longStopPrice;
	private decimal? _longTakeProfit;
	private decimal? _shortStopPrice;
	private decimal? _shortTakeProfit;
	private decimal? _bestBid;
	private decimal? _bestAsk;

	/// <summary>
	/// Trade volume used for market orders.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// %K lookback for the stochastic oscillator.
	/// </summary>
	public int StochasticLength
	{
		get => _stochasticLength.Value;
		set => _stochasticLength.Value = value;
	}

	/// <summary>
	/// Smoothing period applied to %K.
	/// </summary>
	public int StochasticSmoothing
	{
		get => _stochasticSmoothing.Value;
		set => _stochasticSmoothing.Value = value;
	}

	/// <summary>
	/// %D moving average length.
	/// </summary>
	public int StochasticSignalLength
	{
		get => _stochasticSignalLength.Value;
		set => _stochasticSignalLength.Value = value;
	}

	/// <summary>
	/// ATR lookback used for volatility-based exits.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Multiplier applied to ATR for stop-loss calculation.
	/// </summary>
	public decimal StopLossMultiplier
	{
		get => _stopLossMultiplier.Value;
		set => _stopLossMultiplier.Value = value;
	}

	/// <summary>
	/// Multiplier applied to ATR for take-profit calculation.
	/// </summary>
	public decimal TakeProfitMultiplier
	{
		get => _takeProfitMultiplier.Value;
		set => _takeProfitMultiplier.Value = value;
	}

	/// <summary>
	/// Minimum portfolio value required before placing trades.
	/// </summary>
	public decimal MinMargin
	{
		get => _minMargin.Value;
		set => _minMargin.Value = value;
	}

	/// <summary>
	/// Maximum spread (in price units) tolerated on standard accounts.
	/// </summary>
	public decimal MaxSpreadStandard
	{
		get => _maxSpreadStandard.Value;
		set => _maxSpreadStandard.Value = value;
	}

	/// <summary>
	/// Maximum spread (in price units) tolerated on cent accounts.
	/// </summary>
	public decimal MaxSpreadCent
	{
		get => _maxSpreadCent.Value;
		set => _maxSpreadCent.Value = value;
	}

	/// <summary>
	/// Threshold for oversold conditions.
	/// </summary>
	public decimal OversoldLevel
	{
		get => _oversoldLevel.Value;
		set => _oversoldLevel.Value = value;
	}

	/// <summary>
	/// Threshold for overbought conditions.
	/// </summary>
	public decimal OverboughtLevel
	{
		get => _overboughtLevel.Value;
		set => _overboughtLevel.Value = value;
	}

	/// <summary>
	/// Primary timeframe used for signal execution.
	/// </summary>
	public DataType BaseCandleType
	{
		get => _baseCandleType.Value;
		set => _baseCandleType.Value = value;
	}

	/// <summary>
	/// Intermediate timeframe used for stochastic confirmation.
	/// </summary>
	public DataType MidCandleType
	{
		get => _midCandleType.Value;
		set => _midCandleType.Value = value;
	}

	/// <summary>
	/// Higher timeframe used for stochastic confirmation and ATR calculation.
	/// </summary>
	public DataType HighCandleType
	{
		get => _highCandleType.Value;
		set => _highCandleType.Value = value;
	}

	public RmStochasticBandStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume of each market order", "Trading");

		_stochasticLength = Param(nameof(StochasticLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Length", "%K lookback period", "Indicators")
			
			.SetOptimize(3, 15, 1);

		_stochasticSmoothing = Param(nameof(StochasticSmoothing), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Smoothing", "Smoothing period applied to %K", "Indicators")
			
			.SetOptimize(1, 7, 1);

		_stochasticSignalLength = Param(nameof(StochasticSignalLength), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Signal", "%D moving average length", "Indicators")
			
			.SetOptimize(1, 10, 1);

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "Lookback for ATR volatility filter", "Indicators")
			
			.SetOptimize(7, 30, 1);

		_stopLossMultiplier = Param(nameof(StopLossMultiplier), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("SL Multiplier", "ATR multiplier for stop-loss", "Risk")
			
			.SetOptimize(0.5m, 3m, 0.25m);

		_takeProfitMultiplier = Param(nameof(TakeProfitMultiplier), 3m)
			.SetGreaterThanZero()
			.SetDisplay("TP Multiplier", "ATR multiplier for take-profit", "Risk")
			
			.SetOptimize(1m, 6m, 0.5m);

		_minMargin = Param(nameof(MinMargin), 100m)
			.SetGreaterThanZero()
			.SetDisplay("Minimum Margin", "Required portfolio value before trading", "Risk");

		_maxSpreadStandard = Param(nameof(MaxSpreadStandard), 3m)
			.SetGreaterThanZero()
			.SetDisplay("Max Spread Standard", "Maximum spread allowed for standard accounts", "Filters");

		_maxSpreadCent = Param(nameof(MaxSpreadCent), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Max Spread Cent", "Maximum spread allowed for cent accounts", "Filters");

		_oversoldLevel = Param(nameof(OversoldLevel), 20m)
			.SetDisplay("Oversold Level", "Threshold that defines oversold conditions", "Signals")
			
			.SetOptimize(5m, 40m, 5m);

		_overboughtLevel = Param(nameof(OverboughtLevel), 80m)
			.SetDisplay("Overbought Level", "Threshold that defines overbought conditions", "Signals")
			
			.SetOptimize(60m, 95m, 5m);

		_baseCandleType = Param(nameof(BaseCandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Base Timeframe", "Primary execution timeframe", "General");

		_midCandleType = Param(nameof(MidCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Mid Timeframe", "Secondary confirmation timeframe", "General");

		_highCandleType = Param(nameof(HighCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("High Timeframe", "Higher confirmation timeframe", "General");
	}

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

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

		_stochM1 = null;
		_stochM5 = null;
		_stochM15 = null;
		_atrValue = null;
		_longStopPrice = null;
		_longTakeProfit = null;
		_shortStopPrice = null;
		_shortTakeProfit = null;
		_bestBid = null;
		_bestAsk = null;
	}

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

		var baseStochastic = CreateStochastic();
		var midStochastic = CreateStochastic();
		var highStochastic = CreateStochastic();
		var atr = new AverageTrueRange { Length = AtrPeriod };

		var baseSubscription = SubscribeCandles(BaseCandleType);
		baseSubscription.BindEx(baseStochastic, ProcessBaseCandle).Start();

		SubscribeCandles(MidCandleType)
			.BindEx(midStochastic, ProcessMidCandle)
			.Start();

		SubscribeCandles(HighCandleType)
			.BindEx(highStochastic, atr, ProcessHighCandle)
			.Start();

		// Level1 removed for backtest compatibility
	}

	private StochasticOscillator CreateStochastic()
	{
		return new StochasticOscillator
		{
			K = { Length = StochasticLength },
			D = { Length = StochasticSignalLength }
		};
	}

	private void ProcessLevel1(Level1ChangeMessage message)
	{
		if (message.Changes.TryGetValue(Level1Fields.BestBidPrice, out var bidValue))
			_bestBid = (decimal)bidValue;

		if (message.Changes.TryGetValue(Level1Fields.BestAskPrice, out var askValue))
			_bestAsk = (decimal)askValue;
	}

	private void ProcessMidCandle(ICandleMessage candle, IIndicatorValue stochValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!stochValue.IsFinal)
			return;

		var stochastic = (StochasticOscillatorValue)stochValue;

		if (stochastic.K is decimal kValue)
			_stochM5 = kValue;
	}

	private void ProcessHighCandle(ICandleMessage candle, IIndicatorValue stochValue, IIndicatorValue atrValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!stochValue.IsFinal || !atrValue.IsFinal)
			return;

		var stochastic = (StochasticOscillatorValue)stochValue;

		if (stochastic.K is decimal kValue)
			_stochM15 = kValue;

		_atrValue = atrValue.ToDecimal();
	}

	private void ProcessBaseCandle(ICandleMessage candle, IIndicatorValue stochValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!stochValue.IsFinal)
			return;

		var stochastic = (StochasticOscillatorValue)stochValue;
		if (stochastic.K is not decimal kValue)
			return;

		_stochM1 = kValue;

		ManageOpenPosition(candle);
		TryEnterPosition(candle);
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		if (Position == 0)
		{
			_longStopPrice = null;
			_longTakeProfit = null;
			_shortStopPrice = null;
			_shortTakeProfit = null;
			return;
		}

		if (Position > 0)
		{
			if (_longStopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Position);
				_longStopPrice = null;
				_longTakeProfit = null;
				return;
			}

			if (_longTakeProfit is decimal target && candle.HighPrice >= target)
			{
				SellMarket(Position);
				_longStopPrice = null;
				_longTakeProfit = null;
			}
		}
		else if (Position < 0)
		{
			var shortVolume = Math.Abs(Position);
			if (_shortStopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(shortVolume);
				_shortStopPrice = null;
				_shortTakeProfit = null;
				return;
			}

			if (_shortTakeProfit is decimal target && candle.LowPrice <= target)
			{
				BuyMarket(shortVolume);
				_shortStopPrice = null;
				_shortTakeProfit = null;
			}
		}
	}

	private void TryEnterPosition(ICandleMessage candle)
	{
		if (!HasSufficientMargin())
			return;

		if (Position != 0)
			return;

		if (HasActiveOrders())
			return;

		if (_stochM1 is not decimal stochFast ||
			_stochM5 is not decimal stochMid ||
			_stochM15 is not decimal stochSlow ||
			_atrValue is not decimal atr)
		{
			return;
		}

		var oversold = OversoldLevel;
		var overbought = OverboughtLevel;

		if (stochFast < oversold && stochMid < oversold && stochSlow < oversold)
		{
			EnterLong(candle.ClosePrice, atr);
		}
		else if (stochFast > overbought && stochMid > overbought && stochSlow > overbought)
		{
			EnterShort(candle.ClosePrice, atr);
		}
	}

	private bool HasSufficientMargin()
	{
		var currentValue = Portfolio?.CurrentValue ?? 0m;
		return currentValue >= MinMargin;
	}

	private bool IsSpreadAcceptable()
	{
		if (_bestBid is not decimal bid || _bestAsk is not decimal ask)
			return false;

		var spread = ask - bid;
		if (spread <= 0m)
			return true;

		var limit = spread > MaxSpreadStandard ? MaxSpreadCent : MaxSpreadStandard;
		return spread <= limit;
	}

	private bool HasActiveOrders()
	{
		foreach (var order in Orders)
		{
			if (!order.State.IsFinal())
				return true;
		}

		return false;
	}

	private void EnterLong(decimal price, decimal atr)
	{
		var volume = OrderVolume;
		if (volume <= 0m)
			return;

		BuyMarket(volume);

		_longStopPrice = price - atr * StopLossMultiplier;
		_longTakeProfit = price + atr * TakeProfitMultiplier;
		_shortStopPrice = null;
		_shortTakeProfit = null;
	}

	private void EnterShort(decimal price, decimal atr)
	{
		var volume = OrderVolume;
		if (volume <= 0m)
			return;

		SellMarket(volume);

		_shortStopPrice = price + atr * StopLossMultiplier;
		_shortTakeProfit = price - atr * TakeProfitMultiplier;
		_longStopPrice = null;
		_longTakeProfit = null;
	}
}