デルタニュートラル・アービトラージ戦略
このアービトラージ戦略は、2つの相関する資産間のスプレッドを取引しながら、組み合わせたポジションをデルタニュートラルに近い状態に保ちます。一方の資産でロングポジションを持ち、もう一方でショートポジションを持つことでバランスを取り、市場の方向性ではなくスプレッドの平均回帰から利益を得ようとします。
テストでは年平均リターンは約43%を示しています。株式市場で最もよいパフォーマンスを発揮します。
価格差のz-スコアが-EntryThresholdを下回ったときにロングスプレッドに入ります。最初の資産を買い、2番目の資産を同じサイズで売ります。z-スコアが正のしきい値を超えて上昇したときはショートスプレッドで逆を行います。スプレッドが移動平均に戻ったときにトレードを決済します。
デルタニュートラルトレードは、低ボラティリティのエクスポージャーを求める定量的トレーダーの間で人気があります。ヘッジされていても、資産間の極端な乖離に対して保護するためにストップロス保護が適用されます。
詳細
- エントリー条件:
- ロング: Spread Z-Score < -EntryThreshold
- ショート: Spread Z-Score > EntryThreshold
- ロング/ショート: 両方。
- エグジット条件:
- ロング: スプレッドが平均を上回って戻ったときに決済
- ショート: スプレッドが平均を下回って戻ったときに決済
- ストップ: あり、スプレッド値に対するパーセンテージストップロス。
- デフォルト値:
LookbackPeriod= 20EntryThreshold= 2StopLossPercent= 2CandleType= TimeSpan.FromMinutes(5)Asset2Security— 必須、既定値なしAsset2Portfolio— 空の場合は戦略のポートフォリオ スプレッドは同時刻のローソク足における戦略のSecurityの終値からAsset2Securityの終値を引いた値で、そのzスコアは直近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>
/// Delta neutral arbitrage strategy.
/// The spread is the first instrument's close minus Asset2Security's on candles of the same time, and its z-score is measured against the
/// mean and standard deviation of the last LookbackPeriod spreads. A z-score below minus EntryThreshold buys the first instrument and sells
/// Asset2Security in equal size, one above EntryThreshold does the opposite, reversing an opposite pair. Both legs close once the spread
/// crosses back over its mean, or once it moves StopLossPercent of its entry value against the pair.
/// </summary>
public class DeltaNeutralArbitrageStrategy : Strategy
{
private readonly StrategyParam<Security> _asset2Security;
private readonly StrategyParam<Portfolio> _asset2Portfolio;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _entryThreshold;
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> _spreads = [];
// 1 while long the spread, -1 while short it, 0 while flat.
private int _side;
private decimal _entrySpread;
/// <summary>
/// Second asset of the pair.
/// </summary>
public Security Asset2Security
{
get => _asset2Security.Value;
set => _asset2Security.Value = value;
}
/// <summary>
/// Portfolio for the second asset; the strategy's own portfolio when empty.
/// </summary>
public Portfolio Asset2Portfolio
{
get => _asset2Portfolio.Value;
set => _asset2Portfolio.Value = value;
}
/// <summary>
/// Number of spreads the mean and standard deviation are measured over.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Z-score distance from zero that opens a pair.
/// </summary>
public decimal EntryThreshold
{
get => _entryThreshold.Value;
set => _entryThreshold.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>
/// Constructor.
/// </summary>
public DeltaNeutralArbitrageStrategy()
{
_asset2Security = Param<Security>(nameof(Asset2Security))
.SetDisplay("Asset 2", "Second asset of the pair", "Securities")
.SetRequired();
_asset2Portfolio = Param<Portfolio>(nameof(Asset2Portfolio))
.SetDisplay("Portfolio 2", "Portfolio for the second asset", "Portfolios");
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Lookback Period", "Spreads 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");
_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");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType), (Asset2Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (Asset2Security == null)
throw new InvalidOperationException("Asset2Security is not specified.");
ResetState();
var firstSubscription = SubscribeCandles(CandleType);
firstSubscription.Bind(candle => ProcessCandle(candle, _firstCloses)).Start();
var secondSubscription = SubscribeCandles(CandleType, security: Asset2Security);
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)));
if (deviation == 0)
return;
var zScore = (spread - mean) / deviation;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var stopDistance = Math.Abs(_entrySpread) * StopLossPercent / 100;
if (zScore < -EntryThreshold && _side <= 0)
{
MoveLegs(1);
_entrySpread = spread;
}
else if (zScore > EntryThreshold && _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 portfolio = Asset2Portfolio ?? Portfolio;
var secondChange = -side * Volume - (GetPositionValue(Asset2Security, portfolio) ?? 0m);
if (secondChange != 0)
{
RegisterOrder(new Order
{
Security = Asset2Security,
Portfolio = portfolio,
Side = secondChange > 0 ? Sides.Buy : Sides.Sell,
Volume = Math.Abs(secondChange),
Type = OrderTypes.Market,
});
}
}
}
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, Sides, OrderTypes
from StockSharp.BusinessEntities import Security, Portfolio, Order
from StockSharp.Algo.Strategies import Strategy
class delta_neutral_arbitrage_strategy(Strategy):
"""
Delta neutral arbitrage strategy.
The spread is the first instrument's close minus Asset2Security's on candles of the same time, and its z-score is measured against the
mean and standard deviation of the last LookbackPeriod spreads. A z-score below minus EntryThreshold buys the first instrument and sells
Asset2Security in equal size, one above EntryThreshold does the opposite, reversing an opposite pair. Both legs close once the spread
crosses back over its mean, or once it moves StopLossPercent of its entry value against the pair.
"""
def __init__(self):
super(delta_neutral_arbitrage_strategy, self).__init__()
self._asset2_security = self.Param[Security]("Asset2Security", None).SetDisplay("Asset 2", "Second asset of the pair", "Securities").SetRequired()
self._asset2_portfolio = self.Param[Portfolio]("Asset2Portfolio", None).SetDisplay("Portfolio 2", "Portfolio for the second asset", "Portfolios")
self._lookback_period = self.Param("LookbackPeriod", 20).SetGreaterThanZero().SetDisplay("Lookback Period", "Spreads 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._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._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
@property
def asset2_security(self):
return self._asset2_security.Value
@asset2_security.setter
def asset2_security(self, value):
self._asset2_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.asset2_security, self.candle_type)]
def OnReseted(self):
super(delta_neutral_arbitrage_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(delta_neutral_arbitrage_strategy, self).OnStarted2(time)
if self.asset2_security is None:
raise Exception("Asset2Security 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_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))))
if deviation == 0:
return
z_score = (spread - mean) / deviation
threshold = Decimal(self._entry_threshold.Value)
if not self.IsFormedAndOnlineAndAllowTrading():
return
stop_percent = Decimal(self._stop_loss_percent.Value)
stop_distance = abs(self._entry_spread) * stop_percent / Decimal(100)
if z_score < -threshold and self._side <= 0:
self._move_legs(1)
self._entry_spread = spread
elif z_score > threshold 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)
portfolio = self._asset2_portfolio.Value
if portfolio is None:
portfolio = self.Portfolio
second_position = self.GetPositionValue(self.asset2_security, portfolio)
if second_position is None:
second_position = Decimal(0)
second_change = Decimal(-side) * self.Volume - second_position
if second_change != 0:
order = Order()
order.Security = self.asset2_security
order.Portfolio = portfolio
order.Side = Sides.Buy if second_change > 0 else Sides.Sell
order.Volume = abs(second_change)
order.Type = OrderTypes.Market
self.RegisterOrder(order)
def CreateClone(self):
return delta_neutral_arbitrage_strategy()