在 GitHub 上查看

Pending tread 网格策略

概述

Pending tread 网格策略 是 MetaTrader 4 专家顾问 Pending_tread.mq4 的 StockSharp 版本。原始 EA 会在行情上方与下方持续维护两组挂单梯形,每一组都可以选择使用买单或卖单,并以点数控制间距。本移植完全使用 StockSharp 高阶 API 实现同样的逻辑,没有引入额外指标或自建数据集合。

交易逻辑

  1. 买卖价驱动的维护 – 通过 SubscribeLevel1 订阅一级行情,缓存最新的买价与卖价。每当接收到新报价时(受可配置的节流限制),维护流程会检查当前挂单数量与目标网格是否一致。
  2. 上方挂单梯形AboveMarketSide 决定在市场上方放置买入止损或卖出限价单。每一个阶梯相距 PipStep 点,并附带 TakeProfitPips 点的止盈。
  3. 下方挂单梯形BelowMarketSide 控制在市场下方堆叠买入限价或卖出止损单,点距与止盈计算与上方梯形相同。
  4. 止损距离保护MinStopDistancePoints 用来模拟 MetaTrader 的 MODE_STOPLEVEL 限制。如果挂单价格与对应的买价/卖价之间的距离小于限制,挂单会被跳过。
  5. 节流机制ThrottleSeconds 复刻了原程序中防止 “TRADE_CONTEXT_BUSY” 的 5 秒节流。在该时间窗口内只会执行一次维护,即使行情频繁更新。

所有以点数表示的输入(PipStepTakeProfitPips)都会根据品种的 PriceStepDecimals 转换为绝对价格偏移。对于五位或三位报价会自动乘以十,以匹配 MetaTrader 的 “adjusted point” 处理方式。

参数

参数 默认值 说明
OrderVolume 0.01 每个挂单的下单数量,下单前会根据品种的最小步长进行修正。
PipStep 12 相邻挂单之间的点数间隔。
TakeProfitPips 10 每个挂单对应的止盈距离,单位为点。
OrdersPerSide 10 市场上方与下方各维护的最大挂单数量。
AboveMarketSide Buy 市场上方使用的挂单类型。Buy 表示买入止损,Sell 表示卖出限价。
BelowMarketSide Sell 市场下方使用的挂单类型。Buy 表示买入限价,Sell 表示卖出止损。
MinStopDistancePoints 0 买卖价与挂单价格之间允许的最小距离(点)。如有需要,可填写经纪商提供的 MODE_STOPLEVEL
ThrottleSeconds 5 每次维护之间的冷却时间,单位为秒。
SlippagePoints 3 为与 MT4 输入保持一致而保留;在 StockSharp 中对挂单不起作用。

实现说明

  • 仅使用 StockSharp 高阶接口(SubscribeLevel1BuyLimitSellLimitBuyStopSellStop)。
  • 所有价格通过 Security.ShrinkPrice 归一化,确保满足交易所最小跳动要求。
  • 下单数量会根据 VolumeStepMinVolumeMaxVolume 自动调整。
  • 日志信息使用 AddInfoLog / AddWarningLog 输出,保留原 EA 的详细提示风格。
  • 根据任务要求,本目录未提供 Python 版本。

使用提示

  1. 绑定好品种与投资组合后启动策略。收到首个一级行情后两组挂单会立即生成。
  2. 调整 OrdersPerSide 时需注意风险,每增加一个阶梯就会在经纪商侧新增一张挂单。
  3. 若要完全复刻原 EA,请保持 5 秒节流并设置 MinStopDistancePoints 为经纪商要求的止损距离。
  4. StockSharp 采用净头寸模型,若上下两侧同时触发,成交会互相对冲,而不会形成 MT4 式的双向持仓。
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Pending grid strategy converted from the MetaTrader 4 expert advisor "Pending_tread".
/// Maintains two independent ladders of limit orders above and below the market with configurable direction and spacing.
/// When price reaches a grid level, a market order is placed in the configured direction.
/// </summary>
public class PendingTreadStrategy : Strategy
{
	private readonly StrategyParam<decimal> _pipStep;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _ordersPerSide;
	private readonly StrategyParam<Sides> _aboveMarketSide;
	private readonly StrategyParam<Sides> _belowMarketSide;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _pipSize;
	private decimal _anchorPrice;
	private bool _initialized;
	private readonly List<decimal> _triggeredLevelsAbove = new();
	private readonly List<decimal> _triggeredLevelsBelow = new();
	private decimal _entryPrice;

	public PendingTreadStrategy()
	{
		_pipStep = Param(nameof(PipStep), 200000m)
			.SetGreaterThanZero()
			.SetDisplay("Grid step (pips)", "Distance between adjacent pending orders expressed in pips", "Trading");

		_takeProfitPips = Param(nameof(TakeProfitPips), 150000m)
			.SetGreaterThanZero()
			.SetDisplay("Take profit (pips)", "Individual take-profit distance assigned to every pending order", "Trading");

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order volume", "Volume sent with each pending order", "Trading");

		_ordersPerSide = Param(nameof(OrdersPerSide), 2)
			.SetGreaterThanZero()
			.SetDisplay("Orders per side", "Maximum number of grid levels maintained above and below the anchor", "Trading");

		_aboveMarketSide = Param(nameof(AboveMarketSide), Sides.Buy)
			.SetDisplay("Above market side", "Type of orders triggered above the current price", "Orders");

		_belowMarketSide = Param(nameof(BelowMarketSide), Sides.Sell)
			.SetDisplay("Below market side", "Type of orders triggered below the current price", "Orders");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle type", "Candle timeframe", "General");
	}

	public decimal PipStep
	{
		get => _pipStep.Value;
		set => _pipStep.Value = value;
	}

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

	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	public int OrdersPerSide
	{
		get => _ordersPerSide.Value;
		set => _ordersPerSide.Value = value;
	}

	public Sides AboveMarketSide
	{
		get => _aboveMarketSide.Value;
		set => _aboveMarketSide.Value = value;
	}

	public Sides BelowMarketSide
	{
		get => _belowMarketSide.Value;
		set => _belowMarketSide.Value = value;
	}

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

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

		_pipSize = 0m;
		_anchorPrice = 0m;
		_initialized = false;
		_triggeredLevelsAbove.Clear();
		_triggeredLevelsBelow.Clear();
		_entryPrice = 0m;
	}

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

		_pipSize = GetPipSize();

		this
			.SubscribeCandles(CandleType)
			.Bind(ProcessCandle)
			.Start();
	}

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

		var close = candle.ClosePrice;

		if (!_initialized)
		{
			_anchorPrice = close;
			_initialized = true;
			return;
		}

		var distance = PipStep * _pipSize;
		if (distance <= 0m)
			return;

		var tpOffset = TakeProfitPips * _pipSize;

		// Check above-market grid levels
		for (var i = 1; i <= OrdersPerSide; i++)
		{
			var level = _anchorPrice + distance * i;

			if (_triggeredLevelsAbove.Contains(level))
				continue;

			if (close >= level)
			{
				_triggeredLevelsAbove.Add(level);
				ExecuteGridOrder(AboveMarketSide, close, tpOffset);
				return; // one order per candle
			}
		}

		// Check below-market grid levels
		for (var i = 1; i <= OrdersPerSide; i++)
		{
			var level = _anchorPrice - distance * i;

			if (_triggeredLevelsBelow.Contains(level))
				continue;

			if (close <= level)
			{
				_triggeredLevelsBelow.Add(level);
				ExecuteGridOrder(BelowMarketSide, close, tpOffset);
				return; // one order per candle
			}
		}

		// Check take-profit for existing position
		CheckTakeProfit(close, tpOffset);
	}

	private void ExecuteGridOrder(Sides side, decimal price, decimal tpOffset)
	{
		// Close existing opposite position first
		if (Position != 0)
		{
			if ((Position > 0 && side == Sides.Sell) || (Position < 0 && side == Sides.Buy))
			{
				ClosePosition(side);
			}
		}

		var vol = OrderVolume;

		if (side == Sides.Buy)
		{
			BuyMarket(vol);
			_entryPrice = price;
		}
		else
		{
			SellMarket(vol);
			_entryPrice = price;
		}
	}

	private void ClosePosition(Sides newSide)
	{
		var absPos = Position.Abs();
		if (absPos <= 0)
			return;

		if (Position > 0)
			SellMarket(absPos);
		else
			BuyMarket(absPos);
	}

	private void CheckTakeProfit(decimal close, decimal tpOffset)
	{
		if (Position == 0 || _entryPrice == 0 || tpOffset <= 0)
			return;

		if (Position > 0 && close >= _entryPrice + tpOffset)
		{
			SellMarket(Position.Abs());
			_entryPrice = 0;

			// Reset grid to re-establish levels around current price
			ResetGrid(close);
		}
		else if (Position < 0 && close <= _entryPrice - tpOffset)
		{
			BuyMarket(Position.Abs());
			_entryPrice = 0;

			ResetGrid(close);
		}
	}

	private void ResetGrid(decimal newAnchor)
	{
		_anchorPrice = newAnchor;
		_triggeredLevelsAbove.Clear();
		_triggeredLevelsBelow.Clear();
	}

	private decimal GetPipSize()
	{
		var security = Security;
		if (security == null)
			return 0.01m;

		var step = security.PriceStep ?? 0.01m;
		return step > 0m ? step : 0.01m;
	}
}