在 GitHub 上查看

E-News Lucky 策略

概览

E-News Lucky 策略是 MetaTrader 智能交易系统 e-News-Lucky 的 StockSharp 版本。策略实现了典型的新闻突破思路:

  • 在设定的 PlacementTime 时间点,策略会在当前价格上下按照 DistancePips 的距离分别挂出 Buy Stop 与 Sell Stop 订单。
  • 任一方向触发成交后,会立即撤销另一侧的挂单,并根据参数设置自动创建止损与止盈价位。
  • 通过 TrailingStopPipsTrailingStepPips 可以启用移动止损,在行情顺势运行时逐步锁定利润。
  • 到达 CancelTime 时,策略会撤销所有剩余挂单,并平掉所有持仓,以避免在交易窗口之外暴露风险。

策略仅使用蜡烛图数据(CandleType,默认 1 分钟)来判断时间与更新移动止损,不依赖任何指标。

参数说明

参数 说明
Volume 每个挂单的下单手数。策略会同时放置对称的 Buy Stop 与 Sell Stop。
StopLossPips 入场价与止损之间的距离(单位:点)。设置为 0 表示不使用止损。
TakeProfitPips 入场价与止盈之间的距离(单位:点)。设置为 0 表示不使用止盈。
TrailingStopPips 移动止损的基础距离(单位:点)。大于 0 时才会启动移动止损。
TrailingStepPips 每次调整移动止损所需的最小获利(单位:点),可减少震荡行情下的频繁移动。
DistancePips 挂单距离当前价格的偏移量(单位:点)。
PlacementTime 提交挂单的服务器时间。默认值:10:30。
CancelTime 撤单并平仓的服务器时间。默认值:22:30。
CandleType 用于调度与移动止损的蜡烛类型。默认值:1 分钟。

实现细节

  • 点值计算与原始 EA 保持一致:若品种报价保留 3 位或 5 位小数,则将最小价格步长乘以 10 作为点值。
  • 所有下单价格都会先按品种的最小价格变动单位进行规范化。
  • 移动止损会比较最新收盘价与 PositionPrice,只有当浮盈超过 TrailingStopPipsTrailingStepPips 时才会上移(或下移)止损。
  • 每个交易日到达 PlacementTime 会重新挂出双向突破单,到达 CancelTime 则保证所有订单与仓位被清理。

使用建议

  1. 选择流动性高、点差小的品种运行本策略,以适应新闻行情的快速波动。
  2. 根据关注的经济事件调整 PlacementTimeCancelTime,确保在新闻公布前后执行策略。
  3. 依据市场波动率调整各类点数参数:较大的距离可减少假突破,较小的距离可以更早入场但风险更高。
  4. 如需固定止损,可将 TrailingStopPips 设为 0 以关闭移动止损功能。
  5. 在重大事件期间注意观察滑点与点差变化,确保挂单能按预期成交。
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>
/// Scheduled breakout strategy that monitors price around a reference level and enters on breakout.
/// Converted from the original pending-order version to use market orders.
/// </summary>
public class ENewsLuckyStrategy : Strategy
{
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<decimal> _distancePips;
	private readonly StrategyParam<int> _placementHour;
	private readonly StrategyParam<int> _cancelHour;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _pipSize;
	private decimal? _buyLevel;
	private decimal? _sellLevel;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private bool _pendingActive;
	private bool _lastWasPlacementDay;

	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	public decimal DistancePips
	{
		get => _distancePips.Value;
		set => _distancePips.Value = value;
	}

	public int PlacementHour
	{
		get => _placementHour.Value;
		set => _placementHour.Value = value;
	}

	public int CancelHour
	{
		get => _cancelHour.Value;
		set => _cancelHour.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public ENewsLuckyStrategy()
	{
		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetDisplay("Stop Loss", "Stop loss in pips", "Trading");

		_takeProfitPips = Param(nameof(TakeProfitPips), 150m)
			.SetDisplay("Take Profit", "Take profit in pips", "Trading");

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

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetDisplay("Trailing Step", "Minimum trailing step in pips", "Risk");

		_distancePips = Param(nameof(DistancePips), 20m)
			.SetGreaterThanZero()
			.SetDisplay("Entry Distance", "Distance from market in pips", "Trading");

		_placementHour = Param(nameof(PlacementHour), 2)
			.SetDisplay("Placement Hour", "Hour to set breakout levels", "General");

		_cancelHour = Param(nameof(CancelHour), 22)
			.SetDisplay("Cancel Hour", "Hour to cancel and close", "General");

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

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_pipSize = 0m;
		_buyLevel = null;
		_sellLevel = null;
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
		_pendingActive = false;
		_lastWasPlacementDay = false;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var step = Security?.PriceStep ?? 0m;
		_pipSize = step > 0 ? step : 1m;

		SubscribeCandles(CandleType)
			.Bind(ProcessCandle)
			.Start();
	}

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

		var hour = candle.CloseTime.Hour;
		var price = candle.ClosePrice;

		// Set breakout levels at placement hour
		if (hour == PlacementHour && !_lastWasPlacementDay && Position == 0)
		{
			var distance = DistancePips * _pipSize;
			_buyLevel = price + distance;
			_sellLevel = price - distance;
			_pendingActive = true;
			_lastWasPlacementDay = true;
		}

		if (hour != PlacementHour)
			_lastWasPlacementDay = false;

		// Cancel at cancel hour
		if (hour == CancelHour && _pendingActive)
		{
			_pendingActive = false;
			_buyLevel = null;
			_sellLevel = null;

			if (Position > 0)
				SellMarket(Position);
			else if (Position < 0)
				BuyMarket(-Position);

			_entryPrice = 0m;
			_stopPrice = null;
			_takePrice = null;
			return;
		}

		// Check breakout triggers
		if (_pendingActive && Position == 0)
		{
			if (_buyLevel.HasValue && candle.HighPrice >= _buyLevel.Value)
			{
				var buyLevel = _buyLevel.Value;
				BuyMarket(Volume);
				_entryPrice = buyLevel;
				_stopPrice = StopLossPips > 0 ? _entryPrice - StopLossPips * _pipSize : null;
				_takePrice = TakeProfitPips > 0 ? _entryPrice + TakeProfitPips * _pipSize : null;
				_pendingActive = false;
				_buyLevel = null;
				_sellLevel = null;
			}
			else if (_sellLevel.HasValue && candle.LowPrice <= _sellLevel.Value)
			{
				var sellLevel = _sellLevel.Value;
				SellMarket(Volume);
				_entryPrice = sellLevel;
				_stopPrice = StopLossPips > 0 ? _entryPrice + StopLossPips * _pipSize : null;
				_takePrice = TakeProfitPips > 0 ? _entryPrice - TakeProfitPips * _pipSize : null;
				_pendingActive = false;
				_buyLevel = null;
				_sellLevel = null;
			}
		}

		// Manage open position
		if (Position > 0)
		{
			// Trailing stop
			if (TrailingStopPips > 0 && _entryPrice > 0)
			{
				var trailDist = TrailingStopPips * _pipSize;
				var stepDist = TrailingStepPips * _pipSize;
				if (price - _entryPrice > trailDist + stepDist)
				{
					var newStop = price - trailDist;
					if (!_stopPrice.HasValue || newStop > _stopPrice.Value)
						_stopPrice = newStop;
				}
			}

			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Position);
				ResetPosition();
				return;
			}

			if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				SellMarket(Position);
				ResetPosition();
			}
		}
		else if (Position < 0)
		{
			// Trailing stop
			if (TrailingStopPips > 0 && _entryPrice > 0)
			{
				var trailDist = TrailingStopPips * _pipSize;
				var stepDist = TrailingStepPips * _pipSize;
				if (_entryPrice - price > trailDist + stepDist)
				{
					var newStop = price + trailDist;
					if (!_stopPrice.HasValue || newStop < _stopPrice.Value)
						_stopPrice = newStop;
				}
			}

			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(-Position);
				ResetPosition();
				return;
			}

			if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				BuyMarket(-Position);
				ResetPosition();
			}
		}
	}

	private void ResetPosition()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
	}
}