Стратегия NTK 07 Range Trader
Стратегия NTK 07 Range Trader представляет собой конвертацию MetaTrader-советника «NTK 07». Алгоритм поддерживает симметричные отложенные заявки вокруг текущей цены и управляет открытыми позициями при помощи настраиваемых правил трейлинг-стопа и тейк-профита. Цель — захватывать прорывы, возникающие у границ или в центре краткосрочного диапазона, при строгом контроле риска.
Основные идеи
- Триггеры входа. В плоском состоянии стратегия анализирует диапазон с заданным количеством свечей. Если цена находится на границе диапазона либо около его середины (зависит от режима), выставляются buy stop и sell stop с указанным смещением в шагах цены.
- Учёт диапазона. Максимум и минимум последних N завершённых свечей задают рабочий коридор. Значение
0отключает фильтр, и заявки размещаются сразу. - Адаптивный риск. Каждая первичная заявка использует базовый объём, а при включённом множителе стратегия дозапускает дополнительные стоп-заявки в сторону текущей позиции. Глобальный лимит объёма не позволяет превысить суммарное плечо.
- Управление выходом. После исполнения одной стороны противоположная заявка отменяется. Далее выставляются защитный стоп и при необходимости тейк-профит. Трейлинг может ориентироваться на экстремумы предыдущей свечи, на значение скользящей средней или на фиксированное смещение.
- Торговая сессия. Сделки допускаются только между заданными часами и автоматически блокируются в выходные.
Параметры
| Параметр | Описание |
|---|---|
| Entry Volume | Базовый объём каждой входной заявки. |
| Total Volume Limit | Максимальный совокупный объём; 0 снимает ограничение. |
| Net Step | Смещение стоп-заявок относительно рынка в шагах цены. |
| Stop Loss | Первоначальная дистанция стоп-лосса в шагах цены. |
| Take Profit | Дистанция тейк-профита, 0 отключает его. |
| Trailing Stop | Шаг для расчёта трейлинг-стопа. |
| Lot Multiplier | Множитель объёма при пирамидинге. |
| Trail High/Low | Использовать экстремумы предыдущей свечи для трейлинга. |
| Trail Moving Average | Использовать скользящую среднюю для трейлинга; режимы взаимоисключающие. |
| Trading Start/End Hour | Временное окно торговли (включительно). |
| Range Bars | Количество завершённых свечей для расчёта диапазона, 0 — без фильтра. |
| Trade Mode | EdgesOfRange требует касания границ; CenterOfRange ждёт сближения с серединой диапазона. |
| MA Period | Период скользящей средней. |
| Candle Type | Тип свечей, используемых в расчётах. |
Последовательность работы
- Подписка на данные. Стратегия подписывается на выбранный тип свечей, рассчитывает скользящую среднюю, максимум и минимум диапазона.
- Отсутствие позиции. При выполнении условий диапазона выставляются парные buy stop и sell stop с заданным смещением, при этом учитывается общий лимит объёма.
- Сопровождение позиции. После входа противоположная заявка отменяется, ставятся защитный стоп и при необходимости тейк. Каждый новый бар обновляет трейлинг-стоп.
- Пирамидинг. Если множитель объёма >
1и лимит объёма не исчерпан, добавляется новая стоп-заявка в сторону текущей позиции. - Выход. Исполнение стопа или тейка закрывает позицию и отменяет оставшиеся защитные заявки. Далее алгоритм снова ждёт сигнала диапазона.
Дополнительно
- Все смещения выражены в шагах цены, поэтому стратегия адаптируется к инструментам с различным тиком.
- Торговля автоматически отключается в субботу и воскресенье, как и в оригинальном MQL-решении.
- Попытка включить оба режима трейлинга приведёт к ошибке конфигурации при запуске.
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>
/// Port of the NTK 07 MetaTrader strategy that trades stop orders around a recent range.
/// </summary>
public class Ntk07RangeTraderStrategy : Strategy
{
public enum TradeModeOptions
{
EdgesOfRange,
CenterOfRange,
}
private readonly StrategyParam<decimal> _entryVolume;
private readonly StrategyParam<decimal> _totalVolumeLimit;
private readonly StrategyParam<decimal> _netStepPoints;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _lotMultiplier;
private readonly StrategyParam<bool> _trailHighLow;
private readonly StrategyParam<bool> _trailMa;
private readonly StrategyParam<int> _tradingStartHour;
private readonly StrategyParam<int> _tradingEndHour;
private readonly StrategyParam<int> _rangeBars;
private readonly StrategyParam<TradeModeOptions> _tradeMode;
private readonly StrategyParam<int> _maPeriod;
private readonly StrategyParam<DataType> _candleType;
private SimpleMovingAverage _movingAverage;
private Highest _rangeHighIndicator;
private Lowest _rangeLowIndicator;
private ICandleMessage _previousCandle;
private decimal _priceStep;
private decimal _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
private int _candlesSinceLastTrade;
private const int CooldownCandles = 210;
/// <summary>
/// Base volume used for each entry order.
/// </summary>
public decimal EntryVolume
{
get => _entryVolume.Value;
set => _entryVolume.Value = value;
}
/// <summary>
/// Maximum total exposure allowed for the strategy. Set to zero for unlimited exposure.
/// </summary>
public decimal TotalVolumeLimit
{
get => _totalVolumeLimit.Value;
set => _totalVolumeLimit.Value = value;
}
/// <summary>
/// Distance of stop orders from the market in price steps.
/// </summary>
public decimal NetStepPoints
{
get => _netStepPoints.Value;
set => _netStepPoints.Value = value;
}
/// <summary>
/// Initial stop-loss distance in price steps.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance in price steps.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Trailing stop distance in price steps.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Additional volume multiplier used when pyramiding into an existing position.
/// </summary>
public decimal LotMultiplier
{
get => _lotMultiplier.Value;
set => _lotMultiplier.Value = value;
}
/// <summary>
/// Enables trailing based on previous candle extremes.
/// </summary>
public bool UseTrailingAtHighLow
{
get => _trailHighLow.Value;
set => _trailHighLow.Value = value;
}
/// <summary>
/// Enables trailing based on a moving average.
/// </summary>
public bool UseTrailingMa
{
get => _trailMa.Value;
set => _trailMa.Value = value;
}
/// <summary>
/// Inclusive starting hour for trading (platform time).
/// </summary>
public int TradingStartHour
{
get => _tradingStartHour.Value;
set => _tradingStartHour.Value = value;
}
/// <summary>
/// Inclusive ending hour for trading (platform time).
/// </summary>
public int TradingEndHour
{
get => _tradingEndHour.Value;
set => _tradingEndHour.Value = value;
}
/// <summary>
/// Number of completed candles that define the reference range. Set to zero to disable range filtering.
/// </summary>
public int RangeBars
{
get => _rangeBars.Value;
set => _rangeBars.Value = value;
}
/// <summary>
/// Range interaction mode for entry logic.
/// </summary>
public TradeModeOptions TradeMode
{
get => _tradeMode.Value;
set => _tradeMode.Value = value;
}
/// <summary>
/// Length of the moving average used for trailing stops.
/// </summary>
public int MovingAveragePeriod
{
get => _maPeriod.Value;
set => _maPeriod.Value = value;
}
/// <summary>
/// Candle type used for signal calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the strategy.
/// </summary>
public Ntk07RangeTraderStrategy()
{
_entryVolume = Param(nameof(EntryVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Entry Volume", "Base volume for each entry order", "Risk");
_totalVolumeLimit = Param(nameof(TotalVolumeLimit), 7m)
.SetNotNegative()
.SetDisplay("Total Volume Limit", "Maximum aggregated volume (0 disables the limit)", "Risk");
_netStepPoints = Param(nameof(NetStepPoints), 5m)
.SetGreaterThanZero()
.SetDisplay("Net Step", "Offset for stop entries measured in price steps", "Entries");
_stopLossPoints = Param(nameof(StopLossPoints), 11m)
.SetNotNegative()
.SetDisplay("Stop Loss", "Initial stop distance measured in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 30m)
.SetNotNegative()
.SetDisplay("Take Profit", "Take-profit distance measured in price steps", "Risk");
_trailingStopPoints = Param(nameof(TrailingStopPoints), 8m)
.SetGreaterThanZero()
.SetDisplay("Trailing Stop", "Distance used for trailing calculations in price steps", "Risk");
_lotMultiplier = Param(nameof(LotMultiplier), 1.7m)
.SetGreaterThanZero()
.SetDisplay("Lot Multiplier", "Volume multiplier when pyramiding", "Risk");
_trailHighLow = Param(nameof(UseTrailingAtHighLow), true)
.SetDisplay("Trail High/Low", "Use previous candle extremes for trailing", "Risk");
_trailMa = Param(nameof(UseTrailingMa), false)
.SetDisplay("Trail Moving Average", "Use moving average value for trailing", "Risk");
_tradingStartHour = Param(nameof(TradingStartHour), 0)
.SetDisplay("Trading Start Hour", "Trading window opening hour", "Sessions");
_tradingEndHour = Param(nameof(TradingEndHour), 23)
.SetDisplay("Trading End Hour", "Trading window closing hour", "Sessions");
_rangeBars = Param(nameof(RangeBars), 0)
.SetNotNegative()
.SetDisplay("Range Bars", "Number of completed candles used for the range", "Entries");
_tradeMode = Param(nameof(TradeMode), TradeModeOptions.EdgesOfRange)
.SetDisplay("Trade Mode", "How price interacts with the range before placing orders", "Entries");
_maPeriod = Param(nameof(MovingAveragePeriod), 100)
.SetGreaterThanZero()
.SetDisplay("MA Period", "Moving average length for trailing", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousCandle = null;
_movingAverage = null;
_rangeHighIndicator = null;
_rangeLowIndicator = null;
_entryPrice = 0m;
_stopPrice = null;
_takePrice = null;
_priceStep = 0;
_candlesSinceLastTrade = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
if (TradingStartHour < 0 || TradingStartHour > 23)
throw new InvalidOperationException("TradingStartHour must be between 0 and 23.");
if (TradingEndHour < 0 || TradingEndHour > 23)
throw new InvalidOperationException("TradingEndHour must be between 0 and 23.");
if (TradingStartHour >= TradingEndHour)
throw new InvalidOperationException("TradingStartHour must be strictly less than TradingEndHour.");
if (UseTrailingAtHighLow && UseTrailingMa)
throw new InvalidOperationException("Only one trailing mode can be enabled at a time.");
_priceStep = Security?.PriceStep ?? 1m;
_movingAverage = new SimpleMovingAverage { Length = MovingAveragePeriod };
var subscription = SubscribeCandles(CandleType);
if (RangeBars > 0)
{
_rangeHighIndicator = new Highest { Length = Math.Max(2, RangeBars) };
_rangeLowIndicator = new Lowest { Length = Math.Max(2, RangeBars) };
subscription
.Bind(_movingAverage, _rangeHighIndicator, _rangeLowIndicator, ProcessCandleWithRange)
.Start();
}
else
{
subscription
.Bind(_movingAverage, ProcessCandleWithoutRange)
.Start();
}
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
if (UseTrailingMa)
DrawIndicator(area, _movingAverage);
DrawOwnTrades(area);
}
}
private void ProcessCandleWithoutRange(ICandleMessage candle, decimal maValue)
{
ProcessCandleInternal(candle, maValue, null, null);
}
private void ProcessCandleWithRange(ICandleMessage candle, decimal maValue, decimal rangeHigh, decimal rangeLow)
{
var highValue = _rangeHighIndicator != null && _rangeHighIndicator.IsFormed ? rangeHigh : (decimal?)null;
var lowValue = _rangeLowIndicator != null && _rangeLowIndicator.IsFormed ? rangeLow : (decimal?)null;
ProcessCandleInternal(candle, maValue, highValue, lowValue);
}
private void ProcessCandleInternal(ICandleMessage candle, decimal maValue, decimal? rangeHigh, decimal? rangeLow)
{
if (candle.State != CandleStates.Finished)
{
return;
}
var hour = candle.CloseTime.Hour;
if (hour < TradingStartHour || hour > TradingEndHour)
{
_previousCandle = candle;
return;
}
// Check SL/TP first.
CheckProtection(candle);
var netOffset = ToPrice(NetStepPoints);
if (Position == 0 && netOffset > 0m)
{
_candlesSinceLastTrade++;
if (_candlesSinceLastTrade > CooldownCandles)
{
// Flat - check if candle broke through entry levels.
var allowEntries = true;
if (rangeHigh.HasValue && rangeLow.HasValue && rangeHigh.Value > rangeLow.Value)
{
allowEntries = TradeMode switch
{
TradeModeOptions.EdgesOfRange => candle.ClosePrice >= rangeHigh.Value || candle.ClosePrice <= rangeLow.Value,
TradeModeOptions.CenterOfRange => Math.Abs(candle.ClosePrice - ((rangeHigh.Value + rangeLow.Value) / 2m)) <= _priceStep,
_ => true,
};
}
if (allowEntries && _previousCandle != null)
{
var buyLevel = _previousCandle.ClosePrice + netOffset;
var sellLevel = _previousCandle.ClosePrice - netOffset;
if (candle.HighPrice >= buyLevel)
{
BuyMarket(EntryVolume);
_entryPrice = candle.ClosePrice;
_candlesSinceLastTrade = 0;
SetProtectionLevels(true, candle, maValue);
}
else if (candle.LowPrice <= sellLevel)
{
SellMarket(EntryVolume);
_entryPrice = candle.ClosePrice;
_candlesSinceLastTrade = 0;
SetProtectionLevels(false, candle, maValue);
}
}
}
}
else if (Position > 0)
{
// Update trailing stop for longs.
UpdateLongTrailing(candle, maValue);
}
else if (Position < 0)
{
// Update trailing stop for shorts.
UpdateShortTrailing(candle, maValue);
}
_previousCandle = candle;
}
private void SetProtectionLevels(bool isLong, ICandleMessage candle, decimal maValue)
{
var stopLossOffset = ToPrice(StopLossPoints);
var takeProfitOffset = ToPrice(TakeProfitPoints);
if (isLong)
{
_stopPrice = stopLossOffset > 0m ? _entryPrice - stopLossOffset : null;
_takePrice = takeProfitOffset > 0m ? _entryPrice + takeProfitOffset : null;
}
else
{
_stopPrice = stopLossOffset > 0m ? _entryPrice + stopLossOffset : null;
_takePrice = takeProfitOffset > 0m ? _entryPrice - takeProfitOffset : null;
}
}
private void CheckProtection(ICandleMessage candle)
{
if (Position > 0)
{
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
SellMarket(Position);
ResetProtection();
return;
}
if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
{
SellMarket(Position);
ResetProtection();
}
}
else if (Position < 0)
{
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetProtection();
return;
}
if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetProtection();
}
}
}
private void UpdateLongTrailing(ICandleMessage candle, decimal maValue)
{
var trailingOffset = ToPrice(TrailingStopPoints);
decimal? newStop = _stopPrice;
if (UseTrailingAtHighLow && _previousCandle != null)
{
var candidate = _previousCandle.LowPrice;
if (candidate > 0m && (newStop == null || candidate > newStop.Value))
newStop = candidate;
}
else if (UseTrailingMa && maValue > 0m)
{
if (newStop == null || maValue > newStop.Value)
newStop = maValue;
}
else if (trailingOffset > 0m)
{
var candidate = candle.ClosePrice - trailingOffset;
if (newStop == null || candidate > newStop.Value)
newStop = candidate;
}
if (newStop.HasValue)
{
var maxStop = candle.ClosePrice - _priceStep;
newStop = Math.Min(newStop.Value, maxStop);
newStop = Math.Max(newStop.Value, 0m);
}
_stopPrice = newStop;
}
private void UpdateShortTrailing(ICandleMessage candle, decimal maValue)
{
var trailingOffset = ToPrice(TrailingStopPoints);
decimal? newStop = _stopPrice;
if (UseTrailingAtHighLow && _previousCandle != null)
{
var candidate = _previousCandle.HighPrice;
if (candidate > 0m && (newStop == null || candidate < newStop.Value))
newStop = candidate;
}
else if (UseTrailingMa && maValue > 0m)
{
if (newStop == null || maValue < newStop.Value)
newStop = maValue;
}
else if (trailingOffset > 0m)
{
var candidate = candle.ClosePrice + trailingOffset;
if (newStop == null || candidate < newStop.Value)
newStop = candidate;
}
if (newStop.HasValue)
{
var minStop = candle.ClosePrice + _priceStep;
newStop = Math.Max(newStop.Value, minStop);
}
_stopPrice = newStop;
}
private void ResetProtection()
{
_entryPrice = 0m;
_stopPrice = null;
_takePrice = null;
_candlesSinceLastTrade = 0;
}
private decimal ToPrice(decimal points)
{
if (points <= 0m)
return 0m;
var step = _priceStep > 0m ? _priceStep : 1m;
return points * step;
}
}
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
from StockSharp.Algo.Indicators import SimpleMovingAverage, Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class ntk_07_range_trader_strategy(Strategy):
def __init__(self):
super(ntk_07_range_trader_strategy, self).__init__()
self._entry_volume = self.Param("EntryVolume", 1.0).SetGreaterThanZero().SetDisplay("Entry Volume", "Base volume for each entry order", "Risk")
self._sl_points = self.Param("StopLossPoints", 11.0).SetNotNegative().SetDisplay("Stop Loss", "Initial stop distance in price steps", "Risk")
self._tp_points = self.Param("TakeProfitPoints", 30.0).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk")
self._trailing_points = self.Param("TrailingStopPoints", 8.0).SetGreaterThanZero().SetDisplay("Trailing Stop", "Trailing stop distance in price steps", "Risk")
self._net_step = self.Param("NetStepPoints", 5.0).SetGreaterThanZero().SetDisplay("Net Step", "Offset for stop entries in price steps", "Entries")
self._ma_period = self.Param("MovingAveragePeriod", 100).SetGreaterThanZero().SetDisplay("MA Period", "Moving average length for trailing", "Risk")
self._candle_type = self.Param("CandleType", tf(5)).SetDisplay("Candle Type", "Primary timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(ntk_07_range_trader_strategy, self).OnReseted()
self._prev_candle = None
self._entry_price = 0
self._stop_price = None
self._take_price = None
def OnStarted2(self, time):
super(ntk_07_range_trader_strategy, self).OnStarted2(time)
self._prev_candle = None
self._entry_price = 0
self._stop_price = None
self._take_price = None
self._price_step = 1.0
if self.Security is not None and self.Security.PriceStep is not None and self.Security.PriceStep > 0:
self._price_step = float(self.Security.PriceStep)
ma = SimpleMovingAverage()
ma.Length = self._ma_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(ma, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def _to_price(self, points):
if points <= 0:
return 0
step = self._price_step if self._price_step > 0 else 1
return points * step
def OnProcess(self, candle, ma_val):
if candle.State != CandleStates.Finished:
return
# Check SL/TP
if self.Position > 0:
if self._stop_price is not None and candle.LowPrice <= self._stop_price:
self.SellMarket(self.Position)
self._reset()
self._prev_candle = candle
return
if self._take_price is not None and candle.HighPrice >= self._take_price:
self.SellMarket(self.Position)
self._reset()
self._prev_candle = candle
return
# trailing
trail_offset = self._to_price(self._trailing_points.Value)
if trail_offset > 0:
candidate = candle.ClosePrice - trail_offset
if self._stop_price is None or candidate > self._stop_price:
self._stop_price = min(candidate, candle.ClosePrice - self._price_step)
elif self.Position < 0:
if self._stop_price is not None and candle.HighPrice >= self._stop_price:
self.BuyMarket(Math.Abs(self.Position))
self._reset()
self._prev_candle = candle
return
if self._take_price is not None and candle.LowPrice <= self._take_price:
self.BuyMarket(Math.Abs(self.Position))
self._reset()
self._prev_candle = candle
return
trail_offset = self._to_price(self._trailing_points.Value)
if trail_offset > 0:
candidate = candle.ClosePrice + trail_offset
if self._stop_price is None or candidate < self._stop_price:
self._stop_price = max(candidate, candle.ClosePrice + self._price_step)
# Entry
net_offset = self._to_price(self._net_step.Value)
if self.Position == 0 and net_offset > 0 and self._prev_candle is not None:
buy_level = self._prev_candle.ClosePrice + net_offset
sell_level = self._prev_candle.ClosePrice - net_offset
if candle.HighPrice >= buy_level:
self.BuyMarket(self._entry_volume.Value)
self._entry_price = candle.ClosePrice
sl_offset = self._to_price(self._sl_points.Value)
tp_offset = self._to_price(self._tp_points.Value)
self._stop_price = self._entry_price - sl_offset if sl_offset > 0 else None
self._take_price = self._entry_price + tp_offset if tp_offset > 0 else None
elif candle.LowPrice <= sell_level:
self.SellMarket(self._entry_volume.Value)
self._entry_price = candle.ClosePrice
sl_offset = self._to_price(self._sl_points.Value)
tp_offset = self._to_price(self._tp_points.Value)
self._stop_price = self._entry_price + sl_offset if sl_offset > 0 else None
self._take_price = self._entry_price - tp_offset if tp_offset > 0 else None
self._prev_candle = candle
def _reset(self):
self._entry_price = 0
self._stop_price = None
self._take_price = None
def CreateClone(self):
return ntk_07_range_trader_strategy()