在 GitHub 上查看

Forecast Oscillator 策略

该策略将经典的 Forecast Oscillator 指标移植到 StockSharp。线性回归作为基准,随后使用 Tillson T3 平滑以捕捉趋势反转。当振荡器向上穿越其平滑线并且平滑线仍为负值时产生做多信号;当振荡器向下穿越且平滑线为正值时产生做空信号。

算法遵循原始 MQL 实现,并支持分别启用或禁用开仓和平仓。

细节

  • 入场条件
    • 多头:振荡器上穿 T3 且 T3 < 0。
    • 空头:振荡器下穿 T3 且 T3 > 0。
  • 多/空:均支持。
  • 出场条件
    • 若相应的平仓选项开启,则在反向信号时退出。
  • 止损:无。
  • 过滤器:无。
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy based on the Forecast Oscillator indicator.
/// Uses linear regression forecast with T3 smoothing for signal generation.
/// </summary>
public class ForecastOscillatorStrategy : Strategy
{
	private readonly StrategyParam<int> _length;
	private readonly StrategyParam<int> _t3Period;
	private readonly StrategyParam<decimal> _bFactor;
	private readonly StrategyParam<DataType> _candleType;

	private LinearRegression _linReg;

	private decimal _b2, _b3, _c1, _c2, _c3, _c4, _w1, _w2;
	private decimal _e1, _e2, _e3, _e4, _e5, _e6;
	private decimal? _forecastPrev1, _forecastPrev2;
	private decimal? _sigPrev1, _sigPrev2, _sigPrev3;

	public int Length { get => _length.Value; set => _length.Value = value; }
	public int T3Period { get => _t3Period.Value; set => _t3Period.Value = value; }
	public decimal BFactor { get => _bFactor.Value; set => _bFactor.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public ForecastOscillatorStrategy()
	{
		_length = Param(nameof(Length), 15)
			.SetGreaterThanZero()
			.SetDisplay("Length", "Regression length", "Indicators");

		_t3Period = Param(nameof(T3Period), 3)
			.SetGreaterThanZero()
			.SetDisplay("T3 Period", "T3 smoothing period", "Indicators");

		_bFactor = Param(nameof(BFactor), 0.7m)
			.SetDisplay("T3 Factor", "T3 smoothing factor", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_b2 = default; _b3 = default; _c1 = default; _c2 = default; _c3 = default; _c4 = default;
		_w1 = default; _w2 = default;
		_e1 = default; _e2 = default; _e3 = default; _e4 = default; _e5 = default; _e6 = default;
		_forecastPrev1 = default; _forecastPrev2 = default;
		_sigPrev1 = default; _sigPrev2 = default; _sigPrev3 = default;
	}

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

		var linReg = new LinearRegression { Length = Length };
		_linReg = linReg;

		// Pre-calculate T3 constants
		var b = BFactor;
		_b2 = b * b;
		_b3 = _b2 * b;
		_c1 = -_b3;
		_c2 = 3m * (_b2 + _b3);
		_c3 = -3m * (2m * _b2 + b + _b3);
		_c4 = 1m + 3m * b + _b3 + 3m * _b2;

		var n = 1m + 0.5m * ((decimal)T3Period - 1m);
		_w1 = 2m / (n + 1m);
		_w2 = 1m - _w1;

		_e1 = _e2 = _e3 = _e4 = _e5 = _e6 = 0;
		_forecastPrev1 = _forecastPrev2 = null;
		_sigPrev1 = _sigPrev2 = _sigPrev3 = null;

		Indicators.Add(linReg);

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

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

		var price = candle.ClosePrice;
		var lrResult = _linReg.Process(price, candle.OpenTime, true);
		if (!lrResult.IsFormed)
			return;

		var lrValue = (LinearRegressionValue)lrResult;
		if (lrValue.LinearReg is not decimal regValue || regValue == 0)
			return;

		var forecast = (price - regValue) / regValue * 100m;

		// T3 smoothing
		_e1 = _w1 * forecast + _w2 * _e1;
		_e2 = _w1 * _e1 + _w2 * _e2;
		_e3 = _w1 * _e2 + _w2 * _e3;
		_e4 = _w1 * _e3 + _w2 * _e4;
		_e5 = _w1 * _e4 + _w2 * _e5;
		_e6 = _w1 * _e5 + _w2 * _e6;
		var t3 = _c1 * _e6 + _c2 * _e5 + _c3 * _e4 + _c4 * _e3;

		// Cross detection: forecast crosses signal line
		if (_forecastPrev1 != null && _forecastPrev2 != null && _sigPrev1 != null && _sigPrev2 != null && _sigPrev3 != null)
		{
			var buySignal = _forecastPrev1 > _sigPrev2 && _forecastPrev2 <= _sigPrev3 && _sigPrev1 < 0;
			var sellSignal = _forecastPrev1 < _sigPrev2 && _forecastPrev2 >= _sigPrev3 && _sigPrev1 > 0;

			if (buySignal && Position <= 0)
			{
				if (Position < 0) BuyMarket();
				BuyMarket();
			}
			else if (sellSignal && Position >= 0)
			{
				if (Position > 0) SellMarket();
				SellMarket();
			}
		}

		// Shift previous values
		_forecastPrev2 = _forecastPrev1;
		_forecastPrev1 = forecast;
		_sigPrev3 = _sigPrev2;
		_sigPrev2 = _sigPrev1;
		_sigPrev1 = t3;
	}
}