E回归通道策略
概述
E Regression Channel Strategy 基于 StockSharp 高级 API 复刻 MetaTrader 的 “e-Regr” 策略。该策略对最近的收盘价进行多项式回归,利用残差标准差生成上下通道,并在价格触及通道时触发信号。它是一种侧重均值回归的系统,同时提供日内时间过滤、日波动过滤以及可选的保护性止损与移动止损。
交易逻辑
- 订阅参数
Candle Type指定的主时间框架,使用最近Regression Length根K线计算回归通道。 - 中轨是回归曲线,
Std Dev Multiplier控制的标准差倍数决定上轨与下轨的距离。 - 当收盘价向上穿越中轨时,立即平掉所有多头;当收盘价向下跌破中轨时,立即平掉所有空头。
- 当当前K线最低价触碰或跌破下轨时,先平仓现有空头,再开仓做多。
- 当当前K线最高价触碰或突破上轨时,先平掉多头,再开仓做空。
- 若
Enable Trailing打开,则当价格达到Trailing Activation指定的盈利幅度后,按照Trailing Distance的距离启动移动止损。 - 若前一日K线的高低价差超过
Daily Range Filter或当前时间不在[Trade Start, Trade End)区间内,则忽略新的入场信号。
参数说明
Volume– 每次入场使用的下单手数(反向前会先平仓)。Trade Start/Trade End– 每日可交易时段,支持跨午夜。Regression Length– 回归计算使用的K线数量。Degree– 多项式阶数(1–6)。Std Dev Multiplier– 残差标准差的倍数,用于计算上下轨。Enable Trailing– 是否启用移动止损。Trailing Activation– 移动止损开始前所需的盈利点数。Trailing Distance– 移动止损保持的点差距离。Stop Loss– 固定止损距离(点),0 表示禁用。Take Profit– 固定止盈距离(点),0 表示禁用。Daily Range Filter– 前一日最大允许波动范围(点)。Candle Type– 主K线周期(默认30分钟)。
默认设置
Volume= 0.1Trade Start= 03:00Trade End= 21:20Regression Length= 250Degree= 3Std Dev Multiplier= 1.0Enable Trailing= falseTrailing Activation= 30 点Trailing Distance= 30 点Stop Loss= 0 点Take Profit= 0 点Daily Range Filter= 150 点Candle Type= 30 分钟
补充说明
- 策略仅使用已完成的K线,不会在同一根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>
/// Polynomial regression channel strategy. Calculates a regression midline with
/// standard deviation bands and trades mean reversion between the bands and midline.
/// </summary>
public class ERegressionChannelStrategy : Strategy
{
private readonly StrategyParam<int> _regressionLength;
private readonly StrategyParam<int> _degree;
private readonly StrategyParam<decimal> _stdMultiplier;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<DataType> _candleType;
private readonly Queue<decimal> _closes = new();
private ExponentialMovingAverage _ema;
private decimal? _previousMid;
/// <summary>
/// Initializes a new instance of the <see cref="ERegressionChannelStrategy"/> class.
/// </summary>
public ERegressionChannelStrategy()
{
_regressionLength = Param(nameof(RegressionLength), 100)
.SetGreaterThanZero()
.SetDisplay("Regression Length", "Number of bars used for regression", "Regression");
_degree = Param(nameof(Degree), 3)
.SetGreaterThanZero()
.SetDisplay("Degree", "Polynomial degree for the regression", "Regression");
_stdMultiplier = Param(nameof(StdDevMultiplier), 1m)
.SetGreaterThanZero()
.SetDisplay("Std Dev Multiplier", "Width multiplier for the regression bands", "Regression");
_stopLossPoints = Param(nameof(StopLossPoints), 500m)
.SetNotNegative()
.SetDisplay("Stop Loss", "Protective stop in absolute points (0 disables)", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
.SetNotNegative()
.SetDisplay("Take Profit", "Target in absolute points (0 disables)", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary candle type used for trading", "General");
Volume = 1;
}
/// <summary>
/// Number of bars used for regression.
/// </summary>
public int RegressionLength
{
get => _regressionLength.Value;
set => _regressionLength.Value = value;
}
/// <summary>
/// Polynomial degree for the regression.
/// </summary>
public int Degree
{
get => _degree.Value;
set => _degree.Value = value;
}
/// <summary>
/// Width multiplier for the regression bands.
/// </summary>
public decimal StdDevMultiplier
{
get => _stdMultiplier.Value;
set => _stdMultiplier.Value = value;
}
/// <summary>
/// Protective stop in absolute points (0 disables).
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Target in absolute points (0 disables).
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Primary candle type used for trading.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_closes.Clear();
_previousMid = null;
_ema = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
_ema = new ExponentialMovingAverage { Length = Math.Max(2, RegressionLength) };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_ema, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ema);
DrawOwnTrades(area);
}
var tp = TakeProfitPoints > 0 ? new Unit(TakeProfitPoints, UnitTypes.Absolute) : null;
var sl = StopLossPoints > 0 ? new Unit(StopLossPoints, UnitTypes.Absolute) : null;
if (tp != null || sl != null)
StartProtection(tp, sl);
base.OnStarted2(time);
}
private void ProcessCandle(ICandleMessage candle, decimal emaValue)
{
if (candle.State != CandleStates.Finished)
return;
_closes.Enqueue(candle.ClosePrice);
if (_closes.Count > RegressionLength)
_closes.Dequeue();
if (_closes.Count < RegressionLength)
return;
var prices = new List<decimal>(_closes);
var coeffs = PolyFit(prices, Degree);
var currentIndex = prices.Count - 1;
var mid = PolyEval(coeffs, currentIndex);
var std = CalcStd(prices, coeffs) * StdDevMultiplier;
var upper = mid + std;
var lower = mid - std;
// Exit at midline
if (Position > 0 && candle.ClosePrice >= mid)
{
SellMarket(Position);
_previousMid = mid;
return;
}
if (Position < 0 && candle.ClosePrice <= mid)
{
BuyMarket(Math.Abs(Position));
_previousMid = mid;
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
{
_previousMid = mid;
return;
}
// Entry signals: mean reversion from bands
if (candle.LowPrice <= lower && Position <= 0)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
BuyMarket(Volume);
}
else if (candle.HighPrice >= upper && Position >= 0)
{
if (Position > 0)
SellMarket(Position);
SellMarket(Volume);
}
_previousMid = mid;
}
private static decimal[] PolyFit(IReadOnlyList<decimal> y, int degree)
{
var n = y.Count;
var actualDegree = Math.Min(degree, Math.Max(1, n - 1));
var size = actualDegree + 1;
var matrix = new decimal[size, size + 1];
for (var row = 0; row < size; row++)
{
for (var col = 0; col < size; col++)
{
decimal sum = 0m;
for (var i = 0; i < n; i++)
sum += (decimal)Math.Pow(i, row + col);
matrix[row, col] = sum;
}
decimal sumY = 0m;
for (var i = 0; i < n; i++)
sumY += y[i] * (decimal)Math.Pow(i, row);
matrix[row, size] = sumY;
}
for (var i = 0; i < size; i++)
{
var pivot = matrix[i, i];
if (pivot == 0m)
{
for (var k = i + 1; k < size; k++)
{
if (matrix[k, i] == 0m)
continue;
for (var j = i; j < size + 1; j++)
(matrix[i, j], matrix[k, j]) = (matrix[k, j], matrix[i, j]);
pivot = matrix[i, i];
break;
}
}
if (pivot == 0m)
continue;
for (var j = i; j < size + 1; j++)
matrix[i, j] /= pivot;
for (var row = 0; row < size; row++)
{
if (row == i)
continue;
var factor = matrix[row, i];
if (factor == 0m)
continue;
for (var col = i; col < size + 1; col++)
matrix[row, col] -= factor * matrix[i, col];
}
}
var coeffs = new decimal[size];
for (var i = 0; i < size; i++)
coeffs[i] = matrix[i, size];
return coeffs;
}
private static decimal PolyEval(IReadOnlyList<decimal> coeffs, decimal x)
{
decimal result = 0m;
decimal power = 1m;
for (var i = 0; i < coeffs.Count; i++)
{
result += coeffs[i] * power;
power *= x;
}
return result;
}
private static decimal CalcStd(IReadOnlyList<decimal> values, decimal[] coeffs)
{
var n = values.Count;
if (n == 0)
return 0m;
decimal sum = 0m;
for (var i = 0; i < n; i++)
{
var fitted = PolyEval(coeffs, i);
var diff = values[i] - fitted;
sum += diff * diff;
}
return (decimal)Math.Sqrt((double)(sum / n));
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from collections import deque
class e_regression_channel_strategy(Strategy):
"""
Polynomial regression channel strategy. Calculates a regression midline with
standard deviation bands and trades mean reversion between the bands and midline.
"""
def __init__(self):
super(e_regression_channel_strategy, self).__init__()
self._regression_length = self.Param("RegressionLength", 100) \
.SetDisplay("Regression Length", "Number of bars used for regression", "Regression")
self._degree = self.Param("Degree", 3) \
.SetDisplay("Degree", "Polynomial degree for the regression", "Regression")
self._std_multiplier = self.Param("StdDevMultiplier", 1.0) \
.SetDisplay("Std Dev Multiplier", "Width multiplier for the regression bands", "Regression")
self._stop_loss_points = self.Param("StopLossPoints", 500.0) \
.SetDisplay("Stop Loss", "Protective stop in absolute points (0 disables)", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 500.0) \
.SetDisplay("Take Profit", "Target in absolute points (0 disables)", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Primary candle type used for trading", "General")
self._closes = deque()
self._previous_mid = None
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(e_regression_channel_strategy, self).OnReseted()
self._closes.clear()
self._previous_mid = None
def OnStarted2(self, time):
super(e_regression_channel_strategy, self).OnStarted2(time)
ema = ExponentialMovingAverage()
length = self._regression_length.Value
ema.Length = max(2, length)
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(ema, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
tp = self._take_profit_points.Value
sl = self._stop_loss_points.Value
tp_unit = Unit(float(tp), UnitTypes.Absolute) if tp > 0 else None
sl_unit = Unit(float(sl), UnitTypes.Absolute) if sl > 0 else None
if tp_unit is not None or sl_unit is not None:
self.StartProtection(tp_unit, sl_unit)
def _process_candle(self, candle, ema_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
reg_len = self._regression_length.Value
self._closes.append(close)
if len(self._closes) > reg_len:
self._closes.popleft()
if len(self._closes) < reg_len:
return
prices = list(self._closes)
coeffs = self._poly_fit(prices, self._degree.Value)
current_index = len(prices) - 1
mid = self._poly_eval(coeffs, current_index)
std = self._calc_std(prices, coeffs) * self._std_multiplier.Value
upper = mid + std
lower = mid - std
if self.Position > 0 and close >= mid:
self.SellMarket()
self._previous_mid = mid
return
if self.Position < 0 and close <= mid:
self.BuyMarket()
self._previous_mid = mid
return
if not self.IsFormedAndOnlineAndAllowTrading():
self._previous_mid = mid
return
if float(candle.LowPrice) <= lower and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif float(candle.HighPrice) >= upper and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._previous_mid = mid
@staticmethod
def _poly_fit(y, degree):
n = len(y)
actual_degree = min(degree, max(1, n - 1))
size = actual_degree + 1
matrix = [[0.0] * (size + 1) for _ in range(size)]
for row in range(size):
for col in range(size):
s = 0.0
for i in range(n):
s += i ** (row + col)
matrix[row][col] = s
s_y = 0.0
for i in range(n):
s_y += y[i] * (i ** row)
matrix[row][size] = s_y
for i in range(size):
pivot = matrix[i][i]
if pivot == 0.0:
for k in range(i + 1, size):
if matrix[k][i] != 0.0:
matrix[i], matrix[k] = matrix[k], matrix[i]
pivot = matrix[i][i]
break
if pivot == 0.0:
continue
for j in range(i, size + 1):
matrix[i][j] /= pivot
for row in range(size):
if row == i:
continue
factor = matrix[row][i]
if factor == 0.0:
continue
for col in range(i, size + 1):
matrix[row][col] -= factor * matrix[i][col]
return [matrix[i][size] for i in range(size)]
@staticmethod
def _poly_eval(coeffs, x):
result = 0.0
power = 1.0
for c in coeffs:
result += c * power
power *= x
return result
@staticmethod
def _calc_std(values, coeffs):
n = len(values)
if n == 0:
return 0.0
s = 0.0
for i in range(n):
fitted = e_regression_channel_strategy._poly_eval(coeffs, i)
diff = values[i] - fitted
s += diff * diff
return math.sqrt(s / n)
def CreateClone(self):
return e_regression_channel_strategy()