在 GitHub 上查看

Two Pending Orders 2

概览

本策略是 MetaTrader 智能交易系统 “Two pending orders 2” 的 StockSharp 版本。策略始终在市场价格的上下各保持一个对称的挂单,并在首个成交方向上按照配置的止损、止盈与移动止损规则管理仓位。转换完全基于 StockSharp 的高级 API,实现原策略的核心思想,同时保留所有关键参数以便调优。

交易逻辑

  1. 策略订阅所选周期的K线(默认使用日线)。每当一根K线收盘,就触发新的决策周期。
  2. 所有过期的挂单会被取消;在下达新挂单前也会先撤销旧单,确保市场中仅保留最新的价格水平。
  3. 如果当前点差处于允许范围内,且激活的仓位/挂单数量尚未达到上限,则根据模式放置两笔对称挂单:
    • Stop 模式(默认)在现价上方放置买入止损单,在现价下方放置卖出止损单。
    • Limit 模式 在现价下方放置买入限价单,在现价上方放置卖出限价单。
    • 开启 Reverse Levels 选项会互换挂单方向,对应原EA的“反向”开关。
  4. 挂单价格在当前买/卖价基础上偏移 Pending Indent 参数指定的点数。当价格距离现有仓位小于 Min Step 时,挂单会被跳过。
  5. 挂单可以设置过期时间,超过指定分钟数后会被自动撤销。

仓位管理

  • 挂单成交后,策略会跟踪对应方向的平均入场价与持仓量。反向成交会优先减仓或平仓,再视情况开新仓。
  • 多头仓位在以下任一条件满足时平仓:
    • 价格下破平均入场价减去止损距离。
    • 价格上破平均入场价加上止盈距离。
    • 盈利超过移动止损激活阈值后,若价格回落触发移动止损(按设定步长移动)。
  • 空头仓位使用完全对称的价格比较规则。
  • 启用 Only One Position 后,策略会等待当前仓位完全平仓后才重新挂单。

参数说明

名称 说明
StopLossPoints 止损距离(点)。设为 0 表示不使用止损。
TakeProfitPoints 止盈距离(点)。设为 0 表示不使用止盈。
MaxPositions 同时允许存在的仓位与挂单总数上限。
MinStepPoints 新挂单与当前仓位入场价之间要求的最小距离。
TrailingActivatePoints 移动止损的激活阈值(点)。设为 0 表示关闭移动止损。
TrailingStopPoints 移动止损激活后与当前价格之间的距离。
TrailingStepPoints 每次调整移动止损所需的最小价格改善幅度。
TradeMode 允许的交易方向:Buy(仅多头)、Sell(仅空头)或 BuySell(双向)。
PendingType 挂单类型:StopLimit
PendingExpirationMinutes 挂单有效期(分钟)。设为 0 表示不设置过期时间。
PendingIndentPoints 计算挂单价格时使用的点差偏移。
PendingMaxSpreadPoints 允许的最大买卖价差(点)。设为 0 表示关闭过滤。
OnlyOnePosition true 时限制同一时间仅持有一个方向的仓位。
ReverseLevels 反转挂单方向,对应原EA的反向模式。
CandleType 用于触发信号评估的K线周期(默认日线)。

备注

  • 所有距离参数均以“点”为单位,并根据标的的最小价位(tick size)自动换算为价格。
  • 策略使用 StockSharp 的 BuyStopSellStopBuyLimitSellLimit 等高阶方法下单,并在每次重新决策前调用 CancelActiveOrders 撤销旧挂单。
  • 移动止损逻辑在每根完成的K线上评估。若需要更细粒度的移动止损,可选择更短周期的 CandleType
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Simplified from "Two pending orders 2" MetaTrader expert.
/// Places symmetric breakout levels around recent range and enters on breakout.
/// Uses high/low of N bars as breakout boundaries.
/// </summary>
public class TwoPendingOrders2Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _lookback;

	private readonly Queue<decimal> _highs = new();
	private readonly Queue<decimal> _lows = new();

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

	public int Lookback
	{
		get => _lookback.Value;
		set => _lookback.Value = value;
	}

	public TwoPendingOrders2Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for breakout detection", "General");

		_lookback = Param(nameof(Lookback), 10)
			.SetGreaterThanZero()
			.SetDisplay("Lookback", "Number of bars for high/low range", "Indicators");
	}

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

		_highs.Clear();
		_lows.Clear();

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

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

		if (_highs.Count < Lookback)
		{
			EnqueueCandle(candle);
			return;
		}

		decimal highest = decimal.MinValue;
		decimal lowest = decimal.MaxValue;
		var highs = _highs.ToArray();
		var lows = _lows.ToArray();
		foreach (var h in highs)
			if (h > highest) highest = h;
		foreach (var l in lows)
			if (l < lowest) lowest = l;

		var close = candle.ClosePrice;
		var volume = Volume;
		if (volume <= 0)
			volume = 1;
		var range = highest - lowest;
		var breakoutPadding = range * 0.05m;

		// Breakout above range
		if (close > highest + breakoutPadding)
		{
			if (Position <= 0)
				BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
		}
		// Breakout below range
		else if (close < lowest - breakoutPadding)
		{
			if (Position >= 0)
				SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
		}

		EnqueueCandle(candle);
	}

	private void EnqueueCandle(ICandleMessage candle)
	{
		_highs.Enqueue(candle.HighPrice);
		_lows.Enqueue(candle.LowPrice);

		if (_highs.Count > Lookback)
		{
			_highs.Dequeue();
			_lows.Dequeue();
		}
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_highs.Clear();
		_lows.Clear();

		base.OnReseted();
	}
}