在 GitHub 上查看

DeMarker Pending 2 策略

概述

该策略使用 StockSharp 高级 API 复刻 MetaTrader 专家顾问“DeMarker Pending 2”的核心思路。系统在设定的时间框架上计算 DeMarker 振荡指标,并在指标穿越自定义阈值时准备挂单买入或卖出。下单方式可选择止损单或限价单,并在当前市价基础上加入额外偏移。策略还提供交易时段过滤、价差限制以及距离控制,用于管理入场条件。

交易逻辑

  1. 订阅所需的K线序列,并以给定周期计算 DeMarker 指标。
  2. 当上一笔指标值高于下轨且当前值跌破下轨时,排队准备多头挂单;当上一笔指标值低于上轨且当前值突破上轨时,排队准备空头挂单。每根K线只处理一个信号。
  3. 挂单按照“偏移点数”转换成价格,支持止损单或限价单。若启用替换选项,则在发送新挂单前取消旧单。策略限制“持仓 + 挂单”的总数量,并检查新的挂单价与当前持仓均价之间的最小距离。
  4. 多空头寸可启用止损、止盈和跟踪止损。防护价格以点数计算,并在每根收盘K线上检测。当利润达到激活条件并再获得额外的跟踪步长后,跟踪止损会向有利方向移动。
  5. 如果当前最优买卖价价差超过阈值,则跳过下单。可选的交易时段过滤器会在非交易时间禁止新挂单。

参数

名称 说明
Working Candles 用于生成信号和检查风控的时间框架。
Order Volume 挂单的默认下单量。
Stop Loss (pts) 止损距离,单位为价格点。
Take Profit (pts) 止盈距离,单位为价格点。
Trailing Activate (pts) 启动跟踪止损所需的盈利点数。
Trailing Stop (pts) 跟踪止损与当前价格之间的距离。
Trailing Step (pts) 每次移动跟踪止损所需的额外盈利。
Trail On Close 若启用,仅在K线收盘后更新跟踪止损。
Max Positions 持仓与挂单的最大合计数量,0 表示不限制。
Min Distance (pts) 新挂单价格与当前持仓均价之间的最小距离。
Use Stop Orders 为 true 时使用止损挂单,否则使用限价挂单。
Single Pending 只允许存在一张活动挂单。
Replace Pendings 发送新挂单前先取消旧挂单。
Pending Offset (pts) 相对于当前市价的挂单偏移点数。
Max Spread (pts) 允许的最大买卖价差,超出则放弃下单。
Use Session Filter 是否启用交易时段过滤。
Start Hour/Minute, End Hour/Minute 启用交易窗口后使用的起止时间。
DeMarker Period DeMarker 指标的平均周期。
Upper Level 触发做空信号的阈值。
Lower Level 触发做多信号的阈值。

说明

  • 原始 EA 中的挂单到期时间与风险资金管理未被移植,此处使用固定下单量。
  • 止损与止盈在K线收盘时通过最高价/最低价判定,可能与 MetaTrader 中的盘中执行存在差异。
  • 跟踪止损仅在K线收盘时计算,未实现逐笔行情的实时移动。
  • 挂单依赖行情源提供的最优买卖价,请确保 Level1 行情订阅可用。
namespace StockSharp.Samples.Strategies;

using System;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>
/// Pending order strategy driven by the DeMarker oscillator.
/// Simplified from "DeMarker Pending 2" to use market orders on threshold crossovers.
/// </summary>
public class DeMarkerPending2Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _demarkerPeriod;
	private readonly StrategyParam<decimal> _demarkerUpperLevel;
	private readonly StrategyParam<decimal> _demarkerLowerLevel;

	private RelativeStrengthIndex _rsi;
	private decimal? _prevOscillator;

	/// <summary>
	/// Candle type for calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// DeMarker oscillator period.
	/// </summary>
	public int DemarkerPeriod
	{
		get => _demarkerPeriod.Value;
		set => _demarkerPeriod.Value = value;
	}

	/// <summary>
	/// Upper DeMarker threshold for sell signal.
	/// </summary>
	public decimal DemarkerUpperLevel
	{
		get => _demarkerUpperLevel.Value;
		set => _demarkerUpperLevel.Value = value;
	}

	/// <summary>
	/// Lower DeMarker threshold for buy signal.
	/// </summary>
	public decimal DemarkerLowerLevel
	{
		get => _demarkerLowerLevel.Value;
		set => _demarkerLowerLevel.Value = value;
	}

	public DeMarkerPending2Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for calculations", "General");

		_demarkerPeriod = Param(nameof(DemarkerPeriod), 14)
			.SetDisplay("DeMarker Period", "DeMarker oscillator period", "Indicator")
			.SetGreaterThanZero();

		_demarkerUpperLevel = Param(nameof(DemarkerUpperLevel), 0.7m)
			.SetDisplay("Upper Level", "Overbought threshold", "Indicator");

		_demarkerLowerLevel = Param(nameof(DemarkerLowerLevel), 0.3m)
			.SetDisplay("Lower Level", "Oversold threshold", "Indicator");
	}

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

		_prevOscillator = null;
		_rsi = new RelativeStrengthIndex { Length = DemarkerPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_rsi, ProcessCandle)
			.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal rsiValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_rsi.IsFormed)
		{
			_prevOscillator = rsiValue / 100m;
			return;
		}

		if (_prevOscillator is not decimal prev)
		{
			_prevOscillator = rsiValue / 100m;
			return;
		}

		var oscillatorValue = rsiValue / 100m;
		var volume = Volume;
		if (volume <= 0)
			volume = 1;

		// Cross above lower level from below => buy
		if (prev < DemarkerLowerLevel && oscillatorValue >= DemarkerLowerLevel)
		{
			if (Position <= 0)
				BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
		}
		// Cross below upper level from above => sell
		else if (prev > DemarkerUpperLevel && oscillatorValue <= DemarkerUpperLevel)
		{
			if (Position >= 0)
				SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
		}

		_prevOscillator = oscillatorValue;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_rsi = null;
		_prevOscillator = null;

		base.OnReseted();
	}
}