GitHub で見る

WOC 0.1.2 Momentum Strategy

Overview

This strategy is a high-level StockSharp port of the MetaTrader expert advisor "WOC.0.1.2". It listens to Level 1 best bid/ask updates and searches for fast price streaks on the ask side. When the ask price prints a configurable number of consecutive higher or lower ticks within a limited time window, the strategy opens a market position in the breakout direction. Only one position can be open at any moment, which mirrors the single-position behaviour of the original code.

Data and Execution

  • Market data: Level 1 best bid and best ask. The algorithm does not require candles or indicators.
  • Execution: Market orders. Protective exits are emulated inside the strategy by checking bid/ask updates.

Signal Logic

  1. Track the latest ask price and measure how many consecutive new highs (up streak) or new lows (down streak) have been printed.
  2. When an up streak or down streak reaches SequenceLength, check that the streak duration is less than or equal to SequenceTimeoutSeconds seconds.
  3. If the down streak is longer than the up streak, send a sell order; otherwise send a buy order. The check reproduces the original MetaTrader logic where the streak with the highest counter defines the direction.
  4. Reset all streak counters after each entry attempt to ensure the next signal starts from scratch.

Position Management

  • Initial stop: After an entry the strategy immediately records a stop-loss price that is StopLossTicks price steps away from the current bid (for longs) or ask (for shorts).
  • Trailing stop: When price moves in favour of the trade by more than TrailingStopTicks price steps, the stop is tightened to TrailingStopTicks behind the latest bid/ask, as long as the stop remains at least double the trailing distance away from the current price. This reproduces the two-step trailing condition from the MQL expert.
  • Exit execution: When the tracked bid/ask crosses the stored stop level the position is closed via a market order. After the exit the internal state is reset to accept new streaks.

Volume Management

Two position sizing modes are supported:

  • Fixed lot: Use the LotSize parameter as absolute order volume.
  • Auto Lots: Enable UseAutoLotSizing to map the account balance to volume tiers. The balance is taken from Portfolio.CurrentValue and falls back to Portfolio.BeginValue if the current value is unavailable.
Balance (greater than) Volume
0 (default) LotSize
200 0.04
300 0.05
400 0.06
500 0.07
600 0.08
700 0.09
800 0.10
900 0.20
1 000 0.30
2 000 0.40
3 000 0.50
4 000 0.60
5 000 0.70
6 000 0.80
7 000 0.90
8 000 1.00
9 000 2.00
10 000 3.00
11 000 4.00
12 000 5.00
13 000 6.00
14 000 7.00
15 000 8.00
20 000 9.00
30 000 10.00
40 000 11.00
50 000 12.00
60 000 13.00
70 000 14.00
80 000 15.00
90 000 16.00
100 000 17.00
110 000 18.00
120 000 19.00
130 000 20.00

Parameters

  • StopLossTicks – stop-loss distance measured in price steps.
  • TrailingStopTicks – trailing distance measured in price steps (can be zero to disable trailing).
  • SequenceLength – number of consecutive ask moves required before entering a trade.
  • SequenceTimeoutSeconds – maximum duration of the streak in seconds.
  • LotSize – fixed order size used when auto-lot sizing is disabled.
  • UseAutoLotSizing – enables the balance-based volume table shown above.

Usage Notes

  • Works best on fast instruments where the best ask updates frequently; consider testing on tick-level data feeds.
  • The strategy requires hedging accounts because it never holds opposite positions simultaneously.
  • Ensure that Security.PriceStep is configured; otherwise the stop-loss and trailing calculations fall back to a distance of 1 monetary unit per tick.
  • Only one open position is supported at a time, mirroring the original MQL behaviour.
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Momentum strategy based on WOC 0.1.2 concept.
/// Detects consecutive candle close runs in one direction and enters on breakout.
/// Uses ATR-based stop loss and trailing stop.
/// </summary>
public class Woc012Strategy : Strategy
{
	private readonly StrategyParam<int> _sequenceLength;
	private readonly StrategyParam<decimal> _stopLossAtrMult;
	private readonly StrategyParam<decimal> _trailingAtrMult;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevClose;
	private int _upCount;
	private int _downCount;
	private decimal _entryPrice;
	private decimal? _stopPrice;

	public int SequenceLength { get => _sequenceLength.Value; set => _sequenceLength.Value = value; }
	public decimal StopLossAtrMult { get => _stopLossAtrMult.Value; set => _stopLossAtrMult.Value = value; }
	public decimal TrailingAtrMult { get => _trailingAtrMult.Value; set => _trailingAtrMult.Value = value; }
	public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public Woc012Strategy()
	{
		_sequenceLength = Param(nameof(SequenceLength), 6)
			.SetGreaterThanZero()
			.SetDisplay("Sequence Length", "Consecutive bars in same direction to trigger entry", "Signals");

		_stopLossAtrMult = Param(nameof(StopLossAtrMult), 1.5m)
			.SetGreaterThanZero()
			.SetDisplay("SL ATR Mult", "Stop loss as ATR multiple", "Risk");

		_trailingAtrMult = Param(nameof(TrailingAtrMult), 1.0m)
			.SetGreaterThanZero()
			.SetDisplay("Trail ATR Mult", "Trailing stop as ATR multiple", "Risk");

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR calculation length", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_prevClose = 0;
		_upCount = 0;
		_downCount = 0;
		_entryPrice = 0;
		_stopPrice = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var atr = new AverageTrueRange { Length = AtrPeriod };

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

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

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

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var close = candle.ClosePrice;

		// Track consecutive direction
		if (_prevClose > 0)
		{
			if (close > _prevClose)
			{
				_upCount++;
				_downCount = 0;
			}
			else if (close < _prevClose)
			{
				_downCount++;
				_upCount = 0;
			}
			else
			{
				_upCount = 0;
				_downCount = 0;
			}
		}

		_prevClose = close;

		// Manage existing position
		if (Position != 0)
		{
			if (Position > 0)
			{
				// Trail up
				var trail = close - TrailingAtrMult * atr;
				if (_stopPrice == null || trail > _stopPrice)
					_stopPrice = trail;

				if (close <= _stopPrice)
				{
					SellMarket(Math.Abs(Position));
					_stopPrice = null;
					_entryPrice = 0;
					return;
				}
			}
			else
			{
				// Trail down
				var trail = close + TrailingAtrMult * atr;
				if (_stopPrice == null || trail < _stopPrice)
					_stopPrice = trail;

				if (close >= _stopPrice)
				{
					BuyMarket(Math.Abs(Position));
					_stopPrice = null;
					_entryPrice = 0;
					return;
				}
			}
		}

		// Entry: consecutive sequence completed
		if (_upCount >= SequenceLength && Position <= 0)
		{
			var vol = Volume + Math.Abs(Position);
			BuyMarket(vol);
			_entryPrice = close;
			_stopPrice = close - StopLossAtrMult * atr;
			_upCount = 0;
		}
		else if (_downCount >= SequenceLength && Position >= 0)
		{
			var vol = Volume + Math.Abs(Position);
			SellMarket(vol);
			_entryPrice = close;
			_stopPrice = close + StopLossAtrMult * atr;
			_downCount = 0;
		}
	}
}