GitHub で見る

NUp1Down Strategy

Overview

The NUp1Down Strategy is a direct conversion of the MetaTrader 5 expert "N bars up, then one bar down" (file NUp1Down.mq5). It scans completed candles delivered by StockSharp and enters a short trade when a bearish candle appears after a configurable sequence of bullish candles that keep making higher closes. The strategy is designed for discretionary traders who want to automate a classical swing-reversal pattern inside StockSharp Designer, Shell, or Runner.

Trading Logic

  1. Work only on finished candles provided by the CandleType parameter.
  2. Keep the latest BarsCount + 1 candles in memory. The newest candle must close below its open (bearish setup candle).
  3. The previous BarsCount candles all have to close above their opens. Each of these bullish candles (except the oldest one) must also close above the close of the candle that came right before it, enforcing a "staircase" move higher.
  4. When the pattern validates and there is no active short position, the strategy submits a market sell order.
  5. Position sizing uses the RiskPercent parameter. The algorithm estimates how many contracts can be opened so that the capital at risk (distance to the stop-loss converted into monetary value) does not exceed the chosen percentage of the portfolio. The base Volume property remains the minimum lot size and the risk model can only increase the trade size.

Position Management

  • Upon entry a protective stop-loss and a take-profit level are computed from the entry price. Both distances are expressed in pips and translated into prices using the instrument's PriceStep. For symbols with three or five decimal digits the pip size is automatically adjusted to match MetaTrader's pip definition.
  • A trailing stop is recalculated on every finished candle. The trailing distance equals TrailingStopPips and the stop is shifted only if the price has moved at least TrailingStepPips in the trade's favor. The trailing logic emulates the original expert: for short trades it follows the ask price lower, while long trades are not produced by this strategy.
  • Exit conditions are evaluated before looking for new entries on every candle. The strategy closes the position when either the stop-loss or the take-profit is hit, or when the trailing logic tightens the stop above the current ask price.

Parameters

Name Description
BarsCount Number of bullish candles required before the bearish setup candle (default: 3).
TakeProfitPips Take-profit distance in pips applied to the entry price (default: 50).
StopLossPips Stop-loss distance in pips applied to the entry price (default: 50).
TrailingStopPips Distance between market price and the trailing stop (default: 10).
TrailingStepPips Minimum favorable movement before the trailing stop is advanced (default: 5).
RiskPercent Percentage of portfolio capital to risk on each trade (default: 5).
CandleType Candle data type/time frame used for pattern detection (default: 1 hour).

Usage Notes

  • Configure the Volume property to the minimum order size allowed by your broker. The risk-based sizing may raise the trade size but never reduces it below Volume.
  • The strategy keeps only one aggregated short position at any time. If a long position exists, it will be closed before opening the short.
  • The algorithm works on candle data. Intrabar stop-loss or take-profit hits are detected using the candle high/low, so the actual fill timing may differ from tick-level execution.
  • No Python version is provided in this release. Only the C# implementation inside API/2574/CS/NUp1DownStrategy.cs is available.
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 System.Globalization;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy that sells after a sequence of bullish candles followed by a bearish candle.
/// </summary>
public class NUp1DownStrategy : Strategy
{
	private readonly StrategyParam<int> _barsCount;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Queue<(decimal Open, decimal Close)> _recentCandles = new();

	private decimal _pipSize;
	private decimal? _entryPrice;
	private decimal? _activeStopPrice;
	private decimal? _activeTakePrice;

	/// <summary>
	/// Number of consecutive bullish bars required before the bearish setup candle.
	/// </summary>
	public int BarsCount
	{
		get => _barsCount.Value;
		set => _barsCount.Value = value;
	}

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

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

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

	/// <summary>
	/// Trailing stop step in pips.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Risk percentage used to size the position.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="NUp1DownStrategy"/> class.
	/// </summary>
	public NUp1DownStrategy()
	{
		Volume = 1m;

		_barsCount = Param(nameof(BarsCount), 3)
			.SetGreaterThanZero()
			.SetDisplay("Bullish Bars", "Number of bullish bars before the down bar", "General")
			
			.SetOptimize(2, 6, 1);

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk")
			
			.SetOptimize(20m, 120m, 10m);

		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk")
			
			.SetOptimize(20m, 120m, 10m);

		_trailingStopPips = Param(nameof(TrailingStopPips), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk")
			
			.SetOptimize(5m, 30m, 5m);

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Step (pips)", "Trailing step before adjusting stop", "Risk")
			
			.SetOptimize(1m, 20m, 1m);

		_riskPercent = Param(nameof(RiskPercent), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Risk %", "Portfolio risk percentage per trade", "Money Management")
			
			.SetOptimize(1m, 10m, 1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Time frame for candle analysis", "General");
	}

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

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

		_recentCandles.Clear();
		_pipSize = 0m;
		ResetPositionState();
	}

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

		_pipSize = CalculatePipSize();

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

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

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

		UpdateTrailingAndExits(candle);

		_recentCandles.Enqueue((candle.OpenPrice, candle.ClosePrice));
		while (_recentCandles.Count > BarsCount + 1)
			_recentCandles.Dequeue();

		if (_recentCandles.Count < BarsCount + 1)
			return;

		var candles = _recentCandles.ToArray();
		var last = candles[^1];

		if (last.Close >= last.Open)
			return;

		var isPattern = true;

		for (var i = 1; i <= BarsCount; i++)
		{
			var index = candles.Length - 1 - i;
			var bar = candles[index];

			if (bar.Close <= bar.Open)
			{
				isPattern = false;
				break;
			}

			if (i < BarsCount)
			{
				var prev = candles[index - 1];
				if (bar.Close <= prev.Close)
				{
					isPattern = false;
					break;
				}
			}
		}

		if (!isPattern)
			return;

		if (Position < 0)
			return;

		SellMarket();

		_entryPrice = candle.ClosePrice;
		_activeStopPrice = _entryPrice + StopLossPips * _pipSize;
		_activeTakePrice = _entryPrice - TakeProfitPips * _pipSize;

		this.LogInfo($"Short entry after {BarsCount} bullish bars at {_entryPrice:0.#####}");
	}

	private void UpdateTrailingAndExits(ICandleMessage candle)
	{
		if (Position < 0)
		{
			var volumeToClose = Math.Abs(Position);
			if (volumeToClose <= 0m)
				return;

			if (_activeStopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket();
				this.LogInfo($"Short exit by stop-loss at {stop:0.#####}");
				ResetPositionState();
				return;
			}

			if (_activeTakePrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket();
				this.LogInfo($"Short exit by take-profit at {take:0.#####}");
				ResetPositionState();
				return;
			}

			if (_activeStopPrice is decimal trailingStop)
			{
				var trailingDistance = TrailingStopPips * _pipSize;
				var trailingStep = TrailingStepPips * _pipSize;

				if (trailingDistance <= 0m)
					return;

				var currentAsk = candle.ClosePrice;
				var newStopCandidate = currentAsk + trailingDistance;

				if (newStopCandidate + trailingStep < trailingStop)
				{
					_activeStopPrice = newStopCandidate;
					this.LogInfo($"Short trailing stop moved to {_activeStopPrice:0.#####}");
				}
			}
		}
		else if (Position == 0)
		{
			ResetPositionState();
		}
	}

	private decimal CalculatePipSize()
	{
		if (Security?.PriceStep is decimal step && step > 0m)
		{
			var decimals = CountDecimalPlaces(step);
			return decimals is 3 or 5 ? step * 10m : step;
		}

		return 1m;
	}

	private static int CountDecimalPlaces(decimal value)
	{
		var text = value.ToString(CultureInfo.InvariantCulture);
		var separatorIndex = text.IndexOf('.');
		return separatorIndex >= 0 ? text.Length - separatorIndex - 1 : 0;
	}

	private decimal CalculateOrderVolume()
	{
		var baseVolume = Volume;
		var stopDistance = StopLossPips * _pipSize;

		if (Portfolio == null || stopDistance <= 0m)
			return baseVolume;

		var priceStep = Security?.PriceStep ?? 0m;

		if (priceStep <= 0m)
			return baseVolume;

		var capital = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		if (capital <= 0m)
			return baseVolume;

		var riskAmount = capital * (RiskPercent / 100m);
		if (riskAmount <= 0m)
			return baseVolume;

		var riskPerUnit = stopDistance;
		if (riskPerUnit <= 0m)
			return baseVolume;

		var volumeFromRisk = riskAmount / riskPerUnit;
		if (volumeFromRisk <= 0m)
			return baseVolume;

		return Math.Max(baseVolume, volumeFromRisk);
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_activeStopPrice = null;
		_activeTakePrice = null;
	}
}