Ver en GitHub

DVD Estrategia de 100-50 céntimos

Descripción general

La estrategia DVD de 100-50 centavos es un sistema de orden límite contrario adaptado del asesor experto MT4 original. La lógica evalúa el mercado en cuatro períodos de tiempo (M1, M30, H1, D1) y califica las configuraciones potenciales antes de estacionar órdenes límite de compra o venta alrededor de la cuadrícula de precios de "100 niveles" más cercana. Cuando se ejecuta la orden límite, la estrategia gestiona la posición con niveles de stop-loss y take-profit precalculados.

Indicadores y datos

  • RAVI (Índice de verificación de acción de rango) en H1 y D1, calculado con SMA(2) y SMA(24) sobre el precio de apertura.
  • Datos de velas sin procesar en M1, M30 y H1 para filtros de patrones como rechazo de picos, comprobaciones de consolidación y pruebas de impulso.
  • Redondeo de la cuadrícula de precios que ajusta el precio actual al nivel 100 más cercano usando un redondeo de dos decimales y un desplazamiento configurable de 0,1 pips.

Lógica de entrada

  1. Calcule el precio redondeado "Nivel 100" redondeando el último M1 cerca de dos decimales y desplazándolo en PointFromLevelGoPips (predeterminado 50 → 5 pips).
  2. Inicialice una puntuación interna (BAL) en 0 y sume/reste puntos según:
    • Filtro de tendencia: agregue 10 puntos cuando H1 RAVI esté por debajo de cero para configuraciones largas o por encima de cero para configuraciones cortas.
    • Confirmación de pico por hora: agregue 7 puntos cuando los dos máximos/mínimos del primer semestre anteriores superen la cuadrícula en RiseFilterPips.
    • Alineación de la estructura: agregue 45 puntos cuando el cierre actual de M1 vuelva a cruzar el nivel y los últimos tres mínimos/máximos de H1 permanezcan por encima/por debajo del colchón de seguridad (PointFromLevelGoPips ± 30 * 0.1 pip).
    • Guardias de volatilidad: reste 50 puntos si los máximos/mínimos recientes de M1 exceden HighLevelPips (predeterminado 600 → 60 pips) o si aparecen ráfagas rápidas de impulso mientras el D1 RAVI confirma un fuerte régimen direccional.
    • Confirmación de ruptura: reste 50 puntos si las últimas 15 velas H1 nunca cruzaron el umbral LowLevel2Pips.
    • Filtro de consolidación: reste 50 puntos si las últimas ocho velas M30 permanecen dentro de la banda LowLevelPips.
  3. Realice una orden limitada solo cuando la puntuación final sea de al menos 50 y no exista otra exposición (posición u orden pendiente).

Colocación de pedidos

  • Límite de compra: 10 pips por debajo del último cierre de M1. El stop-loss está StopLossPips por debajo del precio límite, la toma de ganancias está TakeProfitPips por encima de él. Cuando el D1 RAVI muestra una escalera ascendente entre -1 y +5 durante los últimos cuatro días, la toma de ganancias recibe una extensión adicional de 25 pips.
  • Límite de venta: 7 pips por encima del último cierre de M1 con reglas de objetivo y stop simétricas. Cuando el D1 RAVI muestra una escalera descendente entre -5 y -1, el objetivo se extiende 25 pips.
  • Los pedidos pendientes caducan automáticamente después de OrderExpiryMinutes (20 minutos predeterminado). Cuando se cancela una orden, los niveles de protección almacenados se restablecen.

Gestión de Puestos

  • Una vez completada, la estrategia mantiene internamente los valores almacenados de stop-loss y take-profit y emite órdenes de salida del mercado cuando el precio toca cualquiera de los niveles.
  • No se aplica ningún trailing stop en la versión portada; el EA original deshabilitó la lógica final de forma predeterminada.
  • Las nuevas operaciones se bloquean mientras exista una posición activa o una orden límite pendiente.

Gestión monetaria

  • Cuando UseMoneyManagement está habilitado, el tamaño del lote imita la implementación de MT4: escala en TradeSizePercent del capital actual, se ajusta para cuentas mini y fija el resultado en [0.1, MaxVolume] (mini) o [1, MaxVolume] (estándar).
  • Deshabilitar la administración del dinero fuerza un volumen fijo controlado por el parámetro FixedVolume.
  • La negociación se detiene cuando el capital de la cartera cae por debajo de MarginCutoff.

Parámetros

Nombre Descripción Predeterminado
AccountIsMini Utilice reglas de redondeo de volumen de minicuentas true
UseMoneyManagement Habilitar el tamaño de lote adaptable true
TradeSizePercent Porcentaje de capital asignado por operación 10
FixedVolume Volumen utilizado cuando la administración del dinero está desactivada 0.01
MaxVolume Volumen comercial máximo permitido 4
StopLossPips Distancia de stop-loss en pips 210
TakeProfitPips Distancia de toma de ganancias en pips 18
PointFromLevelGoPips Cambio de nivel base en 0,1 pips 50
RiseFilterPips Distancia de confirmación de pico por hora (0,1 pips) 700
HighLevelPips Umbral de rechazo de picos de un minuto (0,1 pips) 600
LowLevelPips Banda de consolidación de 30 minutos (0,1 pips) 250
LowLevel2Pips Distancia de confirmación de ruptura por hora (0,1 pips) 450
MarginCutoff El suelo de la renta variable inhabilita nuevas operaciones 300
OrderExpiryMinutes Duración del pedido pendiente en minutos 20

Notas de uso

  • La conversión se basa en las velas terminadas de cada período; asegúrese de que el flujo de datos históricos proporcione velas M1, M30, H1 y D1 sincronizadas.
  • El stop y el objetivo de protección se ejecutan con órdenes de mercado para reflejar el comportamiento MT4 de los valores SL/TP adjuntos.
  • Debido a que la lógica es sensible al tamaño del pip, verifique que las propiedades PriceStep y Decimals del instrumento describan correctamente el formato de cotización.
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>
/// Mean-reversion limit strategy converted from the MT4 expert advisor "DVD 100-50 cent".
/// </summary>
public class Dvd10050CentStrategy : Strategy
{

	private readonly StrategyParam<bool> _accountIsMini;
	private readonly StrategyParam<bool> _useMoneyManagement;
	private readonly StrategyParam<decimal> _tradeSizePercent;
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _pointFromLevelGoPips;
	private readonly StrategyParam<decimal> _riseFilterPips;
	private readonly StrategyParam<decimal> _highLevelPips;
	private readonly StrategyParam<decimal> _lowLevelPips;
	private readonly StrategyParam<decimal> _lowLevel2Pips;
	private readonly StrategyParam<decimal> _marginCutoff;
	private readonly StrategyParam<int> _orderExpiryMinutes;
	private readonly StrategyParam<int> _m1HistoryLength;
	private readonly StrategyParam<int> _m30HistoryLength;
	private readonly StrategyParam<int> _h1HistoryLength;

	private SimpleMovingAverage _h1Fast = null!;
	private SimpleMovingAverage _h1Slow = null!;
	private SimpleMovingAverage _d1Fast = null!;
	private SimpleMovingAverage _d1Slow = null!;

	private readonly List<ICandleMessage> _m1History = new();
	private readonly List<ICandleMessage> _m30History = new();
	private readonly List<ICandleMessage> _h1Finished = new();
	private ICandleMessage _h1Current;

	private decimal? _raviH1;
	private decimal? _raviD1Current;
	private decimal? _raviD1Prev1;
	private decimal? _raviD1Prev2;
	private decimal? _raviD1Prev3;

	private decimal _pipSize;
	private decimal _pointValue;

	private DateTimeOffset? _buyOrderExpiry;
	private DateTimeOffset? _sellOrderExpiry;

	private decimal? _pendingBuyStop;
	private decimal? _pendingBuyTake;
	private decimal? _pendingSellStop;
	private decimal? _pendingSellTake;

	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;

	private decimal _previousPosition;

	/// <summary>
	/// Initializes a new instance of the <see cref="Dvd10050CentStrategy"/> class.
	/// </summary>
	public Dvd10050CentStrategy()
	{
		_accountIsMini = Param(nameof(AccountIsMini), true)
		.SetDisplay("Mini Account", "Use mini account position sizing", "Risk");

		_useMoneyManagement = Param(nameof(UseMoneyManagement), true)
		.SetDisplay("Use Money Management", "Enable adaptive lot sizing", "Risk");

		_tradeSizePercent = Param(nameof(TradeSizePercent), 10m)
		.SetDisplay("Risk Percent", "Percent of equity allocated per trade", "Risk")
		.SetRange(0m, 100m)
		;

		_fixedVolume = Param(nameof(FixedVolume), 0.01m)
		.SetDisplay("Fixed Volume", "Volume used when money management is disabled", "Risk")
		.SetRange(0.01m, 100m)
		;

		_maxVolume = Param(nameof(MaxVolume), 4m)
		.SetDisplay("Max Volume", "Ceiling for calculated trade volume", "Risk")
		.SetRange(0.01m, 100m)
		;

		_stopLossPips = Param(nameof(StopLossPips), 210m)
		.SetDisplay("Stop Loss (pips)", "Protective stop distance", "Orders")
		.SetRange(0m, 1000m)
		;

		_takeProfitPips = Param(nameof(TakeProfitPips), 18m)
		.SetDisplay("Take Profit (pips)", "Initial profit target distance", "Orders")
		.SetRange(0m, 500m)
		;

		_pointFromLevelGoPips = Param(nameof(PointFromLevelGoPips), 50m)
		.SetDisplay("Base Offset (0.1 pips)", "Offset used to build the 100 level grid", "Filters")
		.SetRange(0m, 1000m)
		;

		_riseFilterPips = Param(nameof(RiseFilterPips), 700m)
		.SetDisplay("Rise Filter (0.1 pips)", "Distance for hourly spike confirmation", "Filters")
		.SetRange(0m, 5000m)
		;

		_highLevelPips = Param(nameof(HighLevelPips), 600m)
		.SetDisplay("High Level (0.1 pips)", "One-minute spike rejection threshold", "Filters")
		.SetRange(0m, 5000m)
		;

		_lowLevelPips = Param(nameof(LowLevelPips), 250m)
		.SetDisplay("Low Level (0.1 pips)", "Half-hour consolidation ceiling", "Filters")
		.SetRange(0m, 5000m)
		;

		_lowLevel2Pips = Param(nameof(LowLevel2Pips), 450m)
		.SetDisplay("Low Level 2 (0.1 pips)", "Hourly breakout confirmation threshold", "Filters")
		.SetRange(0m, 5000m)
		;

		_marginCutoff = Param(nameof(MarginCutoff), 300m)
		.SetDisplay("Margin Cutoff", "Stop trading when equity falls below this level", "Risk")
		.SetRange(0m, 1_000_000m)
		;

		_orderExpiryMinutes = Param(nameof(OrderExpiryMinutes), 20)
		.SetDisplay("Order Expiry (minutes)", "Lifetime of pending limit orders", "Orders")
		.SetRange(1, 240)
		;

		_m1HistoryLength = Param(nameof(M1HistoryLength), 64)
			.SetDisplay("M1 History Length", "Number of M1 candles retained for analysis", "History")
			.SetRange(1, 500);

		_m30HistoryLength = Param(nameof(M30HistoryLength), 16)
			.SetDisplay("M30 History Length", "Number of M30 candles retained for analysis", "History")
			.SetRange(1, 200);

		_h1HistoryLength = Param(nameof(H1HistoryLength), 16)
			.SetDisplay("H1 History Length", "Number of H1 candles retained for analysis", "History")
			.SetRange(1, 200);
	}

	/// <summary>
	/// Gets or sets whether the account uses mini lot sizing.
	/// </summary>
	public bool AccountIsMini
	{
		get => _accountIsMini.Value;
		set => _accountIsMini.Value = value;
	}

	/// <summary>
	/// Gets or sets whether money management is enabled.
	/// </summary>
	public bool UseMoneyManagement
	{
		get => _useMoneyManagement.Value;
		set => _useMoneyManagement.Value = value;
	}

	/// <summary>
	/// Gets or sets the risk allocation per trade when money management is enabled.
	/// </summary>
	public decimal TradeSizePercent
	{
		get => _tradeSizePercent.Value;
		set => _tradeSizePercent.Value = value;
	}

	/// <summary>
	/// Gets or sets the fixed trade volume used when money management is disabled.
	/// </summary>
	public decimal FixedVolume
	{
		get => _fixedVolume.Value;
		set => _fixedVolume.Value = value;
	}

	/// <summary>
	/// Gets or sets the maximum volume allowed per trade.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Gets or sets the stop loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Gets or sets the take profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Gets or sets the base offset that defines the 100 level grid in 0.1 pip units.
	/// </summary>
	public decimal PointFromLevelGoPips
	{
		get => _pointFromLevelGoPips.Value;
		set => _pointFromLevelGoPips.Value = value;
	}

	/// <summary>
	/// Gets or sets the spike confirmation distance for hourly candles in 0.1 pip units.
	/// </summary>
	public decimal RiseFilterPips
	{
		get => _riseFilterPips.Value;
		set => _riseFilterPips.Value = value;
	}

	/// <summary>
	/// Gets or sets the rejection distance for one-minute highs in 0.1 pip units.
	/// </summary>
	public decimal HighLevelPips
	{
		get => _highLevelPips.Value;
		set => _highLevelPips.Value = value;
	}

	/// <summary>
	/// Gets or sets the consolidation ceiling distance for 30-minute highs in 0.1 pip units.
	/// </summary>
	public decimal LowLevelPips
	{
		get => _lowLevelPips.Value;
		set => _lowLevelPips.Value = value;
	}

	/// <summary>
	/// Gets or sets the hourly breakout confirmation distance in 0.1 pip units.
	/// </summary>
	public decimal LowLevel2Pips
	{
		get => _lowLevel2Pips.Value;
		set => _lowLevel2Pips.Value = value;
	}

	/// <summary>
	/// Gets or sets the equity level that disables new trades when reached.
	/// </summary>
	public decimal MarginCutoff
	{
		get => _marginCutoff.Value;
		set => _marginCutoff.Value = value;
	}

	/// <summary>
	/// Gets or sets the pending order lifetime in minutes.
	/// </summary>
	public int OrderExpiryMinutes
	{
		get => _orderExpiryMinutes.Value;
		set => _orderExpiryMinutes.Value = value;
	}

	/// <summary>
	/// Number of one-minute candles retained for intraday analysis.
	/// </summary>
	public int M1HistoryLength
	{
		get => _m1HistoryLength.Value;
		set => _m1HistoryLength.Value = value;
	}

	/// <summary>
	/// Number of thirty-minute candles retained for intraday analysis.
	/// </summary>
	public int M30HistoryLength
	{
		get => _m30HistoryLength.Value;
		set => _m30HistoryLength.Value = value;
	}

	/// <summary>
	/// Number of hourly candles retained for intraday analysis.
	/// </summary>
	public int H1HistoryLength
	{
		get => _h1HistoryLength.Value;
		set => _h1HistoryLength.Value = value;
	}

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

		_h1Fast = new SimpleMovingAverage { Length = 2 };
		_h1Slow = new SimpleMovingAverage { Length = 24 };
		_d1Fast = new SimpleMovingAverage { Length = 2 };
		_d1Slow = new SimpleMovingAverage { Length = 24 };

		_pipSize = CalculatePipSize();
		_pointValue = _pipSize / 10m;

		var m1Subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		m1Subscription.Bind(ProcessM1).Start();

		var m30Subscription = SubscribeCandles(TimeSpan.FromMinutes(30).TimeFrame());
		m30Subscription.Bind(ProcessM30).Start();

		var h1Subscription = SubscribeCandles(TimeSpan.FromHours(1).TimeFrame());
		h1Subscription.Bind(ProcessH1).Start();

		var d1Subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		d1Subscription.Bind(ProcessD1).Start();

		StartProtection(
			takeProfit: new Unit(2, UnitTypes.Percent),
			stopLoss: new Unit(1, UnitTypes.Percent)
		);
	}

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

		_h1Fast = null!;
		_h1Slow = null!;
		_d1Fast = null!;
		_d1Slow = null!;

		_m1History.Clear();
		_m30History.Clear();
		_h1Finished.Clear();
		_h1Current = null;

		_raviH1 = null;
		_raviD1Current = null;
		_raviD1Prev1 = null;
		_raviD1Prev2 = null;
		_raviD1Prev3 = null;

		_pipSize = 0m;
		_pointValue = 0m;

		_buyOrderExpiry = null;
		_sellOrderExpiry = null;
		_pendingBuyStop = null;
		_pendingBuyTake = null;
		_pendingSellStop = null;
		_pendingSellTake = null;
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
		_previousPosition = 0m;
	}

	private void ProcessM1(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		return;

		_m1History.Add(candle);
		TrimHistory(_m1History, M1HistoryLength);

		HandlePositionState(candle);
		ManageOrderExpirations(candle.CloseTime);
		ManageActivePosition(candle);

		if (!_h1Fast.IsFormed || !_h1Slow.IsFormed)
		return;

		if (HasExposure())
		return;

		if (!HasSufficientMargin())
		return;

		var orderPlaced = false;

		if (TryCalculateBuyScore(candle, out var buyLevel, out var buyScore) && buyScore >= 0m)
		{
			orderPlaced = PlaceBuyLimit(candle);
		}

		if (!orderPlaced && TryCalculateSellScore(candle, out var sellLevel, out var sellScore) && sellScore >= 0m)
		{
			PlaceSellLimit(candle);
		}
	}

	private void ProcessM30(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		return;

		_m30History.Add(candle);
		TrimHistory(_m30History, M30HistoryLength);
	}

	private void ProcessH1(ICandleMessage candle)
	{
		_h1Current = candle;

		if (candle.State != CandleStates.Finished)
		return;

		_h1Finished.Add(candle);
		TrimHistory(_h1Finished, H1HistoryLength);

		_h1Fast.Process(new DecimalIndicatorValue(_h1Fast, candle.OpenPrice, candle.CloseTime) { IsFinal = true });
		_h1Slow.Process(new DecimalIndicatorValue(_h1Slow, candle.OpenPrice, candle.CloseTime) { IsFinal = true });

		if (!_h1Fast.IsFormed || !_h1Slow.IsFormed)
		return;

		var slow = _h1Slow.GetCurrentValue();
		if (slow == 0m)
		return;

		var fast = _h1Fast.GetCurrentValue();
		_raviH1 = 100m * (fast - slow) / slow;
	}

	private void ProcessD1(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		return;

		_d1Fast.Process(new DecimalIndicatorValue(_d1Fast, candle.OpenPrice, candle.CloseTime) { IsFinal = true });
		_d1Slow.Process(new DecimalIndicatorValue(_d1Slow, candle.OpenPrice, candle.CloseTime) { IsFinal = true });

		if (!_d1Fast.IsFormed || !_d1Slow.IsFormed)
		return;

		var slow = _d1Slow.GetCurrentValue();
		if (slow == 0m)
		return;

		var fast = _d1Fast.GetCurrentValue();
		var ravi = 100m * (fast - slow) / slow;

		_raviD1Prev3 = _raviD1Prev2;
		_raviD1Prev2 = _raviD1Prev1;
		_raviD1Prev1 = _raviD1Current;
		_raviD1Current = ravi;
	}

	private void HandlePositionState(ICandleMessage candle)
	{
		var currentPosition = Position;

		if (currentPosition > 0m && _previousPosition <= 0m)
		{
			_longStop = _pendingBuyStop;
			_longTake = _pendingBuyTake;
			_pendingBuyStop = null;
			_pendingBuyTake = null;
			_buyOrderExpiry = null;
		}
		else if (currentPosition < 0m && _previousPosition >= 0m)
		{
			_shortStop = _pendingSellStop;
			_shortTake = _pendingSellTake;
			_pendingSellStop = null;
			_pendingSellTake = null;
			_sellOrderExpiry = null;
		}
		else if (currentPosition == 0m && _previousPosition != 0m)
		{
			ResetTradeLevels();
		}

		_previousPosition = currentPosition;
	}

	private void ManageOrderExpirations(DateTimeOffset currentTime)
	{
		if (_buyOrderExpiry is DateTimeOffset buyExpiry)
		{
			if (!HasActiveLimitOrder(Sides.Buy))
			{
				_buyOrderExpiry = null;
			}
			else if (currentTime >= buyExpiry)
			{
				CancelSideOrders(Sides.Buy);
				_buyOrderExpiry = null;
				_pendingBuyStop = null;
				_pendingBuyTake = null;
			}
		}

		if (_sellOrderExpiry is DateTimeOffset sellExpiry)
		{
			if (!HasActiveLimitOrder(Sides.Sell))
			{
				_sellOrderExpiry = null;
			}
			else if (currentTime >= sellExpiry)
			{
				CancelSideOrders(Sides.Sell);
				_sellOrderExpiry = null;
				_pendingSellStop = null;
				_pendingSellTake = null;
			}
		}
	}

	private void ManageActivePosition(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_longStop is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeLevels();
				return;
			}

			if (_longTake is decimal take && candle.HighPrice >= take)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeLevels();
			}
		}
		else if (Position < 0m)
		{
			if (_shortStop is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeLevels();
				return;
			}

			if (_shortTake is decimal take && candle.LowPrice <= take)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeLevels();
			}
		}
	}

	private bool TryCalculateBuyScore(ICandleMessage candle, out decimal level100, out decimal score)
	{
		score = 0m;
		level100 = 0m;

		if (_raviH1 is not decimal raviH1 || _raviD1Current is not decimal raviD1)
		return false;

		var previousM1 = GetM1Candle(1);
		var h1Low0 = GetH1Low(0);
		var h1Low1 = GetH1Low(1);
		var h1Low2 = GetH1Low(2);
		var h1High1 = GetH1High(1);
		var h1High2 = GetH1High(2);

		if (previousM1 is null || h1Low0 is null || h1Low1 is null || h1Low2 is null || h1High1 is null || h1High2 is null)
		return false;

		level100 = Math.Round(candle.ClosePrice, 2, MidpointRounding.AwayFromZero) + PointFromLevelGoPips * _pointValue;
		var riseThreshold = level100 + RiseFilterPips * _pointValue;
		var baseLow = level100 - PointFromLevelGoPips * _pointValue;
		var tolerance = 30m * _pointValue;

		if (raviH1 < 0m)
		score += 10m;

		if (h1High1 > riseThreshold || h1High2 > riseThreshold)
		score += 7m;

		if (candle.ClosePrice < level100 && previousM1.ClosePrice > level100 &&
		h1Low0.Value > baseLow + tolerance && h1Low1.Value > baseLow + tolerance && h1Low2.Value > baseLow)
		{
			score += 45m;
		}

		if (CheckM1HighAbove(level100 + HighLevelPips * _pointValue, 12))
		score -= 50m;

		if (raviD1 < -2m && CheckM1ImpulseForBuy())
		score -= 50m;

		if (!CheckH1BreakAbove(level100 + LowLevel2Pips * _pointValue))
		score -= 50m;

		if (CheckM30CompressionAbove(level100 + LowLevelPips * _pointValue))
		score -= 50m;

		return true;
	}

	private bool TryCalculateSellScore(ICandleMessage candle, out decimal level100, out decimal score)
	{
		score = 0m;
		level100 = 0m;

		if (_raviH1 is not decimal raviH1 || _raviD1Current is not decimal raviD1)
		return false;

		var previousM1 = GetM1Candle(1);
		var h1High0 = GetH1High(0);
		var h1High1 = GetH1High(1);
		var h1High2 = GetH1High(2);
		var h1Low1 = GetH1Low(1);
		var h1Low2 = GetH1Low(2);

		if (previousM1 is null || h1High0 is null || h1High1 is null || h1High2 is null || h1Low1 is null || h1Low2 is null)
		return false;

		level100 = Math.Round(candle.ClosePrice, 2, MidpointRounding.AwayFromZero) - PointFromLevelGoPips * _pointValue;
		var fallThreshold = level100 - RiseFilterPips * _pointValue;
		var baseHigh = level100 + PointFromLevelGoPips * _pointValue;
		var tolerance = 30m * _pointValue;

		if (raviH1 > 0m)
		score += 10m;

		if (h1Low1 < fallThreshold || h1Low2 < fallThreshold)
		score += 7m;

		if (candle.ClosePrice > level100 && previousM1.ClosePrice < level100 &&
		h1High0.Value < baseHigh - tolerance && h1High1.Value < baseHigh - tolerance && h1High2.Value < baseHigh)
		{
			score += 45m;
		}

		if (CheckM1LowBelow(level100 - HighLevelPips * _pointValue, 12))
		score -= 50m;

		if (raviD1 > 2m && CheckM1ImpulseForSell())
		score -= 50m;

		if (!CheckH1BreakBelow(level100 - LowLevel2Pips * _pointValue))
		score -= 50m;

		if (CheckM30CompressionBelow(level100 - LowLevelPips * _pointValue))
		score -= 50m;

		return true;
	}

	private bool PlaceBuyLimit(ICandleMessage candle)
	{
		var volume = CalculateOrderVolume();
		if (volume <= 0m)
		return false;

		var entryPrice = Math.Max(candle.ClosePrice - 10m * _pipSize, 0m);
		var stopPrice = entryPrice - StopLossPips * _pipSize;
		var takePrice = entryPrice + TakeProfitPips * _pipSize;

		if (_raviD1Current is decimal ravi && _raviD1Prev1 is decimal prev1 && _raviD1Prev2 is decimal prev2 && _raviD1Prev3 is decimal prev3)
		{
			if (ravi > 1m && ravi < 5m && prev1 < ravi && prev2 < prev1 && prev3 < prev2)
			{
				takePrice += 25m * _pipSize;
			}
		}

		BuyLimit(price: entryPrice, volume: volume);
		_buyOrderExpiry = candle.CloseTime + TimeSpan.FromMinutes(OrderExpiryMinutes);
		_pendingBuyStop = stopPrice;
		_pendingBuyTake = takePrice;
		return true;
	}

	private bool PlaceSellLimit(ICandleMessage candle)
	{
		var volume = CalculateOrderVolume();
		if (volume <= 0m)
		return false;

		var entryPrice = candle.ClosePrice + 7m * _pipSize;
		var stopPrice = entryPrice + StopLossPips * _pipSize;
		var takePrice = entryPrice - TakeProfitPips * _pipSize;

		if (_raviD1Current is decimal ravi && _raviD1Prev1 is decimal prev1 && _raviD1Prev2 is decimal prev2 && _raviD1Prev3 is decimal prev3)
		{
			if (ravi < -1m && ravi > -5m && prev1 > ravi && prev2 > prev1 && prev3 > prev2)
			{
				takePrice -= 25m * _pipSize;
			}
		}

		SellLimit(price: entryPrice, volume: volume);
		_sellOrderExpiry = candle.CloseTime + TimeSpan.FromMinutes(OrderExpiryMinutes);
		_pendingSellStop = stopPrice;
		_pendingSellTake = takePrice;
		return true;
	}

	private bool CheckM1HighAbove(decimal threshold, int candles)
	{
		for (var i = 0; i < candles; i++)
		{
			var candle = GetM1Candle(i);
			if (candle is null)
			break;

			if (candle.HighPrice > threshold)
			return true;
		}

		return false;
	}

	private bool CheckM1LowBelow(decimal threshold, int candles)
	{
		for (var i = 0; i < candles; i++)
		{
			var candle = GetM1Candle(i);
			if (candle is null)
			break;

			if (candle.LowPrice < threshold)
			return true;
		}

		return false;
	}

	private bool CheckM1ImpulseForBuy()
	{
		for (var shift = 0; shift <= 30; shift++)
		{
			var current = GetM1Candle(shift);
			var future = GetM1Candle(shift + 3);

			if (current is null || future is null)
			break;

			if (future.HighPrice - current.LowPrice > 300m * _pointValue && future.OpenPrice > current.ClosePrice)
			return true;
		}

		return false;
	}

	private bool CheckM1ImpulseForSell()
	{
		for (var shift = 0; shift <= 30; shift++)
		{
			var current = GetM1Candle(shift);
			var future = GetM1Candle(shift + 3);

			if (current is null || future is null)
			break;

			if (current.HighPrice - future.LowPrice > 300m * _pointValue && current.ClosePrice > future.OpenPrice)
			return true;
		}

		return false;
	}

	private bool CheckH1BreakAbove(decimal threshold)
	{
		for (var shift = 0; shift <= 14; shift++)
		{
			var high = GetH1High(shift);
			if (high is null)
			break;

			if (high.Value > threshold)
			return true;
		}

		return false;
	}

	private bool CheckH1BreakBelow(decimal threshold)
	{
		for (var shift = 0; shift <= 14; shift++)
		{
			var low = GetH1Low(shift);
			if (low is null)
			break;

			if (low.Value < threshold)
			return true;
		}

		return false;
	}

	private bool CheckM30CompressionAbove(decimal threshold)
	{
		for (var shift = 0; shift <= 7; shift++)
		{
			var candle = GetM30Candle(shift);
			if (candle is null)
			return false;

			if (candle.HighPrice >= threshold)
			return false;
		}

		return true;
	}

	private bool CheckM30CompressionBelow(decimal threshold)
	{
		for (var shift = 0; shift <= 7; shift++)
		{
			var candle = GetM30Candle(shift);
			if (candle is null)
			return false;

			if (candle.LowPrice <= threshold)
			return false;
		}

		return true;
	}

	private ICandleMessage GetM1Candle(int shift)
	{
		var index = _m1History.Count - 1 - shift;
		return index >= 0 && index < _m1History.Count ? _m1History[index] : null;
	}

	private ICandleMessage GetM30Candle(int shift)
	{
		var index = _m30History.Count - 1 - shift;
		return index >= 0 && index < _m30History.Count ? _m30History[index] : null;
	}

	private decimal? GetH1High(int shift)
	{
		if (shift == 0)
		return _h1Current?.HighPrice;

		var index = _h1Finished.Count - shift;
		return index >= 0 && index < _h1Finished.Count ? _h1Finished[index].HighPrice : null;
	}

	private decimal? GetH1Low(int shift)
	{
		if (shift == 0)
		return _h1Current?.LowPrice;

		var index = _h1Finished.Count - shift;
		return index >= 0 && index < _h1Finished.Count ? _h1Finished[index].LowPrice : null;
	}

	private void CancelSideOrders(Sides side)
	{
		foreach (var order in Orders.Where(o => o.State is OrderStates.Active or OrderStates.Pending))
		{
			if (order.Type != OrderTypes.Limit || order.Side != side)
			continue;

			CancelOrder(order);
		}
	}

	private bool HasActiveLimitOrder(Sides side)
	{
		foreach (var order in Orders.Where(o => o.State is OrderStates.Active or OrderStates.Pending))
		{
			if (order.Type == OrderTypes.Limit && order.Side == side)
			return true;
		}

		return false;
	}

	private bool HasExposure()
	{
		if (Position != 0m)
		return true;

		foreach (var order in Orders.Where(o => o.State is OrderStates.Active or OrderStates.Pending))
		{
			if (order.Type == OrderTypes.Limit)
			return true;
		}

		return false;
	}

	private bool HasSufficientMargin()
	{
		if (MarginCutoff <= 0m)
		return true;

		var portfolio = Portfolio;
		if (portfolio is null)
		return true;

		var equity = portfolio.CurrentValue ?? 0m;
		if (equity <= 0m)
		equity = portfolio.BeginValue ?? 0m;

		return equity >= MarginCutoff;
	}

	private decimal CalculateOrderVolume()
	{
		if (!UseMoneyManagement)
		return FixedVolume;

		var portfolio = Portfolio;
		if (portfolio is null)
		return FixedVolume;

		var equity = portfolio.CurrentValue ?? 0m;
		if (equity <= 0m)
		equity = portfolio.BeginValue ?? 0m;

		if (equity <= 0m)
		return FixedVolume;

		var lot = Math.Floor(equity * TradeSizePercent / 1000m) / 100m;

		if (AccountIsMini)
		{
			lot = Math.Floor(lot * 100m) / 100m;
			if (lot < 0.1m)
			lot = 0.1m;
		}
		else
		{
			if (lot < 1m)
			lot = 1m;
		}

		if (lot > MaxVolume)
		lot = MaxVolume;

		return lot;
	}

	private void ResetTradeLevels()
	{
		_pendingBuyStop = null;
		_pendingBuyTake = null;
		_pendingSellStop = null;
		_pendingSellTake = null;
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
	}

	private static void TrimHistory(ICollection<ICandleMessage> history, int maxCount)
	{
		while (history.Count > maxCount)
		{
			if (history is List<ICandleMessage> list)
			list.RemoveAt(0);
			else
			break;
		}
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0.0001m;
		var decimals = Security?.Decimals ?? 0;
		var adjust = decimals == 3 || decimals == 5 ? 10m : 1m;
		var pip = step * adjust;
		return pip == 0m ? 0.0001m : pip;
	}
}