統計的アービトラージ戦略
この統計的アービトラージアプローチは、移動平均周辺の相対的なポジションに基づいて関連する2つの有価証券ペアを取引します。各資産をその独自の平均と比較することで、時間をかけて収束するはずの短期的な乖離を利用することを目指します。
テストによると、平均年間リターンは約94%です。株式市場で最も高いパフォーマンスを発揮します。
最初の資産がその移動平均を下回り、2番目の資産がその独自の平均を上回って取引されているときにロングポジションが開始されます。最初の資産が平均を上回り、2番目が下回っているときにショートポジションが発生します。最初の資産がその移動平均に戻ると、スプレッドが正常化したことを示しポジションが決済されます。
この方法は、2つの銘柄間のエクスポージャーバランスに慣れた市場中立トレーダーに最適です。内蔵のストップロスは、スプレッドが反転する代わりにさらに拡大した場合のドローダウンを制限します。
詳細
- エントリー条件:
- ロング: Asset1 < MA1 && Asset2 > MA2
- ショート: Asset1 > MA1 && Asset2 < MA2
- ロング/ショート: 両方。
- エグジット条件:
- ロング: Asset1がMA1を上回って終値をつけた時に決済
- ショート: Asset1がMA1を下回って終値をつけた時に決済
- ストップ: あり、スプレッドに対するパーセンテージストップロス。
- デフォルト値:
LookbackPeriod= 20StopLossPercent= 2CandleType= TimeSpan.FromMinutes(15)SecondSecurity— 必須、既定値なし Asset1は戦略のSecurity、Asset2はSecondSecurityで、MA1とMA2は同時刻のローソク足における直近LookbackPeriod本の終値の単純移動平均です。ロングはAsset1を買いAsset2を売り、ショートはその逆で、各レッグはVolumeで取引され、逆のシグナルで両レッグがドテンします。ストップはスプレッド(Asset1 − Asset2)がエントリー時の値のStopLossPercentだけ不利に動いたときに両レッグを閉じ、ローソク足の終値で確認されます。0で無効になります。
- フィルター:
- カテゴリ: アービトラージ
- 方向: 両方
- インジケーター: Moving Averages
- ストップ: あり
- 複雑さ: 中級
- 時間軸: イントラデイ
- 季節性: いいえ
- ニューラルネットワーク: いいえ
- ダイバージェンス: はい
- リスクレベル: 中
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>
/// Statistical arbitrage strategy.
/// Each instrument is compared with the simple moving average of its last LookbackPeriod closes on candles of the same time. The first
/// instrument below its average while the second is above its own buys the first and sells the second, the mirror does the opposite,
/// reversing an opposite pair. Both legs close once the first instrument closes back across its average, or once the spread, the first
/// close minus the second, moves StopLossPercent of its entry value against the pair.
/// </summary>
public class StatisticalArbitrageStrategy : Strategy
{
private readonly StrategyParam<int> _lookbackPeriod;
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> _firstHistory = [];
private readonly Queue<decimal> _secondHistory = [];
// 1 while long the spread, -1 while short it, 0 while flat.
private int _side;
private decimal _entrySpread;
/// <summary>
/// Number of closes each moving average spans.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.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 StatisticalArbitrageStrategy()
{
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Lookback Period", "Closes each moving average spans", "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(15).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();
_firstHistory.Clear();
_secondHistory.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);
_firstHistory.Enqueue(first);
_secondHistory.Enqueue(second);
if (_firstHistory.Count > LookbackPeriod)
{
_firstHistory.Dequeue();
_secondHistory.Dequeue();
}
if (_firstHistory.Count < LookbackPeriod)
return;
var firstAverage = _firstHistory.Average();
var secondAverage = _secondHistory.Average();
var spread = first - second;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var stopDistance = Math.Abs(_entrySpread) * StopLossPercent / 100;
if (first < firstAverage && second > secondAverage && _side <= 0)
{
MoveLegs(1);
_entrySpread = spread;
}
else if (first > firstAverage && second < secondAverage && _side >= 0)
{
MoveLegs(-1);
_entrySpread = spread;
}
else if (_side > 0 && (first > firstAverage || (StopLossPercent > 0 && spread <= _entrySpread - stopDistance)))
{
MoveLegs(0);
}
else if (_side < 0 && (first < firstAverage || (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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.BusinessEntities import Security
from StockSharp.Algo.Strategies import Strategy
class statistical_arbitrage_strategy(Strategy):
"""
Statistical arbitrage strategy.
Each instrument is compared with the simple moving average of its last LookbackPeriod closes on candles of the same time. The first
instrument below its average while the second is above its own buys the first and sells the second, the mirror does the opposite,
reversing an opposite pair. Both legs close once the first instrument closes back across its average, or once the spread, the first
close minus the second, moves StopLossPercent of its entry value against the pair.
"""
def __init__(self):
super(statistical_arbitrage_strategy, self).__init__()
self._lookback_period = self.Param("LookbackPeriod", 20).SetGreaterThanZero().SetDisplay("Lookback Period", "Closes each moving average spans", "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(15))).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._first_history = []
self._second_history = []
# 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(statistical_arbitrage_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(statistical_arbitrage_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]
period = self._lookback_period.Value
self._first_history.append(first)
self._second_history.append(second)
if len(self._first_history) > period:
self._first_history.pop(0)
self._second_history.pop(0)
if len(self._first_history) < period:
return
first_total = Decimal(0)
second_total = Decimal(0)
for value in self._first_history:
first_total += value
for value in self._second_history:
second_total += value
first_average = first_total / Decimal(period)
second_average = second_total / Decimal(period)
spread = first - second
if not self.IsFormedAndOnlineAndAllowTrading():
return
stop_percent = Decimal(self._stop_loss_percent.Value)
stop_distance = abs(self._entry_spread) * stop_percent / Decimal(100)
if first < first_average and second > second_average and self._side <= 0:
self._move_legs(1)
self._entry_spread = spread
elif first > first_average and second < second_average and self._side >= 0:
self._move_legs(-1)
self._entry_spread = spread
elif self._side > 0 and (first > first_average or (stop_percent > 0 and spread <= self._entry_spread - stop_distance)):
self._move_legs(0)
elif self._side < 0 and (first < first_average 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 statistical_arbitrage_strategy()