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()