共和分ペア戦略
この戦略は長期的な共和分関係を持つ2つの資産を取引します。最初の資産とベータ調整された2番目の資産の残差を計算することで、歴史的に均衡に戻る乖離を探します。
テストによると、平均年間リターンは約103%です。株式市場で最も高いパフォーマンスを発揮します。
残差Z-Scoreが-EntryThresholdを下回ると、最初の資産を買い2番目を売るロングポジションが開始されます。Z-Scoreが閾値を上回ると、最初の資産を売り2番目を買うショートポジションが発生します。スプレッドがゼロに向かって正常化するとポジションが決済されます。
共和分ペアトレードは、2つの銘柄を同時に管理することに慣れた統計的アービトラージャーに適しています。内蔵のストップロスは、関係が一時的に崩れた場合の極端な動きから保護します。
詳細
- エントリー条件:
- ロング: 残差 Z-Score < -EntryThreshold
- ショート: 残差 Z-Score > EntryThreshold
- ロング/ショート: 両方。
- エグジット条件:
- ロング: |Z-Score| < ExitThreshold の時に決済
- ショート: |Z-Score| < ExitThreshold の時に決済
- ストップ: あり、パーセンテージストップロス。
- デフォルト値:
Period= 20EntryThreshold= 2ExitThreshold= 0.5Beta= 1StopLossPercent= 2CandleType= TimeSpan.FromMinutes(5)Asset2— 必須、既定値なし 残差は同時刻のローソク足における戦略のSecurityの終値からAsset2の終値のBeta倍を引いた値で、そのzスコアは直近Period個の残差の平均と標準偏差から計算します。0.5はルールが示す決済水準の既定値です。第1レッグはVolume、Asset2はVolumeのBeta倍で取引され、逆のシグナルで両レッグがドテンします。ストップは残差がエントリー時の値のStopLossPercentだけ不利に動いたときに両レッグを閉じ、ローソク足の終値で確認されます。0で無効になります。
- フィルター:
- カテゴリ: アービトラージ
- 方向: 両方
- インジケーター: 共和分
- ストップ: あり
- 複雑さ: 中級
- 時間軸: イントラデイ
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: はい
- リスクレベル: 中
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Cointegration pairs trading strategy.
/// The residual is the first instrument's close minus Beta times Asset2's on candles of the same time, and its z-score is measured against
/// the mean and standard deviation of the last Period residuals. A z-score below minus EntryThreshold buys the first instrument and sells
/// Beta times as much of Asset2, one above EntryThreshold does the opposite, reversing an opposite pair. Both legs close once the z-score
/// is back within ExitThreshold of zero, or once the residual moves StopLossPercent of its entry value against the pair.
/// </summary>
public class CointegrationPairsStrategy : Strategy
{
private readonly StrategyParam<int> _period;
private readonly StrategyParam<decimal> _entryThreshold;
private readonly StrategyParam<decimal> _exitThreshold;
private readonly StrategyParam<decimal> _beta;
private readonly StrategyParam<Security> _asset2;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly Dictionary<DateTime, decimal> _firstCloses = [];
private readonly Dictionary<DateTime, decimal> _secondCloses = [];
private readonly Queue<decimal> _residuals = [];
// 1 while long the pair, -1 while short it, 0 while flat.
private int _side;
private decimal _entryResidual;
/// <summary>
/// Number of residuals the mean and standard deviation are measured over.
/// </summary>
public int Period
{
get => _period.Value;
set => _period.Value = value;
}
/// <summary>
/// Z-score distance from zero that opens a pair.
/// </summary>
public decimal EntryThreshold
{
get => _entryThreshold.Value;
set => _entryThreshold.Value = value;
}
/// <summary>
/// Z-score distance from zero within which the pair closes.
/// </summary>
public decimal ExitThreshold
{
get => _exitThreshold.Value;
set => _exitThreshold.Value = value;
}
/// <summary>
/// Hedge ratio of Asset2 to the first instrument.
/// </summary>
public decimal Beta
{
get => _beta.Value;
set => _beta.Value = value;
}
/// <summary>
/// Second asset of the pair.
/// </summary>
public Security Asset2
{
get => _asset2.Value;
set => _asset2.Value = value;
}
/// <summary>
/// Adverse residual move, in percent of the entry residual, that closes the pair.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public CointegrationPairsStrategy()
{
_period = Param(nameof(Period), 20)
.SetGreaterThanZero()
.SetDisplay("Period", "Residuals the mean and standard deviation are measured over", "Parameters");
_entryThreshold = Param(nameof(EntryThreshold), 2m)
.SetGreaterThanZero()
.SetDisplay("Entry Threshold", "Z-score distance from zero that opens a pair", "Parameters");
_exitThreshold = Param(nameof(ExitThreshold), 0.5m)
.SetNotNegative()
.SetDisplay("Exit Threshold", "Z-score distance from zero within which the pair closes", "Parameters");
_beta = Param(nameof(Beta), 1m)
.SetGreaterThanZero()
.SetDisplay("Beta", "Hedge ratio of Asset2 to the first instrument", "Parameters");
_asset2 = Param<Security>(nameof(Asset2))
.SetDisplay("Asset 2", "Second asset of the pair", "Parameters")
.SetRequired();
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetNotNegative()
.SetDisplay("Stop Loss %", "Adverse residual move in percent of the entry residual", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType), (Asset2, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (Asset2 == null)
throw new InvalidOperationException("Second asset is not specified.");
ResetState();
var firstSubscription = SubscribeCandles(CandleType);
firstSubscription.Bind(candle => ProcessCandle(candle, _firstCloses)).Start();
var secondSubscription = SubscribeCandles(CandleType, security: Asset2);
secondSubscription.Bind(candle => ProcessCandle(candle, _secondCloses)).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, firstSubscription);
DrawOwnTrades(area);
}
}
private void ResetState()
{
_firstCloses.Clear();
_secondCloses.Clear();
_residuals.Clear();
_side = 0;
_entryResidual = 0;
}
private void ProcessCandle(ICandleMessage candle, Dictionary<DateTime, decimal> closes)
{
if (candle.State != CandleStates.Finished)
return;
closes[candle.OpenTime] = candle.ClosePrice;
// The residual needs both instruments' candles of the same time, whichever arrives last.
if (!_firstCloses.TryGetValue(candle.OpenTime, out var first) || !_secondCloses.TryGetValue(candle.OpenTime, out var second))
return;
foreach (var stale in _firstCloses.Keys.Where(t => t <= candle.OpenTime).ToArray())
_firstCloses.Remove(stale);
foreach (var stale in _secondCloses.Keys.Where(t => t <= candle.OpenTime).ToArray())
_secondCloses.Remove(stale);
var residual = first - Beta * second;
_residuals.Enqueue(residual);
if (_residuals.Count > Period)
_residuals.Dequeue();
if (_residuals.Count < Period)
return;
var mean = _residuals.Average();
var deviation = (decimal)Math.Sqrt((double)_residuals.Average(r => (r - mean) * (r - mean)));
if (deviation == 0)
return;
var zScore = (residual - mean) / deviation;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var stopDistance = Math.Abs(_entryResidual) * StopLossPercent / 100;
if (zScore < -EntryThreshold && _side <= 0)
{
MoveLegs(1);
_entryResidual = residual;
}
else if (zScore > EntryThreshold && _side >= 0)
{
MoveLegs(-1);
_entryResidual = residual;
}
else if (_side > 0 && (Math.Abs(zScore) < ExitThreshold || (StopLossPercent > 0 && residual <= _entryResidual - stopDistance)))
{
MoveLegs(0);
}
else if (_side < 0 && (Math.Abs(zScore) < ExitThreshold || (StopLossPercent > 0 && residual >= _entryResidual + stopDistance)))
{
MoveLegs(0);
}
}
private void MoveLegs(int side)
{
_side = side;
// Long the pair holds Volume of the first instrument and is short Beta times as much of Asset2.
var firstChange = side * Volume - Position;
if (firstChange > 0)
BuyMarket(firstChange);
else if (firstChange < 0)
SellMarket(-firstChange);
var secondChange = -side * Volume * Beta - (GetPositionValue(Asset2, Portfolio) ?? 0m);
if (secondChange > 0)
BuyMarket(secondChange, Asset2);
else if (secondChange < 0)
SellMarket(-secondChange, Asset2);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
from System import TimeSpan, Decimal, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.BusinessEntities import Security
from StockSharp.Algo.Strategies import Strategy
class cointegration_pairs_strategy(Strategy):
"""
Cointegration pairs trading strategy.
The residual is the first instrument's close minus Beta times Asset2's on candles of the same time, and its z-score is measured against
the mean and standard deviation of the last Period residuals. A z-score below minus EntryThreshold buys the first instrument and sells
Beta times as much of Asset2, one above EntryThreshold does the opposite, reversing an opposite pair. Both legs close once the z-score
is back within ExitThreshold of zero, or once the residual moves StopLossPercent of its entry value against the pair.
"""
def __init__(self):
super(cointegration_pairs_strategy, self).__init__()
self._period = self.Param("Period", 20).SetGreaterThanZero().SetDisplay("Period", "Residuals the mean and standard deviation are measured over", "Parameters")
self._entry_threshold = self.Param("EntryThreshold", 2.0).SetGreaterThanZero().SetDisplay("Entry Threshold", "Z-score distance from zero that opens a pair", "Parameters")
self._exit_threshold = self.Param("ExitThreshold", 0.5).SetNotNegative().SetDisplay("Exit Threshold", "Z-score distance from zero within which the pair closes", "Parameters")
self._beta = self.Param("Beta", 1.0).SetGreaterThanZero().SetDisplay("Beta", "Hedge ratio of Asset2 to the first instrument", "Parameters")
self._asset2 = self.Param[Security]("Asset2", None).SetDisplay("Asset 2", "Second asset of the pair", "Parameters").SetRequired()
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetNotNegative().SetDisplay("Stop Loss %", "Adverse residual move in percent of the entry residual", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
@property
def asset2(self):
return self._asset2.Value
@asset2.setter
def asset2(self, value):
self._asset2.Value = value
def _reset_state(self):
self._first_closes = {}
self._second_closes = {}
self._residuals = []
# 1 while long the pair, -1 while short it, 0 while flat.
self._side = 0
self._entry_residual = Decimal(0)
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.asset2, self.candle_type)]
def OnReseted(self):
super(cointegration_pairs_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(cointegration_pairs_strategy, self).OnStarted2(time)
if self.asset2 is None:
raise Exception("Second asset is not specified.")
self._reset_state()
first_subscription = self.SubscribeCandles(self.candle_type)
first_subscription.Bind(self._process_first_candle).Start()
second_subscription = self.SubscribeCandles(self.candle_type, security=self.asset2)
second_subscription.Bind(self._process_second_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, first_subscription)
self.DrawOwnTrades(area)
def _process_first_candle(self, candle):
self._process_candle(candle, self._first_closes)
def _process_second_candle(self, candle):
self._process_candle(candle, self._second_closes)
def _process_candle(self, candle, closes):
if candle.State != CandleStates.Finished:
return
time = candle.OpenTime
closes[time] = candle.ClosePrice
# The residual needs both instruments' candles of the same time, whichever arrives last.
if time not in self._first_closes or time not in self._second_closes:
return
first = self._first_closes[time]
second = self._second_closes[time]
for stale in [t for t in self._first_closes if t <= time]:
del self._first_closes[stale]
for stale in [t for t in self._second_closes if t <= time]:
del self._second_closes[stale]
residual = first - Decimal(self._beta.Value) * second
period = self._period.Value
self._residuals.append(residual)
if len(self._residuals) > period:
self._residuals.pop(0)
if len(self._residuals) < period:
return
total = Decimal(0)
for value in self._residuals:
total += value
mean = total / Decimal(period)
squares = Decimal(0)
for value in self._residuals:
squares += (value - mean) * (value - mean)
deviation = Decimal(Math.Sqrt(Decimal.ToDouble(squares / Decimal(period))))
if deviation == 0:
return
z_score = (residual - mean) / deviation
if not self.IsFormedAndOnlineAndAllowTrading():
return
entry = Decimal(self._entry_threshold.Value)
exit_level = Decimal(self._exit_threshold.Value)
stop_percent = Decimal(self._stop_loss_percent.Value)
stop_distance = abs(self._entry_residual) * stop_percent / Decimal(100)
if z_score < -entry and self._side <= 0:
self._move_legs(1)
self._entry_residual = residual
elif z_score > entry and self._side >= 0:
self._move_legs(-1)
self._entry_residual = residual
elif self._side > 0 and (abs(z_score) < exit_level or (stop_percent > 0 and residual <= self._entry_residual - stop_distance)):
self._move_legs(0)
elif self._side < 0 and (abs(z_score) < exit_level or (stop_percent > 0 and residual >= self._entry_residual + stop_distance)):
self._move_legs(0)
def _move_legs(self, side):
self._side = side
# Long the pair holds Volume of the first instrument and is short Beta times as much of Asset2.
first_change = Decimal(side) * self.Volume - self.Position
if first_change > 0:
self.BuyMarket(first_change)
elif first_change < 0:
self.SellMarket(-first_change)
second_position = self.GetPositionValue(self.asset2, self.Portfolio)
if second_position is None:
second_position = Decimal(0)
second_change = Decimal(-side) * self.Volume * Decimal(self._beta.Value) - second_position
if second_change > 0:
self.BuyMarket(second_change, self.asset2)
elif second_change < 0:
self.SellMarket(-second_change, self.asset2)
def CreateClone(self):
return cointegration_pairs_strategy()