在 GitHub 上查看

Horizontal Line Levels 策略是同名 MetaTrader 5 智能交易系统的移植版本。它会在最新报价附近动态构建两条水平线,并在市场突破这些水平时给出提醒。StockSharp 版本依赖 Level1 买卖价数据来复刻原始 OnTick/OnTimer 工作流程,全程不发送任何订单。

核心思路

  1. 订阅 Level1 数据,缓存最新的最优买价与最优卖价。
  2. 将 MetaTrader 中的“点”转换为 StockSharp 使用的真实价格增量。
  3. 在卖价上方和买价下方按照设定距离生成两条虚拟水平线。
  4. 借助策略定时器周期性检测是否发生突破,并在日志中输出告警消息。

参数

名称 默认值 说明
TimerPeriodMinutes 1 定时器触发间隔(分钟)。必须为正数。
OffsetPoints 50 在生成水平线时,向上/向下偏移的 MetaTrader 点数。

运行细节

  • 数据订阅GetWorkingSecurities 注册 Level1 流,因此即使没有 K 线数据也能持续接收买卖盘更新。
  • 初始化:当最优买价和最优卖价首次同时可用时,RecalculateLevels 会保存当前的上下水平线。
  • 定时器:每次定时器触发都会在必要时重新计算水平线,并在价格突破时写入日志提示。
  • 点值转换EnsurePointSize 使用 Security.PriceStep 将 MetaTrader 点数换算为真实的价格步长,与其它已移植策略保持一致。
  • 无交易行为:策略仅通过 AddInfoLog 输出信息,不会自动下单,这一点与原始 MT5 脚本的弹窗提醒完全一致。
  • 停止与重置:停止策略时会终止定时器并清理缓存,下次启动将重新计算水平线。

使用场景

  1. 在 Designer 中选择标的,并配置 TimerPeriodMinutesOffsetPoints
  2. 启动策略。首次收到完整报价后会在日志中看到类似 Horizontal levels updated. Upper: 1.12345, Lower: 1.12245. 的提示。
  3. 继续观察日志:当卖价上穿上轨或买价下破下轨时,会打印对应的突破信息。
  4. 修改参数或重新启动后,水平线会按照新设置重新生成。

分类

  • 类别:工具 / 提醒
  • 交易方向:无
  • 执行方式:事件驱动监控
  • 所需数据:Level1 买卖价
  • 复杂度:基础
  • 推荐周期:任意(依赖报价而非 K 线)
  • 风险控制:不适用(不会持仓)

该移植方案在保留原策略告警特性的同时,充分利用了 StockSharp 的订阅与定时器机制,使其能够在 C# 环境中稳定运行。

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;

public class HorizontalLineLevelsStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public HorizontalLineLevelsStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}