Стратегия UmnickTrader
Адаптивная контртрендовая система, портированная из оригинального советника UmnickTrader для MQL5. Стратегия одновременно
удерживает только одну позицию и меняет торговое направление в зависимости от результата предыдущей сделки. Для оценки движения
используется средняя цена свечи (O + H + L + C) / 4, и новые сигналы рассматриваются лишь тогда, когда эта средняя изменилась
минимум на величину StopBase.
Логика работы
- Для каждой закрытой свечи рассчитывается средняя цена по четырём точкам.
- Обработка сигналов начинается лишь тогда, когда модуль разницы между текущей и последней обработанной средней ценой достигает
StopBase. Таким образом стратегия дожидается значимого движения, как и в исходном советнике. - При отсутствии открытых позиций рассчитываются адаптивные уровни тейк-профита и стоп-лосса на основе двух кольцевых буферов, хранящих восемь последних величин прибыли и просадки.
- После прибыльной сделки в буфер прибыли записывается максимальное благоприятное отклонение цены (с вычетом спрэда), а буфер
убытков получает значение
StopBase + 7 * Spread. - После убыточной сделки буфер прибыли сбрасывается до
StopBase - 3 * Spread, буфер убытков пополняется зафиксированной просадкой плюс надбавка на спрэд, а следующее направление торговли разворачивается.
Управление сделками
- Базовое расстояние для тейк-профита и стоп-лосса равно
StopBase. Если сумма значений в соответствующем буфере превышаетStopBase / 2, стратегия использует среднее значение буфера, расширяя или сужая цели динамически. - Входы выполняются рыночными ордерами. Цены тейк-профита и стоп-лосса сохраняются внутри стратегии, и позиции закрываются при достижении уровня максимумом или минимумом свечи.
- Пока позиция открыта, фиксируются наибольшее благоприятное движение и максимальная просадка по экстремумам свечей. Эти данные используются при обновлении буферов после закрытия сделки.
- Одновременно может существовать только одна позиция; новый сигнал игнорируется, если предыдущая сделка ещё не завершена.
Параметры
StopBase– базовое расстояние (в ценовых единицах), определяющее значимость движения и исходные уровни TP/SL. Значение по умолчанию:0.017.TradeVolume– объём рыночных ордеров. Значение по умолчанию:0.1.Spread– величина поправки на спрэд при обновлении адаптивных буферов. Значение по умолчанию:0.0005.CandleType– тип свечей, по которым выполняются расчёты. Значение по умолчанию:TimeSpan.FromMinutes(5).TimeFrame().
Классификация и фильтры
- Направление: Обе стороны (но не одновременно).
- Стиль: Адаптивная свинговая / контртрендовая торговля.
- Индикаторы: Средняя цена, пользовательские буферы экстремумов.
- Стопы: Динамический тейк-профит и стоп-лосс, рассчитываемые стратегией.
- Сложность: Средняя — требуется поддержка буферов и адаптивных расчётов.
- Таймфрейм: Настраивается параметром
CandleType. - Сезонность и новости: Не используются.
- Управление рисками: Размер позиции задаётся
TradeVolume, расстояния выхода адаптируются к недавним результатам.
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()