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 逻辑一致。 InitializeLongTargets与InitializeShortTargets在下单后立刻根据最新确认收盘价估算入场价,并计算对应的绝对止盈/止损价位。- 当启用追踪止损且浮动盈利超过设定距离时,止损价位会沿趋势方向移动以锁定盈利。
- 每次仓位被关闭后,所有目标和止损数值都会重置,避免复用过期信息。
参数
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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import SmoothedMovingAverage, AverageDirectionalIndex
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class adx_ma_crossover_strategy(Strategy):
def __init__(self):
super(adx_ma_crossover_strategy, self).__init__()
self._ma_period = self.Param("MaPeriod", 15)
self._adx_period = self.Param("AdxPeriod", 12)
self._adx_threshold = self.Param("AdxThreshold", 25.0)
self._take_profit_buy = self.Param("TakeProfitBuy", 83.0)
self._stop_loss_buy = self.Param("StopLossBuy", 55.0)
self._trailing_stop_buy = self.Param("TrailingStopBuy", 27.0)
self._take_profit_sell = self.Param("TakeProfitSell", 63.0)
self._stop_loss_sell = self.Param("StopLossSell", 50.0)
self._trailing_stop_sell = self.Param("TrailingStopSell", 27.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._pip_size = 1.0
self._prev_close = 0.0
self._prev_prev_close = 0.0
self._prev_ma = 0.0
self._prev_adx = 0.0
self._has_prev = False
self._has_prev_prev = False
self._long_entry = 0.0
self._long_stop = 0.0
self._long_tp = 0.0
self._short_entry = 0.0
self._short_stop = 0.0
self._short_tp = 0.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 AdxThreshold(self):
return self._adx_threshold.Value
@AdxThreshold.setter
def AdxThreshold(self, value):
self._adx_threshold.Value = value
@property
def TakeProfitBuy(self):
return self._take_profit_buy.Value
@TakeProfitBuy.setter
def TakeProfitBuy(self, value):
self._take_profit_buy.Value = value
@property
def StopLossBuy(self):
return self._stop_loss_buy.Value
@StopLossBuy.setter
def StopLossBuy(self, value):
self._stop_loss_buy.Value = value
@property
def TrailingStopBuy(self):
return self._trailing_stop_buy.Value
@TrailingStopBuy.setter
def TrailingStopBuy(self, value):
self._trailing_stop_buy.Value = value
@property
def TakeProfitSell(self):
return self._take_profit_sell.Value
@TakeProfitSell.setter
def TakeProfitSell(self, value):
self._take_profit_sell.Value = value
@property
def StopLossSell(self):
return self._stop_loss_sell.Value
@StopLossSell.setter
def StopLossSell(self, value):
self._stop_loss_sell.Value = value
@property
def TrailingStopSell(self):
return self._trailing_stop_sell.Value
@TrailingStopSell.setter
def TrailingStopSell(self, value):
self._trailing_stop_sell.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(adx_ma_crossover_strategy, self).OnStarted2(time)
self._pip_size = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if self._pip_size <= 0.0:
self._pip_size = 1.0
self._prev_close = 0.0
self._prev_prev_close = 0.0
self._prev_ma = 0.0
self._prev_adx = 0.0
self._has_prev = False
self._has_prev_prev = False
self._reset_long_targets()
self._reset_short_targets()
self._ma = SmoothedMovingAverage()
self._ma.Length = self.MaPeriod
self._adx = AverageDirectionalIndex()
self._adx.Length = self.AdxPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(self._adx, self.ProcessCandle).Start()
self.StartProtection(
Unit(2000.0, UnitTypes.Absolute),
Unit(1000.0, UnitTypes.Absolute))
def ProcessCandle(self, candle, adx_value):
if candle.State != CandleStates.Finished:
return
if not adx_value.IsFinal:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
median = (high + low) / 2.0
ma_result = process_float(self._ma, Decimal(median), candle.OpenTime, True)
if not ma_result.IsFinal:
return
ma_val = float(ma_result)
adx_typed = adx_value
adx_ma = adx_typed.MovingAverage
adx_val = float(adx_ma) if adx_ma is not None else 0.0
if self._has_prev and self._has_prev_prev:
self._manage_open_positions(close)
long_signal = self._prev_close > self._prev_ma and self._prev_prev_close < self._prev_ma and self._prev_adx >= float(self.AdxThreshold)
short_signal = self._prev_close < self._prev_ma and self._prev_prev_close > self._prev_ma and self._prev_adx >= float(self.AdxThreshold)
if long_signal and self.Position <= 0:
self.BuyMarket()
self._init_long_targets(self._prev_close)
elif short_signal and self.Position >= 0:
self.SellMarket()
self._init_short_targets(self._prev_close)
self._update_history(close, ma_val, adx_val)
def _manage_open_positions(self, current_close):
if self.Position > 0:
if self._prev_close < self._prev_ma:
self.SellMarket()
self._reset_long_targets()
return
self._update_long_trailing(current_close)
if self._long_tp > 0.0 and current_close >= self._long_tp:
self.SellMarket()
self._reset_long_targets()
return
if self._long_stop > 0.0 and current_close <= self._long_stop:
self.SellMarket()
self._reset_long_targets()
return
elif self.Position < 0:
if self._prev_close > self._prev_ma:
self.BuyMarket()
self._reset_short_targets()
return
self._update_short_trailing(current_close)
if self._short_tp > 0.0 and current_close <= self._short_tp:
self.BuyMarket()
self._reset_short_targets()
return
if self._short_stop > 0.0 and current_close >= self._short_stop:
self.BuyMarket()
self._reset_short_targets()
return
else:
self._reset_long_targets()
self._reset_short_targets()
def _update_long_trailing(self, current_close):
trail_buy = float(self.TrailingStopBuy)
if trail_buy <= 0.0 or self._long_entry <= 0.0:
return
trail_dist = trail_buy * self._pip_size
if trail_dist <= 0.0:
return
profit = current_close - self._long_entry
if profit <= trail_dist:
return
new_stop = current_close - trail_dist
if new_stop > self._long_stop:
self._long_stop = new_stop
def _update_short_trailing(self, current_close):
trail_sell = float(self.TrailingStopSell)
if trail_sell <= 0.0 or self._short_entry <= 0.0:
return
trail_dist = trail_sell * self._pip_size
if trail_dist <= 0.0:
return
profit = self._short_entry - current_close
if profit <= trail_dist:
return
new_stop = current_close + trail_dist
if self._short_stop == 0.0 or new_stop < self._short_stop:
self._short_stop = new_stop
def _init_long_targets(self, entry):
self._long_entry = entry
sl_buy = float(self.StopLossBuy)
tp_buy = float(self.TakeProfitBuy)
self._long_stop = entry - sl_buy * self._pip_size if sl_buy > 0.0 else 0.0
self._long_tp = entry + tp_buy * self._pip_size if tp_buy > 0.0 else 0.0
self._reset_short_targets()
def _init_short_targets(self, entry):
self._short_entry = entry
sl_sell = float(self.StopLossSell)
tp_sell = float(self.TakeProfitSell)
self._short_stop = entry + sl_sell * self._pip_size if sl_sell > 0.0 else 0.0
self._short_tp = entry - tp_sell * self._pip_size if tp_sell > 0.0 else 0.0
self._reset_long_targets()
def _reset_long_targets(self):
self._long_entry = 0.0
self._long_stop = 0.0
self._long_tp = 0.0
def _reset_short_targets(self):
self._short_entry = 0.0
self._short_stop = 0.0
self._short_tp = 0.0
def _update_history(self, close, ma, adx):
if self._has_prev:
self._prev_prev_close = self._prev_close
self._has_prev_prev = True
self._prev_close = close
self._prev_ma = ma
self._prev_adx = adx
self._has_prev = True
def OnReseted(self):
super(adx_ma_crossover_strategy, self).OnReseted()
self._pip_size = 1.0
self._prev_close = 0.0
self._prev_prev_close = 0.0
self._prev_ma = 0.0
self._prev_adx = 0.0
self._has_prev = False
self._has_prev_prev = False
self._reset_long_targets()
self._reset_short_targets()
def CreateClone(self):
return adx_ma_crossover_strategy()