Ver no GitHub

Estratégia de tendência de cesta vetorial (porta MT4)

Esta pasta contém a porta StockSharp de alto nível API do consultor especialista MetaTrader 4 Vector (script original: MQL/8305/Vector.mq4). A estratégia coordena até quatro pares forex principais – EURUSD (primário), GBPUSD, USDCHF e USDJPY – e os negocia na mesma direção quando aparece um alinhamento de média móvel suavizada compartilhada. A conversão mantém as ideias principais do Vector enquanto as adapta aos padrões idiomáticos StockSharp.

Lógica de negociação

  1. Médias móveis suavizadas (SMMA) – cada instrumento rastreia uma SMMA rápida (3 períodos) e lenta (7 períodos) calculada com base nos preços médios do período de negociação configurável (15 minutos por padrão).
  2. Filtro de tendência vetorial – as diferenças entre cada par rápido/lento são somadas. Uma soma positiva sinaliza uma dinâmica de alta sincronizada em toda a cesta, enquanto uma soma negativa implica uma pressão coletiva de baixa.
  3. Regras de entrada – a estratégia abre ou reverte posições com ordens de mercado somente quando:
    • A tendência da cesta é positiva e o SMMA rápido do instrumento permanece acima do SMMA lento (entrada longa).
    • A tendência da cesta é negativa e o SMMA rápido está abaixo do SMMA lento (entrada curta).
  4. Meta de pip da faixa H4 – para cada instrumento, uma assinatura de vela separada de 4 horas mede a faixa anterior. Um quinto desse intervalo (limitado a 13 pips) torna-se o objetivo de lucro por posição, refletindo a saída de pip fixo do código MT4.
  5. Global equity guard – limites de lucro e rebaixamento baseados em porcentagem (retirados das entradas originais PrcProfit e PrcLose) fecham todas as posições abertas uma vez acionadas.

Principais diferenças em relação ao original EA

  • As assinaturas de velas de alto nível e vinculação de indicadores de StockSharp substituem a pesquisa de baixo nível encontrada no MT4 (SubscribeCandles().Bind(...)).
  • A porta suporta instrumentos secundários opcionais: deixe os slots GBPUSD / USDCHF / USDJPY vazios para negociar apenas o título principal.
  • O tamanho dinâmico do lote vinculado à margem da conta MT4 foi substituído por um parâmetro BaseVolume limpo que é normalizado para VolumeStep, MinVolume e MaxVolume de cada título.
  • O gerenciamento comercial armazena preços de entrada por meio de retornos de chamada OnNewMyTrade, evitando pesquisas diretas de valores de indicadores não permitidas.

Parâmetros

Nome Padrão Descrição
CandleType TimeSpan.FromMinutes(15) Prazo utilizado para os cálculos do SMMA e verificações de entrada.
RangeCandleType TimeSpan.FromHours(4) Período de tempo mais alto usado para derivar o alvo do pip adaptativo.
SecondSecurity null Slot GBPUSD opcional (defina um Security antes de começar).
ThirdSecurity null Slot USDCHF opcional.
FourthSecurity null Slot USDJPY opcional.
BaseVolume 1 Volume de negociação solicitado por pedido, normalizado de acordo com os limites cambiais.
TakeProfitPercent 0.5 Ganho de capital global (em %) que desencadeia uma saída de todo o portfólio.
MaxDrawdownPercent 30 Redução máxima permitida do patrimônio (em%) antes do fechamento de todas as posições.

Notas de uso

  • Atribua o mesmo conector e portfólio a todos os títulos referenciados pelos parâmetros antes de iniciar a estratégia.
  • Certifique-se de que a fonte de dados forneça o período de negociação e o intervalo de tempo para todos os instrumentos.
  • Quando os títulos facultativos não são fornecidos, o cálculo vetorial se adapta automaticamente aos instrumentos disponíveis.
  • As saídas sempre acontecem com ordens de mercado para corresponder ao comportamento original do MT4.

Arquivos

  • CS/VectorStrategy.cs – implementação de C# seguindo as diretrizes de alto nível StockSharp.
  • README.md, README_ru.md, README_zh.md – documentação de estratégia em inglês, russo e chinês.
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Port of the MetaTrader 4 expert advisor "Vector".
/// The strategy trades up to four correlated forex pairs using smoothed moving averages.
/// </summary>
public class VectorBasketTrendStrategy : Strategy
{
	private readonly StrategyParam<Security> _secondSecurity;
	private readonly StrategyParam<Security> _thirdSecurity;
	private readonly StrategyParam<Security> _fourthSecurity;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<DataType> _rangeCandleType;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _takeProfitPercent;
	private readonly StrategyParam<decimal> _maxDrawdownPercent;

	private readonly Dictionary<Security, InstrumentContext> _contexts = new();
	private readonly Dictionary<Security, decimal> _lastPositions = new();

	private decimal _initialBalance;
	private bool _profitTargetTriggered;
	private bool _drawdownTriggered;

	/// <summary>
	/// Secondary instrument representing GBPUSD in the original script.
	/// </summary>
	public Security SecondSecurity
	{
		get => _secondSecurity.Value;
		set => _secondSecurity.Value = value;
	}

	/// <summary>
	/// Third instrument representing USDCHF in the original script.
	/// </summary>
	public Security ThirdSecurity
	{
		get => _thirdSecurity.Value;
		set => _thirdSecurity.Value = value;
	}

	/// <summary>
	/// Fourth instrument representing USDJPY in the original script.
	/// </summary>
	public Security FourthSecurity
	{
		get => _fourthSecurity.Value;
		set => _fourthSecurity.Value = value;
	}

	/// <summary>
	/// Trading candle type used to compute the smoothed moving averages.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Higher timeframe candle type that defines the profit target distance.
	/// </summary>
	public DataType RangeCandleType
	{
		get => _rangeCandleType.Value;
		set => _rangeCandleType.Value = value;
	}

	/// <summary>
	/// Base volume requested for each trade.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Percentage profit target that closes every open position.
	/// </summary>
	public decimal TakeProfitPercent
	{
		get => _takeProfitPercent.Value;
		set => _takeProfitPercent.Value = value;
	}

	/// <summary>
	/// Maximum tolerated equity drawdown expressed in percent.
	/// </summary>
	public decimal MaxDrawdownPercent
	{
		get => _maxDrawdownPercent.Value;
		set => _maxDrawdownPercent.Value = value;
	}

	/// <summary>
/// Initializes a new instance of the <see cref="VectorBasketTrendStrategy"/> class.
/// </summary>
public VectorBasketTrendStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for MA calculation", "General");

		_rangeCandleType = Param(nameof(RangeCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Range Candle Type", "Higher timeframe for pip target", "General");

		_secondSecurity = Param<Security>(nameof(SecondSecurity))
			.SetDisplay("Second Security", "Optional correlated instrument", "General");

		_thirdSecurity = Param<Security>(nameof(ThirdSecurity))
			.SetDisplay("Third Security", "Optional correlated instrument", "General");

		_fourthSecurity = Param<Security>(nameof(FourthSecurity))
			.SetDisplay("Fourth Security", "Optional correlated instrument", "General");

		_baseVolume = Param(nameof(BaseVolume), 1m)
			.SetDisplay("Base Volume", "Requested volume for each trade", "Trading");

		_takeProfitPercent = Param(nameof(TakeProfitPercent), 0.5m)
			.SetDisplay("Account Take Profit %", "Equity gain that forces a global exit", "Risk");

		_maxDrawdownPercent = Param(nameof(MaxDrawdownPercent), 30m)
			.SetDisplay("Max Drawdown %", "Equity loss that forces a global exit", "Risk");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		foreach (var security in EnumerateSecurities())
		{
			yield return (security, CandleType);
			yield return (security, RangeCandleType);
		}
	}

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

		_contexts.Clear();
		_lastPositions.Clear();
		_initialBalance = 0m;
		_profitTargetTriggered = false;
		_drawdownTriggered = false;
	}

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

		if (Security == null)
			throw new InvalidOperationException("Primary security is not specified.");

		_initialBalance = Portfolio?.CurrentValue ?? 0m;
		CreateContext(Security);
		CreateContext(SecondSecurity);
		CreateContext(ThirdSecurity);
		CreateContext(FourthSecurity);
	}

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

		var security = trade.Order.Security;
		if (security == null)
			return;

		if (!_contexts.TryGetValue(security, out var context))
			return;

		var position = GetPositionVolume(security);
		_lastPositions.TryGetValue(security, out var previousPosition);
		var tradeVolume = trade.Trade.Volume;

		if (previousPosition == 0m && position != 0m)
		{
			context.EntryPrice = trade.Trade.Price;
		}
		else if (position == 0m)
		{
			context.EntryPrice = null;
		}
		else if (Math.Sign((double)previousPosition) != Math.Sign((double)position))
		{
			context.EntryPrice = trade.Trade.Price;
		}
		else if (position > 0m && trade.Order.Side == Sides.Buy)
		{
			UpdateAverageEntry(context, previousPosition, position, tradeVolume, trade.Trade.Price);
		}
		else if (position < 0m && trade.Order.Side == Sides.Sell)
		{
			UpdateAverageEntry(context, Math.Abs(previousPosition), Math.Abs(position), tradeVolume, trade.Trade.Price);
		}

		_lastPositions[security] = position;
	}

	private void CreateContext(Security security)
	{
		if (security == null)
			return;

		if (_contexts.ContainsKey(security))
			return;

		var fast = new SmoothedMovingAverage { Length = 3 };
		var slow = new SmoothedMovingAverage { Length = 7 };
		var context = new InstrumentContext(security, fast, slow, GetPipSize(security));
		_contexts.Add(security, context);
		_lastPositions[security] = GetPositionVolume(security);

		var subscription = SubscribeCandles(CandleType, security: security);
		subscription
			.Bind(fast, slow, (candle, fastValue, slowValue) => ProcessInstrumentCandle(context, candle, fastValue, slowValue))
			.Start();

		var rangeSubscription = SubscribeCandles(RangeCandleType, security: security);
		rangeSubscription
			.Bind(candle => ProcessRangeCandle(context, candle))
			.Start();
	}

	private void ProcessInstrumentCandle(InstrumentContext context, ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		context.FastValue = fastValue;
		context.SlowValue = slowValue;
		context.LastClose = candle.ClosePrice;

		EvaluateExits(context);
		EvaluateEntries();
		CheckGlobalRisk();
	}

	private void ProcessRangeCandle(InstrumentContext context, ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var range = candle.HighPrice - candle.LowPrice;
		if (range <= 0m || context.PipSize <= 0m)
		{
			context.TargetDistance = 0m;
			return;
		}

		var pipRange = range / context.PipSize;
		var targetPips = Math.Min(13m, pipRange / 5m);
		context.TargetDistance = targetPips * context.PipSize;
	}

	private void EvaluateEntries()
	{
		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var totalTrend = 0m;
		var readyPairs = 0;

		foreach (var context in _contexts.Values)
		{
			if (context.FastValue is decimal fast && context.SlowValue is decimal slow)
			{
				totalTrend += fast - slow;
				readyPairs++;
			}
		}

		if (readyPairs == 0 || totalTrend == 0m)
			return;

		var bullish = totalTrend > 0m;

		foreach (var context in _contexts.Values)
		{
			if (context.FastValue is not decimal fast || context.SlowValue is not decimal slow)
				continue;

			var position = GetPositionVolume(context.Security);
			if (bullish)
			{
				if (fast > slow && position <= 0m)
					TryEnter(context, Sides.Buy, Math.Abs(position));
			}
			else
			{
				if (fast < slow && position >= 0m)
					TryEnter(context, Sides.Sell, Math.Abs(position));
			}
		}
	}

	private void EvaluateExits(InstrumentContext context)
	{
		if (context.EntryPrice is null || context.TargetDistance <= 0m || context.LastClose is null)
			return;

		var position = GetPositionVolume(context.Security);
		if (position == 0m)
			return;

		var entryPrice = context.EntryPrice.Value;
		var lastClose = context.LastClose.Value;
		var profit = lastClose - entryPrice;

		if (position > 0m && profit >= context.TargetDistance)
		{
			SellMarket(position, context.Security);
		}
		else if (position < 0m && -profit >= context.TargetDistance)
		{
			BuyMarket(Math.Abs(position), context.Security);
		}
	}

	private void TryEnter(InstrumentContext context, Sides side, decimal currentPosition)
	{
		var volume = NormalizeVolume(context.Security, BaseVolume);
		if (volume <= 0m)
			return;

		if (side == Sides.Buy)
		{
			if (currentPosition > 0m)
			{
				SellMarket(currentPosition, context.Security);
			}
			BuyMarket(volume, context.Security);
		}
		else
		{
			if (currentPosition > 0m)
			{
				BuyMarket(currentPosition, context.Security);
			}
			SellMarket(volume, context.Security);
		}
	}

	private decimal NormalizeVolume(Security security, decimal requestedVolume)
	{
		var step = security.VolumeStep ?? 0m;
		var min = security.MinVolume ?? 0m;
		var max = security.MaxVolume;
		var volume = requestedVolume;

		if (step > 0m)
			volume = Math.Round(volume / step) * step;

		if (min > 0m && volume < min)
			volume = min;

		if (max != null && volume > max.Value)
			volume = max.Value;

		return volume;
	}

	private void CheckGlobalRisk()
	{
		if (Portfolio?.CurrentValue is not decimal currentValue || _initialBalance <= 0m)
			return;

		var profit = currentValue - _initialBalance;
		var profitThreshold = _initialBalance * (TakeProfitPercent / 100m);
		var lossThreshold = _initialBalance * (MaxDrawdownPercent / 100m);

		if (!_profitTargetTriggered && profitThreshold > 0m && profit >= profitThreshold)
		{
			_profitTargetTriggered = true;
			CloseAllPositions("Global profit target reached");
		}

		if (!_drawdownTriggered && lossThreshold > 0m && -profit >= lossThreshold)
		{
			_drawdownTriggered = true;
			CloseAllPositions("Global drawdown limit reached");
		}
	}

	private void CloseAllPositions(string reason)
	{
		foreach (var context in _contexts.Values)
		{
			var position = GetPositionVolume(context.Security);
			if (position > 0m)
			{
				SellMarket(position, context.Security);
			}
			else if (position < 0m)
			{
				BuyMarket(Math.Abs(position), context.Security);
			}
		}

		LogInfo(reason);
	}

	private void UpdateAverageEntry(InstrumentContext context, decimal previousPosition, decimal currentPosition, decimal tradeVolume, decimal tradePrice)
	{
		if (tradeVolume <= 0m || previousPosition <= 0m || currentPosition <= 0m)
		{
			context.EntryPrice ??= tradePrice;
			return;
		}

		if (context.EntryPrice is decimal existing)
		{
			context.EntryPrice = ((existing * previousPosition) + (tradePrice * tradeVolume)) / currentPosition;
		}
		else
		{
			context.EntryPrice = tradePrice;
		}
	}

	private IEnumerable<Security> EnumerateSecurities()
	{
		if (Security != null)
			yield return Security;
		if (SecondSecurity != null)
			yield return SecondSecurity;
		if (ThirdSecurity != null)
			yield return ThirdSecurity;
		if (FourthSecurity != null)
			yield return FourthSecurity;
	}

	private decimal GetPositionVolume(Security security)
	{
		return GetPositionValue(security, Portfolio) ?? 0m;
	}

	private static decimal GetPipSize(Security security)
	{
		var step = security.PriceStep;
		if (step == null || step == 0m)
			return 0.0001m;

		return step.Value;
	}

	private sealed class InstrumentContext
	{
		public InstrumentContext(Security security, SmoothedMovingAverage fast, SmoothedMovingAverage slow, decimal pipSize)
		{
			Security = security;
			Fast = fast;
			Slow = slow;
			PipSize = pipSize;
		}

		public Security Security { get; }
		public SmoothedMovingAverage Fast { get; }
		public SmoothedMovingAverage Slow { get; }
		public decimal PipSize { get; }
		public decimal? FastValue { get; set; }
		public decimal? SlowValue { get; set; }
		public decimal TargetDistance { get; set; }
		public decimal? EntryPrice { get; set; }
		public decimal? LastClose { get; set; }
	}
}