PosNegDiCrossoverStrategy
概述
PosNegDiCrossoverStrategy 是 MetaTrader 指标 _HPCS_PosNegDIsCrossOver_Mt4_EA_V01_WE 的 StockSharp 版本。原始 EA 根据 ADX
指标的 +DI 与 -DI 交叉来开仓,并为每笔交易设置对称的止盈/止损(以点数表示)。若发生亏损,会按照固定倍数放大手数再次进场,直至获利或达到指定的马丁格尔次数上限。
交易逻辑
- 信号识别:当新的完整 K 线到来时,策略获取最新的 ADX 值,并与上一根 K 线的 +DI/-DI 比较;若 +DI 从下向上穿越 -DI 触发做多,若 +DI 从上向下跌破 -DI 触发做空。为了复现 MQL 中的去重保护,每根 K 线仅允许一次初始入场。
- 时间过滤:只有在
StartTime与StopTime所限定的交易时段内才允许开仓。时段之外,策略仍会跟踪已有仓位的虚拟止盈/止损,但不会启动新的交易循环或继续马丁格尔加仓。 - 下单与转换:触发信号后按照
OrderVolume发送市价单。成交后,将TakeProfitPips、StopLossPips按照标的物的最小变动价位转换为绝对价格(若报价有 3 或 5 位小数,会乘以 10),并保存为后续平仓判定的价格。 - 止盈止损处理:每根完整 K 线都会检查价格区间。对于多头,当最低价触及止损或最高价触及止盈时,以市价单平仓;空头使用对称条件。这样可以在平仓后立即判断交易结果。
- 马丁格尔循环:若上一笔交易亏损,则将手数乘以
MartingaleMultiplier并立即按原方向再次入场(仍需满足时间过滤)。一旦达到MartingaleCycleLimit次或出现盈利平仓,循环被重置,等待下一次 ADX 交叉。
参数
| 名称 | 默认值 | 说明 |
|---|---|---|
CandleType |
15 分钟时间框 | 用于计算 ADX 及监控止盈/止损的 K 线类型。 |
AdxPeriod |
14 | ADX 指标的周期长度。 |
UseTimeFilter |
true |
是否启用交易时间过滤。 |
StartTime |
00:00 | 允许开仓的开始时间(交易所时间)。 |
StopTime |
23:59 | 允许开仓的结束时间(交易所时间)。 |
OrderVolume |
0.1 | 初始市价单的交易手数。 |
TakeProfitPips |
10 | 止盈距离(点数),转换成价格后用于虚拟止盈。 |
StopLossPips |
10 | 止损距离(点数),转换成价格后用于虚拟止损。 |
MartingaleMultiplier |
2 | 马丁格尔加仓时的手数倍增系数。 |
MartingaleCycleLimit |
5 | 每个信号允许的最大马丁格尔次数。 |
说明
- 策略在下单前会调用
IsFormedAndOnlineAndAllowTrading(),确保所有订阅与风控状态已经准备就绪。 - 止盈/止损采用“虚拟”方式,模仿 MetaTrader 将保护单直接挂在持仓上的行为,同时保持对 StockSharp 高阶 API 的兼容。
- 如果将
StartTime与StopTime设置为相同的时间,或关闭UseTimeFilter,则策略会像原 EA 一样在全天候运行。
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>
/// Port of the MetaTrader expert "_HPCS_PosNegDIsCrossOver_Mt4_EA_V01_WE".
/// Trades +DI/-DI crossovers of the ADX indicator and applies a martingale re-entry loop after losing trades.
/// </summary>
public class PosNegDiCrossoverStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<bool> _useTimeFilter;
private readonly StrategyParam<TimeSpan> _startTime;
private readonly StrategyParam<TimeSpan> _stopTime;
private readonly StrategyParam<decimal> _orderVolume;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _martingaleMultiplier;
private readonly StrategyParam<int> _martingaleCycleLimit;
private decimal _previousPlusDi;
private decimal _previousMinusDi;
private bool _diInitialized;
private bool _cycleActive;
private Sides? _cycleSide;
private decimal _currentVolume;
private int _currentCycle;
private decimal? _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
private bool _awaitingCycleResolution;
private bool _lastExitWasLoss;
private DateTimeOffset? _lastSignalTime;
/// <summary>
/// Initializes a new instance of the <see cref="PosNegDiCrossoverStrategy"/> class.
/// </summary>
public PosNegDiCrossoverStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for indicator calculations", "General");
_adxPeriod = Param(nameof(AdxPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ADX Period", "Length of the Average Directional Index", "Indicators")
.SetOptimize(7, 50, 1);
_useTimeFilter = Param(nameof(UseTimeFilter), true)
.SetDisplay("Use Time Filter", "Restrict entries to a daily time window", "Schedule");
_startTime = Param(nameof(StartTime), new TimeSpan(0, 0, 0))
.SetDisplay("Start Time", "Daily time when trading becomes available", "Schedule");
_stopTime = Param(nameof(StopTime), new TimeSpan(23, 59, 0))
.SetDisplay("Stop Time", "Daily time after which new entries are blocked", "Schedule");
_orderVolume = Param(nameof(OrderVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Order Volume", "Baseline market order volume", "Trading");
_takeProfitPips = Param(nameof(TakeProfitPips), 10m)
.SetNotNegative()
.SetDisplay("Take-Profit (pips)", "Distance to the profit target expressed in pips", "Risk");
_stopLossPips = Param(nameof(StopLossPips), 10m)
.SetNotNegative()
.SetDisplay("Stop-Loss (pips)", "Distance to the protective stop expressed in pips", "Risk");
_martingaleMultiplier = Param(nameof(MartingaleMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Martingale Multiplier", "Volume multiplier applied after a loss", "Money Management");
_martingaleCycleLimit = Param(nameof(MartingaleCycleLimit), 5)
.SetGreaterThanZero()
.SetDisplay("Martingale Cycle Limit", "Maximum number of martingale steps per signal", "Money Management");
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Period of the Average Directional Index indicator.
/// </summary>
public int AdxPeriod
{
get => _adxPeriod.Value;
set => _adxPeriod.Value = value;
}
/// <summary>
/// Enable or disable the trading time window.
/// </summary>
public bool UseTimeFilter
{
get => _useTimeFilter.Value;
set => _useTimeFilter.Value = value;
}
/// <summary>
/// Daily start time of the trading window.
/// </summary>
public TimeSpan StartTime
{
get => _startTime.Value;
set => _startTime.Value = value;
}
/// <summary>
/// Daily end time of the trading window.
/// </summary>
public TimeSpan StopTime
{
get => _stopTime.Value;
set => _stopTime.Value = value;
}
/// <summary>
/// Base market order volume used to open a new cycle.
/// </summary>
public decimal OrderVolume
{
get => _orderVolume.Value;
set => _orderVolume.Value = value;
}
/// <summary>
/// Take-profit distance expressed in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Volume multiplier applied after a losing trade.
/// </summary>
public decimal MartingaleMultiplier
{
get => _martingaleMultiplier.Value;
set => _martingaleMultiplier.Value = value;
}
/// <summary>
/// Maximum number of martingale steps executed per signal.
/// </summary>
public int MartingaleCycleLimit
{
get => _martingaleCycleLimit.Value;
set => _martingaleCycleLimit.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_diInitialized = false;
_previousPlusDi = 0m;
_previousMinusDi = 0m;
ResetCycle();
_lastSignalTime = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetCycle();
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);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue adxValue)
{
if (candle.State != CandleStates.Finished)
{
return;
}
HandleOpenPosition(candle);
if (!IsFormedAndOnlineAndAllowTrading())
{
return;
}
var value = (AverageDirectionalIndexValue)adxValue;
if (value.Dx.Plus is not decimal plusDi || value.Dx.Minus is not decimal minusDi)
{
return;
}
if (!_diInitialized)
{
_previousPlusDi = plusDi;
_previousMinusDi = minusDi;
_diInitialized = true;
return;
}
var bullishCross = plusDi > minusDi && _previousPlusDi <= _previousMinusDi;
var bearishCross = plusDi < minusDi && _previousPlusDi >= _previousMinusDi;
var time = candle.CloseTime;
var withinWindow = !UseTimeFilter || IsWithinTradingWindow(time.TimeOfDay);
if (withinWindow && !_cycleActive && !_awaitingCycleResolution)
{
if (bullishCross && Position <= 0m && !IsSameSignalBar(candle.OpenTime))
{
StartNewCycle(Sides.Buy);
_lastSignalTime = candle.OpenTime;
}
else if (bearishCross && Position >= 0m && !IsSameSignalBar(candle.OpenTime))
{
StartNewCycle(Sides.Sell);
_lastSignalTime = candle.OpenTime;
}
}
_previousPlusDi = plusDi;
_previousMinusDi = minusDi;
}
private void HandleOpenPosition(ICandleMessage candle)
{
if (Position > 0m)
{
if (_awaitingCycleResolution)
{
return;
}
var exitVolume = Math.Abs(Position);
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
// Long stop-loss reached inside the finished bar range.
ExecuteExit(Sides.Sell, exitVolume, true);
return;
}
if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
{
// Long take-profit reached.
ExecuteExit(Sides.Sell, exitVolume, false);
}
}
else if (Position < 0m)
{
if (_awaitingCycleResolution)
{
return;
}
var exitVolume = Math.Abs(Position);
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
// Short stop-loss reached inside the finished bar range.
ExecuteExit(Sides.Buy, exitVolume, true);
return;
}
if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
{
// Short take-profit reached.
ExecuteExit(Sides.Buy, exitVolume, false);
}
}
}
private void ExecuteExit(Sides exitSide, decimal volume, bool isLoss)
{
if (volume <= 0m)
{
return;
}
if (exitSide == Sides.Buy)
{
BuyMarket(volume);
}
else
{
SellMarket(volume);
}
_stopPrice = null;
_takePrice = null;
_entryPrice = null;
_awaitingCycleResolution = true;
_lastExitWasLoss = isLoss;
}
private void StartNewCycle(Sides side)
{
var volume = OrderVolume;
if (volume <= 0m)
{
return;
}
_cycleActive = true;
_cycleSide = side;
_currentCycle = 1;
_currentVolume = volume;
_entryPrice = null;
_stopPrice = null;
_takePrice = null;
_awaitingCycleResolution = false;
_lastExitWasLoss = false;
if (side == Sides.Buy)
{
BuyMarket(volume);
}
else
{
SellMarket(volume);
}
}
private void ContinueMartingale()
{
if (_cycleSide is not Sides side)
{
ResetCycle();
return;
}
var volume = _currentVolume;
if (volume <= 0m)
{
ResetCycle();
return;
}
if (UseTimeFilter && !IsWithinTradingWindow(CurrentTime.TimeOfDay))
{
ResetCycle();
return;
}
_entryPrice = null;
_stopPrice = null;
_takePrice = null;
_awaitingCycleResolution = false;
_lastExitWasLoss = false;
if (side == Sides.Buy)
{
BuyMarket(volume);
}
else
{
SellMarket(volume);
}
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade.Order.Security != Security)
{
return;
}
if (_cycleSide is not Sides side)
{
return;
}
var direction = trade.Order.Side;
if ((side == Sides.Buy && direction != Sides.Buy) || (side == Sides.Sell && direction != Sides.Sell))
{
return;
}
// Store the most recent entry price to recalculate protective levels.
_entryPrice = trade.Order.AveragePrice ?? trade.Trade.Price;
UpdateProtectionLevels();
}
/// <inheritdoc />
protected override void OnPositionReceived(Position position)
{
base.OnPositionReceived(position);
if (Position != 0m)
{
return;
}
if (_awaitingCycleResolution)
{
if (_lastExitWasLoss && _cycleActive && _currentCycle < MartingaleCycleLimit)
{
_currentCycle++;
_currentVolume *= MartingaleMultiplier;
ContinueMartingale();
}
else
{
ResetCycle();
}
_awaitingCycleResolution = false;
_lastExitWasLoss = false;
}
else if (_cycleActive)
{
// Position was closed externally; stop the martingale loop.
ResetCycle();
}
}
private void UpdateProtectionLevels()
{
if (_entryPrice is not decimal entry || _cycleSide is not Sides side)
{
return;
}
var pip = GetPipSize();
if (pip <= 0m)
{
return;
}
_stopPrice = StopLossPips > 0m
? side == Sides.Buy ? entry - StopLossPips * pip : entry + StopLossPips * pip
: null;
_takePrice = TakeProfitPips > 0m
? side == Sides.Buy ? entry + TakeProfitPips * pip : entry - TakeProfitPips * pip
: null;
}
private decimal GetPipSize()
{
var security = Security;
if (security == null)
{
return 0.0001m;
}
var step = security.PriceStep ?? 0.0001m;
if (step <= 0m)
{
step = 0.0001m;
}
var decimals = security.Decimals;
return decimals is 3 or 5 ? step * 10m : step;
}
private bool IsWithinTradingWindow(TimeSpan timeOfDay)
{
var start = StartTime;
var stop = StopTime;
if (start == stop)
{
return true;
}
return start <= stop
? timeOfDay >= start && timeOfDay <= stop
: timeOfDay >= start || timeOfDay <= stop;
}
private bool IsSameSignalBar(DateTimeOffset candleOpenTime)
{
return _lastSignalTime != null && _lastSignalTime.Value == candleOpenTime;
}
private void ResetCycle()
{
_cycleActive = false;
_cycleSide = null;
_currentVolume = OrderVolume;
_currentCycle = 0;
_entryPrice = null;
_stopPrice = null;
_takePrice = null;
_awaitingCycleResolution = false;
_lastExitWasLoss = false;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import AverageDirectionalIndex
from StockSharp.Algo.Strategies import Strategy
class pos_neg_di_crossover_strategy(Strategy):
def __init__(self):
super(pos_neg_di_crossover_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15)))
self._adx_period = self.Param("AdxPeriod", 14)
self._stop_loss_pct = self.Param("StopLossPct", 2.0)
self._take_profit_pct = self.Param("TakeProfitPct", 3.0)
self._prev_plus_di = 0.0
self._prev_minus_di = 0.0
self._di_initialized = False
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def AdxPeriod(self):
return self._adx_period.Value
@AdxPeriod.setter
def AdxPeriod(self, value):
self._adx_period.Value = value
@property
def StopLossPct(self):
return self._stop_loss_pct.Value
@StopLossPct.setter
def StopLossPct(self, value):
self._stop_loss_pct.Value = value
@property
def TakeProfitPct(self):
return self._take_profit_pct.Value
@TakeProfitPct.setter
def TakeProfitPct(self, value):
self._take_profit_pct.Value = value
def OnReseted(self):
super(pos_neg_di_crossover_strategy, self).OnReseted()
self._prev_plus_di = 0.0
self._prev_minus_di = 0.0
self._di_initialized = False
self._entry_price = 0.0
def OnStarted2(self, time):
super(pos_neg_di_crossover_strategy, self).OnStarted2(time)
self._prev_plus_di = 0.0
self._prev_minus_di = 0.0
self._di_initialized = False
self._entry_price = 0.0
adx = AverageDirectionalIndex()
adx.Length = self.AdxPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(adx, self._process_candle).Start()
def _process_candle(self, candle, adx_value):
if not adx_value.IsFinal:
return
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
plus_di = adx_value.Dx.Plus
minus_di = adx_value.Dx.Minus
if plus_di is None or minus_di is None:
return
plus_di_val = float(plus_di)
minus_di_val = float(minus_di)
# Check SL/TP on existing position
if self.Position != 0 and self._entry_price > 0:
if self.Position > 0:
pnl_pct = (close - self._entry_price) / self._entry_price * 100.0
if pnl_pct <= -float(self.StopLossPct) or pnl_pct >= float(self.TakeProfitPct):
self.SellMarket()
self._entry_price = 0.0
self._prev_plus_di = plus_di_val
self._prev_minus_di = minus_di_val
return
elif self.Position < 0:
pnl_pct = (self._entry_price - close) / self._entry_price * 100.0
if pnl_pct <= -float(self.StopLossPct) or pnl_pct >= float(self.TakeProfitPct):
self.BuyMarket()
self._entry_price = 0.0
self._prev_plus_di = plus_di_val
self._prev_minus_di = minus_di_val
return
if not self._di_initialized:
self._prev_plus_di = plus_di_val
self._prev_minus_di = minus_di_val
self._di_initialized = True
return
bullish_cross = plus_di_val > minus_di_val and self._prev_plus_di <= self._prev_minus_di
bearish_cross = plus_di_val < minus_di_val and self._prev_plus_di >= self._prev_minus_di
if bullish_cross and self.Position <= 0:
self.BuyMarket()
self._entry_price = close
elif bearish_cross and self.Position >= 0:
self.SellMarket()
self._entry_price = close
self._prev_plus_di = plus_di_val
self._prev_minus_di = minus_di_val
def CreateClone(self):
return pos_neg_di_crossover_strategy()