GitHub で見る

Crunchsters 正規化トレンド戦略

リターンを正規化し、累積正規化価格にHull Moving Averageを適用する戦略です。 正規化価格がHMAを上抜けたときにロングエントリーし、下抜けたときにショートエントリーします。

テストでは年平均リターン約105%を示しています。暗号資産市場で最もよいパフォーマンスを発揮します。

正規化リターンにより、直近のボラティリティに対して価格をスケーリングできます。ATRベースのストップでリスクを管理します。

詳細

  • エントリー条件:
    • ロング: nPriceがHMAを上抜け
    • ショート: nPriceがHMAを下抜け
  • ロング/ショート: 両方
  • エグジット条件: 逆方向のクロスオーバーまたはATRストップ
  • ストップ: StopMultipleを使用したATRベース
  • デフォルト値:
    • NormPeriod = 14
    • HmaPeriod = 100
    • HmaOffset = 0
    • StopMultiple = 1m
    • CandleType = TimeSpan.FromMinutes(5).TimeFrame()
  • フィルター:
    • カテゴリ: トレンドフォロー
    • 方向: 両方
    • インジケーター: Hull Moving Average, Standard Deviation, ATR
    • ストップ: はい
    • 複雑さ: 中級
    • 時間軸: 中期
    • 季節性: いいえ
    • ニューラルネットワーク: いいえ
    • ダイバージェンス: いいえ
    • リスクレベル: 中
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Crunchster's Normalised Trend strategy.
/// Each close-to-close return is divided by the standard deviation of the last NormPeriod returns and the results are summed
/// into a normalised price. A cross of the normalised price above its Hull moving average (taken HmaOffset bars back) goes long
/// and a cross below goes short, reversing an opposite position. A stop StopMultiple ATRs (NormPeriod long) from the entry
/// closes a losing trade.
/// </summary>
public class CrunchstersNormalisedTrendStrategy : Strategy
{
	private readonly StrategyParam<int> _normPeriod;
	private readonly StrategyParam<int> _hmaPeriod;
	private readonly StrategyParam<int> _hmaOffset;
	private readonly StrategyParam<decimal> _stopMultiple;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _hmaHistory = [];
	private StandardDeviation _returnDeviation;
	private HullMovingAverage _hma;
	private decimal? _prevClose;
	private decimal _normalizedPrice;
	private decimal? _prevPrice;
	private decimal? _prevHma;
	private decimal? _stopPrice;

	/// <summary>
	/// Period of the return standard deviation.
	/// </summary>
	public int NormPeriod
	{
		get => _normPeriod.Value;
		set => _normPeriod.Value = value;
	}

	/// <summary>
	/// Hull moving average period.
	/// </summary>
	public int HmaPeriod
	{
		get => _hmaPeriod.Value;
		set => _hmaPeriod.Value = value;
	}

	/// <summary>
	/// Bars the Hull moving average is shifted back.
	/// </summary>
	public int HmaOffset
	{
		get => _hmaOffset.Value;
		set => _hmaOffset.Value = value;
	}

	/// <summary>
	/// ATR multiple for the stop.
	/// </summary>
	public decimal StopMultiple
	{
		get => _stopMultiple.Value;
		set => _stopMultiple.Value = value;
	}

	/// <summary>
	/// Candle type.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Constructor.
	/// </summary>
	public CrunchstersNormalisedTrendStrategy()
	{
		_normPeriod = Param(nameof(NormPeriod), 14)
			.SetRange(2, 10000)
			.SetDisplay("Norm Period", "Period of the return standard deviation", "Indicators");

		_hmaPeriod = Param(nameof(HmaPeriod), 100)
			.SetGreaterThanZero()
			.SetDisplay("HMA Period", "Hull moving average period", "Indicators");

		_hmaOffset = Param(nameof(HmaOffset), 0)
			.SetNotNegative()
			.SetDisplay("HMA Offset", "Bars the Hull moving average is shifted back", "Indicators");

		_stopMultiple = Param(nameof(StopMultiple), 1m)
			.SetNotNegative()
			.SetDisplay("Stop Multiple", "ATR multiple for the stop", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to use", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		ResetState();
		_returnDeviation = null;
		_hma = null;
	}

	private void ResetState()
	{
		_hmaHistory.Clear();
		_prevClose = null;
		_normalizedPrice = 0m;
		_prevPrice = null;
		_prevHma = null;
		_stopPrice = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		ResetState();

		var atr = new AverageTrueRange { Length = NormPeriod };
		_returnDeviation = new StandardDeviation { Length = NormPeriod };
		_hma = new HullMovingAverage { Length = HmaPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(atr, ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue atrValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var close = candle.ClosePrice;
		var prevClose = _prevClose;
		_prevClose = close;

		if (prevClose is not decimal lastClose || lastClose == 0)
			return;

		var ret = (close - lastClose) / lastClose;
		var deviationValue = _returnDeviation.Process(ret, candle.OpenTime, true);

		if (!deviationValue.IsFormed)
			return;

		var deviation = deviationValue.ToDecimal();
		if (deviation > 0)
			_normalizedPrice += ret / deviation;

		var price = _normalizedPrice;
		var hmaValue = _hma.Process(price, candle.OpenTime, true);

		if (!hmaValue.IsFormed)
			return;

		_hmaHistory.Add(hmaValue.ToDecimal());
		if (_hmaHistory.Count > HmaOffset + 1)
			_hmaHistory.RemoveAt(0);

		if (_hmaHistory.Count <= HmaOffset)
			return;

		var hma = _hmaHistory[0];
		var prevPrice = _prevPrice;
		var prevHma = _prevHma;
		_prevPrice = price;
		_prevHma = hma;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (Position > 0 && _stopPrice is decimal longStop && candle.LowPrice <= longStop)
		{
			SellMarket(Position);
			_stopPrice = null;
			return;
		}

		if (Position < 0 && _stopPrice is decimal shortStop && candle.HighPrice >= shortStop)
		{
			BuyMarket(-Position);
			_stopPrice = null;
			return;
		}

		if (prevPrice is not decimal pp || prevHma is not decimal ph || !atrValue.IsFormed)
			return;

		var stopDistance = atrValue.ToDecimal() * StopMultiple;

		if (pp <= ph && price > hma && Position <= 0)
		{
			BuyMarket(Volume + Math.Abs(Position));
			_stopPrice = StopMultiple > 0 ? close - stopDistance : null;
		}
		else if (pp >= ph && price < hma && Position >= 0)
		{
			SellMarket(Volume + Math.Abs(Position));
			_stopPrice = StopMultiple > 0 ? close + stopDistance : null;
		}
	}
}