Profit Labels Strategy — это конвертация советника MetaTrader 5 Profit Labels (54352) на высокоуровневый API StockSharp. Стратегия отслеживает пересечения Triple Exponential Moving Average (TEMA), чтобы открывать позиции, и рисует подписи с прибылью на графике после закрытия сделки. При смене тренда вверх открывается длинная позиция, при смене вниз — короткая. Если уже есть противоположная позиция, она сначала закрывается, после чего на графике отображается прибыль.
Свечи поступают через подписку SubscribeCandles, индикатор подключается через Bind, что позволяет обойтись без низкоуровневого доступа к буферам.
Правила торговли
Бычье пересечение: текущее значение TEMA поднимается выше предыдущего, а старые значения показывают нисходящий уклон — открываем покупку, если длинная позиция отсутствует.
Медвежье пересечение: TEMA разворачивается вниз по той же схеме — открываем продажу при отсутствии короткой позиции.
Реверс позиции: при появлении сигнала, противоположного текущей позиции, стратегия сначала закрывает её и только затем открывает новую.
Подписи прибыли: после полного закрытия позиции рассчитывается реализованный 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}");
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import TripleExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class profit_labels_strategy(Strategy):
def __init__(self):
super(profit_labels_strategy, self).__init__()
self._tema_period = self.Param("TemaPeriod", 6)
self._trade_volume = self.Param("TradeVolume", 1.0)
self._placing_trade = self.Param("PlacingTrade", True)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._last_signal_time = None
self._prev_trade_buy = False
self._prev_trade_sell = False
self._tema0 = None
self._tema1 = None
self._tema2 = None
self._tema3 = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def TradeVolume(self):
return self._trade_volume.Value
@TradeVolume.setter
def TradeVolume(self, value):
self._trade_volume.Value = value
@property
def PlacingTrade(self):
return self._placing_trade.Value
@PlacingTrade.setter
def PlacingTrade(self, value):
self._placing_trade.Value = value
def OnReseted(self):
super(profit_labels_strategy, self).OnReseted()
self._last_signal_time = None
self._prev_trade_buy = False
self._prev_trade_sell = False
self._tema0 = None
self._tema1 = None
self._tema2 = None
self._tema3 = None
def OnStarted2(self, time):
super(profit_labels_strategy, self).OnStarted2(time)
self.Volume = float(self.TradeVolume)
tema = TripleExponentialMovingAverage()
tema.Length = self._tema_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(tema, self._process_candle).Start()
def _process_candle(self, candle, tema_val):
if candle.State != CandleStates.Finished:
return
self._tema3 = self._tema2
self._tema2 = self._tema1
self._tema1 = self._tema0
self._tema0 = float(tema_val)
if self._tema3 is None or self._tema2 is None or self._tema1 is None or self._tema0 is None:
return
trend_up = self._tema2 < self._tema3 and self._tema0 > self._tema1
trend_down = self._tema2 > self._tema3 and self._tema0 < self._tema1
if not self.PlacingTrade:
return
if trend_up:
self._handle_long_signal(candle)
elif trend_down:
self._handle_short_signal(candle)
def _handle_long_signal(self, candle):
if self._last_signal_time == candle.OpenTime:
return
self._last_signal_time = candle.OpenTime
if self.Position < 0:
self.BuyMarket(abs(float(self.Position)))
self._prev_trade_buy = False
self._prev_trade_sell = False
return
if self.Position != 0 or self._prev_trade_buy:
return
self.BuyMarket(float(self.TradeVolume))
self._prev_trade_buy = True
self._prev_trade_sell = False
def _handle_short_signal(self, candle):
if self._last_signal_time == candle.OpenTime:
return
self._last_signal_time = candle.OpenTime
if self.Position > 0:
self.SellMarket(float(self.Position))
self._prev_trade_buy = False
self._prev_trade_sell = False
return
if self.Position != 0 or self._prev_trade_sell:
return
self.SellMarket(float(self.TradeVolume))
self._prev_trade_buy = False
self._prev_trade_sell = True
def CreateClone(self):
return profit_labels_strategy()