在 GitHub 上查看

RSI 布林线分形突破策略

概述

该策略在 StockSharp 中复现 MetaTrader 的“RSI and Bollinger Bands”专家顾问。布林带作用在 RSI 指标上而不是价格。当检测到新的确认分形时,策略会按设置的点差在分形价格外侧挂入止损单,并使用 Parabolic SAR 对持仓进行动态跟踪。

指标说明

  • RSI(默认 8 周期):核心动量指标,超买/超卖阈值用于取消挂单。
  • 布林带(作用于 RSI):长度 14,偏差 1.0。只有当 RSI 收盘突破上轨或下轨时才会触发信号。
  • 威廉分形:寻找最近的五根 K 线模式,获得上方和下方分形价格作为突破点。
  • Parabolic SAR:初始步长 0.003,最大 0.2,用于生成跟踪止损位置。

入场逻辑

  1. 仅在所选时间框架的已完成 K 线上运行(默认 4 小时)。
  2. 当出现上方分形且 RSI 收盘高于 布林上轨,同时上一根 K 线收盘仍低于该分形时,挂出 买入止损单
    • 挂单价格 = 分形高点 + IndentPips(默认 15 点)。
    • StopLossPips > 0,则止损价格 = 挂单价 − StopLossPips
    • TakeProfitPips > 0,则止盈价格 = 挂单价 + TakeProfitPips
  3. 当出现下方分形且 RSI 收盘低于 布林下轨,上一根 K 线收盘仍高于分形时,挂出 卖出止损单(分形价 − IndentPips)。
  4. RSI 回到区间内时取消挂单:
    • RSI < RsiLower → 取消买入止损单。
    • RSI > RsiUpper → 取消卖出止损单。

离场与风险控制

  • 固定止盈/止损使用点值设置,与原版 EA 相同;为 0 则禁用该保护。
  • Parabolic SAR 只有在 SAR 与当前价格的距离大于 SarTrailingPips 时才会向盈利方向推进止损。
  • 当价格触及动态止损或固定止盈时,通过市价单平仓。
  • 挂单成交后会取消反向挂单,并记录当前仓位的止损/止盈水平以便跟踪。

参数

参数 说明 默认值
RsiPeriod RSI 平滑周期。 8
BandsPeriod RSI 布林带周期。 14
BandsDeviation 布林带标准差系数。 1.0
SarStep Parabolic SAR 加速步长。 0.003
SarMax Parabolic SAR 最大加速值。 0.2
TakeProfitPips 止盈点数。 50
StopLossPips 止损点数。 135
IndentPips 相对分形的额外偏移。 15
RsiUpper RSI 超买阈值,用于取消卖单。 70
RsiLower RSI 超卖阈值,用于取消买单。 30
SarTrailingPips SAR 与价格之间的最小距离(点)。 10
CandleType 处理的蜡烛时间框架。 4 小时

其他说明

  • 按要求不提供 Python 版本。
  • 头寸数量由基础类的 Volume 属性控制(默认 1)。
  • 推荐使用与原 EA 相同的时间框架,例如 EURUSD H4。
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>
/// Strategy that combines RSI-based Bollinger Bands with fractal breakouts and Parabolic SAR trailing.
/// </summary>
public class RsiBollingerFractalBreakoutStrategy : Strategy
{
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<int> _bandsPeriod;
	private readonly StrategyParam<decimal> _bandsDeviation;
	private readonly StrategyParam<decimal> _sarStep;
	private readonly StrategyParam<decimal> _sarMax;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _indentPips;
	private readonly StrategyParam<decimal> _rsiUpper;
	private readonly StrategyParam<decimal> _rsiLower;
	private readonly StrategyParam<decimal> _sarTrailingPips;
	private readonly StrategyParam<DataType> _candleType;
	
	private RelativeStrengthIndex _rsi = null!;
	private BollingerBands _bollinger = null!;
	private ParabolicSar _parabolicSar = null!;
	
	private Order _buyStopOrder;
	private Order _sellStopOrder;
	
	private decimal? _pendingLongEntry;
	private decimal? _pendingLongStop;
	private decimal? _pendingLongTake;
	private decimal? _pendingShortEntry;
	private decimal? _pendingShortStop;
	private decimal? _pendingShortTake;
	
	private decimal? _longStopPrice;
	private decimal? _longTakeProfit;
	private decimal? _shortStopPrice;
	private decimal? _shortTakeProfit;
	
	private decimal _pipSize;
	private decimal _previousPosition;
	
	private decimal _h1;
	private decimal _h2;
	private decimal _h3;
	private decimal _h4;
	private decimal _h5;
	private decimal _l1;
	private decimal _l2;
	private decimal _l3;
	private decimal _l4;
	private decimal _l5;
	private int _fractalCount;
	
	/// <summary>
	/// RSI averaging period.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}
	
	/// <summary>
	/// Bollinger Bands period applied to RSI values.
	/// </summary>
	public int BandsPeriod
	{
		get => _bandsPeriod.Value;
		set => _bandsPeriod.Value = value;
	}
	
	/// <summary>
	/// Bollinger Bands standard deviation multiplier.
	/// </summary>
	public decimal BandsDeviation
	{
		get => _bandsDeviation.Value;
		set => _bandsDeviation.Value = value;
	}
	
	/// <summary>
	/// Parabolic SAR acceleration step.
	/// </summary>
	public decimal SarStep
	{
		get => _sarStep.Value;
		set => _sarStep.Value = value;
	}
	
	/// <summary>
	/// Parabolic SAR maximum acceleration.
	/// </summary>
	public decimal SarMax
	{
		get => _sarMax.Value;
		set => _sarMax.Value = value;
	}
	
	/// <summary>
	/// Take profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}
	
	/// <summary>
	/// Stop loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}
	
	/// <summary>
	/// Offset added to the fractal breakout level in pips.
	/// </summary>
	public decimal IndentPips
	{
		get => _indentPips.Value;
		set => _indentPips.Value = value;
	}
	
	/// <summary>
	/// RSI upper threshold used to cancel sell stops.
	/// </summary>
	public decimal RsiUpper
	{
		get => _rsiUpper.Value;
		set => _rsiUpper.Value = value;
	}
	
	/// <summary>
	/// RSI lower threshold used to cancel buy stops.
	/// </summary>
	public decimal RsiLower
	{
		get => _rsiLower.Value;
		set => _rsiLower.Value = value;
	}
	
	/// <summary>
	/// Additional distance required between Parabolic SAR and price in pips before trailing.
	/// </summary>
	public decimal SarTrailingPips
	{
		get => _sarTrailingPips.Value;
		set => _sarTrailingPips.Value = value;
	}
	
	/// <summary>
	/// Candle data type to subscribe.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}
	
	/// <summary>
	/// Initialize <see cref="RsiBollingerFractalBreakoutStrategy"/>.
	/// </summary>
	public RsiBollingerFractalBreakoutStrategy()
	{
		_rsiPeriod = Param(nameof(RsiPeriod), 8)
			.SetDisplay("RSI Period", "RSI averaging period", "RSI")
			.SetGreaterThanZero();
		
		_bandsPeriod = Param(nameof(BandsPeriod), 10)
			.SetDisplay("Bollinger Period", "RSI Bollinger period", "Bollinger")
			.SetGreaterThanZero();
		
		_bandsDeviation = Param(nameof(BandsDeviation), 1m)
			.SetDisplay("Bollinger Deviation", "Standard deviations on RSI", "Bollinger")
			.SetGreaterThanZero();
		
		_sarStep = Param(nameof(SarStep), 0.003m)
			.SetDisplay("SAR Step", "Parabolic SAR acceleration step", "Parabolic SAR")
			.SetGreaterThanZero();
		
		_sarMax = Param(nameof(SarMax), 0.2m)
			.SetDisplay("SAR Max", "Parabolic SAR maximum acceleration", "Parabolic SAR")
			.SetGreaterThanZero();
		
		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetDisplay("Take Profit (pips)", "Take profit distance", "Risk");
		
		_stopLossPips = Param(nameof(StopLossPips), 135m)
			.SetDisplay("Stop Loss (pips)", "Stop loss distance", "Risk");
		
		_indentPips = Param(nameof(IndentPips), 15m)
			.SetDisplay("Indent (pips)", "Offset from fractal breakout", "Entries");
		
		_rsiUpper = Param(nameof(RsiUpper), 75m)
			.SetDisplay("RSI Upper", "Overbought threshold", "RSI");

		_rsiLower = Param(nameof(RsiLower), 25m)
			.SetDisplay("RSI Lower", "Oversold threshold", "RSI");
		
		_sarTrailingPips = Param(nameof(SarTrailingPips), 10m)
			.SetDisplay("SAR Trailing (pips)", "Extra distance before SAR trailing", "Risk");
		
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for analysis", "General");
	}
	
	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_rsi = null!;
		_bollinger = null!;
		_parabolicSar = null!;
		_buyStopOrder = null;
		_sellStopOrder = null;
		_pendingLongEntry = null;
		_pendingLongStop = null;
		_pendingLongTake = null;
		_pendingShortEntry = null;
		_pendingShortStop = null;
		_pendingShortTake = null;
		_longStopPrice = null;
		_longTakeProfit = null;
		_shortStopPrice = null;
		_shortTakeProfit = null;
		_pipSize = 0m;
		_previousPosition = 0m;
		_h1 = 0m; _h2 = 0m; _h3 = 0m; _h4 = 0m; _h5 = 0m;
		_l1 = 0m; _l2 = 0m; _l3 = 0m; _l4 = 0m; _l5 = 0m;
		_fractalCount = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		
		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
		_bollinger = new BollingerBands { Length = BandsPeriod, Width = BandsDeviation };
		_parabolicSar = new ParabolicSar
		{
			AccelerationStep = SarStep,
			AccelerationMax = SarMax
		};

		_pipSize = GetPipSize();
		if (_pipSize <= 0m)
			_pipSize = Security?.PriceStep ?? 1m;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();
		
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _rsi);
			DrawIndicator(area, _bollinger);
			DrawIndicator(area, _parabolicSar);
			DrawOwnTrades(area);
		}
	}
	
	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var rsiResult = _rsi.Process(new DecimalIndicatorValue(_rsi, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		var sarResult = _parabolicSar.Process(new CandleIndicatorValue(_parabolicSar, candle));
		var sarValue = (_parabolicSar.IsFormed && !sarResult.IsEmpty) ? sarResult.ToDecimal() : candle.ClosePrice;

		if (!_rsi.IsFormed)
		{
			UpdateFractals(candle);
			UpdateTrailingAndExits(candle, sarValue);
			return;
		}

		var rsiValue = rsiResult.ToDecimal();

		UpdateFractals(candle);
		UpdateTrailingAndExits(candle, sarValue);

		// Buy when RSI is above upper threshold (bullish momentum)
		if (rsiValue > RsiUpper && Position <= 0)
		{
			var entryPrice = candle.ClosePrice;
			var stopPrice = StopLossPips > 0m ? NormalizePrice(entryPrice - StopLossPips * _pipSize) : (decimal?)null;
			var takePrice = TakeProfitPips > 0m ? NormalizePrice(entryPrice + TakeProfitPips * _pipSize) : (decimal?)null;

			if (Position < 0)
				BuyMarket();
			BuyMarket();
			_longStopPrice = stopPrice;
			_longTakeProfit = takePrice;
		}
		// Sell when RSI is below lower threshold (bearish momentum)
		else if (rsiValue < RsiLower && Position >= 0)
		{
			var entryPrice = candle.ClosePrice;
			var stopPrice = StopLossPips > 0m ? NormalizePrice(entryPrice + StopLossPips * _pipSize) : (decimal?)null;
			var takePrice = TakeProfitPips > 0m ? NormalizePrice(entryPrice - TakeProfitPips * _pipSize) : (decimal?)null;

			if (Position > 0)
				SellMarket();
			SellMarket();
			_shortStopPrice = stopPrice;
			_shortTakeProfit = takePrice;
		}
	}
	
	private void UpdateFractals(ICandleMessage candle)
	{
		_h1 = _h2;
		_h2 = _h3;
		_h3 = _h4;
		_h4 = _h5;
		_h5 = candle.HighPrice;
		
		_l1 = _l2;
		_l2 = _l3;
		_l3 = _l4;
		_l4 = _l5;
		_l5 = candle.LowPrice;
		
		if (_fractalCount < 5)
			_fractalCount++;
	}
	
	private decimal? DetectUpperFractal()
	{
		if (_fractalCount < 5)
			return null;
		
		return _h3 > _h1 && _h3 > _h2 && _h3 > _h4 && _h3 > _h5 ? _h3 : null;
	}
	
	private decimal? DetectLowerFractal()
	{
		if (_fractalCount < 5)
			return null;
		
		return _l3 < _l1 && _l3 < _l2 && _l3 < _l4 && _l3 < _l5 ? _l3 : null;
	}
	
	private void UpdateTrailingAndExits(ICandleMessage candle, decimal sarValue)
	{
		if (Position > 0)
		{
			if (_longTakeProfit is decimal tp && candle.HighPrice >= tp)
			{
				SellMarket();
				return;
			}
			
			if (_longStopPrice is decimal sl && candle.LowPrice <= sl)
			{
				SellMarket();
				return;
			}
			
			if (SarTrailingPips > 0m)
			{
				var trailingDistance = SarTrailingPips * _pipSize;
				if (sarValue < candle.ClosePrice - trailingDistance)
				{
					if (_longStopPrice is null || sarValue > _longStopPrice.Value)
					_longStopPrice = NormalizePrice(sarValue);
				}
			}
		}
		else if (Position < 0)
		{
			if (_shortTakeProfit is decimal tp && candle.LowPrice <= tp)
			{
				BuyMarket();
				return;
			}
			
			if (_shortStopPrice is decimal sl && candle.HighPrice >= sl)
			{
				BuyMarket();
				return;
			}
			
			if (SarTrailingPips > 0m)
			{
				var trailingDistance = SarTrailingPips * _pipSize;
				if (sarValue > candle.ClosePrice + trailingDistance)
				{
					if (_shortStopPrice is null || sarValue < _shortStopPrice.Value)
					_shortStopPrice = NormalizePrice(sarValue);
				}
			}
		}
	}
	
	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);
		
		var delta = Position - _previousPosition;
		_previousPosition = Position;

		if (Position == 0)
		{
			_longStopPrice = null;
			_longTakeProfit = null;
			_shortStopPrice = null;
			_shortTakeProfit = null;
			_pendingLongEntry = null;
			_pendingLongStop = null;
			_pendingLongTake = null;
			_pendingShortEntry = null;
			_pendingShortStop = null;
			_pendingShortTake = null;
			return;
		}

		if (delta > 0 && Position > 0)
		{
			if (_pendingLongEntry is decimal)
			{
				_longStopPrice = _pendingLongStop;
				_longTakeProfit = _pendingLongTake;
			}
			
			CancelSellStop();
			_buyStopOrder = null;
			_pendingLongEntry = null;
			_pendingLongStop = null;
			_pendingLongTake = null;
		}
		else if (delta < 0 && Position < 0)
		{
			if (_pendingShortEntry is decimal)
			{
				_shortStopPrice = _pendingShortStop;
				_shortTakeProfit = _pendingShortTake;
			}
			
			CancelBuyStop();
			_sellStopOrder = null;
			_pendingShortEntry = null;
			_pendingShortStop = null;
			_pendingShortTake = null;
		}
	}
	
	private void CancelBuyStop()
	{
		if (_buyStopOrder != null && _buyStopOrder.State == OrderStates.Active)
			{} // CancelOrder not available
		
		_buyStopOrder = null;
		_pendingLongEntry = null;
		_pendingLongStop = null;
		_pendingLongTake = null;
	}
	
	private void CancelSellStop()
	{
		if (_sellStopOrder != null && _sellStopOrder.State == OrderStates.Active)
			{} // CancelOrder not available
		
		_sellStopOrder = null;
		_pendingShortEntry = null;
		_pendingShortStop = null;
		_pendingShortTake = null;
	}
	
	private decimal GetPipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return 0m;
		
		var temp = step;
		var decimals = 0;
		while (temp != Math.Truncate(temp) && decimals < 10)
		{
			temp *= 10m;
			decimals++;
		}
		
		return decimals == 3 || decimals == 5 ? step * 10m : step;
	}
	
	private decimal NormalizePrice(decimal price)
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return price;
		
		var steps = decimal.Round(price / step, 0, MidpointRounding.AwayFromZero);
		return steps * step;
	}
}