在 GitHub 上查看

夜间区间突破策略(Night Flat Trade)

该策略复刻了原始的 MQL5 智能交易程序:在 EURUSD 的 H1 周期寻找夜间的窄幅震荡区间,并尝试在价格靠近区间边缘时参与突破。StockSharp 版本完全使用高级 API —— 蜡烛订阅、Highest/Lowest 指标绑定以及 StrategyParam<T> 参数体系 —— 既保持了原有思想,也便于配置与优化。

概览

  • 市场与周期:默认针对 EURUSD 的 1 小时图;任何具有明确报价步长的品种均可应用。
  • 交易时间窗:只在 OpenHourOpenHour + 1 的两个交易小时内寻找信号(交易所时间)。
  • 区间过滤:最近三根已完成蜡烛的高低差需处于 DiffMinPipsDiffMaxPips(换算成价格)之间。
  • 方向判定:若收盘价位于区间下四分之一则做多,在上四分之一则做空。

交易流程

  1. 计算区间边界

    • 通过 HighestLowest 指标(长度为 3)获取最近三根蜡烛的最高价与最低价。
    • 两者之间的差值即为后续判断与风控所用的有效区间。
  2. 入场条件

    • 做多:在允许时间段内,若收盘价高于区间下沿且不超过下四分之一(lowest + range/4),则市价买入,并将初始止损放在 lowest - range/3
    • 做空:若收盘价低于区间上沿并处于上四分之一(highest - range/4),则市价卖出,同时把止损设为 highest + range/3
  3. 离场与风控

    • 止损:将目标价位保存在变量中,只要下一根蜡烛突破该水平,就以市价平仓。
    • 止盈:当 TakeProfitPips > 0 时,会在入场价基础上设置固定点数的止盈目标。
    • 移动止损:当 TrailingStopPipsTrailingStepPips 都大于 0 时,只有在价格朝有利方向移动至少 TrailingStop + TrailingStep 点后才会上调/下调止损;之后每次更新都需要额外 TrailingStepPips 的推进,以还原原策略的阶梯式拖尾逻辑。
  4. 再入场控制

    • 策略在持仓结束之前不会寻找新的信号,确保交易模式始终保持「一笔仓位对应一个方向」。

参数

参数 说明 默认值
CandleType 订阅的蜡烛类型(默认 H1)。 1 小时蜡烛
TakeProfitPips 止盈距离(点数,0 表示不使用)。 50
TrailingStopPips 基础拖尾距离(点数,0 表示禁用)。 15
TrailingStepPips 每次调整拖尾所需的附加点数。 5
DiffMinPips 三根蜡烛区间的最小允许值(点数)。 18
DiffMaxPips 三根蜡烛区间的最大允许值(点数)。 28
OpenHour 交易时间窗的起始小时(交易所时间)。 0

指标

  • Highest(Length = 3):追踪最近区间的上边界。
  • Lowest(Length = 3):追踪最近区间的下边界。

实现细节

  • 点值换算会在报价精度为 3 或 5 位小数的品种上自动将最小报价步长乘以 10,与原 MQ5 代码保持一致。
  • 由于该版本基于已完成蜡烛运行,所有入场判断使用收盘价来近似盘中行为,从而保持确定性并忠实反映原策略意图。
  • 全部输入参数都封装为 StrategyParam<T>,方便在图形界面查看,也可以直接用于批量测试或参数优化。
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>
/// Night session flat trading strategy that enters near range extremes.
/// </summary>
public class NightFlatTradeStrategy : Strategy
{
	private readonly StrategyParam<int> _rangeLength;

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _diffMinPips;
	private readonly StrategyParam<decimal> _diffMaxPips;
	private readonly StrategyParam<int> _openHour;

	private Highest _highest = null!;
	private Lowest _lowest = null!;

	private decimal _pipSize;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

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

	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	public decimal DiffMinPips
	{
		get => _diffMinPips.Value;
		set => _diffMinPips.Value = value;
	}

	public decimal DiffMaxPips
	{
		get => _diffMaxPips.Value;
		set => _diffMaxPips.Value = value;
	}

	public int OpenHour
	{
		get => _openHour.Value;
		set => _openHour.Value = value;
	}

	/// <summary>
	/// Number of candles used to form the overnight range.
	/// </summary>
	public int RangeLength
	{
		get => _rangeLength.Value;
		set => _rangeLength.Value = value;
	}

	public NightFlatTradeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for the setup", "General");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetRange(0m, 500m)
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 15m)
			.SetRange(0m, 200m)
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetRange(0m, 200m)
			.SetDisplay("Trailing Step (pips)", "Extra advance required to shift the trailing stop", "Risk");

		_diffMinPips = Param(nameof(DiffMinPips), 18m)
			.SetGreaterThanZero()
			.SetDisplay("Min Range (pips)", "Minimum three-candle range in pips", "Setup");

		_diffMaxPips = Param(nameof(DiffMaxPips), 28m)
			.SetGreaterThanZero()
			.SetDisplay("Max Range (pips)", "Maximum three-candle range in pips", "Setup");

		_openHour = Param(nameof(OpenHour), 0)
			.SetRange(0, 23)
			.SetDisplay("Open Hour", "Hour (exchange time) when entries become active", "Schedule");

		_rangeLength = Param(nameof(RangeLength), 3)
			.SetGreaterThanZero()
			.SetDisplay("Range Length", "Number of candles composing the range", "Setup");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	protected override void OnReseted()
	{
		base.OnReseted();

		_highest = null!;
		_lowest = null!;
		_pipSize = 0m;
		_entryPrice = 0m;
		_stopPrice = null;
		_takeProfitPrice = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_highest = new Highest { Length = RangeLength };
		_lowest = new Lowest { Length = RangeLength };

		var priceStep = Security?.PriceStep ?? 0m;
		var decimals = Security?.Decimals;

		if (priceStep <= 0m)
			priceStep = 0.0001m;

		_pipSize = priceStep;

		if (decimals.HasValue && (decimals.Value == 3 || decimals.Value == 5))
			_pipSize *= 10m;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_highest, _lowest, ProcessCandle)
			.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal highestValue, decimal lowestValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Manage active trades before scanning for new setups.
		HandleExistingPosition(candle);

		if (Position != 0m)
			return;

		if (_highest == null || _lowest == null)
			return;

		if (!_highest.IsFormed || !_lowest.IsFormed)
			return;

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var diff = highestValue - lowestValue;
		if (diff <= 0m)
			return;

		var quarter = diff / 4m;
		var closePrice = candle.ClosePrice;

		if (closePrice > lowestValue && closePrice <= lowestValue + quarter)
		{
			BuyMarket();
			_entryPrice = closePrice;
			_stopPrice = lowestValue - diff / 3m;
			_takeProfitPrice = TakeProfitPips > 0m ? closePrice + ToPrice(TakeProfitPips) : null;
			return;
		}

		if (closePrice < highestValue && closePrice >= highestValue - quarter)
		{
			SellMarket();
			_entryPrice = closePrice;
			_stopPrice = highestValue + diff / 3m;
			_takeProfitPrice = TakeProfitPips > 0m ? closePrice - ToPrice(TakeProfitPips) : null;
		}
	}

	private void HandleExistingPosition(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			UpdateTrailingForLong(candle);

			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
		else if (Position < 0m)
		{
			UpdateTrailingForShort(candle);

			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
	}

	private void UpdateTrailingForLong(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _stopPrice == null)
			return;

		var trailingDistance = ToPrice(TrailingStopPips);
		var stepDistance = ToPrice(TrailingStepPips);

		var advance = candle.HighPrice - _entryPrice;
		if (advance < trailingDistance + stepDistance)
			return;

		var newStop = candle.HighPrice - trailingDistance;

		if (newStop <= _stopPrice.Value || newStop - _stopPrice.Value < stepDistance)
			return;

		// Raise the stop only after price travels an additional step distance.
		_stopPrice = newStop;
	}

	private void UpdateTrailingForShort(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || _stopPrice == null)
			return;

		var trailingDistance = ToPrice(TrailingStopPips);
		var stepDistance = ToPrice(TrailingStepPips);

		var advance = _entryPrice - candle.LowPrice;
		if (advance < trailingDistance + stepDistance)
			return;

		var newStop = candle.LowPrice + trailingDistance;

		if (newStop >= _stopPrice.Value || _stopPrice.Value - newStop < stepDistance)
			return;

		// Lower the stop only after price moves the additional step distance in favor of the trade.
		_stopPrice = newStop;
	}

	private decimal ToPrice(decimal pips)
	{
		if (pips <= 0m)
			return 0m;

		var pip = _pipSize > 0m ? _pipSize : 0.0001m;
		return pips * pip;
	}

	private void ResetTradeState()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takeProfitPrice = null;
	}
}