在 GitHub 上查看

Doji Arrows 策略

概述

Doji Arrows Strategy 是将 MetaTrader 指标 Doji_arrows_expert1.mq4 移植到 StockSharp 的实现。策略思想是在市场出现中性十字星后,跟随下一根 K 线的方向性突破。当价格先形成接近开收价相等的十字星,再由下一根 K 线收盘突破十字星的高点或低点时,系统认为行情出现动量突破并顺势入场。

交易逻辑

  • 信号窗口:策略始终缓存最近两根已完成的 K 线。较早的一根必须满足十字星条件,后一根用于确认突破方向。
  • 十字星判定:若 |Open - Close| ≤ DojiBodyThresholdSteps * PriceStep,则判定该 K 线为十字星。默认阈值为 1 个最小报价单位,容许一跳的误差。
  • 突破确认
    • 多头信号:第二根 K 线的收盘价高于十字星最高价加上 BreakoutBufferSteps 设定的缓冲。
    • 空头信号:第二根 K 线的收盘价低于十字星最低价减去相同的缓冲。
  • 单次触发:策略会记录上一根 K 线是否已经触发信号,只有在出现新的突破时才会再次下单,这与原版指标只画一次箭头的方式一致。
  • 下单方式
    • 若出现与当前仓位相反的信号,策略会先平掉旧仓位,再以 Volume + |Position| 的手数在新方向建立头寸,实现快速翻仓。
    • 在空仓状态下直接按突破方向发出市价单。

风险管理

  • 初始止损:开仓后立即在距离入场价 InitialStopSteps * PriceStep 的位置记录内部止损线。
  • 固定止盈:当价格到达 TakeProfitSteps * PriceStep 的距离时平仓获利。
  • 移动止损:收益超过 TrailingStopSteps * PriceStep 时,止损会随着每根 K 线移动,锁定部分利润同时保留继续盈利的空间。
  • 以上所有风险控制均以最小报价单位为基础,适用于不同品种。

参数

名称 说明 默认值
CandleType 用于分析的 K 线类型/周期。 5 分钟 K 线
DojiBodyThresholdSteps 判定十字星的最大实体(单位:报价步长)。 1
BreakoutBufferSteps 突破确认所需的额外缓冲距离。 0
InitialStopSteps 初始止损距离(步长)。 20
TakeProfitSteps 固定止盈距离(步长)。 25
TrailingStopSteps 移动止损距离(步长)。 10

所有参数均通过 StrategyParam<T> 暴露,便于界面展示与批量优化。

实现细节

  • 采用高层级的蜡烛线订阅接口 SubscribeCandles().Bind(...),确保与框架推荐做法一致。
  • 通过 _previousCandle_twoCandlesAgo 保存状态,只在收到完整 K 线时才进行决策。
  • 多头与空头的止损/止盈分别管理,并在平仓或缺乏行情数据时及时重置。
  • 日志输出涵盖信号触发、止损、止盈等事件,方便回测阶段分析与排错。

使用建议

  1. 针对不同品种校准十字星阈值:若价格跳动较大,可适当增加 DojiBodyThresholdSteps
  2. 在点差或噪声明显的市场上,可调节 BreakoutBufferSteps 过滤虚假突破。
  3. 多品种部署时,可结合账户层面的风险控制(如组合止损、交易时段限制)。
  4. 由于信号基于收盘数据,建议选择与交易节奏匹配的 CandleType(例如 1 分钟用于短线、15 分钟用于波段)。
using System;
using System.Collections.Generic;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Doji Arrows breakout strategy.
/// Detects doji candles (small body) and trades breakout of the doji range on the next candle.
/// Uses ATR to define what constitutes a small body.
/// </summary>
public class DojiArrowsBreakoutStrategy : Strategy
{
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _dojiThreshold;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevHigh;
	private decimal _prevLow;
	private bool _prevWasDoji;
	private bool _hasPrev;

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

	public DojiArrowsBreakoutStrategy()
	{
		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetDisplay("ATR Period", "ATR period for doji detection", "Indicators");

		_dojiThreshold = Param(nameof(DojiThreshold), 0.3m)
			.SetDisplay("Doji Threshold", "Max body/ATR ratio for doji", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
	protected override void OnReseted() { base.OnReseted(); _prevHigh = 0m; _prevLow = 0m; _prevWasDoji = false; _hasPrev = false; }

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_hasPrev = false;
		_prevWasDoji = false;

		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 (atr <= 0)
			return;

		var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);
		var isDoji = body / atr < DojiThreshold;

		if (_hasPrev && _prevWasDoji)
		{
			// Breakout above doji high
			if (candle.ClosePrice > _prevHigh && Position <= 0)
			{
				if (Position < 0)
					BuyMarket();
				BuyMarket();
			}
			// Breakout below doji low
			else if (candle.ClosePrice < _prevLow && Position >= 0)
			{
				if (Position > 0)
					SellMarket();
				SellMarket();
			}
		}

		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
		_prevWasDoji = isDoji;
		_hasPrev = true;
	}
}