在 GitHub 上查看

Rapid Doji 策略

概述

Rapid Doji 策略忠实重现原始 “Rapid Doji EA” 专家顾问的逻辑。策略默认在日线级别上扫描已完成的 K 线,一旦识别到十字星(Doji),就会在最高价和最低价附近挂入场止损单。止损价格基于 Average True Range (ATR) 指标的倍数计算,仓位获利后再通过固定点差的跟踪止损来锁定利润并限制回撤。

交易流程

  1. 数据订阅:订阅所选时间框架的收盘 K 线,并维护可配置周期的 ATR 指标。
  2. 十字星识别:当 K 线实体长度不超过整个波幅的 3% 时,将其视为十字星,只处理已经收盘的 K 线。
  3. 挂单布局
    • 在十字星的最高价放置 Buy Stop;
    • 在十字星的最低价放置 Sell Stop;
    • 同时记录对应的保护性止损价 = 对侧极值 ± ATR × 系数。
  4. 风险控制:当其中一笔挂单触发建仓后,立即撤销另一笔挂单,并用预先记录的价格下达保护性止损单,之后交由跟踪逻辑维护。
  5. 跟踪止损:每根新 K 线收盘时,根据最新收盘价重新计算止损,使其与价格保持固定的点差距离(使用合约的最小变动价位换算),仅在仓位已经盈利时才移动。

策略不设定任何止盈目标,平仓完全依赖保护性或跟踪止损,或人工干预。

参数

参数 说明
CandleType 用于识别形态的 K 线类型,默认是日线。
AtrPeriod ATR 指标的计算周期。
AtrMultiplier 计算止损时使用的 ATR 倍数。
TrailingDistancePoints 跟踪止损的固定点数距离。

所有参数都可以在 StockSharp 环境中进行优化。

实现要点

  • 使用高层次的 SubscribeCandles API,并通过 Bind 将 ATR 指标与行情绑定,无需手动加载历史数据。
  • 通过 Security.ShrinkPrice 对价格进行标准化,确保符合交易所的最小价格步长。
  • 保护性止损的放置与移动均由代码显式控制,以贴近原始 MQL5 版本的行为。
  • 根据任务要求,本项目暂未提供 Python 版本。
namespace StockSharp.Samples.Strategies;

using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// Rapid Doji strategy: detects doji candles and trades the breakout direction.
/// Buys on next candle if it closes above doji high, sells if below doji low.
/// Uses ATR for volatility confirmation.
/// </summary>
public class RapidDojiStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _dojiThreshold;
	private readonly StrategyParam<int> _signalCooldownCandles;

	private decimal _prevHigh;
	private decimal _prevLow;
	private bool _prevWasDoji;
	private int _candlesSinceTrade;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
	public decimal DojiThreshold { get => _dojiThreshold.Value; set => _dojiThreshold.Value = value; }
	public int SignalCooldownCandles { get => _signalCooldownCandles.Value; set => _signalCooldownCandles.Value = value; }

	public RapidDojiStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR period for volatility filter", "Indicators");
		_dojiThreshold = Param(nameof(DojiThreshold), 0.15m)
			.SetDisplay("Doji Threshold", "Max body/range ratio for doji detection", "Pattern");
		_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 6)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait between breakouts", "Trading");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevHigh = 0m;
		_prevLow = 0m;
		_prevWasDoji = false;
		_candlesSinceTrade = SignalCooldownCandles;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_prevWasDoji = false;
		_candlesSinceTrade = SignalCooldownCandles;
		var atr = new AverageTrueRange { Length = AtrPeriod };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(atr, ProcessCandle).Start();
	}

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

		if (_candlesSinceTrade < SignalCooldownCandles)
			_candlesSinceTrade++;

		if (_prevWasDoji && atr > 0 && _candlesSinceTrade >= SignalCooldownCandles)
		{
			var close = candle.ClosePrice;
			if (close > _prevHigh + atr * 0.2m && Position <= 0)
			{
				BuyMarket();
				_candlesSinceTrade = 0;
			}
			else if (close < _prevLow - atr * 0.2m && Position >= 0)
			{
				SellMarket();
				_candlesSinceTrade = 0;
			}
		}

		var range = candle.HighPrice - candle.LowPrice;
		var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);
		_prevWasDoji = range > 0 && body <= DojiThreshold * range;
		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
	}
}