Ver en GitHub

Estrategia Kijun-Sen Robot

Descripción general

La Estrategia Kijun-Sen Robot es una conversión directa del asesor experto de MetaTrader 5 "Kijun Sen Robot" a la API de estrategias de alto nivel de StockSharp. Opera por defecto en velas de 30 minutos y se centra en cruces del precio con la línea Kijun-sen de Ichimoku confirmados por una media móvil linealmente ponderada (LWMA) de 20 períodos. La estrategia mantiene la idea original del experto de operar solo durante las horas más activas, aplicando protección de posición con lógica dinámica de stop, break-even y trailing.

Indicadores y datos

  • Ichimoku con Tenkan, Kijun y Senkou Span B configurados en 6/12/24 períodos.
  • Media Móvil Linealmente Ponderada (LWMA) de 20 barras para confirmación de pendiente y filtrado de distancia.
  • Velas de 30 minutos (por defecto) para generación de señales. Se puede seleccionar cualquier otro marco temporal a través del parámetro CandleType.

Lógica de trading

Entrada larga

  1. La vela atraviesa la línea Kijun desde abajo. La vela debe abrir por debajo de la línea, cerrar por encima, o tocarla durante la barra mientras el cierre anterior también estaba por debajo.
  2. El Kijun actual es plano o sube comparado con dos barras atrás.
  3. La LWMA está al menos MaFilterPips (convertido a unidades de precio) por debajo del nivel Kijun, manteniendo la línea base sobre la media móvil.
  4. La pendiente de la LWMA es positiva (LWMA actual mayor que el valor anterior).
  5. El horario de trading está dentro de [TradingStartHour, TradingEndHour), por defecto 07:00–19:00 hora de la bolsa.

Cuando todas las condiciones se satisfacen y la estrategia no tiene posición neta larga, se envía una orden de compra a mercado (cualquier corto existente se cubre primero). El precio de entrada es el cierre de la vela.

Entrada corta

  1. La vela atraviesa la línea Kijun desde arriba (espejo de la lógica larga).
  2. El Kijun es plano o cae relativo a dos barras atrás.
  3. La LWMA está al menos MaFilterPips por encima del nivel Kijun.
  4. La pendiente de la LWMA es negativa (LWMA actual menor que el valor anterior).
  5. La entrada ocurre solo dentro de la ventana de trading permitida.

Se coloca una orden de venta a mercado (la exposición larga existente se cierra antes de abrir un corto).

Gestión de posición y salidas

  • Stop-loss inicial – colocado StopLossPips por debajo/encima del precio de entrada (convertido a unidades de precio mediante el paso de precio del instrumento). Esto reproduce el stop protector de la versión MQL.
  • Movimiento break-even – una vez que el beneficio no realizado supera BreakEvenPips, el stop se mueve al precio de entrada más un pip (largo) o menos un pip (corto). El umbral se mide usando la misma lógica de conversión de pips.
  • Trailing stop – después de que el precio avanza TrailingStopPips, el stop sigue al precio a esa distancia, solo en la dirección favorable.
  • Take-profit fijo – objetivo opcional definido por TakeProfitPips. Poner en cero para deshabilitar.
  • Salida por pendiente Kijun – si la LWMA gira contra la operación antes de que el stop se mueva más allá del break-even, la posición se cierra inmediatamente, coincidiendo con la salida de emergencia del experto original.
  • Filtro de tiempo – las nuevas operaciones se ignoran fuera de la ventana configurada, pero las operaciones abiertas siguen gestionándose hasta cerrarse por las reglas anteriores.
  • Manejo de órdenes – la estrategia StockSharp usa exclusivamente órdenes de mercado; la lógica compleja de entrada límite-vs-mercado del EA original se simplifica porque se usan datos de velas en vez de datos de tick.

Si tanto el nivel de stop-loss como el de take-profit se incumplirían dentro de la misma barra, el stop-loss tiene precedencia para mantenerse conservador sin información intrabar.

Parámetros

Parámetro Valor predeterminado Descripción
TenkanPeriod 6 Longitud de Ichimoku Tenkan-sen.
KijunPeriod 12 Longitud de Ichimoku Kijun-sen.
SenkouSpanBPeriod 24 Longitud de Ichimoku Senkou Span B.
LwmaPeriod 20 Longitud del filtro de confirmación LWMA.
MaFilterPips 6 Distancia mínima LWMA-a-Kijun en pips.
StopLossPips 50 Distancia del stop protector inicial.
BreakEvenPips 9 Beneficio necesario para mover el stop al break-even.
TrailingStopPips 10 Distancia para el movimiento del trailing stop.
TakeProfitPips 120 Distancia opcional de take-profit fijo.
TradingStartHour 7 Primera hora de trading permitida (inclusive).
TradingEndHour 19 Última hora de trading permitida (exclusiva).
CandleType Marco temporal de 30 minutos Tipo de datos usado para la evaluación de señales.

Todos los parámetros basados en pips se traducen en unidades de precio usando el PriceStep del instrumento. Los instrumentos con 3 o 5 dígitos decimales reciben automáticamente un factor de 10 para replicar el tamaño clásico de pip forex.

Notas de implementación

  • La conversión mantiene las variables de estado de la estrategia (comportamiento longcross, shortcross) a través de _pendingLongLevel y _pendingShortLevel, asegurando que las nuevas posiciones requieran un cruce Kijun fresco.
  • Las comprobaciones intrabar como "último bid/ask" de la versión MT5 se aproximan con condiciones a nivel de vela (Open, Close, High, Low). Esto hace la lógica determinista para backtesting en StockSharp.
  • La protección de posición usa ClosePosition() y seguimiento manual del stop en lugar de modificaciones de órdenes MT5. Los ajustes de break-even y trailing se ejecutan una vez por vela finalizada.
  • El método auxiliar ConvertPips realiza la conversión pip-a-precio usando Security.PriceStep o Security.MinPriceStep, aplicando un multiplicador 10× para tamaños de tick de 3 o 5 decimales para emular la regla digits_adjust de MT5.
  • Porque la estrategia está ligada a la API de alto nivel, los indicadores se vinculan vía SubscribeCandles().BindEx(...), y los dibujos del gráfico se configuran automáticamente (velas, Ichimoku, LWMA, operaciones propias).

Directrices de uso

  1. Adjuntar la estrategia a un instrumento que soporte velas de 30 minutos (o configurar un CandleType diferente).
  2. Configurar Volume en la instancia de la estrategia al tamaño de orden deseado antes de iniciar.
  3. Opcionalmente ajustar los parámetros basados en pips para reflejar la volatilidad del instrumento o reproducir configuraciones optimizadas para pares de divisas específicos.
  4. Ejecutar en el backtester de alto nivel o entorno en vivo; la estrategia aplicará la misma ventana de trading, reglas de stop y trailing que el experto original.
  5. Monitorear el registro o gráfico para ver las actualizaciones de break-even y trailing. Todos los comentarios en el código están en inglés por claridad según lo solicitado.

La versión de Python se omite intencionalmente; solo la implementación en C# se proporciona en esta carpeta.

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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Ichimoku Kijun-sen robot converted from MetaTrader.
/// The strategy looks for price crossing the Kijun line while a 20-period LWMA confirms the trend.
/// It manages risk with time filters, configurable stop-loss, break-even and trailing rules.
/// </summary>
public class KijunSenRobotStrategy : Strategy
{
	private readonly StrategyParam<int> _tenkanPeriod;
	private readonly StrategyParam<int> _kijunPeriod;
	private readonly StrategyParam<int> _senkouSpanBPeriod;
	private readonly StrategyParam<int> _lwmaPeriod;
	private readonly StrategyParam<decimal> _maFilterPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _breakEvenPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<int> _tradingStartHour;
	private readonly StrategyParam<int> _tradingEndHour;
	private readonly StrategyParam<DataType> _candleType;

	private Ichimoku _ichimoku = null!;
	private WeightedMovingAverage _lwma = null!;

	private decimal? _previousClose;
	private decimal? _previousMa;
	private decimal? _previousPrevMa;
	private decimal? _previousKijun;
	private decimal? _previousPrevKijun;
	private decimal? _pendingLongLevel;
	private decimal? _pendingShortLevel;

	private bool? _isLongPosition;
	private decimal? _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal _stopLossDistance;
	private decimal _takeProfitDistance;
	private decimal _breakEvenDistance;
	private decimal _breakEvenStep;
	private decimal _trailingDistance;
	private bool _breakEvenApplied;

	/// <summary>
	/// Tenkan-sen calculation period.
	/// </summary>
	public int TenkanPeriod
	{
		get => _tenkanPeriod.Value;
		set => _tenkanPeriod.Value = value;
	}

	/// <summary>
	/// Kijun-sen calculation period.
	/// </summary>
	public int KijunPeriod
	{
		get => _kijunPeriod.Value;
		set => _kijunPeriod.Value = value;
	}

	/// <summary>
	/// Senkou Span B calculation period.
	/// </summary>
	public int SenkouSpanBPeriod
	{
		get => _senkouSpanBPeriod.Value;
		set => _senkouSpanBPeriod.Value = value;
	}

	/// <summary>
	/// Weighted moving average period used for slope confirmation.
	/// </summary>
	public int LwmaPeriod
	{
		get => _lwmaPeriod.Value;
		set => _lwmaPeriod.Value = value;
	}

	/// <summary>
	/// Minimum distance in pips between price and Kijun required by the LWMA filter.
	/// </summary>
	public decimal MaFilterPips
	{
		get => _maFilterPips.Value;
		set => _maFilterPips.Value = value;
	}

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

	/// <summary>
	/// Profit distance in pips required to move the stop-loss to break-even.
	/// </summary>
	public decimal BreakEvenPips
	{
		get => _breakEvenPips.Value;
		set => _breakEvenPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

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

	/// <summary>
	/// First trading hour (inclusive) in exchange time.
	/// </summary>
	public int TradingStartHour
	{
		get => _tradingStartHour.Value;
		set => _tradingStartHour.Value = value;
	}

	/// <summary>
	/// Last trading hour (exclusive) in exchange time.
	/// </summary>
	public int TradingEndHour
	{
		get => _tradingEndHour.Value;
		set => _tradingEndHour.Value = value;
	}

	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="KijunSenRobotStrategy"/>.
	/// </summary>
	public KijunSenRobotStrategy()
	{
		_tenkanPeriod = Param(nameof(TenkanPeriod), 6)
		.SetGreaterThanZero()
		.SetDisplay("Tenkan Period", "Period for Ichimoku Tenkan line", "Ichimoku")
		
		.SetOptimize(4, 12, 1);

		_kijunPeriod = Param(nameof(KijunPeriod), 12)
		.SetGreaterThanZero()
		.SetDisplay("Kijun Period", "Period for Ichimoku Kijun line", "Ichimoku")
		
		.SetOptimize(8, 20, 1);

		_senkouSpanBPeriod = Param(nameof(SenkouSpanBPeriod), 24)
		.SetGreaterThanZero()
		.SetDisplay("Senkou Span B Period", "Period for Ichimoku Senkou Span B", "Ichimoku")
		
		.SetOptimize(18, 30, 1);

		_lwmaPeriod = Param(nameof(LwmaPeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("LWMA Period", "Length of the confirmation LWMA", "Trend Filter")
		
		.SetOptimize(10, 40, 2);

		_maFilterPips = Param(nameof(MaFilterPips), 20m)
		.SetNotNegative()
		.SetDisplay("LWMA Filter (pips)", "Minimum distance between price and Kijun required by the LWMA", "Trend Filter")
		
		.SetOptimize(0m, 20m, 1m);

		_stopLossPips = Param(nameof(StopLossPips), 50m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (pips)", "Initial protective stop distance", "Risk Management")
		
		.SetOptimize(20m, 100m, 5m);

		_breakEvenPips = Param(nameof(BreakEvenPips), 9m)
		.SetNotNegative()
		.SetDisplay("Break-even Trigger (pips)", "Profit distance required to protect the position", "Risk Management")
		
		.SetOptimize(5m, 20m, 1m);

		_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop (pips)", "Distance for the trailing stop after the position moves in profit", "Risk Management")
		
		.SetOptimize(5m, 30m, 1m);

		_takeProfitPips = Param(nameof(TakeProfitPips), 120m)
		.SetNotNegative()
		.SetDisplay("Take Profit (pips)", "Optional fixed profit target", "Risk Management")
		
		.SetOptimize(40m, 200m, 10m);

		_tradingStartHour = Param(nameof(TradingStartHour), 7)
		.SetRange(0, 23)
		.SetDisplay("Start Hour", "First trading hour (inclusive)", "Scheduling");

		_tradingEndHour = Param(nameof(TradingEndHour), 19)
		.SetRange(1, 24)
		.SetDisplay("End Hour", "Last trading hour (exclusive)", "Scheduling");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe used for signal generation", "General");
	}

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

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

		_ichimoku = null!;
		_lwma = null!;

		_previousClose = null;
		_previousMa = null;
		_previousPrevMa = null;
		_previousKijun = null;
		_previousPrevKijun = null;
		_pendingLongLevel = null;
		_pendingShortLevel = null;

		ResetPositionState();
	}

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

		_ichimoku = new Ichimoku
		{
			Tenkan = { Length = TenkanPeriod },
			Kijun = { Length = KijunPeriod },
			SenkouB = { Length = SenkouSpanBPeriod }
		};

		_lwma = new WeightedMovingAverage
		{
			Length = LwmaPeriod
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
		.BindEx(_ichimoku, _lwma, ProcessCandle)
		.Start();

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

		// protection handled manually via SL/TP/trailing
	}

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

		if (!maValue.IsFinal)
		return;

		var ichimokuTyped = (IchimokuValue)ichimokuValue;
		if (ichimokuTyped.Kijun is not decimal kijun)
		return;

		var maCurrent = maValue.ToDecimal();

		ManageOpenPosition(candle, maCurrent);

		if (IsFormedAndOnlineAndAllowTrading() && IsWithinTradingHours(candle.OpenTime))
		{
			EvaluateEntrySignals(candle, kijun, maCurrent);
		}

		UpdateHistory(candle, kijun, maCurrent);
	}

	private void ManageOpenPosition(ICandleMessage candle, decimal maCurrent)
	{
		if (Position == 0)
		{
			if (_isLongPosition != null || _entryPrice != null)
			ResetPositionState();
			return;
		}

		var isLong = Position > 0;
		var actualEntry = _entryPrice ?? candle.ClosePrice;
		if (_isLongPosition is null || _isLongPosition.Value != isLong || _entryPrice is null)
		{
			var entry = actualEntry != 0m ? actualEntry : candle.ClosePrice;
			SetupPositionState(isLong, entry);
		}
		else if (actualEntry != 0m && _entryPrice.Value != actualEntry)
		{
			_entryPrice = actualEntry;
			if (_isLongPosition.Value)
			{
				_stopLossPrice = _stopLossDistance > 0m ? _entryPrice - _stopLossDistance : null;
				_takeProfitPrice = _takeProfitDistance > 0m ? _entryPrice + _takeProfitDistance : null;
			}
			else
			{
				_stopLossPrice = _stopLossDistance > 0m ? _entryPrice + _stopLossDistance : null;
				_takeProfitPrice = _takeProfitDistance > 0m ? _entryPrice - _takeProfitDistance : null;
			}
		}

		if (_entryPrice is not decimal entryPrice)
		return;

		_isLongPosition = isLong;

		if (_previousMa.HasValue && _previousPrevMa.HasValue && _stopLossPrice.HasValue)
		{
			if (isLong && _stopLossPrice.Value < entryPrice && _previousMa.Value < _previousPrevMa.Value)
			{
				ClosePositionAndReset();
				return;
			}

			if (!isLong && _stopLossPrice.Value > entryPrice && _previousMa.Value > _previousPrevMa.Value)
			{
				ClosePositionAndReset();
				return;
			}
		}

		if (isLong)
		{
			ApplyBreakEvenAndTrailingForLong(candle, entryPrice);

			if (CheckStopLossHit(candle.LowPrice, _stopLossPrice))
			{
				ClosePositionAndReset();
				return;
			}

			if (CheckTakeProfitHit(candle.HighPrice, _takeProfitPrice))
			{
				ClosePositionAndReset();
				return;
			}
		}
		else
		{
			ApplyBreakEvenAndTrailingForShort(candle, entryPrice);

			if (CheckStopLossHitForShort(candle.HighPrice, _stopLossPrice))
			{
				ClosePositionAndReset();
				return;
			}

			if (CheckTakeProfitHitForShort(candle.LowPrice, _takeProfitPrice))
			{
				ClosePositionAndReset();
				return;
			}
		}
	}

	private void ApplyBreakEvenAndTrailingForLong(ICandleMessage candle, decimal entryPrice)
	{
		if (!_breakEvenApplied && _breakEvenDistance > 0m)
		{
			if (candle.ClosePrice - entryPrice >= _breakEvenDistance)
			{
				var newStop = entryPrice + (_breakEvenStep > 0m ? _breakEvenStep : 0m);
				if (_stopLossPrice is not decimal currentStop || newStop > currentStop)
				_stopLossPrice = newStop;
				_breakEvenApplied = true;
			}
		}

		if (_trailingDistance > 0m && candle.ClosePrice - entryPrice >= _trailingDistance)
		{
			var newStop = candle.ClosePrice - _trailingDistance;
			if (_stopLossPrice is not decimal currentStop || newStop > currentStop)
			_stopLossPrice = newStop;
		}
	}

	private void ApplyBreakEvenAndTrailingForShort(ICandleMessage candle, decimal entryPrice)
	{
		if (!_breakEvenApplied && _breakEvenDistance > 0m)
		{
			if (entryPrice - candle.ClosePrice >= _breakEvenDistance)
			{
				var newStop = entryPrice - (_breakEvenStep > 0m ? _breakEvenStep : 0m);
				if (_stopLossPrice is not decimal currentStop || newStop < currentStop)
				_stopLossPrice = newStop;
				_breakEvenApplied = true;
			}
		}

		if (_trailingDistance > 0m && entryPrice - candle.ClosePrice >= _trailingDistance)
		{
			var newStop = candle.ClosePrice + _trailingDistance;
			if (_stopLossPrice is not decimal currentStop || newStop < currentStop)
			_stopLossPrice = newStop;
		}
	}

	private static bool CheckStopLossHit(decimal lowPrice, decimal? stopPrice)
	{
		return stopPrice.HasValue && lowPrice <= stopPrice.Value;
	}

	private static bool CheckTakeProfitHit(decimal highPrice, decimal? takeProfitPrice)
	{
		return takeProfitPrice.HasValue && highPrice >= takeProfitPrice.Value;
	}

	private static bool CheckStopLossHitForShort(decimal highPrice, decimal? stopPrice)
	{
		return stopPrice.HasValue && highPrice >= stopPrice.Value;
	}

	private static bool CheckTakeProfitHitForShort(decimal lowPrice, decimal? takeProfitPrice)
	{
		return takeProfitPrice.HasValue && lowPrice <= takeProfitPrice.Value;
	}

	private void EvaluateEntrySignals(ICandleMessage candle, decimal kijun, decimal maCurrent)
	{
		if (_previousClose is not decimal previousClose ||
		_previousMa is not decimal previousMa ||
		_previousKijun is not decimal previousKijun)
		{
			return;
		}

		var maTrendUp = maCurrent > previousMa;
		var maTrendDown = maCurrent < previousMa;
		var filterOffset = ConvertPips(MaFilterPips);

		var priceOpenedBelow = candle.OpenPrice < kijun;
		var priceOpenedAbove = candle.OpenPrice > kijun;
		var priceClosedAbove = candle.ClosePrice > kijun;
		var priceClosedBelow = candle.ClosePrice < kijun;
		var priceTouchedBelow = candle.LowPrice <= kijun;
		var priceTouchedAbove = candle.HighPrice >= kijun;
		var priceWasBelow = previousClose < previousKijun;
		var priceWasAbove = previousClose > previousKijun;
		var kijunNotFalling = !_previousPrevKijun.HasValue || kijun >= _previousPrevKijun.Value;
		var kijunNotRising = !_previousPrevKijun.HasValue || kijun <= _previousPrevKijun.Value;

		if (_pendingLongLevel is null)
		{
			if (priceClosedAbove && (priceOpenedBelow || priceWasBelow || priceTouchedBelow) && kijunNotFalling)
			{
				if (filterOffset <= 0m || maCurrent < kijun - filterOffset)
				{
					_pendingLongLevel = kijun;
					_pendingShortLevel = null;
				}
			}
		}

		if (_pendingShortLevel is null)
		{
			if (priceClosedBelow && (priceOpenedAbove || priceWasAbove || priceTouchedAbove) && kijunNotRising)
			{
				if (filterOffset <= 0m || maCurrent > kijun + filterOffset)
				{
					_pendingShortLevel = kijun;
					_pendingLongLevel = null;
				}
			}
		}

		var volume = Volume + Math.Abs(Position);
		if (volume <= 0m)
		return;

		if (_pendingLongLevel.HasValue && maTrendUp && Position <= 0)
		{
			BuyMarket(volume);
			SetupPositionState(true, candle.ClosePrice);
			_pendingLongLevel = null;
			_pendingShortLevel = null;
			return;
		}

		if (_pendingShortLevel.HasValue && maTrendDown && Position >= 0)
		{
			SellMarket(volume);
			SetupPositionState(false, candle.ClosePrice);
			_pendingLongLevel = null;
			_pendingShortLevel = null;
		}
	}

	private void UpdateHistory(ICandleMessage candle, decimal kijun, decimal maCurrent)
	{
		_previousPrevKijun = _previousKijun;
		_previousKijun = kijun;

		_previousPrevMa = _previousMa;
		_previousMa = maCurrent;

		_previousClose = candle.ClosePrice;
	}

	private bool IsWithinTradingHours(DateTimeOffset time)
	{
		var hour = time.Hour;
		return hour >= TradingStartHour && hour < TradingEndHour;
	}

	private void SetupPositionState(bool isLong, decimal entryPrice)
	{
		_isLongPosition = isLong;
		_entryPrice = entryPrice;
		_breakEvenApplied = false;

		_stopLossDistance = ConvertPips(StopLossPips);
		_takeProfitDistance = ConvertPips(TakeProfitPips);
		_breakEvenDistance = ConvertPips(BreakEvenPips);
		_breakEvenStep = ConvertPips(1m);
		_trailingDistance = ConvertPips(TrailingStopPips);

		_stopLossPrice = _stopLossDistance > 0m ? (isLong ? entryPrice - _stopLossDistance : entryPrice + _stopLossDistance) : null;
		_takeProfitPrice = _takeProfitDistance > 0m ? (isLong ? entryPrice + _takeProfitDistance : entryPrice - _takeProfitDistance) : null;
	}

	private void ClosePositionAndReset()
	{
		if (Position != 0)
			if (Position > 0) SellMarket(Math.Abs(Position)); else if (Position < 0) BuyMarket(Math.Abs(Position));

		ResetPositionState();
	}

	private void ResetPositionState()
	{
		_isLongPosition = null;
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_stopLossDistance = 0m;
		_takeProfitDistance = 0m;
		_breakEvenDistance = 0m;
		_breakEvenStep = 0m;
		_trailingDistance = 0m;
		_breakEvenApplied = false;
	}

	private decimal ConvertPips(decimal value)
	{
		if (value <= 0m)
		return 0m;

		var step = GetPipStep();
		return value * step;
	}

	private decimal GetPipStep()
	{
		var priceStep = Security?.PriceStep;
		if (priceStep is null || priceStep <= 0m)
		return 1m;

		var stepValue = priceStep.Value;
		var decimals = GetDecimalPlaces(stepValue);
		if (decimals == 3 || decimals == 5)
		return stepValue * 10m;

		return stepValue;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		value = Math.Abs(value);
		var decimals = 0;
		while (value != Math.Truncate(value) && decimals < 10)
		{
			value *= 10m;
			decimals++;
		}

		return decimals;
	}
}