在 GitHub 上查看

ADX 交叉策略

ADX 交叉策略基于平均趋向指数 (ADX) 指标,通过分析正向指标线 (+DI) 与负向指标线 (-DI) 的交叉来识别趋势变化。

当 +DI 上穿 -DI 时视为看涨信号,策略可以开多仓并可选择平掉现有空仓。相反,当 +DI 下穿 -DI 时视为看跌信号,策略开空并可选择平掉多仓。策略通过内置的风险管理支持可选的止损和止盈。

指标

该策略使用 StockSharp 的 AverageDirectionalIndex 指标。策略仅使用方向线,ADX 主线不用于决策。

参数

  • ADX Period – ADX 计算周期,默认 50
  • Candle Type – 使用的K线周期,默认 1 小时
  • Allow Buy Open – 是否允许开多,默认 true
  • Allow Sell Open – 是否允许开空,默认 true
  • Allow Buy Close – 是否允许在卖出信号时平多,默认 true
  • Allow Sell Close – 是否允许在买入信号时平空,默认 true
  • Stop Loss – 以绝对价格表示的止损距离,默认 1000
  • Take Profit – 以绝对价格表示的止盈距离,默认 2000

交易逻辑

  1. 订阅指定周期的K线并计算 ADX 指标。
  2. 跟踪 +DI 和 -DI 的前一值以检测交叉。
  3. 当 +DI 上穿 -DI:
    • 若启用 Allow Sell Close,平掉空头。
    • 若启用 Allow Buy Open,开立多头。
  4. 当 +DI 下穿 -DI:
    • 若启用 Allow Buy Close,平掉多头。
    • 若启用 Allow Sell Open,开立空头。
  5. 使用 StartProtection 应用止损和止盈。

注意事项

  • 仅处理已完成的K线 (CandleStates.Finished)。
  • 策略依赖 StockSharp 内置的风险管理来执行止损/止盈。
  • 平仓通过发送相反方向的市价单完成。

该策略仅用于教育目的,在实盘交易前可能需要进一步优化。

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>
/// Strategy based on crossovers of the +DI and -DI lines of the ADX indicator.
/// It opens or closes positions when directional lines cross each other.
/// </summary>
public class AdxCrossingStrategy : Strategy
{
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<bool> _allowBuyOpen;
	private readonly StrategyParam<bool> _allowSellOpen;
	private readonly StrategyParam<bool> _allowBuyClose;
	private readonly StrategyParam<bool> _allowSellClose;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _trendThreshold;

	private decimal _prevPlusDi;
	private decimal _prevMinusDi;
	private bool _isInitialized;

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

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

	/// <summary>
	/// Permission to open long positions.
	/// </summary>
	public bool AllowBuyOpen
	{
		get => _allowBuyOpen.Value;
		set => _allowBuyOpen.Value = value;
	}

	/// <summary>
	/// Permission to open short positions.
	/// </summary>
	public bool AllowSellOpen
	{
		get => _allowSellOpen.Value;
		set => _allowSellOpen.Value = value;
	}

	/// <summary>
	/// Permission to close long positions.
	/// </summary>
	public bool AllowBuyClose
	{
		get => _allowBuyClose.Value;
		set => _allowBuyClose.Value = value;
	}

	/// <summary>
	/// Permission to close short positions.
	/// </summary>
	public bool AllowSellClose
	{
		get => _allowSellClose.Value;
		set => _allowSellClose.Value = value;
	}

	/// <summary>
	/// Stop-loss in absolute price units.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Take-profit in absolute price units.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Minimal ADX strength required to trade.
	/// </summary>
	public decimal TrendThreshold
	{
		get => _trendThreshold.Value;
		set => _trendThreshold.Value = value;
	}

	/// <summary>
	/// Initialize strategy parameters.
	/// </summary>
	public AdxCrossingStrategy()
	{
		_adxPeriod = Param(nameof(AdxPeriod), 50)
			.SetDisplay("ADX Period", "Period of ADX indicator", "Indicators")
			
			.SetOptimize(10, 100, 5);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles for calculations", "General");

		_allowBuyOpen = Param(nameof(AllowBuyOpen), true)
			.SetDisplay("Allow Buy Open", "Enable opening long positions", "Permissions");

		_allowSellOpen = Param(nameof(AllowSellOpen), true)
			.SetDisplay("Allow Sell Open", "Enable opening short positions", "Permissions");

		_allowBuyClose = Param(nameof(AllowBuyClose), true)
			.SetDisplay("Allow Buy Close", "Enable closing long positions", "Permissions");

		_allowSellClose = Param(nameof(AllowSellClose), true)
			.SetDisplay("Allow Sell Close", "Enable closing short positions", "Permissions");

		_stopLoss = Param(nameof(StopLoss), 1000m)
			.SetDisplay("Stop Loss", "Absolute stop-loss in price units", "Risk");

		_takeProfit = Param(nameof(TakeProfit), 2000m)
			.SetDisplay("Take Profit", "Absolute take-profit in price units", "Risk");

		_trendThreshold = Param(nameof(TrendThreshold), 15m)
			.SetDisplay("Trend Threshold", "Minimal ADX strength required to trade", "Indicators");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevPlusDi = default;
		_prevMinusDi = default;
		_isInitialized = false;
	}

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

		var adx = new AverageDirectionalIndex { Length = AdxPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(adx, ProcessCandle)
			.Start();

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

		StartProtection(
			stopLoss: StopLoss > 0m ? new Unit(StopLoss, UnitTypes.Absolute) : null,
			takeProfit: TakeProfit > 0m ? new Unit(TakeProfit, UnitTypes.Absolute) : null);
	}

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

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		var adx = (AverageDirectionalIndexValue)adxValue;

		if (adx.Dx.Plus is not decimal plusDi || adx.Dx.Minus is not decimal minusDi)
			return;

		if (!_isInitialized)
		{
			_prevPlusDi = plusDi;
			_prevMinusDi = minusDi;
			_isInitialized = true;
			return;
		}

		if (adx.MovingAverage is not decimal adxStrength || adxStrength < TrendThreshold)
		{
			_prevPlusDi = plusDi;
			_prevMinusDi = minusDi;
			return;
		}

		var buySignal = plusDi > minusDi && _prevPlusDi <= _prevMinusDi;
		var sellSignal = plusDi < minusDi && _prevPlusDi >= _prevMinusDi;

		if (buySignal)
		{
			if (AllowBuyOpen && Position <= 0)
				BuyMarket(Position < 0 ? Volume + Math.Abs(Position) : Volume);
			else if (AllowSellClose && Position < 0)
				BuyMarket(Math.Abs(Position));
		}

		if (sellSignal)
		{
			if (AllowSellOpen && Position >= 0)
				SellMarket(Position > 0 ? Volume + Position : Volume);
			else if (AllowBuyClose && Position > 0)
				SellMarket(Math.Abs(Position));
		}

		_prevPlusDi = plusDi;
		_prevMinusDi = minusDi;
	}
}