Парный трейдинг с учетом беты
Стратегия Beta Adjusted Pairs Trading использует коэффициент бета вместе с фильтрами волатильности. Вход в сделки происходит только при совпадении заданных условий.
Сигналы формируются, когда индикатор превышает порог, а волатильность отвечает установленным критериям. Позиции могут быть длинными или короткими и имеют встроенные стопы.
Стратегия ориентирована на трейдеров, ценящих контроль риска: выход осуществляется, как только индикатор возвращается к среднему или волатильность изменяется. Начальное значение Asset2 = (Security.
Детали
- Условия входа: Индикатор разворачивается в сторону среднего значения.
- Длинные/Короткие: Оба направления.
- Условия выхода: Индикатор возвращается к среднему.
- Стопы: Да.
- Значения по умолчанию:
Asset2= (SecurityAsset2Portfolio= (PortfolioBetaAsset1= 1.0mBetaAsset2= 1.0mLookbackPeriod= 20EntryThreshold= 2.0mStopLoss= 2.0m
- Фильтры:
- Категория: Средневозвратная
- Направление: Оба
- Индикаторы: Beta
- Стопы: Да
- Сложность: Средняя
- Таймфрейм: Краткосрочный
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Configuration;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Mean-reversion strategy that trades the primary instrument when a beta-adjusted spread versus the secondary instrument becomes stretched.
/// </summary>
public class BetaAdjustedPairsStrategy : Strategy
{
private readonly StrategyParam<string> _security2Id;
private readonly StrategyParam<decimal> _betaAsset1;
private readonly StrategyParam<decimal> _betaAsset2;
private readonly StrategyParam<int> _lookbackPeriod;
private readonly StrategyParam<decimal> _entryThreshold;
private readonly StrategyParam<decimal> _exitThreshold;
private readonly StrategyParam<decimal> _stopLossPercent;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private Security _security2;
private SimpleMovingAverage _spreadAverage;
private StandardDeviation _spreadStdDev;
private decimal _latestPrice1;
private decimal _latestPrice2;
private decimal _entrySpread;
private bool _primaryUpdated;
private bool _secondaryUpdated;
private int _cooldown;
/// <summary>
/// Secondary security identifier.
/// </summary>
public string Security2Id
{
get => _security2Id.Value;
set => _security2Id.Value = value;
}
/// <summary>
/// Beta coefficient of the primary security.
/// </summary>
public decimal BetaAsset1
{
get => _betaAsset1.Value;
set => _betaAsset1.Value = value;
}
/// <summary>
/// Beta coefficient of the secondary security.
/// </summary>
public decimal BetaAsset2
{
get => _betaAsset2.Value;
set => _betaAsset2.Value = value;
}
/// <summary>
/// Lookback period for spread statistics.
/// </summary>
public int LookbackPeriod
{
get => _lookbackPeriod.Value;
set => _lookbackPeriod.Value = value;
}
/// <summary>
/// Entry threshold measured in spread standard deviations.
/// </summary>
public decimal EntryThreshold
{
get => _entryThreshold.Value;
set => _entryThreshold.Value = value;
}
/// <summary>
/// Exit threshold measured in spread standard deviations.
/// </summary>
public decimal ExitThreshold
{
get => _exitThreshold.Value;
set => _exitThreshold.Value = value;
}
/// <summary>
/// Stop loss percentage applied to spread distance from entry.
/// </summary>
public decimal StopLossPercent
{
get => _stopLossPercent.Value;
set => _stopLossPercent.Value = value;
}
/// <summary>
/// Bars to wait between orders.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Candle type used for both instruments.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public BetaAdjustedPairsStrategy()
{
_security2Id = Param(nameof(Security2Id), Paths.HistoryDefaultSecurity2)
.SetDisplay("Second Security Id", "Identifier of the secondary security", "General");
_betaAsset1 = Param(nameof(BetaAsset1), 1m)
.SetRange(0.1m, 5m)
.SetDisplay("Primary Beta", "Beta coefficient of the primary security", "Spread");
_betaAsset2 = Param(nameof(BetaAsset2), 1m)
.SetRange(0.1m, 5m)
.SetDisplay("Secondary Beta", "Beta coefficient of the secondary security", "Spread");
_lookbackPeriod = Param(nameof(LookbackPeriod), 30)
.SetRange(10, 150)
.SetDisplay("Lookback Period", "Lookback period for spread statistics", "Indicators");
_entryThreshold = Param(nameof(EntryThreshold), 1.1m)
.SetRange(0.25m, 5m)
.SetDisplay("Entry Threshold", "Entry threshold in spread standard deviations", "Signals");
_exitThreshold = Param(nameof(ExitThreshold), 0.15m)
.SetRange(0m, 2m)
.SetDisplay("Exit Threshold", "Exit threshold in spread standard deviations", "Signals");
_stopLossPercent = Param(nameof(StopLossPercent), 2m)
.SetRange(0.5m, 10m)
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk");
_cooldownBars = Param(nameof(CooldownBars), 120)
.SetRange(1, 500)
.SetDisplay("Cooldown Bars", "Bars to wait between orders", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle series for both instruments", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security != null)
yield return (Security, CandleType);
if (!Security2Id.IsEmpty())
yield return (new Security { Id = Security2Id }, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_security2 = null;
_spreadAverage = null;
_spreadStdDev = null;
_latestPrice1 = 0m;
_latestPrice2 = 0m;
_entrySpread = 0m;
_primaryUpdated = false;
_secondaryUpdated = false;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (Security == null)
throw new InvalidOperationException("Primary security is not specified.");
if (Security2Id.IsEmpty())
throw new InvalidOperationException("Secondary security identifier is not specified.");
_security2 = this.LookupById(Security2Id) ?? new Security { Id = Security2Id };
_spreadAverage = new SimpleMovingAverage { Length = LookbackPeriod };
_spreadStdDev = new StandardDeviation { Length = LookbackPeriod };
_cooldown = 0;
var primarySubscription = SubscribeCandles(CandleType, security: Security);
var secondarySubscription = SubscribeCandles(CandleType, security: _security2);
primarySubscription
.Bind(ProcessPrimaryCandle)
.Start();
secondarySubscription
.Bind(ProcessSecondaryCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, primarySubscription);
DrawCandles(area, secondarySubscription);
DrawOwnTrades(area);
}
StartProtection(new Unit(0, UnitTypes.Absolute), new Unit(StopLossPercent, UnitTypes.Percent), false);
}
private void ProcessPrimaryCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_latestPrice1 = candle.ClosePrice;
_primaryUpdated = true;
TryProcessSpread(candle.OpenTime);
}
private void ProcessSecondaryCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_latestPrice2 = candle.ClosePrice;
_secondaryUpdated = true;
TryProcessSpread(candle.OpenTime);
}
private void TryProcessSpread(DateTimeOffset time)
{
if (!_primaryUpdated || !_secondaryUpdated)
return;
_primaryUpdated = false;
_secondaryUpdated = false;
if (_latestPrice1 <= 0 || _latestPrice2 <= 0 || BetaAsset1 <= 0 || BetaAsset2 <= 0)
return;
var spread = (_latestPrice1 / BetaAsset1) - (_latestPrice2 / BetaAsset2);
var averageSpread = _spreadAverage.Process(spread, time.UtcDateTime, true).ToDecimal();
var spreadStdDev = _spreadStdDev.Process(spread, time.UtcDateTime, true).ToDecimal();
if (!_spreadAverage.IsFormed || !_spreadStdDev.IsFormed)
return;
if (ProcessState != ProcessStates.Started)
return;
if (_cooldown > 0)
{
_cooldown--;
return;
}
if (spreadStdDev <= 0)
return;
var zScore = (spread - averageSpread) / spreadStdDev;
if (Position == 0)
{
if (zScore <= -EntryThreshold)
{
_entrySpread = spread;
BuyMarket();
_cooldown = CooldownBars;
}
else if (zScore >= EntryThreshold)
{
_entrySpread = spread;
SellMarket();
_cooldown = CooldownBars;
}
return;
}
var stopDistance = Math.Max(Math.Abs(_entrySpread) * StopLossPercent / 100m, Security.PriceStep ?? 1m);
var stopTriggered = Position > 0
? spread <= _entrySpread - stopDistance
: spread >= _entrySpread + stopDistance;
if (Position > 0 && (zScore >= -ExitThreshold || stopTriggered))
{
SellMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
else if (Position < 0 && (zScore <= ExitThreshold || stopTriggered))
{
BuyMarket(Math.Abs(Position));
_cooldown = CooldownBars;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import SimpleMovingAverage, StandardDeviation
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Security
from indicator_extensions import *
class beta_adjusted_pairs_strategy(Strategy):
"""
Mean-reversion strategy that trades the primary instrument when a beta-adjusted
spread versus the secondary instrument becomes stretched.
"""
def __init__(self):
super(beta_adjusted_pairs_strategy, self).__init__()
self._security2_id = self.Param("Security2Id", "TONUSDT@BNBFT") \
.SetDisplay("Second Security Id", "Identifier of the secondary security", "General")
self._beta_asset1 = self.Param("BetaAsset1", 1.0) \
.SetDisplay("Primary Beta", "Beta coefficient of the primary security", "Spread")
self._beta_asset2 = self.Param("BetaAsset2", 1.0) \
.SetDisplay("Secondary Beta", "Beta coefficient of the secondary security", "Spread")
self._lookback_period = self.Param("LookbackPeriod", 30) \
.SetDisplay("Lookback Period", "Lookback period for spread statistics", "Indicators")
self._entry_threshold = self.Param("EntryThreshold", 1.1) \
.SetDisplay("Entry Threshold", "Entry threshold in spread standard deviations", "Signals")
self._exit_threshold = self.Param("ExitThreshold", 0.15) \
.SetDisplay("Exit Threshold", "Exit threshold in spread standard deviations", "Signals")
self._stop_loss_percent = self.Param("StopLossPercent", 2.0) \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk")
self._cooldown_bars = self.Param("CooldownBars", 120) \
.SetDisplay("Cooldown Bars", "Bars to wait between orders", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Candle series for both instruments", "General")
self._security2 = None
self._spread_average = None
self._spread_std_dev = None
self._latest_price1 = Decimal(0)
self._latest_price2 = Decimal(0)
self._entry_spread = Decimal(0)
self._primary_updated = False
self._secondary_updated = False
self._cooldown = 0
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(beta_adjusted_pairs_strategy, self).OnReseted()
self._security2 = None
self._spread_average = None
self._spread_std_dev = None
self._latest_price1 = Decimal(0)
self._latest_price2 = Decimal(0)
self._entry_spread = Decimal(0)
self._primary_updated = False
self._secondary_updated = False
self._cooldown = 0
def OnStarted2(self, time):
super(beta_adjusted_pairs_strategy, self).OnStarted2(time)
sec2_id = str(self._security2_id.Value)
if not sec2_id:
raise Exception("Secondary security identifier is not specified.")
s = Security()
s.Id = sec2_id
self._security2 = s
self._spread_average = SimpleMovingAverage()
self._spread_average.Length = int(self._lookback_period.Value)
self._spread_std_dev = StandardDeviation()
self._spread_std_dev.Length = int(self._lookback_period.Value)
self._cooldown = 0
primary_subscription = self.SubscribeCandles(self.candle_type, False, self.Security)
secondary_subscription = self.SubscribeCandles(self.candle_type, False, self._security2)
primary_subscription.Bind(self._process_primary_candle).Start()
secondary_subscription.Bind(self._process_secondary_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, primary_subscription)
self.DrawCandles(area, secondary_subscription)
self.DrawOwnTrades(area)
self.StartProtection(Unit(0, UnitTypes.Absolute), Unit(self._stop_loss_percent.Value, UnitTypes.Percent), False)
def _process_primary_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._latest_price1 = candle.ClosePrice
self._primary_updated = True
self._try_process_spread(candle.OpenTime)
def _process_secondary_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._latest_price2 = candle.ClosePrice
self._secondary_updated = True
self._try_process_spread(candle.OpenTime)
def _try_process_spread(self, time):
if not self._primary_updated or not self._secondary_updated:
return
self._primary_updated = False
self._secondary_updated = False
try:
time = time.UtcDateTime
except:
pass
beta1 = float(self._beta_asset1.Value)
beta2 = float(self._beta_asset2.Value)
if float(self._latest_price1) <= 0 or float(self._latest_price2) <= 0 or beta1 <= 0 or beta2 <= 0:
return
spread = float(self._latest_price1) / beta1 - float(self._latest_price2) / beta2
avg_result = process_float(self._spread_average, Decimal(spread), time, True)
average_spread = float(avg_result)
std_result = process_float(self._spread_std_dev, Decimal(spread), time, True)
spread_std_dev = float(std_result)
if not self._spread_average.IsFormed or not self._spread_std_dev.IsFormed:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._cooldown > 0:
self._cooldown -= 1
return
if spread_std_dev <= 0:
return
z_score = (spread - average_spread) / spread_std_dev
entry_thresh = float(self._entry_threshold.Value)
exit_thresh = float(self._exit_threshold.Value)
cd = int(self._cooldown_bars.Value)
if self.Position == 0:
if z_score <= -entry_thresh:
self._entry_spread = spread
self.BuyMarket()
self._cooldown = cd
elif z_score >= entry_thresh:
self._entry_spread = spread
self.SellMarket()
self._cooldown = cd
return
stop_pct = float(self._stop_loss_percent.Value)
price_step = float(self.Security.PriceStep) if self.Security.PriceStep is not None else 1.0
stop_distance = max(abs(self._entry_spread) * stop_pct / 100.0, price_step)
if self.Position > 0:
stop_triggered = spread <= self._entry_spread - stop_distance
else:
stop_triggered = spread >= self._entry_spread + stop_distance
if self.Position > 0 and (z_score >= -exit_thresh or stop_triggered):
self.SellMarket(Math.Abs(self.Position))
self._cooldown = cd
elif self.Position < 0 and (z_score <= exit_thresh or stop_triggered):
self.BuyMarket(Math.Abs(self.Position))
self._cooldown = cd
def CreateClone(self):
return beta_adjusted_pairs_strategy()