Ver no GitHub

Estratégia do sistema de posição média móvel

Visão geral

O Moving Average Position System é uma porta direta do consultor especialista MetaTrader 4 "MovingAveragePositionSystem.mq4". A estratégia monitora uma média móvel retrospectiva longa e reage aos cruzamentos de preços que ocorrem em velas concluídas. Ele oferece suporte à seleção manual de lotes e a uma rotina opcional de escalonamento de volume do tipo martingale que reage aos lucros e perdas acumulados expressos em MetaTrader pontos.

Lógica de negociação

  1. Detecção de sinal
    • O sistema calcula uma média móvel configurável (simples, exponencial, suavizada ou linear ponderada).
    • Quando o fechamento da vela finalizada mais recentemente cruza a média móvel na direção oposta do fechamento anterior, a estratégia abre uma nova posição.
    • É permitida apenas uma posição por sentido; se a estratégia já for longa, ela não aumentará a posição até que a atual seja fechada, e o mesmo se aplica às negociações curtas.
  2. Gerenciamento de posição
    • Se a vela que acabou de fechar ficar abaixo da média móvel enquanto uma posição longa estiver aberta, a posição será imediatamente fechada no mercado.
    • Se a vela fechar acima da média móvel enquanto uma posição curta estiver aberta, a venda será fechada.
    • Um take-profit estilo MetaTrader expresso em etapas de preço (pontos) pode ser ativado por meio dos parâmetros de estratégia. Caso contrário, as paradas são gerenciadas pelo cruzamento da média móvel.
  3. Gerenciamento de dinheiro
    • Quando o bloco martingale está habilitado, a estratégia acumula PnL realizado e flutuante em MetaTrader pontos para o ciclo atual.
    • Se as perdas acumuladas excederem o limite de perda configurado, o próximo volume de negociação será duplicado (embora nunca exceda o tamanho máximo do lote) e todas as posições abertas serão achatadas.
    • Quando os lucros acumulados excedem a meta de lucro configurada, o volume é redefinido para o tamanho do lote inicial e quaisquer posições abertas são fechadas para garantir ganhos.

Parâmetros

Parâmetro Descrição
MaType Método de cálculo de média móvel: Simples, Exponencial, Suavizado ou LinearWeighted. Espelha a entrada TypeMA do especialista original.
MaPeríodo Período de lookback para a média móvel (padrão 240).
MaShift Deslocamento para frente aplicado aos valores da média móvel antes de gerar sinais. Equivalente à entrada SdvigMA.
Tipo de vela Tipo de dados Candle usado para cálculos de sinal. O padrão é velas de período de 1 hora.
Volume inicial O volume usado antes da rotina martingale o modificar. Corresponde à entrada Lots.
Volume inicial Tamanho do lote base para o qual o martingale é redefinido após um ciclo lucrativo (StarLots).
Volume máximo Limite superior para o volume de negociação (MaxLots). A estratégia reduz para metade o volume de trabalho se este limite for excedido.
LossThresholdPips Limite de perda em MetaTrader pontos que aciona um evento de duplicação de volume (LossPips).
Limite de LucroPips Meta de lucro em pontos que redefine o volume de volta ao valor inicial (ProfitPips).
TakeProfitPips Distância fixa opcional de lucro em pontos aplicada por meio do auxiliar de proteção integrado (TakeProfit).
UsarGerenciamento de Dinheiro Ativa ou desativa a rotina de dimensionamento de posição tipo martingale (MM).

Notas de uso

  • Configure a estratégia com o mesmo símbolo e intervalo de tempo que foram usados em MetaTrader; o período padrão de 240 funciona bem com velas H1, replicando a configuração original.
  • Os limites de pontos pressupõem que o instrumento fornece um PriceStep e um StepPrice válidos. Para símbolos que não possuem esses metadados, pode ser necessário ajustar os limites manualmente.
  • Como o código original recalcula as margens antes de cada entrada, a porta executa uma etapa simplificada de normalização de volume que reduz pela metade o tamanho da negociação sempre que excede MaxVolume. Controles de risco adicionais podem ser adicionados por meio dos provedores de risco StockSharp padrão, se necessário.
  • Somente velas concluídas acionam entradas e saídas, espelhando a implementação MQL que verificou os valores Close[1] e Close[2] em cada nova barra.

Arquivos

  • CS/MovingAveragePositionSystemStrategy.cs – implementação em C# da lógica de negociação usando a estratégia de alto nível StockSharp API.
  • README.md – Documentação em inglês (este arquivo).
  • README_ru.md – Documentação russa.
  • README_zh.md – documentação chinesa.
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;
		}
	}
}