在 GitHub 上查看

极限强度反转策略

摘要

  • 将 MetaTrader EXSR 专家顾问转换为 StockSharp 策略的逆势系统。
  • 结合布林带与 RSI 极值识别价格耗竭区域。
  • 采用按账户权益百分比计算的仓位大小,并使用以点(pip)表示的固定止损/止盈距离。

交易逻辑

  1. 订阅所选周期的 K 线(默认 1 小时)。
  2. 计算布林带(周期、偏差)与 RSI 指标。
  3. 每当 K 线收盘:
    • 做多条件:RSI 低于超卖阈值但仍大于零,最低价跌破下轨,且收出阳线(收盘价高于开盘价)。
    • 做空条件:RSI 高于超买阈值,最高价突破上轨,且收出阴线(收盘价低于开盘价)。
  4. 同一时间只允许持有一个方向的仓位,如需反向会先平掉原有头寸。
  5. 依据成交价加减 MetaTrader 风格的点差距离放置止损和止盈,之后每根 K 线都会检测是否触发退出。

风险管理

  • Volume 参数大于零,则使用固定手数;否则根据 RiskPercent 与止损距离自动计算仓位。
  • 风险基于当前账户权益(若不可用则使用余额或初始资金),并将止损距离转换为价格或货币单位(使用合约的最小跳动及其价格)。
  • 计算出的仓位会根据合约的最小手数、最大手数和步长进行规范化。

参数

名称 说明 默认值
Risk Percent 单笔交易占用的账户权益百分比。 1%
Stop Loss (pips) 止损距离(以点 pip 表示)。 150
Take Profit (pips) 止盈距离(以点 pip 表示)。 300
Bollinger Period 计算布林带使用的 K 线数量。 20
Bollinger Deviation 布林带标准差倍数。 2.0
RSI Period 计算 RSI 使用的 K 线数量。 14
RSI Overbought 判定极端超买的 RSI 水平。 80
RSI Oversold 判定极端超卖的 RSI 水平。 20
Candle Type 用于分析的 K 线周期。 1 小时

备注

  • 为了获得精确的仓位计算,请确保品种提供价格跳动、跳动价值和手数步长信息;若缺失则使用默认值。
  • 即使临时禁止开仓,风险管理仍会生效,以保证止损和止盈的防护。
  • 策略仅处理收盘完成的 K 线,保持与原始 EA 基于上一根 K 线的行为一致。
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>
/// Extreme Strength Reversal strategy converted from MQL.
/// Enters counter-trend trades when price pierces Bollinger Bands and RSI shows an extreme reading.
/// Uses percent-based risk sizing with fixed stop-loss and take-profit distances expressed in pips.
/// </summary>
public class ExtremeStrengthReversalStrategy : Strategy
{
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _bollingerPeriod;
	private readonly StrategyParam<decimal> _bollingerDeviation;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _rsiOverbought;
	private readonly StrategyParam<decimal> _rsiOversold;
	private readonly StrategyParam<DataType> _candleType;

	private BollingerBands _bollinger;
	private RelativeStrengthIndex _rsi;

	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal? _entryPrice;

	/// <summary>
	/// Risk percentage applied to portfolio equity for sizing.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in MetaTrader pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in MetaTrader pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Bollinger Bands lookback period.
	/// </summary>
	public int BollingerPeriod
	{
		get => _bollingerPeriod.Value;
		set => _bollingerPeriod.Value = value;
	}

	/// <summary>
	/// Standard deviation multiplier for Bollinger Bands.
	/// </summary>
	public decimal BollingerDeviation
	{
		get => _bollingerDeviation.Value;
		set => _bollingerDeviation.Value = value;
	}

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

	/// <summary>
	/// Overbought threshold for RSI.
	/// </summary>
	public decimal RsiOverbought
	{
		get => _rsiOverbought.Value;
		set => _rsiOverbought.Value = value;
	}

	/// <summary>
	/// Oversold threshold for RSI.
	/// </summary>
	public decimal RsiOversold
	{
		get => _rsiOversold.Value;
		set => _rsiOversold.Value = value;
	}

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

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public ExtremeStrengthReversalStrategy()
	{
		_riskPercent = Param(nameof(RiskPercent), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Risk Percent", "Risk per trade as percentage of equity.", "Risk Management")
			
			.SetOptimize(0.5m, 5m, 0.5m);

		_stopLossPips = Param(nameof(StopLossPips), 150)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips.", "Risk Management")
			
			.SetOptimize(50, 250, 10);

		_takeProfitPips = Param(nameof(TakeProfitPips), 300)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Take-profit distance in pips.", "Risk Management")
			
			.SetOptimize(100, 400, 20);

		_bollingerPeriod = Param(nameof(BollingerPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Bollinger Period", "Number of candles used for Bollinger Bands.", "Indicators")
			
			.SetOptimize(10, 40, 5);

		_bollingerDeviation = Param(nameof(BollingerDeviation), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("Bollinger Deviation", "Standard deviation multiplier for Bollinger Bands.", "Indicators")
			
			.SetOptimize(1m, 3m, 0.25m);

		_rsiPeriod = Param(nameof(RsiPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Number of candles used for RSI.", "Indicators")
			
			.SetOptimize(7, 28, 1);

		_rsiOverbought = Param(nameof(RsiOverbought), 65m)
			.SetDisplay("RSI Overbought", "RSI level that marks extreme overbought conditions.", "Indicators")
			
			.SetOptimize(60m, 90m, 5m);

		_rsiOversold = Param(nameof(RsiOversold), 35m)
			.SetDisplay("RSI Oversold", "RSI level that marks extreme oversold conditions.", "Indicators")
			
			.SetOptimize(10m, 40m, 5m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe used for analysis.", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_bollinger = null;
		_rsi = null;
		ResetTradeState();
	}

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

		_bollinger = new BollingerBands
		{
			Length = BollingerPeriod,
			Width = BollingerDeviation
		};

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_bollinger, _rsi, ProcessCandle)
			.Start();

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

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

		if (bbValue is not IBollingerBandsValue bb)
			return;

		var middleBand = bb.MovingAverage ?? 0m;
		var upperBand = bb.UpBand ?? 0m;
		var lowerBand = bb.LowBand ?? 0m;
		var rsiValue = rsiInd.ToDecimal();

		if (middleBand == 0m)
			return;

		ManageOpenPosition(candle);

		if (Position != 0m)
			return;

		var closePrice = candle.ClosePrice;
		var openPrice = candle.OpenPrice;

		var bullishReversal = rsiValue < RsiOversold && rsiValue > 0m && candle.LowPrice < lowerBand && closePrice > openPrice;
		if (bullishReversal)
		{
			TryEnterLong(closePrice);
			return;
		}

		var bearishReversal = rsiValue > RsiOverbought && candle.HighPrice > upperBand && closePrice < openPrice;
		if (bearishReversal)
			TryEnterShort(closePrice);
	}

	private void TryEnterLong(decimal closePrice)
	{
		var volume = CalculateOrderVolume();
		if (volume <= 0m)
			return;

		if (Position < 0m)
			BuyMarket(-Position);

		BuyMarket(volume);
		_entryPrice = closePrice;
		_stopLossPrice = StopLossPips > 0 ? closePrice - GetPipOffset(StopLossPips) : null;
		_takeProfitPrice = TakeProfitPips > 0 ? closePrice + GetPipOffset(TakeProfitPips) : null;
	}

	private void TryEnterShort(decimal closePrice)
	{
		var volume = CalculateOrderVolume();
		if (volume <= 0m)
			return;

		if (Position > 0m)
			SellMarket(Position);

		SellMarket(volume);
		_entryPrice = closePrice;
		_stopLossPrice = StopLossPips > 0 ? closePrice + GetPipOffset(StopLossPips) : null;
		_takeProfitPrice = TakeProfitPips > 0 ? closePrice - GetPipOffset(TakeProfitPips) : null;
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
			{
				SellMarket(Position);
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(Position);
				ResetTradeState();
			}
		}
		else if (Position < 0m)
		{
			var shortPosition = -Position;

			if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
			{
				BuyMarket(shortPosition);
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(shortPosition);
				ResetTradeState();
			}
		}
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue || _entryPrice.HasValue)
		{
			ResetTradeState();
		}
	}

	private decimal CalculateOrderVolume()
	{
		if (Volume > 0m)
			return Volume;

		if (RiskPercent <= 0m)
			return 0m;

		var stopDistance = GetStopDistance();
		if (stopDistance <= 0m)
			return 0m;

		var portfolio = Portfolio;
		if (portfolio is null)
			return 0m;

		var equity = portfolio.CurrentValue ?? portfolio.BeginValue ?? 0m;
		if (equity <= 0m)
			return 0m;

		var riskAmount = equity * RiskPercent / 100m;
		if (riskAmount <= 0m)
			return 0m;

		var priceStep = Security?.PriceStep ?? 0m;
		var stepPrice = 1m;

		decimal perUnitRisk;
		if (priceStep > 0m && stepPrice > 0m)
		{
			perUnitRisk = stopDistance / priceStep * stepPrice;
		}
		else
		{
			perUnitRisk = stopDistance;
		}

		if (perUnitRisk <= 0m)
			return 0m;

		var rawVolume = riskAmount / perUnitRisk;
		if (rawVolume <= 0m)
			return 0m;

		rawVolume = NormalizeVolume(rawVolume);
		return rawVolume;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		var minVolume = Security?.MinVolume ?? 0m;
		var maxVolume = Security?.MaxVolume ?? 0m;
		var step = Security?.VolumeStep ?? 0m;

		if (step > 0m && volume > 0m)
		{
			var steps = decimal.Floor(volume / step);
			if (steps <= 0m)
				steps = 1m;

			volume = steps * step;
		}

		if (minVolume > 0m && volume < minVolume)
			volume = minVolume;

		if (maxVolume > 0m && volume > maxVolume)
			volume = maxVolume;

		return Math.Max(volume, 0m);
	}

	private decimal GetStopDistance()
	{
		if (StopLossPips <= 0)
			return 0m;

		return GetPipOffset(StopLossPips);
	}

	private decimal GetPipOffset(int pips)
	{
		var pipSize = GetPipSize();
		if (pipSize <= 0m)
			return 0m;

		return pips * pipSize;
	}

	private decimal GetPipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step > 0m)
			return step;

		var decimals = Security?.Decimals;
		if (decimals.HasValue && decimals.Value > 0)
		{
			var value = Math.Pow(10, -decimals.Value);
			return Convert.ToDecimal(value);
		}

		return 0.0001m;
	}

	private void ResetTradeState()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_entryPrice = null;
	}
}