在 GitHub 上查看

双均线趋势共振策略

概述

双均线趋势共振策略 复刻了原始的 MetaTrader 智能交易系统:通过一条慢速指数移动平均线(EMA)和一条快速线性加权移动平均线(LWMA)来确认趋势。策略会等待两条均线在同一方向上连续倾斜,并使用上一根 K 线的收盘价作为附加确认信号,只有在动量与趋势完全一致时才入场,旨在捕捉最有力量的趋势波段。

在 StockSharp 中的实现仅处理已完成的蜡烛线,保存最近三根 K 线的均线斜率,并通过 StartProtection 自动管理止损和止盈。策略与具体品种无关,只要证券提供蜡烛数据并定义了价格最小变动单位(point),就可以运行。

指标

  • 慢速 EMA(默认 57):代表主趋势方向,要求连续两根 K 线的 EMA 值递增或递减。
  • 快速 LWMA(默认 3):作为动量确认指标,只有当其斜率与慢速 EMA 一致时才允许入场。

参数

参数 默认值 说明
SlowMaLength 57 慢速 EMA 趋势过滤器的周期。
FastMaLength 3 快速 LWMA 动量过滤器的周期。
StopLossPoints 100 止损距离,按品种的 point 表示,最终乘以 Security.PriceStep 转换为价格。
TakeProfitPoints 100 止盈距离,按 point 表示,最终乘以 Security.PriceStep 转换为价格。
CandleType 15 分钟 用于计算的蜡烛类型。

全部参数都暴露为 StrategyParam<T>,因此可以在运行时修改,也可以通过 StockSharp 的优化工具进行批量优化。

交易规则

做多条件

  1. 慢速 EMA 上升:当前值 > 前一根 > 前两根。
  2. 快速 LWMA 上升:当前值 > 前一根 > 前两根。
  3. 上一根 K 线的收盘价高于上一根慢速 EMA 值。
  4. 当前慢速 EMA 高于当前快速 LWMA。
  5. 当前仓位为空仓或净空。
  6. 满足全部条件时,发送市价买入订单,数量为 Volume + |Position|,以便在需要时翻多。

做空条件

  1. 慢速 EMA 下降:当前值 < 前一根 < 前两根。
  2. 快速 LWMA 下降:当前值 < 前一根 < 前两根。
  3. 上一根 K 线的收盘价低于上一根慢速 EMA 值。
  4. 当前慢速 EMA 低于当前快速 LWMA。
  5. 当前仓位为空仓或净多。
  6. 满足全部条件时,发送市价卖出订单,数量为 Volume + |Position|,实现快速翻空。

防护机制

  • StartProtection 会将 StopLossPointsTakeProfitPoints 乘以 Security.PriceStep,从而得到绝对价格距离,然后以市价方式执行止损或止盈,确保在无挂单支持时也能退出。
  • 当出现反向信号时,策略会立即反向开仓,即使已有的止损/止盈单仍在等待。

实现细节

  • 仅处理 CandleStates.Finished 的蜡烛,等同于 MQL 版本中的“新柱”检测。
  • 使用私有字段保存最近两根均线值和上一根收盘价,避免直接访问指标历史。
  • 通过 IsFormedAndOnlineAndAllowTrading() 确保数据完备且允许交易后才下单。
  • LogInfo 输出多空入场的详细信息,便于调试与实时监控。
  • 如果图表可用,会同时绘制蜡烛与两条均线,方便视觉确认。

使用建议

  • 根据标的物的合约规模设置 Volume。策略使用 Volume + |Position| 的市价单来完成反手。
  • 如果证券未设置 PriceStep,代码会回退到 1。此时请根据真实最小跳动调整参数。
  • 优化时可重点关注均线周期与止损/止盈距离,以适配不同市场的波动特征。
  • 可以在此基础上增加波动率、交易时段等过滤器,代码结构已经为扩展做好准备。

建议的优化范围

  • SlowMaLength:20 – 120(步长 5–10)。
  • FastMaLength:2 – 10(步长 1)。
  • StopLossPoints / TakeProfitPoints:50 – 200(根据波动率调整)。

上述范围兼顾了原策略的默认设置,同时为其他品种提供足够的弹性。

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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Dual moving average trend confirmation strategy.
/// Uses a slow EMA and a fast LWMA to detect synchronized trends.
/// Enters long when both averages slope upward, price stays above the slow EMA, and the slow EMA is above the fast LWMA.
/// Enters short when both averages slope downward, price stays below the slow EMA, and the slow EMA is below the fast LWMA.
/// Built-in stop-loss and take-profit are defined in instrument points.
/// </summary>
public class DualMaTrendConfirmationStrategy : Strategy
{
	private readonly StrategyParam<int> _slowMaLength;
	private readonly StrategyParam<int> _fastMaLength;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _previousClose;
	private decimal _slowPrevious;
	private decimal _slowPrevious2;
	private decimal _fastPrevious;
	private decimal _fastPrevious2;
	private int _historyCount;

	/// <summary>
	/// Slow EMA period length.
	/// </summary>
	public int SlowMaLength
	{
		get => _slowMaLength.Value;
		set => _slowMaLength.Value = value;
	}

	/// <summary>
	/// Fast LWMA period length.
	/// </summary>
	public int FastMaLength
	{
		get => _fastMaLength.Value;
		set => _fastMaLength.Value = value;
	}

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

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

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

	/// <summary>
	/// Initializes a new instance of the <see cref="DualMaTrendConfirmationStrategy"/> class.
	/// </summary>
	public DualMaTrendConfirmationStrategy()
	{
		_slowMaLength = Param(nameof(SlowMaLength), 57)
			.SetDisplay("Slow EMA Length", "Period for the slow EMA trend filter", "Moving Averages")
			.SetRange(10, 200)
			;

		_fastMaLength = Param(nameof(FastMaLength), 3)
			.SetDisplay("Fast LWMA Length", "Period for the fast LWMA confirmation filter", "Moving Averages")
			.SetRange(1, 50)
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 100m)
			.SetDisplay("Stop Loss (points)", "Stop-loss distance measured in instrument points", "Risk Management")
			.SetRange(10m, 500m)
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 100m)
			.SetDisplay("Take Profit (points)", "Take-profit distance measured in instrument points", "Risk Management")
			.SetRange(10m, 500m)
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for moving average calculations", "General");
	}

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

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

		// Clear stored history so the next candle starts with a clean state.
		_previousClose = 0m;
		_slowPrevious = 0m;
		_slowPrevious2 = 0m;
		_fastPrevious = 0m;
		_fastPrevious2 = 0m;
		_historyCount = 0;
	}

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

		var slowEma = new ExponentialMovingAverage
		{
			Length = SlowMaLength
		};

		var fastLwma = new WeightedMovingAverage
		{
			Length = FastMaLength
		};

		var subscription = SubscribeCandles(CandleType);

		var step = Security.PriceStep ?? 1m;

		// Enable automatic stop-loss and take-profit management based on point offsets.
		StartProtection(
			takeProfit: new Unit(TakeProfitPoints * step, UnitTypes.Absolute),
			stopLoss: new Unit(StopLossPoints * step, UnitTypes.Absolute),
			useMarketOrders: true);

		subscription
			.Bind(slowEma, fastLwma, (candle, slowValue, fastValue) => ProcessCandle(candle, slowValue, fastValue, slowEma, fastLwma))
			.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal slowValue, decimal fastValue, ExponentialMovingAverage slowEma, WeightedMovingAverage fastLwma)
	{
		// Work only with fully formed candles to avoid premature decisions.
		if (candle.State != CandleStates.Finished)
			return;

		// Ensure both indicators produced reliable values before trading logic.
		if (!slowEma.IsFormed || !fastLwma.IsFormed)
		{
			UpdateHistory(slowValue, fastValue, candle.ClosePrice);
			return;
		}

		// Accumulate at least two previous candles for slope calculations.
		if (_historyCount < 2)
		{
			UpdateHistory(slowValue, fastValue, candle.ClosePrice);
			return;
		}

		var slowRising = slowValue > _slowPrevious && _slowPrevious > _slowPrevious2;
		var fastRising = fastValue > _fastPrevious && _fastPrevious > _fastPrevious2;
		var slowFalling = slowValue < _slowPrevious && _slowPrevious < _slowPrevious2;
		var fastFalling = fastValue < _fastPrevious && _fastPrevious < _fastPrevious2;
		var priceAboveSlow = _previousClose > _slowPrevious;
		var priceBelowSlow = _previousClose < _slowPrevious;
		var slowAboveFast = slowValue > fastValue;
		var slowBelowFast = slowValue < fastValue;

		if (slowRising && fastRising && priceAboveSlow && slowAboveFast && Position <= 0)
		{
			BuyMarket();
		}
		else if (slowFalling && fastFalling && priceBelowSlow && slowBelowFast && Position >= 0)
		{
			SellMarket();
		}

		UpdateHistory(slowValue, fastValue, candle.ClosePrice);
	}

	private void UpdateHistory(decimal slowValue, decimal fastValue, decimal closePrice)
	{
		// Shift previous values so the last two candles are always available.
		_slowPrevious2 = _slowPrevious;
		_slowPrevious = slowValue;
		_fastPrevious2 = _fastPrevious;
		_fastPrevious = fastValue;
		_previousClose = closePrice;

		if (_historyCount < 2)
			_historyCount++;
	}
}