Стратегия дельта‑нейтрального арбитража
Эта арбитражная стратегия торгует спред между двумя коррелированными активами, поддерживая общую позицию близкой к дельта‑нейтральной. Балансируя длинную позицию по одному активу короткой по другому, она пытается заработать на возврате спреда к среднему, а не на движении рынка.
Тестирование показывает среднегодичную доходность около 43%. Стратегию лучше запускать на фондовом рынке.
Длинный спред открывается, когда z‑значение разницы цен падает ниже -EntryThreshold. Первый актив покупается, а второй продаётся в равном объёме. Короткий спред делает наоборот, когда z‑значение превышает положительный порог. Сделка закрывается, когда спред возвращается к скользящей средней.
Дельта‑нейтральная торговля популярна среди количественных трейдеров, стремящихся к низкой волатильности. Несмотря на хеджирование, применяется стоп‑лосс для защиты от сильного расхождения цен.
Подробности
- Условия входа:
- Long: Z‑скор спреда < -EntryThreshold
- Short: Z‑скор спреда > EntryThreshold
- Long/Short: обе стороны.
- Условия выхода:
- Long: выход, когда спред вновь пересекает среднее снизу вверх
- Short: выход, когда спред пересекает среднее сверху вниз
- Стопы: да, процентный стоп‑лосс по значению спреда.
- Параметры по умолчанию:
LookbackPeriod= 20EntryThreshold= 2mStopLossPercent= 2mCandleType= 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>
/// Strategy that creates delta neutral arbitrage positions between two correlated assets.
/// Goes long one asset and short another when spread deviates from the mean.
/// </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 SimpleMovingAverage _spreadSma;
private StandardDeviation _spreadStdDev;
private decimal _currentSpread;
private decimal _lastAsset1Price;
private decimal _lastAsset2Price;
private decimal _asset1Volume;
private decimal _asset2Volume;
/// <summary>
/// Secondary security for pair trading.
/// </summary>
public Security Asset2Security
{
get => _asset2Security.Value;
set => _asset2Security.Value = value;
}
/// <summary>
/// Portfolio for trading second asset.
/// </summary>
public Portfolio Asset2Portfolio
{
get => _asset2Portfolio.Value;
set => _asset2Portfolio.Value = value;
}
/// <summary>
/// Period for spread statistics calculation.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Threshold for entries, in standard deviations.
/// </summary>
public decimal EntryThreshold
{
get => _entryThreshold.Value;
set => _entryThreshold.Value = value;
}
/// <summary>
/// Stop-loss percentage.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Type of candles to use.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public DeltaNeutralArbitrageStrategy()
{
_asset2Security = Param<Security>(nameof(Asset2Security))
.SetDisplay("Asset 2", "Secondary asset for arbitrage", "Securities");
_asset2Portfolio = Param<Portfolio>(nameof(Asset2Portfolio))
.SetDisplay("Portfolio 2", "Portfolio for trading Asset 2", "Portfolios");
_lookbackPeriod = Param(nameof(LookbackPeriod), 20)
.SetDisplay("Lookback period", "Period for spread statistics calculation", "Strategy parameters")
.SetOptimize(10, 50, 5);
_entryThreshold = Param(nameof(EntryThreshold), 2m)
.SetDisplay("Entry threshold", "Entry threshold in standard deviations", "Strategy parameters")
.SetOptimize(1.5m, 3m, 0.5m);
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetDisplay("Stop-loss %", "Stop-loss as percentage from entry spread", "Risk management")
.SetOptimize(1m, 3m, 0.5m);
_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();
_currentSpread = default;
_lastAsset1Price = default;
_lastAsset2Price = default;
_asset1Volume = default;
_asset2Volume = default;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (Asset2Security == null)
throw new InvalidOperationException("Asset2Security is not specified.");
Asset2Security = this.LookupById(Asset2Security.Id) ?? Asset2Security;
if (Asset2Portfolio == null)
Asset2Portfolio = Portfolio;
// Initialize indicators for spread statistics
_spreadSma = new SMA { Length = LookbackPeriod };
_spreadStdDev = new StandardDeviation { Length = LookbackPeriod };
// Create subscriptions to both securities
var asset1Subscription = SubscribeCandles(CandleType, security: Security);
var asset2Subscription = SubscribeCandles(CandleType, security: Asset2Security);
// Subscribe to candle processing for Asset 1
asset1Subscription
.Bind(ProcessAsset1Candle)
.Start();
// Subscribe to candle processing for Asset 2
asset2Subscription
.Bind(ProcessAsset2Candle)
.Start();
// Calculate volumes to maintain beta neutrality (simplified approach)
// In a real implementation, beta would be calculated dynamically
_asset1Volume = Volume;
_asset2Volume = Volume; // Simplified, in reality would be Volume * Beta ratio
// Setup chart if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, asset1Subscription);
DrawOwnTrades(area);
}
}
private void ProcessAsset1Candle(ICandleMessage candle)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Update asset1 price
_lastAsset1Price = candle.ClosePrice;
// Process spread if we have both prices
ProcessSpreadIfReady(candle);
}
private void ProcessAsset2Candle(ICandleMessage candle)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Update asset2 price
_lastAsset2Price = candle.ClosePrice;
// Process spread if we have both prices
ProcessSpreadIfReady(candle);
}
private void ProcessSpreadIfReady(ICandleMessage candle)
{
// Ensure we have both prices
if (_lastAsset1Price == 0 || _lastAsset2Price == 0)
return;
// Calculate the spread
_currentSpread = _lastAsset1Price - _lastAsset2Price;
// Process the spread with our indicators
var spreadValue = _spreadSma.Process(new DecimalIndicatorValue(_spreadSma, _currentSpread, candle.ServerTime) { IsFinal = true });
var stdDevValue = _spreadStdDev.Process(new DecimalIndicatorValue(_spreadStdDev, _currentSpread, candle.ServerTime) { IsFinal = true });
// Check if indicators are formed
if (!_spreadSma.IsFormed || !_spreadStdDev.IsFormed)
return;
decimal spreadSma = spreadValue.ToDecimal();
decimal spreadStdDev = stdDevValue.ToDecimal();
// Calculate z-score
decimal zScore = (spreadStdDev == 0) ? 0 : (_currentSpread - spreadSma) / spreadStdDev;
LogInfo($"Current spread: {_currentSpread}, SMA: {spreadSma}, StdDev: {spreadStdDev}, Z-score: {zScore}");
// Trading logic
if (Math.Abs(Position) == 0) // No position, check for entry
{
// Spread is too low (Asset1 cheap relative to Asset2)
if (zScore < -EntryThreshold)
{
EnterLongSpread();
LogInfo($"Long spread entry: Asset1 price={_lastAsset1Price}, Asset2 price={_lastAsset2Price}, Spread={_currentSpread}");
}
// Spread is too high (Asset1 expensive relative to Asset2)
else if (zScore > EntryThreshold)
{
EnterShortSpread();
LogInfo($"Short spread entry: Asset1 price={_lastAsset1Price}, Asset2 price={_lastAsset2Price}, Spread={_currentSpread}");
}
}
else // Have position, check for exit
{
if ((Position > 0 && _currentSpread >= spreadSma) || // Long spread and spread has reverted to mean
(Position < 0 && _currentSpread <= spreadSma)) // Short spread and spread has reverted to mean
{
ClosePositions();
LogInfo($"Spread exit: Asset1 price={_lastAsset1Price}, Asset2 price={_lastAsset2Price}, Spread={_currentSpread}");
}
}
}
private void EnterLongSpread()
{
// Buy Asset1
var asset1Order = CreateOrder(Sides.Buy, _lastAsset1Price, _asset1Volume);
asset1Order.Security = Security;
asset1Order.Portfolio = Portfolio;
RegisterOrder(asset1Order);
// Sell Asset2
var asset2Order = CreateOrder(Sides.Sell, _lastAsset2Price, _asset2Volume);
asset2Order.Security = Asset2Security;
asset2Order.Portfolio = Asset2Portfolio;
RegisterOrder(asset2Order);
}
private void EnterShortSpread()
{
// Sell Asset1
var asset1Order = CreateOrder(Sides.Sell, _lastAsset1Price, _asset1Volume);
asset1Order.Security = Security;
asset1Order.Portfolio = Portfolio;
RegisterOrder(asset1Order);
// Buy Asset2
var asset2Order = CreateOrder(Sides.Buy, _lastAsset2Price, _asset2Volume);
asset2Order.Security = Asset2Security;
asset2Order.Portfolio = Asset2Portfolio;
RegisterOrder(asset2Order);
}
private void ClosePositions()
{
// Close position in Asset1
if (Position > 0)
SellMarket(Math.Abs(Position));
else if (Position < 0)
BuyMarket(Math.Abs(Position));
// Note: In a real implementation, you would also close the position
// in Asset2 by checking its position via separate portfolio tracking
// For simplicity, this example assumes symmetrical positions
// Close position in Asset2 (simplified example)
var asset2Order = CreateOrder(
Position > 0 ? Sides.Buy : Sides.Sell,
_lastAsset2Price,
_asset2Volume);
asset2Order.Security = Asset2Security;
asset2Order.Portfolio = Asset2Portfolio;
RegisterOrder(asset2Order);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.BusinessEntities import Security, Portfolio
from StockSharp.Algo.Indicators import SimpleMovingAverage, StandardDeviation
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class delta_neutral_arbitrage_strategy(Strategy):
"""
Strategy that creates delta neutral arbitrage positions between two correlated assets.
Goes long one asset and short another when spread deviates from the mean.
"""
def __init__(self):
super(delta_neutral_arbitrage_strategy, self).__init__()
# Secondary security for pair trading.
self._asset2_security = self.Param[Security]("Asset2Security") \
.SetDisplay("Asset 2", "Secondary asset for arbitrage", "Securities")
# Portfolio for trading second asset.
self._asset2_portfolio = self.Param[Portfolio]("Asset2Portfolio") \
.SetDisplay("Portfolio 2", "Portfolio for trading Asset 2", "Portfolios")
# Period for spread statistics calculation.
self._lookback_period = self.Param("LookbackPeriod", 20) \
.SetDisplay("Lookback period", "Period for spread statistics calculation", "Strategy parameters") \
.SetCanOptimize(True) \
.SetOptimize(10, 50, 5)
# Threshold for entries, in standard deviations.
self._entry_threshold = self.Param("EntryThreshold", 2.0) \
.SetDisplay("Entry threshold", "Entry threshold in standard deviations", "Strategy parameters") \
.SetCanOptimize(True) \
.SetOptimize(1.5, 3.0, 0.5)
# Stop-loss percentage.
self._stop_loss_percent = self.Param("StopLossPercent", 2.0) \
.SetDisplay("Stop-loss %", "Stop-loss as percentage from entry spread", "Risk management") \
.SetCanOptimize(True) \
.SetOptimize(1.0, 3.0, 0.5)
# Type of candles to use.
self._candle_type = self.Param("CandleType", tf(5)) \
.SetDisplay("Candle type", "Type of candles to use", "General")
self._spread_sma = None
self._spread_std_dev = None
self._current_spread = 0.0
self._last_asset1_price = 0.0
self._last_asset2_price = 0.0
self._asset1_volume = 0.0
self._asset2_volume = 0.0
@property
def asset2_security(self):
"""Secondary security for pair trading."""
return self._asset2_security.Value
@asset2_security.setter
def asset2_security(self, value):
self._asset2_security.Value = value
@property
def asset2_portfolio(self):
"""Portfolio for trading second asset."""
return self._asset2_portfolio.Value
@asset2_portfolio.setter
def asset2_portfolio(self, value):
self._asset2_portfolio.Value = value
@property
def lookback_period(self):
"""Period for spread statistics calculation."""
return self._lookback_period.Value
@lookback_period.setter
def lookback_period(self, value):
self._lookback_period.Value = value
@property
def entry_threshold(self):
"""Threshold for entries, in standard deviations."""
return self._entry_threshold.Value
@entry_threshold.setter
def entry_threshold(self, value):
self._entry_threshold.Value = value
@property
def stop_loss_percent(self):
"""Stop-loss percentage."""
return self._stop_loss_percent.Value
@stop_loss_percent.setter
def stop_loss_percent(self, value):
self._stop_loss_percent.Value = value
@property
def candle_type(self):
"""Type of candles to use."""
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
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._current_spread = 0.0
self._last_asset1_price = 0.0
self._last_asset2_price = 0.0
self._asset1_volume = 0.0
self._asset2_volume = 0.0
def OnStarted2(self, time):
super(delta_neutral_arbitrage_strategy, self).OnStarted2(time)
if self.asset2_security is None:
raise Exception("Asset2Security is not specified.")
if self.asset2_portfolio is None:
raise Exception("Asset2Portfolio is not specified.")
# Initialize indicators for spread statistics
self._spread_sma = SimpleMovingAverage()
self._spread_sma.Length = self.lookback_period
self._spread_std_dev = StandardDeviation()
self._spread_std_dev.Length = self.lookback_period
# Create subscriptions to both securities
asset1_subscription = self.SubscribeCandles(self.candle_type)
asset2_subscription = self.SubscribeCandles(self.candle_type, security=self.asset2_security)
# Subscribe to candle processing for Asset 1
asset1_subscription.Bind(self.ProcessAsset1Candle).Start()
# Subscribe to candle processing for Asset 2
asset2_subscription.Bind(self.ProcessAsset2Candle).Start()
# Calculate volumes to maintain beta neutrality (simplified approach)
# In a real implementation, beta would be calculated dynamically
self._asset1_volume = self.Volume
self._asset2_volume = self.Volume # Simplified, in reality would be Volume * Beta ratio
# Setup chart if available
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, asset1_subscription)
self.DrawOwnTrades(area)
def ProcessAsset1Candle(self, candle):
# Skip unfinished candles
if candle.State != CandleStates.Finished:
return
# Update asset1 price
self._last_asset1_price = float(candle.ClosePrice)
# Process spread if we have both prices
self.ProcessSpreadIfReady(candle)
def ProcessAsset2Candle(self, candle):
# Skip unfinished candles
if candle.State != CandleStates.Finished:
return
# Update asset2 price
self._last_asset2_price = float(candle.ClosePrice)
# Process spread if we have both prices
self.ProcessSpreadIfReady(candle)
def ProcessSpreadIfReady(self, candle):
# Ensure we have both prices
if self._last_asset1_price == 0 or self._last_asset2_price == 0:
return
# Check if strategy is ready to trade
# Calculate the spread
self._current_spread = self._last_asset1_price - self._last_asset2_price
# Process the spread with our indicators
spread_value = process_float(self._spread_sma, self._current_spread, candle.ServerTime, candle.State == CandleStates.Finished)
std_dev_value = process_float(self._spread_std_dev, self._current_spread, candle.ServerTime, candle.State == CandleStates.Finished)
# Check if indicators are formed
if not self._spread_sma.IsFormed or not self._spread_std_dev.IsFormed:
return
spread_sma = float(spread_value)
spread_std_dev = float(std_dev_value)
# Calculate z-score
z_score = 0 if spread_std_dev == 0 else (self._current_spread - spread_sma) / spread_std_dev
self.LogInfo("Current spread: {0}, SMA: {1}, StdDev: {2}, Z-score: {3}".format(
self._current_spread, spread_sma, spread_std_dev, z_score))
# Trading logic
if Math.Abs(self.Position) == 0: # No position, check for entry
# Spread is too low (Asset1 cheap relative to Asset2)
if z_score < -self.entry_threshold:
self.EnterLongSpread()
self.LogInfo(
"Long spread entry: Asset1 price={0}, Asset2 price={1}, Spread={2}".format(
self._last_asset1_price, self._last_asset2_price, self._current_spread))
# Spread is too high (Asset1 expensive relative to Asset2)
elif z_score > self.entry_threshold:
self.EnterShortSpread()
self.LogInfo(
"Short spread entry: Asset1 price={0}, Asset2 price={1}, Spread={2}".format(
self._last_asset1_price, self._last_asset2_price, self._current_spread))
else: # Have position, check for exit
if (self.Position > 0 and self._current_spread >= spread_sma) or \
(self.Position < 0 and self._current_spread <= spread_sma): # Long spread and spread has reverted to mean / Short spread and spread has reverted to mean
self.ClosePositions()
self.LogInfo(
"Spread exit: Asset1 price={0}, Asset2 price={1}, Spread={2}".format(
self._last_asset1_price, self._last_asset2_price, self._current_spread))
def EnterLongSpread(self):
# Buy Asset1
asset1_order = self.CreateOrder(Sides.Buy, self._last_asset1_price, self._asset1_volume)
asset1_order.Security = self.Security
asset1_order.Portfolio = self.Portfolio
self.RegisterOrder(asset1_order)
# Sell Asset2
asset2_order = self.CreateOrder(Sides.Sell, self._last_asset2_price, self._asset2_volume)
asset2_order.Security = self.asset2_security
asset2_order.Portfolio = self.asset2_portfolio
self.RegisterOrder(asset2_order)
def EnterShortSpread(self):
# Sell Asset1
asset1_order = self.CreateOrder(Sides.Sell, self._last_asset1_price, self._asset1_volume)
asset1_order.Security = self.Security
asset1_order.Portfolio = self.Portfolio
self.RegisterOrder(asset1_order)
# Buy Asset2
asset2_order = self.CreateOrder(Sides.Buy, self._last_asset2_price, self._asset2_volume)
asset2_order.Security = self.asset2_security
asset2_order.Portfolio = self.asset2_portfolio
self.RegisterOrder(asset2_order)
def ClosePositions(self):
# Close position in Asset1
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
elif self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
# Note: In a real implementation, you would also close the position
# in Asset2 by checking its position via separate portfolio tracking
# For simplicity, this example assumes symmetrical positions
# Close position in Asset2 (simplified example)
asset2_order = self.CreateOrder(
Sides.Buy if self.Position > 0 else Sides.Sell,
self._last_asset2_price,
self._asset2_volume)
asset2_order.Security = self.asset2_security
asset2_order.Portfolio = self.asset2_portfolio
self.RegisterOrder(asset2_order)
def CreateClone(self):
"""!! REQUIRED!! Creates a new instance of the strategy."""
return delta_neutral_arbitrage_strategy()