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 数值。
交易逻辑
多头入场
- 蜡烛收盘价位于均线上方。
- +DI 大于或等于 −DI,表明多头动能更强。
- 收盘价高于抛物线 SAR。
- 当前没有多头仓位(
Position <= 0)。
满足以上条件时,策略以市价买入 Volume + |Position| 数量,若之前持有空头仓位会一并平掉。
空头入场
- 蜡烛收盘价位于均线下方。
- +DI 小于或等于 −DI,表明空头动能更强。
- 收盘价低于抛物线 SAR。
- 当前没有空头仓位(
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 指标,方便回测或实时调试。
- 日志记录每次交易决策,并附带关键指标值,便于排查与复现。
使用指南
- 在 Designer 或 Backtester 中将策略绑定到目标证券与投资组合。
- 根据交易周期调整
CandleType(如 M15、H1、D1 等)。 - 结合标的波动性调节均线周期、ADX 周期与 SAR 参数。
- 根据资金管理设置
Volume。若需要原 EA 的保证金手数算法,可在发送订单前自行实现。 - 启动策略,待所有指标形成(
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);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import SimpleMovingAverage, ParabolicSar
from StockSharp.Algo.Strategies import Strategy
class ma_sar_adx_bind_strategy(Strategy):
def __init__(self):
super(ma_sar_adx_bind_strategy, self).__init__()
self._ma_period = self.Param("MaPeriod", 120)
self._adx_period = self.Param("AdxPeriod", 18)
self._sar_step = self.Param("SarStep", 0.02)
self._sar_max = self.Param("SarMax", 0.1)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2)))
self._previous_high = None
self._previous_low = None
self._previous_close = None
self._smoothed_plus_dm = 0.0
self._smoothed_minus_dm = 0.0
self._smoothed_true_range = 0.0
self._adx_samples = 0
@property
def MaPeriod(self):
return self._ma_period.Value
@MaPeriod.setter
def MaPeriod(self, value):
self._ma_period.Value = value
@property
def AdxPeriod(self):
return self._adx_period.Value
@AdxPeriod.setter
def AdxPeriod(self, value):
self._adx_period.Value = value
@property
def SarStep(self):
return self._sar_step.Value
@SarStep.setter
def SarStep(self, value):
self._sar_step.Value = value
@property
def SarMax(self):
return self._sar_max.Value
@SarMax.setter
def SarMax(self, value):
self._sar_max.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(ma_sar_adx_bind_strategy, self).OnStarted2(time)
self._previous_high = None
self._previous_low = None
self._previous_close = None
self._smoothed_plus_dm = 0.0
self._smoothed_minus_dm = 0.0
self._smoothed_true_range = 0.0
self._adx_samples = 0
ma = SimpleMovingAverage()
ma.Length = self.MaPeriod
sar = ParabolicSar()
sar.Acceleration = float(self.SarStep)
sar.AccelerationStep = float(self.SarStep)
sar.AccelerationMax = float(self.SarMax)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ma, sar, self.ProcessCandle).Start()
def ProcessCandle(self, candle, moving_average, sar):
if candle.State != CandleStates.Finished:
return
ma_val = float(moving_average)
sar_val = float(sar)
plus_di, minus_di, is_ready = self._update_directional_movement(candle)
if not is_ready:
return
close = float(candle.ClosePrice)
# Exit conditions
if self.Position > 0 and close < sar_val:
self.SellMarket()
return
if self.Position < 0 and close > sar_val:
self.BuyMarket()
return
# Entry conditions
bullish_signal = close > ma_val and plus_di >= minus_di and close > sar_val
bearish_signal = close < ma_val and plus_di <= minus_di and close < sar_val
if bullish_signal and self.Position <= 0:
self.BuyMarket()
return
if bearish_signal and self.Position >= 0:
self.SellMarket()
def _update_directional_movement(self, candle):
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if self._previous_high is None or self._previous_low is None or self._previous_close is None:
self._previous_high = high
self._previous_low = low
self._previous_close = close
return (0.0, 0.0, False)
up_move = high - self._previous_high
down_move = self._previous_low - low
plus_dm = up_move if (up_move > down_move and up_move > 0.0) else 0.0
minus_dm = down_move if (down_move > up_move and down_move > 0.0) else 0.0
true_range = max(high - low, max(abs(high - self._previous_close), abs(low - self._previous_close)))
adx_period = int(self.AdxPeriod)
if self._adx_samples < adx_period:
self._smoothed_plus_dm += plus_dm
self._smoothed_minus_dm += minus_dm
self._smoothed_true_range += true_range
self._adx_samples += 1
else:
self._smoothed_plus_dm = self._smoothed_plus_dm - (self._smoothed_plus_dm / adx_period) + plus_dm
self._smoothed_minus_dm = self._smoothed_minus_dm - (self._smoothed_minus_dm / adx_period) + minus_dm
self._smoothed_true_range = self._smoothed_true_range - (self._smoothed_true_range / adx_period) + true_range
self._previous_high = high
self._previous_low = low
self._previous_close = close
if self._adx_samples < adx_period or self._smoothed_true_range <= 0.0:
return (0.0, 0.0, False)
return (
100.0 * self._smoothed_plus_dm / self._smoothed_true_range,
100.0 * self._smoothed_minus_dm / self._smoothed_true_range,
True)
def OnReseted(self):
super(ma_sar_adx_bind_strategy, self).OnReseted()
self._previous_high = None
self._previous_low = None
self._previous_close = None
self._smoothed_plus_dm = 0.0
self._smoothed_minus_dm = 0.0
self._smoothed_true_range = 0.0
self._adx_samples = 0
def CreateClone(self):
return ma_sar_adx_bind_strategy()