Ver en GitHub

Estrategia del sistema de posición de media móvil

Descripción general

El sistema de posición de media móvil es un puerto directo del asesor experto MetaTrader 4 "MovingAveragePositionSystem.mq4". La estrategia monitorea una media móvil retrospectiva larga y reacciona a los cruces de precios que ocurren en velas completadas. Admite tanto la selección manual de lotes como una rutina opcional de aumento de volumen similar a una martingala que reacciona a las ganancias y pérdidas acumuladas expresadas en MetaTrader puntos.

Lógica de trading

  1. Detección de señal
    • El sistema calcula una media móvil configurable (simple, exponencial, suavizada o lineal ponderada).
    • Cuando el cierre de la vela finalizada más recientemente cruza la media móvil en la dirección opuesta al cierre anterior, la estrategia abre una nueva posición.
    • Sólo se permite una posición por dirección; si la estrategia ya es larga, no aumentará la posición hasta que se cierre la actual, y lo mismo se aplica a las operaciones cortas.
  2. Gestión de posiciones
    • Si la vela que acaba de cerrar termina por debajo de la media móvil mientras una posición larga está abierta, la posición se cierra inmediatamente en el mercado.
    • Si la vela vuelve a cerrar por encima de la media móvil mientras hay una posición corta abierta, la venta corta se cierra.
    • Se puede activar una toma de ganancias estilo MetaTrader expresada en pasos de precio (puntos) a través de los parámetros de la estrategia. Por lo demás, las paradas son gestionadas por el cruce de media móvil.
  3. Gestión del dinero
    • Cuando el bloque martingala está habilitado, la estrategia acumula PnL realizado y flotante en MetaTrader puntos para el ciclo actual.
    • Si las pérdidas acumuladas exceden el umbral de pérdida configurado, el siguiente volumen comercial se duplica (sin exceder nunca el tamaño máximo de lote) y todas las posiciones abiertas se nivelan.
    • Cuando las ganancias acumuladas exceden el objetivo de ganancias configurado, el volumen se restablece al tamaño del lote inicial y cualquier posición abierta se cierra para asegurar las ganancias.

Parámetros

Parámetro Descripción
MaType Método de cálculo de la media móvil: simple, exponencial, suavizado o ponderado lineal. Refleja la entrada TypeMA del experto original.
MaPeriodo Período retroactivo para la media móvil (predeterminado 240).
MaShift Desplazamiento hacia adelante aplicado a los valores de media móvil antes de generar señales. Equivalente a la entrada SdvigMA.
Tipo de vela Tipo de datos de vela utilizado para cálculos de señales. El valor predeterminado es velas de marco de tiempo de 1 hora.
Volumen inicial Volumen utilizado antes de que la rutina de martingala lo modifique. Corresponde a la entrada Lots.
Volumen inicial Tamaño de lote base al que se restablece la martingala después de un ciclo rentable (StarLots).
Volumen máximo Límite superior para el volumen comercial (MaxLots). La estrategia reduce a la mitad el volumen de trabajo si se excede este límite.
PérdidaUmbralPips Umbral de pérdida en MetaTrader puntos que desencadena un evento de duplicación de volumen (LossPips).
Pips de umbral de beneficio Objetivo de beneficio en puntos que restablece el volumen al valor inicial (ProfitPips).
TakeProfitPips Distancia de obtención de beneficios fija opcional en puntos aplicada a través del asistente de protección integrado (TakeProfit).
UsarAdministración de Dinero Activa o desactiva la rutina de dimensionamiento de posiciones tipo martingala (MM).

Notas de uso

  • Configurar la estrategia con el mismo símbolo y plazo que se utilizaron en MetaTrader; el período predeterminado de 240 funciona bien con velas H1, replicando la configuración original.
  • Los umbrales de puntos suponen que el instrumento proporciona un PriceStep y un StepPrice válidos. Para los símbolos que carecen de estos metadatos, es posible que deba ajustar los umbrales manualmente.
  • Debido a que el código original recalcula los márgenes antes de cada entrada, el puerto realiza un paso de normalización de volumen simplificado que reduce a la mitad el tamaño de la negociación cada vez que excede MaxVolume. Se pueden agregar controles de riesgo adicionales a través de los proveedores de riesgos estándar StockSharp si es necesario.
  • Solo las velas completadas activan entradas y salidas, reflejando la implementación de MQL que verificó los valores de Close[1] y Close[2] en cada nueva barra.

Archivos

  • CS/MovingAveragePositionSystemStrategy.cs: implementación en C# de la lógica comercial utilizando la StockSharp estrategia de alto nivel API.
  • README.md – Documentación en inglés (este archivo).
  • README_ru.md – Documentación rusa.
  • README_zh.md – Documentación china.
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Port of the FORTRADER MovingAveragePositionSystem expert advisor.
/// The strategy opens or closes positions on moving average crossings and optionally applies
/// a martingale-like position sizing routine based on cumulative results expressed in MetaTrader points.
/// </summary>
public class MovingAveragePositionSystemStrategy : Strategy
{
	/// <summary>
	/// Moving average calculation mode.
	/// </summary>
	public enum MovingAverageModes
	{
		Simple,
		Exponential,
		Smoothed,
		LinearWeighted,
	}

	private readonly StrategyParam<MovingAverageModes> _maType;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _maShift;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<decimal> _startVolume;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _lossThresholdPips;
	private readonly StrategyParam<decimal> _profitThresholdPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<bool> _useMoneyManagement;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closeHistory = new();
	private readonly List<decimal> _maHistory = new();

	private decimal _currentVolume;
	private decimal _cycleStartRealizedPnL;
	private decimal _priceStep;
	private decimal _stepPrice;
	private decimal _entryPrice;

	/// <summary>
	/// Moving average type used for signal calculation.
	/// </summary>
	public MovingAverageModes MaType
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	/// <summary>
	/// Moving average length.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Forward shift applied to the moving average before generating signals.
	/// </summary>
	public int MaShift
	{
		get => _maShift.Value;
		set => _maShift.Value = value;
	}

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

	/// <summary>
	/// Initial lot size before the martingale routine modifies it.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Base lot size restored after profitable cycles.
	/// </summary>
	public decimal StartVolume
	{
		get => _startVolume.Value;
		set => _startVolume.Value = value;
	}

	/// <summary>
	/// Maximum allowed lot size.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Loss threshold in MetaTrader points that doubles the next trade volume.
	/// </summary>
	public decimal LossThresholdPips
	{
		get => _lossThresholdPips.Value;
		set => _lossThresholdPips.Value = value;
	}

	/// <summary>
	/// Profit target in MetaTrader points that resets the volume to the starting lot.
	/// </summary>
	public decimal ProfitThresholdPips
	{
		get => _profitThresholdPips.Value;
		set => _profitThresholdPips.Value = value;
	}

	/// <summary>
	/// Fixed take profit distance in MetaTrader points.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Enables the martingale-style money management block.
	/// </summary>
	public bool UseMoneyManagement
	{
		get => _useMoneyManagement.Value;
		set => _useMoneyManagement.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="MovingAveragePositionSystemStrategy"/> class.
	/// </summary>
	public MovingAveragePositionSystemStrategy()
	{
		_maType = Param(nameof(MaType), MovingAverageModes.LinearWeighted)
		.SetDisplay("MA Type", "Moving average method", "Indicators");

		_maPeriod = Param(nameof(MaPeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("MA Period", "Moving average length", "Indicators");

		_maShift = Param(nameof(MaShift), 0)
		.SetRange(0, 100)
		.SetDisplay("MA Shift", "Forward shift for the moving average", "Indicators");

		_initialVolume = Param(nameof(InitialVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Initial Volume", "Starting lot size", "Trading");

		_startVolume = Param(nameof(StartVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Start Volume", "Base lot restored after profits", "Trading");

		_maxVolume = Param(nameof(MaxVolume), 10m)
		.SetGreaterThanZero()
		.SetDisplay("Max Volume", "Maximum allowed lot size", "Trading");

		_lossThresholdPips = Param(nameof(LossThresholdPips), 90m)
		.SetGreaterThanZero()
		.SetDisplay("Loss Threshold (pts)", "Loss in points that doubles the lot", "Risk");

		_profitThresholdPips = Param(nameof(ProfitThresholdPips), 170m)
		.SetGreaterThanZero()
		.SetDisplay("Profit Target (pts)", "Profit in points that resets the lot", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 1000m)
		.SetNotNegative()
		.SetDisplay("Take Profit (pts)", "Fixed take profit distance", "Risk");

		_useMoneyManagement = Param(nameof(UseMoneyManagement), true)
		.SetDisplay("Use Money Management", "Enable martingale volume control", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Candles used for calculations", "Market Data");
	}

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

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

		_closeHistory.Clear();
		_maHistory.Clear();
		_currentVolume = InitialVolume;
		Volume = _currentVolume;
		_cycleStartRealizedPnL = PnLManager?.RealizedPnL ?? 0m;
		_priceStep = 0m;
		_stepPrice = 0m;
		_entryPrice = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		_currentVolume = InitialVolume;
		Volume = _currentVolume;
		_cycleStartRealizedPnL = PnLManager?.RealizedPnL ?? 0m;

		_priceStep = Security?.PriceStep ?? 1m;
		_stepPrice = _priceStep;

		var movingAverage = CreateMovingAverage();

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

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

		var takeProfitUnit = TakeProfitPips > 0m ? new Unit(TakeProfitPips, UnitTypes.Absolute) : null;
		StartProtection(takeProfitUnit, null);

		base.OnStarted2(time);
	}

	private DecimalLengthIndicator CreateMovingAverage()
	{
		return MaType switch
		{
			MovingAverageModes.Exponential => new EMA { Length = MaPeriod },
			MovingAverageModes.Smoothed => new SmoothedMovingAverage { Length = MaPeriod },
			MovingAverageModes.LinearWeighted => new WeightedMovingAverage { Length = MaPeriod },
			_ => new SMA { Length = MaPeriod },
		};
	}

	private void ProcessCandle(ICandleMessage candle, decimal maValue)
	{
		// Work only with finished candles to reproduce the MQL4 behaviour.
		if (candle.State != CandleStates.Finished)
		return;

		var canTrade = IsFormedAndOnlineAndAllowTrading();

		var previousClose = _closeHistory.Count >= 1 ? _closeHistory[^1] : (decimal?)null;
		var previousPreviousClose = _closeHistory.Count >= 2 ? _closeHistory[^2] : (decimal?)null;

		decimal? shiftedMa = null;
		if (_maHistory.Count > MaShift)
		{
			var index = _maHistory.Count - 1 - MaShift;
			if (index >= 0)
			shiftedMa = _maHistory[index];
		}

		if (previousClose.HasValue && previousPreviousClose.HasValue && shiftedMa.HasValue)
		{
			// Manage existing positions based on the opposite crossing.
			ManageOpenPosition(previousClose.Value, shiftedMa.Value);

			// Update the working volume according to the martingale routine.
			UpdateVolume(previousClose.Value, shiftedMa.Value);

			if (canTrade)
			{
				TryEnter(previousClose.Value, previousPreviousClose.Value, shiftedMa.Value);
			}
		}

		// Store the latest values for the next iteration.
		_closeHistory.Add(candle.ClosePrice);
		_maHistory.Add(maValue);
	}

	private void ManageOpenPosition(decimal previousClose, decimal shiftedMa)
	{
		// Close long positions when the latest closed candle falls back below the moving average.
		if (Position > 0 && previousClose < shiftedMa)
		{
			if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
			return;
		}

		// Close short positions when the latest closed candle climbs back above the average.
		if (Position < 0 && previousClose > shiftedMa)
		{
			if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
		}
	}

	private void UpdateVolume(decimal previousClose, decimal shiftedMa)
	{
		if (!UseMoneyManagement)
		return;

		var realizedPnL = PnLManager?.RealizedPnL ?? 0m;
		var realizedDiff = realizedPnL - _cycleStartRealizedPnL;

		var stepPrice = _stepPrice != 0m ? _stepPrice : GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 1m;
		var priceStep = _priceStep != 0m ? _priceStep : Security?.PriceStep ?? 1m;

		var resultInSteps = stepPrice != 0m ? realizedDiff / stepPrice : 0m;

		if (Position != 0 && priceStep > 0m && _entryPrice > 0m)
		{
			// Consider only floating losses as in the original script.
			var diff = Position > 0
			? previousClose - _entryPrice
			: _entryPrice - previousClose;

			if (diff < 0m)
			{
				resultInSteps += diff / priceStep;
			}
		}

		if (resultInSteps <= -LossThresholdPips)
		{
			// Double the lot size while keeping it within the maximum allowed range.
			var newVolume = Math.Min(_currentVolume * 2m, MaxVolume);
			if (newVolume > 0m)
			{
				_currentVolume = newVolume;
				NormalizeVolume();
				Volume = _currentVolume;
			}

			if (Position != 0)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
			}

			_cycleStartRealizedPnL = realizedPnL;
		}
		else if (resultInSteps >= ProfitThresholdPips)
		{
			// Reset the lot size to the configured starting volume and lock in profits.
			_currentVolume = StartVolume;
			NormalizeVolume();
			Volume = _currentVolume;

			if (Position != 0)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(Math.Abs(Position));
			}

			_cycleStartRealizedPnL = realizedPnL;
		}
		else
		{
			NormalizeVolume();
		}
	}

	private void TryEnter(decimal previousClose, decimal previousPreviousClose, decimal shiftedMa)
	{
		NormalizeVolume();

		if (_currentVolume <= 0m)
		return;

		// Detect upward crossing: price moved from below the moving average to above it.
		var crossedUp = previousClose > shiftedMa && previousPreviousClose < shiftedMa;
		if (crossedUp && Position <= 0)
		{
			BuyMarket(_currentVolume);
			return;
		}

		// Detect downward crossing: price moved from above the moving average to below it.
		var crossedDown = previousClose < shiftedMa && previousPreviousClose > shiftedMa;
		if (crossedDown && Position >= 0)
		{
			SellMarket(_currentVolume);
		}
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;

		if (Position == 0m)
			_entryPrice = 0m;
	}

	private void NormalizeVolume()
	{
		// Reduce the working lot if it exceeds the maximum allowed size.
		while (_currentVolume > MaxVolume && _currentVolume > 0m)
		{
			_currentVolume /= 2m;
		}

		if (Portfolio is not null)
		{
			var portfolioValue = Portfolio.CurrentValue ?? Portfolio.BeginValue ?? 0m;
			var marginThreshold = 1000m * _currentVolume;

			while (_currentVolume > 0m && portfolioValue < marginThreshold)
			{
				_currentVolume /= 2m;
				marginThreshold = 1000m * _currentVolume;
			}
		}

		if (_currentVolume < 0m)
		{
			_currentVolume = 0m;
		}
	}
}