在 GitHub 上查看

GoldWarrior02b 策略

该策略源自 MetaTrader 专家顾问 GoldWarrior02b,现移植到 StockSharp 平台。 策略结合自定义的动量指标、CCI 与简化的 ZigZag,用于在每个 15 分钟区间的最后时刻进行交易。

实现基于 StockSharp 的高级 API,并以净头寸方式运作。 原始脚本的多层对冲在此未实现,因为 StockSharp 默认使用净额结算模式。

核心思路

  • 自定义动量指标计算每根蜡烛的开收盘差值的滑动平均。
  • 通过 CCI 识别超买/超卖反转及强势动量。
  • 依据最近高低点生成 ZigZag 方向,避免逆势交易。
  • 仅在第 14、29、44、59 分钟且秒数>=45 时评估信号。
  • 通过止损、止盈、移动止损及整体盈利目标管理风险。

入场条件

只有当前没有持仓,且蜡烛在上述时间窗口内收盘时才会考虑开仓。

多头

  • ZigZag 指向下方(最新低点低于前一个低点)。
  • 满足以下任一条件:
    • CCI 较前值上升,前值低于 -50,当前值低于 -30,动量从负转正,且上一动量为负。
    • 或 CCI 低于 -200,上一 CCI 更低,动量低于买入阈值,且上一动量弱于当前动量。

空头

  • ZigZag 指向上方(最新高点高于前一个高点)。
  • 满足以下任一条件:
    • CCI 较前值下降,前值高于 50,当前值高于 30,动量从正转负,且上一动量为正。
    • 或 CCI 高于 200,上一 CCI 更高,动量高于卖出阈值,且上一动量强于当前动量。

若上一根动量值位于买入和卖出阈值之间,则忽略信号。

出场条件

  • 止损:价格触及止损距离时立即平仓。
  • 止盈:价格达到设定的盈利距离时平仓。
  • 移动止损:当浮盈达到 (TrailingStop + TrailingStep) 点后,移动止损跟随价格,保持 TrailingStop 点的距离。 一旦价格回踩到移动止损位置即退出。
  • 总体盈利目标:未实现盈亏达到设定金额(账户货币)时强制平仓。

参数

参数 说明 默认值
BaseVolume 入场手数。 0.1
StopLossPoints 止损距离(点)。 100
TakeProfitPoints 止盈距离(点)。 150
TrailingStopPoints 移动止损基础距离。 5
TrailingStepPoints 启动移动止损前的额外距离。 5
ImpulsePeriod CCI 与动量的计算周期。 21
ZigZagDepth 新 ZigZag 拐点之间的最小柱数。 12
ZigZagDeviation 确认拐点所需的最小价格波动(点)。 5
ZigZagBackstep 接受新拐点前的最少柱数。 3
ProfitTarget 触发强制平仓的未实现盈利阈值。 300
ImpulseSellThreshold 做空动量阈值(通常为负)。 -30
ImpulseBuyThreshold 做多动量阈值(通常为正)。 30
CandleType 计算所用的时间框架。 5 分钟

说明

  • 动量指标等同于对开收盘差值进行滑动平均,并按 PriceStep 缩放。
  • 移动止损和盈利计算使用 PriceStepStepPrice 将点数换算为账户货币。
  • 原策略包含加仓与对冲机制,当前移植版本仅保留单一净头寸,与 StockSharp 的撮合方式一致。
  • 若需更贴近原策略,可订阅 15 分钟蜡烛,并确保数据延迟足以在收盘后及时执行。

免责声明

该示例仅用于教学目的。投入真实资金前,请在真实数据、延迟与手续费条件下充分测试。

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>
/// Port of the MetaTrader GoldWarrior02b expert advisor adapted for StockSharp.
/// Combines CCI, an impulse gauge and a ZigZag swing detector to trade near the end of 15 minute blocks.
/// </summary>
public class GoldWarrior02bStrategy : Strategy
{
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<int> _impulsePeriod;
	private readonly StrategyParam<int> _zigZagDepth;
	private readonly StrategyParam<decimal> _zigZagDeviation;
	private readonly StrategyParam<int> _zigZagBackstep;
	private readonly StrategyParam<decimal> _profitTarget;
	private readonly StrategyParam<decimal> _impulseSellThreshold;
	private readonly StrategyParam<decimal> _impulseBuyThreshold;
	private readonly StrategyParam<DataType> _candleType;

	private CommodityChannelIndex _cci = null!;
	private ImpulseIndicator _impulse = null!;

	private decimal? _lastZigZag;
	private decimal? _previousZigZag;
	private int _searchDirection;
	private decimal? _currentExtreme;
	private int _barsSinceExtreme;

	private decimal _previousCci;
	private decimal _previousImpulse;
	private bool _hasPreviousCci;
	private bool _hasPreviousImpulse;

	private DateTimeOffset _lastTradeTime;
	private decimal _entryPrice;
	private decimal _trailingStopPrice;
	private bool _trailingActive;
	private decimal _maxPriceSinceEntry;
	private decimal _minPriceSinceEntry;

	/// <summary>
	/// Base trading volume.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in points.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Additional offset before activating the trailing stop.
	/// </summary>
	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Period used both for CCI and impulse calculations.
	/// </summary>
	public int ImpulsePeriod
	{
		get => _impulsePeriod.Value;
		set => _impulsePeriod.Value = value;
	}

	/// <summary>
	/// Minimum bars between ZigZag turning points.
	/// </summary>
	public int ZigZagDepth
	{
		get => _zigZagDepth.Value;
		set => _zigZagDepth.Value = value;
	}

	/// <summary>
	/// Minimum price deviation to confirm a new ZigZag swing.
	/// </summary>
	public decimal ZigZagDeviation
	{
		get => _zigZagDeviation.Value;
		set => _zigZagDeviation.Value = value;
	}

	/// <summary>
	/// Minimum number of bars before accepting a new swing.
	/// </summary>
	public int ZigZagBackstep
	{
		get => _zigZagBackstep.Value;
		set => _zigZagBackstep.Value = value;
	}

	/// <summary>
	/// Profit target that forces an early exit from open positions.
	/// </summary>
	public decimal ProfitTarget
	{
		get => _profitTarget.Value;
		set => _profitTarget.Value = value;
	}

	/// <summary>
	/// Threshold applied to the impulse gauge before opening shorts.
	/// </summary>
	public decimal ImpulseSellThreshold
	{
		get => _impulseSellThreshold.Value;
		set => _impulseSellThreshold.Value = value;
	}

	/// <summary>
	/// Threshold applied to the impulse gauge before opening longs.
	/// </summary>
	public decimal ImpulseBuyThreshold
	{
		get => _impulseBuyThreshold.Value;
		set => _impulseBuyThreshold.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the strategy.
	/// </summary>
	public GoldWarrior02bStrategy()
	{
		_baseVolume = Param(nameof(BaseVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Volume", "Base trade size", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 100m)
		.SetNotNegative()
		.SetDisplay("Stop Loss", "Stop-loss distance in points", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 150m)
		.SetNotNegative()
		.SetDisplay("Take Profit", "Take-profit distance in points", "Risk");

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 5m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop", "Trailing stop distance in points", "Risk");

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
		.SetNotNegative()
		.SetDisplay("Trailing Step", "Extra distance before trailing activates", "Risk");

		_impulsePeriod = Param(nameof(ImpulsePeriod), 21)
		.SetGreaterThanZero()
		.SetDisplay("Impulse Period", "Period for CCI and impulse averages", "Indicators");

		_zigZagDepth = Param(nameof(ZigZagDepth), 12)
		.SetGreaterThanZero()
		.SetDisplay("ZigZag Depth", "Minimum bars between swings", "Indicators");

		_zigZagDeviation = Param(nameof(ZigZagDeviation), 5m)
		.SetGreaterThanZero()
		.SetDisplay("ZigZag Deviation", "Required price move in points", "Indicators");

		_zigZagBackstep = Param(nameof(ZigZagBackstep), 3)
		.SetGreaterThanZero()
		.SetDisplay("ZigZag Backstep", "Bars before confirming a new swing", "Indicators");

		_profitTarget = Param(nameof(ProfitTarget), 300m)
		.SetNotNegative()
		.SetDisplay("Profit Target", "Close all profit in account currency", "Risk");

		_impulseSellThreshold = Param(nameof(ImpulseSellThreshold), -30m)
		.SetDisplay("Impulse Sell", "Impulse threshold for shorts", "Indicators");

		_impulseBuyThreshold = Param(nameof(ImpulseBuyThreshold), 30m)
		.SetDisplay("Impulse Buy", "Impulse threshold for longs", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
		.SetDisplay("Candle Type", "Working timeframe", "General");
	}

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

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

		_cci?.Reset();
		_impulse?.Reset();

		_lastZigZag = null;
		_previousZigZag = null;
		_searchDirection = 1;
		_currentExtreme = null;
		_barsSinceExtreme = 0;

		_previousCci = 0m;
		_previousImpulse = 0m;
		_hasPreviousCci = false;
		_hasPreviousImpulse = false;

		_lastTradeTime = DateTimeOffset.MinValue;
		ResetPositionState();
	}

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

		Volume = BaseVolume;

		_cci = new CommodityChannelIndex { Length = ImpulsePeriod };
		_impulse = new ImpulseIndicator
		{
			Length = ImpulsePeriod,
			PriceStep = GetPriceStep()
		};

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

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

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

	
		_impulse.PriceStep = GetPriceStep();

		if (!_cci.IsFormed || !_impulse.IsFormed)
		{
			_previousCci = cciValue;
			_previousImpulse = impulseValue;
			_hasPreviousCci = true;
			_hasPreviousImpulse = true;
			UpdateZigZag(candle);
			return;
		}

		UpdateZigZag(candle);

		var hasZigZag = _lastZigZag.HasValue && _previousZigZag.HasValue;
		var zigZagUp = hasZigZag && _lastZigZag.Value > _previousZigZag.Value;
		var zigZagDown = hasZigZag && _lastZigZag.Value < _previousZigZag.Value;

		if (!_hasPreviousCci || !_hasPreviousImpulse)
		{
			_previousCci = cciValue;
			_previousImpulse = impulseValue;
			_hasPreviousCci = true;
			_hasPreviousImpulse = true;
			return;
		}

		var now = candle.CloseTime;
		if ((now - _lastTradeTime).TotalSeconds < 15)
		{
			_previousCci = cciValue;
			_previousImpulse = impulseValue;
			return;
		}

		var sellCondition1 = cciValue < _previousCci && _previousCci > 20m && impulseValue < 0m;
		var sellCondition2 = cciValue > 100m && _previousCci > cciValue;
		var buyCondition1 = cciValue > _previousCci && _previousCci < -20m && impulseValue > 0m;
		var buyCondition2 = cciValue < -100m && _previousCci < cciValue;

		var sellSignal = hasZigZag && zigZagUp && (sellCondition1 || sellCondition2);
		var buySignal = hasZigZag && zigZagDown && (buyCondition1 || buyCondition2);

		if (!hasZigZag || Position != 0)
		{
			sellSignal = false;
			buySignal = false;
		}

		if (Position == 0 && AllowEntryTime(now))
		{
			if (sellSignal)
			OpenShort(candle, BaseVolume);
			else if (buySignal)
			OpenLong(candle, BaseVolume);
		}

		if (Position != 0)
		{
			HandleActivePosition(candle, now);
		}

		_previousCci = cciValue;
		_previousImpulse = impulseValue;
	}

	private void HandleActivePosition(ICandleMessage candle, DateTimeOffset now)
	{
		var step = GetPriceStep();
		var stepPrice = GetStepPrice(step);

		var stopLossDistance = StopLossPoints * step;
		var takeProfitDistance = TakeProfitPoints * step;
		var trailingStopDistance = TrailingStopPoints * step;
		var trailingStepDistance = TrailingStepPoints * step;

		if (Position > 0)
		{
			_maxPriceSinceEntry = Math.Max(_maxPriceSinceEntry, candle.HighPrice);

			if (stopLossDistance > 0m && candle.LowPrice <= _entryPrice - stopLossDistance)
			{
				SellMarket(Position);
				_lastTradeTime = now;
				ResetPositionState();
				return;
			}

			if (takeProfitDistance > 0m && candle.HighPrice >= _entryPrice + takeProfitDistance)
			{
				SellMarket(Position);
				_lastTradeTime = now;
				ResetPositionState();
				return;
			}

			if (trailingStopDistance > 0m)
			{
				var move = candle.ClosePrice - _entryPrice;
				if (move >= trailingStopDistance + trailingStepDistance)
				{
					var newTrail = candle.ClosePrice - trailingStopDistance;
					if (!_trailingActive || newTrail > _trailingStopPrice)
					{
						_trailingStopPrice = newTrail;
						_trailingActive = true;
					}
				}

				if (_trailingActive && candle.LowPrice <= _trailingStopPrice)
				{
					SellMarket(Position);
					_lastTradeTime = now;
					ResetPositionState();
					return;
				}
			}
		}
		else if (Position < 0)
		{
			_minPriceSinceEntry = Math.Min(_minPriceSinceEntry, candle.LowPrice);

			if (stopLossDistance > 0m && candle.HighPrice >= _entryPrice + stopLossDistance)
			{
				BuyMarket(-Position);
				_lastTradeTime = now;
				ResetPositionState();
				return;
			}

			if (takeProfitDistance > 0m && candle.LowPrice <= _entryPrice - takeProfitDistance)
			{
				BuyMarket(-Position);
				_lastTradeTime = now;
				ResetPositionState();
				return;
			}

			if (trailingStopDistance > 0m)
			{
				var move = _entryPrice - candle.ClosePrice;
				if (move >= trailingStopDistance + trailingStepDistance)
				{
					var newTrail = candle.ClosePrice + trailingStopDistance;
					if (!_trailingActive || newTrail < _trailingStopPrice)
					{
						_trailingStopPrice = newTrail;
						_trailingActive = true;
					}
				}

				if (_trailingActive && candle.HighPrice >= _trailingStopPrice)
				{
					BuyMarket(-Position);
					_lastTradeTime = now;
					ResetPositionState();
					return;
				}
			}
		}

		var currentPnL = CalculateOpenPnL(candle.ClosePrice, step, stepPrice);
		if (ProfitTarget > 0m && currentPnL >= ProfitTarget)
		{
			if (Position > 0)
			SellMarket(Position);
			else if (Position < 0)
			BuyMarket(-Position);

			_lastTradeTime = now;
			ResetPositionState();
			return;
		}
	}

	private decimal CalculateOpenPnL(decimal closePrice, decimal step, decimal stepPrice)
	{
		if (Position == 0)
		return 0m;

		if (step <= 0m)
		step = 1m;
		if (stepPrice <= 0m)
		stepPrice = step;

		if (Position > 0)
		{
			var diff = closePrice - _entryPrice;
			return diff / step * stepPrice * Position;
		}
		else
		{
			var diff = _entryPrice - closePrice;
			return diff / step * stepPrice * -Position;
		}
	}

	private void OpenLong(ICandleMessage candle, decimal volume)
	{
		BuyMarket(volume);
		_entryPrice = candle.ClosePrice;
		_maxPriceSinceEntry = candle.ClosePrice;
		_minPriceSinceEntry = candle.ClosePrice;
		_trailingActive = false;
		_trailingStopPrice = 0m;
		_lastTradeTime = candle.CloseTime;
	}

	private void OpenShort(ICandleMessage candle, decimal volume)
	{
		SellMarket(volume);
		_entryPrice = candle.ClosePrice;
		_maxPriceSinceEntry = candle.ClosePrice;
		_minPriceSinceEntry = candle.ClosePrice;
		_trailingActive = false;
		_trailingStopPrice = 0m;
		_lastTradeTime = candle.CloseTime;
	}

	private void ResetPositionState()
	{
		_entryPrice = 0m;
		_maxPriceSinceEntry = 0m;
		_minPriceSinceEntry = 0m;
		_trailingActive = false;
		_trailingStopPrice = 0m;
	}

	private bool AllowEntryTime(DateTimeOffset time)
	{
		return true;
	}

	private void UpdateZigZag(ICandleMessage candle)
	{
		var step = GetPriceStep();
		var deviation = ZigZagDeviation * step;
		var minBars = Math.Max(1, Math.Max(ZigZagDepth, ZigZagBackstep));

		if (_currentExtreme is null)
		{
			_currentExtreme = _searchDirection > 0 ? candle.HighPrice : candle.LowPrice;
			_barsSinceExtreme = 0;
			return;
		}

		if (_searchDirection > 0)
		{
			if (candle.HighPrice > _currentExtreme.Value)
			{
				_currentExtreme = candle.HighPrice;
				_barsSinceExtreme = 0;
			}
			else
			{
				_barsSinceExtreme++;
			}

			var drop = _currentExtreme.Value - candle.LowPrice;
			if (drop >= deviation && _barsSinceExtreme >= minBars)
			{
				_previousZigZag = _lastZigZag;
				_lastZigZag = _currentExtreme;
				_searchDirection = -1;
				_currentExtreme = candle.LowPrice;
				_barsSinceExtreme = 0;
			}
		}
		else
		{
			if (candle.LowPrice < _currentExtreme.Value)
			{
				_currentExtreme = candle.LowPrice;
				_barsSinceExtreme = 0;
			}
			else
			{
				_barsSinceExtreme++;
			}

			var rise = candle.HighPrice - _currentExtreme.Value;
			if (rise >= deviation && _barsSinceExtreme >= minBars)
			{
				_previousZigZag = _lastZigZag;
				_lastZigZag = _currentExtreme;
				_searchDirection = 1;
				_currentExtreme = candle.HighPrice;
				_barsSinceExtreme = 0;
			}
		}
	}

	private decimal GetPriceStep()
	{
		var step = Security?.PriceStep ?? 1m;
		return step > 0m ? step : 1m;
	}

	private decimal GetStepPrice(decimal step)
	{
		var stepPrice = step;
		return stepPrice > 0m ? stepPrice : step;
	}

	private sealed class ImpulseIndicator : BaseIndicator
	{
		public int Length { get; set; } = 21;
		public decimal PriceStep { get; set; } = 1m;

		private readonly Queue<decimal> _buffer = new();
		private decimal _sum;

		protected override IIndicatorValue OnProcess(IIndicatorValue input)
		{
			var candle = input.GetValue<ICandleMessage>();
			var step = PriceStep > 0m ? PriceStep : 1m;
			var value = (candle.OpenPrice - candle.ClosePrice) / step;

			_buffer.Enqueue(value);
			_sum += value;

			if (_buffer.Count > Length)
			_sum -= _buffer.Dequeue();

			if (_buffer.Count < Length)
			{
				IsFormed = false;
				return new DecimalIndicatorValue(this, 0m, input.Time);
			}

			IsFormed = true;
			var average = _sum / Length;
			return new DecimalIndicatorValue(this, average, input.Time);
		}

		public override void Reset()
		{
			base.Reset();
			_buffer.Clear();
			_sum = 0m;
		}
	}
}