Pearson's Rオシレーター戦略
Pearson's Rオシレーター戦略は、ピアソン相関係数を使って価格が線形回帰チャネルに最もよく適合する期間を動的に探索します。相関が指定された正または負の閾値に達すると、戦略は回帰チャネルを形成しブレイクアウトを取引します。
価格がチャネル境界を越えた際にポジションを建て、中心線のクロスで決済できます。このアプローチは最も強い相関にルックバックウィンドウを自動調整することで市場状況に適応します。
詳細
- エントリー条件:
- 価格が上部回帰ラインを上抜け → ロング。
- 価格が下部回帰ラインを下抜け → ショート。
- ロング/ショート: 両サイド。
- エグジット条件:
- 反対方向への中心線クロス。
- ストップ: なし。
- デフォルト値:
MinPeriod= 48MaxPeriod= 360Step= 12IdealPositive= 0.85IdealNegative= -0.85Deviations= 2
- フィルター:
- カテゴリ: トレンドフォロー
- 方向: 両方
- インジケーター: Pearson's R, 線形回帰
- ストップ: なし
- 複雑さ: 中程度
- 時間軸: 任意
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: いいえ
- リスクレベル: 中
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()