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
Track the latest ask price and measure how many consecutive new highs (up streak) or new lows (down streak) have been printed.
When an up streak or down streak reaches SequenceLength, check that the streak duration is less than or equal to SequenceTimeoutSeconds seconds.
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.
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;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import AverageTrueRange
class woc012_strategy(Strategy):
def __init__(self):
super(woc012_strategy, self).__init__()
self._sequence_length = self.Param("SequenceLength", 6) \
.SetGreaterThanZero() \
.SetDisplay("Sequence Length", "Consecutive bars in same direction to trigger entry", "Signals")
self._stop_loss_atr_mult = self.Param("StopLossAtrMult", 1.5) \
.SetGreaterThanZero() \
.SetDisplay("SL ATR Mult", "Stop loss as ATR multiple", "Risk")
self._trailing_atr_mult = self.Param("TrailingAtrMult", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("Trail ATR Mult", "Trailing stop as ATR multiple", "Risk")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("ATR Period", "ATR calculation length", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))) \
.SetDisplay("Candle Type", "Candle timeframe", "General")
self._prev_close = 0.0
self._up_count = 0
self._down_count = 0
self._entry_price = 0.0
self._stop_price = None
@property
def SequenceLength(self):
return self._sequence_length.Value
@SequenceLength.setter
def SequenceLength(self, value):
self._sequence_length.Value = value
@property
def StopLossAtrMult(self):
return self._stop_loss_atr_mult.Value
@StopLossAtrMult.setter
def StopLossAtrMult(self, value):
self._stop_loss_atr_mult.Value = value
@property
def TrailingAtrMult(self):
return self._trailing_atr_mult.Value
@TrailingAtrMult.setter
def TrailingAtrMult(self, value):
self._trailing_atr_mult.Value = value
@property
def AtrPeriod(self):
return self._atr_period.Value
@AtrPeriod.setter
def AtrPeriod(self, value):
self._atr_period.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(woc012_strategy, self).OnStarted2(time)
atr = AverageTrueRange()
atr.Length = self.AtrPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription \
.Bind(atr, self.process_candle) \
.Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def process_candle(self, candle, atr_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
atr = float(atr_val)
if self._prev_close > 0:
if close > self._prev_close:
self._up_count += 1
self._down_count = 0
elif close < self._prev_close:
self._down_count += 1
self._up_count = 0
else:
self._up_count = 0
self._down_count = 0
self._prev_close = close
if self.Position != 0:
if self.Position > 0:
trail = close - float(self.TrailingAtrMult) * atr
if self._stop_price is None or trail > self._stop_price:
self._stop_price = trail
if close <= self._stop_price:
self.SellMarket(abs(self.Position))
self._stop_price = None
self._entry_price = 0.0
return
else:
trail = close + float(self.TrailingAtrMult) * atr
if self._stop_price is None or trail < self._stop_price:
self._stop_price = trail
if close >= self._stop_price:
self.BuyMarket(abs(self.Position))
self._stop_price = None
self._entry_price = 0.0
return
if self._up_count >= self.SequenceLength and self.Position <= 0:
vol = self.Volume + abs(self.Position)
self.BuyMarket(vol)
self._entry_price = close
self._stop_price = close - float(self.StopLossAtrMult) * atr
self._up_count = 0
elif self._down_count >= self.SequenceLength and self.Position >= 0:
vol = self.Volume + abs(self.Position)
self.SellMarket(vol)
self._entry_price = close
self._stop_price = close + float(self.StopLossAtrMult) * atr
self._down_count = 0
def OnReseted(self):
super(woc012_strategy, self).OnReseted()
self._prev_close = 0.0
self._up_count = 0
self._down_count = 0
self._entry_price = 0.0
self._stop_price = None
def CreateClone(self):
return woc012_strategy()