Auf GitHub ansehen

Profit Labels Strategy

Overview

The Profit Labels Strategy converts the MetaTrader 5 expert advisor Profit Labels (54352) to the StockSharp high-level API. The strategy monitors Triple Exponential Moving Average (TEMA) crossovers to open positions and draws profit labels on the chart after a position is closed. When the trend flips upward the algorithm opens a long position, and when the trend flips downward it opens a short position. If an opposite position is still active, the strategy first closes it and prints the realized profit label.

Candles are processed through a SubscribeCandles subscription, and the indicator is bound via Bind to keep the implementation fully high-level. Finished candles update the TEMA values and trigger trading decisions.

Trading Rules

  1. Bullish crossover: when the current TEMA value moves above the previous value while the older readings show a downward slope, the strategy opens a long position if no long is currently active.
  2. Bearish crossover: when the TEMA turns down in the same manner, it opens a short position if no short is active.
  3. Position reversal: if an opposite position exists at the moment of a new signal, the strategy closes the open position before placing a new order.
  4. Profit labels: once the position is fully closed, the realized PnL is calculated and displayed on the chart using DrawText.

Parameters

Name Default Description
CandleType TimeSpan.FromMinutes(1).TimeFrame() Time frame used for candle subscription.
TemaPeriod 6 Period of the Triple Exponential Moving Average.
TradeVolume 0.1 Volume submitted with each market order.
PlacingTrade false Enables or disables live order placement.
LabelOffset 0 Vertical offset applied to the profit label above the trade price.

Notes

  • The strategy relies solely on finished candles and does not access indicator buffers directly.
  • Protective stop-loss and take-profit levels from the MQL version are not replicated; positions are reversed when an opposite signal arrives.
  • Profit labels use the security currency whenever it is available and fall back to raw values otherwise.
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}");
	}
}