View on GitHub

Nova Strategy

Overview

  • Conversion of the MetaTrader 5 "Nova" expert advisor that monitors price momentum over a fixed number of seconds.
  • Works with any candle type chosen through the CandleType parameter and evaluates logic only on finished candles.
  • Tracks the best ask and bid prices using Level1 data and stores their values from SecondsAgo seconds earlier.
  • Enters a long position when the previous candle is bullish and the current ask is higher than the stored ask by at least StepPips.
  • Enters a short position when the previous candle is bearish and the current bid is lower than the stored ask by at least StepPips.
  • Applies automatic stop-loss and take-profit levels using StockSharp protection if the corresponding parameters are greater than zero.
  • After a loss (stop-loss activation) the next trade volume is multiplied by LossCoefficient; after a profitable exit the volume is reset to BaseVolume.

Parameters

  • SecondsAgo – number of seconds between the reference price snapshot and the current evaluation moment.
  • StepPips – breakout filter in pips; converted into price units using the security price step (3/5 decimal instruments are adjusted by ×10).
  • BaseVolume – initial trade size; normalized to the exchange volume step and min/max limits.
  • StopLossPips – distance in pips for the protective stop-loss (0 disables it).
  • TakeProfitPips – distance in pips for the protective take-profit (0 disables it).
  • LossCoefficient – multiplier applied to the last executed volume after a losing trade.
  • CandleType – candle source used for signals (timeframe, tick, range, etc.).

Additional Notes

  • The strategy requires Level1 data (best bid/ask) to replicate the original MT5 behaviour; candles provide a fallback using their close price when Level1 is unavailable.
  • Volume recalculation respects Security.VolumeStep, Security.MinVolume, and Security.MaxVolume to avoid invalid orders.
  • Price conversions rely on Security.PriceStep and Security.Decimals so the strategy adapts to both 4/5-digit forex symbols and other instruments.
  • No Python version is provided for this strategy.
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;
	}
}