在 GitHub 上查看

待定止损网格策略

概述

Pending Stop Grid Strategy 是对 MetaTrader 4 专家顾问 new.mq4 的直接迁移。策略在行情两侧同时维护两个对称的挂单阶梯:

  • 在当前卖价之上分层放置买入止损单;
  • 在当前买价之下分层放置卖出止损单。

每一层的下单距离与交易量都按序号线性增加,并为每个挂单单独设置止损与止盈目标。

交易逻辑

  1. 策略订阅 Level 1 行情,持续跟踪最新买价和卖价。
  2. 在确认收到行情并允许交易后,根据标的的最小报价步长自动换算点值(对三位或五位小数的外汇品种做额外归一化)。
  3. 下单前会验证基础手数是否符合交易所的最小与最大成交量限制。
  4. 对于从 1 到 NumberOfTrades 的每个序号 i
    • 订单手数等于 BaseVolume * i,并根据成交量步长进行四舍五入;
    • Ask + DistancePips * i * pipSize 处挂买入止损单,同时设置对应的止损与止盈;
    • Bid - DistancePips * i * pipSize 处挂卖出止损单,并镜像设置止损与止盈。
  5. 当任一挂单被成交、取消或拒单时,对应的槽位会被清空,并在行情允许时立即补回新的挂单。
  6. 启动时调用 StartProtection() 以启用平台级别的风险保护模块。

参数

名称 说明 默认值
BaseVolume 第一层挂单的手数,后续每层以序号倍数放大。 0.1
NumberOfTrades 同时维持的买入止损与卖出止损挂单数量。 10
DistancePips 每层挂单距离当前价格的点数。 10
StopLossPips 每个挂单的止损距离,设为 0 表示不放置止损单。 10
TakeProfitPips 每个挂单的止盈距离,设为 0 表示不放置止盈单。 10

所有参数均以策略参数形式暴露,可用于优化,并对非法的负值或零值进行了校验。

额外说明

  • 订单量会按照成交量步长进行取整,并限制在交易所允许的最小与最大区间内。
  • 价格通过 Security.ShrinkPrice 规整,确保符合合约的最小跳动单位。
  • 策略不保存历史状态,在重置或权限变化时会重新构建全部挂单阶梯。
  • 实现完全采用 StockSharp 的高级 API 绑定,避免自定义指标缓存,符合项目转换规范。
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 PendingStopGridStrategy : 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 PendingStopGridStrategy()
	{
		_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;
	}
}