在 GitHub 上查看

Get Trend 策略

概述

该策略是 MetaTrader 顾问 Get trend 的 StockSharp 版本。核心思想是在 15 分钟图上寻找顺势回调入场,并通过 1 小时图的趋势确认过滤逆势交易。策略同时运用平滑移动平均线与随机指标来确定入场时机。

交易逻辑

  • 主图周期: 15 分钟蜡烛负责生成信号并提交订单。
  • 确认周期: 1 小时蜡烛提供平滑移动平均线与收盘价,用于判断更高周期的趋势方向。
  • 趋势过滤: M15 与 H1 的收盘价必须位于各自平滑均线的同一侧;同时 M15 收盘价与均线的距离需小于允许阈值,确保是在回调区域进场。
  • 动量触发: 多头信号要求随机指标 %K 线在超卖区(<20)向上穿越 %D 线;空头信号则要求在超买区(>80)向下穿越。
  • 仓位管理: 入场后设置固定点数的止损与止盈,且可选的移动止损在盈利达到指定距离时逐步跟随价格。

入场条件

做多

  1. M15 收盘价低于其平滑均线。
  2. H1 收盘价低于其平滑均线。
  3. M15 收盘价与均线的差值不超过 Price Threshold(点数)。
  4. 随机指标 %K 与 %D 均小于 20。
  5. 上一根 %K 低于 %D,当前 %K 上穿 %D。
  6. 当前没有多头仓位(若持有空头则先平仓再反向开多)。

做空

  1. M15 收盘价高于其平滑均线。
  2. H1 收盘价高于其平滑均线。
  3. M15 收盘价与均线的差值不超过 Price Threshold
  4. 随机指标 %K 与 %D 均大于 80。
  5. 上一根 %K 高于 %D,当前 %K 下穿 %D。
  6. 当前没有空头仓位(若持有多头则先平仓再反向开空)。

离场规则

  • 止损: 按点数设定的固定距离,基于入场价格计算。
  • 止盈: 按点数设定的固定距离,基于入场价格计算。
  • 移动止损: 当浮盈超过设定距离时启动,随后按设定偏移紧跟价格移动。

参数

参数 说明 默认值
M15CandleType 主图信号使用的蜡烛类型。 15 分钟
H1CandleType 趋势确认使用的蜡烛类型。 1 小时
MaM15Length M15 平滑移动平均周期。 99
MaH1Length H1 平滑移动平均周期。 184
StochasticLength 随机指标 %K 周期。 27
StochasticSignalLength 随机指标 %D 平滑周期。 3
ThresholdPoints 允许价格偏离均线的最大点数。 10
TakeProfitPoints 止盈距离(点数)。 540
StopLossPoints 止损距离(点数)。 90
TrailingStopPoints 移动止损距离(点数)。 20
TradeVolume 开仓的基础手数/数量。 0.1

所有以点数表示的参数都会乘以品种的 PriceStep,转换为绝对价格差值。

实现细节

  • 使用 StockSharp 高层 API,通过 BindEx 将蜡烛订阅与指标连接,避免手动复制指标缓冲区。
  • 移动止损逻辑与原始 EA 保持一致:当盈利超出阈值后不断将止损向有利方向推进。
  • 反向开仓前先取消未完成订单,防止出现相反方向的挂单冲突。
  • 图表展示 M15 蜡烛与平滑均线,并提供单独的随机指标面板,方便回测和监控。

使用建议

  • 根据数据源调整蜡烛类型(若数据源支持其它周期或成交量蜡烛,可替换为对应的 DataType)。
  • 不同市场的波动和最小跳动价差不同,请根据实际情况调节阈值、止损和止盈。
  • 策略更适合趋势明显且存在均线回踩机会的交易品种。
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>
/// Trend-following strategy converted from the MetaTrader "Get trend" expert.
/// </summary>
public class GetTrendStrategy : Strategy
{
	private readonly StrategyParam<DataType> _m15CandleType;
	private readonly StrategyParam<DataType> _h1CandleType;
	private readonly StrategyParam<int> _maM15Length;
	private readonly StrategyParam<int> _maH1Length;
	private readonly StrategyParam<int> _stochasticLength;
	private readonly StrategyParam<int> _stochasticSignalLength;
	private readonly StrategyParam<decimal> _thresholdPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _tradeVolume;

	private SmoothedMovingAverage _maM15;
	private SmoothedMovingAverage _maH1;
	private StochasticOscillator _stochastic;

	private decimal? _maH1Value;
	private decimal? _lastH1Close;

	private decimal? _prevStochFast;
	private decimal? _prevStochSlow;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;

	/// <summary>
	/// Initializes a new instance of the <see cref="GetTrendStrategy"/> class.
	/// </summary>
	public GetTrendStrategy()
	{
		_m15CandleType = Param(nameof(M15CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("M15 Candle", "Primary timeframe", "General");

		_h1CandleType = Param(nameof(H1CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("H1 Candle", "Higher timeframe", "General");

		_maM15Length = Param(nameof(MaM15Length), 99)
			.SetGreaterThanZero()
			.SetDisplay("M15 MA Length", "Smoothed MA length on M15", "Indicators")
			;

		_maH1Length = Param(nameof(MaH1Length), 184)
			.SetGreaterThanZero()
			.SetDisplay("H1 MA Length", "Smoothed MA length on H1", "Indicators")
			;

		_stochasticLength = Param(nameof(StochasticLength), 27)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %K", "%K period", "Indicators")
			;

		_stochasticSignalLength = Param(nameof(StochasticSignalLength), 3)
			.SetGreaterThanZero()
			.SetDisplay("Stochastic %D", "%D smoothing period", "Indicators");

		_thresholdPoints = Param(nameof(ThresholdPoints), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Price Threshold", "Maximum distance from MA", "Filters")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 540m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit", "Take profit distance", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 90m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss", "Stop loss distance", "Risk")
			;

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 20m)
			.SetDisplay("Trailing Stop", "Trailing stop distance", "Risk")
			;

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Order volume", "Risk");
	}

	/// <summary>
	/// Primary trading timeframe (M15 by default).
	/// </summary>
	public DataType M15CandleType
	{
		get => _m15CandleType.Value;
		set => _m15CandleType.Value = value;
	}

	/// <summary>
	/// Confirmation timeframe (H1 by default).
	/// </summary>
	public DataType H1CandleType
	{
		get => _h1CandleType.Value;
		set => _h1CandleType.Value = value;
	}

	/// <summary>
	/// Smoothed moving average length on the 15-minute chart.
	/// </summary>
	public int MaM15Length
	{
		get => _maM15Length.Value;
		set => _maM15Length.Value = value;
	}

	/// <summary>
	/// Smoothed moving average length on the hourly chart.
	/// </summary>
	public int MaH1Length
	{
		get => _maH1Length.Value;
		set => _maH1Length.Value = value;
	}

	/// <summary>
	/// %K period of the stochastic oscillator.
	/// </summary>
	public int StochasticLength
	{
		get => _stochasticLength.Value;
		set => _stochasticLength.Value = value;
	}

	/// <summary>
	/// %D period of the stochastic oscillator.
	/// </summary>
	public int StochasticSignalLength
	{
		get => _stochasticSignalLength.Value;
		set => _stochasticSignalLength.Value = value;
	}

	/// <summary>
	/// Maximum allowed distance between price and the M15 moving average in points.
	/// </summary>
	public decimal ThresholdPoints
	{
		get => _thresholdPoints.Value;
		set => _thresholdPoints.Value = value;
	}

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

	/// <summary>
	/// Stop-loss distance expressed in points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in points.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Default order volume.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, M15CandleType), (Security, H1CandleType)];

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

		_maH1Value = null;
		_lastH1Close = null;
		_prevStochFast = null;
		_prevStochSlow = null;
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
	}

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

		Volume = TradeVolume;

		_maM15 = new SmoothedMovingAverage { Length = MaM15Length };
		_maH1 = new SmoothedMovingAverage { Length = MaH1Length };
		_stochastic = new StochasticOscillator();
		_stochastic.K.Length = StochasticLength;
		_stochastic.D.Length = StochasticSignalLength;

		// Subscribe to 15-minute candles and bind the required indicators.
		var m15Subscription = SubscribeCandles(M15CandleType);
		m15Subscription
			.BindEx(_maM15, _stochastic, ProcessM15Candle)
			.Start();

		// Subscribe to hourly candles for trend confirmation.
		var h1Subscription = SubscribeCandles(H1CandleType);
		h1Subscription
			.Bind(_maH1, ProcessH1Candle)
			.Start();

		var priceArea = CreateChartArea();
		if (priceArea != null)
		{
			DrawCandles(priceArea, m15Subscription);
			DrawIndicator(priceArea, _maM15);
			DrawOwnTrades(priceArea);

			var oscillatorArea = CreateChartArea();
			if (oscillatorArea != null)
			{
				oscillatorArea.Title = "Stochastic";
				DrawIndicator(oscillatorArea, _stochastic);
			}
		}
	}

	private void ProcessH1Candle(ICandleMessage candle, decimal maValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Store the latest H1 moving average and close for trend checks.
		_maH1Value = maValue;
		_lastH1Close = candle.ClosePrice;
	}

	private void ProcessM15Candle(ICandleMessage candle, IIndicatorValue maValue, IIndicatorValue stochasticValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!maValue.IsFinal || !stochasticValue.IsFinal)
			return;

		var ma = maValue.ToDecimal();
		var stochTyped = (StochasticOscillatorValue)stochasticValue;

		if (stochTyped.K is not decimal stochFast || stochTyped.D is not decimal stochSlow)
			return;

		// Manage protective levels before looking for new entries.
		ManageOpenPosition(candle);

		if (_maH1Value is not decimal maH1 || _lastH1Close is not decimal priceH1)
			goto UpdateStochastic;

		var priceM15 = candle.ClosePrice;
		var priceStep = Security?.PriceStep ?? 1m;
		var threshold = ThresholdPoints * priceStep;

		var nearLowerBand = priceM15 < ma && priceH1 < maH1 && ma - priceM15 <= threshold;
		var nearUpperBand = priceM15 > ma && priceH1 > maH1 && priceM15 - ma <= threshold;

		var crossUp = _prevStochFast is decimal prevFastUp && _prevStochSlow is decimal prevSlowUp && prevFastUp < prevSlowUp && stochFast > stochSlow;
		var crossDown = _prevStochFast is decimal prevFastDown && _prevStochSlow is decimal prevSlowDown && prevFastDown > prevSlowDown && stochFast < stochSlow;

		if (nearLowerBand && stochSlow < 20m && stochFast < 20m && crossUp && Position <= 0)
		{
			EnterLong(candle.ClosePrice, priceStep);
		}
		else if (nearUpperBand && stochSlow > 80m && stochFast > 80m && crossDown && Position >= 0)
		{
			EnterShort(candle.ClosePrice, priceStep);
		}

	UpdateStochastic:
		_prevStochFast = stochFast;
		_prevStochSlow = stochSlow;
	}

	private void EnterLong(decimal entryPrice, decimal priceStep)
	{
		// Cancel opposite orders and flip the position if needed.
		CancelActiveOrders();

		BuyMarket();

		_entryPrice = entryPrice;
		_takePrice = entryPrice + TakeProfitPoints * priceStep;
		_stopPrice = entryPrice - StopLossPoints * priceStep;
	}

	private void EnterShort(decimal entryPrice, decimal priceStep)
	{
		// Cancel opposite orders and flip the position if needed.
		CancelActiveOrders();

		SellMarket();

		_entryPrice = entryPrice;
		_takePrice = entryPrice - TakeProfitPoints * priceStep;
		_stopPrice = entryPrice + StopLossPoints * priceStep;
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		if (Position == 0)
			return;

		var priceStep = Security?.PriceStep ?? 1m;
		var trailingDistance = TrailingStopPoints * priceStep;

		if (Position > 0)
		{
			if (_entryPrice is decimal entry && TrailingStopPoints > 0 && candle.ClosePrice - entry >= trailingDistance)
			{
				var candidate = candle.ClosePrice - trailingDistance;
				_stopPrice = _stopPrice.HasValue ? Math.Max(_stopPrice.Value, candidate) : candidate;
			}

			if (_takePrice is decimal take && candle.HighPrice >= take)
			{
				SellMarket();
				ResetProtection();
				return;
			}

			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket();
				ResetProtection();
			}
		}
		else if (Position < 0)
		{
			if (_entryPrice is decimal entry && TrailingStopPoints > 0 && entry - candle.ClosePrice >= trailingDistance)
			{
				var candidate = candle.ClosePrice + trailingDistance;
				_stopPrice = _stopPrice.HasValue ? Math.Min(_stopPrice.Value, candidate) : candidate;
			}

			if (_takePrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket();
				ResetProtection();
				return;
			}

			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket();
				ResetProtection();
			}
		}
	}

	private void ResetProtection()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
	}
}