The Urdala Trol Hedging Grid Strategy is a direct conversion of the MetaTrader 5 expert advisor Urdala_Trol.mq5 into the StockSharp high-level API. The strategy continuously maintains exposure in both directions and scales positions using a martingale-like grid when stops are hit. It operates entirely on Level1 data (best bid/ask) without any indicators.
Trading Logic
Initial hedge (Step 0) – when there are no active positions, the strategy immediately opens one long and one short market order using the Base Volume parameter.
Losing-side scale-in (Step 1.2) – if only one direction remains open and the worst losing position on that side is at least Grid Step pips away from the current price, the strategy opens an additional position in the same direction. The new volume equals the volume of the least profitable position plus Min Lots Multiplier * minVolumeStep, where minVolumeStep is derived from the security's VolumeStep or MinVolume.
Stop-loss handling (Step 1.1) – when a position is closed by the stop-loss (including trailing adjustments) with a negative result, the strategy re-enters in the same direction unless there is already a live trade closer than Min Nearest pips to the exit price.
Profitable stop reaction (Step 2.1) – when the stop closes a position with profit, the strategy immediately opens a trade in the opposite direction with the scaled volume.
Trailing stop – once price advances by Trailing Stop + Trailing Step pips beyond the entry, the stop is trailed to keep a distance of Trailing Stop pips. Trailing is optional and enforced only when both parameters are greater than zero.
All distances expressed in pips are converted to absolute price offsets through the security's PriceStep. For five- or three-digit quotes, the conversion multiplies the step by ten to match the original MQL "adjusted point" logic.
Parameters
Parameter
Default
Description
BaseVolume
0.1
Initial lot size used to open the first hedge pair.
MinLotsMultiplier
3
Number of minimum lots added to the losing trade volume when scaling.
StopLossPips
50
Stop-loss distance in pips. A value of zero disables the stop and trailing logic.
TrailingStopPips
5
Trailing stop distance in pips. Set to zero to disable trailing.
TrailingStepPips
5
Additional pip distance required before the trailing stop moves. Must be positive when trailing is enabled.
GridStepPips
50
Minimum price distance (in pips) between the losing position and the current price before a new scale-in order is placed.
MinNearestPips
3
If an existing position is closer than this distance to the last stop price, the strategy skips the immediate re-entry.
Implementation Notes
Uses SubscribeLevel1() to track bid/ask updates and run the decision engine on every tick.
Orders are registered via the high-level RegisterOrder helper, allowing precise tracking through OnOwnTradeReceived.
Individual position objects are managed internally to reproduce hedged behaviour, because StockSharp portfolios are net-position based by default.
Stop-loss and trailing logic are executed inside the strategy by sending market orders once the thresholds are breached; no native stop orders are registered.
Usage Tips
Assign a liquid instrument and portfolio to the strategy and ensure that PriceStep, VolumeStep, and min/max volume values are configured for accurate conversions.
Start the strategy; it will instantly build a hedged pair and then react to stop events according to the original MQL logic.
Adjust pip parameters to align with the instrument's volatility. Large Grid Step values reduce the frequency of additional orders, while larger Min Lots Multiplier accelerates martingale growth.
Monitor the resulting exposure carefully; the martingale behaviour can escalate volume quickly when multiple stops are hit consecutively.
Python implementation is intentionally not provided in this folder, matching the requirements for this conversion task.
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;
/// <summary>
/// Urdala Trol strategy (simplified). Uses EMA with trailing stop logic
/// for grid-style entries based on trend direction.
/// </summary>
public class UrdalaTrolStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaLength;
private readonly StrategyParam<decimal> _trailingPercent;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int EmaLength
{
get => _emaLength.Value;
set => _emaLength.Value = value;
}
public decimal TrailingPercent
{
get => _trailingPercent.Value;
set => _trailingPercent.Value = value;
}
public UrdalaTrolStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candles", "General");
_emaLength = Param(nameof(EmaLength), 14)
.SetGreaterThanZero()
.SetDisplay("EMA Length", "EMA period", "Indicators");
_trailingPercent = Param(nameof(TrailingPercent), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Trailing %", "Trailing stop percent", "Risk");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var ema = new ExponentialMovingAverage { Length = EmaLength };
decimal highSinceEntry = 0;
decimal lowSinceEntry = decimal.MaxValue;
decimal prevClose = 0;
decimal prevEma = 0;
var hasPrev = false;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ema, (ICandleMessage candle, decimal emaVal) =>
{
if (candle.State != CandleStates.Finished)
return;
if (!hasPrev)
{
prevClose = candle.ClosePrice;
prevEma = emaVal;
hasPrev = true;
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
{
prevClose = candle.ClosePrice;
prevEma = emaVal;
return;
}
var close = candle.ClosePrice;
var high = candle.HighPrice;
var low = candle.LowPrice;
// Track trailing stop levels
if (Position > 0)
{
if (high > highSinceEntry)
highSinceEntry = high;
var trailStop = highSinceEntry * (1m - TrailingPercent / 100m);
if (close < trailStop)
{
SellMarket();
highSinceEntry = 0;
lowSinceEntry = decimal.MaxValue;
return;
}
}
else if (Position < 0)
{
if (low < lowSinceEntry)
lowSinceEntry = low;
var trailStop = lowSinceEntry * (1m + TrailingPercent / 100m);
if (close > trailStop)
{
BuyMarket();
highSinceEntry = 0;
lowSinceEntry = decimal.MaxValue;
return;
}
}
// Entry signals based on EMA
var bullishCross = prevClose <= prevEma && close > emaVal;
var bearishCross = prevClose >= prevEma && close < emaVal;
if (bullishCross && Position <= 0)
{
BuyMarket();
highSinceEntry = high;
lowSinceEntry = decimal.MaxValue;
}
else if (bearishCross && Position >= 0)
{
SellMarket();
lowSinceEntry = low;
highSinceEntry = 0;
}
prevClose = close;
prevEma = emaVal;
})
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
}
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.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class urdala_trol_strategy(Strategy):
def __init__(self):
super(urdala_trol_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candles", "General")
self._ema_length = self.Param("EmaLength", 14) \
.SetDisplay("EMA Length", "EMA period", "Indicators")
self._trailing_percent = self.Param("TrailingPercent", 1.5) \
.SetDisplay("Trailing %", "Trailing stop percent", "Risk")
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = 0.0
self._prev_ema = 0.0
self._has_prev = False
@property
def CandleType(self):
return self._candle_type.Value
@property
def EmaLength(self):
return self._ema_length.Value
@property
def TrailingPercent(self):
return self._trailing_percent.Value
def OnReseted(self):
super(urdala_trol_strategy, self).OnReseted()
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = 0.0
self._prev_ema = 0.0
self._has_prev = False
def OnStarted2(self, time):
super(urdala_trol_strategy, self).OnStarted2(time)
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = 0.0
self._prev_ema = 0.0
self._has_prev = False
ema = ExponentialMovingAverage()
ema.Length = self.EmaLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ema, self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def _on_process(self, candle, ema_value):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
ev = float(ema_value)
if not self._has_prev:
self._prev_close = close
self._prev_ema = ev
self._has_prev = True
return
if self.Position > 0:
if high > self._high_since_entry:
self._high_since_entry = high
trail_stop = self._high_since_entry * (1.0 - self.TrailingPercent / 100.0)
if close < trail_stop:
self.SellMarket()
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = close
self._prev_ema = ev
return
elif self.Position < 0:
if low < self._low_since_entry:
self._low_since_entry = low
trail_stop = self._low_since_entry * (1.0 + self.TrailingPercent / 100.0)
if close > trail_stop:
self.BuyMarket()
self._high_since_entry = 0.0
self._low_since_entry = 1e18
self._prev_close = close
self._prev_ema = ev
return
bullish_cross = self._prev_close <= self._prev_ema and close > ev
bearish_cross = self._prev_close >= self._prev_ema and close < ev
if bullish_cross and self.Position <= 0:
self.BuyMarket()
self._high_since_entry = high
self._low_since_entry = 1e18
elif bearish_cross and self.Position >= 0:
self.SellMarket()
self._low_since_entry = low
self._high_since_entry = 0.0
self._prev_close = close
self._prev_ema = ev
def CreateClone(self):
return urdala_trol_strategy()