Ver en GitHub

Estrategia Nova

Descripción General

  • Conversión del asesor experto "Nova" de MetaTrader 5 que monitorea el momentum del precio en un número fijo de segundos.
  • Funciona con cualquier tipo de vela elegida a través del parámetro CandleType y evalúa la lógica solo en velas completadas.
  • Rastrea los mejores precios de compra y venta usando datos Level1 y almacena sus valores de SecondsAgo segundos antes.
  • Entra en una posición larga cuando la vela anterior es alcista y el ask actual es mayor que el ask almacenado por al menos StepPips.
  • Entra en una posición corta cuando la vela anterior es bajista y el bid actual es menor que el ask almacenado por al menos StepPips.
  • Aplica niveles automáticos de stop-loss y take-profit usando protección de StockSharp si los parámetros correspondientes son mayores que cero.
  • Después de una pérdida (activación del stop-loss), el volumen del siguiente trade se multiplica por LossCoefficient; después de una salida rentable el volumen se resetea a BaseVolume.

Parámetros

  • SecondsAgo – número de segundos entre la instantánea del precio de referencia y el momento de evaluación actual.
  • StepPips – filtro de ruptura en pips; convertido a unidades de precio usando el paso de precio de la seguridad (los instrumentos de 3/5 decimales se ajustan por ×10).
  • BaseVolume – tamaño inicial del trade; normalizado al paso de volumen del intercambio y límites mín/máx.
  • StopLossPips – distancia en pips para el stop-loss de protección (0 lo deshabilita).
  • TakeProfitPips – distancia en pips para el take-profit de protección (0 lo deshabilita).
  • LossCoefficient – multiplicador aplicado al último volumen ejecutado después de un trade perdedor.
  • CandleType – fuente de velas usada para señales (marco temporal, tick, rango, etc.).

Notas Adicionales

  • La estrategia requiere datos Level1 (mejor bid/ask) para replicar el comportamiento original de MT5; las velas proporcionan un respaldo usando su precio de cierre cuando Level1 no está disponible.
  • El recálculo de volumen respeta Security.VolumeStep, Security.MinVolume y Security.MaxVolume para evitar órdenes inválidas.
  • Las conversiones de precio dependen de Security.PriceStep y Security.Decimals para que la estrategia se adapte tanto a símbolos forex de 4/5 dígitos como a otros instrumentos.
  • No se proporciona versión en Python para esta estrategia.
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>
/// Nova strategy converted from MQL that compares the current price with the price N seconds ago.
/// Opens a long position when the previous candle is bullish and the ask price has moved up by a threshold.
/// Opens a short position when the previous candle is bearish and the bid price has dropped below the stored ask price.
/// After a stop-loss the position size is multiplied by a coefficient, after a take-profit it resets to the base volume.
/// </summary>
public class NovaStrategy : Strategy
{
	private readonly StrategyParam<int> _secondsAgo;
	private readonly StrategyParam<int> _stepPips;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<decimal> _lossCoefficient;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _referenceAsk;
	private decimal? _referenceBid;
	private DateTimeOffset? _lastCheckTime;
	private decimal _stepOffset;
	private decimal _stopLossOffset;
	private decimal _takeProfitOffset;
	private decimal _currentVolume;
	private decimal? _lastTradeVolume;
	private decimal _previousPnL;
	private decimal? _currentAsk;
	private decimal? _currentBid;
	private bool _hasPreviousCandle;
	private decimal _prevCandleOpen;
	private decimal _prevCandleClose;

	/// <summary>
	/// Seconds to look back for the price comparison.
	/// </summary>
	public int SecondsAgo
	{
		get => _secondsAgo.Value;
		set => _secondsAgo.Value = value;
	}

	/// <summary>
	/// Step in pips that is required for the breakout condition.
	/// </summary>
	public int StepPips
	{
		get => _stepPips.Value;
		set => _stepPips.Value = value;
	}

	/// <summary>
	/// Base trading volume.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

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

	/// <summary>
	/// Coefficient used to increase the volume after a stop-loss.
	/// </summary>
	public decimal LossCoefficient
	{
		get => _lossCoefficient.Value;
		set => _lossCoefficient.Value = value;
	}

	/// <summary>
	/// Candle type used for signal generation.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public NovaStrategy()
	{
		_secondsAgo = Param(nameof(SecondsAgo), 10)
		.SetGreaterThanZero()
		.SetDisplay("Seconds window", "Seconds to look back for price comparison", "General")
		
		.SetOptimize(5, 30, 5);

		_stepPips = Param(nameof(StepPips), 1)
		.SetNotNegative()
		.SetDisplay("Step (pips)", "Price offset in pips for breakout check", "Signals")
		
		.SetOptimize(0, 5, 1);

		_baseVolume = Param(nameof(BaseVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Base volume", "Initial order volume", "Risk")
		
		.SetOptimize(0.05m, 0.5m, 0.05m);

		_stopLossPips = Param(nameof(StopLossPips), 500)
		.SetNotNegative()
		.SetDisplay("Stop-loss (pips)", "Stop-loss distance in pips", "Risk")
		.SetOptimize(0, 5, 1);

		_takeProfitPips = Param(nameof(TakeProfitPips), 500)
		.SetNotNegative()
		.SetDisplay("Take-profit (pips)", "Take-profit distance in pips", "Risk")
		.SetOptimize(0, 5, 1);

		_lossCoefficient = Param(nameof(LossCoefficient), 1.6m)
		.SetGreaterThanZero()
		.SetDisplay("Loss coefficient", "Multiplier for the next trade after a stop-loss", "Risk")
		
		.SetOptimize(1m, 2.5m, 0.1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle type", "Candles used for signal calculations", "General");
	}

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

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

		_referenceAsk = null;
		_referenceBid = null;
		_lastCheckTime = null;
		_stepOffset = 0m;
		_stopLossOffset = 0m;
		_takeProfitOffset = 0m;
		_currentVolume = 0m;
		_lastTradeVolume = null;
		_previousPnL = 0m;
		_currentAsk = null;
		_currentBid = null;
		_hasPreviousCandle = false;
		_prevCandleOpen = 0m;
		_prevCandleClose = 0m;
	}

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

		_previousPnL = PnL;

		var pipSize = GetPipSize();
		_stepOffset = StepPips * pipSize;
		_stopLossOffset = StopLossPips * pipSize;
		_takeProfitOffset = TakeProfitPips * pipSize;

		_currentVolume = NormalizeVolume(BaseVolume);
		Volume = _currentVolume;

		var candleSubscription = SubscribeCandles(CandleType);
		candleSubscription
		.Bind(ProcessCandle)
		.Start();

		if (StopLossPips > 0 || TakeProfitPips > 0)
		{
			StartProtection(
			TakeProfitPips > 0 ? new Unit(_takeProfitOffset, UnitTypes.Absolute) : default,
			StopLossPips > 0 ? new Unit(_stopLossOffset, UnitTypes.Absolute) : default);
		}

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, candleSubscription);
			DrawOwnTrades(area);
		}
	}

	private void ProcessLevel1(Level1ChangeMessage level1)
	{
		if (level1.Changes.TryGetValue(Level1Fields.BestAskPrice, out var ask))
		_currentAsk = (decimal)ask;

		if (level1.Changes.TryGetValue(Level1Fields.BestBidPrice, out var bid))
		_currentBid = (decimal)bid;
	}

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

		UpdateVolumeFromPnL();

		if (!_hasPreviousCandle)
		{
			_hasPreviousCandle = true;
			_prevCandleOpen = candle.OpenPrice;
			_prevCandleClose = candle.ClosePrice;
			return;
		}

		if (Position != 0)
		return;

		var now = candle.CloseTime;
		var interval = TimeSpan.FromSeconds(SecondsAgo);

		if (_lastCheckTime != null && now - _lastCheckTime < interval)
		return;

		var currentAsk = candle.ClosePrice;
		var currentBid = candle.ClosePrice;

		if (currentAsk == 0m || currentBid == 0m)
		{
			_referenceAsk = null;
			_referenceBid = null;
			_lastCheckTime = now;
			return;
		}

		if (_referenceAsk is null || _referenceBid is null)
		{
			_referenceAsk = currentAsk;
			_referenceBid = currentBid;
			_lastCheckTime = now;
			return;
		}

		var bullishPrevious = _prevCandleClose > _prevCandleOpen;
		var bearishPrevious = _prevCandleClose < _prevCandleOpen;
		var referenceAsk = _referenceAsk.Value;

		if (bullishPrevious && currentAsk - _stepOffset > referenceAsk)
		{
			TryEnterLong(currentAsk);
		}
		else if (bearishPrevious && currentBid + _stepOffset < referenceAsk)
		{
			TryEnterShort(currentBid);
		}

		_referenceAsk = currentAsk;
		_referenceBid = currentBid;
		_lastCheckTime = now;
		_prevCandleOpen = candle.OpenPrice;
		_prevCandleClose = candle.ClosePrice;
	}

	private void TryEnterLong(decimal price)
	{
		if (_currentVolume <= 0m)
		return;

		BuyMarket();
		_lastTradeVolume = _currentVolume;
		LogInfo($"Open long at {price:F5} with volume {_currentVolume:F2}");
	}

	private void TryEnterShort(decimal price)
	{
		if (_currentVolume <= 0m)
		return;

		SellMarket();
		_lastTradeVolume = _currentVolume;
		LogInfo($"Open short at {price:F5} with volume {_currentVolume:F2}");
	}

	private void UpdateVolumeFromPnL()
	{
		var realizedPnL = PnL;
		if (realizedPnL == _previousPnL)
		return;

		var delta = realizedPnL - _previousPnL;
		_previousPnL = realizedPnL;

		if (delta > 0m)
		{
			_currentVolume = NormalizeVolume(BaseVolume);
			Volume = _currentVolume;
			LogInfo("Reset volume after profitable trade");
		}
		else if (delta < 0m)
		{
			var referenceVolume = _lastTradeVolume ?? _currentVolume;
			_currentVolume = NormalizeVolume(referenceVolume * LossCoefficient);
			Volume = _currentVolume;
			LogInfo($"Increase volume after loss to {_currentVolume:F2}");
		}
	}

	private decimal NormalizeVolume(decimal volume)
	{
		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			volume = Math.Floor(volume / step) * step;
		}

		var min = Security?.MinVolume ?? 0m;
		if (min > 0m && volume < min)
		{
			volume = min;
		}

		var max = Security?.MaxVolume ?? 0m;
		if (max > 0m && volume > max)
		{
			volume = max;
		}

		return volume;
	}

	private decimal GetPipSize()
	{
		var step = Security?.PriceStep ?? 1m;
		var decimals = Security?.Decimals ?? 0;
		var factor = decimals is 3 or 5 ? 10m : 1m;
		return step * factor;
	}
}