在 GitHub 上查看

ADX MA Crossover

概述

本策略复刻 MetaTrader 中的“ADX & MA”专家顾问,结合平滑移动平均线(SMMA)与平均趋向指数(ADX)过滤信号。策略仅在所选周期的最近两根已完成K线都给出最终指标值后才会评估入场,从而避免在指标尚未形成时提前交易。实现基于净头寸模型,会在出现反向信号时自动翻转仓位,模拟原始对冲版本的行为。

移动平均线以每根K线的中位价 (High + Low) / 2 计算,与原脚本使用的 SMMA 完全一致。ADX 阈值用于确认趋势强度,帮助过滤掉短暂的虚假突破。

入场逻辑

  • 等待平滑移动平均线与 ADX 同时输出最终值。
  • 使用上一根已完成K线 (n-1) 的收盘价与该K线对应的 SMMA 值进行比较。
  • 当满足以下条件时做多:
    • n-1 的收盘价高于其对应的 SMMA;
    • n-2 的收盘价低于同一 SMMA(形成向上交叉);
    • n-1 的 ADX 值大于或等于 AdxThreshold
  • 当条件反向时做空(收盘价从上向下穿越 SMMA,并且 ADX 达到阈值)。
  • 订单数量为策略 Volume 加上当前反向仓位的绝对值,确保出现反向信号时能够直接翻转仓位。

离场逻辑

多头持仓将在以下任一情况出现时平仓:

  • 最近确认的收盘价 (n-1) 再次跌破 SMMA(反向交叉);
  • 价格上涨到长仓的止盈距离;
  • 价格回落到长仓的止损距离;
  • 启用了长仓追踪止损并且价格较入场价盈利超过 TrailingStopBuy 点时,追踪止损开始跟随价格锁定利润。

空头仓位使用同样的规则,只是方向相反,并拥有独立的止盈、止损和追踪距离设置。每次出现反向信号时,策略都会发送足够大的市价单来平掉原仓并开立新仓。

风险与资金管理

  • 止盈、止损和追踪距离都以**点(pip)**为单位。策略根据 Security.PriceStep 计算点值,当品种报价保留 3 或 5 位小数时,会使用 PriceStep × 10 的修正,与原始 MQL 逻辑一致。
  • InitializeLongTargetsInitializeShortTargets 在下单后立刻根据最新确认收盘价估算入场价,并计算对应的绝对止盈/止损价位。
  • 当启用追踪止损且浮动盈利超过设定距离时,止损价位会沿趋势方向移动以锁定盈利。
  • 每次仓位被关闭后,所有目标和止损数值都会重置,避免复用过期信息。

参数

  • MaPeriod – 平滑移动平均线的周期(默认 15)。
  • AdxPeriod – ADX 的平滑周期(默认 12)。
  • AdxThreshold – 触发交易所需的最小 ADX 值(默认 16)。
  • TakeProfitBuy / StopLossBuy / TrailingStopBuy – 多头的止盈、止损与追踪距离(点)。
  • TakeProfitSell / StopLossSell / TrailingStopSell – 空头的止盈、止损与追踪距离(点)。
  • CandleType – 使用的K线周期,默认 1 分钟。

请通过策略的 Volume 属性设置基础下单手数。如需复现原脚本的行为,可将多空方向的风险参数设置为相同数值。

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 System.Globalization;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// ADX filtered smoothed moving average crossover strategy.
/// Opens trades when the previous candle crosses the smoothed MA and ADX confirms the trend.
/// Adds configurable take profit, stop loss and trailing stop distances measured in pips.
/// </summary>
public class AdxMaCrossoverStrategy : Strategy
{
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<decimal> _adxThreshold;
	private readonly StrategyParam<decimal> _takeProfitBuy;
	private readonly StrategyParam<decimal> _stopLossBuy;
	private readonly StrategyParam<decimal> _trailingStopBuy;
	private readonly StrategyParam<decimal> _takeProfitSell;
	private readonly StrategyParam<decimal> _stopLossSell;
	private readonly StrategyParam<decimal> _trailingStopSell;
	private readonly StrategyParam<DataType> _candleType;

	private SmoothedMovingAverage _ma = null!;
	private AverageDirectionalIndex _adx = null!;
	private decimal _pipSize;
	private decimal _prevClose;
	private decimal _prevPrevClose;
	private decimal _prevMa;
	private decimal _prevAdx;
	private bool _hasPrev;
	private bool _hasPrevPrev;

	private decimal _longEntryPrice;
	private decimal _longStopPrice;
	private decimal _longTakeProfitPrice;
	private decimal _shortEntryPrice;
	private decimal _shortStopPrice;
	private decimal _shortTakeProfitPrice;

	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	public int AdxPeriod
	{
		get => _adxPeriod.Value;
		set => _adxPeriod.Value = value;
	}

	public decimal AdxThreshold
	{
		get => _adxThreshold.Value;
		set => _adxThreshold.Value = value;
	}

	public decimal TakeProfitBuy
	{
		get => _takeProfitBuy.Value;
		set => _takeProfitBuy.Value = value;
	}

	public decimal StopLossBuy
	{
		get => _stopLossBuy.Value;
		set => _stopLossBuy.Value = value;
	}

	public decimal TrailingStopBuy
	{
		get => _trailingStopBuy.Value;
		set => _trailingStopBuy.Value = value;
	}

	public decimal TakeProfitSell
	{
		get => _takeProfitSell.Value;
		set => _takeProfitSell.Value = value;
	}

	public decimal StopLossSell
	{
		get => _stopLossSell.Value;
		set => _stopLossSell.Value = value;
	}

	public decimal TrailingStopSell
	{
		get => _trailingStopSell.Value;
		set => _trailingStopSell.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public AdxMaCrossoverStrategy()
	{
		_maPeriod = Param(nameof(MaPeriod), 15)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Period of the smoothed moving average", "General")
			;
		_adxPeriod = Param(nameof(AdxPeriod), 12)
			.SetGreaterThanZero()
			.SetDisplay("ADX Period", "Smoothing period for Average Directional Index", "Indicators")
			;
		_adxThreshold = Param(nameof(AdxThreshold), 25m)
			.SetDisplay("ADX Threshold", "Minimum ADX value required to trade", "Indicators")
			;
		_takeProfitBuy = Param(nameof(TakeProfitBuy), 83m)
			.SetDisplay("Buy Take Profit (pips)", "Take profit distance for long trades", "Risk Management")
			.SetNotNegative();
		_stopLossBuy = Param(nameof(StopLossBuy), 55m)
			.SetDisplay("Buy Stop Loss (pips)", "Stop loss distance for long trades", "Risk Management")
			.SetNotNegative();
		_trailingStopBuy = Param(nameof(TrailingStopBuy), 27m)
			.SetDisplay("Buy Trailing Stop (pips)", "Trailing stop distance for long trades", "Risk Management")
			.SetNotNegative();
		_takeProfitSell = Param(nameof(TakeProfitSell), 63m)
			.SetDisplay("Sell Take Profit (pips)", "Take profit distance for short trades", "Risk Management")
			.SetNotNegative();
		_stopLossSell = Param(nameof(StopLossSell), 50m)
			.SetDisplay("Sell Stop Loss (pips)", "Stop loss distance for short trades", "Risk Management")
			.SetNotNegative();
		_trailingStopSell = Param(nameof(TrailingStopSell), 27m)
			.SetDisplay("Sell Trailing Stop (pips)", "Trailing stop distance for short trades", "Risk Management")
			.SetNotNegative();
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for calculations", "General");
	}

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

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

		_ma?.Reset();
		_adx?.Reset();

		_pipSize = 0m;
		_prevClose = 0m;
		_prevPrevClose = 0m;
		_prevMa = 0m;
		_prevAdx = 0m;
		_hasPrev = false;
		_hasPrevPrev = false;

		ResetLongTargets();
		ResetShortTargets();
	}

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

		_ma = new SmoothedMovingAverage { Length = MaPeriod };
		_adx = new AverageDirectionalIndex { Length = AdxPeriod };
		_pipSize = CalculatePipSize();

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

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

			var adxArea = CreateChartArea();
			if (adxArea != null)
			{
				DrawIndicator(adxArea, _adx);
			}
		}
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue adxValue)
	{
		// Only react to closed candles to match the MQL implementation.
		if (candle.State != CandleStates.Finished)
			return;

		var median = (candle.HighPrice + candle.LowPrice) / 2m;
		var maValue = _ma.Process(new DecimalIndicatorValue(_ma, median, candle.OpenTime) { IsFinal = true });

		if (!maValue.IsFinal || !adxValue.IsFinal)
			return;

		var ma = maValue.GetValue<decimal>();
		var adx = ((AverageDirectionalIndexValue)adxValue).MovingAverage ?? 0m;
		var close = candle.ClosePrice;

		if (_hasPrev && _hasPrevPrev)
		{
			ManageOpenPositions(close);

			var longSignal = _prevClose > _prevMa && _prevPrevClose < _prevMa && _prevAdx >= AdxThreshold;
			var shortSignal = _prevClose < _prevMa && _prevPrevClose > _prevMa && _prevAdx >= AdxThreshold;

			if (longSignal && Position <= 0)
			{
				BuyMarket();
				InitializeLongTargets(_prevClose);
			}
			else if (shortSignal && Position >= 0)
			{
				SellMarket();
				InitializeShortTargets(_prevClose);
			}
		}

		UpdateHistory(close, ma, adx);
	}

	private void ManageOpenPositions(decimal currentClose)
	{
		// Manage long position exits before evaluating new entries.
		if (Position > 0)
		{
			if (_prevClose < _prevMa)
			{
				SellMarket();
				ResetLongTargets();
				return;
			}

			UpdateLongTrailing(currentClose);

			if (_longTakeProfitPrice > 0m && currentClose >= _longTakeProfitPrice)
			{
				SellMarket();
				ResetLongTargets();
				return;
			}

			if (_longStopPrice > 0m && currentClose <= _longStopPrice)
			{
				SellMarket();
				ResetLongTargets();
				return;
			}
		}
		else if (Position < 0)
		{
			if (_prevClose > _prevMa)
			{
				BuyMarket();
				ResetShortTargets();
				return;
			}

			UpdateShortTrailing(currentClose);

			if (_shortTakeProfitPrice > 0m && currentClose <= _shortTakeProfitPrice)
			{
				BuyMarket();
				ResetShortTargets();
				return;
			}

			if (_shortStopPrice > 0m && currentClose >= _shortStopPrice)
			{
				BuyMarket();
				ResetShortTargets();
				return;
			}
		}
		else
		{
			ResetLongTargets();
			ResetShortTargets();
		}
	}

	private void UpdateLongTrailing(decimal currentClose)
	{
		if (TrailingStopBuy <= 0m || _longEntryPrice <= 0m)
			return;

		var trailingDistance = TrailingStopBuy * _pipSize;
		if (trailingDistance <= 0m)
			return;

		var profit = currentClose - _longEntryPrice;
		if (profit <= trailingDistance)
			return;

		var newStop = currentClose - trailingDistance;
		if (newStop > _longStopPrice)
			_longStopPrice = newStop;
	}

	private void UpdateShortTrailing(decimal currentClose)
	{
		if (TrailingStopSell <= 0m || _shortEntryPrice <= 0m)
			return;

		var trailingDistance = TrailingStopSell * _pipSize;
		if (trailingDistance <= 0m)
			return;

		var profit = _shortEntryPrice - currentClose;
		if (profit <= trailingDistance)
			return;

		var newStop = currentClose + trailingDistance;
		if (_shortStopPrice == 0m || newStop < _shortStopPrice)
			_shortStopPrice = newStop;
	}

	private void InitializeLongTargets(decimal entryPrice)
	{
		_longEntryPrice = entryPrice;
		_longStopPrice = StopLossBuy > 0m ? entryPrice - StopLossBuy * _pipSize : 0m;
		_longTakeProfitPrice = TakeProfitBuy > 0m ? entryPrice + TakeProfitBuy * _pipSize : 0m;

		ResetShortTargets();
	}

	private void InitializeShortTargets(decimal entryPrice)
	{
		_shortEntryPrice = entryPrice;
		_shortStopPrice = StopLossSell > 0m ? entryPrice + StopLossSell * _pipSize : 0m;
		_shortTakeProfitPrice = TakeProfitSell > 0m ? entryPrice - TakeProfitSell * _pipSize : 0m;

		ResetLongTargets();
	}

	private void ResetLongTargets()
	{
		_longEntryPrice = 0m;
		_longStopPrice = 0m;
		_longTakeProfitPrice = 0m;
	}

	private void ResetShortTargets()
	{
		_shortEntryPrice = 0m;
		_shortStopPrice = 0m;
		_shortTakeProfitPrice = 0m;
	}

	private void UpdateHistory(decimal close, decimal ma, decimal adx)
	{
		if (_hasPrev)
		{
			_prevPrevClose = _prevClose;
			_hasPrevPrev = true;
		}
		else
		{
			_hasPrevPrev = false;
		}

		_prevClose = close;
		_prevMa = ma;
		_prevAdx = adx;
		_hasPrev = true;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var decimals = GetDecimalPlaces(step);
		if (decimals == 3 || decimals == 5)
			return step * 10m;

		return step;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		var text = value.ToString(CultureInfo.InvariantCulture);
		var separatorIndex = text.IndexOf('.') >= 0 ? text.IndexOf('.') : text.IndexOf(',');
		if (separatorIndex < 0)
			return 0;

		return text.Length - separatorIndex - 1;
	}
}