Ver en GitHub

Estrategia MAMy Expert

Descripción general

  • Port del asesor de MetaTrader 5 "MAMy Expert" de Victor Chebotariov a la API de estrategia de alto nivel de StockSharp.
  • Reproduce el indicador personalizado original que compara tres medias móviles de diferentes fuentes de precio (apertura, cierre, precio ponderado).
  • Funciona estrictamente con velas completadas y gestiona como máximo una posición neta a la vez, reflejando el comportamiento del expert MQL.

Base del indicador

  • La estrategia construye tres medias móviles usando la misma longitud y algoritmo de suavizado:
    • MA(close) – calculada sobre los precios de cierre de velas.
    • MA(open) – calculada sobre los precios de apertura de velas.
    • MA(weighted) – calculada sobre el precio ponderado (High + Low + 2 × Close) / 4.
  • El parámetro MaType selecciona el algoritmo de promediado (Simple, Exponencial, Suavizado o LWMA Ponderado) para las tres series, coincidiendo con las opciones MODE_* de MetaTrader.
  • Un "búfer de cierre" se calcula como la diferencia MA(close) − MA(weighted).
  • Un "búfer de apertura" potencial se produce solo cuando las medias móviles se alinean en una configuración de tendencia:
    • Configuración bajista: tanto MA(close) como MA(weighted) caen, la MA de cierre permanece por debajo de la MA ponderada, ambas permanecen por debajo de la MA de apertura, y el búfer de cierre disminuye.
    • Configuración alcista: tanto MA(close) como MA(weighted) suben, la MA de cierre permanece por encima de la MA ponderada, ambas permanecen por encima de la MA de apertura, y el búfer de cierre aumenta.
    • Cuando cualquiera de las configuraciones es verdadera, el búfer de apertura se convierte en (MA(weighted) − MA(open)) + (MA(close) − MA(weighted)); de lo contrario se restablece a cero.
  • Si un nuevo búfer de apertura positivo acompaña un cruce negativo del búfer de cierre, el búfer de cierre se fuerza a cero, igual que en el código del indicador original.

Lógica de señales

  • Condiciones de entrada
    • Comprar cuando el búfer de apertura cruza hacia arriba por cero (anterior ≤ 0, actual > 0).
    • Vender cuando el búfer de apertura cruza hacia abajo por cero (anterior ≥ 0, actual < 0).
    • Las entradas se consideran solo cuando no hay posición existente.
  • Condiciones de salida
    • Cerrar largo cuando el búfer de cierre cruza por debajo de cero (anterior ≥ 0, actual < 0).
    • Cerrar corto cuando el búfer de cierre cruza por encima de cero (anterior ≤ 0, actual > 0).
    • Las salidas se evalúan antes que las nuevas entradas, por lo que la estrategia nunca mantiene exposición larga y corta simultánea.
  • Las órdenes se emiten al mercado usando el TradeVolume configurado. La automatización protectora mediante StartProtection() refleja la llamada de seguridad en los ejemplos de StockSharp.

Gráficos y flujo de datos

  • Se suscribe al marco temporal definido por CandleType y procesa solo velas terminadas.
  • Dibuja velas de precio junto con las tres medias móviles y anota órdenes ejecutadas, proporcionando las mismas señales visuales que el indicador original entregaba en MetaTrader.

Parámetros

Nombre Tipo Predeterminado Descripción
CandleType DataType TimeSpan.FromHours(1).TimeFrame() Marco temporal principal que suministra velas para el indicador y las señales.
MaPeriod int 3 Longitud aplicada a las tres medias móviles.
MaType MaCalculationType Weighted Algoritmo de promediado (Simple, Exponencial, Suavizado, Ponderado).
TradeVolume decimal 1 Volumen usado para cada entrada de orden de mercado.

Notas de implementación

  • Usa el flujo de trabajo de alto nivel SubscribeCandles().Bind(...) de StockSharp y los indicadores de media móvil integrados; no se almacenan búferes personalizados más allá de los últimos valores requeridos para la detección de señales.
  • Las señales se evalúan solo después de que todos los indicadores estén completamente formados y la estrategia esté lista para trading en vivo (IsFormedAndOnlineAndAllowTrading()).
  • La estrategia ignora intencionalmente las entradas concurrentes mientras una posición está abierta, coincidiendo fielmente con la lógica del asesor experto de origen.

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
	}
}