View on GitHub

Cycle Market Order Strategy

Converted from the MetaTrader 4 expert advisor "CycleMarketOrder_V181". The strategy organises a fixed number of slots inside a price ladder and opens market orders when the live bid/ask trades through an individual slot. Each slot carries its own volume, break-even threshold and trailing stop value so the grid can gradually scale into a position while protecting profits that already reached the required distance.

Trading logic

  1. The pip size is derived from the instrument price step and decimal precision (5/3-digit symbols map to 10 points per pip). The MaxPrice, SpanPips and MaxCount parameters are then used to pre-compute the price range handled by each slot.
  2. Level-1 market data is consumed to mirror the tick-based behaviour of the original Expert Advisor. Each update refreshes the cached best bid/ask prices.
  3. If UseWeekendMode is enabled the strategy refuses to trade outside the configured weekend window (Saturday from WeekendHour, the whole Sunday and Monday before WeekstartHour).
  4. For long cycles (EntryDirection = 1) the algorithm scans slots from lowest to highest identifier. Whenever the current ask price falls between the slot's startPrice and endPrice, a market buy order with OrderVolume volume is sent. Short cycles (EntryDirection = -1) mirror this logic and use the bid price.
  5. Slot states track pending entry/exit orders, filled volume and the average entry price. Logging uses MagicNumberBase + index to match the MT4 "magic" identifiers.
  6. Trailing management is executed on every level-1 update before new entries are evaluated. Once the profit on a long slot exceeds BreakEvenPips + TrailingStopPips, the stop is pushed to Bid - TrailingStopPips. Short slots use Ask + TrailingStopPips and the mirrored break-even condition. When the market price crosses the stored stop the slot is closed with a market order.
  7. Because only market orders are used there are no pending orders to cancel. Partial fills adjust the remaining slot volume so the strategy can continue to trail or re-arm the slot once it becomes flat.

Parameters

Parameter Description
EntryDirection Trading direction: 1 buys the ladder, -1 sells it, 0 disables new entries while keeping trailing active.
MaxPrice Upper anchor price used to calculate the slot ranges.
MaxCount Total number of active slots inside the grid.
SpanPips Distance in pips between consecutive slot boundaries.
OrderVolume Volume submitted when a slot triggers.
BreakEvenPips Profit distance that must be exceeded before the trailing stop is armed.
TrailingStopPips Trailing distance applied once break-even is achieved.
UseWeekendMode Enables the weekend trading blackout window.
WeekendHour Hour on Saturday (terminal time) when trading is halted.
WeekstartHour Hour on Monday when trading resumes.
MagicNumberBase Identifier offset used in log messages to match the original magic numbers.

Implementation notes

  • Slot management keeps track of pending entry and exit orders so that repeated fills do not register duplicate volume.
  • The strategy resets its trailing stop whenever a new fill increases the slot's exposure, ensuring that the stop reflects the most recent average entry price.
  • Weekend protection simply skips both trailing and entry logic; existing positions remain untouched while the blackout is active.
  • Level-1 data is required because the logic compares raw bid/ask prices instead of candle closes, closely replicating the tick-by-tick behaviour of the MT4 version.
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

public class CycleMarketOrderStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public CycleMarketOrderStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}