共和分ペア戦略
この戦略は長期的な共和分関係を持つ2つの資産を取引します。最初の資産とベータ調整された2番目の資産の残差を計算することで、歴史的に均衡に戻る乖離を探します。
テストによると、平均年間リターンは約103%です。株式市場で最も高いパフォーマンスを発揮します。
残差Z-Scoreが-EntryThresholdを下回ると、最初の資産を買い2番目を売るロングポジションが開始されます。Z-Scoreが閾値を上回ると、最初の資産を売り2番目を買うショートポジションが発生します。スプレッドがゼロに向かって正常化するとポジションが決済されます。
共和分ペアトレードは、2つの銘柄を同時に管理することに慣れた統計的アービトラージャーに適しています。内蔵のストップロスは、関係が一時的に崩れた場合の極端な動きから保護します。
詳細
- エントリー条件:
- ロング: 残差 Z-Score < -EntryThreshold
- ショート: 残差 Z-Score > EntryThreshold
- ロング/ショート: 両方。
- エグジット条件:
- ロング: |Z-Score| < 0.5 の時に決済
- ショート: |Z-Score| < 0.5 の時に決済
- ストップ: あり、パーセンテージストップロス。
- デフォルト値:
Period= 20EntryThreshold= 2.0mBeta= 1.0mStopLossPercent= 2.0mCandleType= TimeSpan.FromMinutes(5)
- フィルター:
- カテゴリ: アービトラージ
- 方向: 両方
- インジケーター: 共和分
- ストップ: あり
- 複雑さ: 中級
- 時間軸: イントラデイ
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: はい
- リスクレベル: 中
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>
/// Cointegration pairs trading strategy.
/// Trades based on cointegration relationship between two assets.
/// </summary>
public class CointegrationPairsStrategy : Strategy
{
private readonly StrategyParam<int> _periodParam;
private readonly StrategyParam<decimal> _entryThresholdParam;
private readonly StrategyParam<decimal> _betaParam;
private readonly StrategyParam<Security> _asset2Param;
private readonly StrategyParam<decimal> _stopLossPercentParam;
private readonly StrategyParam<DataType> _candleTypeParam;
private decimal _residualMean;
private decimal _residualStdDev;
private decimal _residualSum;
private decimal _squaredResidualSum;
private readonly Queue<decimal> _residuals = [];
private decimal _asset1Price;
private decimal _asset2Price;
private const int _tradeCooldownTicks = 30;
private int _cooldownTicksLeft;
private Portfolio _asset2Portfolio;
/// <summary>
/// Period for calculation of residual mean and standard deviation.
/// </summary>
public int Period
{
get => _periodParam.Value;
set => _periodParam.Value = value;
}
/// <summary>
/// Entry threshold as a multiple of standard deviation.
/// </summary>
public decimal EntryThreshold
{
get => _entryThresholdParam.Value;
set => _entryThresholdParam.Value = value;
}
/// <summary>
/// Beta coefficient for calculation of residual.
/// </summary>
public decimal Beta
{
get => _betaParam.Value;
set => _betaParam.Value = value;
}
/// <summary>
/// Second asset for pair trading.
/// </summary>
public Security Asset2
{
get => _asset2Param.Value;
set => _asset2Param.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercentParam.Value;
set => _stopLossPercentParam.Value = value;
}
/// <summary>
/// Candle type for strategy.
/// </summary>
public DataType CandleType
{
get => _candleTypeParam.Value;
set => _candleTypeParam.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public CointegrationPairsStrategy()
{
_periodParam = Param(nameof(Period), 20)
.SetGreaterThanZero()
.SetDisplay("Period", "Period for residual calculations", "Parameters")
.SetOptimize(10, 50, 10);
_entryThresholdParam = Param(nameof(EntryThreshold), 2.0m)
.SetRange(0.1m, decimal.MaxValue)
.SetDisplay("Entry Threshold", "Entry threshold as multiple of standard deviation", "Parameters")
.SetOptimize(1.0m, 3.0m, 0.5m);
_betaParam = Param(nameof(Beta), 1.0m)
.SetRange(0.01m, decimal.MaxValue)
.SetDisplay("Beta", "Coefficient of cointegration", "Parameters")
.SetOptimize(0.5m, 2.0m, 0.1m);
_asset2Param = Param<Security>(nameof(Asset2))
.SetDisplay("Asset 2", "Second asset for pair trading", "Parameters");
_stopLossPercentParam = Param(nameof(StopLossPercent), 2.0m)
.SetRange(0.1m, decimal.MaxValue)
.SetDisplay("Stop Loss %", "Stop loss percentage", "Parameters")
.SetOptimize(1.0m, 5.0m, 1.0m);
_candleTypeParam = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle type for strategy", "Common");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return
[
(Security, CandleType),
(Asset2, CandleType)
];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_residualMean = 0;
_residualStdDev = 0;
_residualSum = 0;
_squaredResidualSum = 0;
_residuals.Clear();
_asset1Price = 0;
_asset2Price = 0;
_cooldownTicksLeft = 0;
// Portfolio is not guaranteed to be assigned by the time a strategy is reset,
// so the hedge portfolio is resolved on start instead.
_asset2Portfolio = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (Asset2 == null)
throw new InvalidOperationException("Second asset is not specified.");
// Use the same portfolio for second asset or find another portfolio
_asset2Portfolio = Portfolio;
// Subscribe to Asset1 candles
var asset1Subscription = SubscribeCandles(CandleType)
.Bind(ProcessAsset1Candle)
.Start();
// Subscribe to Asset2 candles
var asset2Subscription = SubscribeCandles(CandleType, security: Asset2)
.Bind(ProcessAsset2Candle)
.Start();
// Setup chart visualization if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, asset1Subscription);
DrawOwnTrades(area);
}
// Enable position protection with stop loss
StartProtection(
takeProfit: new Unit(0, UnitTypes.Absolute), // No take profit
stopLoss: new Unit(StopLossPercent, UnitTypes.Percent) // Stop loss percentage
);
}
private void ProcessAsset1Candle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_asset1Price = candle.ClosePrice;
ProcessPair();
}
private void ProcessAsset2Candle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_asset2Price = candle.ClosePrice;
ProcessPair();
}
private void ProcessPair()
{
if (_asset1Price == 0 || _asset2Price == 0)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldownTicksLeft > 0)
{
_cooldownTicksLeft--;
_asset1Price = 0;
_asset2Price = 0;
return;
}
// Calculate residual = Asset1Price - Beta * Asset2Price
var residual = _asset1Price - Beta * _asset2Price;
var hasTraded = false;
// Track residual statistics over period
_residuals.Enqueue(residual);
_residualSum += residual;
_squaredResidualSum += residual * residual;
if (_residuals.Count > Period)
{
var oldResidual = _residuals.Dequeue();
_residualSum -= oldResidual;
_squaredResidualSum -= oldResidual * oldResidual;
}
if (_residuals.Count == Period)
{
// Calculate mean and standard deviation
_residualMean = _residualSum / Period;
var variance = (_squaredResidualSum / Period) - (_residualMean * _residualMean);
_residualStdDev = variance <= 0 ? 0.0001m : (decimal)Math.Sqrt((double)variance);
// Calculate z-score of current residual
var zScore = (_residualStdDev == 0) ? 0 : (residual - _residualMean) / _residualStdDev;
// The strategy never intends to hold more than one leg, so every closing order is
// capped at Volume. Sizing an order from the raw position instead feeds exposure
// that has not been netted yet into the next order, which compounds without bound.
var closingVolume = Math.Min(Math.Abs(Position), Volume);
// The hedge leg fills independently of Asset1, so it is measured from its own
// position and bounded the same way: a closing order takes at most the open leg,
// an entry adds only the part of it that its own side nets out.
var hedgeVolume = Volume * Beta;
var asset2Position = _asset2Portfolio == null ? 0m : GetPositionValue(Asset2, _asset2Portfolio) ?? 0m;
var asset2ClosingVolume = Math.Min(Math.Abs(asset2Position), hedgeVolume);
// Check for trading signals
if (zScore < -EntryThreshold && Position <= 0)
{
// Long Asset1, Short Asset2
// First, close any existing short position on Asset1
BuyMarket(Volume + closingVolume);
hasTraded = true;
// Then, short Asset2 using the second portfolio
if (_asset2Portfolio != null)
{
RegisterAsset2Order(Sides.Sell, hedgeVolume + (asset2Position > 0 ? asset2ClosingVolume : 0m));
hasTraded = true;
}
}
else if (zScore > EntryThreshold && Position >= 0)
{
// Short Asset1, Long Asset2
// First, close any existing long position on Asset1
SellMarket(Volume + closingVolume);
hasTraded = true;
// Then, buy Asset2 using the second portfolio
if (_asset2Portfolio != null)
{
RegisterAsset2Order(Sides.Buy, hedgeVolume + (asset2Position < 0 ? asset2ClosingVolume : 0m));
hasTraded = true;
}
}
else if (Math.Abs(zScore) < 0.5m)
{
// Close positions when spread reverts to mean
if (Position != 0)
{
if (Position > 0)
SellMarket(closingVolume);
else
BuyMarket(closingVolume);
hasTraded = true;
}
// Close position on Asset2 from the real size and side of that leg
if (asset2ClosingVolume > 0)
{
RegisterAsset2Order(asset2Position > 0 ? Sides.Sell : Sides.Buy, asset2ClosingVolume);
hasTraded = true;
}
}
}
if (hasTraded)
_cooldownTicksLeft = _tradeCooldownTicks;
// Reset prices for next update
_asset1Price = 0;
_asset2Price = 0;
}
private void RegisterAsset2Order(Sides side, decimal volume)
{
var asset2Order = new Order
{
Side = side,
Security = Asset2,
Portfolio = _asset2Portfolio,
Volume = volume,
Type = OrderTypes.Market
};
RegisterOrder(asset2Order);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from System.Collections.Generic import Queue
from StockSharp.Messages import DataType, Unit, UnitTypes, CandleStates, Sides, OrderTypes
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Order, Security
from datatype_extensions import *
class cointegration_pairs_strategy(Strategy):
"""
Cointegration pairs trading strategy.
Trades based on cointegration relationship between two assets.
"""
# Number of pair updates skipped after any trade.
_tradeCooldownTicks = 30
def __init__(self):
super(cointegration_pairs_strategy, self).__init__()
# Period for calculation of residual mean and standard deviation.
self._period = self.Param("Period", 20) \
.SetGreaterThanZero() \
.SetDisplay("Period", "Period for residual calculations", "Parameters") \
.SetCanOptimize(True) \
.SetOptimize(10, 50, 10)
# Entry threshold as a multiple of standard deviation.
self._entryThreshold = self.Param("EntryThreshold", 2.0) \
.SetRange(0.1, 100.0) \
.SetDisplay("Entry Threshold", "Entry threshold as multiple of standard deviation", "Parameters") \
.SetCanOptimize(True) \
.SetOptimize(1.0, 3.0, 0.5)
# Beta coefficient for calculation of residual.
self._beta = self.Param("Beta", 1.0) \
.SetDisplay("Beta", "Coefficient of cointegration", "Parameters") \
.SetCanOptimize(True) \
.SetOptimize(0.5, 2.0, 0.1)
# Second asset for pair trading.
self._asset2 = self.Param[Security]("Asset2", None) \
.SetDisplay("Asset 2", "Second asset for pair trading", "Parameters")
# Stop loss percentage.
self._stopLossPercent = self.Param("StopLossPercent", 2.0) \
.SetRange(0.1, 100.0) \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Parameters") \
.SetCanOptimize(True) \
.SetOptimize(1.0, 5.0, 1.0)
# Candle type for strategy.
self._candleType = self.Param("CandleType", tf(5)) \
.SetDisplay("Candle Type", "Candle type for strategy", "Common")
# Internal state
self._residualMean = 0.0
self._residualStdDev = 0.0
self._residualSum = 0.0
self._squaredResidualSum = 0.0
self._residuals = Queue[float]()
self._asset1Price = 0.0
self._asset2Price = 0.0
self._cooldownTicksLeft = 0
self._asset2Portfolio = None
@property
def Period(self):
return self._period.Value
@Period.setter
def Period(self, value):
self._period.Value = value
@property
def EntryThreshold(self):
return self._entryThreshold.Value
@EntryThreshold.setter
def EntryThreshold(self, value):
self._entryThreshold.Value = value
@property
def Beta(self):
return self._beta.Value
@Beta.setter
def Beta(self, value):
self._beta.Value = value
@property
def Asset2(self):
return self._asset2.Value
@Asset2.setter
def Asset2(self, value):
self._asset2.Value = value
@property
def StopLossPercent(self):
return self._stopLossPercent.Value
@StopLossPercent.setter
def StopLossPercent(self, value):
self._stopLossPercent.Value = value
@property
def CandleType(self):
return self._candleType.Value
@CandleType.setter
def CandleType(self, value):
self._candleType.Value = value
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType), (self.Asset2, self.CandleType)]
def OnReseted(self):
super(cointegration_pairs_strategy, self).OnReseted()
self._residualMean = 0
self._residualStdDev = 0
self._residualSum = 0
self._squaredResidualSum = 0
self._residuals.Clear()
self._asset1Price = 0
self._asset2Price = 0
self._cooldownTicksLeft = 0
# Portfolio is not guaranteed to be assigned by the time a strategy is reset,
# so the hedge portfolio is resolved on start instead.
self._asset2Portfolio = None
def OnStarted2(self, time):
super(cointegration_pairs_strategy, self).OnStarted2(time)
if self.Asset2 is None:
raise Exception("Second asset is not specified.")
# Use the same portfolio for second asset or find another portfolio
self._asset2Portfolio = self.Portfolio
# Create subscriptions for both assets
asset1Subscription = self.SubscribeCandles(self.CandleType)
asset2Subscription = self.SubscribeCandles(self.CandleType, self.Asset2)
# Subscribe to Asset1 candles
asset1Subscription.Bind(self.ProcessAsset1Candle).Start()
# Subscribe to Asset2 candles
asset2Subscription.Bind(self.ProcessAsset2Candle).Start()
# Setup chart visualization if available
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, asset1Subscription)
self.DrawOwnTrades(area)
# Enable position protection with stop loss
self.StartProtection(
takeProfit=Unit(0, UnitTypes.Absolute),
stopLoss=Unit(self.StopLossPercent, UnitTypes.Percent)
)
def ProcessAsset1Candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._asset1Price = float(candle.ClosePrice)
self.ProcessPair()
def ProcessAsset2Candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._asset2Price = float(candle.ClosePrice)
self.ProcessPair()
def ProcessPair(self):
if self._asset1Price == 0 or self._asset2Price == 0:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._cooldownTicksLeft > 0:
self._cooldownTicksLeft -= 1
self._asset1Price = 0
self._asset2Price = 0
return
# Calculate residual = Asset1Price - Beta * Asset2Price
residual = self._asset1Price - self.Beta * self._asset2Price
hasTraded = False
# Track residual statistics over period
self._residuals.Enqueue(residual)
self._residualSum += residual
self._squaredResidualSum += residual * residual
if self._residuals.Count > self.Period:
oldResidual = self._residuals.Dequeue()
self._residualSum -= oldResidual
self._squaredResidualSum -= oldResidual * oldResidual
if self._residuals.Count == self.Period:
# Calculate mean and standard deviation
self._residualMean = self._residualSum / self.Period
variance = (self._squaredResidualSum / self.Period) - (self._residualMean * self._residualMean)
self._residualStdDev = 0.0001 if variance <= 0 else Math.Sqrt(float(variance))
# Calculate z-score of current residual
zScore = 0 if self._residualStdDev == 0 else (residual - self._residualMean) / self._residualStdDev
# The strategy never intends to hold more than one leg, so every closing order is
# capped at Volume. Sizing an order from the raw position instead feeds exposure
# that has not been netted yet into the next order, which compounds without bound.
closingVolume = Math.Min(Math.Abs(self.Position), self.Volume)
# The hedge leg fills independently of Asset1, so it is measured from its own
# position and bounded the same way: a closing order takes at most the open leg,
# an entry adds only the part of it that its own side nets out.
hedgeVolume = float(self.Volume) * float(self.Beta)
asset2Position = 0.0
if self._asset2Portfolio is not None:
asset2PositionValue = self.GetPositionValue(self.Asset2, self._asset2Portfolio)
if asset2PositionValue is not None:
asset2Position = float(asset2PositionValue)
asset2ClosingVolume = Math.Min(Math.Abs(asset2Position), hedgeVolume)
# Check for trading signals
if zScore < -self.EntryThreshold and self.Position <= 0:
# Long Asset1, Short Asset2
# First, close any existing short position on Asset1
self.BuyMarket(self.Volume + closingVolume)
hasTraded = True
# Then, short Asset2 using the second portfolio
if self._asset2Portfolio is not None:
self.RegisterAsset2Order(Sides.Sell, hedgeVolume + (asset2ClosingVolume if asset2Position > 0 else 0.0))
hasTraded = True
elif zScore > self.EntryThreshold and self.Position >= 0:
# Short Asset1, Long Asset2
# First, close any existing long position on Asset1
self.SellMarket(self.Volume + closingVolume)
hasTraded = True
# Then, buy Asset2 using the second portfolio
if self._asset2Portfolio is not None:
self.RegisterAsset2Order(Sides.Buy, hedgeVolume + (asset2ClosingVolume if asset2Position < 0 else 0.0))
hasTraded = True
elif Math.Abs(zScore) < 0.5:
# Close positions when spread reverts to mean
if self.Position != 0:
if self.Position > 0:
self.SellMarket(closingVolume)
else:
self.BuyMarket(closingVolume)
hasTraded = True
# Close position on Asset2 from the real size and side of that leg
if asset2ClosingVolume > 0:
self.RegisterAsset2Order(Sides.Sell if asset2Position > 0 else Sides.Buy, asset2ClosingVolume)
hasTraded = True
if hasTraded:
self._cooldownTicksLeft = self._tradeCooldownTicks
# Reset prices for next update
self._asset1Price = 0
self._asset2Price = 0
def RegisterAsset2Order(self, side, volume):
asset2Order = Order()
asset2Order.Side = side
asset2Order.Security = self.Asset2
asset2Order.Portfolio = self._asset2Portfolio
asset2Order.Volume = volume
asset2Order.Type = OrderTypes.Market
self.RegisterOrder(asset2Order)
def CreateClone(self):
"""
!! REQUIRED!! Creates a new instance of the strategy.
"""
return cointegration_pairs_strategy()