Открыть на GitHub

Стратегия Hull Ma Stochastic

Стратегия основана на скользящей средней Халла и осцилляторе Стохастик. Вход выполняется при смене направления HMA, когда Стохастик подтверждает перепроданность или перекупленность.

Тестирование показывает среднегодичную доходность около 94%. Стратегию лучше запускать на фондовом рынке.

Hull MA быстро показывает направление тренда. Стохастик ждёт откат или всплеск внутри этого тренда, чтобы открыть сделку.

Гибкий подход для тех, кто предпочитает плавные сигналы. Стопы по ATR ограничивают возможные потери.

Подробности

  • Условия входа:
    • Длинная: HullMA turning up && StochK < StochOversold
    • Короткая: HullMA turning down && StochK > StochOverbought
  • Long/Short: Оба
  • Условия выхода:
    • изменение направления Hull MA
  • Стопы: основаны на ATR через StopLossAtr
  • Параметры по умолчанию:
    • HmaPeriod = 9
    • StochPeriod = 14
    • StochK = 3
    • StochOversold = 20
    • StochOverbought = 80
    • CandleType = TimeSpan.FromMinutes(5).TimeFrame()
    • StopLossAtr = 2
    • AtrPeriod = 14 HMA разворачивается вверх, когда растёт после снижения, и вниз, когда снижается после роста; лонг закрывается при её снижении, шорт — при росте. %K — стохастик за StochPeriod свечей, сглаженный за StochK свечей, линия %D не участвует. Стоп стоит в StopLossAtr ATR (AtrPeriod) от цены закрытия при входе и проверяется по закрытиям свечей. Сигнал на вход против открытой позиции разворачивает её.
  • Фильтры:
    • Категория: Mean reversion
    • Направление: Оба
    • Индикаторы: Hull MA, Moving Average, Stochastic Oscillator
    • Стопы: Да
    • Сложность: Средняя
    • Таймфрейм: Среднесрочный
    • Сезонность: Нет
    • Нейросети: Нет
    • Дивергенция: Нет
    • Уровень риска: Средний
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>
/// Hull MA Stochastic strategy.
/// The Hull average turns up when it rises after falling and turns down when it falls after rising. A turn up with %K below StochOversold
/// goes long and a turn down with %K above StochOverbought goes short, reversing an opposite position. A long closes when the Hull average
/// falls and a short when it rises. %K is the stochastic over StochPeriod candles
/// smoothed over StochK candles. The stop lies StopLossAtr ATR from the entry close and is checked on candle closes.
/// </summary>
public class HullMaStochasticStrategy : Strategy
{
	private readonly StrategyParam<int> _hmaPeriod;
	private readonly StrategyParam<int> _stochPeriod;
	private readonly StrategyParam<int> _stochK;
	private readonly StrategyParam<decimal> _stochOversold;
	private readonly StrategyParam<decimal> _stochOverbought;
	private readonly StrategyParam<decimal> _stopLossAtr;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _prevHull;
	private decimal? _prevPrevHull;
	private decimal _stopPrice;

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

	/// <summary>
	/// Lookback period of the raw stochastic.
	/// </summary>
	public int StochPeriod
	{
		get => _stochPeriod.Value;
		set => _stochPeriod.Value = value;
	}

	/// <summary>
	/// Smoothing period of %K.
	/// </summary>
	public int StochK
	{
		get => _stochK.Value;
		set => _stochK.Value = value;
	}

	/// <summary>
	/// %K level for longs.
	/// </summary>
	public decimal StochOversold
	{
		get => _stochOversold.Value;
		set => _stochOversold.Value = value;
	}

	/// <summary>
	/// %K level for shorts.
	/// </summary>
	public decimal StochOverbought
	{
		get => _stochOverbought.Value;
		set => _stochOverbought.Value = value;
	}

	/// <summary>
	/// Stop distance from the entry in ATRs.
	/// </summary>
	public decimal StopLossAtr
	{
		get => _stopLossAtr.Value;
		set => _stopLossAtr.Value = value;
	}

	/// <summary>
	/// Period of the stop ATR.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

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

	/// <summary>
	/// Constructor.
	/// </summary>
	public HullMaStochasticStrategy()
	{
		_hmaPeriod = Param(nameof(HmaPeriod), 9)
			.SetGreaterThanZero()
			.SetDisplay("HMA Period", "Period of the Hull moving average", "Indicators");

		_stochPeriod = Param(nameof(StochPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic Period", "Lookback period of the raw stochastic", "Stochastic");

		_stochK = Param(nameof(StochK), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %K", "Smoothing period of %K", "Stochastic");

		_stochOversold = Param(nameof(StochOversold), 20m)
			.SetDisplay("Stochastic Oversold", "%K level for longs", "Stochastic");

		_stochOverbought = Param(nameof(StochOverbought), 80m)
			.SetDisplay("Stochastic Overbought", "%K level for shorts", "Stochastic");

		_stopLossAtr = Param(nameof(StopLossAtr), 2m)
			.SetNotNegative()
			.SetDisplay("Stop Loss ATR", "Stop distance from the entry in ATRs", "Risk");

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "Period of the stop ATR", "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();
		_prevHull = null;
		_prevPrevHull = null;
		_stopPrice = default;
	}

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

		_prevHull = null;
		_prevPrevHull = null;
		_stopPrice = default;

		var hull = new HullMovingAverage { Length = HmaPeriod };
		// The D line of the core oscillator is the smoothed %K.
		var stochastic = new StochasticOscillator
		{
			K = { Length = StochPeriod },
			D = { Length = StochK },
		};
		var atr = new AverageTrueRange { Length = AtrPeriod };

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

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

			var oscillators = CreateChartArea();
			if (oscillators != null)
			{
				DrawIndicator(oscillators, stochastic);
			}
		}
	}

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

		if (!hullValue.IsFormed || !stochasticValue.IsFormed || !atrValue.IsFormed)
			return;

		if (stochasticValue is not IStochasticOscillatorValue { D: decimal k })
			return;

		var hull = hullValue.GetValue<decimal>();
		var prevHull = _prevHull;
		var prevPrevHull = _prevPrevHull;
		_prevPrevHull = prevHull;
		_prevHull = hull;

		if (prevHull is not decimal last || prevPrevHull is not decimal beforeLast)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var atr = atrValue.GetValue<decimal>();
		var close = candle.ClosePrice;
		var rising = hull > last;
		var falling = hull < last;
		var turnsUp = rising && last < beforeLast;
		var turnsDown = falling && last > beforeLast;

		if (turnsUp && k < StochOversold && Position <= 0)
		{
			BuyMarket(Volume + Math.Abs(Position));
			_stopPrice = close - StopLossAtr * atr;
		}
		else if (turnsDown && k > StochOverbought && Position >= 0)
		{
			SellMarket(Volume + Math.Abs(Position));
			_stopPrice = close + StopLossAtr * atr;
		}
		else if (Position > 0 && (falling || (StopLossAtr > 0 && close <= _stopPrice)))
		{
			SellMarket(Position);
		}
		else if (Position < 0 && (rising || (StopLossAtr > 0 && close >= _stopPrice)))
		{
			BuyMarket(-Position);
		}
	}
}