在 GitHub 上查看

Alligator 趋势策略

该策略复现了 MetaTrader 脚本 (Alligator.mq5) 中的经典比尔·威廉姆斯 Alligator 系统。算法基于三条在中价上计算的平滑移动平均线,并向前偏移以展示市场状态。当快速的 Lips 线位于 Teeth 之上且 Teeth 位于 Jaw 之上时开多单;当排列顺序相反时开空单。系统同一时间只允许持有一个方向的仓位。

进场后,策略会按照点数设置止损与止盈。当价格向有利方向运行并达到设定的“零轴”距离时,止损会移动到开仓价。随后启用追踪止损:多头跟随最高价,空头跟随最低价,并只有在收益改善超过最小步长时才更新,以避免频繁调整。一旦价格触及止损、追踪止损或止盈水平,仓位立即平仓。

默认配置面向 30 分钟 K 线和外汇点值,但所有参数都可以针对其他市场进行优化。由于原始 MQL 版本依赖券商的点值处理,本转换使用标的资产的 PriceStep 来把点数转换成实际价格差。

交易规则

入场

  • 多头:当前无持仓,且在最近一根完成的 K 线上满足 Lips > Teeth > Jaw
  • 空头:当前无持仓,且在最近一根完成的 K 线上满足 Lips < Teeth < Jaw

离场与风控

  • 初始止损:多头放在开仓价下方 StopLossPips 点,空头放在开仓价上方 StopLossPips 点。
  • 止盈:距离开仓价 TakeProfitPips 点。
  • 零轴规则:当盈利幅度达到 ZeroLevelPips 点时,把止损移动到开仓价。
  • 追踪止损:启用零轴后,止损距离最新极值 TrailingStopPips 点,仅当改进超过 TrailingStepPips 点时才更新。
  • 当 K 线数据触及任一止损或止盈时,立刻退出仓位。

参数

参数 默认值 说明
CandleType 30 分钟周期 用于计算指标与信号的 K 线类型。
JawLength 13 蓝色 Jaw 线的平滑均线周期。
TeethLength 8 红色 Teeth 线的平滑均线周期。
LipsLength 5 绿色 Lips 线的平滑均线周期。
JawShift 8 Jaw 线向前偏移的柱数。
TeethShift 5 Teeth 线向前偏移的柱数。
LipsShift 3 Lips 线向前偏移的柱数。
EnableLong true 是否允许做多。
EnableShort true 是否允许做空。
StopLossPips 45 距离开仓价的止损点数。
TakeProfitPips 145 距离开仓价的止盈点数。
ZeroLevelPips 30 触发移到保本所需的盈利点数。
TrailingStopPips 50 追踪止损与最新极值之间的点数距离。
TrailingStepPips 10 更新追踪止损所需的最小改善点数。

说明

  • Alligator 指标基于 (High + Low) / 2 的中价计算,以匹配 MetaTrader 的实现。
  • 通过内部缓冲来模拟指标线的前移,使得比较使用与原脚本相同的偏移值。
  • 假设每根 K 线的信号在下一根出现前已完全执行,从而与原专家顾问的逐棒处理保持一致。
  • 请根据交易品种的最小跳动和波动性调整点数参数。
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>
/// Classic Bill Williams Alligator strategy with stop management rules.
/// </summary>
/// <remarks>
/// The strategy opens a long position when the Lips, Teeth, and Jaw lines are aligned upward
/// and opens a short position when they are aligned downward. Once in a trade the algorithm
/// applies the zero level rule to move the stop to break-even, updates a trailing stop with a
/// configurable step, and closes the position at the stop or take-profit levels.
/// </remarks>
public class AlligatorTrendStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _jawLength;
	private readonly StrategyParam<int> _teethLength;
	private readonly StrategyParam<int> _lipsLength;
	private readonly StrategyParam<int> _jawShift;
	private readonly StrategyParam<int> _teethShift;
	private readonly StrategyParam<int> _lipsShift;
	private readonly StrategyParam<bool> _enableLong;
	private readonly StrategyParam<bool> _enableShort;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _zeroLevelPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;

	private readonly List<decimal> _jawBuffer = new();
	private readonly List<decimal> _teethBuffer = new();
	private readonly List<decimal> _lipsBuffer = new();

	private decimal _entryPrice;

	private decimal? _longStop;
	private decimal? _longTake;
	private bool _longBreakevenActivated;
	private decimal _longBestPrice;

	private decimal? _shortStop;
	private decimal? _shortTake;
	private bool _shortBreakevenActivated;
	private decimal _shortBestPrice;

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

	/// <summary>
	/// Jaw length.
	/// </summary>
	public int JawLength
	{
		get => _jawLength.Value;
		set => _jawLength.Value = value;
	}

	/// <summary>
	/// Teeth length.
	/// </summary>
	public int TeethLength
	{
		get => _teethLength.Value;
		set => _teethLength.Value = value;
	}

	/// <summary>
	/// Lips length.
	/// </summary>
	public int LipsLength
	{
		get => _lipsLength.Value;
		set => _lipsLength.Value = value;
	}

	/// <summary>
	/// Forward shift applied to the jaw line.
	/// </summary>
	public int JawShift
	{
		get => _jawShift.Value;
		set => _jawShift.Value = value;
	}

	/// <summary>
	/// Forward shift applied to the teeth line.
	/// </summary>
	public int TeethShift
	{
		get => _teethShift.Value;
		set => _teethShift.Value = value;
	}

	/// <summary>
	/// Forward shift applied to the lips line.
	/// </summary>
	public int LipsShift
	{
		get => _lipsShift.Value;
		set => _lipsShift.Value = value;
	}

	/// <summary>
	/// Enable long trades.
	/// </summary>
	public bool EnableLong
	{
		get => _enableLong.Value;
		set => _enableLong.Value = value;
	}

	/// <summary>
	/// Enable short trades.
	/// </summary>
	public bool EnableShort
	{
		get => _enableShort.Value;
		set => _enableShort.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Distance to move the stop to break-even.
	/// </summary>
	public decimal ZeroLevelPips
	{
		get => _zeroLevelPips.Value;
		set => _zeroLevelPips.Value = value;
	}

	/// <summary>
	/// Distance between price extreme and trailing stop.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Minimum increment for trailing stop updates.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="AlligatorTrendStrategy"/> class.
	/// </summary>
	public AlligatorTrendStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for calculations", "General");

		_jawLength = Param(nameof(JawLength), 13)
			.SetGreaterThanZero()
			.SetDisplay("Jaw Length", "Smoothed moving average period for the jaw", "Alligator")
			;

		_teethLength = Param(nameof(TeethLength), 8)
			.SetGreaterThanZero()
			.SetDisplay("Teeth Length", "Smoothed moving average period for the teeth", "Alligator")
			;

		_lipsLength = Param(nameof(LipsLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("Lips Length", "Smoothed moving average period for the lips", "Alligator")
			;

		_jawShift = Param(nameof(JawShift), 8)
			.SetDisplay("Jaw Shift", "Forward shift applied to the jaw line", "Alligator")
			;

		_teethShift = Param(nameof(TeethShift), 5)
			.SetDisplay("Teeth Shift", "Forward shift applied to the teeth line", "Alligator")
			;

		_lipsShift = Param(nameof(LipsShift), 3)
			.SetDisplay("Lips Shift", "Forward shift applied to the lips line", "Alligator")
			;

		_enableLong = Param(nameof(EnableLong), true)
			.SetDisplay("Enable Long", "Allow long entries", "Trading");

		_enableShort = Param(nameof(EnableShort), true)
			.SetDisplay("Enable Short", "Allow short entries", "Trading");

		_stopLossPips = Param(nameof(StopLossPips), 500m)
			.SetDisplay("Stop Loss", "Stop-loss distance in pips", "Risk")
			;

		_takeProfitPips = Param(nameof(TakeProfitPips), 2000m)
			.SetDisplay("Take Profit", "Take-profit distance in pips", "Risk")
			;

		_zeroLevelPips = Param(nameof(ZeroLevelPips), 300m)
			.SetDisplay("Zero Level", "Distance to move stop to break-even", "Risk")
			;

		_trailingStopPips = Param(nameof(TrailingStopPips), 500m)
			.SetDisplay("Trailing Stop", "Trailing stop distance in pips", "Risk")
			;

		_trailingStepPips = Param(nameof(TrailingStepPips), 100m)
			.SetDisplay("Trailing Step", "Minimum trailing stop increment in pips", "Risk")
			;
	}

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

		_jawBuffer.Clear();
		_teethBuffer.Clear();
		_lipsBuffer.Clear();
		_entryPrice = 0m;
		ResetLong();
		ResetShort();
	}

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

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

		var jaw = new SmoothedMovingAverage { Length = JawLength };
		var teeth = new SmoothedMovingAverage { Length = TeethLength };
		var lips = new SmoothedMovingAverage { Length = LipsLength };

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

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

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

			var medianPrice = (candle.HighPrice + candle.LowPrice) / 2m;

			var inputValue = new DecimalIndicatorValue(jaw, medianPrice, candle.CloseTime) { IsFinal = true };
			var jawValue = jaw.Process(inputValue);
			var teethValue = teeth.Process(new DecimalIndicatorValue(teeth, medianPrice, candle.CloseTime) { IsFinal = true });
			var lipsValue = lips.Process(new DecimalIndicatorValue(lips, medianPrice, candle.CloseTime) { IsFinal = true });

			if (!jawValue.IsFormed || !teethValue.IsFormed || !lipsValue.IsFormed)
				return;

			var jawShifted = GetShiftedValue(_jawBuffer, jawValue.GetValue<decimal>(), JawShift);
			var teethShifted = GetShiftedValue(_teethBuffer, teethValue.GetValue<decimal>(), TeethShift);
			var lipsShifted = GetShiftedValue(_lipsBuffer, lipsValue.GetValue<decimal>(), LipsShift);

			if (!jawShifted.HasValue || !teethShifted.HasValue || !lipsShifted.HasValue)
				return;

			if (!jaw.IsFormed)
				return;

			if (ManagePosition(candle))
				return;

			var bullishAlignment = lipsShifted.Value > teethShifted.Value && teethShifted.Value > jawShifted.Value;
			var bearishAlignment = lipsShifted.Value < teethShifted.Value && teethShifted.Value < jawShifted.Value;

			if (Position == 0)
			{
				if (bullishAlignment && EnableLong)
				{
					BuyMarket();
				}
				else if (bearishAlignment && EnableShort)
				{
					SellMarket();
				}
			}
		}
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		var tradeMsg = trade.Trade;
		if (tradeMsg is null)
			return;
		var price = tradeMsg.Price;
		var direction = trade.Order.Side;

		var distanceToStop = StopLossPips > 0m ? GetPriceByPips(StopLossPips) : (decimal?)null;
		var distanceToTake = TakeProfitPips > 0m ? GetPriceByPips(TakeProfitPips) : (decimal?)null;

		if (direction == Sides.Buy)
		{
			if (Position > 0)
			{
				_entryPrice = price;
				_longStop = distanceToStop.HasValue ? price - distanceToStop.Value : null;
				_longTake = distanceToTake.HasValue ? price + distanceToTake.Value : null;
				_longBreakevenActivated = false;
				_longBestPrice = price;
			}
			else if (Position == 0)
			{
				ResetShort();
			}
		}
		else if (direction == Sides.Sell)
		{
			if (Position < 0)
			{
				_entryPrice = price;
				_shortStop = distanceToStop.HasValue ? price + distanceToStop.Value : null;
				_shortTake = distanceToTake.HasValue ? price - distanceToTake.Value : null;
				_shortBreakevenActivated = false;
				_shortBestPrice = price;
			}
			else if (Position == 0)
			{
				ResetLong();
			}
		}

		if (Position == 0)
		{
			ResetLong();
			ResetShort();
		}
	}

	private bool ManagePosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var entryPrice = _entryPrice;
			if (entryPrice == 0m)
				return false;

			_longBestPrice = Math.Max(_longBestPrice, candle.HighPrice);

			if (_longTake.HasValue && candle.HighPrice >= _longTake.Value)
			{
				SellMarket();
				ResetLong();
				return true;
			}

			if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
			{
				SellMarket();
				ResetLong();
				return true;
			}

			if (ZeroLevelPips > 0m && !_longBreakevenActivated && _longStop.HasValue && entryPrice > _longStop.Value)
			{
				var zeroDistance = GetPriceByPips(ZeroLevelPips);
				if (_longBestPrice - entryPrice >= zeroDistance)
				{
					_longStop = entryPrice;
					_longBreakevenActivated = true;
				}
			}

			if (TrailingStopPips > 0m)
			{
				var trailingDistance = GetPriceByPips(TrailingStopPips);
				var step = GetPriceByPips(TrailingStepPips);
				var candidate = _longBestPrice - trailingDistance;

				if (!_longStop.HasValue || candidate - _longStop.Value >= step)
					_longStop = candidate;
			}
		}
		else if (Position < 0)
		{
			var entryPrice = _entryPrice;
			if (entryPrice == 0m)
				return false;

			_shortBestPrice = _shortBestPrice == 0m ? candle.LowPrice : Math.Min(_shortBestPrice, candle.LowPrice);

			if (_shortTake.HasValue && candle.LowPrice <= _shortTake.Value)
			{
				BuyMarket();
				ResetShort();
				return true;
			}

			if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
			{
				BuyMarket();
				ResetShort();
				return true;
			}

			if (ZeroLevelPips > 0m && !_shortBreakevenActivated && _shortStop.HasValue && entryPrice < _shortStop.Value)
			{
				var zeroDistance = GetPriceByPips(ZeroLevelPips);
				if (entryPrice - candle.LowPrice >= zeroDistance)
				{
					_shortStop = entryPrice;
					_shortBreakevenActivated = true;
				}
			}

			if (TrailingStopPips > 0m)
			{
				var trailingDistance = GetPriceByPips(TrailingStopPips);
				var step = GetPriceByPips(TrailingStepPips);
				var candidate = _shortBestPrice + trailingDistance;

				if (!_shortStop.HasValue || _shortStop.Value - candidate >= step)
					_shortStop = candidate;
			}
		}
		else
		{
			ResetLong();
			ResetShort();
		}

		return false;
	}

	private static decimal? GetShiftedValue(List<decimal> buffer, decimal value, int shift)
	{
		if (shift <= 0)
			return value;

		buffer.Add(value);

		if (buffer.Count <= shift)
			return null;

		var result = buffer[0];
		try { buffer.RemoveAt(0); } catch { }
		return result;
	}

	private decimal GetPriceByPips(decimal pips)
	{
		if (pips <= 0m)
			return 0m;

		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 1m;

		return pips * step;
	}

	private void ResetLong()
	{
		_longStop = null;
		_longTake = null;
		_longBreakevenActivated = false;
		_longBestPrice = 0m;
	}

	private void ResetShort()
	{
		_shortStop = null;
		_shortTake = null;
		_shortBreakevenActivated = false;
		_shortBestPrice = 0m;
	}
}