在 GitHub 上查看

Daily BreakPoint 策略

概述

Daily BreakPoint Strategy 是将 MetaTrader 5 专家顾问 “Daily BreakPoint”(构建号 19498)迁移到 StockSharp 的版本。策略关注当前价格与当日开盘价之间的距离。当价格偏离开盘价超过可配置阈值,并且上一根 K 线的实体满足设定的范围时,系统会按照 CloseBySignal 参数的设置选择顺势建仓或直接反向开仓。

策略同时订阅两类数据:

  1. CandleType 指定的日内 K 线,用于产生交易信号。
  2. 日线数据,用于跟踪最近一个交易日的开盘价。

交易逻辑

  1. 每当一根日内 K 线收盘时,策略读取最新的日线开盘价,并使用 BreakPointPips(通过合约最小价位转换为绝对价格)计算上下突破水平。
  2. 最近收盘的 K 线实体必须位于 [LastBarSizeMinPips, LastBarSizeMaxPips] 区间内。
  3. 看涨条件
    • K 线收阳 (Close > Open)。
    • 收盘价至少高于当日开盘价 BreakPointPips
    • 突破价格(开盘价 + BreakPoint)必须落在 K 线实体内部。
    • CloseBySignal = false 时,策略做多;CloseBySignal = true 时,先平掉已有多头再开新空头。
  4. 看跌条件 对称:收阴 K 线、收盘价至少低于当日开盘价 BreakPointPips,并且突破价落在实体内部。满足条件后,CloseBySignal = false 时做空,CloseBySignal = true 时先平旧空头再开多头。
  5. 所有订单均以市价下单,手数为 OrderVolume。仓位是累计的,多次信号可以逐步加仓或反向减仓。

风险控制

  • 止损 / 止盈:通过 StopLossPipsTakeProfitPips(单位为点)设置。值为 0 表示关闭该功能。策略使用 K 线最高价和最低价判断是否触发。
  • 移动止损:当 TrailingStopPips > 0 时启用。当浮动盈利超过 TrailingStopPips + TrailingStepPips 后,将止损价跟随价格移动,保持 TrailingStopPips 的距离;TrailingStepPips 可避免在震荡行情中过度调整。
  • 所有以点为单位的距离都会根据 PriceStep 转换为真实价格。对于 3 位或 5 位小数的报价,1 点等于 10 个最小价位,与原始 EA 的处理一致。

参数

名称 说明
OrderVolume 每次市价单的基础手数。
CloseBySignal true 时出现反向信号会先平仓再开反向单。
BreakPointPips 触发突破所需的开盘价偏离幅度(点)。
LastBarSizeMinPips / LastBarSizeMaxPips 信号 K 线实体允许的最小与最大范围。
TrailingStopPips 移动止损距离,0 表示关闭。
TrailingStepPips 每次移动止损前需要的额外盈利。
StopLossPips 固定止损距离,0 表示不使用。
TakeProfitPips 固定止盈距离,0 表示不使用。
CandleType 用于交易逻辑的日内 K 线类型。

使用提示

  • 策略会自动订阅日内与日线数据,请确认数据源支持所需的时间框架。
  • 仅在 K 线收盘后评估信号,因此订单在信号 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>
/// Daily breakout strategy that reacts to the distance from the daily open.
/// Converted from the MetaTrader Daily BreakPoint expert advisor.
/// </summary>
public class DailyBreakPointStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<bool> _closeBySignal;
	private readonly StrategyParam<decimal> _breakPointPips;
	private readonly StrategyParam<decimal> _lastBarSizeMinPips;
	private readonly StrategyParam<decimal> _lastBarSizeMaxPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _currentDayOpen;
	private decimal? _longStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakePrice;
	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal _pipSize;

	/// <summary>
	/// Order volume.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Reverse the position when the opposite signal appears.
	/// </summary>
	public bool CloseBySignal
	{
		get => _closeBySignal.Value;
		set => _closeBySignal.Value = value;
	}

	/// <summary>
	/// Break distance from the daily open expressed in pips.
	/// </summary>
	public decimal BreakPointPips
	{
		get => _breakPointPips.Value;
		set => _breakPointPips.Value = value;
	}

	/// <summary>
	/// Minimum size of the previous bar body in pips.
	/// </summary>
	public decimal LastBarSizeMinPips
	{
		get => _lastBarSizeMinPips.Value;
		set => _lastBarSizeMinPips.Value = value;
	}

	/// <summary>
	/// Maximum size of the previous bar body in pips.
	/// </summary>
	public decimal LastBarSizeMaxPips
	{
		get => _lastBarSizeMaxPips.Value;
		set => _lastBarSizeMaxPips.Value = value;
	}

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

	/// <summary>
	/// Trailing stop step in pips.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Fixed stop loss in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Fixed take profit in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="DailyBreakPointStrategy"/> class.
	/// </summary>
	public DailyBreakPointStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Volume", "Default order volume", "General");

		_closeBySignal = Param(nameof(CloseBySignal), true)
		.SetDisplay("Close By Signal", "Reverse existing position on opposite signal", "General");

		_breakPointPips = Param(nameof(BreakPointPips), 5m)
		.SetGreaterThanZero()
		.SetDisplay("Break Point (pips)", "Distance from the daily open", "Signals");

		_lastBarSizeMinPips = Param(nameof(LastBarSizeMinPips), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Last Bar Min (pips)", "Minimum body size of the previous bar", "Signals");

		_lastBarSizeMaxPips = Param(nameof(LastBarSizeMaxPips), 5000m)
		.SetGreaterThanZero()
		.SetDisplay("Last Bar Max (pips)", "Maximum body size of the previous bar", "Signals");

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

		_trailingStepPips = Param(nameof(TrailingStepPips), 2m)
		.SetDisplay("Trailing Step (pips)", "Minimum move before trailing", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 0m)
		.SetDisplay("Stop Loss (pips)", "Fixed stop loss distance", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 30m)
		.SetDisplay("Take Profit (pips)", "Fixed take profit distance", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
		.SetDisplay("Candle Type", "Intraday candle series", "Data");
	}

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

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

		_currentDayOpen = null;
		_longStopPrice = null;
		_longTakePrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
		_longEntryPrice = null;
		_shortEntryPrice = null;
		_pipSize = 0m;
	}

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

		Volume = OrderVolume;
		_pipSize = CalculatePipSize();

		StartProtection(
			takeProfit: new Unit(2, UnitTypes.Percent),
			stopLoss: new Unit(1, UnitTypes.Percent)
		);

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

		var dailySubscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		dailySubscription.Bind(ProcessDailyCandle).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, intradaySubscription);
			DrawOwnTrades(area);
		}
	}

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

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

		return step;
	}

	private decimal NormalizePrice(decimal price)
	{
		var step = Security?.PriceStep;
		if (step is null || step.Value <= 0m)
		return price;

		var value = price / step.Value;
		var rounded = Math.Round(value, 0, MidpointRounding.AwayFromZero);
		return rounded * step.Value;
	}

	private void ProcessDailyCandle(ICandleMessage candle)
	{
		if (candle.State == CandleStates.Finished || candle.State == CandleStates.Active)
		_currentDayOpen = candle.OpenPrice;
	}

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

		Volume = OrderVolume;


		if (_pipSize <= 0m)
		_pipSize = CalculatePipSize();

		var dayOpen = _currentDayOpen;
		if (dayOpen is null)
		return;

		var breakOffset = BreakPointPips * _pipSize;
		var minBody = LastBarSizeMinPips * _pipSize;
		var maxBody = LastBarSizeMaxPips * _pipSize;
		var trailingStop = TrailingStopPips * _pipSize;
		var trailingStep = TrailingStepPips * _pipSize;
		var stopLossOffset = StopLossPips > 0m ? StopLossPips * _pipSize : 0m;
		var takeProfitOffset = TakeProfitPips > 0m ? TakeProfitPips * _pipSize : 0m;

		UpdateTrailing(candle, trailingStop, trailingStep);
		HandleRiskExits(candle);

		var bodySize = Math.Abs(candle.ClosePrice - candle.OpenPrice);
		var minPrice = Math.Min(candle.OpenPrice, candle.ClosePrice);
		var maxPrice = Math.Max(candle.OpenPrice, candle.ClosePrice);

		var breakBuy = dayOpen.Value + breakOffset;
		var breakSell = dayOpen.Value - breakOffset;

		var bullishBody = candle.ClosePrice > candle.OpenPrice;
		var bearishBody = candle.ClosePrice < candle.OpenPrice;

		var bullishSignal = bullishBody && breakOffset > 0m &&
		candle.ClosePrice - dayOpen.Value >= breakOffset &&
		bodySize >= minBody &&
		(maxBody <= 0m || bodySize <= maxBody);

		var bearishSignal = bearishBody && breakOffset > 0m &&
		dayOpen.Value - candle.ClosePrice >= breakOffset &&
		bodySize >= minBody &&
		(maxBody <= 0m || bodySize <= maxBody);

		if (bullishSignal)
		{
			ExecuteBullishSignal(candle.ClosePrice, stopLossOffset, takeProfitOffset);
		}
		else if (bearishSignal)
		{
			ExecuteBearishSignal(candle.ClosePrice, stopLossOffset, takeProfitOffset);
		}
	}

	private void UpdateTrailing(ICandleMessage candle, decimal trailingStop, decimal trailingStep)
	{
		if (trailingStop <= 0m)
		return;

		if (Position > 0 && _longEntryPrice.HasValue)
		{
			var profit = candle.ClosePrice - _longEntryPrice.Value;
			if (profit > trailingStop + trailingStep)
			{
				var threshold = candle.ClosePrice - (trailingStop + trailingStep);
				if (!_longStopPrice.HasValue || _longStopPrice.Value < threshold)
				_longStopPrice = NormalizePrice(candle.ClosePrice - trailingStop);
			}
		}
		else if (Position < 0 && _shortEntryPrice.HasValue)
		{
			var profit = _shortEntryPrice.Value - candle.ClosePrice;
			if (profit > trailingStop + trailingStep)
			{
				var threshold = candle.ClosePrice + (trailingStop + trailingStep);
				if (!_shortStopPrice.HasValue || _shortStopPrice.Value > threshold || _shortStopPrice.Value == 0m)
				_shortStopPrice = NormalizePrice(candle.ClosePrice + trailingStop);
			}
		}
	}

	private void HandleRiskExits(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var volume = Math.Abs(Position);
			if (volume > 0m && _longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value)
			{
				SellMarket();
				ResetLongState();
				return;
			}

			if (volume > 0m && _longTakePrice.HasValue && candle.HighPrice >= _longTakePrice.Value)
			{
				SellMarket();
				ResetLongState();
			}
		}
		else if (Position < 0)
		{
			var volume = Math.Abs(Position);
			if (volume > 0m && _shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value)
			{
				BuyMarket();
				ResetShortState();
				return;
			}

			if (volume > 0m && _shortTakePrice.HasValue && candle.LowPrice <= _shortTakePrice.Value)
			{
				BuyMarket();
				ResetShortState();
			}
		}
		else
		{
			ResetLongState();
			ResetShortState();
		}
	}

	private void ExecuteBullishSignal(decimal entryPrice, decimal stopLossOffset, decimal takeProfitOffset)
	{
		if (CloseBySignal)
		{
			if (Position > 0)
			{
				var volume = Math.Abs(Position);
				SellMarket();
			}

			ResetLongState();

			SellMarket();

			_shortEntryPrice = entryPrice;
			_shortStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice + stopLossOffset) : null;
			_shortTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice - takeProfitOffset) : null;
		}
		else
		{
			BuyMarket();

			_longEntryPrice = entryPrice;
			_longStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice - stopLossOffset) : null;
			_longTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice + takeProfitOffset) : null;
			ResetShortState();
		}
	}

	private void ExecuteBearishSignal(decimal entryPrice, decimal stopLossOffset, decimal takeProfitOffset)
	{
		if (CloseBySignal)
		{
			if (Position < 0)
			{
				var volume = Math.Abs(Position);
				BuyMarket();
			}

			ResetShortState();

			BuyMarket();

			_longEntryPrice = entryPrice;
			_longStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice - stopLossOffset) : null;
			_longTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice + takeProfitOffset) : null;
		}
		else
		{
			SellMarket();

			_shortEntryPrice = entryPrice;
			_shortStopPrice = stopLossOffset > 0m ? NormalizePrice(entryPrice + stopLossOffset) : null;
			_shortTakePrice = takeProfitOffset > 0m ? NormalizePrice(entryPrice - takeProfitOffset) : null;
			ResetLongState();
		}
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longStopPrice = null;
		_longTakePrice = null;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
	}
}