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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import LinearRegression
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class forecast_oscillator_strategy(Strategy):
def __init__(self):
super(forecast_oscillator_strategy, self).__init__()
self._length = self.Param("Length", 15) \
.SetDisplay("Length", "Regression length", "Indicators")
self._t3_period = self.Param("T3Period", 3) \
.SetDisplay("T3 Period", "T3 smoothing period", "Indicators")
self._b_factor = self.Param("BFactor", 0.7) \
.SetDisplay("T3 Factor", "T3 smoothing factor", "Indicators")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles", "General")
self._lin_reg = None
self._b2 = 0.0
self._b3 = 0.0
self._c1 = 0.0
self._c2 = 0.0
self._c3 = 0.0
self._c4 = 0.0
self._w1 = 0.0
self._w2 = 0.0
self._e1 = 0.0
self._e2 = 0.0
self._e3 = 0.0
self._e4 = 0.0
self._e5 = 0.0
self._e6 = 0.0
self._forecast_prev1 = None
self._forecast_prev2 = None
self._sig_prev1 = None
self._sig_prev2 = None
self._sig_prev3 = None
@property
def Length(self):
return self._length.Value
@Length.setter
def Length(self, value):
self._length.Value = value
@property
def T3Period(self):
return self._t3_period.Value
@T3Period.setter
def T3Period(self, value):
self._t3_period.Value = value
@property
def BFactor(self):
return self._b_factor.Value
@BFactor.setter
def BFactor(self, value):
self._b_factor.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(forecast_oscillator_strategy, self).OnStarted2(time)
self._lin_reg = LinearRegression()
self._lin_reg.Length = self.Length
b = float(self.BFactor)
self._b2 = b * b
self._b3 = self._b2 * b
self._c1 = -self._b3
self._c2 = 3.0 * (self._b2 + self._b3)
self._c3 = -3.0 * (2.0 * self._b2 + b + self._b3)
self._c4 = 1.0 + 3.0 * b + self._b3 + 3.0 * self._b2
n = 1.0 + 0.5 * (float(self.T3Period) - 1.0)
self._w1 = 2.0 / (n + 1.0)
self._w2 = 1.0 - self._w1
self._e1 = self._e2 = self._e3 = self._e4 = self._e5 = self._e6 = 0.0
self._forecast_prev1 = None
self._forecast_prev2 = None
self._sig_prev1 = None
self._sig_prev2 = None
self._sig_prev3 = None
self.SubscribeCandles(self.CandleType) \
.Bind(self.ProcessCandle) \
.Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
price = float(candle.ClosePrice)
t = candle.OpenTime
lr_result = process_float(self._lin_reg, price, t, True)
if not lr_result.IsFormed:
return
lr_val = lr_result.LinearReg
if lr_val is None:
return
reg_value = float(lr_val)
if reg_value == 0:
return
forecast = (price - reg_value) / reg_value * 100.0
self._e1 = self._w1 * forecast + self._w2 * self._e1
self._e2 = self._w1 * self._e1 + self._w2 * self._e2
self._e3 = self._w1 * self._e2 + self._w2 * self._e3
self._e4 = self._w1 * self._e3 + self._w2 * self._e4
self._e5 = self._w1 * self._e4 + self._w2 * self._e5
self._e6 = self._w1 * self._e5 + self._w2 * self._e6
t3 = self._c1 * self._e6 + self._c2 * self._e5 + self._c3 * self._e4 + self._c4 * self._e3
if self._forecast_prev1 is not None and self._forecast_prev2 is not None and \
self._sig_prev1 is not None and self._sig_prev2 is not None and self._sig_prev3 is not None:
buy_signal = self._forecast_prev1 > self._sig_prev2 and self._forecast_prev2 <= self._sig_prev3 and self._sig_prev1 < 0
sell_signal = self._forecast_prev1 < self._sig_prev2 and self._forecast_prev2 >= self._sig_prev3 and self._sig_prev1 > 0
if buy_signal and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif sell_signal and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._forecast_prev2 = self._forecast_prev1
self._forecast_prev1 = forecast
self._sig_prev3 = self._sig_prev2
self._sig_prev2 = self._sig_prev1
self._sig_prev1 = t3
def OnReseted(self):
super(forecast_oscillator_strategy, self).OnReseted()
self._lin_reg = None
self._b2 = 0.0
self._b3 = 0.0
self._c1 = 0.0
self._c2 = 0.0
self._c3 = 0.0
self._c4 = 0.0
self._w1 = 0.0
self._w2 = 0.0
self._e1 = 0.0
self._e2 = 0.0
self._e3 = 0.0
self._e4 = 0.0
self._e5 = 0.0
self._e6 = 0.0
self._forecast_prev1 = None
self._forecast_prev2 = None
self._sig_prev1 = None
self._sig_prev2 = None
self._sig_prev3 = None
def CreateClone(self):
return forecast_oscillator_strategy()