NTK 07 区间交易策略
NTK 07 区间交易策略移植自 MetaTrader 的 “NTK 07” 专家顾问。该算法围绕当前价格同时挂出对称的买入和卖出止损单,并使用可配置的追踪和止盈逻辑管理持仓,目的是在短期区间的边缘或中心附近捕捉突破,同时保持严格的风险控制。
核心思想
- 入场触发:在空仓状态下,策略会评估配置的历史区间。当价格位于区间边缘或接近区间中点(取决于所选交易模式)时,会按照设定的价格步长偏移同时挂出买入止损和卖出止损订单。
- 区间感知:最近 N 根已完成 K 线的最高价与最低价定义交易区间。若长度为
0,则关闭过滤器,允许立即挂单。 - 自适应风险:每次入场使用基础手数,若启用手数乘数,则在已有头寸的方向上追加新的止损订单。全局手数上限可防止总仓位超过限制。
- 离场管理:一旦有订单成交,另一侧的止损挂单会被撤销。随后策略根据设置的偏移注册保护性止损和可选的止盈订单。追踪止损可以跟随前一根 K 线的高/低点、移动均线或固定距离。
- 交易时段:仅在设定的开始和结束小时之间交易,并在周末自动停止。
参数
| 参数 | 说明 |
|---|---|
| Entry Volume | 每笔入场订单的基础手数。 |
| Total Volume Limit | 累计持仓上限,0 表示无限制。 |
| Net Step | 市价与入场止损单之间的价格步数。 |
| Stop Loss | 相对入场价的初始止损步数。 |
| Take Profit | 止盈距离,0 表示禁用止盈。 |
| Trailing Stop | 追踪止损所用的步数距离。 |
| Lot Multiplier | 持仓后加仓止损的手数乘数。 |
| Trail High/Low | 启用后使用上一根 K 线极值追踪止损。 |
| Trail Moving Average | 启用后使用移动均线追踪止损;两种追踪方式不可同时启用。 |
| Trading Start/End Hour | 交易时间窗口的起止小时。 |
| Range Bars | 计算区间的已完成 K 线数量,0 表示不使用区间过滤。 |
| Trade Mode | EdgesOfRange 要求价格触及区间边界;CenterOfRange 要求价格接近区间中点。 |
| MA Period | 追踪用移动均线的周期。 |
| Candle Type | 用于计算的 K 线聚合类型。 |
工作流程
- 数据订阅:订阅配置的 K 线,并计算移动均线以及指定区间长度内的最高价和最低价。
- 空仓阶段:若满足区间条件,则按设定偏移挂出买入和卖出止损单,同时检查总体手数上限。
- 持仓管理:当任一方向成交后,另一侧挂单会被撤销,并立即放置保护性止损与可选止盈订单。每根新 K 线都会更新追踪止损。
- 加仓:当手数乘数大于
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()