在 GitHub 上查看

Ma SAR ADX 策略

概述

本策略是 MetaTrader 5 平台 MaSarADX.mq5 专家顾问在 StockSharp 高级 API 上的完整移植。系统通过简单均线确认趋势方向,结合方向性运动指标(ADX)的 +DI / −DI 关系以及抛物线 SAR 轨迹来决定入场与离场。策略仅在蜡烛线收盘后评估信号,复刻原始脚本“仅在新柱子的第一个跳动上交易”的行为。

指标与数据

  • 简单移动平均线(SMA):提供主要趋势过滤,默认长度 100 根 K 线。
  • 平均方向性指数(ADX):生成 +DI 与 −DI 方向指标以确认多空力量,默认周期 14。
  • 抛物线 SAR:作为止损/反转轨迹,定义离场条件,默认加速度 0.02,最大加速度 0.10。
  • 蜡烛线数据:默认请求 1 小时周期,可根据标的和测试需求调整。

在实现中,策略对蜡烛流建立订阅,并通过 BindEx 同步绑定所有指标,确保回调中获取到同一根蜡烛的 SMA、ADX 和 SAR 数值。

交易逻辑

多头入场

  1. 蜡烛收盘价位于均线上方。
  2. +DI 大于或等于 −DI,表明多头动能更强。
  3. 收盘价高于抛物线 SAR。
  4. 当前没有多头仓位(Position <= 0)。

满足以上条件时,策略以市价买入 Volume + |Position| 数量,若之前持有空头仓位会一并平掉。

空头入场

  1. 蜡烛收盘价位于均线下方。
  2. +DI 小于或等于 −DI,表明空头动能更强。
  3. 收盘价低于抛物线 SAR。
  4. 当前没有空头仓位(Position >= 0)。

满足条件时发送市价卖单,数量同样为基础手数加上现有多头仓位的绝对值。

平仓

  • 多头仓位:当收盘价跌破抛物线 SAR,立即市价卖出全部数量。
  • 空头仓位:当收盘价突破抛物线 SAR,立即市价买入全部数量。

策略不额外设置止盈或止损,完全遵循原脚本的 SAR 穿越规则。因为先检查离场再考虑开仓,策略不会在同一根 K 线上直接反手,保持与原始 EA 相同的节奏。

参数

参数 描述 默认值 说明
MaPeriod 趋势过滤所用 SMA 长度。 100 可优化,必须大于 0。
AdxPeriod 计算 ADX 的周期。 14 可优化,必须大于 0。
SarStep 抛物线 SAR 的加速度步长。 0.02 等同于 MQL 中的 step 参数。
SarMax 抛物线 SAR 的最大加速度。 0.10 等同于 MQL 中的 maximum 参数。
Volume 新仓位的基础下单数量。 1 取代原脚本的保证金动态手数。实际下单量为 Volume + |Position|
CandleType 策略订阅的蜡烛类型。 1 小时 可根据需求改为任意周期。

实现要点

  • 通过高层 BindEx 管线同步 SMA、ADX、SAR,避免手动缓存数据。
  • 即使临时禁止交易(AllowTrading 关闭),策略仍会执行 SAR 离场以控制风险。
  • 自带图表绘制:主图展示价格、均线与 SAR,副图绘制 ADX 指标,方便回测或实时调试。
  • 日志记录每次交易决策,并附带关键指标值,便于排查与复现。

使用指南

  1. 在 Designer 或 Backtester 中将策略绑定到目标证券与投资组合。
  2. 根据交易周期调整 CandleType(如 M15、H1、D1 等)。
  3. 结合标的波动性调节均线周期、ADX 周期与 SAR 参数。
  4. 根据资金管理设置 Volume。若需要原 EA 的保证金手数算法,可在发送订单前自行实现。
  5. 启动策略,待所有指标形成(IsFormed)后才会开始交易。

与原 EA 的差异

  • 取消基于账户保证金的手数计算,改为固定的 Volume 参数,以保持在 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>
/// Conversion of the MaSarADX MetaTrader strategy to StockSharp high level API.
/// Combines a moving average, ADX directional movement and Parabolic SAR for entries and exits.
/// </summary>
public class MaSarAdxBindStrategy : Strategy
{
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<decimal> _sarStep;
	private readonly StrategyParam<decimal> _sarMax;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _previousHigh;
	private decimal? _previousLow;
	private decimal? _previousClose;
	private decimal _smoothedPlusDm;
	private decimal _smoothedMinusDm;
	private decimal _smoothedTrueRange;
	private int _adxSamples;

	/// <summary>
	/// Moving average period used for the trend filter.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

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

	/// <summary>
	/// Acceleration step for the Parabolic SAR indicator.
	/// </summary>
	public decimal SarStep
	{
		get => _sarStep.Value;
		set => _sarStep.Value = value;
	}

	/// <summary>
	/// Maximum acceleration factor for the Parabolic SAR indicator.
	/// </summary>
	public decimal SarMax
	{
		get => _sarMax.Value;
		set => _sarMax.Value = value;
	}


	/// <summary>
	/// Type of candles processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="MaSarAdxBindStrategy"/>.
	/// </summary>
	public MaSarAdxBindStrategy()
	{
		_maPeriod = Param(nameof(MaPeriod), 120)
		.SetGreaterThanZero()
		.SetDisplay("MA Period", "Length of the trend moving average", "Indicators")
		
		.SetOptimize(20, 200, 10);

		_adxPeriod = Param(nameof(AdxPeriod), 18)
		.SetGreaterThanZero()
		.SetDisplay("ADX Period", "Length of the Average Directional Index", "Indicators")
		
		.SetOptimize(7, 28, 1);

		_sarStep = Param(nameof(SarStep), 0.02m)
		.SetRange(0.005m, 0.2m)
		.SetDisplay("SAR Step", "Acceleration step for Parabolic SAR", "Indicators")
		;

		_sarMax = Param(nameof(SarMax), 0.1m)
		.SetRange(0.05m, 1m)
		.SetDisplay("SAR Maximum", "Maximum acceleration for Parabolic SAR", "Indicators")
		;


		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
		.SetDisplay("Candle Type", "Type of candles to request", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_previousHigh = null;
		_previousLow = null;
		_previousClose = null;
		_smoothedPlusDm = 0m;
		_smoothedMinusDm = 0m;
		_smoothedTrueRange = 0m;
		_adxSamples = 0;
	}

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

		// Instantiate indicators used in the original MetaTrader script.
		var movingAverage = new SimpleMovingAverage
		{
			Length = MaPeriod
		};

		var parabolicSar = new ParabolicSar
		{
			Acceleration = SarStep,
			AccelerationStep = SarStep,
			AccelerationMax = SarMax
		};

		// Subscribe to candle data and bind indicator updates to a single handler.
		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(movingAverage, parabolicSar, ProcessCandle)
			.Start();

		// Draw the trading context for visual debugging when charts are available.
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, movingAverage);
			DrawIndicator(area, parabolicSar);
			DrawOwnTrades(area);

		}
	}

	private void ProcessCandle(ICandleMessage candle, decimal movingAverage, decimal sar)
	{
		// Work only with completed candles to mirror the original first-tick logic.
		if (candle.State != CandleStates.Finished)
			return;

		var (plusDi, minusDi, isReady) = UpdateDirectionalMovement(candle);
		if (!isReady)
			return;

		// Always allow risk exits even if trading is temporarily disabled.
		if (Position > 0 && candle.ClosePrice < sar)
		{
			SellMarket();
			return;
		}

		if (Position < 0 && candle.ClosePrice > sar)
		{
			BuyMarket();
			return;
		}

		// Entry conditions replicated from the MetaTrader version.
		var bullishSignal = candle.ClosePrice > movingAverage && plusDi >= minusDi && candle.ClosePrice > sar;
		var bearishSignal = candle.ClosePrice < movingAverage && plusDi <= minusDi && candle.ClosePrice < sar;

		if (bullishSignal && Position <= 0)
		{
			BuyMarket();
			return;
		}

		if (bearishSignal && Position >= 0)
		{
			SellMarket();
		}
	}

	private (decimal plusDi, decimal minusDi, bool isReady) UpdateDirectionalMovement(ICandleMessage candle)
	{
		if (_previousHigh is not decimal previousHigh ||
			_previousLow is not decimal previousLow ||
			_previousClose is not decimal previousClose)
		{
			_previousHigh = candle.HighPrice;
			_previousLow = candle.LowPrice;
			_previousClose = candle.ClosePrice;
			return (0m, 0m, false);
		}

		var upMove = candle.HighPrice - previousHigh;
		var downMove = previousLow - candle.LowPrice;
		var plusDm = upMove > downMove && upMove > 0m ? upMove : 0m;
		var minusDm = downMove > upMove && downMove > 0m ? downMove : 0m;
		var trueRange = Math.Max(
			candle.HighPrice - candle.LowPrice,
			Math.Max(
				Math.Abs(candle.HighPrice - previousClose),
				Math.Abs(candle.LowPrice - previousClose)));

		if (_adxSamples < AdxPeriod)
		{
			_smoothedPlusDm += plusDm;
			_smoothedMinusDm += minusDm;
			_smoothedTrueRange += trueRange;
			_adxSamples++;
		}
		else
		{
			_smoothedPlusDm = _smoothedPlusDm - (_smoothedPlusDm / AdxPeriod) + plusDm;
			_smoothedMinusDm = _smoothedMinusDm - (_smoothedMinusDm / AdxPeriod) + minusDm;
			_smoothedTrueRange = _smoothedTrueRange - (_smoothedTrueRange / AdxPeriod) + trueRange;
		}

		_previousHigh = candle.HighPrice;
		_previousLow = candle.LowPrice;
		_previousClose = candle.ClosePrice;

		if (_adxSamples < AdxPeriod || _smoothedTrueRange <= 0m)
			return (0m, 0m, false);

		return (
			100m * _smoothedPlusDm / _smoothedTrueRange,
			100m * _smoothedMinusDm / _smoothedTrueRange,
			true);
	}
}