Pearson's R Oscillator Strategy
The Pearson's R Oscillator strategy dynamically searches for the period where price best fits a linear regression channel using the Pearson correlation coefficient. When the correlation reaches the specified positive or negative threshold, the strategy forms a regression channel and trades breakouts.
Positions are opened when price crosses the channel boundaries and can be closed on midline crosses. The approach adapts to market conditions by automatically adjusting the lookback window to the strongest correlation.
Details
- Entry Criteria:
- Price crosses above the upper regression line → Long.
- Price crosses below the lower regression line → Short.
- Long/Short: Both sides.
- Exit Criteria:
- Midline cross in the opposite direction.
- Stops: None.
- Default Values:
MinPeriod= 48MaxPeriod= 360Step= 12IdealPositive= 0.85IdealNegative= -0.85Deviations= 2
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: Pearson's R, Linear Regression
- Stops: None
- Complexity: Medium
- Timeframe: Any
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk level: Medium
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Pearson's R oscillator strategy.
/// On every candle the closes of the last MinPeriod, MinPeriod + Step, ... MaxPeriod bars are fitted with a linear regression
/// and the period with the strongest Pearson correlation is chosen. When that correlation is at least IdealPositive or at most
/// IdealNegative, a channel is drawn around the regression line at Deviations standard deviations of the residuals. A close
/// crossing above the upper line goes long and a close crossing below the lower line goes short, reversing an opposite
/// position. A long closes when the close crosses below the midline and a short when it crosses above it.
/// </summary>
public class PearsonsROscillatorStrategy : Strategy
{
private readonly StrategyParam<int> _minPeriod;
private readonly StrategyParam<int> _maxPeriod;
private readonly StrategyParam<int> _step;
private readonly StrategyParam<decimal> _idealPositive;
private readonly StrategyParam<decimal> _idealNegative;
private readonly StrategyParam<decimal> _deviations;
private readonly StrategyParam<DataType> _candleType;
// Prefix sums of the closes (shifted by the first close), their squares and index-weighted values.
private readonly List<double> _sumY = [];
private readonly List<double> _sumYy = [];
private readonly List<double> _sumIy = [];
private double? _origin;
private double? _prevClose;
private (double upper, double mid, double lower)? _prevChannel;
private (double upper, double mid, double lower)? _lastChannel;
/// <summary>
/// Shortest regression period.
/// </summary>
public int MinPeriod
{
get => _minPeriod.Value;
set => _minPeriod.Value = value;
}
/// <summary>
/// Longest regression period.
/// </summary>
public int MaxPeriod
{
get => _maxPeriod.Value;
set => _maxPeriod.Value = value;
}
/// <summary>
/// Increment between tested periods.
/// </summary>
public int Step
{
get => _step.Value;
set => _step.Value = value;
}
/// <summary>
/// Correlation that confirms an upward channel.
/// </summary>
public decimal IdealPositive
{
get => _idealPositive.Value;
set => _idealPositive.Value = value;
}
/// <summary>
/// Correlation that confirms a downward channel.
/// </summary>
public decimal IdealNegative
{
get => _idealNegative.Value;
set => _idealNegative.Value = value;
}
/// <summary>
/// Channel width in standard deviations.
/// </summary>
public decimal Deviations
{
get => _deviations.Value;
set => _deviations.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public PearsonsROscillatorStrategy()
{
_minPeriod = Param(nameof(MinPeriod), 48)
.SetGreaterThanZero()
.SetDisplay("Min Period", "Shortest regression period", "Regression");
_maxPeriod = Param(nameof(MaxPeriod), 360)
.SetGreaterThanZero()
.SetDisplay("Max Period", "Longest regression period", "Regression");
_step = Param(nameof(Step), 12)
.SetGreaterThanZero()
.SetDisplay("Step", "Increment between tested periods", "Regression");
_idealPositive = Param(nameof(IdealPositive), 0.85m)
.SetDisplay("Ideal Positive", "Correlation that confirms an upward channel", "Regression");
_idealNegative = Param(nameof(IdealNegative), -0.85m)
.SetDisplay("Ideal Negative", "Correlation that confirms a downward channel", "Regression");
_deviations = Param(nameof(Deviations), 2m)
.SetGreaterThanZero()
.SetDisplay("Deviations", "Channel width in standard deviations", "Regression");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
private void ResetState()
{
_sumY.Clear();
_sumYy.Clear();
_sumIy.Clear();
_origin = null;
_prevClose = null;
_prevChannel = null;
_lastChannel = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var close = (double)candle.ClosePrice;
_origin ??= close;
var y = close - _origin.Value;
var index = _sumY.Count;
_sumY.Add((index > 0 ? _sumY[index - 1] : 0d) + y);
_sumYy.Add((index > 0 ? _sumYy[index - 1] : 0d) + y * y);
_sumIy.Add((index > 0 ? _sumIy[index - 1] : 0d) + index * y);
var channel = FindChannel(index);
var prevClose = _prevClose;
var prevChannel = _prevChannel;
_prevClose = close;
_prevChannel = channel ?? _lastChannel;
if (channel != null)
_lastChannel = channel;
if (prevClose is not double pc || prevChannel is not (double prevUpper, double prevMid, double prevLower) || _lastChannel is not (double upper, double mid, double lower))
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (channel != null && pc <= prevUpper && close > upper && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (channel != null && pc >= prevLower && close < lower && Position >= 0)
SellMarket(Volume + Math.Abs(Position));
else if (Position > 0 && pc >= prevMid && close < mid)
SellMarket(Position);
else if (Position < 0 && pc <= prevMid && close > mid)
BuyMarket(-Position);
}
private (double upper, double mid, double lower)? FindChannel(int last)
{
var bestR = 0d;
var bestPeriod = 0;
var bestSlope = 0d;
var bestIntercept = 0d;
var bestStd = 0d;
for (var period = MinPeriod; period <= MaxPeriod; period += Step)
{
if (period > last + 1)
break;
var start = last - period + 1;
double Range(List<double> sums) => sums[last] - (start > 0 ? sums[start - 1] : 0d);
var n = (double)period;
var sy = Range(_sumY);
var syy = Range(_sumYy);
// Local x runs from 0 to period - 1.
var sxy = Range(_sumIy) - start * sy;
var sx = n * (n - 1) / 2;
var sxx = (n - 1) * n * (2 * n - 1) / 6;
var covXy = sxy - sx * sy / n;
var varX = sxx - sx * sx / n;
var varY = syy - sy * sy / n;
if (varX <= 0 || varY <= 0)
continue;
var r = covXy / Math.Sqrt(varX * varY);
if (Math.Abs(r) <= Math.Abs(bestR))
continue;
var slope = covXy / varX;
bestR = r;
bestPeriod = period;
bestSlope = slope;
bestIntercept = (sy - slope * sx) / n;
bestStd = Math.Sqrt(Math.Max(0d, varY - slope * covXy) / n);
}
if (bestPeriod == 0 || (bestR < (double)IdealPositive && bestR > (double)IdealNegative))
return null;
var mid = bestIntercept + bestSlope * (bestPeriod - 1) + _origin.Value;
var width = (double)Deviations * bestStd;
return (mid + width, mid, mid - width);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
import math
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class pearsons_r_oscillator_strategy(Strategy):
"""
Pearson's R oscillator strategy.
On every candle the closes of the last MinPeriod, MinPeriod + Step, ... MaxPeriod bars are fitted with a linear regression
and the period with the strongest Pearson correlation is chosen. When that correlation is at least IdealPositive or at most
IdealNegative, a channel is drawn around the regression line at Deviations standard deviations of the residuals. A close
crossing above the upper line goes long and a close crossing below the lower line goes short, reversing an opposite
position. A long closes when the close crosses below the midline and a short when it crosses above it.
"""
def __init__(self):
super(pearsons_r_oscillator_strategy, self).__init__()
self._min_period = self.Param("MinPeriod", 48) \
.SetGreaterThanZero() \
.SetDisplay("Min Period", "Shortest regression period", "Regression")
self._max_period = self.Param("MaxPeriod", 360) \
.SetGreaterThanZero() \
.SetDisplay("Max Period", "Longest regression period", "Regression")
self._step = self.Param("Step", 12) \
.SetGreaterThanZero() \
.SetDisplay("Step", "Increment between tested periods", "Regression")
self._ideal_positive = self.Param("IdealPositive", 0.85) \
.SetDisplay("Ideal Positive", "Correlation that confirms an upward channel", "Regression")
self._ideal_negative = self.Param("IdealNegative", -0.85) \
.SetDisplay("Ideal Negative", "Correlation that confirms a downward channel", "Regression")
self._deviations = self.Param("Deviations", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Deviations", "Channel width in standard deviations", "Regression")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def CandleType(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def _reset_state(self):
# Prefix sums of the closes (shifted by the first close), their squares and index-weighted values.
self._sum_y = []
self._sum_yy = []
self._sum_iy = []
self._origin = None
self._prev_close = None
self._prev_channel = None
self._last_channel = None
def OnReseted(self):
super(pearsons_r_oscillator_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(pearsons_r_oscillator_strategy, self).OnStarted2(time)
self._reset_state()
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
if self._origin is None:
self._origin = close
y = close - self._origin
index = len(self._sum_y)
self._sum_y.append((self._sum_y[index - 1] if index > 0 else 0.0) + y)
self._sum_yy.append((self._sum_yy[index - 1] if index > 0 else 0.0) + y * y)
self._sum_iy.append((self._sum_iy[index - 1] if index > 0 else 0.0) + index * y)
channel = self._find_channel(index)
prev_close = self._prev_close
prev_channel = self._prev_channel
self._prev_close = close
self._prev_channel = channel if channel is not None else self._last_channel
if channel is not None:
self._last_channel = channel
if prev_close is None or prev_channel is None or self._last_channel is None:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
prev_upper, prev_mid, prev_lower = prev_channel
upper, mid, lower = self._last_channel
if channel is not None and prev_close <= prev_upper and close > upper and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif channel is not None and prev_close >= prev_lower and close < lower and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
elif self.Position > 0 and prev_close >= prev_mid and close < mid:
self.SellMarket(self.Position)
elif self.Position < 0 and prev_close <= prev_mid and close > mid:
self.BuyMarket(-self.Position)
def _find_channel(self, last):
best_r = 0.0
best_period = 0
best_slope = 0.0
best_intercept = 0.0
best_std = 0.0
period = self._min_period.Value
max_period = self._max_period.Value
step = self._step.Value
while period <= max_period:
if period > last + 1:
break
start = last - period + 1
n = float(period)
sy = self._sum_y[last] - (self._sum_y[start - 1] if start > 0 else 0.0)
syy = self._sum_yy[last] - (self._sum_yy[start - 1] if start > 0 else 0.0)
# Local x runs from 0 to period - 1.
sxy = self._sum_iy[last] - (self._sum_iy[start - 1] if start > 0 else 0.0) - start * sy
sx = n * (n - 1) / 2
sxx = (n - 1) * n * (2 * n - 1) / 6
cov_xy = sxy - sx * sy / n
var_x = sxx - sx * sx / n
var_y = syy - sy * sy / n
if var_x > 0 and var_y > 0:
r = cov_xy / math.sqrt(var_x * var_y)
if abs(r) > abs(best_r):
slope = cov_xy / var_x
best_r = r
best_period = period
best_slope = slope
best_intercept = (sy - slope * sx) / n
best_std = math.sqrt(max(0.0, var_y - slope * cov_xy) / n)
period += step
if best_period == 0 or (best_r < float(self._ideal_positive.Value) and best_r > float(self._ideal_negative.Value)):
return None
mid = best_intercept + best_slope * (best_period - 1) + self._origin
width = float(self._deviations.Value) * best_std
return (mid + width, mid, mid - width)
def CreateClone(self):
return pearsons_r_oscillator_strategy()