Ver no GitHub

Estratégia RRS emaranhada EA

Visão geral

A Estratégia RRS Tangled EA é uma versão StockSharp do MetaTrader 4 consultor especialista "RRS Tangled EA". O sistema original escolhe aleatoriamente a direção e o símbolo da negociação, ao mesmo tempo que limita o número de ordens simultâneas e protege o lucro flutuante através de trailing stops e limites de risco rigorosos. A versão convertida concentra-se no instrumento atualmente selecionado, reproduzindo a entrada aleatória, o rastreamento e o comportamento de gerenciamento de risco usando o StockSharp API de alto nível.

Lógica principal

  1. Assine a série de velas configuradas e aguarde as velas concluídas.
  2. Em cada barra:
    • Atualize os níveis de trailing stop para cestas longas e curtas existentes.
    • Verifique as distâncias de stop-loss e take-profit usando os máximos e mínimos das velas.
    • Avalie o lucro flutuante de todas as entradas abertas; feche tudo se ultrapassar o limite de dinheiro em risco.
    • Se a negociação for permitida, o spread for aceitável e o número de entradas estiver abaixo do limite, sorteie um número inteiro aleatório em [0, 3].
    • Abra um novo comprado quando o valor aleatório for 1, ou um novo vendido quando o valor for 2, usando um volume aleatório entre os limites configurados.
  3. Os trailing stops seguem o melhor bid/ask quando o preço se move pela distância de ativação, bloqueando os lucros se o preço retroceder pelo trailing gap.
  4. A gestão de risco pode funcionar na modalidade de dinheiro fixo ou como uma porcentagem do saldo da conta corrente. Quando a perda flutuante excede o valor configurado, todas as posições são achatadas imediatamente.

Parâmetros

Nome Descrição
MinVolume Limite inferior para o volume de negociação gerado aleatoriamente.
MaxVolume Limite superior para o volume de negociação aleatório.
TakeProfitPips Distância alvo em pips, aplicada ao preço médio de entrada da cesta.
StopLossPips Distância de parada protetora em pips, medida a partir do preço médio de entrada.
TrailingStartPips Distância de lucro necessária antes que a lógica final seja ativada.
TrailingGapPips Gap mantido entre o trailing stop e o melhor preço de compra/venda.
MaxSpreadPips Spread máximo permitido antes de abrir uma nova entrada aleatória.
MaxOpenTrades Número máximo de entradas simultâneas em ambas as direções.
RiskManagementMode Alterna entre o tratamento de risco de dinheiro fixo e percentual de saldo.
RiskAmount Quantidade de risco (moeda ou percentual) monitorado em relação ao PnL flutuante.
TradeComment Comentário opcional para escrituração contábil, mantido para compatibilidade com a fonte EA.
Notes Texto informativo exibido dentro da string de status da estratégia.
CandleType Série de velas usadas para tomada de decisões.

Diferenças da versão MQL

  • As negociações são executadas no instrumento atribuído à estratégia, em vez de selecionar aleatoriamente símbolos da observação de mercado MetaTrader. Isso mantém a implementação compatível com as estratégias de segurança única de StockSharp.
  • O gerenciamento de pedidos é realizado em cestas longas/curtas agregadas, refletindo como o EA original agrupava as posições com os mesmos números mágicos.
  • O controle de spread depende do melhor lance/venda mais recente da carteira de pedidos, em vez das chamadas MarketInfo de MetaTrader.

Notas de uso

  • Certifique-se de que o corretor ou simulador conectado forneça cotações de compra e venda para que os cálculos de spread e trailing permaneçam precisos.
  • Defina MinVolume e MaxVolume dentro da faixa de volume permitida do instrumento. A estratégia ajusta automaticamente o volume aleatório à etapa e aos limites de volume do símbolo.
  • A lógica de gerenciamento de risco fecha todas as negociações imediatamente quando a perda flutuante excede o limite configurado; nenhuma nova posição será aberta até a próxima vela.
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;

/// <summary>
/// Randomized hedging strategy converted from the MetaTrader "RRS Tangled EA" advisor.
/// </summary>
public class RrsTangledEaStrategy : Strategy
{
	/// <summary>
	/// Risk handling modes that mirror the original MetaTrader inputs.
	/// </summary>
	public enum RiskModes
	{
		/// <summary>
		/// Risk a fixed monetary amount.
		/// </summary>
		FixedMoney,
		/// <summary>
		/// Risk a percentage of the account balance.
		/// </summary>
		BalancePercentage,
	}

	private readonly StrategyParam<decimal> _minVolume;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _trailingStartPips;
	private readonly StrategyParam<decimal> _trailingGapPips;
	private readonly StrategyParam<decimal> _maxSpreadPips;
	private readonly StrategyParam<int> _maxOpenTrades;
	private readonly StrategyParam<RiskModes> _riskMode;
	private readonly StrategyParam<decimal> _riskAmount;
	private readonly StrategyParam<string> _tradeComment;
	private readonly StrategyParam<string> _notes;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<TradeEntry> _buyEntries = new();
	private readonly List<TradeEntry> _sellEntries = new();

	private int _tradeCounter;
	private decimal _point;
	private decimal? _buyTrailingStop;
	private decimal? _sellTrailingStop;
	private decimal? _lastSpread;
	private decimal _initialBalance;

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public RrsTangledEaStrategy()
	{
		_minVolume = Param(nameof(MinVolume), 0.01m)
			.SetDisplay("Minimum Volume", "Lower bound for random position sizing", "Money Management")
			.SetGreaterThanZero();

		_maxVolume = Param(nameof(MaxVolume), 0.50m)
			.SetDisplay("Maximum Volume", "Upper bound for random position sizing", "Money Management")
			.SetGreaterThanZero();

		_takeProfitPips = Param(nameof(TakeProfitPips), 50000m)
			.SetDisplay("Take Profit (pips)", "Distance in pips for profit targets", "Risk")
			.SetRange(0m, 10_000m);

		_stopLossPips = Param(nameof(StopLossPips), 50000m)
			.SetDisplay("Stop Loss (pips)", "Distance in pips for protective stops", "Risk")
			.SetRange(0m, 10_000m);

		_trailingStartPips = Param(nameof(TrailingStartPips), 50000m)
			.SetDisplay("Trailing Start (pips)", "Activation distance for the trailing logic", "Risk")
			.SetRange(0m, 10_000m);

		_trailingGapPips = Param(nameof(TrailingGapPips), 50m)
			.SetDisplay("Trailing Gap (pips)", "Gap maintained by the trailing stop", "Risk")
			.SetRange(0m, 10_000m);

		_maxSpreadPips = Param(nameof(MaxSpreadPips), 100m)
			.SetDisplay("Max Spread (pips)", "Maximum allowed spread before opening new trades", "Filters")
			.SetRange(0m, 10_000m);

		_maxOpenTrades = Param(nameof(MaxOpenTrades), 10)
			.SetDisplay("Max Open Trades", "Maximum simultaneous random entries", "General")
			.SetRange(1, 1000);

		_riskMode = Param(nameof(RiskManagementMode), RiskModes.BalancePercentage)
			.SetDisplay("Risk Mode", "Select fixed risk or balance percentage", "Risk");

		_riskAmount = Param(nameof(RiskAmount), 5m)
			.SetDisplay("Risk Amount", "Money risk (fixed or percentage)", "Risk")
			.SetGreaterThanZero();

		_tradeComment = Param(nameof(TradeComment), "RRS")
			.SetDisplay("Trade Comment", "Comment stored with each order", "General");

		_notes = Param(nameof(Notes), "Note For Your Reference")
			.SetDisplay("Notes", "Informational note shown in the status string", "General");

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

	/// <summary>
	/// Minimum random volume.
	/// </summary>
	public decimal MinVolume
	{
		get => _minVolume.Value;
		set => _minVolume.Value = value;
	}

	/// <summary>
	/// Maximum random volume.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Trailing start distance expressed in pips.
	/// </summary>
	public decimal TrailingStartPips
	{
		get => _trailingStartPips.Value;
		set => _trailingStartPips.Value = value;
	}

	/// <summary>
	/// Trailing gap distance expressed in pips.
	/// </summary>
	public decimal TrailingGapPips
	{
		get => _trailingGapPips.Value;
		set => _trailingGapPips.Value = value;
	}

	/// <summary>
	/// Maximum allowed spread in pips.
	/// </summary>
	public decimal MaxSpreadPips
	{
		get => _maxSpreadPips.Value;
		set => _maxSpreadPips.Value = value;
	}

	/// <summary>
	/// Maximum number of simultaneous open trades.
	/// </summary>
	public int MaxOpenTrades
	{
		get => _maxOpenTrades.Value;
		set => _maxOpenTrades.Value = value;
	}

	/// <summary>
	/// Risk handling mode.
	/// </summary>
	public RiskModes RiskManagementMode
	{
		get => _riskMode.Value;
		set => _riskMode.Value = value;
	}

	/// <summary>
	/// Risk amount (fixed money or percentage).
	/// </summary>
	public decimal RiskAmount
	{
		get => _riskAmount.Value;
		set => _riskAmount.Value = value;
	}

	/// <summary>
	/// Optional trade comment stored for reference.
	/// </summary>
	public string TradeComment
	{
		get => _tradeComment.Value;
		set => _tradeComment.Value = value;
	}

	/// <summary>
	/// Informational note displayed in the status string.
	/// </summary>
	public string Notes
	{
		get => _notes.Value;
		set => _notes.Value = value;
	}

	/// <summary>
	/// Candle data type used for processing.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_tradeCounter = 0;
		_buyEntries.Clear();
		_sellEntries.Clear();
		_buyTrailingStop = null;
		_sellTrailingStop = null;
		_lastSpread = null;
		_initialBalance = 0m;
		_point = 0m;
	}

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

		_point = GetPointValue();
		_initialBalance = GetCurrentBalance();

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

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

		StartProtection(null, null);
	}

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

		UpdateSpread();
		UpdateTrailing(candle);
		CheckStopsAndTargets(candle);

		var price = candle.ClosePrice;
		var floating = CalculateUnrealizedPnL(price);
		var riskLimit = CalculateRiskLimit();

		if (floating <= riskLimit && (_buyEntries.Count > 0 || _sellEntries.Count > 0))
		{
			CloseAllTrades();
			LogInfo($"Risk management triggered. Floating={floating:F2} Threshold={riskLimit:F2}");
			return;
		}

		UpdateStatus(price, floating);

		if (!IsSpreadAcceptable())
			return;

		if (Position != 0m)
			return;

		if (_tradeCounter % 2 == 0)
		{
			OpenBuy(price);
		}
		else
		{
			OpenSell(price);
		}
		_tradeCounter++;
	}

	private void UpdateSpread()
	{
		var bid = GetSecurityValue<decimal?>(Level1Fields.BestBidPrice);
		var ask = GetSecurityValue<decimal?>(Level1Fields.BestAskPrice);
		if (bid.HasValue && ask.HasValue)
			_lastSpread = ask.Value - bid.Value;
	}

	private void UpdateTrailing(ICandleMessage candle)
	{
		if (TrailingStartPips <= 0m || TrailingGapPips <= 0m)
		{
			_buyTrailingStop = null;
			_sellTrailingStop = null;
			return;
		}

		var bid = GetSecurityValue<decimal?>(Level1Fields.BestBidPrice) ?? candle.ClosePrice;
		var ask = GetSecurityValue<decimal?>(Level1Fields.BestAskPrice) ?? candle.ClosePrice;

		var startDistance = TrailingStartPips * _point;
		var gapDistance = TrailingGapPips * _point;

		if (_buyEntries.Count > 0)
		{
			var avgBuy = GetAveragePrice(_buyEntries);
			if (bid - avgBuy >= startDistance)
			{
				var desiredStop = bid - gapDistance;
				if (_buyTrailingStop == null || desiredStop > _buyTrailingStop.Value)
					_buyTrailingStop = desiredStop;

				if (_buyTrailingStop != null && bid <= _buyTrailingStop.Value)
					CloseBuys();
			}
		}
		else
		{
			_buyTrailingStop = null;
		}

		if (_sellEntries.Count > 0)
		{
			var avgSell = GetAveragePrice(_sellEntries);
			if (avgSell - ask >= startDistance)
			{
				var desiredStop = ask + gapDistance;
				if (_sellTrailingStop == null || desiredStop < _sellTrailingStop.Value)
					_sellTrailingStop = desiredStop;

				if (_sellTrailingStop != null && ask >= _sellTrailingStop.Value)
					CloseSells();
			}
		}
		else
		{
			_sellTrailingStop = null;
		}
	}

	private void CheckStopsAndTargets(ICandleMessage candle)
	{
		var stopDistance = StopLossPips * _point;
		var takeDistance = TakeProfitPips * _point;

		if (_buyEntries.Count > 0)
		{
			var avgBuy = GetAveragePrice(_buyEntries);
			if (StopLossPips > 0m && avgBuy - candle.LowPrice >= stopDistance)
			{
				CloseBuys();
			}
			else if (TakeProfitPips > 0m && candle.HighPrice - avgBuy >= takeDistance)
			{
				CloseBuys();
			}
		}
		else
		{
			_buyTrailingStop = null;
		}

		if (_sellEntries.Count > 0)
		{
			var avgSell = GetAveragePrice(_sellEntries);
			if (StopLossPips > 0m && candle.HighPrice - avgSell >= stopDistance)
			{
				CloseSells();
			}
			else if (TakeProfitPips > 0m && avgSell - candle.LowPrice >= takeDistance)
			{
				CloseSells();
			}
		}
		else
		{
			_sellTrailingStop = null;
		}
	}

	private void OpenBuy(decimal price)
	{
		var volume = Volume > 0m ? Volume : 1m;
		BuyMarket(volume);
		_buyEntries.Add(new TradeEntry(price, volume));
	}

	private void OpenSell(decimal price)
	{
		var volume = Volume > 0m ? Volume : 1m;
		SellMarket(volume);
		_sellEntries.Add(new TradeEntry(price, volume));
	}

	private bool IsSpreadAcceptable()
	{
		if (MaxSpreadPips <= 0m)
			return true;

		if (!_lastSpread.HasValue)
			return true;

		return _lastSpread.Value <= MaxSpreadPips * _point;
	}

	private void CloseAllTrades()
	{
		CloseBuys();
		CloseSells();
	}

	private void CloseBuys()
	{
		var total = GetTotalVolume(_buyEntries);
		if (total <= 0m)
			return;

		SellMarket(total);
		_buyEntries.Clear();
		_buyTrailingStop = null;
	}

	private void CloseSells()
	{
		var total = GetTotalVolume(_sellEntries);
		if (total <= 0m)
			return;

		BuyMarket(total);
		_sellEntries.Clear();
		_sellTrailingStop = null;
	}

	private decimal CalculateUnrealizedPnL(decimal price)
	{
		if (_point <= 0m)
			return 0m;

		var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 1m;
		decimal total = 0m;

		for (var i = 0; i < _buyEntries.Count; i++)
		{
			var entry = _buyEntries[i];
			var difference = price - entry.Price;
			var steps = difference / _point;
			total += steps * stepPrice * entry.Volume;
		}

		for (var i = 0; i < _sellEntries.Count; i++)
		{
			var entry = _sellEntries[i];
			var difference = entry.Price - price;
			var steps = difference / _point;
			total += steps * stepPrice * entry.Volume;
		}

		return total;
	}

	private decimal CalculateRiskLimit()
	{
		var mode = RiskManagementMode;
		var risk = Math.Abs(RiskAmount);

		return mode switch
		{
			RiskModes.BalancePercentage => -GetCurrentBalance() * risk / 100m,
			_ => -risk,
		};
	}

	private decimal GetCurrentBalance()
	{
		var portfolio = Portfolio;
		if ((portfolio?.CurrentValue ?? 0m) > 0m)
			return portfolio.CurrentValue.Value;

		if ((portfolio?.BeginValue ?? 0m) > 0m)
			return portfolio.BeginValue.Value;

		return _initialBalance;
	}

	private void UpdateStatus(decimal price, decimal floating)
	{
		var balance = GetCurrentBalance();
		var modeDescription = RiskManagementMode == RiskModes.BalancePercentage
			? $"Balance % ({RiskAmount:F2})"
			: $"Fixed ({RiskAmount:F2})";

		var spreadText = _lastSpread.HasValue ? (_lastSpread.Value / _point).ToString("F2") : "n/a";

		LogInfo($"Balance={balance:F2} FloatingPnL={floating:F2} Trades(Buy={_buyEntries.Count}, Sell={_sellEntries.Count}) " +
			$"Risk={modeDescription} Spread(pips)={spreadText} Notes={Notes}");
	}

	private decimal GetPointValue()
	{
		var point = Security?.PriceStep;
		if (point == null || point == 0m)
			return 0.0001m;

		return point.Value;
	}

	private static decimal GetTotalVolume(List<TradeEntry> entries)
	{
		decimal total = 0m;
		for (var i = 0; i < entries.Count; i++)
			total += entries[i].Volume;
		return total;
	}

	private static decimal GetAveragePrice(List<TradeEntry> entries)
	{
		decimal volume = 0m;
		decimal weighted = 0m;
		for (var i = 0; i < entries.Count; i++)
		{
			var entry = entries[i];
			volume += entry.Volume;
			weighted += entry.Price * entry.Volume;
		}

		return volume > 0m ? weighted / volume : 0m;
	}

	private readonly struct TradeEntry
	{
		public TradeEntry(decimal price, decimal volume)
		{
			Price = price;
			Volume = volume;
		}

		public decimal Price { get; }

		public decimal Volume { get; }
	}
}