在 GitHub 上查看

Big Dog 突破策略

Big Dog 策略用于在伦敦早盘寻找窄幅整理区,并在价格向上或向下突破该区间时入场交易。原始的 MQL 专家顾问会在 StartHourStopHour 之间形成的价格区间足够窄时挂出止损单。本移植版在 StockSharp 框架中保持同样的思路:先确认区间,再在突破时用市价单开仓,同时根据整理区的高低点计算止损与止盈。

交易逻辑

  1. 在每天 StartHour(含)到 StopHour(默认不含)之间收集完成的 K 线,构建当天的高低区间。
  2. 如果区间高度超过 MaxRangePoints(通过“调整点值”转换为价格单位),则忽略该交易日。
  3. 区间结束后,比较最新最优买卖价与突破价之间的距离。只有当市场价格距离上沿或下沿至少有 DistancePoints(调整点值)时,才认为突破计划有效。
  4. 之后的 K 线若向上突破上沿,则按照 OrderVolume(自动抵消反向持仓)开多;若向下突破下沿,则开空。
  5. 入场后立即设定退出条件:
    • 多单止损放在整理区低点,止盈位于入场价上方 TakeProfitPoints 的距离。
    • 空单止损放在整理区高点,止盈位于入场价下方 TakeProfitPoints 的距离。
  6. 每根完成的 K 线都会检查最高价或最低价是否触及止损 / 止盈,满足条件时立即平仓。
  7. 新交易日开始时会重置所有缓存数据,避免上一日的水平影响当日决策。

关于调整点值:策略会将以“点”为单位的参数乘以 PriceStep,若标的价格有 3 或 5 位小数,则再乘以 10,以模拟原 EA 中“1 点 = 1 个标准 pip” 的换算。

参数说明

参数 说明 默认值
StartHour 计算整理区的起始小时(0-23)。 14
StopHour 计算整理区的结束小时(0-23)。 16
MaxRangePoints 整理区允许的最大高度(调整点值)。 50
TakeProfitPoints 突破后的止盈距离(调整点值)。 50
DistancePoints 触发突破前价格与区间边界的最小距离(调整点值)。 20
OrderVolume 每次突破下单的手数(同时赋值给策略 Volume)。 1
CandleType 用于构建整理区的 K 线类型,默认 1 小时。 1h

实现细节

  • 策略同时订阅 K 线与盘口深度。若盘口不可用,则使用最新 K 线收盘价作为距离判断的回退值。
  • 入场采用市价单,模拟原策略的挂止损单行为,但实现更贴合 StockSharp 的高层 API。
  • 止损与止盈并未额外挂单,而是在 K 线完成时根据最高 / 最低价判断是否触发,以接近原 EA 设置的保护水平。
  • 当日期变化时,策略会取消活动委托并重置区间信息,确保新的一天从零开始。
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>
/// Big Dog range breakout strategy that trades breakouts from a tight range built between configurable hours.
/// </summary>
public class BigDogStrategy : Strategy
{
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _stopHour;
	private readonly StrategyParam<decimal> _maxRangePoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _distancePoints;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _rangeHigh;
	private decimal? _rangeLow;
	private DateTime? _rangeDate;
	private bool _longReady;
	private bool _shortReady;
	private decimal? _longStopPrice;
	private decimal? _longTakeProfitPrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakeProfitPrice;
	private decimal _longEntryPrice;
	private decimal _shortEntryPrice;
	private decimal? _bestBid;
	private decimal? _bestAsk;
	private decimal _adjustedPointSize;

	/// <summary>
	/// Hour (0-23) when the consolidation range calculation starts.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Hour (0-23) when the consolidation range calculation stops.
	/// </summary>
	public int StopHour
	{
		get => _stopHour.Value;
		set => _stopHour.Value = value;
	}

	/// <summary>
	/// Maximum acceptable range height measured in adjusted points.
	/// </summary>
	public decimal MaxRangePoints
	{
		get => _maxRangePoints.Value;
		set => _maxRangePoints.Value = value;
	}

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

	/// <summary>
	/// Minimum distance required between the current price and breakout level, measured in adjusted points.
	/// </summary>
	public decimal DistancePoints
	{
		get => _distancePoints.Value;
		set => _distancePoints.Value = value;
	}

	/// <summary>
	/// Order volume that will be used for entries.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Candle type used for range calculation and breakout detection.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="BigDogStrategy"/> class.
	/// </summary>
	public BigDogStrategy()
	{
		_startHour = Param(nameof(StartHour), 2)
			.SetRange(0, 23)
			.SetDisplay("Start Hour", "Hour to begin measuring the range", "Session");

		_stopHour = Param(nameof(StopHour), 8)
			.SetRange(0, 23)
			.SetDisplay("Stop Hour", "Hour to stop measuring the range", "Session");

		_maxRangePoints = Param(nameof(MaxRangePoints), 50000m)
			.SetGreaterThanZero()
			.SetDisplay("Max Range", "Maximum allowed height of the consolidation range (points)", "Trading");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit", "Take-profit distance in adjusted points", "Trading");

		_distancePoints = Param(nameof(DistancePoints), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Min Distance", "Minimum distance from current price to breakout level (points)", "Trading");

		_orderVolume = Param(nameof(OrderVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume used for each breakout order", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candles timeframe used for range detection", "Data");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_rangeHigh = null;
		_rangeLow = null;
		_rangeDate = null;
		_longReady = false;
		_shortReady = false;
		_longStopPrice = null;
		_longTakeProfitPrice = null;
		_shortStopPrice = null;
		_shortTakeProfitPrice = null;
		_longEntryPrice = 0m;
		_shortEntryPrice = 0m;
		_bestBid = null;
		_bestAsk = null;
		_adjustedPointSize = 0m;
	}

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

		Volume = OrderVolume;
		_adjustedPointSize = CalculateAdjustedPointSize();

		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;

		var currentDate = candle.OpenTime.Date;

		if (_rangeDate != currentDate)
		{
			ResetDailyState(currentDate);
		}

		UpdateRange(candle);

		if (candle.OpenTime.Hour >= StopHour)
		{
			PrepareBreakoutLevels(candle);
		}

		ProcessEntries(candle);
		ProcessRiskManagement(candle);
	}

	private void ResetDailyState(DateTime date)
	{
		_rangeDate = date;
		_rangeHigh = null;
		_rangeLow = null;
		_longReady = false;
		_shortReady = false;
		_longStopPrice = null;
		_longTakeProfitPrice = null;
		_shortStopPrice = null;
		_shortTakeProfitPrice = null;
	}

	private void UpdateRange(ICandleMessage candle)
	{
		var hour = candle.OpenTime.Hour;

		if (hour < StartHour || hour >= StopHour)
			return;

		_rangeHigh = _rangeHigh.HasValue
			? Math.Max(_rangeHigh.Value, candle.HighPrice)
			: candle.HighPrice;

		_rangeLow = _rangeLow.HasValue
			? Math.Min(_rangeLow.Value, candle.LowPrice)
			: candle.LowPrice;
	}

	private void PrepareBreakoutLevels(ICandleMessage candle)
	{
		if (!_rangeHigh.HasValue || !_rangeLow.HasValue)
			return;

		var rangeHeight = _rangeHigh.Value - _rangeLow.Value;
		var maxRange = ConvertToPrice(MaxRangePoints);

		if (rangeHeight >= maxRange)
		{
			// Reset pending plans when the range becomes too wide.
			_longReady = false;
			_shortReady = false;
			return;
		}

		var minDistance = ConvertToPrice(DistancePoints);
		var ask = _bestAsk ?? candle.ClosePrice;
		var bid = _bestBid ?? candle.ClosePrice;

		if (!_longReady && Position >= 0 && (_rangeHigh.Value - ask) > minDistance)
		{
			_longReady = true;
			_longEntryPrice = _rangeHigh.Value;
			_longStopPrice = _rangeLow.Value;
			_longTakeProfitPrice = _rangeHigh.Value + ConvertToPrice(TakeProfitPoints);
		}

		if (!_shortReady && Position <= 0 && (bid - _rangeLow.Value) > minDistance)
		{
			_shortReady = true;
			_shortEntryPrice = _rangeLow.Value;
			_shortStopPrice = _rangeHigh.Value;
			_shortTakeProfitPrice = _rangeLow.Value - ConvertToPrice(TakeProfitPoints);
		}
	}

	private void ProcessEntries(ICandleMessage candle)
	{
		var volume = OrderVolume;

		if (_longReady && candle.HighPrice >= _longEntryPrice && Position <= 0)
		{
			// Enter long on breakout of the session high.
			BuyMarket();

			_longReady = false;
			_shortReady = false;
		}

		if (_shortReady && candle.LowPrice <= _shortEntryPrice && Position >= 0)
		{
			// Enter short on breakout of the session low.
			SellMarket();

			_shortReady = false;
			_longReady = false;
		}
	}

	private void ProcessRiskManagement(ICandleMessage candle)
	{
		if (Position > 0 && _longStopPrice.HasValue && _longTakeProfitPrice.HasValue)
		{
			// Close the long position if stop-loss or take-profit levels are touched.
			if (candle.LowPrice <= _longStopPrice.Value)
			{
				SellMarket();
				_longStopPrice = null;
				_longTakeProfitPrice = null;
			}
			else if (candle.HighPrice >= _longTakeProfitPrice.Value)
			{
				SellMarket();
				_longStopPrice = null;
				_longTakeProfitPrice = null;
			}
		}
		else if (Position < 0 && _shortStopPrice.HasValue && _shortTakeProfitPrice.HasValue)
		{
			// Close the short position if stop-loss or take-profit levels are touched.
			if (candle.HighPrice >= _shortStopPrice.Value)
			{
				BuyMarket();
				_shortStopPrice = null;
				_shortTakeProfitPrice = null;
			}
			else if (candle.LowPrice <= _shortTakeProfitPrice.Value)
			{
				BuyMarket();
				_shortStopPrice = null;
				_shortTakeProfitPrice = null;
			}
		}
		else
		{
			_longStopPrice = null;
			_longTakeProfitPrice = null;
			_shortStopPrice = null;
			_shortTakeProfitPrice = null;
		}
	}

	private decimal ConvertToPrice(decimal points)
	{
		return points * _adjustedPointSize;
	}

	private decimal CalculateAdjustedPointSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 1m;

		var decimals = Security?.Decimals ?? 0;
		var multiplier = decimals == 3 || decimals == 5 ? 10m : 1m;

		return step * multiplier;
	}
}