在 GitHub 上查看

Smart Trend Follower 策略

概述

Smart Trend Follower 策略 是 MetaTrader 5 智能交易系统 Smart Trend Follower 的 StockSharp 版本。原始 EA 在反向移动平均线交叉和顺势的随机指标确认之间切换,并使用倍量网格扩大持仓。本移植版完全使用 StockSharp 高级 API(K 线订阅、指标绑定、市场委托)实现相同的交易流程,同时保留分批加仓和统一止盈/止损 管理。

信号逻辑

通过 SignalMode 参数可以选择两种信号模式:

  1. CrossMa – 保留原策略的“逆势交叉”逻辑。当快速 SMA 从上向下穿越慢速 SMA(当前 fast < slow,前一根 fast > slow)时建立或加仓多头;当快速 SMA 从下向上穿越慢速 SMA(当前 fast > slow,前一根 fast < slow) 时建立或加仓空头。
  2. Trend – 对应原策略的顺势模式。仅当 fast > slow、当前 K 线收阳且随机指标 %K ≤ 30 时触发多头;当 fast < slow、当前 K 线收阴且 %K ≥ 70 时触发空头。

所有条件仅在已完成的 K 线上评估。如果出现新信号而方向相反的持仓仍存在,策略会先用市价单平掉反向仓, 然后再根据新信号处理建仓与加仓,确保始终与当前信号方向保持一致。

网格加仓

策略按以下规则复制原 EA 的马丁加仓方式:

  • 首单使用 InitialVolume 指定的手数。
  • 之后每次加仓的手数均乘以 Multiplier(当 ≤ 1 时视为关闭倍量)。
  • 仅当价格相对当前方向的最佳入场价(多头取最低成交价,空头取最高成交价)偏移至少 LayerDistancePips 点时,才允许追加同向订单。
  • 下单量根据交易品种的 VolumeStepVolumeMinVolumeMax 自动归一化。

风险控制

策略为每个方向分别维护加权平均价,并据此设置统一止盈/止损:

  • TakeProfitPips 指定从平均价到篮子止盈价的距离。多头在 K 线最高价触及该水平时全部平仓,空头在最低价 触及时平仓;设为 0 可关闭止盈。
  • StopLossPips 以同样方式设置保护性止损。多头在最低价跌破止损时平仓,空头在最高价突破止损时平仓;设为 0 可关闭硬止损。

平仓通过下一根完成的 K 线确认达到价位后,以市场委托执行。_longExitRequested_shortExitRequested 标志避免在成交回报到达前重复发送平仓指令。

参数

参数 类型 默认值 说明
SignalMode 枚举 (CrossMa, Trend) CrossMa 选择使用逆势交叉或顺势+随机指标逻辑。
CandleType DataType 30 分钟 指标与信号使用的主时间框。
InitialVolume decimal 0.01 首次建仓的手数。
Multiplier decimal 2 每次加仓的手数乘数。
LayerDistancePips decimal 200 同向再次加仓所需的最小点差。
FastPeriod int 14 快速 SMA 周期。
SlowPeriod int 28 慢速 SMA 周期,必须大于 FastPeriod
StochasticKPeriod int 10 随机指标 %K 的基础周期。
StochasticDPeriod int 3 %D 平滑周期。
StochasticSlowing int 3 %K 额外平滑周期。
TakeProfitPips decimal 500 从均价到止盈位的点差,0 表示关闭。
StopLossPips decimal 0 从均价到止损位的点差,0 表示关闭。

实现细节

  • 点值根据品种的 PriceStepDecimals 推算,匹配 MetaTrader 中的 point 定义(例如五位报价为 0.0001)。
  • 使用两个 PositionEntry 列表保存多头与空头篮子的逐笔成交,并在反向成交时按先进先出方式扣减。
  • 指标全部通过 SubscribeCandles().BindEx(...) 绑定,无需手动调用 GetValue,也不会把指标直接加入 Strategy.Indicators
  • 启动时调用 StartProtection(),以便使用 StockSharp 的风险保护模块(保本、风控等)。
  • 为保持逻辑确定性并贴近原始 EA,当存在反向仓时会先行平仓,再处理新的同向信号。

文件

  • CS/SmartTrendFollowerStrategy.cs – 使用 StockSharp 高级 API 编写的 C# 策略实现。
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>
/// Port of the "Smart Trend Follower" MetaTrader 5 expert advisor that combines moving average signals
/// with stochastic confirmation and a martingale-style layering engine.
/// </summary>
public class SmartTrendFollowerStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<SignalModes> _signalMode;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<decimal> _multiplier;
	private readonly StrategyParam<decimal> _layerDistancePips;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stochasticKPeriod;
	private readonly StrategyParam<int> _stochasticDPeriod;
	private readonly StrategyParam<int> _stochasticSlowing;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;

	private SimpleMovingAverage _fastSma;
	private SimpleMovingAverage _slowSma;
	private StochasticOscillator _stochastic;

	private readonly List<PositionEntry> _longEntries = new();
	private readonly List<PositionEntry> _shortEntries = new();

	private decimal? _prevFast;
	private decimal? _prevSlow;
	private decimal _pipSize;
	private bool _longExitRequested;
	private bool _shortExitRequested;

	/// <summary>
	/// Trading signal mode.
	/// </summary>
	public SignalModes SignalMode
	{
		get => _signalMode.Value;
		set => _signalMode.Value = value;
	}

	/// <summary>
	/// Base candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initial order volume expressed in lots.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the volume of every additional averaging order.
	/// </summary>
	public decimal Multiplier
	{
		get => _multiplier.Value;
		set => _multiplier.Value = value;
	}

	/// <summary>
	/// Distance in pips required before stacking another order in the same direction.
	/// </summary>
	public decimal LayerDistancePips
	{
		get => _layerDistancePips.Value;
		set => _layerDistancePips.Value = value;
	}

	/// <summary>
	/// Fast simple moving average period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow simple moving average period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Stochastic oscillator %K length.
	/// </summary>
	public int StochasticKPeriod
	{
		get => _stochasticKPeriod.Value;
		set => _stochasticKPeriod.Value = value;
	}

	/// <summary>
	/// Stochastic oscillator %D smoothing length.
	/// </summary>
	public int StochasticDPeriod
	{
		get => _stochasticDPeriod.Value;
		set => _stochasticDPeriod.Value = value;
	}

	/// <summary>
	/// Additional smoothing applied to the %K line.
	/// </summary>
	public int StochasticSlowing
	{
		get => _stochasticSlowing.Value;
		set => _stochasticSlowing.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips relative to the average entry price.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips relative to the average entry price.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="SmartTrendFollowerStrategy"/>.
	/// </summary>
	public SmartTrendFollowerStrategy()
	{
		_signalMode = Param(nameof(SignalMode), SignalModes.CrossMa)
		.SetDisplay("Signal Mode", "Trading logic selection", "Signals");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
		.SetDisplay("Candle Type", "Primary timeframe", "General");

		_initialVolume = Param(nameof(InitialVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Initial Volume", "Starting order volume in lots", "Money Management");

		_multiplier = Param(nameof(Multiplier), 2m)
		.SetNotNegative()
		.SetDisplay("Volume Multiplier", "Martingale multiplier applied to additional entries", "Money Management");

		_layerDistancePips = Param(nameof(LayerDistancePips), 200m)
		.SetNotNegative()
		.SetDisplay("Layer Distance", "Pip distance before adding another order", "Money Management");

		_fastPeriod = Param(nameof(FastPeriod), 14)
		.SetGreaterThanZero()
		.SetDisplay("Fast MA", "Fast moving average period", "Indicators")
		;

		_slowPeriod = Param(nameof(SlowPeriod), 28)
		.SetGreaterThanZero()
		.SetDisplay("Slow MA", "Slow moving average period", "Indicators")
		;

		_stochasticKPeriod = Param(nameof(StochasticKPeriod), 10)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic %K", "%K lookback length", "Indicators");

		_stochasticDPeriod = Param(nameof(StochasticDPeriod), 3)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic %D", "%D smoothing length", "Indicators");

		_stochasticSlowing = Param(nameof(StochasticSlowing), 3)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic Slowing", "Extra smoothing for %K", "Indicators");

		_takeProfitPips = Param(nameof(TakeProfitPips), 500m)
		.SetNotNegative()
		.SetDisplay("Take Profit", "Target distance in pips", "Risk Management");

		_stopLossPips = Param(nameof(StopLossPips), 0m)
		.SetNotNegative()
		.SetDisplay("Stop Loss", "Protective distance in pips", "Risk Management");
	}

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

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

		_fastSma = null;
		_slowSma = null;
		_stochastic = null;

		_longEntries.Clear();
		_shortEntries.Clear();

		_prevFast = null;
		_prevSlow = null;
		_pipSize = 0m;
		_longExitRequested = false;
		_shortExitRequested = false;
	}

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

		_fastSma = new SimpleMovingAverage { Length = Math.Max(1, FastPeriod) };
		_slowSma = new SimpleMovingAverage { Length = Math.Max(1, SlowPeriod) };
		_stochastic = new StochasticOscillator();
		_stochastic.K.Length = Math.Max(1, StochasticKPeriod);
		_stochastic.D.Length = Math.Max(1, StochasticDPeriod);

		var subscription = SubscribeCandles(CandleType);
		subscription
		.BindEx(_fastSma, _slowSma, _stochastic, ProcessCandle)
		.Start();

		_pipSize = CalculatePipSize();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _fastSma);
			DrawIndicator(area, _slowSma);
			DrawIndicator(area, _stochastic);
			DrawOwnTrades(area);
		}

		// protection managed manually via ManageExits
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		var price = trade.Trade.Price;
		var volume = trade.Trade.Volume;

		if (trade.Order.Side == Sides.Buy)
		{
			ReduceEntries(_shortEntries, ref volume);

			if (volume > 0m)
			{
				_longEntries.Add(new PositionEntry(price, volume));
			}
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			ReduceEntries(_longEntries, ref volume);

			if (volume > 0m)
			{
				_shortEntries.Add(new PositionEntry(price, volume));
			}
		}

		if (GetTotalVolume(_longEntries) <= 0m)
		{
			_longEntries.Clear();
			_longExitRequested = false;
		}

		if (GetTotalVolume(_shortEntries) <= 0m)
		{
			_shortEntries.Clear();
			_shortExitRequested = false;
		}
	}

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

		var fast = fastValue.ToDecimal();
		var slow = slowValue.ToDecimal();

		ManageExits(candle);

		var signal = SignalDirections.None;

		if (SignalMode == SignalModes.CrossMa)
		{
			if (_prevFast.HasValue && _prevSlow.HasValue)
			{
				var crossBuy = fast < slow && _prevSlow.Value < _prevFast.Value;
				var crossSell = fast > slow && _prevSlow.Value > _prevFast.Value;

				if (crossBuy)
					signal = SignalDirections.Buy;
				else if (crossSell)
					signal = SignalDirections.Sell;
			}
		}
		else if (_stochastic?.IsFormed == true)
		{
			var kValue = stochasticValue.ToDecimal();
			var bullish = candle.ClosePrice > candle.OpenPrice;
			var bearish = candle.ClosePrice < candle.OpenPrice;

			if (fast > slow && bullish && kValue <= 30m)
				signal = SignalDirections.Buy;
			else if (fast < slow && bearish && kValue >= 70m)
				signal = SignalDirections.Sell;
		}

		if (signal != SignalDirections.None)
		{
			ProcessSignal(signal, candle.ClosePrice);
		}

		_prevFast = fast;
		_prevSlow = slow;
	}

	private void ProcessSignal(SignalDirections signal, decimal referencePrice)
	{
		switch (signal)
		{
			case SignalDirections.Buy:
			{
				var shortVolume = GetTotalVolume(_shortEntries);
				if (shortVolume > 0m)
				{
					if (!_shortExitRequested)
					{
						_shortExitRequested = true;
						BuyMarket(shortVolume);
					}
					return;
				}

				var longCount = _longEntries.Count;
				var requested = CalculateRequestedVolume(longCount);
				var volume = PrepareNextVolume(requested);
				if (volume <= 0m)
					return;

				if (longCount == 0)
				{
					BuyMarket(volume);
					return;
				}

				var lowest = GetExtremePrice(_longEntries, true);
				var threshold = lowest - LayerDistancePips * (_pipSize > 0m ? _pipSize : 1m);

				if (referencePrice <= threshold)
				{
					BuyMarket(volume);
				}

				break;
			}
			case SignalDirections.Sell:
			{
				var longVolume = GetTotalVolume(_longEntries);
				if (longVolume > 0m)
				{
					if (!_longExitRequested)
					{
						_longExitRequested = true;
						SellMarket(longVolume);
					}
					return;
				}

				var shortCount = _shortEntries.Count;
				var requested = CalculateRequestedVolume(shortCount);
				var volume = PrepareNextVolume(requested);
				if (volume <= 0m)
					return;

				if (shortCount == 0)
				{
					SellMarket(volume);
					return;
				}

				var highest = GetExtremePrice(_shortEntries, false);
				var threshold = highest + LayerDistancePips * (_pipSize > 0m ? _pipSize : 1m);

				if (referencePrice >= threshold)
				{
					SellMarket(volume);
				}

				break;
			}
		}
	}

	private void ManageExits(ICandleMessage candle)
	{
		var longVolume = GetTotalVolume(_longEntries);
		if (longVolume > 0m && !_longExitRequested)
		{
			var average = GetAveragePrice(_longEntries);
			var takeProfit = TakeProfitPips > 0m ? average + TakeProfitPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;
			var stopLoss = StopLossPips > 0m ? average - StopLossPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;

			if (takeProfit.HasValue && candle.HighPrice >= takeProfit.Value)
			{
				_longExitRequested = true;
				SellMarket(longVolume);
				return;
			}

			if (stopLoss.HasValue && candle.LowPrice <= stopLoss.Value)
			{
				_longExitRequested = true;
				SellMarket(longVolume);
				return;
			}
		}

		var shortVolume = GetTotalVolume(_shortEntries);
		if (shortVolume > 0m && !_shortExitRequested)
		{
			var average = GetAveragePrice(_shortEntries);
			var takeProfit = TakeProfitPips > 0m ? average - TakeProfitPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;
			var stopLoss = StopLossPips > 0m ? average + StopLossPips * (_pipSize > 0m ? _pipSize : 1m) : (decimal?)null;

			if (takeProfit.HasValue && candle.LowPrice <= takeProfit.Value)
			{
				_shortExitRequested = true;
				BuyMarket(shortVolume);
				return;
			}

			if (stopLoss.HasValue && candle.HighPrice >= stopLoss.Value)
			{
				_shortExitRequested = true;
				BuyMarket(shortVolume);
			}
		}
	}

	private decimal CalculateRequestedVolume(int existingCount)
	{
		if (InitialVolume <= 0m)
			return 0m;

		var result = InitialVolume;

		if (existingCount > 0 && Multiplier > 0m)
		{
			result *= (decimal)Math.Pow((double)Math.Max(Multiplier, 1m), existingCount);
		}

		return result;
	}

	private decimal PrepareNextVolume(decimal requested)
	{
		if (requested <= 0m)
			return 0m;

		var security = Security;
		if (security == null)
			return requested;

		var step = security.VolumeStep ?? 0m;
		if (step > 0m)
		{
			requested = step * Math.Round(requested / step, MidpointRounding.AwayFromZero);
		}

		var min = security.MinVolume ?? 0m;
		if (min > 0m && requested < min)
			return 0m;

		var max = security.MaxVolume ?? decimal.MaxValue;
		if (requested > max)
		{
			requested = max;
		}

		return requested;
	}

	private void ReduceEntries(List<PositionEntry> entries, ref decimal volume)
	{
		var index = 0;
		while (volume > 0m && index < entries.Count)
		{
			var entry = entries[index];
			if (volume >= entry.Volume)
			{
				volume -= entry.Volume;
				entries.RemoveAt(index);
			}
			else
			{
				entry.Volume -= volume;
				volume = 0m;
				entries[index] = entry;
			}
		}
	}

	private static decimal GetTotalVolume(List<PositionEntry> entries)
	{
		var total = 0m;
		for (var i = 0; i < entries.Count; i++)
			total += entries[i].Volume;
		return total;
	}

	private static decimal GetAveragePrice(List<PositionEntry> entries)
	{
		var totalVolume = GetTotalVolume(entries);
		if (totalVolume <= 0m)
			return 0m;

		var weighted = 0m;
		for (var i = 0; i < entries.Count; i++)
			weighted += entries[i].Price * entries[i].Volume;

		return weighted / totalVolume;
	}

	private static decimal GetExtremePrice(List<PositionEntry> entries, bool forLong)
	{
		if (entries.Count == 0)
			return 0m;

		var extreme = entries[0].Price;
		for (var i = 1; i < entries.Count; i++)
		{
			var price = entries[i].Price;
			if (forLong)
			{
				if (price < extreme)
					extreme = price;
			}
			else if (price > extreme)
			{
				extreme = price;
			}
		}

		return extreme;
	}

	private decimal CalculatePipSize()
	{
		var security = Security;
		if (security == null)
			return 0m;

		var step = security.PriceStep ?? 0m;
		if (step <= 0m)
			return 0m;

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

		return step;
	}

	private enum SignalDirections
	{
		None,
		Buy,
		Sell
	}

	/// <summary>
	/// Signal selector for the strategy.
	/// </summary>
	public enum SignalModes
	{
		/// <summary>
		/// Use moving average crossovers in a contrarian fashion.
		/// </summary>
		CrossMa,

		/// <summary>
		/// Follow trend direction using moving averages with stochastic confirmation.
		/// </summary>
		Trend
	}

	private sealed class PositionEntry
	{
		public PositionEntry(decimal price, decimal volume)
		{
			Price = price;
			Volume = volume;
		}

		public decimal Price { get; }

		public decimal Volume { get; set; }
	}
}