Pairs 戦略
このペアトレーディング戦略は、2つの相関した銘柄間の価格スプレッドを監視します。スプレッドをその履歴平均と標準偏差と比較することで、システムは最終的に回帰する一時的な乖離を活用しようとします。
テストでは年間平均リターン約88%を示しています。株式市場で最もパフォーマンスが高いです。
スプレッドが指定された乖離乗数以上に平均を下回ったときにロングスプレッドに入ります。これは最初の資産を買い、2番目を売ることを意味します。スプレッドが同じ量だけ平均を上回ったときにショートスプレッドは逆のことをします。スプレッドが平均レベルに戻るとポジションは閉じられます。
ペアトレーディングは、純粋な方向性よりも相対的価値の機会を好むマーケットニュートラルなトレーダーに魅力的です。両レグがヘッジされているため、ボラティリティは低くなる傾向がありますが、戦略はリスクを管理するためにスプレッドのストップロスを引き続き使用します。
詳細
- エントリー条件:
- ロング: Spread < Mean - Multiplier * StdDev
- ショート: Spread > Mean + Multiplier * StdDev
- ロング/ショート: 両方。
- エグジット条件:
- ロング: スプレッドが平均に回帰したときに終了
- ショート: スプレッドが平均に回帰したときに終了
- ストップ: はい、スプレッド価値に基づくパーセンテージストップ。
- デフォルト値:
LookbackPeriod= 20DeviationMultiplier= 2StopLossPercent= 2CandleType= TimeSpan.FromMinutes(5)SecondSecurity— 必須、既定値なし スプレッドは同時刻のローソク足における戦略のSecurityの終値からSecondSecurityの終値を引いた値で、MeanとStdDevは直近LookbackPeriod個のスプレッドから計算します。各レッグはVolumeで取引され、逆のシグナルで両レッグがドテンします。ストップはスプレッドがエントリー時の値のStopLossPercentだけ不利に動いたときに両レッグを閉じ、ローソク足の終値で確認されます。0で無効になります。
- フィルター:
- カテゴリ: 裁定取引
- 方向: 両方
- インジケーター: Spread statistics
- ストップ: はい
- 複雑さ: 中級
- 時間軸: イントラデイ
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: はい
- リスクレベル: 中
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>
/// Pairs trading strategy.
/// The spread is the first instrument's close minus the second's on candles of the same time. A spread more than DeviationMultiplier
/// standard deviations below the mean of the last LookbackPeriod spreads buys the first instrument and sells the second, one more than
/// that above it does the opposite, reversing an opposite pair. Both legs close once the spread returns to the mean, or once it moves
/// StopLossPercent of its entry value against the pair.
/// </summary>
public class PairsTradingStrategy : Strategy
{
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _deviationMultiplier;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<Security> _secondSecurity;
private readonly Dictionary<DateTime, decimal> _firstCloses = [];
private readonly Dictionary<DateTime, decimal> _secondCloses = [];
private readonly Queue<decimal> _spreads = [];
// 1 while long the spread, -1 while short it, 0 while flat.
private int _side;
private decimal _entrySpread;
/// <summary>
/// Number of spreads the mean and standard deviation are measured over.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Standard deviations between the mean and an entry level.
/// </summary>
public decimal DeviationMultiplier
{
get => _deviationMultiplier.Value;
set => _deviationMultiplier.Value = value;
}
/// <summary>
/// Adverse spread move, in percent of the entry spread, 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>
/// Second security in the pair.
/// </summary>
public Security SecondSecurity
{
get => _secondSecurity.Value;
set => _secondSecurity.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public PairsTradingStrategy()
{
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Lookback Period", "Spreads the mean and standard deviation are measured over", "Parameters");
_deviationMultiplier = Param(nameof(DeviationMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Deviation Multiplier", "Standard deviations between the mean and an entry level", "Parameters");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetNotNegative()
.SetDisplay("Stop-loss %", "Adverse spread move in percent of the entry spread", "Risk Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
_secondSecurity = Param<Security>(nameof(SecondSecurity))
.SetDisplay("Second Security", "Second security in the pair", "General")
.SetRequired();
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType), (SecondSecurity, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (SecondSecurity == null)
throw new InvalidOperationException("Second security is not specified.");
ResetState();
var firstSubscription = SubscribeCandles(CandleType);
firstSubscription.Bind(candle => ProcessCandle(candle, _firstCloses)).Start();
var secondSubscription = SubscribeCandles(CandleType, security: SecondSecurity);
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();
_spreads.Clear();
_side = 0;
_entrySpread = 0;
}
private void ProcessCandle(ICandleMessage candle, Dictionary<DateTime, decimal> closes)
{
if (candle.State != CandleStates.Finished)
return;
closes[candle.OpenTime] = candle.ClosePrice;
// The spread 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 spread = first - second;
_spreads.Enqueue(spread);
if (_spreads.Count > LookbackPeriod)
_spreads.Dequeue();
if (_spreads.Count < LookbackPeriod)
return;
var mean = _spreads.Average();
var deviation = (decimal)Math.Sqrt((double)_spreads.Average(s => (s - mean) * (s - mean)));
var upper = mean + DeviationMultiplier * deviation;
var lower = mean - DeviationMultiplier * deviation;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var stopDistance = Math.Abs(_entrySpread) * StopLossPercent / 100;
if (spread < lower && _side <= 0)
{
MoveLegs(1);
_entrySpread = spread;
}
else if (spread > upper && _side >= 0)
{
MoveLegs(-1);
_entrySpread = spread;
}
else if (_side > 0 && (spread >= mean || (StopLossPercent > 0 && spread <= _entrySpread - stopDistance)))
{
MoveLegs(0);
}
else if (_side < 0 && (spread <= mean || (StopLossPercent > 0 && spread >= _entrySpread + stopDistance)))
{
MoveLegs(0);
}
}
private void MoveLegs(int side)
{
_side = side;
// Long the spread holds the first instrument and is short the second, each by Volume.
var firstChange = side * Volume - Position;
if (firstChange > 0)
BuyMarket(firstChange);
else if (firstChange < 0)
SellMarket(-firstChange);
var secondChange = -side * Volume - (GetPositionValue(SecondSecurity, Portfolio) ?? 0m);
if (secondChange > 0)
BuyMarket(secondChange, SecondSecurity);
else if (secondChange < 0)
SellMarket(-secondChange, SecondSecurity);
}
}
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 pairs_trading_strategy(Strategy):
"""
Pairs trading strategy.
The spread is the first instrument's close minus the second's on candles of the same time. A spread more than DeviationMultiplier
standard deviations below the mean of the last LookbackPeriod spreads buys the first instrument and sells the second, one more than
that above it does the opposite, reversing an opposite pair. Both legs close once the spread returns to the mean, or once it moves
StopLossPercent of its entry value against the pair.
"""
def __init__(self):
super(pairs_trading_strategy, self).__init__()
self._lookback_period = self.Param("LookbackPeriod", 20).SetGreaterThanZero().SetDisplay("Lookback Period", "Spreads the mean and standard deviation are measured over", "Parameters")
self._deviation_multiplier = self.Param("DeviationMultiplier", 2.0).SetGreaterThanZero().SetDisplay("Deviation Multiplier", "Standard deviations between the mean and an entry level", "Parameters")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0).SetNotNegative().SetDisplay("Stop-loss %", "Adverse spread move in percent of the entry spread", "Risk Management")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._second_security = self.Param[Security]("SecondSecurity", None).SetDisplay("Second Security", "Second security in the pair", "General").SetRequired()
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
@property
def second_security(self):
return self._second_security.Value
@second_security.setter
def second_security(self, value):
self._second_security.Value = value
def _reset_state(self):
self._first_closes = {}
self._second_closes = {}
self._spreads = []
# 1 while long the spread, -1 while short it, 0 while flat.
self._side = 0
self._entry_spread = Decimal(0)
def GetWorkingSecurities(self):
return [(self.Security, self.candle_type), (self.second_security, self.candle_type)]
def OnReseted(self):
super(pairs_trading_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(pairs_trading_strategy, self).OnStarted2(time)
if self.second_security is None:
raise Exception("Second security 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.second_security)
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 spread 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]
spread = first - second
period = self._lookback_period.Value
self._spreads.append(spread)
if len(self._spreads) > period:
self._spreads.pop(0)
if len(self._spreads) < period:
return
total = Decimal(0)
for value in self._spreads:
total += value
mean = total / Decimal(period)
squares = Decimal(0)
for value in self._spreads:
squares += (value - mean) * (value - mean)
deviation = Decimal(Math.Sqrt(Decimal.ToDouble(squares / Decimal(period))))
multiplier = Decimal(self._deviation_multiplier.Value)
upper = mean + multiplier * deviation
lower = mean - multiplier * deviation
if not self.IsFormedAndOnlineAndAllowTrading():
return
stop_percent = Decimal(self._stop_loss_percent.Value)
stop_distance = abs(self._entry_spread) * stop_percent / Decimal(100)
if spread < lower and self._side <= 0:
self._move_legs(1)
self._entry_spread = spread
elif spread > upper and self._side >= 0:
self._move_legs(-1)
self._entry_spread = spread
elif self._side > 0 and (spread >= mean or (stop_percent > 0 and spread <= self._entry_spread - stop_distance)):
self._move_legs(0)
elif self._side < 0 and (spread <= mean or (stop_percent > 0 and spread >= self._entry_spread + stop_distance)):
self._move_legs(0)
def _move_legs(self, side):
self._side = side
# Long the spread holds the first instrument and is short the second, each by Volume.
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.second_security, self.Portfolio)
if second_position is None:
second_position = Decimal(0)
second_change = Decimal(-side) * self.Volume - second_position
if second_change > 0:
self.BuyMarket(second_change, self.second_security)
elif second_change < 0:
self.SellMarket(-second_change, self.second_security)
def CreateClone(self):
return pairs_trading_strategy()