在 GitHub 上查看

汇率规律策略

该策略是 MetaTrader 4 专家顾问 Strategy_of_Regularities_of_Exchange_Rates.mq4 的 StockSharp 版本。它实现了一种典型的日内突破对敲:在指定的时间同时挂出多、空方向的止损单,到了夜间的收盘时间则无条件撤单并平掉所有仓位。这样可以确保交易活动完全限制在一个交易日之内。

策略不依赖技术指标,仅根据时间和距离做决策。当出现 OpeningHour 时,程序读取当前的买一/卖一价格,按 EntryOffsetPoints(以经纪商“点”为单位)向上和向下偏移,分别放置 Buy StopSell Stop。代码会根据 PriceStep 自动放大 3 位或 5 位小数报价的最小变动,以保持与原始 MQL 脚本一致。

交易流程

  1. 开仓时间:当一根完成的蜡烛属于 OpeningHour 时,策略会先清理残留的挂单,再在买卖价两侧按照 EntryOffsetPoints * point 的距离挂出对称止损单。
  2. 保护止损:启动后立即调用 StartProtection,将 StopLossPoints 转换成绝对价格偏移,确保成交后立刻挂上平台侧的止损单。
  3. 止盈监控:每当收盘价刷新,如果浮动利润超过 TakeProfitPoints * point,就会用市价单平仓,复制了原脚本中 OrderClose 的盈利退出逻辑。
  4. 收盘时间:当时间到达 ClosingHour,策略会撤销所有未成交的挂单,并无条件平掉剩余仓位。
  5. 日内重置:每天只会重新布置一次挂单,避免在 1 小时以下的时间框架里重复提交同一组订单。

参数

参数 默认值 说明
OpeningHour 9 安排挂单的小时(0–23)。
ClosingHour 2 撤单并强制平仓的小时(0–23)。
EntryOffsetPoints 20 挂单与当前买卖价之间的点数距离。
TakeProfitPoints 20 触发手动止盈的点数距离,设置为 0 可关闭。
StopLossPoints 500 传递给 StartProtection 的止损点数。
OrderVolume 0.1 每个止损挂单的下单量。
CandleType 30 分钟 用于判定时间窗口的蜡烛类型,建议保持在 1 小时及以下以贴近原策略。

移植说明

  • 原脚本基于逐笔报价并直接调用 Hour()。在 StockSharp 中改为监听已完成的蜡烛,并读取其 OpenTime.Hour,既符合仓库仅处理完成蜡烛的规范,也保持了时间逻辑。
  • 挂单价格通过 Security.ShrinkPrice 归一化,从而保证与标的的最小价位变动对齐。
  • 保护性止损交给 StartProtection 管理,相当于在 MetaTrader 的 OrderSend 中附带 stop-loss 参数。
  • 新代码记录最近一次布单的日期,避免在子小时级别的图表上重复铺设相同的双向挂单。
  • 源码加入了详细的英文注释,完整解释每一步的意图,方便后续维护和二次开发。
using System;
using System.Linq;
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>
/// Time-based breakout strategy converted from the "Strategy of Regularities of Exchange Rates" MQL expert advisor.
/// At a scheduled hour captures reference price, then enters on breakout above/below offset levels.
/// Exits at a closing hour or on take-profit/stop-loss hit.
/// </summary>
public class RegularitiesOfExchangeRatesStrategy : Strategy
{
	private readonly StrategyParam<int> _openingHour;
	private readonly StrategyParam<int> _closingHour;
	private readonly StrategyParam<decimal> _entryOffsetPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<DataType> _candleType;

	private SimpleMovingAverage _dummySma;
	private decimal _pointSize;
	private DateTime? _lastEntryDate;
	private decimal _referencePrice;
	private decimal _entryPrice;
	private bool _waitingForBreakout;

	public int OpeningHour
	{
		get => _openingHour.Value;
		set => _openingHour.Value = value;
	}

	public int ClosingHour
	{
		get => _closingHour.Value;
		set => _closingHour.Value = value;
	}

	public decimal EntryOffsetPoints
	{
		get => _entryOffsetPoints.Value;
		set => _entryOffsetPoints.Value = value;
	}

	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

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

	public RegularitiesOfExchangeRatesStrategy()
	{
		_openingHour = Param(nameof(OpeningHour), 9)
			.SetDisplay("Opening Hour", "Hour (0-23) when breakout levels are set", "Schedule")
			.SetRange(0, 23);

		_closingHour = Param(nameof(ClosingHour), 2)
			.SetDisplay("Closing Hour", "Hour (0-23) when the strategy exits", "Schedule")
			.SetRange(0, 23);

		_entryOffsetPoints = Param(nameof(EntryOffsetPoints), 20m)
			.SetDisplay("Entry Offset (points)", "Distance from reference price for breakout", "Orders")
			.SetGreaterThanZero();

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 20m)
			.SetDisplay("Take Profit (points)", "Profit target distance in points", "Risk")
			.SetNotNegative();

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
			.SetDisplay("Stop Loss (points)", "Stop-loss distance in points", "Risk")
			.SetGreaterThanZero();

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to evaluate trading hours", "General");
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_pointSize = 0m;
		_lastEntryDate = null;
		_referencePrice = 0m;
		_entryPrice = 0m;
		_waitingForBreakout = false;
	}

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

		_pointSize = Security?.PriceStep ?? 0.01m;
		if (_pointSize <= 0m)
			_pointSize = 0.01m;

		_dummySma = new SimpleMovingAverage { Length = 2 };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_dummySma, ProcessCandle)
			.Start();
	}

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

		var hour = candle.OpenTime.Hour;
		var close = candle.ClosePrice;
		var high = candle.HighPrice;
		var low = candle.LowPrice;

		// At closing hour: flatten position and cancel breakout watch
		if (hour == ClosingHour)
		{
			if (Position > 0)
				SellMarket(Position);
			else if (Position < 0)
				BuyMarket(-Position);

			_waitingForBreakout = false;
			_entryPrice = 0m;
		}

		// Manage take-profit and stop-loss for existing position
		if (Position != 0 && _entryPrice > 0m)
		{
			var tp = TakeProfitPoints * _pointSize;
			var sl = StopLossPoints * _pointSize;

			if (Position > 0)
			{
				if ((tp > 0m && close - _entryPrice >= tp) || (sl > 0m && _entryPrice - close >= sl))
				{
					SellMarket(Position);
					_entryPrice = 0m;
					_waitingForBreakout = false;
				}
			}
			else if (Position < 0)
			{
				if ((tp > 0m && _entryPrice - close >= tp) || (sl > 0m && close - _entryPrice >= sl))
				{
					BuyMarket(-Position);
					_entryPrice = 0m;
					_waitingForBreakout = false;
				}
			}
		}

		// At opening hour: set reference price for breakout
		if (hour == OpeningHour && Position == 0)
		{
			var date = candle.OpenTime.Date;
			if (!_lastEntryDate.HasValue || _lastEntryDate.Value != date)
			{
				_referencePrice = close;
				_waitingForBreakout = true;
				_lastEntryDate = date;
			}
		}

		// Check for breakout entry
		if (_waitingForBreakout && Position == 0 && _referencePrice > 0m)
		{
			var offset = EntryOffsetPoints * _pointSize;
			var buyLevel = _referencePrice + offset;
			var sellLevel = _referencePrice - offset;

			if (high >= buyLevel)
			{
				BuyMarket();
				_entryPrice = close;
				_waitingForBreakout = false;
			}
			else if (low <= sellLevel)
			{
				SellMarket();
				_entryPrice = close;
				_waitingForBreakout = false;
			}
		}
	}
}