Auf GitHub ansehen

MAMy Expert Strategie

Übersicht

  • Port des MetaTrader-5-"MAMy Expert"-Beraters von Victor Chebotariov auf die StockSharp-High-Level-Strategie-API.
  • Reproduziert den ursprünglichen benutzerdefinierten Indikator, der drei gleitende Durchschnitte verschiedener Preisquellen vergleicht (Eröffnung, Schluss, gewichteter Preis).
  • Arbeitet ausschließlich mit abgeschlossenen Kerzen und verwaltet höchstens eine Nettoposition gleichzeitig, was das Verhalten des MQL-Experten widerspiegelt.

Indikatorbasis

  • Die Strategie baut drei gleitende Durchschnitte mit derselben Länge und demselben Glättungsalgorithmus:
    • MA(close) – berechnet auf Kerzen-Schlusskursen.
    • MA(open) – berechnet auf Kerzen-Eröffnungskursen.
    • MA(weighted) – berechnet auf dem gewichteten Preis (High + Low + 2 × Close) / 4.
  • Der MaType-Parameter wählt den Mittelungsalgorithmus (Simple, Exponential, Smoothed oder Weighted LWMA) für alle drei Serien aus und entspricht den MODE_*-Optionen von MetaTrader.
  • Ein "Schlusspuffer" wird als Differenz MA(close) − MA(weighted) berechnet.
  • Ein potenzieller "Eröffnungspuffer" wird nur erzeugt, wenn die gleitenden Durchschnitte sich in einer Trendkonfiguration ausrichten:
    • Abwärts-Setup: Sowohl MA(close) als auch MA(weighted) fallen, die Schluss-MA bleibt unter der gewichteten MA, beide bleiben unter der Eröffnungs-MA, und der Schlusspuffer nimmt ab.
    • Aufwärts-Setup: Sowohl MA(close) als auch MA(weighted) steigen, die Schluss-MA bleibt über der gewichteten MA, beide bleiben über der Eröffnungs-MA, und der Schlusspuffer nimmt zu.
    • Wenn eines der Setups zutrifft, wird der Eröffnungspuffer zu (MA(weighted) − MA(open)) + (MA(close) − MA(weighted)); andernfalls wird er auf null zurückgesetzt.
  • Wenn ein frischer positiver Eröffnungspuffer von einem negativen Kreuz des Schlusspuffers begleitet wird, wird der Schlusspuffer auf null gezwungen, genau wie im ursprünglichen Indikatorcode.

Signallogik

  • Einstiegsbedingungen
    • Kaufen wenn der Eröffnungspuffer nach oben durch null kreuzt (vorher ≤ 0, aktuell > 0).
    • Verkaufen wenn der Eröffnungspuffer nach unten durch null kreuzt (vorher ≥ 0, aktuell < 0).
    • Einstiege werden nur berücksichtigt, wenn keine bestehende Position vorhanden ist.
  • Ausstiegsbedingungen
    • Long schließen wenn der Schlusspuffer unter null kreuzt (vorher ≥ 0, aktuell < 0).
    • Short schließen wenn der Schlusspuffer über null kreuzt (vorher ≤ 0, aktuell > 0).
    • Ausstiege werden vor neuen Einstiegen ausgewertet, daher hält die Strategie niemals gleichzeitig Long- und Short-Engagements.
  • Orders werden zu Markt mit dem konfigurierten TradeVolume ausgegeben. Schutzautomatisierung über StartProtection() spiegelt den Sicherheitsaufruf in den StockSharp-Beispielen wider.

Charting und Datenfluss

  • Abonniert den durch CandleType definierten Zeitrahmen und verarbeitet nur abgeschlossene Kerzen.
  • Zeichnet Preiskerzen zusammen mit allen drei gleitenden Durchschnitten und annotiert ausgeführte Orders, und liefert dieselben visuellen Hinweise, die der ursprüngliche Indikator in MetaTrader bereitstellte.

Parameter

Name Typ Standard Beschreibung
CandleType DataType TimeSpan.FromHours(1).TimeFrame() Primärer Zeitrahmen, der Kerzen für den Indikator und Signale liefert.
MaPeriod int 3 Länge für alle drei gleitenden Durchschnitte.
MaType MaCalculationType Weighted Mittelungsalgorithmus (Simple, Exponential, Smoothed, Weighted).
TradeVolume decimal 1 Volumen für jeden Marktorder-Einstieg.

Implementierungshinweise

  • Verwendet den High-Level-SubscribeCandles().Bind(...)-Workflow und eingebaute Moving-Average-Indikatoren von StockSharp; es werden keine benutzerdefinierten Puffer jenseits der für die Signaldetection benötigten letzten Werte gespeichert.
  • Signale werden erst ausgewertet, wenn alle Indikatoren vollständig gebildet sind und die Strategie bereit für den Live-Handel ist (IsFormedAndOnlineAndAllowTrading()).
  • Die Strategie ignoriert absichtlich gleichzeitige Einstiege, solange eine Position offen ist, und spiegelt dadurch die Logik des Quell-Experten-Beraters getreu wider.

namespace StockSharp.Samples.Strategies;

using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

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

using StockSharp.Algo;

public class MamyExpertStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<MaCalculationTypes> _maType;
	private readonly StrategyParam<decimal> _tradeVolume;

	private DecimalLengthIndicator _closeMa;
	private DecimalLengthIndicator _openMa;
	private DecimalLengthIndicator _weightedPriceMa;

	private decimal? _previousCloseMa;
	private decimal? _previousOpenMa;
	private decimal? _previousWeightedMa;
	private decimal? _previousOpenSignal;
	private decimal? _previousCloseSignal;

	public MamyExpertStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle type", "Primary timeframe used for price aggregation.", "General");

		_maPeriod = Param(nameof(MaPeriod), 3)
			.SetGreaterThanZero()
			.SetDisplay("MA period", "Length applied to all moving averages.", "Indicator");

		_maType = Param(nameof(MaType), MaCalculationTypes.Weighted)
			.SetDisplay("MA method", "Averaging algorithm applied to open/close/weighted prices.", "Indicator");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Volume", "Order volume submitted for entries.", "Trading");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	public MaCalculationTypes MaType
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_previousCloseMa = null;
		_previousOpenMa = null;
		_previousWeightedMa = null;
		_previousOpenSignal = null;
		_previousCloseSignal = null;
	}

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

		Volume = TradeVolume;
		StartProtection(null, null);

		_closeMa = CreateMovingAverage(MaType, MaPeriod);
		_openMa = CreateMovingAverage(MaType, MaPeriod);
		_weightedPriceMa = CreateMovingAverage(MaType, MaPeriod);

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

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

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

		if (_closeMa == null || _openMa == null || _weightedPriceMa == null)
			return;

		var closeMaResult = _closeMa.Process(new DecimalIndicatorValue(_closeMa, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
		var openMaResult = _openMa.Process(new DecimalIndicatorValue(_openMa, candle.OpenPrice, candle.OpenTime) { IsFinal = true });
		var weightedPrice = CalculateWeightedPrice(candle);
		var weightedMaResult = _weightedPriceMa.Process(new DecimalIndicatorValue(_weightedPriceMa, weightedPrice, candle.OpenTime) { IsFinal = true });

		if (closeMaResult.IsEmpty || openMaResult.IsEmpty || weightedMaResult.IsEmpty)
			return;

		var closeMaValue = closeMaResult.ToDecimal();
		var openMaValue = openMaResult.ToDecimal();
		var weightedMaValue = weightedMaResult.ToDecimal();

		var previousCloseMa = _previousCloseMa;
		var previousOpenMa = _previousOpenMa;
		var previousWeightedMa = _previousWeightedMa;
		var previousOpenSignal = _previousOpenSignal;
		var previousCloseSignal = _previousCloseSignal;

		_previousCloseMa = closeMaValue;
		_previousOpenMa = openMaValue;
		_previousWeightedMa = weightedMaValue;

		if (!_closeMa.IsFormed || !_openMa.IsFormed || !_weightedPriceMa.IsFormed)
		{
			_previousOpenSignal = null;
			_previousCloseSignal = null;
			return;
		}

		var closeSignal = closeMaValue - weightedMaValue;
		var openSignal = 0m;

		if (previousCloseMa.HasValue && previousOpenMa.HasValue && previousWeightedMa.HasValue && previousCloseSignal.HasValue)
		{
			var closeDecreasing = closeMaValue < previousCloseMa.Value &&
				weightedMaValue < previousWeightedMa.Value &&
				closeMaValue < weightedMaValue &&
				weightedMaValue < openMaValue &&
				previousWeightedMa.Value < previousOpenMa.Value &&
				closeSignal <= previousCloseSignal.Value;

			var closeIncreasing = closeMaValue > previousCloseMa.Value &&
				weightedMaValue > previousWeightedMa.Value &&
				closeMaValue > weightedMaValue &&
				weightedMaValue > openMaValue &&
				previousWeightedMa.Value > previousOpenMa.Value &&
				closeSignal >= previousCloseSignal.Value;

			if (closeDecreasing || closeIncreasing)
				openSignal = (weightedMaValue - openMaValue) + (closeMaValue - weightedMaValue);
		}

		if (previousOpenSignal.HasValue && previousCloseSignal.HasValue &&
			openSignal >= 0m &&
			openSignal > previousOpenSignal.Value &&
			closeSignal < 0m &&
			previousCloseSignal.Value >= 0m)
		{
			closeSignal = 0m;
		}

		var hasPreviousOpenSignal = previousOpenSignal.HasValue;
		var hasPreviousCloseSignal = previousCloseSignal.HasValue;

		var openBuy = hasPreviousOpenSignal && openSignal > 0m && previousOpenSignal.Value <= 0m;
		var openSell = hasPreviousOpenSignal && openSignal < 0m && previousOpenSignal.Value >= 0m;
		var closeBuy = hasPreviousCloseSignal && closeSignal < 0m && previousCloseSignal.Value >= 0m;
		var closeSell = hasPreviousCloseSignal && closeSignal > 0m && previousCloseSignal.Value <= 0m;

		_previousOpenSignal = openSignal;
		_previousCloseSignal = closeSignal;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (TradeVolume <= 0m)
			return;

		var longPosition = Position > 0m ? Position : 0m;
		var shortPosition = Position < 0m ? -Position : 0m;

		if (longPosition > 0m)
		{
			if (closeBuy)
				SellMarket(longPosition);
		}
		else if (shortPosition > 0m)
		{
			if (closeSell)
				BuyMarket(shortPosition);
		}
		else
		{
			if (openBuy)
				BuyMarket(TradeVolume);
			else if (openSell)
				SellMarket(TradeVolume);
		}
	}

	private static decimal CalculateWeightedPrice(ICandleMessage candle)
	{
		return (candle.HighPrice + candle.LowPrice + candle.ClosePrice * 2m) / 4m;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MaCalculationTypes type, int length)
	{
		return type switch
		{
			MaCalculationTypes.Simple => new SMA { Length = length },
			MaCalculationTypes.Exponential => new EMA { Length = length },
			MaCalculationTypes.Smoothed => new SmoothedMovingAverage { Length = length },
			_ => new WeightedMovingAverage { Length = length },
		};
	}

	public enum MaCalculationTypes
	{
		Simple,
		Exponential,
		Smoothed,
		Weighted
	}
}