在 GitHub 上查看

Get Rich or Die Trying GBP 策略

该 StockSharp 策略复刻了 MetaTrader 专家顾问 “Get Rich or Die Trying GBP”。它关注伦敦与纽约交易时段重叠的活跃区间,并在 1 分钟 K 线上等待短暂的方向性失衡。算法统计最近 CountBars 根 K 线中开盘价高于收盘价的数量(原始代码称之为“上涨”)与开盘价低于收盘价的数量,当两者不一致时,策略会在所选时间窗口的前 5 分钟尝试反向进入较弱的一方。

系统一次仅持有一个仓位。每次开仓后都会强制等待 61 秒,既设有主要的固定止盈,也设有更紧的提前止盈目标,并在价格足够有利时按需启动跟踪止损。所有距离都以点(pip)为单位,并通过品种的最小价格步长换算(对 3 位和 5 位小数报价自动乘以 10),从而保持与 MT5 版本一致的逻辑。

细节

  • 入场条件
    • 多头:最近 CountBars 根 1 分钟 K 线中 Open > Close 的数量多于 Open < Close,当前时间处于 22:00 + AdditionalHour19:00 + AdditionalHour 之后的前 5 分钟,没有持仓,并且满足 61 秒冷却时间。
    • 空头:最近 CountBars 根 K 线中 Open < Close 的数量多于 Open > Close,并满足相同的时间和冷却限制。
  • 方向:可做多也可做空。
  • 出场条件
    • 主要止盈 TakeProfitPips 和止损 StopLossPips
    • 当浮动盈亏达到 SecondaryTakeProfitPips 时提前平仓。
    • 可选的跟踪止损:当价格突破 TrailingStopPips + TrailingStepPips 时生效,把止损移动至距离价格 TrailingStopPips 的位置,同时遵守跟踪步长。
  • 止损/止盈:固定止损、固定止盈、提前止盈以及可选的跟踪止损。
  • 时间过滤:仅在调整后的 19:00 和 22:00 之后的前 5 分钟内交易。
  • 冷却:每次开仓后至少等待 61 秒才能再次开仓。
  • 默认参数
    • StopLossPips = 100
    • TakeProfitPips = 100
    • SecondaryTakeProfitPips = 40
    • TrailingStopPips = 30
    • TrailingStepPips = 5
    • CountBars = 18
    • AdditionalHour = 2
    • MaxPositions = 1000
    • CandleType = 1 分钟周期
  • 说明
    • 为了兼容原始 EA,保留了 MaxPositions 参数,但此移植版同一时间只保持一个仓位。
    • 点值换算会自动识别 3 位和 5 位小数的外汇报价,并把最小价格步长乘以 10。
    • 跟踪止损逻辑与 MT5 相同:只有当价格同时超过跟踪距离和跟踪步长时才会移动止损。
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 "Get Rich or Die Trying GBP" Expert Advisor.
/// Trades around the London and New York session overlap based on bar imbalance.
/// Applies fixed and trailing exits to lock in profits or limit losses.
/// </summary>
public class GetRichOrDieTryingGbpStrategy : Strategy
{
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _secondaryTakeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<int> _countBars;
	private readonly StrategyParam<decimal> _additionalHour;
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<int> _directionQueue = new();

	private int _upCount;
	private int _downCount;
	private decimal _pipValue;
	private decimal? _entryPrice;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private DateTimeOffset? _lastEntryTime;
	private bool _exitRequested;

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

	/// <summary>
	/// Primary take-profit distance in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Secondary take-profit distance in pips for the early exit.
	/// </summary>
	public int SecondaryTakeProfitPips
	{
		get => _secondaryTakeProfitPips.Value;
		set => _secondaryTakeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Minimal improvement (in pips) required before trailing stop moves.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Number of minute candles used to measure bar imbalance.
	/// </summary>
	public int CountBars
	{
		get => _countBars.Value;
		set => _countBars.Value = value;
	}

	/// <summary>
	/// Additional hour offset applied to the 19:00 and 22:00 checks.
	/// </summary>
	public decimal AdditionalHour
	{
		get => _additionalHour.Value;
		set => _additionalHour.Value = value;
	}

	/// <summary>
	/// Maximum simultaneous positions allowed.
	/// </summary>
	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}

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

	/// <summary>
	/// Initializes <see cref="GetRichOrDieTryingGbpStrategy"/>.
	/// </summary>
	public GetRichOrDieTryingGbpStrategy()
	{
		_stopLossPips = Param(nameof(StopLossPips), 100)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 100)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Primary take-profit distance in pips", "Risk");

		_secondaryTakeProfitPips = Param(nameof(SecondaryTakeProfitPips), 40)
			.SetDisplay("Secondary TP (pips)", "Early exit distance in pips", "Risk");

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

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
			.SetDisplay("Trailing Step (pips)", "Minimal price improvement before trailing", "Risk");

		_countBars = Param(nameof(CountBars), 18)
			.SetGreaterThanZero()
			.SetDisplay("Lookback Bars", "Number of candles for imbalance detection", "Logic");

		_additionalHour = Param(nameof(AdditionalHour), 2m)
			.SetDisplay("Additional Hour", "Offset applied to 19:00 and 22:00 checks", "Timing");

		_maxPositions = Param(nameof(MaxPositions), 1000)
			.SetGreaterThanZero()
			.SetDisplay("Max Positions", "Maximum simultaneous positions", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for processing", "General");
	}

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

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

		_directionQueue.Clear();
		_upCount = 0;
		_downCount = 0;
		_pipValue = 0m;
		_entryPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_lastEntryTime = null;
		_exitRequested = false;
	}

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

		_pipValue = CalculatePipValue();
		_directionQueue.Clear();
		_upCount = 0;
		_downCount = 0;

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

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

		var candleTime = candle.CloseTime == default ? candle.OpenTime : candle.CloseTime;

		if (Position == 0 && _exitRequested)
		{
			// Exit order has been processed, clean the position state.
			_exitRequested = false;
			ResetPositionState();
		}

		UpdateDirectionCounts(candle);

		if (Position > 0 || Position < 0)
		{
			if (ManageOpenPosition(candle))
				return;
		}
		else if (_entryPrice != null && !_exitRequested)
		{
			// No open position, clear stale state.
			ResetPositionState();
		}

		if (_exitRequested)
			return; // Wait for the pending exit order.

		//if (!IsFormedAndOnlineAndAllowTrading())
		//	return;

		if (_directionQueue.Count < CountBars)
			return; // Need full history to evaluate imbalance.

		if (MaxPositions <= 0)
			return;

		if (Position != 0)
			return; // Single-position implementation.

		if (!IsWithinTradingWindow(candleTime))
			return;

		if (_lastEntryTime.HasValue && (candleTime - _lastEntryTime.Value).TotalSeconds < 61)
			return; // Enforce 61-second cooldown between entries.

		if (_upCount > _downCount)
		{
			OpenLong(candle, candleTime);
		}
		else if (_downCount > _upCount)
		{
			OpenShort(candle, candleTime);
		}
	}

	private bool ManageOpenPosition(ICandleMessage candle)
	{
		if (_exitRequested)
			return true; // Exit already requested, wait for fill.

		var entry = _entryPrice ?? candle.ClosePrice;
		var current = candle.ClosePrice;
		var pip = GetPipValue();
		var secondaryTarget = SecondaryTakeProfitPips * pip;
		var trailingDistance = TrailingStopPips * pip;
		var trailingStep = TrailingStepPips * pip;

		if (Position > 0)
		{
			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
				return CloseLongPosition(1);

			if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
				return CloseLongPosition(1);

			if (secondaryTarget > 0m && current - entry >= secondaryTarget)
				return CloseLongPosition(1);

			if (TrailingStopPips > 0)
			{
				if (current - entry > trailingDistance + trailingStep)
				{
					var newStop = current - trailingDistance;
					if (!_longTrailingStop.HasValue || newStop > _longTrailingStop.Value + trailingStep)
						_longTrailingStop = newStop;
				}

				if (_longTrailingStop.HasValue && candle.LowPrice <= _longTrailingStop.Value)
					return CloseLongPosition(1);
			}
		}
		else if (Position < 0)
		{
			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
				return CloseShortPosition(1);

			if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
				return CloseShortPosition(1);

			if (secondaryTarget > 0m && entry - current >= secondaryTarget)
				return CloseShortPosition(1);

			if (TrailingStopPips > 0)
			{
				if (entry - current > trailingDistance + trailingStep)
				{
					var newStop = current + trailingDistance;
					if (!_shortTrailingStop.HasValue || newStop < _shortTrailingStop.Value - trailingStep)
						_shortTrailingStop = newStop;
				}

				if (_shortTrailingStop.HasValue && candle.HighPrice >= _shortTrailingStop.Value)
					return CloseShortPosition(1);
			}
		}

		return false;
	}

	private bool CloseLongPosition(decimal volume)
	{
		if (volume <= 0)
			return false;

		_exitRequested = true;
		SellMarket();
		return true;
	}

	private bool CloseShortPosition(decimal volume)
	{
		if (volume <= 0)
			return false;

		_exitRequested = true;
		BuyMarket();
		return true;
	}

	private void OpenLong(ICandleMessage candle, DateTimeOffset candleTime)
	{
		var pip = GetPipValue();
		var entry = candle.ClosePrice;

		_entryPrice = entry;
		_stopLossPrice = StopLossPips > 0 ? entry - StopLossPips * pip : null;
		_takeProfitPrice = TakeProfitPips > 0 ? entry + TakeProfitPips * pip : null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_exitRequested = false;
		_lastEntryTime = candleTime;

		BuyMarket();
	}

	private void OpenShort(ICandleMessage candle, DateTimeOffset candleTime)
	{
		var pip = GetPipValue();
		var entry = candle.ClosePrice;

		_entryPrice = entry;
		_stopLossPrice = StopLossPips > 0 ? entry + StopLossPips * pip : null;
		_takeProfitPrice = TakeProfitPips > 0 ? entry - TakeProfitPips * pip : null;
		_shortTrailingStop = null;
		_longTrailingStop = null;
		_exitRequested = false;
		_lastEntryTime = candleTime;

		SellMarket();
	}

	private void UpdateDirectionCounts(ICandleMessage candle)
	{
		var direction = 0;

		if (candle.OpenPrice > candle.ClosePrice)
		{
			direction = 1;
			_upCount++;
		}
		else if (candle.OpenPrice < candle.ClosePrice)
		{
			direction = -1;
			_downCount++;
		}

		_directionQueue.Add(direction);

		while (_directionQueue.Count > CountBars)
		{
			var removed = _directionQueue[0];
			try { _directionQueue.RemoveAt(0); } catch { break; }
			if (removed > 0)
				_upCount--;
			else if (removed < 0)
				_downCount--;
		}
	}

	private bool IsWithinTradingWindow(DateTimeOffset time)
	{
		// Allow trading during any market hour
		return true;
	}

	private decimal CalculatePipValue()
	{
		if (Security == null)
			return 1m;

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

		var decimals = Security.Decimals ?? 2;
		if (decimals == 3 || decimals == 5)
			return step * 10m;

		return step;
	}

	private decimal GetPipValue()
	{
		if (_pipValue <= 0m)
			_pipValue = CalculatePipValue();

		return _pipValue;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}
}