在 GitHub 上查看

N Candles 策略

策略概述

N Candles 策略会监控连续收盘方向相同的蜡烛。一旦出现设定数量的连续阳线或连续阴线,系统就按同方向开仓。本实现是 MetaTrader “N Candles v4” 专家的移植版本,在 StockSharp 的高层 API 中保留了原有的风险控制、以点(pip)为单位的配置以及可选的移动止损机制。

入场逻辑

  • 仅在每根蜡烛完全收盘后进行分析。
  • 收盘价高于开盘价的蜡烛记为阳线,低于开盘价的记为阴线,十字星会重置计数。
  • 当连续出现 ConsecutiveCandles 根相同方向的蜡烛时,策略以市价单在该方向入场。
  • 根据 AccountingMode,可以选择净额模式(限制净持仓量)或套保模式(限制同向开仓次数)。

出场与风控

  • StopLossPipsTakeProfitPips 以点(pip)为单位定义固定止损和止盈,参考的是当前持仓的平均成交价。
  • TrailingStopPips 大于 0 时启用移动止损:
    • 如果没有固定止损(例如 StopLossPips 为 0),价格向盈利方向移动 TrailingStopPips 后,止损会被移动至保本位置。
    • 当已有止损时,只要价格与止损之间的距离超过 TrailingStopPips + TrailingStepPips,止损就会向有利方向推进。
  • 每次持仓数量变化时都会重新计算保护价位,并在每根已完成的蜡烛上检查是否触发止损或止盈,一旦满足条件立即平仓。

参数说明

参数 描述 默认值
ConsecutiveCandles 触发信号所需的连续同向蜡烛数量。 3
TakeProfitPips 止盈距离(点)。设为 0 可关闭止盈。 50
StopLossPips 止损距离(点)。设为 0 可关闭止损。 50
TrailingStopPips 移动止损距离(点)。设为 0 表示不启用。 10
TrailingStepPips 移动止损推进前所需的额外距离。 4
MaxPositionsPerDirection 套保模式下,同方向允许的最大开仓次数。 2
MaxNetVolume 净额模式下允许的最大净持仓量。 2
AccountingMode Netting(净额)或 Hedging(套保)。 Netting
CandleType 用于检测形态的蜡烛类型。 1 分钟蜡烛

所有以点为单位的参数都会依据标的物的最小报价步长转换为价格增量。若标的价格拥有 3 或 5 位小数,则点值会放大 10 倍,以保持与 MetaTrader 定义一致。

实现要点

  • 使用 SubscribeCandles 的高层数据订阅,避免手动维护历史队列。
  • 为还原原版 EA 的移动止损效果,策略分别记录多头持仓的最高价和空头持仓的最低价。
  • 限仓规则会根据基础交易量 Volume 自动调整,使风险控制与仓位规模保持一致。
  • 当止损或止盈触发并平仓时,策略会写入日志,便于复盘与调试。

使用建议

  • 若需要模拟可同时持有多笔同方向仓位的平台,请选择 Hedging;若希望限制为单一净额仓位,则使用 Netting
  • TrailingStepPips 设为 0 可以获得传统的“紧贴式”移动止损效果。
  • 因为出场判断基于收盘价,若需要更精细的入场/出场控制,可以考虑使用更短周期的蜡烛。
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy that opens positions after detecting N identical candles in a row.
/// Enters in the direction of the candle streak.
/// </summary>
public class NCandlesSequenceStrategy : Strategy
{
	private readonly StrategyParam<int> _consecutiveCandles;
	private readonly StrategyParam<DataType> _candleType;

	private int _consecutiveDirection;
	private int _consecutiveCount;

	/// <summary>
	/// Number of identical candles required before entering a trade.
	/// </summary>
	public int ConsecutiveCandles
	{
		get => _consecutiveCandles.Value;
		set => _consecutiveCandles.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the strategy.
	/// </summary>
	public NCandlesSequenceStrategy()
	{
		_consecutiveCandles = Param(nameof(ConsecutiveCandles), 3)
			.SetGreaterThanZero()
			.SetDisplay("Consecutive Candles", "Number of identical candles in a row", "Entry")
			.SetOptimize(2, 6, 1);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles to analyze", "General");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_consecutiveDirection = 0;
		_consecutiveCount = 0;
	}

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

		_consecutiveDirection = 0;
		_consecutiveCount = 0;

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

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

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

		var direction = GetCandleDirection(candle);

		if (direction == 0)
		{
			_consecutiveDirection = 0;
			_consecutiveCount = 0;
			return;
		}

		if (direction == _consecutiveDirection)
		{
			_consecutiveCount++;
		}
		else
		{
			_consecutiveDirection = direction;
			_consecutiveCount = 1;
		}

		if (_consecutiveCount < ConsecutiveCandles)
			return;

		if (direction > 0 && Position <= 0)
		{
			BuyMarket();
		}
		else if (direction < 0 && Position >= 0)
		{
			SellMarket();
		}
	}

	private static int GetCandleDirection(ICandleMessage candle)
	{
		if (candle.ClosePrice > candle.OpenPrice)
			return 1;
		if (candle.ClosePrice < candle.OpenPrice)
			return -1;
		return 0;
	}
}