在 GitHub 上查看

NTOqF 多过滤策略

概述

NTOqF 多过滤策略是对 MetaTrader 4 专家顾问“NTOqF”(V1–V3 版本)的 StockSharp 高级 API 移植版本。原始 EA 可以独立启用或禁用多个震荡指标和趋势过滤器。本实现保留了相同的可配置性,为每个指标提供独立的时间框,并通过固定止损/止盈以及以点数表示的可选移动止损来管理仓位。

策略逻辑

指标过滤器

  • RSI 过滤器 – 当配置的偏移量处的 RSI 低于 RSI Lower 时产生多头信号;当 RSI 高于 RSI Upper 时产生空头信号;位于区间内时不会开仓。
  • 随机指标过滤器 – 比较 %K 与 %D。当启用 Use Stochastic High/Low 时,%K 还必须高于 Stoch High(做多)或低于 Stoch Low(做空);否则仅使用 %K/%D 的相对位置。
  • ADX 过滤器 – 通过比较 +DI 与 –DI 决定方向。启用 Use ADX Main 时,ADX 主线必须高于 ADX Main 才允许进场。
  • 抛物线 SAR 过滤器 – 比较 SAR 值与选定柱的收盘价。SAR 位于价格上方视为多头趋势,位于价格下方视为空头趋势(保持与原始 MQL 代码一致)。
  • 移动平均过滤器 – 将所选移动平均(可设置正向偏移)与基础偏移量处的收盘价比较。价格高于均线表示多头,低于均线表示空头。

所有启用的过滤器必须给出相同方向的结果;若其中任意过滤器返回中性状态,则不会入场。

入场规则

  • 在主交易时间框(Candle Type)上评估信号。
  • 同一时间仅允许持有一个方向的仓位;新信号会等待前一笔交易结束。
  • 下单数量由 Trade Volume(手数)决定。

出场规则

  • 固定止损 / 止盈 – 以点数定义,并根据标的的最小价位转换为价格偏移;设置为 0 可关闭相应功能。
  • 移动止损 – 启用后,当未实现盈利超过移动距离且当前止损距离价格超过该距离时,自动将止损沿趋势方向推进。

多时间框处理

每个指标都可以订阅独立的时间框。时间框参数为 0 时复用主交易时间框,正数表示以分钟为单位的 TimeFrameCandle。所有指标只在蜡烛收盘时更新,并遵守 Shift 参数,以复现原 EA 使用历史数据的方式。

参数

  • Candle Type – 用于驱动交易逻辑的时间框。
  • Volume – 市价单手数。
  • Take Profit (pips) – 止盈距离;0 表示禁用。
  • Stop Loss (pips) – 止损距离;0 表示禁用。
  • Use Trailing / Trailing Stop (pips) – 启用移动止损及其点数。
  • Shift – 读取指标与价格时向前回溯的已完成柱数量。
  • RSI 参数 – 开关、周期、上下阈值、时间框。
  • 随机指标参数 – 开关、%K/%D/Slowing 长度、可选高/低确认水平、时间框。
  • ADX 参数 – 开关、周期、DI 时间框、可选主线阈值、主线时间框。
  • 抛物线 SAR 参数 – 开关、加速步长、最大加速、时间框。
  • 移动平均参数 – 开关、周期、额外偏移、类型(SMA/EMA/SMMA/LWMA)、价格源、时间框。

说明

  • 指标队列完全遵循 Shift 设置,确保信号基于与原 EA 相同的历史数据。
  • 移动止损只有在盈利超过设定距离且止损距离价格足够远时才会触发调整。
  • 本策略包仅包含 C# 实现,不提供 Python 版本。
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>
/// NtoQf: RSI + EMA multi-filter strategy with ATR trailing.
/// Combines RSI overbought/oversold with EMA trend confirmation.
/// </summary>
public class NtoQfStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rsiLength;
	private readonly StrategyParam<int> _emaLength;
	private readonly StrategyParam<int> _atrLength;
	private readonly StrategyParam<decimal> _rsiUpper;
	private readonly StrategyParam<decimal> _rsiLower;

	private decimal _prevRsi;

	public NtoQfStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_rsiLength = Param(nameof(RsiLength), 14)
			.SetDisplay("RSI Length", "RSI period.", "Indicators");

		_emaLength = Param(nameof(EmaLength), 50)
			.SetDisplay("EMA Length", "EMA trend filter.", "Indicators");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period for trailing.", "Indicators");

		_rsiUpper = Param(nameof(RsiUpper), 70m)
			.SetDisplay("RSI Upper", "Overbought level.", "Signals");

		_rsiLower = Param(nameof(RsiLower), 30m)
			.SetDisplay("RSI Lower", "Oversold level.", "Signals");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int RsiLength
	{
		get => _rsiLength.Value;
		set => _rsiLength.Value = value;
	}

	public int EmaLength
	{
		get => _emaLength.Value;
		set => _emaLength.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

	public decimal RsiUpper
	{
		get => _rsiUpper.Value;
		set => _rsiUpper.Value = value;
	}

	public decimal RsiLower
	{
		get => _rsiLower.Value;
		set => _rsiLower.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType)];

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_prevRsi = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var rsi = new RelativeStrengthIndex { Length = RsiLength };
		var ema = new ExponentialMovingAverage { Length = EmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(rsi, ema, atr, ProcessCandle)
			.Start();

		StartProtection(
			takeProfit: new Unit(3, UnitTypes.Percent),
			stopLoss: new Unit(2, UnitTypes.Percent));

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, ema);
			DrawOwnTrades(area);
		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal rsiVal, decimal emaVal, decimal atrVal)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_prevRsi == 0 || atrVal <= 0)
		{
			_prevRsi = rsiVal;
			return;
		}

		var close = candle.ClosePrice;

		// Entry: RSI exits extreme zone, confirmed by EMA trend
		if (Position == 0)
		{
			if (_prevRsi < RsiLower && rsiVal >= RsiLower && close > emaVal)
			{
				BuyMarket();
			}
			else if (_prevRsi > RsiUpper && rsiVal <= RsiUpper && close < emaVal)
			{
				SellMarket();
			}
		}

		_prevRsi = rsiVal;
	}
}