在 GitHub 上查看

GreenTrade 策略

概述

GreenTrade 是原始 MQL5 专家顾问的移植版本。策略通过结合平滑移动平均线(SMMA)的斜率过滤器和相对强弱指数(RSI)的动量确认来捕捉中期趋势。信号只在所选时间框架的已收盘 K 线中计算,并且允许在达到上限之前分批加仓,同时使用固定的风险参数和分步式追踪止损。

交易逻辑

  1. 指标准备
    • 使用 MaPeriod 周期在中价 ((High + Low) / 2) 上计算 SMMA。
    • 使用 RsiPeriod 周期在收盘价上计算 RSI。
  2. 趋势形态过滤
    • 根据 ShiftBar, ShiftBar1, ShiftBar2, ShiftBar3 参数读取四个历史 SMMA 值。
    • 多头条件:SMMA(shift0) > SMMA(shift1) > SMMA(shift2) > SMMA(shift3)
    • 空头条件:SMMA(shift0) < SMMA(shift1) < SMMA(shift2) < SMMA(shift3)
  3. 动量确认
    • 多头信号要求 RSI 高于 RsiBuyLevel,空头信号要求 RSI 低于 RsiSellLevel。RSI 值取自 ShiftBar 根之前的收盘 K 线,与 MQL5 版本忽略当前形成中的 K 线保持一致。
  4. 下单规则
    • 当信号满足并且尚未超过仓位上限时,策略以 TradeVolume 发送市价单。
    • 如果存在反向仓位,将先平掉旧仓再打开新方向。
    • 如果已有同方向仓位,则在不超过 MaxPositions * TradeVolume 的前提下继续加仓。

风险管理

  • 初始止损 / 止盈:每笔新交易会根据 StopLossPipsTakeProfitPips 计算价格目标。点数通过合约的 PriceStep 转换为价格;对于五位报价等较小步长的品种,会额外乘以 10,与原始 EA 相同。
  • 追踪止损:当浮动利润超过 TrailingStopPips + TrailingStepPips 时,将止损移动到距当前价格 TrailingStopPips 的距离。之后每次止损移动都需要价格再前进 TrailingStepPips,复制了原策略的阶梯式追踪逻辑。
  • 仓位上限MaxPositions 用于限制最大仓位块数。若加仓会超过上限,则忽略该信号。

参数

参数 说明 默认值
MaPeriod 基于中价的 SMMA 周期。 67
ShiftBar, ShiftBar1, ShiftBar2, ShiftBar3 用于趋势形态过滤的历史 SMMA 位移(单位:K 线)。 1, 1, 2, 3
RsiPeriod RSI 计算周期。 57
RsiBuyLevel 确认多头的 RSI 阈值。 60
RsiSellLevel 确认空头的 RSI 阈值。 36
TradeVolume 每次开仓或加仓的数量。 0.1
StopLossPips 初始止损距离(点),0 表示关闭。 300
TakeProfitPips 初始止盈距离(点),0 表示关闭。 300
TrailingStopPips 追踪止损激活后的固定距离(点),0 表示关闭。 12
TrailingStepPips 每次移动追踪止损所需的额外利润(点)。 5
MaxPositions 可同时持有的仓位块数(TradeVolume 的倍数)。 7
CandleType 指标计算所使用的 K 线类型。 1 小时

说明

  • 所有计算仅在收盘 K 线上进行,以减少噪音并忠实于原策略。
  • 策略内部跟踪仓位状态,通过市价单执行止损、止盈与追踪离场,即使交易所未挂出保护性委托。
  • 转换版本保留了原有的点值换算和追踪步长逻辑,同时利用 StockSharp 的高级 API 完成数据订阅与订单执行。
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>
/// GreenTrade strategy converted from the MQL implementation.
/// Combines a smoothed moving average slope filter with RSI momentum confirmation.
/// </summary>
public class GreenTradeStrategy : Strategy
{
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _shiftBar;
	private readonly StrategyParam<int> _shiftBar1;
	private readonly StrategyParam<int> _shiftBar2;
	private readonly StrategyParam<int> _shiftBar3;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<decimal> _rsiBuyLevel;
	private readonly StrategyParam<decimal> _rsiSellLevel;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<int> _maxPositions;
	private readonly StrategyParam<DataType> _candleType;

	private SmoothedMovingAverage _smma;
	private RelativeStrengthIndex _rsi;

	private readonly List<decimal> _maHistory = new();
	private readonly List<decimal> _rsiHistory = new();

	private decimal _pipSize = 1m;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;

	/// <summary>
	/// Period for the smoothed moving average.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Shift (in bars) used for the most recent MA/RSI sample.
	/// </summary>
	public int ShiftBar
	{
		get => _shiftBar.Value;
		set => _shiftBar.Value = value;
	}

	/// <summary>
	/// Additional shift between the first and second MA comparison.
	/// </summary>
	public int ShiftBar1
	{
		get => _shiftBar1.Value;
		set => _shiftBar1.Value = value;
	}

	/// <summary>
	/// Additional shift between the second and third MA comparison.
	/// </summary>
	public int ShiftBar2
	{
		get => _shiftBar2.Value;
		set => _shiftBar2.Value = value;
	}

	/// <summary>
	/// Additional shift between the third and fourth MA comparison.
	/// </summary>
	public int ShiftBar3
	{
		get => _shiftBar3.Value;
		set => _shiftBar3.Value = value;
	}

	/// <summary>
	/// RSI lookback period.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// RSI threshold to confirm bullish entries.
	/// </summary>
	public decimal RsiBuyLevel
	{
		get => _rsiBuyLevel.Value;
		set => _rsiBuyLevel.Value = value;
	}

	/// <summary>
	/// RSI threshold to confirm bearish entries.
	/// </summary>
	public decimal RsiSellLevel
	{
		get => _rsiSellLevel.Value;
		set => _rsiSellLevel.Value = value;
	}

	/// <summary>
	/// Trade volume for each new position add-on.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Minimum price improvement (in pips) before trailing stop is moved again.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Maximum number of position units (in TradeVolume steps) allowed.
	/// </summary>
	public int MaxPositions
	{
		get => _maxPositions.Value;
		set => _maxPositions.Value = value;
	}

	/// <summary>
	/// Candle type used for backtesting/live trading.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public GreenTradeStrategy()
	{
		_maPeriod = Param(nameof(MaPeriod), 67)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Length of the smoothed moving average", "Indicators");

		_shiftBar = Param(nameof(ShiftBar), 1)
			.SetGreaterThanZero()
			.SetDisplay("Shift #0", "Index of the most recent evaluated bar", "Signals");

		_shiftBar1 = Param(nameof(ShiftBar1), 1)
			.SetGreaterThanZero()
			.SetDisplay("Shift #1", "Offset from bar #0 to bar #1", "Signals");

		_shiftBar2 = Param(nameof(ShiftBar2), 2)
			.SetGreaterThanZero()
			.SetDisplay("Shift #2", "Offset from bar #1 to bar #2", "Signals");

		_shiftBar3 = Param(nameof(ShiftBar3), 3)
			.SetGreaterThanZero()
			.SetDisplay("Shift #3", "Offset from bar #2 to bar #3", "Signals");

		_rsiPeriod = Param(nameof(RsiPeriod), 57)
			.SetGreaterThanZero()
			.SetDisplay("RSI Period", "Length of the RSI indicator", "Indicators");

		_rsiBuyLevel = Param(nameof(RsiBuyLevel), 60m)
			.SetDisplay("RSI Buy Level", "RSI threshold for bullish entries", "Signals");

		_rsiSellLevel = Param(nameof(RsiSellLevel), 36m)
			.SetDisplay("RSI Sell Level", "RSI threshold for bearish entries", "Signals");

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Volume used for each new order", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 300m)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Initial stop-loss distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 300m)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Initial take-profit distance in pips", "Risk");

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

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Step", "Required progress before trailing adjusts", "Risk");

		_maxPositions = Param(nameof(MaxPositions), 7)
			.SetGreaterThanZero()
			.SetDisplay("Max Positions", "Maximum number of volume units allowed", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle subscription", "Data");
	}

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

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

		_smma = null;
		_rsi = null;
		_maHistory.Clear();
		_rsiHistory.Clear();
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
	}

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

		_smma = new SmoothedMovingAverage { Length = MaPeriod };
		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };

		_pipSize = CalculatePipSize();

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _smma);
			DrawIndicator(area, _rsi);
			DrawOwnTrades(area);
		}
	}

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

		if (_smma == null || _rsi == null)
			return;

		var medianPrice = (candle.HighPrice + candle.LowPrice) / 2m;
		var maResult = _smma.Process(new DecimalIndicatorValue(_smma, medianPrice, candle.OpenTime) { IsFinal = true });
		var rsiResult = _rsi.Process(new DecimalIndicatorValue(_rsi, candle.ClosePrice, candle.OpenTime) { IsFinal = true });

		if (!_smma.IsFormed || !_rsi.IsFormed)
		{
			_maHistory.Add(0m);
			_rsiHistory.Add(0m);
			TrimHistory();
			return;
		}

		var maValue = maResult.ToDecimal();
		var rsiValue = rsiResult.ToDecimal();

		_maHistory.Add(maValue);
		_rsiHistory.Add(rsiValue);
		TrimHistory();

		// Indicators checked above.

		var shift0 = ShiftBar;
		var shift1 = shift0 + ShiftBar1;
		var shift2 = shift1 + ShiftBar2;
		var shift3 = shift2 + ShiftBar3;

		var ma0 = GetHistoryValue(_maHistory, shift0);
		var ma1 = GetHistoryValue(_maHistory, shift1);
		var ma2 = GetHistoryValue(_maHistory, shift2);
		var ma3 = GetHistoryValue(_maHistory, shift3);
		var rsiSample = GetHistoryValue(_rsiHistory, ShiftBar);

		if (ma0 is null || ma1 is null || ma2 is null || ma3 is null || rsiSample is null)
			return;

		var buySignal = ma0 > ma1 && ma1 > ma2 && ma2 > ma3 && rsiSample > RsiBuyLevel;
		var sellSignal = ma0 < ma1 && ma1 < ma2 && ma2 < ma3 && rsiSample < RsiSellLevel;

		if (buySignal && CanIncreasePosition(true))
			OpenPosition(true, candle);
		else if (sellSignal && CanIncreasePosition(false))
			OpenPosition(false, candle);

		UpdateTrailing(candle);
		ManageExits(candle);
	}

	private void OpenPosition(bool isLong, ICandleMessage candle)
	{
		var additionalVolume = TradeVolume;
		var currentPosition = Position;

		if (isLong && currentPosition < 0)
			additionalVolume += Math.Abs(currentPosition);
		else if (!isLong && currentPosition > 0)
			additionalVolume += currentPosition;

		if (additionalVolume <= 0)
			return;

		if (isLong)
			BuyMarket();
		else
			SellMarket();

		if (isLong)
		{
			if (currentPosition > 0)
			{
				var total = currentPosition + TradeVolume;
				_entryPrice = total > 0 ? ((currentPosition * _entryPrice) + (TradeVolume * candle.ClosePrice)) / total : candle.ClosePrice;
			}
			else
			{
				_entryPrice = candle.ClosePrice;
			}
		}
		else
		{
			if (currentPosition < 0)
			{
				var total = Math.Abs(currentPosition) + TradeVolume;
				_entryPrice = total > 0 ? ((Math.Abs(currentPosition) * _entryPrice) + (TradeVolume * candle.ClosePrice)) / total : candle.ClosePrice;
			}
			else
			{
				_entryPrice = candle.ClosePrice;
			}
		}

		var stopDistance = StopLossPips * _pipSize;
		var takeDistance = TakeProfitPips * _pipSize;

		_stopPrice = stopDistance > 0 ? (isLong ? _entryPrice - stopDistance : _entryPrice + stopDistance) : null;
		_takePrice = takeDistance > 0 ? (isLong ? _entryPrice + takeDistance : _entryPrice - takeDistance) : null;
	}

	private void UpdateTrailing(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0)
			return;

		var trailingDistance = TrailingStopPips * _pipSize;
		var stepDistance = TrailingStepPips * _pipSize;

		if (Position > 0 && _entryPrice > 0)
		{
			var profit = candle.ClosePrice - _entryPrice;
			if (profit > trailingDistance + stepDistance)
			{
				var threshold = candle.ClosePrice - (trailingDistance + stepDistance);
				if (!_stopPrice.HasValue || _stopPrice.Value < threshold)
					_stopPrice = candle.ClosePrice - trailingDistance;
			}
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			var profit = _entryPrice - candle.ClosePrice;
			if (profit > trailingDistance + stepDistance)
			{
				var threshold = candle.ClosePrice + trailingDistance + stepDistance;
				if (!_stopPrice.HasValue || _stopPrice.Value > threshold)
					_stopPrice = candle.ClosePrice + trailingDistance;
			}
		}
	}

	private void ManageExits(ICandleMessage candle)
	{
		if (Position > 0)
		{
			if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				SellMarket();
				ResetPositionState();
				return;
			}

			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket();
				ResetPositionState();
				return;
			}
		}
		else if (Position < 0)
		{
			if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				BuyMarket();
				ResetPositionState();
				return;
			}

			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket();
				ResetPositionState();
				return;
			}
		}
		else
		{
			ResetPositionState();
		}
	}

	private bool CanIncreasePosition(bool isLong)
	{
		if (TradeVolume <= 0)
			return false;

		if (MaxPositions <= 0)
			return true;

		var maxVolume = MaxPositions * TradeVolume;
		var absolutePosition = Math.Abs(Position);

		if (isLong && Position < 0)
			return true;

		if (!isLong && Position > 0)
			return true;

		var tolerance = Security?.VolumeStep ?? 0.0000001m;
		return absolutePosition + TradeVolume <= maxVolume + tolerance;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;
		if (step < 0.01m)
			step *= 10m;
		return step;
	}

	private static decimal? GetHistoryValue(List<decimal> values, int shift)
	{
		if (shift <= 0)
			return null;

		var index = values.Count - shift;
		if (index < 0)
			return null;

		return values[index];
	}

	private void TrimHistory()
	{
		var maxShift = ShiftBar + ShiftBar1 + ShiftBar2 + ShiftBar3;
		var maxCount = Math.Max(maxShift + 5, 10);

		if (_maHistory.Count > maxCount)
			_maHistory.RemoveRange(0, _maHistory.Count - maxCount);

		if (_rsiHistory.Count > maxCount)
			_rsiHistory.RemoveRange(0, _rsiHistory.Count - maxCount);
	}

	private void ResetPositionState()
	{
		if (Math.Abs(Position) < (Security?.VolumeStep ?? 0.0000001m))
		{
			_entryPrice = 0m;
			_stopPrice = null;
			_takePrice = null;
		}
	}
}