Auf GitHub ansehen

MA Volume Strategie

MA Volume kombiniert einen gleitenden Durchschnitt als Trendfilter mit Volumenspitzen für das Einstiegs-Timing. Steigendes Volumen bei einem Preis über dem Durchschnitt signalisiert starke Akkumulation; fallendes Volumen unterhalb des Durchschnitts deutet auf Distribution hin.

Tests zeigen eine durchschnittliche jährliche Rendite von etwa 136%. Die Strategie funktioniert am besten auf dem Aktienmarkt.

Die Strategie handelt in Richtung des gleitenden Durchschnitts, wenn das Volumen zunimmt, und steigt aus, sobald das Volumen versiegt oder der Durchschnitt dreht.

Ein prozentualer Stop schützt vor plötzlichen Trendwenden.

Details

  • Einstiegskriterien: Indikatorsignal
  • Long/Short: Beide
  • Ausstiegskriterien: Stop-Loss oder entgegengesetztes Signal
  • Stops: Ja, prozentbasiert
  • Standardwerte:
    • CandleType = 15 minute
    • StopLossPercent = 2
    • MaPeriod = 20
    • VolumePeriod = 20
    • VolumeThreshold = 1.2 Volumen dehnt sich aus, wenn das Volumen einer Kerze VolumeThreshold mal den Durchschnitt der vorherigen VolumePeriod Kerzen übersteigt. Ohne Position eröffnet eine solche Kerze mit Schluss über einem steigenden SMA einen Long, mit Schluss unter einem fallenden SMA einen Short. Die Position schließt, wenn das Volumen unter seinen Durchschnitt fällt oder der SMA gegen sie dreht.
  • Filter:
    • Kategorie: Trendfolge
    • Richtung: Beide
    • Indikatoren: Moving Average, Volume
    • Stops: Ja
    • Komplexität: Mittel
    • Zeitrahmen: Intraday
    • Saisonalität: Nein
    • Neuronale Netze: Nein
    • Divergenz: Nein
    • Risikolevel: Mittel
using System;
using System.Collections.Generic;
using System.Linq;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// MA Volume strategy.
/// Volume expands when a candle's volume exceeds VolumeThreshold times the average of the previous VolumePeriod candles.
/// While flat, an expanding candle that closes above a rising MaPeriod SMA goes long and one that closes below a falling SMA goes short.
/// The position closes once volume dries up below its average or the SMA turns against it, and a percent stop limits the loss.
/// </summary>
public class MaVolumeStrategy : Strategy
{
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _volumePeriod;
	private readonly StrategyParam<decimal> _volumeThreshold;
	private readonly StrategyParam<decimal> _stopLossPercent;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _volumes = [];
	private decimal? _prevMa;

	/// <summary>
	/// Period of the trend SMA.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Previous candles the volume is averaged over.
	/// </summary>
	public int VolumePeriod
	{
		get => _volumePeriod.Value;
		set => _volumePeriod.Value = value;
	}

	/// <summary>
	/// How many times the average volume an expanding candle must exceed.
	/// </summary>
	public decimal VolumeThreshold
	{
		get => _volumeThreshold.Value;
		set => _volumeThreshold.Value = value;
	}

	/// <summary>
	/// Stop loss percentage from entry price.
	/// </summary>
	public decimal StopLossPercent
	{
		get => _stopLossPercent.Value;
		set => _stopLossPercent.Value = value;
	}

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

	/// <summary>
	/// Constructor.
	/// </summary>
	public MaVolumeStrategy()
	{
		_maPeriod = Param(nameof(MaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Period of the trend SMA", "Indicators");

		_volumePeriod = Param(nameof(VolumePeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Volume Period", "Previous candles the volume is averaged over", "Indicators");

		_volumeThreshold = Param(nameof(VolumeThreshold), 1.2m)
			.SetGreaterThanZero()
			.SetDisplay("Volume Threshold", "How many times the average volume an expanding candle must exceed", "Indicators");

		_stopLossPercent = Param(nameof(StopLossPercent), 2m)
			.SetNotNegative()
			.SetDisplay("Stop Loss %", "Stop loss percentage from entry price", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).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();
		_volumes.Clear();
		_prevMa = null;
	}

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

		_volumes.Clear();
		_prevMa = null;

		var sma = new SimpleMovingAverage { Length = MaPeriod };

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

		StartProtection(new Unit(), new Unit(StopLossPercent, UnitTypes.Percent), useMarketOrders: true, isLocalStop: true);

		// The stop has to see prices between candles, not only at their close.
		foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
		{
			var quotes = new Subscription(DataType.Level1, Security);
			quotes.MarketData.BuildField = field;
			SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
		}

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

	private void ObserveProtectionQuote(Level1ChangeMessage quote)
	{
		// The high-level handler activates native protection before this callback, also between signal bars.
	}

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

		// Expansion is measured against the candles before this one.
		var average = _volumes.Count == VolumePeriod ? _volumes.Average() : (decimal?)null;

		_volumes.Add(candle.TotalVolume);

		if (_volumes.Count > VolumePeriod)
			_volumes.RemoveAt(0);

		if (!smaValue.IsFormed)
			return;

		var ma = smaValue.GetValue<decimal>();
		var prevMa = _prevMa;
		_prevMa = ma;

		if (average is not decimal avgVolume || prevMa is not decimal lastMa)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var close = candle.ClosePrice;
		var volume = candle.TotalVolume;

		if (Position > 0)
		{
			if (volume < avgVolume || ma < lastMa)
				SellMarket(Position);
		}
		else if (Position < 0)
		{
			if (volume < avgVolume || ma > lastMa)
				BuyMarket(-Position);
		}
		else if (volume > avgVolume * VolumeThreshold)
		{
			if (close > ma && ma > lastMa)
				BuyMarket(Volume);
			else if (close < ma && ma < lastMa)
				SellMarket(Volume);
		}
	}
}