Statistische Arbitrage-Strategie
Dieser statistische Arbitrage-Ansatz handelt ein Paar verwandter Wertpapiere basierend auf ihrer relativen Positionierung um gleitende Durchschnitte. Durch den Vergleich jedes Vermögenswerts mit seinem eigenen Durchschnitt versucht die Strategie, kurzfristige Disbalancen auszunutzen, die im Laufe der Zeit konvergieren sollten.
Tests zeigen eine durchschnittliche jährliche Rendite von etwa 94%. Sie funktioniert am besten am Aktienmarkt.
Eine Long-Position wird eingeleitet, wenn der erste Vermögenswert unter seinem gleitenden Durchschnitt handelt, während der zweite Vermögenswert über seinem eigenen Durchschnitt handelt. Eine Short-Position tritt auf, wenn der erste Vermögenswert über seinem Durchschnitt und der zweite darunter liegt. Positionen werden geschlossen, wenn der erste Vermögenswert wieder durch seinen gleitenden Durchschnitt kreuzt, was signalisiert, dass sich die Spread normalisiert hat.
Die Methode ist ideal für marktneutrale Trader, die mit der Balance von Exponierung über zwei Instrumente vertraut sind. Der eingebaute Stop-Loss begrenzt Drawdowns, wenn sich die Spread weiter ausweitet, anstatt sich umzukehren.
Details
- Einstiegskriterien:
- Long: Asset1 < MA1 && Asset2 > MA2
- Short: Asset1 > MA1 && Asset2 < MA2
- Long/Short: Beide Seiten.
- Ausstiegskriterien:
- Long: Ausstieg, wenn Asset1 über MA1 schließt
- Short: Ausstieg, wenn Asset1 unter MA1 schließt
- Stops: Ja, prozentualer Stop-Loss auf Spread.
- Standardwerte:
LookbackPeriod= 20StopLossPercent= 2mCandleType= TimeSpan.FromMinutes(15)
- Filter:
- Kategorie: Arbitrage
- Richtung: Beide
- Indikatoren: Moving Averages
- Stops: Ja
- Komplexität: Mittel
- Zeitrahmen: Intraday
- Saisonalität: Nein
- Neuronale Netze: Nein
- Divergenz: Ja
- Risikolevel: Mittel
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>
/// Statistical Arbitrage strategy that trades pairs of securities based on their relative mean reversion.
/// Enters when one asset is below its mean while the other is above its mean.
/// </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 SimpleMovingAverage _firstMA;
private SimpleMovingAverage _secondMA;
private decimal _lastFirstPrice;
private decimal _lastSecondPrice;
private decimal _entrySpread;
private decimal _secondMAValue;
/// <summary>
/// Period for calculating moving averages.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Stop-loss percentage parameter.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Candle type parameter.
/// </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", "Period for calculating moving averages", "Parameters")
.SetOptimize(10, 30, 5);
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Stop-loss %", "Stop-loss as percentage of entry price", "Risk Management")
.SetOptimize(1m, 3m, 0.5m);
_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();
_firstMA = null;
_secondMA = null;
_lastFirstPrice = 0;
_lastSecondPrice = 0;
_entrySpread = 0;
_secondMAValue = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (SecondSecurity == null)
throw new InvalidOperationException("Second security is not specified.");
// Initialize indicators
_firstMA = new() { Length = LookbackPeriod };
_secondMA = new() { Length = LookbackPeriod };
// Create subscriptions for both securities
var firstSecuritySubscription = SubscribeCandles(CandleType);
var secondSecuritySubscription = SubscribeCandles(CandleType, security: SecondSecurity);
// Bind to first security candles
firstSecuritySubscription
.Bind(_firstMA, ProcessFirstSecurityCandle)
.Start();
// Bind to second security candles
secondSecuritySubscription
.Bind(ProcessSecondSecurityCandle)
.Start();
// Enable position protection with stop-loss
StartProtection(
takeProfit: new Unit(0, UnitTypes.Absolute), // No take-profit
stopLoss: new Unit(StopLossPercent, UnitTypes.Percent) // Stop-loss as percentage
);
// Setup chart if available
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, firstSecuritySubscription);
DrawIndicator(area, _firstMA);
DrawOwnTrades(area);
}
}
private void ProcessFirstSecurityCandle(ICandleMessage candle, decimal firstMAValue)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Store current price
_lastFirstPrice = candle.ClosePrice;
// Skip if we don't have both prices or if indicators aren't formed
if (_lastSecondPrice == 0 || !_firstMA.IsFormed || !_secondMA.IsFormed)
return;
// Get last second MA value stored earlier
decimal secondMAValue = _secondMAValue;
// Trading logic
bool isFirstBelowMA = _lastFirstPrice < firstMAValue;
bool isSecondAboveMA = _lastSecondPrice > secondMAValue;
bool isFirstAboveMA = _lastFirstPrice > firstMAValue;
bool isSecondBelowMA = _lastSecondPrice < secondMAValue;
decimal currentSpread = _lastFirstPrice - _lastSecondPrice;
// Long signal: First asset below MA, Second asset above MA
if (isFirstBelowMA && isSecondAboveMA)
{
// If we're not already in a long position
if (Position <= 0)
{
BuyMarket(Volume + Math.Abs(Position));
_entrySpread = currentSpread;
LogInfo($"Long Signal: {Security.Code}({_lastFirstPrice:F4}) < MA({firstMAValue:F4}) && " +
$"{SecondSecurity.Code}({_lastSecondPrice:F4}) > MA({secondMAValue:F4})");
// Note: In a real implementation, you would also place a sell order
// for the second security here, using a different strategy instance or connector
}
}
// Short signal: First asset above MA, Second asset below MA
else if (isFirstAboveMA && isSecondBelowMA)
{
// If we're not already in a short position
if (Position >= 0)
{
SellMarket(Volume + Math.Abs(Position));
_entrySpread = currentSpread;
LogInfo($"Short Signal: {Security.Code}({_lastFirstPrice:F4}) > MA({firstMAValue:F4}) && " +
$"{SecondSecurity.Code}({_lastSecondPrice:F4}) < MA({secondMAValue:F4})");
// Note: In a real implementation, you would also place a buy order
// for the second security here, using a different strategy instance or connector
}
}
// Exit signals
else if ((Position > 0 && isFirstAboveMA) || (Position < 0 && isFirstBelowMA))
{
// Exit position when first asset crosses its moving average
if (Position > 0)
{
SellMarket(Math.Abs(Position));
LogInfo($"Exit Long: {Security.Code}({_lastFirstPrice:F4}) > MA({firstMAValue:F4})");
}
else if (Position < 0)
{
BuyMarket(Math.Abs(Position));
LogInfo($"Exit Short: {Security.Code}({_lastFirstPrice:F4}) < MA({firstMAValue:F4})");
}
}
}
private void ProcessSecondSecurityCandle(ICandleMessage candle)
{
// Skip unfinished candles
if (candle.State != CandleStates.Finished)
return;
// Store current price
_lastSecondPrice = candle.ClosePrice;
// Process through MA indicator and store last value
var maValue = _secondMA.Process(new DecimalIndicatorValue(_secondMA, candle.ClosePrice, candle.ServerTime) { IsFinal = true });
_secondMAValue = maValue.ToDecimal();
}
}
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, Unit, UnitTypes, ICandleMessage
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Security
from datatype_extensions import *
from indicator_extensions import *
class statistical_arbitrage_strategy(Strategy):
"""
Statistical Arbitrage strategy that trades pairs of securities based on their relative mean reversion.
Enters when one asset is below its mean while the other is above its mean.
"""
def __init__(self):
super(statistical_arbitrage_strategy, self).__init__()
# Initialize strategy parameters
self._lookback_period = self.Param("LookbackPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("Lookback Period", "Period for calculating moving averages", "Parameters") \
.SetCanOptimize(True) \
.SetOptimize(10, 30, 5)
self._stop_loss_percent = self.Param("StopLossPercent", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Stop-loss %", "Stop-loss as percentage of entry price", "Risk Management") \
.SetCanOptimize(True) \
.SetOptimize(1.0, 3.0, 0.5)
self._candle_type = self.Param("CandleType", tf(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()
# State variables
self._first_ma = None
self._second_ma = None
self._last_first_price = 0
self._last_second_price = 0
self._entry_spread = 0
self._second_ma_value = 0
@property
def LookbackPeriod(self):
return self._lookback_period.Value
@LookbackPeriod.setter
def LookbackPeriod(self, value):
self._lookback_period.Value = value
@property
def StopLossPercent(self):
return self._stop_loss_percent.Value
@StopLossPercent.setter
def StopLossPercent(self, value):
self._stop_loss_percent.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def SecondSecurity(self):
return self._second_security.Value
@SecondSecurity.setter
def SecondSecurity(self, value):
self._second_security.Value = value
def GetWorkingSecurities(self):
"""!! REQUIRED !! Returns securities for strategy."""
return [
(self.Security, self.CandleType),
(self.SecondSecurity, self.CandleType)
]
def OnReseted(self):
"""Resets internal state when strategy is reset."""
super(statistical_arbitrage_strategy, self).OnReseted()
self._first_ma = None
self._second_ma = None
self._last_first_price = 0
self._last_second_price = 0
self._entry_spread = 0
self._second_ma_value = 0
def OnStarted2(self, time):
"""
Called when the strategy starts. Sets up indicators, subscriptions, and charting.
:param time: The time when the strategy started.
"""
super(statistical_arbitrage_strategy, self).OnStarted2(time)
if self.SecondSecurity is None:
raise Exception("Second security is not specified.")
# Initialize indicators
self._first_ma = SimpleMovingAverage()
self._first_ma.Length = self.LookbackPeriod
self._second_ma = SimpleMovingAverage()
self._second_ma.Length = self.LookbackPeriod
# Create subscriptions for both securities
first_security_subscription = self.SubscribeCandles(self.CandleType)
second_security_subscription = self.SubscribeCandles(self.CandleType, security=self.SecondSecurity)
# Bind to first security candles
first_security_subscription.Bind(self._first_ma, self.ProcessFirstSecurityCandle).Start()
# Bind to second security candles
second_security_subscription.Bind(self.ProcessSecondSecurityCandle).Start()
# Enable position protection with stop-loss
self.StartProtection(
takeProfit=Unit(0, UnitTypes.Absolute),
stopLoss=Unit(self.StopLossPercent, UnitTypes.Percent)
)
# Setup chart if available
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, first_security_subscription)
self.DrawIndicator(area, self._first_ma)
self.DrawOwnTrades(area)
def ProcessFirstSecurityCandle(self, candle, first_ma_value):
"""
Process candle of the first security.
:param candle: The candle message.
:param first_ma_value: The moving average value for the first security.
"""
# Skip unfinished candles
if candle.State != CandleStates.Finished:
return
# Skip if strategy is not ready to trade
# Store current price
self._last_first_price = float(candle.ClosePrice)
# Skip if we don't have both prices or if indicators aren't formed
if self._last_second_price == 0 or not self._first_ma.IsFormed or not self._second_ma.IsFormed:
return
# Get last second MA value stored earlier
second_ma_value = self._second_ma_value
# Trading logic
is_first_below_ma = self._last_first_price < first_ma_value
is_second_above_ma = self._last_second_price > second_ma_value
is_first_above_ma = self._last_first_price > first_ma_value
is_second_below_ma = self._last_second_price < second_ma_value
current_spread = self._last_first_price - self._last_second_price
# Long signal: First asset below MA, Second asset above MA
if is_first_below_ma and is_second_above_ma:
# If we're not already in a long position
if self.Position <= 0:
self.BuyMarket(self.Volume + Math.Abs(self.Position))
self._entry_spread = current_spread
self.LogInfo(
"Long Signal: {0}({1:F4}) < MA({2:F4}) && {3}({4:F4}) > MA({5:F4})".format(
self.Security.Code, self._last_first_price, first_ma_value,
self.SecondSecurity.Code, self._last_second_price, second_ma_value))
# Note: In a real implementation, you would also place a sell order
# for the second security here, using a different strategy instance or connector
# Short signal: First asset above MA, Second asset below MA
elif is_first_above_ma and is_second_below_ma:
# If we're not already in a short position
if self.Position >= 0:
self.SellMarket(self.Volume + Math.Abs(self.Position))
self._entry_spread = current_spread
self.LogInfo(
"Short Signal: {0}({1:F4}) > MA({2:F4}) && {3}({4:F4}) < MA({5:F4})".format(
self.Security.Code, self._last_first_price, first_ma_value,
self.SecondSecurity.Code, self._last_second_price, second_ma_value))
# Note: In a real implementation, you would also place a buy order
# for the second security here, using a different strategy instance or connector
# Exit signals
elif (self.Position > 0 and is_first_above_ma) or (self.Position < 0 and is_first_below_ma):
# Exit position when first asset crosses its moving average
if self.Position > 0:
self.SellMarket(Math.Abs(self.Position))
self.LogInfo(
"Exit Long: {0}({1:F4}) > MA({2:F4})".format(
self.Security.Code, self._last_first_price, first_ma_value))
elif self.Position < 0:
self.BuyMarket(Math.Abs(self.Position))
self.LogInfo(
"Exit Short: {0}({1:F4}) < MA({2:F4})".format(
self.Security.Code, self._last_first_price, first_ma_value))
def ProcessSecondSecurityCandle(self, candle):
"""
Process candle of the second security.
:param candle: The second security candle message.
"""
# Skip unfinished candles
if candle.State != CandleStates.Finished:
return
# Store current price
self._last_second_price = float(candle.ClosePrice)
# Process through MA indicator and remember the result
self._second_ma_value = float(
process_float(
self._second_ma,
candle.ClosePrice,
candle.ServerTime,
candle.State == CandleStates.Finished,
)
)
def CreateClone(self):
"""
!! REQUIRED!! Creates a new instance of the strategy.
"""
return statistical_arbitrage_strategy()