在 GitHub 上查看

挂单网格策略(MQL/8147 转换)

概述

挂单网格策略 复刻了 MQL/8147 中的 MetaTrader 专家顾问。策略会在当前 买价和卖价周围构建对称的限价网格,在浮动盈亏保持在预设盈利目标与最大 回撤阈值之间时持续持有这些挂单。一旦达到盈利目标或触发最大回撤,策略 会撤销所有挂单、平掉所有仓位,并以新的账户权益为基准重新建立网格。

交易逻辑

  1. 订阅 Level1 数据以追踪最新买价与卖价。
  2. 在首次收到实时数据时记录组合权益,作为当前交易轮次的基准。
  3. 在市场价格之上放置 LevelsPerSide 个卖出限价单,并在市场价格之下 放置同样数量的买入限价单。GridStepPoints(以点数表示)控制相邻 网格之间的距离,值会换算为交易所的最小价位步长。
  4. 当挂单被成交后不会重新补单,只有在网格被完全重置时才会重新布单。
  5. 持续监控浮动盈亏:
    • 盈利达到 ProfitTargetCurrency 时平仓并重置网格;
    • 浮亏超过 MaxDrawdownCurrency 时同样平仓并重置。
  6. 每次重置后重新记录新的权益基准,并使用最新的买/卖价重新布置网格。

参数

参数 说明
ProfitTargetCurrency 达到该盈利(账户货币)时触发全部平仓并重置。
MaxDrawdownCurrency 允许的最大浮动亏损,超过后立即平仓并重置。
GridStepPoints 相邻网格之间的点数距离,会换算成实际价格。
LevelsPerSide 在市场上下方各放置的限价单数量。
OrderVolume 每张限价单的下单量。

风险控制

策略不为单独的挂单设置止损或止盈,而是直接监控整体盈亏。当需要平仓 时,RequestFlatten 会取消全部挂单,并通过 ClosePosition 发送市价单 快速平掉仓位。完成平仓后,网格状态和权益基准都会被清空,随后再重新建 立新的网格。

备注

  • 使用 Security.ShrinkPrice 对价格进行规范化,确保符合交易所最小报价 单位。
  • 通过读取 PriceStep 推断 MetaTrader 的 Point 大小,以匹配四位和五位 报价。
  • 与原始专家顾问一致,策略在一次网格周期内不会重复发送同一档位的挂单, 直到触发手动或自动的网格重置。
using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

public class PendingLimitGridStrategy : Strategy
{
	private readonly StrategyParam<int> _channelPeriod;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _cooldownBars;

	private decimal _prevClose;
	private decimal _prevMid;
	private bool _hasPrev;
	private int _barsSinceLastTrade;

	public int ChannelPeriod { get => _channelPeriod.Value; set => _channelPeriod.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int CooldownBars { get => _cooldownBars.Value; set => _cooldownBars.Value = value; }

	public PendingLimitGridStrategy()
	{
		_channelPeriod = Param(nameof(ChannelPeriod), 24).SetDisplay("Channel Period", "Grid channel lookback", "Indicators");
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame()).SetDisplay("Candle Type", "Candle timeframe", "General");
		_cooldownBars = Param(nameof(CooldownBars), 200).SetDisplay("Cooldown Bars", "Minimum bars between trades", "Risk");
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_prevClose = 0;
		_prevMid = 0;
		_hasPrev = false;
		_barsSinceLastTrade = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_hasPrev = false;
		_barsSinceLastTrade = 0;
		var highest = new Highest { Length = ChannelPeriod };
		var lowest = new Lowest { Length = ChannelPeriod };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(highest, lowest, ProcessCandle).Start();
	}

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

		var close = candle.ClosePrice;
		var mid = (highest + lowest) / 2;

		if (!_hasPrev) { _prevClose = close; _prevMid = mid; _hasPrev = true; return; }

		_barsSinceLastTrade++;

		if (_barsSinceLastTrade >= CooldownBars)
		{
			if (_prevClose <= _prevMid && close > mid && Position <= 0)
			{
				if (Position < 0) BuyMarket();
				BuyMarket();
				_barsSinceLastTrade = 0;
			}
			else if (_prevClose >= _prevMid && close < mid && Position >= 0)
			{
				if (Position > 0) SellMarket();
				SellMarket();
				_barsSinceLastTrade = 0;
			}
		}

		_prevClose = close;
		_prevMid = mid;
	}
}