在 GitHub 上查看

Gazonkos 回撤策略

概述

Gazonkos 回撤策略源自 MetaTrader 5 平台上的 gazonkos 专家顾问。本策略同样以 EUR/USD 小时级别为主要场景,通过比较两根历史 K 线的收盘价来寻找强势动量,并在出现预设幅度的回撤后顺势入场。StockSharp 版本沿用了原始 EA 的状态机流程,使用高级 API、蜡烛订阅以及保护性订单完成整个逻辑。

交易流程

  1. 资格检查:系统限定每个整点最多只开一笔新仓。如果当前小时已经有交易,或现有仓位数量达到 ActiveTrades 限制,则跳过本次信号。
  2. 动量检测:计算 SecondShiftFirstShift 两根历史 K 线收盘价的差值。差值超过 Delta 时,记录交易方向(最新收盘价更高时做多,反之做空)。
  3. 回撤确认:从动量产生的那一刻起,在同一个小时内监控最高价(做多)或最低价(做空)。当价格向相反方向回撤至少 Rollback 时,视为确认信号;如果在满足回撤之前小时发生改变,则信号作废。
  4. 执行下单:回撤成立后立即按记录的方向市价成交,并通过 StartProtection 设定固定的止盈和止损距离。下单手数由 TradeVolume 控制。

该流程与原始 MT5 程序中依靠 STATETrade 变量驱动的顺序完全一致。

风险控制

  • StartProtection 在绝对价格单位内设置止盈/止损,与原策略一笔订单一组保护单的思路相同。
  • ActiveTrades 根据当前净持仓与允许的最大仓位数量比较,限制总体风险敞口。
  • 小时节奏加上回撤过滤可以减少盘整阶段的过度交易。

参数说明

名称 默认值 说明
TakeProfit 0.0016 止盈距离,单位为绝对价格,约等于 16 个 5 位点。
Rollback 0.0016 信号确认所需的最小回撤幅度。
StopLoss 0.0040 止损距离,约等于 40 个点。
Delta 0.0040 判断动量的最小收盘价差值。
TradeVolume 0.1 市价单默认下单手数。
FirstShift 3 较早的历史 K 线索引(向前回溯的根数)。
SecondShift 2 较新的历史 K 线索引。
ActiveTrades 1 允许同时持有的最大交易数量,设为 0 表示不限制。
CandleType 1 小时 使用的蜡烛类型,默认等同于原始 EA 的小时线。

其他提示

  • 请根据交易品种的点值调整 DeltaRollbackTakeProfitStopLoss
  • 代码中的注释均为英文,符合仓库规范。
  • 当前未提供 Python 版本的实现。
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>
/// Momentum breakout with rollback confirmation inspired by the gazonkos MT5 expert.
/// The strategy waits for a spread between two historical closes, then joins the trend after a pullback.
/// </summary>
public class GazonkosStrategy : Strategy
{
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _rollback;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _delta;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _firstShift;
	private readonly StrategyParam<int> _secondShift;
	private readonly StrategyParam<int> _activeTrades;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closeHistory = new();

	private int _state;
	private int _tradeDirection;
	private decimal _maxPrice;
	private decimal _minPrice;
	private bool _canTrade;
	private int _lastTradeHour;
	private int _lastSignalHour;
	private int _maxHistory;

	/// <summary>
	/// Take profit distance expressed in absolute price units.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Rollback distance that confirms the entry.
	/// </summary>
	public decimal Rollback
	{
		get => _rollback.Value;
		set => _rollback.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in absolute price units.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Minimum difference between historical closes to detect momentum.
	/// </summary>
	public decimal Delta
	{
		get => _delta.Value;
		set => _delta.Value = value;
	}

	/// <summary>
	/// Default volume for market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Older bar shift used in the close difference calculation.
	/// </summary>
	public int FirstShift
	{
		get => _firstShift.Value;
		set => _firstShift.Value = value;
	}

	/// <summary>
	/// Recent bar shift used in the close difference calculation.
	/// </summary>
	public int SecondShift
	{
		get => _secondShift.Value;
		set => _secondShift.Value = value;
	}

	/// <summary>
	/// Maximum simultaneous trades counted in volume units.
	/// </summary>
	public int ActiveTrades
	{
		get => _activeTrades.Value;
		set => _activeTrades.Value = value;
	}

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

	public GazonkosStrategy()
	{
		_takeProfit = Param(nameof(TakeProfit), 700m)
			.SetDisplay("Take Profit", "Take profit distance in price units", "Risk Management")
			;

		_rollback = Param(nameof(Rollback), 300m)
			.SetDisplay("Rollback", "Required pullback before entering", "Signals")
			;

		_stopLoss = Param(nameof(StopLoss), 1000m)
			.SetDisplay("Stop Loss", "Stop loss distance in price units", "Risk Management")
			;

		_delta = Param(nameof(Delta), 200m)
			.SetDisplay("Delta", "Minimum difference between closes", "Signals")
			;

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetDisplay("Trade Volume", "Default volume for market orders", "Orders")
			;

		_firstShift = Param(nameof(FirstShift), 3)
			.SetDisplay("First Shift", "Older close shift for the comparison", "Signals")
			;

		_secondShift = Param(nameof(SecondShift), 2)
			.SetDisplay("Second Shift", "Recent close shift for the comparison", "Signals")
			;

		_activeTrades = Param(nameof(ActiveTrades), 1)
			.SetDisplay("Active Trades", "Maximum simultaneous trades", "Risk Management")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Candle series used for signals", "General");
	}

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

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

		_state = 0;
		_tradeDirection = 0;
		_maxPrice = 0m;
		_minPrice = decimal.MaxValue;
		_canTrade = true;
		_lastTradeHour = -1;
		_lastSignalHour = -1;
		_closeHistory.Clear();
		UpdateHistorySize();
	}

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

		Volume = TradeVolume;
		UpdateHistorySize();

		StartProtection(
			takeProfit: new Unit(TakeProfit, UnitTypes.Absolute),
			stopLoss: new Unit(StopLoss, UnitTypes.Absolute),
			isStopTrailing: false,
			useMarketOrders: true);

		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;

		UpdateHistorySize();
		AddClose(candle.ClosePrice);

		var hour = candle.CloseTime.Hour;

		if (_state == 0)
		{
			// Evaluate if another trade can be started during the current hour.
			_canTrade = true;

			if (_lastTradeHour == hour)
				_canTrade = false;

			if (ActiveTrades > 0 && Volume > 0 && Math.Abs(Position) >= ActiveTrades * Volume)
				_canTrade = false;

			if (_canTrade)
				_state = 1;
		}

		if (_state == 1)
		{
			// Look for momentum using the difference between historical closes.
			if (!TryGetClose(FirstShift, out var closeFirst) || !TryGetClose(SecondShift, out var closeSecond))
				return;

			if (closeSecond - closeFirst > Delta)
			{
				_tradeDirection = 1;
				_maxPrice = candle.ClosePrice;
				_lastSignalHour = hour;
				_state = 2;
			}
			else if (closeFirst - closeSecond > Delta)
			{
				_tradeDirection = -1;
				_minPrice = candle.ClosePrice;
				_lastSignalHour = hour;
				_state = 2;
			}
		}

		if (_state == 2)
		{
			// Wait for a rollback confirmation during the same hour when the signal appeared.
			if (_lastSignalHour != hour)
			{
				ResetToIdle();
				return;
			}

			if (_tradeDirection == 1)
			{
				if (candle.HighPrice > _maxPrice)
					_maxPrice = candle.HighPrice;

				if (candle.LowPrice < _maxPrice - Rollback)
					_state = 3;
			}
			else if (_tradeDirection == -1)
			{
				if (candle.LowPrice < _minPrice)
					_minPrice = candle.LowPrice;

				if (candle.HighPrice > _minPrice + Rollback)
					_state = 3;
			}
		}

		if (_state == 3)
		{
			// Execute the trade after rollback confirmation.
			if (_tradeDirection == 1 && Position <= 0)
			{
				BuyMarket();
				_lastTradeHour = hour;
				ResetToIdle();
			}
			else if (_tradeDirection == -1 && Position >= 0)
			{
				SellMarket();
				_lastTradeHour = hour;
				ResetToIdle();
			}
		}
	}

	private void UpdateHistorySize()
	{
		var required = Math.Max(Math.Max(FirstShift, SecondShift) + 1, 1);

		if (_maxHistory == required)
			return;

		_maxHistory = required;

		if (_closeHistory.Count > _maxHistory)
			_closeHistory.RemoveRange(_maxHistory, _closeHistory.Count - _maxHistory);
	}

	private void AddClose(decimal close)
	{
		_closeHistory.Insert(0, close);

		if (_closeHistory.Count > _maxHistory)
			_closeHistory.RemoveAt(_closeHistory.Count - 1);
	}

	private bool TryGetClose(int shift, out decimal close)
	{
		close = 0m;

		if (shift < 0)
			return false;

		if (_closeHistory.Count <= shift)
			return false;

		close = _closeHistory[shift];
		return true;
	}

	private void ResetToIdle()
	{
		_state = 0;
		_tradeDirection = 0;
		_maxPrice = 0m;
		_minPrice = decimal.MaxValue;
		_canTrade = true;
		_lastSignalHour = -1;
	}
}