在 GitHub 上查看

Second Easiest 策略

概述

Second Easiest 策略是 MetaTrader 专家顾问 Second_Easiest.mq4 的 StockSharp 移植版本。原始 EA 在整天内监控当日 日线的开盘价、最高价和最低价,只要价格证明自己正在远离当日开盘价,就会开出一笔日内仓位;当市场收盘时 它立即平仓,等待下一次交易时段。该移植版本保持了相同的日内突破思想,但使用 StockSharp 的高级 API 来执 行蜡烛订阅、下单和持仓管理。

由于策略只依赖当日的开盘价、最高价和最低价,因此非常轻量,却能够快速捕捉市场方向性。程序同时只允许保 持一个方向的仓位,不会立即反手;只有在上一笔交易彻底平仓后才会考虑新的入场信号。

交易逻辑

  1. 订阅由 CandleType 参数定义的日内蜡烛序列。默认的一分钟周期可以快速感知当日极值,同时与原始 EA 的日 线逻辑保持兼容。
  2. 对于每一根收盘的蜡烛,刷新内存中记录的当日开盘价、最高价和最低价。新交易日处理的第一根蜡烛会设定三 个参考值,其后只有在出现新的极值时才会更新高点或低点。
  3. 当时间达到 EntryCutoffHour 时忽略新的建仓信号。原始 MQL 代码在服务器时间 16:00 后停止开仓,移植版本 完全复现这一限制。
  4. 只有在当前收盘价高于当日开盘价且开盘价与最低价之间的距离超过 RangePointsThreshold 时才允许做多。该 条件与 MQL 中的 "Bid > open" 与 "open - low > 15 points" 完全一致。
  5. 只有在当前收盘价低于当日开盘价且最高价与开盘价之间的距离超过同样的阈值时才允许做空。
  6. 当出现信号且没有持仓时,按照 TradeVolume 指定的手数发送市价单。Strategy 基类的帮助方法负责选择正确 的方向。
  7. 当时间达到 MarketCloseHour 时调用 ClosePosition() 平掉所有持仓。在该时刻之后到下一次交易日开始之前不会 再开出新的仓位。

参数

名称 类型 默认值 说明
CandleType DataType 1 分钟周期 推动入场与出场逻辑的主蜡烛序列。
TradeVolume decimal 1 每笔市价单使用的手数。
EntryCutoffHour int 16 超过该小时 (0-23) 后忽略新的入场信号。
MarketCloseHour int 20 达到该小时 (0-23) 时强制平仓。
RangePointsThreshold decimal 15 以经纪商点值表示的最小距离,用于衡量当日开盘价与最近极值之间的差距。

与 MetaTrader 版本的差异

  • StockSharp 采用净仓制度管理持仓,但因为策略在任意时刻只持有一笔交易,所以行为与原始的单票逻辑完全一致。
  • MetaTrader 通过日线的 iOpen/iHigh/iLow 获取实时的开高低价。移植版本改为使用日内蜡烛重建相同信息,从而避 免调用被禁止的指标方法,并在经纪商不提供日线数据时仍能正常运行。
  • 平仓时调用 ClosePosition(),不再遍历订单号。效果相同:在达到设定的收盘时间后立即退出所有持仓。
  • 如果证券没有提供 PriceStep,则把 RangePointsThreshold 当作绝对价格距离进行比较,以确保在缺乏最小跳动 单位元数据的情况下策略仍可运行。

使用建议

  • OnStarted 中将 Volume 设置为 TradeVolume,因此调整参数后后续订单会立即采用新的手数。
  • 若需要改变 CandleType,请确保新的周期仍能足够精细地跟踪当日开盘价与极值。例如五分钟蜡烛效果良好,而一 小时蜡烛可能会延迟识别日内极值。
  • 提高 RangePointsThreshold 可以过滤掉波动极小的交易日;降低该值则能更早触发交易信号。
  • 策略在每天结束时全部平仓,因此不会占用隔夜保证金;对于存在交易中断的市场,重新开市时日内统计会自动从 新开始。
using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// SecondEasiest: RSI reversal with EMA filter and ATR stops.
/// </summary>
public class SecondEasiestStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rsiLength;
	private readonly StrategyParam<int> _emaLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _prevRsi;
	private decimal _entryPrice;

	public SecondEasiestStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");
		_rsiLength = Param(nameof(RsiLength), 14)
			.SetDisplay("RSI Length", "RSI period.", "Indicators");
		_emaLength = Param(nameof(EmaLength), 25)
			.SetDisplay("EMA Length", "Trend filter.", "Indicators");
		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");
	}

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int RsiLength { get => _rsiLength.Value; set => _rsiLength.Value = value; }
	public int EmaLength { get => _emaLength.Value; set => _emaLength.Value = value; }
	public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }

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

		_prevRsi = 0; _entryPrice = 0;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_prevRsi = 0; _entryPrice = 0;
		var rsi = new RelativeStrengthIndex { Length = RsiLength };
		var ema = new ExponentialMovingAverage { Length = EmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(rsi, ema, atr, ProcessCandle).Start();
		var area = CreateChartArea();
		if (area != null) { DrawCandles(area, subscription); DrawIndicator(area, ema); DrawOwnTrades(area); }
	}

	private void ProcessCandle(ICandleMessage candle, decimal rsiVal, decimal emaVal, decimal atrVal)
	{
		if (candle.State != CandleStates.Finished) return;
		if (_prevRsi == 0 || atrVal <= 0) { _prevRsi = rsiVal; return; }
		var close = candle.ClosePrice;

		if (Position > 0)
		{
			if (close >= _entryPrice + atrVal * 2.5m || close <= _entryPrice - atrVal * 1.5m || rsiVal > 70) { SellMarket(); _entryPrice = 0; }
		}
		else if (Position < 0)
		{
			if (close <= _entryPrice - atrVal * 2.5m || close >= _entryPrice + atrVal * 1.5m || rsiVal < 30) { BuyMarket(); _entryPrice = 0; }
		}

		if (Position == 0)
		{
			if (rsiVal > 55 && _prevRsi <= 55 && close > emaVal) { _entryPrice = close; BuyMarket(); }
			else if (rsiVal < 45 && _prevRsi >= 45 && close < emaVal) { _entryPrice = close; SellMarket(); }
		}
		_prevRsi = rsiVal;
	}
}