Auf GitHub ansehen

Momentum-Faktor-Aktien-Strategie

Dieser systematische Ansatz nutzt den klassischen 12-1-Monats-Momentum-Faktor bei Aktien. Am Ende jedes Monats werden Aktien nach ihrer Performance über die vorangegangenen zwölf Monate eingestuft, wobei der jüngste Monat ausgelassen wird, um kurzfristige Umkehrungen zu umgehen. Wertpapiere im höchsten Quintil werden gekauft und jene im niedrigsten Quintil leerverkauft, wodurch ein marktneutraler Spread gebildet wird.

Das Rebalancing erfolgt am ersten Handelstag jedes Monats. Positionen sind gleichgewichtet und bleiben bis zum nächsten Rebalancing offen; es werden keine expliziten Stop-Losses verwendet.

Umfangreiche akademische und industrielle Forschung zeigt, dass Momentum beständige Überrenditen liefert und wertvolle Diversifikation bietet, wenn es mit anderen Faktoren kombiniert wird.

Details

  • Einstiegskriterien: Monatliches 12-1-Momentum-Ranking; Long oberstes Quintil, Short unterstes Quintil
  • Long/Short: Beide
  • Ausstiegskriterien: Nächstes monatliches Rebalancing
  • Stops: Nein
  • Standardwerte:
    • LookbackDays = 252
    • SkipDays = 21
    • Quintile = 5
    • MinTradeUsd = 200
    • CandleType = TimeSpan.FromDays(1)
  • Filter:
    • Kategorie: Momentum
    • Richtung: Beide
    • Indikatoren: Kursveränderung
    • Stops: Nein
    • Komplexität: Mittel
    • Zeitrahmen: Mittelfristig
    • Saisonalität: Nein
    • Neuronale Netze: Nein
    • Divergenz: Nein
    • Risikolevel: Mittel
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>
/// Momentum factor strategy that trades the primary instrument when its medium-term momentum outperforms a benchmark after filtering out the most recent short-term move.
/// </summary>
public class MomentumFactorStocksStrategy : Strategy
{
	private readonly StrategyParam<string> _security2Id;
	private readonly StrategyParam<int> _lookbackPeriod;
	private readonly StrategyParam<int> _skipPeriod;
	private readonly StrategyParam<int> _normalizationPeriod;
	private readonly StrategyParam<decimal> _entryThreshold;
	private readonly StrategyParam<decimal> _exitThreshold;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<DataType> _candleType;

	private Security _benchmark = null!;
	private RateOfChange _primaryLongMomentum = null!;
	private RateOfChange _benchmarkLongMomentum = null!;
	private RateOfChange _primaryShortMomentum = null!;
	private RateOfChange _benchmarkShortMomentum = null!;
	private SimpleMovingAverage _spreadAverage = null!;
	private StandardDeviation _spreadDeviation = null!;
	private decimal _latestPrimarySignal;
	private decimal _latestBenchmarkSignal;
	private decimal? _previousZScore;
	private bool _primaryUpdated;
	private bool _benchmarkUpdated;
	private int _cooldownRemaining;

	/// <summary>
	/// Benchmark security identifier.
	/// </summary>
	public string Security2Id
	{
		get => _security2Id.Value;
		set => _security2Id.Value = value;
	}

	/// <summary>
	/// Long momentum lookback period.
	/// </summary>
	public int LookbackPeriod
	{
		get => _lookbackPeriod.Value;
		set => _lookbackPeriod.Value = value;
	}

	/// <summary>
	/// Short lookback period removed from the long momentum signal.
	/// </summary>
	public int SkipPeriod
	{
		get => _skipPeriod.Value;
		set => _skipPeriod.Value = value;
	}

	/// <summary>
	/// Lookback period used to normalize the relative factor spread.
	/// </summary>
	public int NormalizationPeriod
	{
		get => _normalizationPeriod.Value;
		set => _normalizationPeriod.Value = value;
	}

	/// <summary>
	/// Z-score threshold required to open a position.
	/// </summary>
	public decimal EntryThreshold
	{
		get => _entryThreshold.Value;
		set => _entryThreshold.Value = value;
	}

	/// <summary>
	/// Z-score threshold required to close a position.
	/// </summary>
	public decimal ExitThreshold
	{
		get => _exitThreshold.Value;
		set => _exitThreshold.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 calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public MomentumFactorStocksStrategy()
	{
		_security2Id = Param(nameof(Security2Id), Paths.HistoryDefaultSecurity2)
			.SetDisplay("Benchmark Security Id", "Identifier of the benchmark security", "General");

		_lookbackPeriod = Param(nameof(LookbackPeriod), 36)
			.SetRange(8, 200)
			.SetDisplay("Lookback Period", "Long momentum lookback period", "Indicators");

		_skipPeriod = Param(nameof(SkipPeriod), 8)
			.SetRange(2, 60)
			.SetDisplay("Skip Period", "Short lookback period removed from the long momentum signal", "Indicators");

		_normalizationPeriod = Param(nameof(NormalizationPeriod), 24)
			.SetRange(5, 120)
			.SetDisplay("Normalization Period", "Lookback period used to normalize the relative factor spread", "Indicators");

		_entryThreshold = Param(nameof(EntryThreshold), 1.15m)
			.SetRange(0.2m, 5m)
			.SetDisplay("Entry Threshold", "Z-score threshold required to open a position", "Signals");

		_exitThreshold = Param(nameof(ExitThreshold), 0.3m)
			.SetRange(0m, 2m)
			.SetDisplay("Exit Threshold", "Z-score threshold required to close a position", "Signals");

		_cooldownBars = Param(nameof(CooldownBars), 8)
			.SetRange(0, 120)
			.SetDisplay("Cooldown Bars", "Closed candles to wait before another position change", "Risk");

		_stopLoss = Param(nameof(StopLoss), 2.5m)
			.SetRange(0.5m, 10m)
			.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for candles", "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!;
		_primaryLongMomentum = null!;
		_benchmarkLongMomentum = null!;
		_primaryShortMomentum = null!;
		_benchmarkShortMomentum = null!;
		_spreadAverage = null!;
		_spreadDeviation = null!;
		_latestPrimarySignal = 0m;
		_latestBenchmarkSignal = 0m;
		_previousZScore = 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 };
		_primaryLongMomentum = new RateOfChange { Length = LookbackPeriod };
		_benchmarkLongMomentum = new RateOfChange { Length = LookbackPeriod };
		_primaryShortMomentum = new RateOfChange { Length = SkipPeriod };
		_benchmarkShortMomentum = new RateOfChange { Length = SkipPeriod };
		_spreadAverage = new SimpleMovingAverage { Length = NormalizationPeriod };
		_spreadDeviation = new StandardDeviation { Length = NormalizationPeriod };

		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;

		var longMomentum = _primaryLongMomentum.Process(candle);
		var shortMomentum = _primaryShortMomentum.Process(candle);

		if (longMomentum.IsEmpty || shortMomentum.IsEmpty || !_primaryLongMomentum.IsFormed || !_primaryShortMomentum.IsFormed)
			return;

		_latestPrimarySignal = longMomentum.ToDecimal() - shortMomentum.ToDecimal();
		_primaryUpdated = true;
		TryProcessSpread(candle.OpenTime);
	}

	private void ProcessBenchmarkCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var longMomentum = _benchmarkLongMomentum.Process(candle);
		var shortMomentum = _benchmarkShortMomentum.Process(candle);

		if (longMomentum.IsEmpty || shortMomentum.IsEmpty || !_benchmarkLongMomentum.IsFormed || !_benchmarkShortMomentum.IsFormed)
			return;

		_latestBenchmarkSignal = longMomentum.ToDecimal() - shortMomentum.ToDecimal();
		_benchmarkUpdated = true;
		TryProcessSpread(candle.OpenTime);
	}

	private void TryProcessSpread(DateTime time)
	{
		if (!_primaryUpdated || !_benchmarkUpdated)
			return;

		_primaryUpdated = false;
		_benchmarkUpdated = false;

		var spread = _latestPrimarySignal - _latestBenchmarkSignal;
		var mean = _spreadAverage.Process(spread, time, true).ToDecimal();
		var deviation = _spreadDeviation.Process(spread, time, true).ToDecimal();

		if (!_spreadAverage.IsFormed || !_spreadDeviation.IsFormed || deviation <= 0m)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		var zScore = (spread - mean) / deviation;
		var bullishEntry = _previousZScore is decimal previousBullish && previousBullish < EntryThreshold && zScore >= EntryThreshold;
		var bearishEntry = _previousZScore is decimal previousBearish && previousBearish > -EntryThreshold && zScore <= -EntryThreshold;

		if (_cooldownRemaining == 0 && Position == 0)
		{
			if (bullishEntry)
			{
				BuyMarket();
				_cooldownRemaining = CooldownBars;
			}
			else if (bearishEntry)
			{
				SellMarket();
				_cooldownRemaining = CooldownBars;
			}
		}
		else if (Position > 0 && zScore <= ExitThreshold)
		{
			SellMarket(Position);
			_cooldownRemaining = CooldownBars;
		}
		else if (Position < 0 && zScore >= -ExitThreshold)
		{
			BuyMarket(Math.Abs(Position));
			_cooldownRemaining = CooldownBars;
		}

		_previousZScore = zScore;
	}
}