在 GitHub 上查看

Doji Arrows 策略

策略概述

Doji Arrows 策略是将 MetaTrader 平台的同名 EA 转换到 StockSharp 高级 API 的实现。核心思想是在出现标准的十字星(Doji)后等待价格突破其高低点范围。十字星表示买卖力量暂时平衡,一旦下一根 K 线在十字星上方收盘说明多头取得主导,反之在下方收盘则说明空头占优。

  1. 策略只处理所选 CandleType 订阅中的已完成 K 线。
  2. 通过比较上一根 K 线的开盘价与收盘价,判断其是否为十字星。当两者的绝对差值小于等于 DojiBodyPoints 乘以品种的最小价格跳动时,视为十字星。若参数设为 0,则使用一个价格跳动作为容差,复现 MQL5 版本中对开收盘相等的判定。
  3. 如果下一根 K 线在十字星最高价之上收盘,则提交买入市价单;若在十字星最低价之下收盘,则提交卖出市价单。若存在反向仓位,市价单的数量会自动平仓并在需要时反手。

整个流程与原始 EA 每个新柱开始时检查一次的行为保持一致。

风险控制

转换版本保留了原策略的保护机制:

  • 止损StopLossPoints 决定入场价格与初始止损之间的距离,单位为价格跳动。为 0 时不放置固定止损。
  • 止盈TakeProfitPoints 指定目标利润的距离(价格跳动)。为 0 时不设定止盈位。
  • 追踪止损TrailingStopPointsTrailingStepPoints 组合还原了追踪逻辑。当浮动利润超过 TrailingStopPoints + TrailingStepPoints 时,止损会被移动到距离最新收盘价 TrailingStopPoints 的位置(多头使用最高收盘价,空头使用最低收盘价)。仅当 TrailingStopPoints 大于零时启用。

策略在每根已完成 K 线之后检查是否触及止损或止盈。当 K 线的最高价或最低价突破任一保护价格时,立即以市价单平仓,并重置保护数据。

参数说明

参数 默认值 说明
StopLossPoints 30 初始止损距离,单位为价格跳动。
TakeProfitPoints 90 止盈目标距离,单位为价格跳动。
TrailingStopPoints 15 追踪止损距离,单位为价格跳动。
TrailingStepPoints 5 在移动追踪止损前所需的额外利润,单位为价格跳动。
DojiBodyPoints 1 判断上一根 K 线是否为十字星的最大实体大小(价格跳动)。0 表示使用一个价格跳动的容差。
CandleType 1 小时 用于生成信号的 K 线类型。

实现细节

  • 通过 SubscribeCandles(CandleType).Bind(ProcessCandle) 订阅蜡烛数据,仅保存最近一根完成的 K 线。
  • 价格跳动从 Security?.PriceStep 读取;若数据源未提供,则回退到 1,确保策略能在合成或历史数据上运行。
  • 每次开仓都会重新计算保护价格,追踪止损即使在禁用固定止损时也能建立止损位,从而复现 MQL 版本“零起步”的追踪行为。
  • 所有交易均使用市价单,保持与原 EA 追求即时成交的思路一致。

使用建议

  1. 在启动策略前配置 SecurityPortfolio 以及 Volume 属性。
  2. 根据交易品种的报价精度调整各个以点数表示的参数,尤其是存在小数点报价的外汇或差价合约。
  3. 若需要更复杂的仓位管理,可结合 StockSharp 的风险控制模块,本次转换保留了原策略的固定手数模式。
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>
/// Doji breakout strategy with optional fixed and trailing protection.
/// </summary>
public class DojiArrowsStrategy : Strategy
{
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<decimal> _trailingStepPoints;
	private readonly StrategyParam<decimal> _dojiBodyPoints;
	private readonly StrategyParam<DataType> _candleType;

	private bool _hasPreviousCandle;
	private decimal _prevOpen;
	private decimal _prevClose;
	private decimal _prevHigh;
	private decimal _prevLow;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;

	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	public decimal TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	public decimal DojiBodyPoints
	{
		get => _dojiBodyPoints.Value;
		set => _dojiBodyPoints.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public DojiArrowsStrategy()
	{
		_stopLossPoints = Param(nameof(StopLossPoints), 30m)
			.SetNotNegative()
			.SetDisplay("Stop Loss Points", "Stop loss distance in price steps.", "Risk")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 90m)
			.SetNotNegative()
			.SetDisplay("Take Profit Points", "Take profit distance in price steps.", "Risk")
			;

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 15m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop Points", "Trailing distance in price steps.", "Risk")
			;

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Step Points", "Minimum profit before the trailing stop moves.", "Risk")
			;

		_dojiBodyPoints = Param(nameof(DojiBodyPoints), 1m)
			.SetNotNegative()
			.SetDisplay("Doji Body Points", "Maximum difference between open and close to treat the candle as a doji.", "Pattern")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used for signal generation.", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_hasPreviousCandle = false;
		_prevOpen = 0m;
		_prevClose = 0m;
		_prevHigh = 0m;
		_prevLow = 0m;
		ResetProtection();
	}

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

		SubscribeCandles(CandleType)
			.Bind(ProcessCandle)
			.Start();

		StartProtection(
			takeProfit: new Unit(2, UnitTypes.Percent),
			stopLoss: new Unit(1, UnitTypes.Percent)
		);
	}

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

		ManageActivePosition(candle);

		if (!_hasPreviousCandle)
		{
			CachePreviousCandle(candle);
			return;
		}

		var step = Security?.PriceStep ?? 1m;
		var tolerance = DojiBodyPoints <= 0m ? step : DojiBodyPoints * step;
		var bodySize = Math.Abs(_prevOpen - _prevClose);
		var isDoji = bodySize <= tolerance;

		var breakoutUp = isDoji && candle.ClosePrice > _prevHigh;
		var breakoutDown = isDoji && candle.ClosePrice < _prevLow;

		if (breakoutUp && Position == 0)
		{
			BuyMarket();
		}
		else if (breakoutDown && Position == 0)
		{
			SellMarket();
		}

		CachePreviousCandle(candle);
	}

	private void ManageActivePosition(ICandleMessage candle)
	{
		if (Position == 0)
			return;

		var step = Security?.PriceStep ?? 1m;
		var trailingDistance = TrailingStopPoints > 0m ? TrailingStopPoints * step : 0m;
		var trailingStep = TrailingStepPoints > 0m ? TrailingStepPoints * step : 0m;

		if (Position > 0)
		{
			if (trailingDistance > 0m && _entryPrice.HasValue)
			{
				var gain = candle.ClosePrice - _entryPrice.Value;

				if (gain > trailingDistance + trailingStep)
				{
					var newStop = candle.ClosePrice - trailingDistance;

					if (!_stopPrice.HasValue || newStop > _stopPrice.Value)
						_stopPrice = newStop;
				}
			}

			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetProtection();
				return;
			}

			if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetProtection();
				return;
			}
		}
		else if (Position < 0)
		{
			if (trailingDistance > 0m && _entryPrice.HasValue)
			{
				var gain = _entryPrice.Value - candle.ClosePrice;

				if (gain > trailingDistance + trailingStep)
				{
					var newStop = candle.ClosePrice + trailingDistance;

					if (!_stopPrice.HasValue || newStop < _stopPrice.Value)
						_stopPrice = newStop;
				}
			}

			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetProtection();
				return;
			}

			if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetProtection();
				return;
			}
		}
	}

	private void InitializeProtection(decimal price, bool isLong, decimal step)
	{
		_entryPrice = price;

		if (StopLossPoints > 0m)
		{
			var offset = StopLossPoints * step;
			_stopPrice = isLong ? price - offset : price + offset;
		}
		else
		{
			_stopPrice = null;
		}

		if (TakeProfitPoints > 0m)
		{
			var offset = TakeProfitPoints * step;
			_takePrice = isLong ? price + offset : price - offset;
		}
		else
		{
			_takePrice = null;
		}
	}

	private void ResetProtection()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
	}

	private void CachePreviousCandle(ICandleMessage candle)
	{
		_prevOpen = candle.OpenPrice;
		_prevClose = candle.ClosePrice;
		_prevHigh = candle.HighPrice;
		_prevLow = candle.LowPrice;
		_hasPreviousCandle = true;
	}
}