Открыть на GitHub

Стратегия Profit Labels

Обзор

Profit Labels Strategy — это конвертация советника MetaTrader 5 Profit Labels (54352) на высокоуровневый API StockSharp. Стратегия отслеживает пересечения Triple Exponential Moving Average (TEMA), чтобы открывать позиции, и рисует подписи с прибылью на графике после закрытия сделки. При смене тренда вверх открывается длинная позиция, при смене вниз — короткая. Если уже есть противоположная позиция, она сначала закрывается, после чего на графике отображается прибыль.

Свечи поступают через подписку SubscribeCandles, индикатор подключается через Bind, что позволяет обойтись без низкоуровневого доступа к буферам.

Правила торговли

  1. Бычье пересечение: текущее значение TEMA поднимается выше предыдущего, а старые значения показывают нисходящий уклон — открываем покупку, если длинная позиция отсутствует.
  2. Медвежье пересечение: TEMA разворачивается вниз по той же схеме — открываем продажу при отсутствии короткой позиции.
  3. Реверс позиции: при появлении сигнала, противоположного текущей позиции, стратегия сначала закрывает её и только затем открывает новую.
  4. Подписи прибыли: после полного закрытия позиции рассчитывается реализованный PnL и выводится на график методом DrawText.

Параметры

Имя Значение по умолчанию Описание
CandleType TimeSpan.FromMinutes(1).TimeFrame() Таймфрейм свечей для подписки.
TemaPeriod 6 Период Triple EMA.
TradeVolume 0.1 Объём заявки при открытии позиции.
PlacingTrade false Включает или отключает регистрацию реальных заявок.
LabelOffset 0 Вертикальное смещение подписи прибыли относительно цены сделки.

Примечания

  • Используются только завершённые свечи, доступ к значениям индикатора осуществляется через биндинг.
  • Стоп-лосс и тейк-профит из версии MQL не перенесены; позиции переворачиваются при появлении противоположного сигнала.
  • Подписи пытаются выводить символ валюты инструмента, при его отсутствии отображается чистое числовое значение.
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}");
	}
}