在 GitHub 上查看

Range Expansion Index 策略

该策略实现 Tom DeMark 的 Range Expansion Index (REI),用于衡量价格的强弱。 指标比较当前的高低价与过去的价格,在正负值之间波动。

工作原理

  • 当 REI 在低于 下限(默认 -60)后再次上穿该水平时,策略开多。
  • 当 REI 在高于 上限(默认 60)后跌破该水平时,策略开空。
  • 当出现反向信号时,会自动平掉相反方向的持仓。

参数

  • REI Period – 计算 REI 所使用的柱数(默认 8)。
  • Up Level – 向下突破时表示价格走弱的上阈值(默认 60)。
  • Down Level – 向上突破时表示价格走强的下阈值(默认 -60)。
  • Candle Type – 用于计算指标的K线时间框架(默认 8 小时)。

使用方法

将策略附加到某个品种并启动。策略会订阅所选的K线序列,并根据 REI 信号使用市价单进出场。

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>
/// Strategy based on the Range Expansion Index (REI).
/// Opens long positions when REI crosses above the down level (-60)
/// and short positions when it crosses below the up level (+60).
/// </summary>
public class RangeExpansionIndexStrategy : Strategy
{
	private readonly StrategyParam<int> _reiPeriod;
	private readonly StrategyParam<decimal> _upLevel;
	private readonly StrategyParam<decimal> _downLevel;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<DataType> _candleType;

	private RangeExpansionIndex _rei;
	private decimal? _prevRei;
	private int _barsSinceTrade;

	/// <summary>
	/// REI calculation period.
	/// </summary>
	public int ReiPeriod
	{
		get => _reiPeriod.Value;
		set => _reiPeriod.Value = value;
	}

	/// <summary>
	/// Upper indicator level.
	/// </summary>
	public decimal UpLevel
	{
		get => _upLevel.Value;
		set => _upLevel.Value = value;
	}

	/// <summary>
	/// Lower indicator level.
	/// </summary>
	public decimal DownLevel
	{
		get => _downLevel.Value;
		set => _downLevel.Value = value;
	}

	/// <summary>
	/// Bars to wait after a completed trade.
	/// </summary>
	public int CooldownBars
	{
		get => _cooldownBars.Value;
		set => _cooldownBars.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of <see cref="RangeExpansionIndexStrategy"/>.
	/// </summary>
	public RangeExpansionIndexStrategy()
	{
		_reiPeriod = Param(nameof(ReiPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("REI Period", "Length of REI indicator", "Parameters")
			;

		_upLevel = Param(nameof(UpLevel), 70m)
			.SetDisplay("Up Level", "Upper threshold", "Parameters")
			;

		_downLevel = Param(nameof(DownLevel), -70m)
			.SetDisplay("Down Level", "Lower threshold", "Parameters")
			;

		_cooldownBars = Param(nameof(CooldownBars), 1)
			.SetDisplay("Cooldown Bars", "Bars to wait after a completed trade", "Parameters");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_rei?.Reset();
		_prevRei = null;
		_barsSinceTrade = CooldownBars;
	}

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

		_rei = new RangeExpansionIndex { Length = ReiPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_rei, ProcessCandle)
			.Start();

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

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

		if (!_rei.IsFormed)
		{
			_prevRei = reiValue;
			return;
		}

		if (_barsSinceTrade < CooldownBars)
			_barsSinceTrade++;

		if (_prevRei is decimal prev)
		{
			if (_barsSinceTrade >= CooldownBars)
			{
				if (prev < DownLevel && reiValue >= DownLevel && Position <= 0)
				{
					BuyMarket(Volume + Math.Abs(Position));
					_barsSinceTrade = 0;
				}
				else if (prev > UpLevel && reiValue <= UpLevel && Position >= 0)
				{
					SellMarket(Volume + Math.Abs(Position));
					_barsSinceTrade = 0;
				}
			}
		}

		_prevRei = reiValue;
	}

	private class RangeExpansionIndex : BaseIndicator
	{
		public int Length { get; set; } = 8;

		private readonly List<ICandleMessage> _buffer = new();

		protected override IIndicatorValue OnProcess(IIndicatorValue input)
		{
			var candle = input.GetValue<ICandleMessage>();

			if (candle == null)
			{
				IsFormed = false;
				return new DecimalIndicatorValue(this, 0m, input.Time);
			}

			_buffer.Add(candle);

			var need = Length + 8;
			if (_buffer.Count > need)
				_buffer.RemoveAt(0);

			if (_buffer.Count < need)
			{
				IsFormed = false;
				return new DecimalIndicatorValue(this, 0m, input.Time);
			}

			var last = _buffer.Count - 1;
			decimal subSum = 0m;
			decimal absSum = 0m;

			for (var i = last; i > last - Length; i--)
			{
				if (_buffer[i] == null || _buffer[i - 2] == null || _buffer[i - 5] == null || _buffer[i - 6] == null || _buffer[i - 7] == null || _buffer[i - 8] == null)
				{
					IsFormed = false;
					return new DecimalIndicatorValue(this, 0m, input.Time);
				}

				var hi = _buffer[i].HighPrice;
				var hi2 = _buffer[i - 2].HighPrice;
				var lo = _buffer[i].LowPrice;
				var lo2 = _buffer[i - 2].LowPrice;

				var diff1 = hi - hi2;
				var diff2 = lo - lo2;

				var num1 = (_buffer[i - 2].HighPrice < _buffer[i - 7].ClosePrice &&
					_buffer[i - 2].HighPrice < _buffer[i - 8].ClosePrice &&
					hi < _buffer[i - 5].HighPrice &&
					hi < _buffer[i - 6].HighPrice) ? 0m : 1m;

				var num2 = (_buffer[i - 2].LowPrice > _buffer[i - 7].ClosePrice &&
					_buffer[i - 2].LowPrice > _buffer[i - 8].ClosePrice &&
					lo > _buffer[i - 5].LowPrice &&
					lo > _buffer[i - 6].LowPrice) ? 0m : 1m;

				subSum += num1 * num2 * (diff1 + diff2);
				absSum += Math.Abs(diff1) + Math.Abs(diff2);
			}

			var rei = absSum == 0m ? 0m : subSum / absSum * 100m;
			IsFormed = true;
			return new DecimalIndicatorValue(this, rei, input.Time);
		}

		public override void Reset()
		{
			base.Reset();
			_buffer.Clear();
		}
	}
}