在 GitHub 上查看

ADX Expert 策略

概述

ADX Expert 策略 是对 MetaTrader 4 原始专家顾问 “ADX Expert”(MQL 脚本 20315)的完整移植。策略在平均趋向指数(ADX)低于阈值时,监控正向指标(+DI)与负向指标(-DI)的交叉,识别横盘市场中的短期动量机会。与原版一致,策略一次只持有一个仓位。

交易逻辑

  1. 订阅所选蜡烛序列(默认 15 分钟)并计算设定周期的 ADX。
  2. 当满足以下条件时开多仓:
    • +DI 上穿 -DI;
    • 当前 ADX 低于阈值(默认 20),表明趋势较弱;
    • 实际点差低于 MaxSpreadPoints 限制;
    • 当前没有持仓。
  3. 当满足以下条件时开空仓:
    • +DI 下穿 -DI;
    • ADX 仍低于阈值;
    • 点差过滤通过且当前为空仓。
  4. 通过 StartProtection 设置止损与止盈,模拟 MQL 版本中的固定止损/止盈。距离以价格点(最小跳动单位)表示,将参数设为 0 可禁用该功能。

策略遵循单仓流程:在现有仓位被止损或止盈平仓之前,新信号会被忽略。

参数

参数 说明 默认值
TradeVolume 每次下单使用的交易量。 0.1
AdxPeriod ADX 计算周期。 14
AdxThreshold 允许开仓的最大 ADX 数值。 20
MaxSpreadPoints 允许的最大点差(价格点)。设为 0 可禁用过滤。 20
StopLossPoints 止损距离(价格点)。 200
TakeProfitPoints 止盈距离(价格点)。 400
CandleType 指标所用的蜡烛类型(默认 15 分钟)。 15 分钟时间框架

额外说明

  • 点差过滤需要 Level2/订单簿数据才能获取最佳买价和卖价,请确认数据源支持。
  • 代码中的注释和日志全部使用英文,以符合仓库约定。
  • 本策略仅供学习与研究使用,请务必在模拟环境中充分测试后再考虑实际交易。
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>
/// ADX crossover strategy translated from the original MQL expert.
/// Opens a single position when DI lines cross while ADX remains weak.
/// </summary>
public class AdxExpertStrategy : Strategy
{
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<decimal> _adxThreshold;
	private readonly StrategyParam<decimal> _maxSpreadPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private AverageDirectionalIndex _adx = null!;
	private decimal _previousPlusDi;
	private decimal _previousMinusDi;
	private bool _hasPreviousDi;

	/// <summary>
	/// Trading volume for every market order.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// ADX calculation period.
	/// </summary>
	public int AdxPeriod
	{
		get => _adxPeriod.Value;
		set => _adxPeriod.Value = value;
	}

	/// <summary>
	/// Maximum ADX level that still allows new trades.
	/// </summary>
	public decimal AdxThreshold
	{
		get => _adxThreshold.Value;
		set => _adxThreshold.Value = value;
	}

	/// <summary>
	/// Maximum allowed bid-ask spread measured in price points.
	/// </summary>
	public decimal MaxSpreadPoints
	{
		get => _maxSpreadPoints.Value;
		set => _maxSpreadPoints.Value = value;
	}

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

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

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

	/// <summary>
	/// Initializes a new instance of <see cref="AdxExpertStrategy"/>.
	/// </summary>
	public AdxExpertStrategy()
	{
		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade volume", "Order volume used for entries", "Risk management")
			
			.SetOptimize(0.1m, 1m, 0.1m);

		_adxPeriod = Param(nameof(AdxPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ADX period", "Smoothing length for the ADX indicator", "Indicators")
			
			.SetOptimize(7, 28, 7);

		_adxThreshold = Param(nameof(AdxThreshold), 20m)
			.SetGreaterThanZero()
			.SetDisplay("ADX threshold", "Upper ADX limit that allows trades", "Signals")
			
			.SetOptimize(15m, 35m, 5m);

		_maxSpreadPoints = Param(nameof(MaxSpreadPoints), 20m)
			.SetNotNegative()
			.SetDisplay("Max spread (points)", "Maximum allowed bid-ask spread in points", "Risk management")
			
			.SetOptimize(5m, 40m, 5m);

		_stopLossPoints = Param(nameof(StopLossPoints), 200m)
			.SetNotNegative()
			.SetDisplay("Stop loss (points)", "Protective stop distance in price points", "Risk management")
			
			.SetOptimize(100m, 400m, 50m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400m)
			.SetNotNegative()
			.SetDisplay("Take profit (points)", "Target distance in price points", "Risk management")
			
			.SetOptimize(200m, 600m, 100m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
			.SetDisplay("Candle type", "Type of candles used for ADX", "General");
	}

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

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

		_previousPlusDi = 0m;
		_previousMinusDi = 0m;
		_hasPreviousDi = false;
		_entryPrice = 0m;
	}

	private decimal _entryPrice;

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

		_adx = new AverageDirectionalIndex { Length = AdxPeriod };

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

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

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

		IIndicatorValue adxResult;

		try
		{
			adxResult = _adx.Process(candle);
		}
		catch (IndexOutOfRangeException)
		{
			return;
		}

		if (adxResult.IsEmpty || !_adx.IsFormed)
			return;

		if (adxResult is not AverageDirectionalIndexValue adxData)
			return;

		var plusDi = adxData.Dx.Plus ?? 0m;
		var minusDi = adxData.Dx.Minus ?? 0m;

		if (adxData.MovingAverage is not decimal currentAdx)
		{
			_previousPlusDi = plusDi;
			_previousMinusDi = minusDi;
			_hasPreviousDi = true;
			return;
		}

		if (!_hasPreviousDi)
		{
			_previousPlusDi = plusDi;
			_previousMinusDi = minusDi;
			_hasPreviousDi = true;
			return;
		}

		// Manage open position SL/TP
		if (Position != 0)
		{
			var step = Security?.PriceStep ?? 1m;
			if (Position > 0)
			{
				if (StopLossPoints > 0m && candle.LowPrice <= _entryPrice - StopLossPoints * step)
				{
					SellMarket(Position);
					goto updateDi;
				}
				if (TakeProfitPoints > 0m && candle.HighPrice >= _entryPrice + TakeProfitPoints * step)
				{
					SellMarket(Position);
					goto updateDi;
				}
			}
			else
			{
				var vol = Math.Abs(Position);
				if (StopLossPoints > 0m && candle.HighPrice >= _entryPrice + StopLossPoints * step)
				{
					BuyMarket(vol);
					goto updateDi;
				}
				if (TakeProfitPoints > 0m && candle.LowPrice <= _entryPrice - TakeProfitPoints * step)
				{
					BuyMarket(vol);
					goto updateDi;
				}
			}
		}

		var bullishCross = _previousPlusDi <= _previousMinusDi && plusDi > minusDi;
		var bearishCross = _previousPlusDi >= _previousMinusDi && plusDi < minusDi;

		if (currentAdx < AdxThreshold && Position == 0)
		{
			if (bullishCross)
			{
				BuyMarket(TradeVolume);
				_entryPrice = candle.ClosePrice;
			}
			else if (bearishCross)
			{
				SellMarket(TradeVolume);
				_entryPrice = candle.ClosePrice;
			}
		}

		updateDi:
		_previousPlusDi = plusDi;
		_previousMinusDi = minusDi;
	}
}