GitHub で見る

N Candles v2 戦略

概要

この戦略は、同じ方向に閉じる連続したロウソク足の設定可能な本数を探します。連続長が達成されると、検出されたモメンタムの方向に成行ポジションをオープンします。この実装はオリジナルのMetaTrader 5エキスパートアドバイザー「N- candles v2」を反映しており、早期シグナルを避けるために閉じたロウソク足にロジックを集中させています。

戦略ロジック

  1. 選択したロウソク足シリーズを購読し、完全に閉じたバーを待ちます。
  2. 各ロウソク足を強気、弱気、または中立(ドージ)に分類します。ドージロウソク足は連続をリセットします。
  3. 同一方向の連続ロウソク足の実行カウンターを維持します。
  4. カウンターがCandlesCountの閾値に達したら、同じ方向に成行注文を送信します。注文サイズは要求されたLotSizeと反対のエクスポージャーを組み合わせて、最終的なネットポジションが意図したサインと数量を持つようにします。
  5. エントリー価格を記録し、設定されたストップロスとテイクプロフィットの距離を使用して保護レベルを初期化します。
  6. 新しいロウソク足ごとにトレーリングストップを更新し(有効な場合)、価格がストップロス、トレーリングストップ、テイクプロフィットに触れたときにポジションをクローズします。

ポジション管理

  • 初期ストップロスとテイクプロフィットは価格ステップ(Security.PriceStep)で測定されます。距離がゼロの場合、対応するレベルは無効になります。
  • トレーリングストップはオプションです。有効な場合、価格が最後のストップ位置を少なくともTrailingStepPips超えて有利に動いたとき、ストップがTrailingStopPips分引き締められます。
  • ポジションをクローズするとキャッシュされたすべてのレベルが削除されるため、次のエントリーには新しい連続が必要です。

パラメーター

名前 説明 デフォルト
CandlesCount 取引前に同じ方向に閉じる必要がある連続ロウソク足の本数。 3
LotSize 各エントリーに使用するポジションサイズ。反対のエクスポージャーは自動的にクローズされます。 1
TakeProfitPips エントリー価格からの価格ステップ単位のテイクプロフィット距離。 50
StopLossPips エントリー価格からの価格ステップ単位のストップロス距離。 50
TrailingStopPips 価格ステップ単位のトレーリングストップ距離。トレーリングを無効にするには0を設定。 10
TrailingStepPips トレーリングストップを引き締める前に価格が動く必要がある追加距離。 4
CandleType シグナル計算に使用するロウソク足の時間軸。 1時間ロウソク足

注記

  • この戦略は有効なPriceStepを提供するあらゆるインストゥルメントで機能します。インストゥルメントがゼロを報告した場合、ソーススクリプトの動作に合わせてフォールバックとして1が使用されます。
  • シグナルは完成したロウソク足のみで生成されるため、バックテストとライブ取引環境間で一貫した動作が維持されます。
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>
/// Trades when a configurable number of consecutive candles share the same direction.
/// Applies fixed stop-loss, take-profit and optional trailing stop in price steps.
/// </summary>
public class NCandlesV2Strategy : Strategy
{
	private readonly StrategyParam<int> _candlesCount;
	private readonly StrategyParam<decimal> _lotSize;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<DataType> _candleType;

	private int _streakLength;
	private int _streakDirection;
	private int _currentPositionDirection;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;

	public int CandlesCount
	{
		get => _candlesCount.Value;
		set => _candlesCount.Value = value;
	}

	public decimal LotSize
	{
		get => _lotSize.Value;
		set => _lotSize.Value = value;
	}

	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

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

	public NCandlesV2Strategy()
	{
		_candlesCount = Param(nameof(CandlesCount), 3)
			.SetGreaterThanZero()
			.SetDisplay("Candles in Row", "Number of identical candles required", "Entry");

		_lotSize = Param(nameof(LotSize), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Lot Size", "Position size used for entries", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Take-profit distance in price steps", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 50)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Stop-loss distance in price steps", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 10)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in price steps", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 4)
			.SetNotNegative()
			.SetDisplay("Trailing Step (pips)", "Additional move required to tighten trailing stop", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Time frame used for analysis", "General");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();
		ResetState();
	}

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

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Process only completed candles to avoid premature decisions.
		if (candle.State != CandleStates.Finished)
			return;

		// Wait until the strategy is fully initialized and allowed to trade.
		// Update trailing logic and close the position if protective levels are hit.
		if (ManageOpenPosition(candle))
			return;

		var direction = GetCandleDirection(candle);

		// Doji candles reset the streak because they do not show clear direction.
		if (direction == 0)
		{
			ResetStreak();
			return;
		}

		// Maintain the running count of identical candles.
		if (direction == _streakDirection)
			_streakLength++;
		else
		{
			_streakDirection = direction;
			_streakLength = 1;
		}

		// Enter only after the required number of matching candles is observed.
		if (_streakLength < CandlesCount)
			return;

		if (direction > 0)
			TryOpenLong(candle);
		else
			TryOpenShort(candle);
	}

	private bool ManageOpenPosition(ICandleMessage candle)
	{
		// Reset cached values once the position is flat.
		if (Position == 0)
		{
			_currentPositionDirection = 0;
			_stopPrice = null;
			_takePrice = null;
			_entryPrice = 0m;
			return false;
		}

		var pip = GetPipSize();
		var trailingStep = TrailingStepPips * pip;

		if (_currentPositionDirection > 0)
		{
			// Raise the stop for long trades when price advances far enough.
			if (TrailingStopPips > 0)
			{
				var desired = candle.ClosePrice - TrailingStopPips * pip;
				if (_stopPrice is decimal stop && desired - trailingStep > stop)
					_stopPrice = desired;
			}

			// Close long positions if take-profit or stop-loss levels are reached.
			if (_takePrice is decimal take && candle.HighPrice >= take)
				return ExitPosition();

			if (_stopPrice is decimal stopLoss && candle.LowPrice <= stopLoss)
				return ExitPosition();
		}
		else if (_currentPositionDirection < 0)
		{
			// Lower the stop for short trades when price keeps moving down.
			if (TrailingStopPips > 0)
			{
				var desired = candle.ClosePrice + TrailingStopPips * pip;
				if (_stopPrice is decimal stop && desired + trailingStep < stop)
					_stopPrice = desired;
			}

			// Close short positions if take-profit or stop-loss levels are reached.
			if (_takePrice is decimal take && candle.LowPrice <= take)
				return ExitPosition();

			if (_stopPrice is decimal stopLoss && candle.HighPrice >= stopLoss)
				return ExitPosition();
		}

		return false;
	}

	private void TryOpenLong(ICandleMessage candle)
	{
		if (Position > 0)
			return;

		if (Position < 0)
			BuyMarket();

		BuyMarket();
		SetPositionState(candle.ClosePrice, 1);
	}

	private void TryOpenShort(ICandleMessage candle)
	{
		if (Position < 0)
			return;

		if (Position > 0)
			SellMarket();

		SellMarket();
		SetPositionState(candle.ClosePrice, -1);
	}

	private bool ExitPosition()
	{
		// Close the active position and clear the cached trade state.
		if (Position > 0)
			SellMarket();
		else if (Position < 0)
			BuyMarket();

		ResetState();
		return true;
	}

	private void SetPositionState(decimal price, int direction)
	{
		// Remember the entry direction and compute initial protective levels.
		_currentPositionDirection = direction;
		_entryPrice = price;

		var pip = GetPipSize();

		if (direction > 0)
		{
			_stopPrice = StopLossPips > 0 ? price - StopLossPips * pip : (TrailingStopPips > 0 ? price : null);
			_takePrice = TakeProfitPips > 0 ? price + TakeProfitPips * pip : null;
		}
		else
		{
			_stopPrice = StopLossPips > 0 ? price + StopLossPips * pip : (TrailingStopPips > 0 ? price : null);
			_takePrice = TakeProfitPips > 0 ? price - TakeProfitPips * pip : null;
		}
	}

	private void ResetState()
	{
		ResetStreak();
		_currentPositionDirection = 0;
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
	}

	private void ResetStreak()
	{
		_streakLength = 0;
		_streakDirection = 0;
	}

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

	private decimal GetPipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 1m;
	}
}