在 GitHub 上查看

Autotrade Pending Stops 策略

概述

该策略是 MetaTrader 顾问 Autotrade (barabashkakvn's edition) 的 StockSharp 版本。策略始终在当前价格上下维持两张对称的挂单:在价格上方放置 Buy Stop,在价格下方放置 Sell Stop。只要没有持仓,挂单会在每根收盘 K 线时刷新;一旦挂单被触发,系统会根据市场波动情况或绝对盈亏阈值来决定何时平仓。实现过程中严格遵循 AGENTS.md 的要求,并完全使用 StockSharp 的高级 API。

参数对照

StockSharp 参数 MQL5 输入 说明
IndentTicks InpIndent 当前价格到挂单的距离(以价格最小跳动数表示)。
MinProfit MinProfit 在行情趋于平静时触发平仓所需的最小浮动盈利(账户货币)。
ExpirationMinutes ExpirationMinutes 挂单的存活时间,超时后挂单会被取消并在下一根 K 线重建。
AbsoluteFixation AbsoluteFixation 触发强制平仓的绝对盈亏阈值(账户货币)。
StabilizationTicks InpStabilization 前一根 K 线实体的最大允许长度,用于识别盘整行情。
OrderVolume Lots Buy Stop 与 Sell Stop 的下单手数。
CandleType Period() 驱动策略的 K 线类型(默认 1 分钟)。

所有以“点”为单位的距离都会根据 Security.PriceStep 转换为实际价格跳动。盈亏阈值通过 Security.StepPrice 计算,以便与原始 MQL5 版本使用的账户货币结果一致。

交易流程

挂单布置

  1. 只处理状态为 CandleStates.Finished 的完整 K 线。
  2. 第一根 K 线用于初始化历史数据(上一根开/收价),随后立即放置挂单。
  3. 当仓位为零时会清理失效引用,然后:
    • Close + IndentTicks * PriceStep 处放置 Buy Stop;
    • Close - IndentTicks * PriceStep 处放置 Sell Stop。
  4. 每张挂单的到期时间均设为 CloseTime + ExpirationMinutes 分钟;一旦过期便取消,并在下一根 K 线上重新创建。

仓位管理

  1. 当其中一张挂单成交后,会立刻取消另一张挂单,以避免在 StockSharp 的净额模型下产生对冲仓位。
  2. 策略保存上一根 K 线的实体长度(|Open - Close|),用于判断市场是否进入低波动区间。
  3. 当持仓存在时:
    • 根据 PositionAvgPrice 计算当前浮动盈亏(使用 PriceStepStepPrice 转换为货币单位)。
    • 若浮盈超过 MinProfit 且上一根 K 线实体小于 StabilizationTicks * PriceStep,则以市价平仓。
    • 无论波动如何,只要绝对盈亏超过 AbsoluteFixation,也会立即平仓。
  4. 仓位归零后,所有剩余的挂单会被彻底移除。

其他行为

  • 策略始终保持单向净头寸,OrderVolume 会同步设置策略的 Volume
  • 在多数回测场景中缺乏实时买卖盘,因此挂单价格基于完成 K 线的收盘价计算。
  • 下单前会检查 IsFormedAndOnlineAndAllowTrading(),确保数据已就绪且允许交易。

实现差异与注意事项

  • 盈亏换算依赖 Security.PriceStepSecurity.StepPrice。若交易品种未提供这些值,代码会退化为使用默认值 1,需要在接入市场数据前确认配置正确。
  • 原始 MQL5 版本允许对冲式的双向仓位;本移植版在挂单成交后立即撤销另一张挂单,以适配 StockSharp 的净额账户模型。
  • 挂单到期时间基于 K 线的 CloseTime。如果数据源缺失该字段,需要扩展数据适配层以提供有效时间戳。
  • 通过调整 CandleType 可以轻松切换不同的时间框架或其他类型的蜡烛图数据。

使用建议

  1. CandleType 设置为与原始策略相同的周期,以保持交易节奏一致。
  2. 根据品种的最小跳动和 tick 价值调节 IndentTicksStabilizationTicksMinProfitAbsoluteFixation
  3. 确认账户模式(净额/对冲)。策略假设为净额模式,会在重新布置挂单前确保仓位归零。
  4. 在 StockSharp Designer 或 Backtester 中利用参数进行优化,以适配不同市场或交易品种。
  5. 关注日志输出:策略仅在收到完整数据且交易被允许时才会提交新订单。

风险提示

量化交易存在较高风险。请在历史数据上充分回测、验证参数,并确保满足券商关于挂单最小距离等限制后再用于真实账户。

using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Conversion of the MQL Autotrade strategy that places symmetric stop orders around the market.
/// Pending stop entries are refreshed on every candle while no position is open.
/// Positions are closed when the market calms down or when absolute profit/loss thresholds are reached.
/// </summary>
public class AutotradePendingStopsStrategy : Strategy
{
	private readonly StrategyParam<int> _indentTicks;
	private readonly StrategyParam<decimal> _minProfit;
	private readonly StrategyParam<int> _expirationMinutes;
	private readonly StrategyParam<decimal> _absoluteFixation;
	private readonly StrategyParam<int> _stabilizationTicks;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevOpen;
	private decimal _prevClose;
	private bool _hasPrevCandle;

	private decimal _tickSize = 1m;
	private decimal _tickValue = 1m;

	/// <summary>
	/// Distance in price steps from the current market to the pending stop entries.
	/// </summary>
	public int IndentTicks
	{
		get => _indentTicks.Value;
		set => _indentTicks.Value = value;
	}

	/// <summary>
	/// Minimal profit in account currency required to exit when price action stabilizes.
	/// </summary>
	public decimal MinProfit
	{
		get => _minProfit.Value;
		set => _minProfit.Value = value;
	}

	/// <summary>
	/// Lifetime of pending stop orders in minutes.
	/// </summary>
	public int ExpirationMinutes
	{
		get => _expirationMinutes.Value;
		set => _expirationMinutes.Value = value;
	}

	/// <summary>
	/// Absolute profit or loss that forces the position to close.
	/// </summary>
	public decimal AbsoluteFixation
	{
		get => _absoluteFixation.Value;
		set => _absoluteFixation.Value = value;
	}

	/// <summary>
	/// Maximum size of the previous candle body that is treated as consolidation.
	/// </summary>
	public int StabilizationTicks
	{
		get => _stabilizationTicks.Value;
		set => _stabilizationTicks.Value = value;
	}

	/// <summary>
	/// Order volume used for entries.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set
		{
			_orderVolume.Value = value;
			Volume = value;
		}
	}

	/// <summary>
	/// Candle type used to drive the strategy logic.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public AutotradePendingStopsStrategy()
	{
		_indentTicks = Param(nameof(IndentTicks), 200)
		.SetGreaterThanZero()
		.SetDisplay("Indent Ticks", "Distance in ticks between price and pending stop orders", "Entries");

		_minProfit = Param(nameof(MinProfit), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Min Profit", "Minimum profit to close during low volatility", "Risk");

		_expirationMinutes = Param(nameof(ExpirationMinutes), 41)
		.SetGreaterThanZero()
		.SetDisplay("Order Expiration", "Lifetime of pending stops in minutes", "Entries");

		_absoluteFixation = Param(nameof(AbsoluteFixation), 43m)
		.SetGreaterThanZero()
		.SetDisplay("Absolute Fixation", "Profit or loss in currency that forces exit", "Risk");

		_stabilizationTicks = Param(nameof(StabilizationTicks), 25)
		.SetGreaterThanZero()
		.SetDisplay("Stabilization Ticks", "Maximum candle body considered as flat market", "Exits");

		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Default volume for both stop orders", "General");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Time frame that drives order refresh", "General");

		Volume = _orderVolume.Value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		// Reset runtime state when the strategy is reloaded.
		_prevOpen = 0m;
		_prevClose = 0m;
		_hasPrevCandle = false;
		_entryPrice = 0m;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		Volume = _orderVolume.Value;

		// Cache price step and tick value for fast profit calculations.
		_tickSize = Security.PriceStep ?? 1m;
		_tickValue = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? _tickSize;

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Only act on completed candles to stay aligned with the original MQL logic.
		if (candle.State != CandleStates.Finished)
		return;

		if (!_hasPrevCandle)
		{
			// Store the first candle so that stabilization checks have history.
			_prevOpen = candle.OpenPrice;
			_prevClose = candle.ClosePrice;
			_hasPrevCandle = true;

			EnsurePendingOrders(candle);
			return;
		}

		UpdatePendingOrdersLifetime(candle);

		if (Position == 0)
		{
			// Refresh pending orders as soon as the market is flat.
			EnsurePendingOrders(candle);
		}
		else
		{
			// Manage the active position and close it when required.
			ManageOpenPosition(candle);
		}

		// Keep the previous candle body for stabilization checks on the next bar.
		_prevOpen = candle.OpenPrice;
		_prevClose = candle.ClosePrice;
	}

	private decimal _entryPrice;

	private void EnsurePendingOrders(ICandleMessage candle)
	{
		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		var indent = IndentTicks * _tickSize;
		var buyPrice = candle.ClosePrice + indent;
		var sellPrice = candle.ClosePrice - indent;

		// Simulate stop-order breakout: if high breaches buy level, go long
		if (candle.HighPrice >= buyPrice && Position <= 0)
		{
			if (Position < 0)
				BuyMarket(Math.Abs(Position));
			BuyMarket(OrderVolume);
			_entryPrice = buyPrice;
		}
		// if low breaches sell level, go short
		else if (candle.LowPrice <= sellPrice && Position >= 0)
		{
			if (Position > 0)
				SellMarket(Math.Abs(Position));
			SellMarket(OrderVolume);
			_entryPrice = sellPrice;
		}
	}

	private void UpdatePendingOrdersLifetime(ICandleMessage candle)
	{
		// No pending orders in simplified version - nothing to expire.
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		var entryPrice = _entryPrice;
		if (entryPrice == 0)
			return;

		var priceDiff = Position > 0 ? candle.ClosePrice - entryPrice : entryPrice - candle.ClosePrice;
		var prevBodySize = Math.Abs(_prevClose - _prevOpen);

		// Exit if profitable and market consolidating, or if loss exceeds threshold
		var exitByProfit = priceDiff > 0 && prevBodySize < candle.ClosePrice * 0.001m;
		var exitByLoss = priceDiff < -candle.ClosePrice * 0.005m;

		if (Position > 0 && (exitByProfit || exitByLoss))
		{
			SellMarket();
		}
		else if (Position < 0 && (exitByProfit || exitByLoss))
		{
			BuyMarket();
		}
	}

}