UmnickTrader Strategy
Adaptive mean-reversion system converted from the original UmnickTrader MQL5 expert advisor. The strategy works with a single
position at a time, alternating between long and short bias depending on the outcome of the previous trade. It evaluates price
movement using the average of open, high, low and close prices and only takes action once that average has shifted by at least
the configured StopBase distance.
Core Logic
- For every finished candle the average price
(O + H + L + C) / 4is calculated. - Signals are processed only when the absolute difference between the current average and the previously processed average is
greater than or equal to
StopBase. This mimics the original EA behaviour of waiting for a sufficiently large move. - When no position is open the strategy computes adaptive take-profit and stop-loss distances using two circular buffers that store the most recent eight profit and loss excursions.
- After a profitable trade the maximum favourable excursion observed while the position was open is saved to the profit buffer
(minus a spread padding), while the loss buffer receives
StopBase + 7 * Spread. - After a losing trade the profit buffer is reset to
StopBase - 3 * Spread, the loss buffer is updated with the recorded drawdown plus a spread padding, and the trading direction is flipped so the next setup trades the opposite side.
Trade Management
- The default distance for both the take-profit and stop-loss is
StopBase. If the accumulated profit or loss buffer exceedsStopBase / 2, their respective averages replace the default distance to widen or tighten the exit levels adaptively. - Market orders are used for entries. The expected take-profit and stop-loss prices are stored and managed by the strategy itself, so positions are closed when candle highs or lows touch the corresponding levels.
- While a position is active the highest favourable move and deepest drawdown are tracked using intrabar extremes. These statistics feed the buffers when the trade closes.
- Only one position can exist at any moment. A new signal is ignored if the previous trade has not been completed.
Parameters
StopBase– base distance (in price units) required to treat a move as significant and the default TP/SL distance. Default:0.017.TradeVolume– volume for market orders. Default:0.1.Spread– spread compensation applied when updating the adaptive buffers. Default:0.0005.CandleType– candle subscription used to evaluate averages. Default:TimeSpan.FromMinutes(5).TimeFrame().
Classification & Filters
- Direction: Both (but never simultaneously).
- Style: Adaptive swing / counter-trend.
- Indicators: Price average, custom excursion buffers.
- Stops: Dynamic stop-loss and take-profit managed by the strategy.
- Complexity: Intermediate – combines stateful buffers with adaptive exit sizing.
- Timeframe: Configurable via
CandleType. - Seasonality / News Filters: Not used.
- Risk Management: Position size is fixed by
TradeVolume; exit distances adapt based on recent performance.
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>
/// Adaptive mean-reversion strategy converted from the UmnickTrader MQL5 expert advisor.
/// </summary>
public class UmnickTraderStrategy : Strategy
{
// Number of trade results stored for adaptive calculations.
private readonly StrategyParam<int> _bufferLength;
private readonly StrategyParam<decimal> _stopBase;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<decimal> _spread;
private readonly StrategyParam<int> _entryCooldownBars;
private readonly StrategyParam<DataType> _candleType;
// Adaptive buffers storing profit and loss distances observed recently.
private decimal[] _profitBuffer = Array.Empty<decimal>();
private decimal[] _lossBuffer = Array.Empty<decimal>();
// Rolling state for signal detection and risk metrics.
private decimal _lastAveragePrice;
private decimal _entryPrice;
private decimal _takeProfitPrice;
private decimal _stopLossPrice;
private decimal _maxProfit;
private decimal _drawdown;
private decimal _lastTradeProfit;
private int _currentIndex;
private int _currentDirection = 1;
private int _cooldownRemaining;
private bool _positionActive;
private bool _isLongPosition;
private bool _positionJustClosed;
/// <summary>
/// Number of trade results stored for adaptive calculations.
/// </summary>
public int BufferLength
{
get => _bufferLength.Value;
set
{
if (_bufferLength.Value == value)
return;
_bufferLength.Value = value;
ResizeBuffers();
}
}
public decimal StopBase
{
get => _stopBase.Value;
set => _stopBase.Value = value;
}
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
public decimal Spread
{
get => _spread.Value;
set => _spread.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int EntryCooldownBars
{
get => _entryCooldownBars.Value;
set => _entryCooldownBars.Value = value;
}
public UmnickTraderStrategy()
{
_bufferLength = Param(nameof(BufferLength), 8)
.SetGreaterThanZero()
.SetDisplay("Buffer Length", "Number of trade results stored for adaptive calculations.", "Parameters")
.SetOptimize(4, 32, 1);
ResizeBuffers();
_stopBase = Param(nameof(StopBase), 0.017m)
.SetDisplay("Base Stop Distance", "Minimum average price move required to trigger evaluation.", "Parameters")
.SetOptimize(0.005m, 0.05m, 0.005m);
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetDisplay("Trade Volume", "Order volume for each position.", "Parameters")
.SetOptimize(0.05m, 1m, 0.05m);
_spread = Param(nameof(Spread), 0.0005m)
.SetDisplay("Spread Padding", "Spread compensation used when updating adaptive buffers.", "Parameters")
.SetOptimize(0.0001m, 0.002m, 0.0001m);
_entryCooldownBars = Param(nameof(EntryCooldownBars), 6)
.SetGreaterThanZero()
.SetDisplay("Entry Cooldown", "Bars to wait after a position is closed.", "Parameters");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Source candle series.", "General");
}
private void ResizeBuffers()
{
var length = BufferLength;
_profitBuffer = new decimal[length];
_lossBuffer = new decimal[length];
_currentIndex = 0;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_lastAveragePrice = 0m;
_entryPrice = 0m;
_takeProfitPrice = 0m;
_stopLossPrice = 0m;
_maxProfit = 0m;
_drawdown = 0m;
_lastTradeProfit = 0m;
_currentIndex = 0;
_currentDirection = 1;
_cooldownRemaining = 0;
_positionActive = false;
_isLongPosition = false;
_positionJustClosed = false;
Array.Clear(_profitBuffer, 0, _profitBuffer.Length);
Array.Clear(_lossBuffer, 0, _lossBuffer.Length);
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResizeBuffers();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// Update metrics for an active position before generating new signals.
UpdateOpenPosition(candle);
if (_cooldownRemaining > 0)
_cooldownRemaining--;
// Average of OHLC replicates the MQL5 price smoothing logic.
var averagePrice = (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m;
if (!ShouldProcessAverage(averagePrice))
return;
if (Position != 0)
return;
var limitDistance = StopBase;
var stopDistance = StopBase;
decimal sumProfit = 0m;
decimal sumLoss = 0m;
var bufferLength = _profitBuffer.Length;
if (bufferLength == 0)
return;
for (var i = 0; i < bufferLength; i++)
{
sumProfit += _profitBuffer[i];
sumLoss += _lossBuffer[i];
}
// Recalculate adaptive take-profit and stop-loss distances.
if (sumProfit > StopBase / 2m)
limitDistance = sumProfit / bufferLength;
if (sumLoss > StopBase / 2m)
stopDistance = sumLoss / bufferLength;
if (_positionJustClosed)
{
_positionJustClosed = false;
// Store the most recent excursion metrics.
if (_lastTradeProfit > 0m)
{
_profitBuffer[_currentIndex] = _maxProfit - Spread * 3m;
_lossBuffer[_currentIndex] = StopBase + Spread * 7m;
}
else
{
_profitBuffer[_currentIndex] = StopBase - Spread * 3m;
_lossBuffer[_currentIndex] = _drawdown + Spread * 7m;
_currentDirection = -_currentDirection;
}
_currentIndex++;
if (_currentIndex >= bufferLength)
_currentIndex = 0;
_cooldownRemaining = EntryCooldownBars;
return;
}
if (limitDistance <= 0m || stopDistance <= 0m)
return;
if (_cooldownRemaining > 0)
return;
var volume = TradeVolume;
if (volume <= 0m)
return;
// Enter in the current direction using market orders.
if (_currentDirection > 0)
OpenLong(candle.ClosePrice, limitDistance, stopDistance, volume);
else
OpenShort(candle.ClosePrice, limitDistance, stopDistance, volume);
}
private bool ShouldProcessAverage(decimal averagePrice)
{
if (_lastAveragePrice == 0m)
{
_lastAveragePrice = averagePrice;
return true;
}
var difference = Math.Abs(averagePrice - _lastAveragePrice);
if (difference >= StopBase)
{
_lastAveragePrice = averagePrice;
return true;
}
return false;
}
private void UpdateOpenPosition(ICandleMessage candle)
{
if (!_positionActive)
return;
// Track intrabar extremes to measure maximum favorable and adverse excursions.
if (_isLongPosition)
{
var profitMove = candle.HighPrice - _entryPrice;
if (profitMove > _maxProfit)
_maxProfit = profitMove;
var lossMove = _entryPrice - candle.LowPrice;
if (lossMove > _drawdown)
_drawdown = lossMove;
if (candle.LowPrice <= _stopLossPrice)
{
CloseCurrentPosition(_stopLossPrice);
return;
}
if (candle.HighPrice >= _takeProfitPrice)
{
CloseCurrentPosition(_takeProfitPrice);
return;
}
}
else
{
var profitMove = _entryPrice - candle.LowPrice;
if (profitMove > _maxProfit)
_maxProfit = profitMove;
var lossMove = candle.HighPrice - _entryPrice;
if (lossMove > _drawdown)
_drawdown = lossMove;
if (candle.HighPrice >= _stopLossPrice)
{
CloseCurrentPosition(_stopLossPrice);
return;
}
if (candle.LowPrice <= _takeProfitPrice)
{
CloseCurrentPosition(_takeProfitPrice);
return;
}
}
}
private void CloseCurrentPosition(decimal exitPrice)
{
// Close the position and record realized profit for buffer updates.
var profit = _isLongPosition ? exitPrice - _entryPrice : _entryPrice - exitPrice;
_positionActive = false;
_isLongPosition = false;
_entryPrice = 0m;
_takeProfitPrice = 0m;
_stopLossPrice = 0m;
_lastTradeProfit = profit;
_positionJustClosed = true;
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
}
private void OpenLong(decimal price, decimal limitDistance, decimal stopDistance, decimal volume)
{
BuyMarket(volume);
// Store trade parameters for managing exits on subsequent candles.
_entryPrice = price;
_takeProfitPrice = price + limitDistance;
_stopLossPrice = price - stopDistance;
_positionActive = true;
_isLongPosition = true;
_lastTradeProfit = 0m;
_maxProfit = 0m;
_drawdown = 0m;
}
private void OpenShort(decimal price, decimal limitDistance, decimal stopDistance, decimal volume)
{
SellMarket(volume);
// Store trade parameters for managing exits on subsequent candles.
_entryPrice = price;
_takeProfitPrice = price - limitDistance;
_stopLossPrice = price + stopDistance;
_positionActive = true;
_isLongPosition = false;
_lastTradeProfit = 0m;
_maxProfit = 0m;
_drawdown = 0m;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class umnick_trader_strategy(Strategy):
def __init__(self):
super(umnick_trader_strategy, self).__init__()
self._buffer_length = self.Param("BufferLength", 8)
self._stop_base = self.Param("StopBase", 0.017)
self._trade_volume = self.Param("TradeVolume", 0.1)
self._spread = self.Param("Spread", 0.0005)
self._entry_cooldown_bars = self.Param("EntryCooldownBars", 6)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._profit_buffer = []
self._loss_buffer = []
self._last_average_price = 0.0
self._entry_price = 0.0
self._take_profit_price = 0.0
self._stop_loss_price = 0.0
self._max_profit = 0.0
self._drawdown = 0.0
self._last_trade_profit = 0.0
self._current_index = 0
self._current_direction = 1
self._cooldown_remaining = 0
self._position_active = False
self._is_long_position = False
self._position_just_closed = False
@property
def BufferLength(self):
return self._buffer_length.Value
@BufferLength.setter
def BufferLength(self, value):
self._buffer_length.Value = value
@property
def StopBase(self):
return self._stop_base.Value
@StopBase.setter
def StopBase(self, value):
self._stop_base.Value = value
@property
def TradeVolume(self):
return self._trade_volume.Value
@TradeVolume.setter
def TradeVolume(self, value):
self._trade_volume.Value = value
@property
def Spread(self):
return self._spread.Value
@Spread.setter
def Spread(self, value):
self._spread.Value = value
@property
def EntryCooldownBars(self):
return self._entry_cooldown_bars.Value
@EntryCooldownBars.setter
def EntryCooldownBars(self, value):
self._entry_cooldown_bars.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def _resize_buffers(self):
length = max(1, int(self.BufferLength))
self._profit_buffer = [0.0] * length
self._loss_buffer = [0.0] * length
self._current_index = 0
def OnStarted2(self, time):
super(umnick_trader_strategy, self).OnStarted2(time)
self._resize_buffers()
self._last_average_price = 0.0
self._entry_price = 0.0
self._take_profit_price = 0.0
self._stop_loss_price = 0.0
self._max_profit = 0.0
self._drawdown = 0.0
self._last_trade_profit = 0.0
self._current_direction = 1
self._cooldown_remaining = 0
self._position_active = False
self._is_long_position = False
self._position_just_closed = False
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
self.StartProtection(
Unit(2000.0, UnitTypes.Absolute),
Unit(1000.0, UnitTypes.Absolute))
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_open_position(candle)
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
open_price = float(candle.OpenPrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
average_price = (open_price + high + low + close) / 4.0
if not self._should_process_average(average_price):
return
if self.Position != 0:
return
stop_base = float(self.StopBase)
limit_distance = stop_base
stop_distance = stop_base
buffer_length = len(self._profit_buffer)
if buffer_length == 0:
return
sum_profit = 0.0
sum_loss = 0.0
for i in range(buffer_length):
sum_profit += self._profit_buffer[i]
sum_loss += self._loss_buffer[i]
if sum_profit > stop_base / 2.0:
limit_distance = sum_profit / buffer_length
if sum_loss > stop_base / 2.0:
stop_distance = sum_loss / buffer_length
if self._position_just_closed:
self._position_just_closed = False
spread = float(self.Spread)
if self._last_trade_profit > 0.0:
self._profit_buffer[self._current_index] = self._max_profit - spread * 3.0
self._loss_buffer[self._current_index] = stop_base + spread * 7.0
else:
self._profit_buffer[self._current_index] = stop_base - spread * 3.0
self._loss_buffer[self._current_index] = self._drawdown + spread * 7.0
self._current_direction = -self._current_direction
self._current_index += 1
if self._current_index >= buffer_length:
self._current_index = 0
self._cooldown_remaining = int(self.EntryCooldownBars)
return
if limit_distance <= 0.0 or stop_distance <= 0.0:
return
if self._cooldown_remaining > 0:
return
if self._current_direction > 0:
self._open_long(close, limit_distance, stop_distance)
else:
self._open_short(close, limit_distance, stop_distance)
def _should_process_average(self, average_price):
if self._last_average_price == 0.0:
self._last_average_price = average_price
return True
difference = abs(average_price - self._last_average_price)
if difference >= float(self.StopBase):
self._last_average_price = average_price
return True
return False
def _update_open_position(self, candle):
if not self._position_active:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self._is_long_position:
profit_move = high - self._entry_price
if profit_move > self._max_profit:
self._max_profit = profit_move
loss_move = self._entry_price - low
if loss_move > self._drawdown:
self._drawdown = loss_move
if low <= self._stop_loss_price:
self._close_current_position(self._stop_loss_price)
return
if high >= self._take_profit_price:
self._close_current_position(self._take_profit_price)
return
else:
profit_move = self._entry_price - low
if profit_move > self._max_profit:
self._max_profit = profit_move
loss_move = high - self._entry_price
if loss_move > self._drawdown:
self._drawdown = loss_move
if high >= self._stop_loss_price:
self._close_current_position(self._stop_loss_price)
return
if low <= self._take_profit_price:
self._close_current_position(self._take_profit_price)
return
def _close_current_position(self, exit_price):
if self._is_long_position:
profit = exit_price - self._entry_price
else:
profit = self._entry_price - exit_price
self._position_active = False
self._is_long_position = False
self._entry_price = 0.0
self._take_profit_price = 0.0
self._stop_loss_price = 0.0
self._last_trade_profit = profit
self._position_just_closed = True
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
def _open_long(self, price, limit_distance, stop_distance):
self.BuyMarket()
self._entry_price = price
self._take_profit_price = price + limit_distance
self._stop_loss_price = price - stop_distance
self._position_active = True
self._is_long_position = True
self._last_trade_profit = 0.0
self._max_profit = 0.0
self._drawdown = 0.0
def _open_short(self, price, limit_distance, stop_distance):
self.SellMarket()
self._entry_price = price
self._take_profit_price = price - limit_distance
self._stop_loss_price = price + stop_distance
self._position_active = True
self._is_long_position = False
self._last_trade_profit = 0.0
self._max_profit = 0.0
self._drawdown = 0.0
def OnReseted(self):
super(umnick_trader_strategy, self).OnReseted()
self._last_average_price = 0.0
self._entry_price = 0.0
self._take_profit_price = 0.0
self._stop_loss_price = 0.0
self._max_profit = 0.0
self._drawdown = 0.0
self._last_trade_profit = 0.0
self._current_index = 0
self._current_direction = 1
self._cooldown_remaining = 0
self._position_active = False
self._is_long_position = False
self._position_just_closed = False
for i in range(len(self._profit_buffer)):
self._profit_buffer[i] = 0.0
self._loss_buffer[i] = 0.0
def CreateClone(self):
return umnick_trader_strategy()