View on GitHub

Trend Scalper Strategy (API/3858)

Overview

The TrendScalperStrategy is a C# conversion of the MetaTrader 4 expert advisor Currencyprofits_01_1.mq4. The original robot is a lightweight trend-following scalper that combines a short-term EMA/SMA crossover filter with breakout entries around the most recent swing highs and lows. The StockSharp port keeps the same decision rules while embracing the framework's high-level candle subscription and indicator pipeline.

Trading Logic

  1. Indicators
    • Fast EMA (default 6) on close prices.
    • Slow SMA (default 12) on close prices.
    • Highest High (default window 6) and Lowest Low (default window 6) calculated from the candle highs and lows.
  2. Entry Conditions
    • Long: price sweeps into the recent low band (Lowest Low) while the fast EMA is above the slow SMA. The strategy sends a market buy order with the volume defined by the money-management rule.
    • Short: price touches the recent high band (Highest High) while the fast EMA is below the slow SMA. A market sell order is placed using the same volume calculation.
    • The system stays flat while a position is open, mirroring the single-order behaviour of the MQL version.
  3. Exit Conditions
    • Long exit: when an open long position sees the candle high break the recorded Highest High, the position is closed at market.
    • Short exit: when an open short position observes the candle low fall through the Lowest Low, the short is covered at market.
    • A protective stop-loss managed by StartProtection is attached to every trade when StopLossPoints is greater than zero.

Money Management

The lot sizing logic reproduces the three modes exposed in the MQL script:

Mode Description Behaviour in the port
0 Fixed lots (LotsIfNoMM). Returns the configured FixedVolume.
<0 Fractional lots computed from the account balance and risk factor. Calculates ceil(balance * risk / 10000) / 10, capped at 100 lots.
>0 Whole-lot scaling from balance and risk factor. Uses the same base formula, but the result is rounded up to the next integer, floored at 1 lot and capped at 100.

The balance is taken from Portfolio.CurrentValue (falling back to BeginValue). If the portfolio value is unavailable, the strategy reverts to the fixed volume so orders are still emitted during backtests.

Risk Management

  • Stop-loss: the StopLossPoints parameter is expressed in price points (pips). During OnStarted the distance is multiplied by Security.PriceStep and passed to StartProtection, letting StockSharp maintain the protective order.
  • Single position: the logic enforces Position == 0 before opening a new trade, preventing overlapping positions exactly like the MT4 expert.

Parameters

Name Default Description
CandleType 15-minute time frame Candle series used for indicator calculations and signals.
FastLength 6 Period of the fast EMA.
SlowLength 12 Period of the slow SMA.
BreakoutWindow 6 Number of candles inspected for the highest high / lowest low breakout filter.
FixedVolume 0.1 lots Volume when money management is disabled or fallback is required.
MoneyManagementMode 0 Selects between fixed, fractional, or rounded lot sizing.
MoneyManagementRisk 40 Risk factor multiplier used in balance-based lot sizing.
StopLossPoints 50 Stop-loss distance in price points (converted to absolute price before calling StartProtection).

Implementation Notes

  • Indicator chaining relies on the high-level SubscribeCandles().Bind(...) workflow; no manual series buffering is required.
  • Comments in the code were added in English to match the repository guidelines.
  • Behavioural contract tests for this strategy live in Tests/StrategyTests.Point1Part10.cs; they still require an executed BTC/TON replay.

Usage Tips

  • Select a candle interval that matches the original trading environment (e.g., short intraday time frames for scalping).
  • Ensure the portfolio has a valid PriceStep so the stop-loss conversion to absolute price works correctly.
  • Adjust MoneyManagementRisk carefully: higher values lead to larger positions because of the ceil(balance * risk / 10000) calculation inherited from the MQL expert.

Exact signal window: The highest high and lowest low are computed from the previous BreakoutWindow fully completed candles, excluding the signal candle. On a new close, a long requires EMA(FastLength) > SMA(SlowLength) and currentLow <= previousWindowLow; a short requires the reverse trend and currentHigh >= previousWindowHigh. Existing longs exit when currentHigh > previousWindowHigh, and shorts when currentLow < previousWindowLow. MA and channel warm-up are mandatory. An actual protective stop is also configured through StartProtection when StopLossPoints > 0. Default mode 0 uses FixedVolume; other modes use the documented rounded risk formula and instrument volume limits.

namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;
using System.Linq;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>
/// Fast EMA versus slow SMA direction filtered by a sweep of the previous
/// candle-high/low band. Independent natural exits and a native stop protect fills.
/// </summary>
public class TrendScalperStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _breakoutWindow;
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<int> _moneyManagementMode;
	private readonly StrategyParam<decimal> _moneyManagementRisk;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Queue<decimal> _fastSeed = new();
	private readonly Queue<decimal> _slowCloses = new();
	private readonly Queue<decimal> _highs = new();
	private readonly Queue<decimal> _lows = new();
	private decimal? _fastEma;
	private Order _pendingOrder;

	public int FastLength { get => _fastLength.Value; set => _fastLength.Value = value; }
	public int SlowLength { get => _slowLength.Value; set => _slowLength.Value = value; }
	public int BreakoutWindow { get => _breakoutWindow.Value; set => _breakoutWindow.Value = value; }
	public decimal FixedVolume { get => _fixedVolume.Value; set => _fixedVolume.Value = value; }
	public int MoneyManagementMode { get => _moneyManagementMode.Value; set => _moneyManagementMode.Value = value; }
	public decimal MoneyManagementRisk { get => _moneyManagementRisk.Value; set => _moneyManagementRisk.Value = value; }
	public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public TrendScalperStrategy()
	{
		_fastLength = Param(nameof(FastLength), 6).SetGreaterThanZero();
		_slowLength = Param(nameof(SlowLength), 12).SetGreaterThanZero();
		_breakoutWindow = Param(nameof(BreakoutWindow), 6).SetGreaterThanZero();
		_fixedVolume = Param(nameof(FixedVolume), 0.1m).SetGreaterThanZero();
		_moneyManagementMode = Param(nameof(MoneyManagementMode), 0);
		_moneyManagementRisk = Param(nameof(MoneyManagementRisk), 40m).SetNotNegative();
		_stopLossPoints = Param(nameof(StopLossPoints), 50m).SetNotNegative();
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame());
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType), (Security, DataType.Level1)];

	protected override void OnReseted()
	{
		base.OnReseted();
		ClearState();
	}

	private void ClearState()
	{
		_fastSeed.Clear();
		_slowCloses.Clear();
		_highs.Clear();
		_lows.Clear();
		_fastEma = null;
		_pendingOrder = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		ClearState();
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			throw new InvalidOperationException("Trend Scalper requires a positive PriceStep.");

		if (StopLossPoints > 0m)
		{
			StartProtection(new Unit(), new Unit(StopLossPoints * step, UnitTypes.Absolute),
				useMarketOrders: true, isLocalStop: true);
			foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
			{
				var quotes = new Subscription(DataType.Level1, Security);
				quotes.MarketData.BuildField = field;
				SubscribeLevel1(quotes).Bind(ObserveQuote).Start();
			}
		}
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
	}

	private void ObserveQuote(Level1ChangeMessage message)
	{
		// A Level1 stream is needed for native stop processing between candle closes.
	}

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

		var hasBand = _highs.Count == BreakoutWindow;
		var bandHigh = hasBand ? _highs.Max() : 0m;
		var bandLow = hasBand ? _lows.Min() : 0m;
		_highs.Enqueue(candle.HighPrice);
		_lows.Enqueue(candle.LowPrice);
		while (_highs.Count > BreakoutWindow) _highs.Dequeue();
		while (_lows.Count > BreakoutWindow) _lows.Dequeue();

		var price = candle.ClosePrice;
		if (_fastEma is null)
		{
			_fastSeed.Enqueue(price);
			if (_fastSeed.Count == FastLength)
				_fastEma = _fastSeed.Average();
		}
		else
			_fastEma += (price - _fastEma.Value) * 2m / (FastLength + 1);

		_slowCloses.Enqueue(price);
		while (_slowCloses.Count > SlowLength)
			_slowCloses.Dequeue();
		if (!hasBand || _fastEma is null || _slowCloses.Count < SlowLength ||
			_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
			return;

		if (Position > 0m && candle.HighPrice > bandHigh)
		{
			Submit(Sides.Sell, Position, "Trend scalper band exit");
			return;
		}
		if (Position < 0m && candle.LowPrice < bandLow)
		{
			Submit(Sides.Buy, Math.Abs(Position), "Trend scalper band exit");
			return;
		}
		if (Position != 0m)
			return;

		var slow = _slowCloses.Average();
		if (_fastEma > slow && candle.LowPrice <= bandLow)
			Enter(Sides.Buy);
		else if (_fastEma < slow && candle.HighPrice >= bandHigh)
			Enter(Sides.Sell);
	}

	private void Enter(Sides side)
	{
		var volume = CalculateOrderVolume();
		if (volume > 0m)
			Submit(side, volume, "Trend scalper entry");
	}

	private decimal CalculateOrderVolume()
	{
		var volume = FixedVolume;
		if (MoneyManagementMode != 0)
		{
			var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
			if (balance > 0m)
			{
				var fractional = Math.Ceiling(balance * MoneyManagementRisk / 10000m) / 10m;
				volume = MoneyManagementMode > 0 ? Math.Max(1m, Math.Ceiling(fractional)) : fractional;
				volume = Math.Min(volume, 100m);
			}
		}
		if (Security?.VolumeStep is decimal step && step > 0m)
			volume = decimal.Floor(volume / step) * step;
		if (Security?.MinVolume is decimal min && min > 0m)
			volume = Math.Max(volume, min);
		if (Security?.MaxVolume is decimal max && max > 0m)
			volume = Math.Min(volume, max);
		return volume;
	}

	private void Submit(Sides side, decimal volume, string comment)
	{
		_pendingOrder = new Order
		{
			Security = Security,
			Portfolio = Portfolio,
			Type = OrderTypes.Market,
			Side = side,
			Volume = volume,
			Comment = comment,
		};
		RegisterOrder(_pendingOrder);
	}
}