在 GitHub 上查看

晨星暮星配合随机指标策略

本策略将 MetaTrader 5 专家顾问 Expert_AMS_ES_Stoch(晨星/暮星形态结合随机指标确认)移植到 StockSharp。代码沿用了原始的蜡烛形态识别与随机指标过滤规则,并利用高级的蜡烛订阅 API,确保所有决策都基于已经收盘的K线。

策略逻辑

  • 指标
    • 标准随机振荡指标,%K%D 以及平滑参数均可配置。
    • 对蜡烛实体长度(|open-close|)计算的简单移动平均,用于区分长实体与小实体,复现 MQL 中的 AvgBody() 功能。
  • 做多条件
    • 最近三根完成K线形成晨星形态:
      1. 两根之前为长阴线,实体长度大于平均值。
      2. 前一根为小实体蜡烛,开盘价与收盘价都低于第一根。
      3. 当前蜡烛收盘价高于第一根实体的中点并收阳。
    • 随机指标信号线 %D 低于超卖阈值(默认 30)。
    • 在开多前会平掉现有空头仓位。
  • 做空条件
    • 最近三根K线形成暮星形态(与上面镜像)。
    • %D 高于超买阈值(默认 70)。
    • 在开空前会先平掉已有多头仓位。
  • 离场条件
    • %D 上穿恢复阈值 20 或极值 80 时平掉空单。
    • %D 下穿 8020 时平掉多单。
    • 上述规则对应 MQL 信号模块中的平仓逻辑。

参数

名称 说明
CandleType 用于分析的K线类型或时间框架。
StochasticKPeriod, StochasticDPeriod, StochasticSlowing 随机指标 %K%D 与平滑周期。
StochasticOverbought, StochasticOversold 确认晨星/暮星入场时使用的 %D 阈值。
PatternAveragePeriod 计算蜡烛实体平均长度所用的K线数量。
ShortExitLevel, LongExitLevel %D 穿越这些水平时触发空头/多头平仓。

实现说明

  • 通过 SubscribeCandles().BindEx(...) 订阅蜡烛,指标准确接收已完成的K线数据,代码中不会调用 GetValue()
  • 采用 SimpleMovingAverage 对实体长度求平均值,等价于原始 MQL 库中的体型计算。
  • 晨星与暮星的判定封装在独立方法中,便于阅读,同时保持与 CCandlePattern 的规则一致。
  • 策略在方向反转前会先平掉当前持仓,以模拟原专家顾问一次只持有单方向仓位的行为。

与 MQL5 版本的差异

  • 未移植 MetaTrader 框架中的资金管理、追踪止损或固定手数设置;在 StockSharp 中通过策略的 Volume 属性控制下单手数。
  • 随机指标使用 StockSharp 的实现,如需对齐原平台,可自行调整阈值。
  • 日志(英文)详细记录每次开仓和平仓,方便回测与调试。
namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// Morning/Evening Star pattern strategy with Stochastic confirmation.
/// Buys on morning star + oversold stochastic, sells on evening star + overbought stochastic.
/// </summary>
public class MorningEveningStochasticStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _stochPeriod;
	private readonly StrategyParam<decimal> _oversold;
	private readonly StrategyParam<decimal> _overbought;
	private readonly StrategyParam<int> _signalCooldownCandles;

	private readonly List<ICandleMessage> _candles = new();
	private decimal _prevK;
	private bool _hasPrevK;
	private int _candlesSinceTrade;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int StochPeriod { get => _stochPeriod.Value; set => _stochPeriod.Value = value; }
	public decimal Oversold { get => _oversold.Value; set => _oversold.Value = value; }
	public decimal Overbought { get => _overbought.Value; set => _overbought.Value = value; }
	public int SignalCooldownCandles { get => _signalCooldownCandles.Value; set => _signalCooldownCandles.Value = value; }

	public MorningEveningStochasticStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
		_stochPeriod = Param(nameof(StochPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("Stoch Period", "Stochastic K period", "Indicators");
		_oversold = Param(nameof(Oversold), 30m)
			.SetDisplay("Oversold", "Stochastic oversold level", "Signals");
		_overbought = Param(nameof(Overbought), 70m)
			.SetDisplay("Overbought", "Stochastic overbought level", "Signals");
		_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 6)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait between trades", "Trading");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_candles.Clear();
		_prevK = 0m;
		_hasPrevK = false;
		_candlesSinceTrade = SignalCooldownCandles;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_candles.Clear();
		_hasPrevK = false;
		_candlesSinceTrade = SignalCooldownCandles;
		var stoch = new StochasticOscillator { K = { Length = StochPeriod }, D = { Length = 3 } };
		var subscription = SubscribeCandles(CandleType);
		subscription.BindEx(stoch, ProcessCandle).Start();
	}

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

		if (_candlesSinceTrade < SignalCooldownCandles)
			_candlesSinceTrade++;

		var stochTyped = stochValue as StochasticOscillatorValue;
		if (stochTyped?.K is not decimal kValue) return;

		_candles.Add(candle);
		if (_candles.Count > 5)
			_candles.RemoveAt(0);

		if (_candles.Count >= 3)
		{
			var c3 = _candles[^1]; // current
			var c2 = _candles[^2]; // middle (star)
			var c1 = _candles[^3]; // first

			var body1 = Math.Abs(c1.ClosePrice - c1.OpenPrice);
			var body2 = Math.Abs(c2.ClosePrice - c2.OpenPrice);
			var body3 = Math.Abs(c3.ClosePrice - c3.OpenPrice);

			// Morning Star: bearish + small body + bullish, close above midpoint of first
			var isMorningStar = c1.OpenPrice > c1.ClosePrice  // first bearish
				&& body2 < body1 * 0.5m                       // small middle body
				&& c3.ClosePrice > c3.OpenPrice                // third bullish
				&& c3.ClosePrice > (c1.OpenPrice + c1.ClosePrice) / 2m;

			// Evening Star: bullish + small body + bearish, close below midpoint of first
			var isEveningStar = c1.ClosePrice > c1.OpenPrice   // first bullish
				&& body2 < body1 * 0.5m                        // small middle body
				&& c3.OpenPrice > c3.ClosePrice                // third bearish
				&& c3.ClosePrice < (c1.OpenPrice + c1.ClosePrice) / 2m;

			if (isMorningStar && kValue < Oversold && Position <= 0 && _candlesSinceTrade >= SignalCooldownCandles)
			{
				BuyMarket();
				_candlesSinceTrade = 0;
			}
			else if (isEveningStar && kValue > Overbought && Position >= 0 && _candlesSinceTrade >= SignalCooldownCandles)
			{
				SellMarket();
				_candlesSinceTrade = 0;
			}
		}

		// Exit on stochastic cross
		if (_hasPrevK)
		{
			if (Position > 0 && _prevK >= Overbought && kValue < Overbought && _candlesSinceTrade >= SignalCooldownCandles)
			{
				SellMarket();
				_candlesSinceTrade = 0;
			}
			else if (Position < 0 && _prevK <= Oversold && kValue > Oversold && _candlesSinceTrade >= SignalCooldownCandles)
			{
				BuyMarket();
				_candlesSinceTrade = 0;
			}
		}

		_prevK = kValue;
		_hasPrevK = true;
	}
}