在 GitHub 上查看

Send Close Order 策略

Send Close Order 是 2009 年由 Vladimir Hlystov 编写的 MetaTrader 4 专家顾问 “SendCloseOrder” 的移植版本。原始脚本基于比尔·威廉姆斯分形在图表上绘制四条趋势线,只要价格触及这些预测水平就会开仓或平仓。StockSharp 版本保留了决策流程,并自动维护这些线条,可用于任何蜡烛序列。

交易逻辑

  1. 分形识别 —— 每根收盘蜡烛都会推动一个包含五根蜡烛的滑动窗口。当窗口填满后,会按照比尔·威廉姆斯的条件检查中间那根蜡烛,并按时间顺序保存确认的高点和低点。
  2. 趋势线重建
    • Sell line 连接最近的两个上行分形,它们之间存在一个下行分形,形成阻力线。
    • Close #1 为 Sell line 向上平移 15 个价格步长(15 × Security.PriceStep),作为多头离场轨道。
    • Buy line 连接最近的两个下行分形,它们之间存在一个上行分形,形成支撑线。
    • Close #2 为 Buy line 向下平移 15 个价格步长,作为空头离场轨道。
  3. 信号评估 —— 将四条线外推到当前收盘蜡烛的时间戳。如果预测价格落在蜡烛的最高价/最低价区间内(容差为两个价格步长),就会触发对应操作。
  4. 订单管理
    • 触及 Close #1 或 Close #2 时调用 ClosePosition() 立即平掉全部仓位。
    • 触及 Sell 或 Buy 线时,以 TradeVolume 的数量发送市价单,只要结果仓位的绝对值不超过 MaxOrders × TradeVolume。如果存在反向仓位,系统会先对冲再叠加新的方向,模拟 MetaTrader 的对冲模式。

参数

名称 默认值 说明
EnableSellLine true 当价格触及阻力线时允许交易。
EnableBuyLine true 当价格触及支撑线时允许交易。
EnableCloseLongLine true 多头在上移的阻力线(Close #1)上止盈。
EnableCloseShortLine true 空头在下移的支撑线(Close #2)上止盈。
MaxOrders 1 单方向允许叠加的最大入场次数。
TradeVolume 0.1 每次市价单的下单量。
CandleType 1 小时 用于分形计算的蜡烛周期。

与 MetaTrader 版本的差异

  • StockSharp 端每当出现新分形都会自动重新计算四条线;在 MetaTrader 中需要手工删除并重画。
  • 策略在净头寸模型下运行,默认不支持同时持有多空两个篮子。
  • 触发检测使用收盘蜡烛的最高价和最低价,并额外允许两个价格步长的容差,而不是依赖逐笔的 Bid/Ask 报价。
  • 不再创建图形对象(趋势线和文字),专注于交易信号。

使用说明

  • 该策略适用于任何提供蜡烛数据且 PriceStep 有效的品种;若交易所未提供步长,则使用 0.0001 作为回退值。
  • 调高 MaxOrders 可以模拟原版 EA 的加仓行为,设置 TradeVolume 时请考虑品种的最小交易单位。
  • 线条偏移固定为 15 个点,如需修改 MetaTrader 中的参数请同步调整源代码。

当前仅提供 C# 实现,如需 Python 版本可在后续添加。

using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// SendCloseOrder: Fractal high/low breakout with EMA filter and ATR stops.
/// </summary>
public class SendCloseOrderStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _emaLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _entryPrice;
	private decimal _fractalHigh;
	private decimal _fractalLow;
	private decimal _prev2High;
	private decimal _prev1High;
	private decimal _prev2Low;
	private decimal _prev1Low;
	private int _barCount;

	public SendCloseOrderStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");

		_emaLength = Param(nameof(EmaLength), 50)
			.SetDisplay("EMA Length", "Trend filter.", "Indicators");

		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");
	}

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

	public int EmaLength
	{
		get => _emaLength.Value;
		set => _emaLength.Value = value;
	}

	public int AtrLength
	{
		get => _atrLength.Value;
		set => _atrLength.Value = value;
	}

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

		_entryPrice = 0;
		_fractalHigh = 0;
		_fractalLow = 0;
		_prev2High = 0;
		_prev1High = 0;
		_prev2Low = 0;
		_prev1Low = 0;
		_barCount = 0;
	}

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

		_entryPrice = 0;
		_fractalHigh = 0;
		_fractalLow = 0;
		_prev2High = 0;
		_prev1High = 0;
		_prev2Low = 0;
		_prev1Low = 0;
		_barCount = 0;

		var ema = new ExponentialMovingAverage { Length = EmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };

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

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

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

		_barCount++;

		var high = candle.HighPrice;
		var low = candle.LowPrice;
		var close = candle.ClosePrice;

		// Detect fractal high: prev1High > prev2High and prev1High > current high
		if (_barCount > 3 && _prev1High > _prev2High && _prev1High > high)
			_fractalHigh = _prev1High;

		// Detect fractal low: prev1Low < prev2Low and prev1Low < current low
		if (_barCount > 3 && _prev1Low < _prev2Low && _prev1Low < low)
			_fractalLow = _prev1Low;

		_prev2High = _prev1High;
		_prev1High = high;
		_prev2Low = _prev1Low;
		_prev1Low = low;

		if (_fractalHigh == 0 || _fractalLow == 0 || atrVal <= 0)
			return;

		if (Position > 0)
		{
			if (close >= _entryPrice + atrVal * 3m || close <= _entryPrice - atrVal * 1.5m)
			{
				SellMarket();
				_entryPrice = 0;
			}
		}
		else if (Position < 0)
		{
			if (close <= _entryPrice - atrVal * 3m || close >= _entryPrice + atrVal * 1.5m)
			{
				BuyMarket();
				_entryPrice = 0;
			}
		}

		if (Position == 0)
		{
			if (close > _fractalHigh && close > emaVal)
			{
				_entryPrice = close;
				BuyMarket();
			}
			else if (close < _fractalLow && close < emaVal)
			{
				_entryPrice = close;
				SellMarket();
			}
		}
	}
}