Betting Against Beta Stocks
Betting Against Beta Stocks 策略在股票池中做多最低贝塔十分位并做空最高贝塔十分位,于每月的第一个交易日进行再平衡。
该方法利用低贝塔股票在风险调整后表现更优的现象,计算贝塔时需要一个基准证券。
详情
- 入场条件:每月选择低/高贝塔股票。
- 多空方向:双向。
- 退出条件:在下一次再平衡时调整仓位。
- 止损:无明确止损逻辑。
- 默认值:
WindowDays = 252Deciles = 10CandleType = TimeSpan.FromMinutes(5).TimeFrame()MinTradeUsd = 100
- 过滤器:
- 分类: 统计
- 方向: 双向
- 指标: 贝塔
- 止损: 否
- 复杂度: 中等
- 时间框架: 日线
- 季节性: 否
- 神经网络: 否
- 背离: 否
- 风险等级: 中等
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Configuration;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Betting-against-beta strategy that goes long the primary instrument when its rolling beta versus the benchmark is low and short when beta becomes elevated.
/// </summary>
public class BettingAgainstBetaStocksStrategy : Strategy
{
private readonly StrategyParam<string> _security2Id;
private readonly StrategyParam<int> _betaLength;
private readonly StrategyParam<decimal> _lowBetaThreshold;
private readonly StrategyParam<decimal> _highBetaThreshold;
private readonly StrategyParam<decimal> _exitBetaThreshold;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<DataType> _candleType;
private Security _benchmark = null!;
private Correlation _correlation = null!;
private StandardDeviation _primaryDeviation = null!;
private StandardDeviation _benchmarkDeviation = null!;
private decimal _latestPrimaryPrice;
private decimal _latestBenchmarkPrice;
private decimal _previousPrimaryPrice;
private decimal _previousBenchmarkPrice;
private decimal? _previousBeta;
private bool _primaryUpdated;
private bool _benchmarkUpdated;
private int _cooldownRemaining;
/// <summary>
/// Secondary benchmark security identifier.
/// </summary>
public string Security2Id
{
get => _security2Id.Value;
set => _security2Id.Value = value;
}
/// <summary>
/// Rolling beta lookback length.
/// </summary>
public int BetaLength
{
get => _betaLength.Value;
set => _betaLength.Value = value;
}
/// <summary>
/// Maximum beta required to open a long position.
/// </summary>
public decimal LowBetaThreshold
{
get => _lowBetaThreshold.Value;
set => _lowBetaThreshold.Value = value;
}
/// <summary>
/// Minimum beta required to open a short position.
/// </summary>
public decimal HighBetaThreshold
{
get => _highBetaThreshold.Value;
set => _highBetaThreshold.Value = value;
}
/// <summary>
/// Neutral beta threshold used to close positions.
/// </summary>
public decimal ExitBetaThreshold
{
get => _exitBetaThreshold.Value;
set => _exitBetaThreshold.Value = value;
}
/// <summary>
/// Closed candles to wait before another position change.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.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 BettingAgainstBetaStocksStrategy()
{
_security2Id = Param(nameof(Security2Id), Paths.HistoryDefaultSecurity2)
.SetDisplay("Benchmark Security Id", "Identifier of the benchmark security", "General");
_betaLength = Param(nameof(BetaLength), 16)
.SetRange(10, 150)
.SetDisplay("Beta Length", "Rolling beta lookback length", "Indicators");
_lowBetaThreshold = Param(nameof(LowBetaThreshold), 0.95m)
.SetRange(0.2m, 1.2m)
.SetDisplay("Low Beta Threshold", "Maximum beta required to open a long position", "Signals");
_highBetaThreshold = Param(nameof(HighBetaThreshold), 1.05m)
.SetRange(0.8m, 2.5m)
.SetDisplay("High Beta Threshold", "Minimum beta required to open a short position", "Signals");
_exitBetaThreshold = Param(nameof(ExitBetaThreshold), 1m)
.SetRange(0.5m, 1.5m)
.SetDisplay("Exit Beta Threshold", "Neutral beta threshold used to close positions", "Signals");
_cooldownBars = Param(nameof(CooldownBars), 8)
.SetRange(0, 100)
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "Risk");
_stopLoss = Param(nameof(StopLoss), 2m)
.SetRange(0.5m, 10m)
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).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();
_benchmark = null!;
_correlation = null!;
_primaryDeviation = null!;
_benchmarkDeviation = null!;
_latestPrimaryPrice = 0m;
_latestBenchmarkPrice = 0m;
_previousPrimaryPrice = 0m;
_previousBenchmarkPrice = 0m;
_previousBeta = null;
_primaryUpdated = false;
_benchmarkUpdated = false;
_cooldownRemaining = 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("Benchmark security identifier is not specified.");
_benchmark = this.LookupById(Security2Id) ?? new Security { Id = Security2Id };
_correlation = new Correlation { Length = BetaLength };
_primaryDeviation = new StandardDeviation { Length = BetaLength };
_benchmarkDeviation = new StandardDeviation { Length = BetaLength };
_cooldownRemaining = 0;
var primarySubscription = SubscribeCandles(CandleType, security: Security);
var benchmarkSubscription = SubscribeCandles(CandleType, security: _benchmark);
primarySubscription
.Bind(ProcessPrimaryCandle)
.Start();
benchmarkSubscription
.Bind(ProcessBenchmarkCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, primarySubscription);
DrawCandles(area, benchmarkSubscription);
DrawOwnTrades(area);
}
StartProtection(
new Unit(2, UnitTypes.Percent),
new Unit(StopLoss, UnitTypes.Percent));
}
private void ProcessPrimaryCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_latestPrimaryPrice = candle.ClosePrice;
_primaryUpdated = true;
TryProcessBeta(candle.OpenTime);
}
private void ProcessBenchmarkCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_latestBenchmarkPrice = candle.ClosePrice;
_benchmarkUpdated = true;
TryProcessBeta(candle.OpenTime);
}
private void TryProcessBeta(DateTime time)
{
if (!_primaryUpdated || !_benchmarkUpdated)
return;
_primaryUpdated = false;
_benchmarkUpdated = false;
if (_previousPrimaryPrice <= 0m || _previousBenchmarkPrice <= 0m)
{
_previousPrimaryPrice = _latestPrimaryPrice;
_previousBenchmarkPrice = _latestBenchmarkPrice;
return;
}
var primaryReturn = (_latestPrimaryPrice - _previousPrimaryPrice) / Math.Max(_previousPrimaryPrice, 1m);
var benchmarkReturn = (_latestBenchmarkPrice - _previousBenchmarkPrice) / Math.Max(_previousBenchmarkPrice, 1m);
_previousPrimaryPrice = _latestPrimaryPrice;
_previousBenchmarkPrice = _latestBenchmarkPrice;
var correlationInput = new PairIndicatorValue<decimal>(_correlation, (primaryReturn, benchmarkReturn), time)
{
IsFinal = true
};
var correlation = _correlation.Process(correlationInput).ToDecimal();
var primaryDeviation = _primaryDeviation.Process(primaryReturn, time, true).ToDecimal();
var benchmarkDeviation = _benchmarkDeviation.Process(benchmarkReturn, time, true).ToDecimal();
if (!_correlation.IsFormed || !_primaryDeviation.IsFormed || !_benchmarkDeviation.IsFormed || benchmarkDeviation <= 0m)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var beta = correlation * (primaryDeviation / benchmarkDeviation);
var bullishEntry = beta <= LowBetaThreshold;
var bearishEntry = beta >= HighBetaThreshold;
if (_cooldownRemaining == 0 && Position == 0)
{
if (bullishEntry)
{
BuyMarket();
_cooldownRemaining = CooldownBars;
}
else if (bearishEntry)
{
SellMarket();
_cooldownRemaining = CooldownBars;
}
}
else if (Position > 0 && beta >= ExitBetaThreshold)
{
SellMarket(Position);
_cooldownRemaining = CooldownBars;
}
else if (Position < 0 && beta <= ExitBetaThreshold)
{
BuyMarket(Math.Abs(Position));
_cooldownRemaining = CooldownBars;
}
_previousBeta = beta;
}
}
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, ValueTuple
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import Correlation, StandardDeviation, PairIndicatorValue
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Security
from indicator_extensions import *
class betting_against_beta_stocks_strategy(Strategy):
"""Betting-against-beta strategy using dual securities and rolling beta."""
def __init__(self):
super(betting_against_beta_stocks_strategy, self).__init__()
self._security2_id = self.Param("Security2Id", "TONUSDT@BNBFT") \
.SetDisplay("Benchmark Security Id", "Identifier of the benchmark security", "General")
self._beta_length = self.Param("BetaLength", 16) \
.SetRange(10, 150) \
.SetDisplay("Beta Length", "Rolling beta lookback length", "Indicators")
self._low_beta_threshold = self.Param("LowBetaThreshold", 0.95) \
.SetRange(0.2, 1.2) \
.SetDisplay("Low Beta Threshold", "Maximum beta required to open a long position", "Signals")
self._high_beta_threshold = self.Param("HighBetaThreshold", 1.05) \
.SetRange(0.8, 2.5) \
.SetDisplay("High Beta Threshold", "Minimum beta required to open a short position", "Signals")
self._exit_beta_threshold = self.Param("ExitBetaThreshold", 1.0) \
.SetRange(0.5, 1.5) \
.SetDisplay("Exit Beta Threshold", "Neutral beta threshold used to close positions", "Signals")
self._cooldown_bars = self.Param("CooldownBars", 8) \
.SetRange(0, 100) \
.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "Risk")
self._stop_loss = self.Param("StopLoss", 2.0) \
.SetRange(0.5, 10.0) \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle series for both instruments", "General")
self._benchmark = None
self._correlation = None
self._primary_deviation = None
self._benchmark_deviation = None
self._latest_primary_price = 0.0
self._latest_benchmark_price = 0.0
self._previous_primary_price = 0.0
self._previous_benchmark_price = 0.0
self._previous_beta = None
self._primary_updated = False
self._benchmark_updated = False
self._cooldown_remaining = 0
@property
def candle_type(self):
return self._candle_type.Value
def GetWorkingSecurities(self):
result = []
if self.Security is not None:
result.append((self.Security, self.candle_type))
sec2_id = str(self._security2_id.Value)
if sec2_id:
s = Security()
s.Id = sec2_id
result.append((s, self.candle_type))
return result
def OnReseted(self):
super(betting_against_beta_stocks_strategy, self).OnReseted()
self._benchmark = None
self._correlation = None
self._primary_deviation = None
self._benchmark_deviation = None
self._latest_primary_price = 0.0
self._latest_benchmark_price = 0.0
self._previous_primary_price = 0.0
self._previous_benchmark_price = 0.0
self._previous_beta = None
self._primary_updated = False
self._benchmark_updated = False
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(betting_against_beta_stocks_strategy, self).OnStarted2(time)
sec2_id = str(self._security2_id.Value)
if not sec2_id:
raise Exception("Benchmark security identifier is not specified.")
s = Security()
s.Id = sec2_id
self._benchmark = s
beta_len = int(self._beta_length.Value)
self._correlation = Correlation()
self._correlation.Length = beta_len
self._primary_deviation = StandardDeviation()
self._primary_deviation.Length = beta_len
self._benchmark_deviation = StandardDeviation()
self._benchmark_deviation.Length = beta_len
self._cooldown_remaining = 0
primary_subscription = self.SubscribeCandles(self.candle_type, True, self.Security)
benchmark_subscription = self.SubscribeCandles(self.candle_type, True, self._benchmark)
primary_subscription.Bind(self.ProcessPrimaryCandle).Start()
benchmark_subscription.Bind(self.ProcessBenchmarkCandle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, primary_subscription)
self.DrawCandles(area, benchmark_subscription)
self.DrawOwnTrades(area)
self.StartProtection(
Unit(2, UnitTypes.Percent),
Unit(float(self._stop_loss.Value), UnitTypes.Percent)
)
def ProcessPrimaryCandle(self, candle):
if candle.State != CandleStates.Finished:
return
self._latest_primary_price = float(candle.ClosePrice)
self._primary_updated = True
self.TryProcessBeta(candle.OpenTime)
def ProcessBenchmarkCandle(self, candle):
if candle.State != CandleStates.Finished:
return
self._latest_benchmark_price = float(candle.ClosePrice)
self._benchmark_updated = True
self.TryProcessBeta(candle.OpenTime)
def TryProcessBeta(self, time):
if not self._primary_updated or not self._benchmark_updated:
return
self._primary_updated = False
self._benchmark_updated = False
if self._previous_primary_price <= 0.0 or self._previous_benchmark_price <= 0.0:
self._previous_primary_price = self._latest_primary_price
self._previous_benchmark_price = self._latest_benchmark_price
return
primary_return = (self._latest_primary_price - self._previous_primary_price) / max(self._previous_primary_price, 1.0)
benchmark_return = (self._latest_benchmark_price - self._previous_benchmark_price) / max(self._previous_benchmark_price, 1.0)
self._previous_primary_price = self._latest_primary_price
self._previous_benchmark_price = self._latest_benchmark_price
pair_val = ValueTuple[Decimal, Decimal](Decimal(primary_return), Decimal(benchmark_return))
pair_input = PairIndicatorValue[Decimal](self._correlation, pair_val, time)
pair_input.IsFinal = True
corr_result = self._correlation.Process(pair_input)
correlation = float(corr_result)
prim_dev_result = process_float(self._primary_deviation, Decimal(primary_return), time, True)
primary_dev = float(prim_dev_result)
bench_dev_result = process_float(self._benchmark_deviation, Decimal(benchmark_return), time, True)
benchmark_dev = float(bench_dev_result)
if not self._correlation.IsFormed or not self._primary_deviation.IsFormed or not self._benchmark_deviation.IsFormed or benchmark_dev <= 0:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
beta = correlation * (primary_dev / benchmark_dev)
low_thresh = float(self._low_beta_threshold.Value)
high_thresh = float(self._high_beta_threshold.Value)
exit_thresh = float(self._exit_beta_threshold.Value)
cooldown = int(self._cooldown_bars.Value)
bullish_entry = beta <= low_thresh
bearish_entry = beta >= high_thresh
if self._cooldown_remaining == 0 and self.Position == 0:
if bullish_entry:
self.BuyMarket()
self._cooldown_remaining = cooldown
elif bearish_entry:
self.SellMarket()
self._cooldown_remaining = cooldown
elif self.Position > 0 and beta >= exit_thresh:
self.SellMarket(self.Position)
self._cooldown_remaining = cooldown
elif self.Position < 0 and beta <= exit_thresh:
self.BuyMarket(Math.Abs(self.Position))
self._cooldown_remaining = cooldown
self._previous_beta = beta
def CreateClone(self):
return betting_against_beta_stocks_strategy()