Statistical Arbitrage Strategy
该统计套利策略根据两只相关资产相对于自身均线的位置进行交易,利用价差在短期内回归的特性。
测试表明年均收益约为 94%,该策略在股票市场表现最佳。
当第一只资产低于其均线而第二只高于自身均线时做多第一只并做空第二只;反之则做空第一只做多第二只。第一只资产回到均线上方或下方时平仓,表明价差已恢复正常。
适合习惯于在两只工具之间保持中性敞口的交易者。内置的止损在价差持续扩大时限制回撤。
细节
- 入场条件:
- 多头:
Asset1 < MA1 && Asset2 > MA2 - 空头:
Asset1 > MA1 && Asset2 < MA2
- 多头:
- 多/空: 双向
- 离场条件:
- 多头: 当Asset1收盘价上穿MA1
- 空头: 当Asset1收盘价下穿MA1
- 止损: 对价差使用百分比止损
- 默认值:
LookbackPeriod= 20StopLossPercent= 2mCandleType= TimeSpan.FromMinutes(15)
- 过滤器:
- 类别: Arbitrage
- 方向: 双向
- 指标: Moving Averages
- 止损: 是
- 复杂度: 中等
- 时间框架: 日内
- 季节性: 否
- 神经网络: 否
- 背离: 是
- 风险等级: 中等
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()