Ver no GitHub

Estratégia de rótulos de lucro

Visão geral

A Estratégia de rótulos de lucro converte o MetaTrader 5 consultor especialista Rótulos de lucro (54352) em StockSharp API de alto nível. A estratégia monitora os cruzamentos da média móvel exponencial tripla (TEMA) para abrir posições e desenha rótulos de lucro no gráfico após o fechamento de uma posição. Quando a tendência sobe, o algoritmo abre uma posição longa e, quando a tendência cai, ele abre uma posição curta. Se uma posição oposta ainda estiver ativa, a estratégia primeiro a fecha e imprime a etiqueta de lucro realizado.

As velas são processadas por meio de uma assinatura SubscribeCandles, e o indicador é vinculado por meio de Bind para manter a implementação totalmente de alto nível. As velas finalizadas atualizam os valores TEMA e acionam decisões de negociação.

Regras de negociação

  1. Cruzamento de alta: quando o valor TEMA atual se move acima do valor anterior enquanto as leituras mais antigas mostram uma inclinação descendente, a estratégia abre uma posição longa se nenhuma posição longa estiver ativa no momento.
  2. Cruzamento de baixa: quando o TEMA cai da mesma maneira, ele abre uma posição curta se nenhuma venda estiver ativa.
  3. Reversão de posição: se existir uma posição oposta no momento de um novo sinal, a estratégia fecha a posição aberta antes de colocar uma nova ordem.
  4. Rótulos de lucro: assim que a posição for totalmente fechada, o PnL realizado é calculado e exibido no gráfico usando DrawText.

Parâmetros

Nome Padrão Descrição
CandleType TimeSpan.FromMinutes(1).TimeFrame() Período usado para assinatura de velas.
TemaPeriod 6 Período da média móvel exponencial tripla.
TradeVolume 0.1 Volume enviado com cada ordem de mercado.
PlacingTrade false Ativa ou desativa a colocação de pedidos em tempo real.
LabelOffset 0 Deslocamento vertical aplicado ao rótulo de lucro acima do preço comercial.

Notas

  • A estratégia depende apenas de velas finalizadas e não acessa diretamente os buffers do indicador.
  • Os níveis protetores de stop-loss e take-profit da versão MQL não são replicados; as posições são invertidas quando chega um sinal oposto.
  • Os rótulos de lucro usam a moeda de segurança sempre que ela está disponível e, caso contrário, voltam aos valores brutos.
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>
/// Profit Labels translation (54352).
/// Draws realized PnL labels after trades close and opens positions on TEMA trend changes.
/// </summary>
public class ProfitLabelsStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _temaPeriod;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<bool> _placingTrade;
	private readonly StrategyParam<decimal> _labelOffset;

	private DateTimeOffset? _lastSignalTime;
	private bool _previousTradeBuy;
	private bool _previousTradeSell;
	private decimal? _entryPrice;
	private Sides? _entrySide;
	private decimal _positionVolume;

	private decimal? _tema0;
	private decimal? _tema1;
	private decimal? _tema2;
	private decimal? _tema3;

	/// <summary>
	/// Initializes a new instance of <see cref="ProfitLabelsStrategy"/>.
	/// </summary>
	public ProfitLabelsStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to process", "General");

		_temaPeriod = Param(nameof(TemaPeriod), 6)
			.SetDisplay("TEMA Period", "Period used for the triple EMA", "Indicator");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetDisplay("Trade Volume", "Volume used for each position", "Trading");

		_placingTrade = Param(nameof(PlacingTrade), true)
			.SetDisplay("Enable Trading", "Place live orders on signals", "Trading")
			;

		_labelOffset = Param(nameof(LabelOffset), 0m)
			.SetDisplay("Label Offset", "Vertical offset for profit labels", "Visualization")
			;
	}

	/// <summary>
	/// Candle type to process.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Period used for the triple EMA calculation.
	/// </summary>
	public int TemaPeriod
	{
		get => _temaPeriod.Value;
		set => _temaPeriod.Value = value;
	}

	/// <summary>
	/// Volume used for each position.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Defines whether live orders should be placed.
	/// </summary>
	public bool PlacingTrade
	{
		get => _placingTrade.Value;
		set => _placingTrade.Value = value;
	}

	/// <summary>
	/// Vertical offset applied to profit labels.
	/// </summary>
	public decimal LabelOffset
	{
		get => _labelOffset.Value;
		set => _labelOffset.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_lastSignalTime = null;
		_previousTradeBuy = false;
		_previousTradeSell = false;
		_entryPrice = null;
		_entrySide = null;
		_positionVolume = 0m;
		_tema0 = null;
		_tema1 = null;
		_tema2 = null;
		_tema3 = null;
	}

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

		Volume = TradeVolume;

		var tema = new TripleExponentialMovingAverage
		{
			Length = TemaPeriod
		};

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

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

	// Process each finished candle and react to TEMA trend flips.
	private void ProcessCandle(ICandleMessage candle, decimal temaValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		_tema3 = _tema2;
		_tema2 = _tema1;
		_tema1 = _tema0;
		_tema0 = temaValue;

		if (_tema3 is null || _tema2 is null || _tema1 is null || _tema0 is null)
			return;

		var trendUp = _tema2 < _tema3 && _tema0 > _tema1;
		var trendDown = _tema2 > _tema3 && _tema0 < _tema1;

		if (!PlacingTrade)
			return;

		if (trendUp)
		{
			HandleLongSignal(candle);
		}
		else if (trendDown)
		{
			HandleShortSignal(candle);
		}
	}

	// React to a bullish TEMA crossover.
	private void HandleLongSignal(ICandleMessage candle)
	{
		if (_lastSignalTime == candle.OpenTime)
			return;

		_lastSignalTime = candle.OpenTime;

		if (Position < 0m)
		{
			BuyMarket(Math.Abs(Position));
			_previousTradeBuy = false;
			_previousTradeSell = false;
			return;
		}

		if (Position != 0m || _previousTradeBuy)
			return;

		BuyMarket(TradeVolume);
		_previousTradeBuy = true;
		_previousTradeSell = false;
	}

	// React to a bearish TEMA crossover.
	private void HandleShortSignal(ICandleMessage candle)
	{
		if (_lastSignalTime == candle.OpenTime)
			return;

		_lastSignalTime = candle.OpenTime;

		if (Position > 0m)
		{
			SellMarket(Position);
			_previousTradeBuy = false;
			_previousTradeSell = false;
			return;
		}

		if (Position != 0m || _previousTradeSell)
			return;

		SellMarket(TradeVolume);
		_previousTradeBuy = false;
		_previousTradeSell = true;
	}

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

		if (Position != 0m && _entrySide is null)
		{
			_entrySide = Position > 0m ? Sides.Buy : Sides.Sell;
			_entryPrice = trade.Trade.Price;
			_positionVolume = Math.Abs(Position);
			return;
		}

		if (Position == 0m && _entrySide != null && _entryPrice.HasValue)
		{
			var exitPrice = trade.Trade.Price;
			var profit = CalculateProfit(_entrySide.Value, _entryPrice.Value, exitPrice, _positionVolume);

			DrawProfitLabel(profit, trade.Trade.ServerTime, exitPrice);

			_entrySide = null;
			_entryPrice = null;
			_positionVolume = 0m;
		}
	}

	// Calculate realized PnL using instrument parameters when possible.
	private decimal CalculateProfit(Sides entrySide, decimal entryPrice, decimal exitPrice, decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var priceStep = Security?.PriceStep;
		var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice);

		if (priceStep.HasValue && priceStep.Value > 0m && stepPrice.HasValue && stepPrice.Value > 0m)
		{
			var points = (exitPrice - entryPrice) / priceStep.Value;
			var direction = entrySide == Sides.Buy ? 1m : -1m;
			return points * stepPrice.Value * volume * direction;
		}

		var rawDifference = entrySide == Sides.Buy
			? exitPrice - entryPrice
			: entryPrice - exitPrice;

		return rawDifference * volume;
	}

	// Draw a label with realized profit information.
	private void DrawProfitLabel(decimal profit, DateTimeOffset time, decimal price)
	{
		// DrawText not available, log instead
		LogInfo($"Profit: {profit:F2} at {time:yyyy-MM-dd HH:mm} price {price}");
	}
}