GitHub で見る

BrakeoutTraderV1

BrakeoutTraderV1は静的な価格レベルを中心に構築されたシンプルなブレイクアウトシステムです。戦略は完成したローソク足の終値を監視し、市場が選択したブレイクアウトレベルを越えて終値を付けると参入します。終値がレベルを上抜けると、ロングポジションが開かれます(方向フィルターに従う);下抜けるとショートポジションが取られます。ポジションサイズは設定されたリスクパーセンテージとストップロスまでの距離から計算され、口座資産に応じた自動スケーリングが可能です。

トレードロジック

  • 選択した CandleType の完成したローソク足のみを処理します。未完成のローソク足は無視されます。
  • ユーザー指定の BreakoutLevel のブレイクアウトを検出するために最後の終値を保持します。
  • ロングエントリー: 最新のローソク足が BreakoutLevel を上抜けて終値を付け、前の終値がレベルに等しいかそれ以下であり、EnableLong が真の場合。新しい注文を送信する前に、未決済のショートポジションはすべてフラット化されます。
  • ショートエントリー: 最新のローソク足が BreakoutLevel を下抜けて終値を付け、前の終値がレベルに等しいかそれ以上であり、EnableShort が真の場合。まずロングポジションがすべてクローズされます。
  • 注文は成行で送信されます。数量は、エントリー価格とストップロス距離の間の損失が現在の口座資産の RiskPercent に対応するように計算されます。リスクベースのサイズが決定できない場合、戦略は基本の Volume 値に戻ります。
  • 各エントリー後、戦略はpipで表現された静的なテイクプロフィットとストップロスレベルを保存します(StopLossPointsTakeProfitPoints)。価格がいずれかのレベルに達すると、未決済ポジションは成行でクローズされ、キャッシュされたレベルがクリアされます。
  • ネットポジションが明示的に管理されるため、同じ方向に複数の同時オープントレードが発生することはありません。

ポジション管理

  • 保護的なストップはロングトレードではエントリーの下に、ショートではエントリーの上に設定されます。距離は StopLossPoints * pip で、pipは Security.PriceStep とその精度から導出されます(3桁または5桁の小数点以下は元のMQL実装と同様に10倍の調整を意味します)。
  • 利益目標は TakeProfitPoints を使用して対称的に設定されます。
  • 同じローソク足内でストップとターゲットの両方がトリガーされる場合、保守的なサーバーサイド実行を反映してストップが最初に評価されます。
  • 反対のシグナルは、新しいポジションを確立する前に常にアクティブなポジションをすべてクローズし、ヘッジされた露出を防ぎます。
  • ポジションがゼロに戻るとキャッシュされたレベルが自動的にリセットされます。

パラメーター

  • BreakoutLevel – ブレイクアウトを監視する静的な価格レベル。
  • EnableLong / EnableShort – ロングまたはショートポジションを開くことを許可する方向フィルター。
  • StopLossPoints – pipでのストップロス距離(導出されたpipサイズの倍数)。
  • TakeProfitPoints – pipでのテイクプロフィット距離。
  • RiskPercent – トレードごとにリスクを取る口座資産のパーセンテージ。ストップロス距離から注文数量を決定するために使用されます。
  • CandleType – シグナル生成に使用するローソク足データシリーズ(デフォルト:15分足)。
  • Volume – リスクベースの計算が利用できない場合に使用する基本注文サイズ。

詳細

  • エントリー条件: 終値が最後の完成したローソク足で BreakoutLevel の上/下にクロスします。
  • ロング/ショート: EnableLongEnableShort フラグで制御される両方向を取引します。
  • エグジット条件: 静的なストップロスとテイクプロフィットレベル、および反対のブレイクアウトシグナルでのフラット化。
  • ストップ: pipで測定した固定距離のストップロス。
  • デフォルト値: BreakoutLevel = 0StopLossPoints = 140TakeProfitPoints = 180RiskPercent = 10CandleType = 15分EnableLong = EnableShort = true
  • フィルター: 方向トグルを超えるものはなし;トレンドやボラティリティフィルターは適用されません。

使用上の注意

  • 銘柄は元のEAが使用するpip計算をサポートする必要があります。3桁または5桁の小数点以下のシンボルでは、pipが自動的に10倍にスケールされます。
  • リスクベースのサイジングが正常に機能するために、接続されたポートフォリオが CurrentValue を提供することを確認してください。資産が利用できない場合、トレードは設定された Volume にデフォルトします。
  • 注文は成行で執行されるため、実際の約定はローソク足の終値と異なる場合があります。必要に応じてスリッページを考慮してストップと取得距離を調整してください。
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>
/// Breakout strategy that trades when the closing price crosses a predefined level.
/// The strategy sizes positions based on the selected risk percentage and applies static stop-loss and take-profit levels.
/// </summary>
public class BrakeoutTraderV1Strategy : Strategy
{
	private readonly StrategyParam<decimal> _breakoutLevel;
	private readonly StrategyParam<bool> _enableLong;
	private readonly StrategyParam<bool> _enableShort;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _previousClose;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal _pipSize;

	/// <summary>
	/// Price level that must be broken to generate signals.
	/// </summary>
	public decimal BreakoutLevel
	{
		get => _breakoutLevel.Value;
		set => _breakoutLevel.Value = value;
	}

	/// <summary>
	/// Enables or disables long breakout trades.
	/// </summary>
	public bool EnableLong
	{
		get => _enableLong.Value;
		set => _enableLong.Value = value;
	}

	/// <summary>
	/// Enables or disables short breakout trades.
	/// </summary>
	public bool EnableShort
	{
		get => _enableShort.Value;
		set => _enableShort.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in points relative to the pip size.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance in points relative to the pip size.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Percentage of account equity to risk on each trade.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Candle type used to evaluate breakouts.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="BrakeoutTraderV1Strategy"/>.
	/// </summary>
	public BrakeoutTraderV1Strategy()
	{
		_breakoutLevel = Param(nameof(BreakoutLevel), 65000m)
			.SetDisplay("Breakout Level", "Static price level monitored for breakouts", "Signal");

		_enableLong = Param(nameof(EnableLong), true)
			.SetDisplay("Enable Long", "Allow long breakout positions", "Signal");

		_enableShort = Param(nameof(EnableShort), true)
			.SetDisplay("Enable Short", "Allow short breakout positions", "Signal");

		_stopLossPoints = Param(nameof(StopLossPoints), 140m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss Points", "Stop-loss distance expressed in pip points", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 180m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit Points", "Take-profit distance expressed in pip points", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 10m)
			.SetGreaterThanZero()
			.SetDisplay("Risk %", "Percentage of equity risked per trade", "Risk Management");

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

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

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

		_previousClose = null;
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
		_pipSize = 0m;
	}

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

		var priceStep = Security?.PriceStep ?? 1m;
		var decimals = Security?.Decimals;
		_pipSize = priceStep;

		if (decimals is 3 or 5)
			_pipSize *= 10m;

		if (_pipSize <= 0m)
			_pipSize = 1m;

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

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

		// removed StartProtection(null, null)
	}

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

		if (ManageOpenPosition(candle))
		{
			UpdatePreviousClose(candle.ClosePrice);
			return;
		}

		var prevClose = _previousClose;
		if (prevClose is null)
		{
			_previousClose = candle.ClosePrice;
			return;
		}

		var currentClose = candle.ClosePrice;
		var prevValue = prevClose.Value;

		var breakoutUp = currentClose > BreakoutLevel && prevValue <= BreakoutLevel;
		var breakoutDown = currentClose < BreakoutLevel && prevValue >= BreakoutLevel;

		if (breakoutUp && EnableLong)
		{
			EnterLong(currentClose);
		}
		else if (breakoutDown && EnableShort)
		{
			EnterShort(currentClose);
		}

		_previousClose = currentClose;
	}

	private bool ManageOpenPosition(ICandleMessage candle)
	{
		if (Position > 0)
		{
			// Exit long if stop-loss is touched.
			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket();
				ResetPositionState();
				return true;
			}

			// Exit long if take-profit is reached.
			if (_takePrice is decimal take && candle.HighPrice >= take)
			{
				SellMarket();
				ResetPositionState();
				return true;
			}
		}
		else if (Position < 0)
		{
			// Exit short if stop-loss is touched.
			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket();
				ResetPositionState();
				return true;
			}

			// Exit short if take-profit is reached.
			if (_takePrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket();
				ResetPositionState();
				return true;
			}
		}
		else if (_entryPrice.HasValue)
		{
			// Reset cached levels after position is fully closed.
			ResetPositionState();
		}

		return false;
	}

	private void EnterLong(decimal price)
	{
		if (Position > 0)
			return;

		var volume = CalculateOrderVolume();
		var closingVolume = Position < 0 ? Math.Abs(Position) : 0m;
		var totalVolume = closingVolume + volume;

		if (totalVolume <= 0m)
			return;

		if (closingVolume > 0m)
			ResetPositionState();

		BuyMarket();
		SetPositionTargets(price, true, volume > 0m);
	}

	private void EnterShort(decimal price)
	{
		if (Position < 0)
			return;

		var volume = CalculateOrderVolume();
		var closingVolume = Position > 0 ? Position : 0m;
		var totalVolume = closingVolume + volume;

		if (totalVolume <= 0m)
			return;

		if (closingVolume > 0m)
			ResetPositionState();

		SellMarket();
		SetPositionTargets(price, false, volume > 0m);
	}

	private void SetPositionTargets(decimal entryPrice, bool isLong, bool hasNewPosition)
	{
		if (!hasNewPosition)
		{
			return;
		}

		_entryPrice = entryPrice;

		if (StopLossPoints > 0m && _pipSize > 0m)
			_stopPrice = isLong
				? entryPrice - StopLossPoints * _pipSize
				: entryPrice + StopLossPoints * _pipSize;
		else
			_stopPrice = null;

		if (TakeProfitPoints > 0m && _pipSize > 0m)
			_takePrice = isLong
				? entryPrice + TakeProfitPoints * _pipSize
				: entryPrice - TakeProfitPoints * _pipSize;
		else
			_takePrice = null;
	}

	private decimal CalculateOrderVolume()
	{
		var baseVolume = Volume;
		var stopDistance = StopLossPoints * _pipSize;

		if (stopDistance <= 0m || RiskPercent <= 0m)
			return AdjustVolume(baseVolume);

		var equity = Portfolio?.CurrentValue ?? 0m;
		if (equity <= 0m)
			return AdjustVolume(baseVolume);

		var riskValue = equity * RiskPercent / 100m;
		if (riskValue <= 0m)
			return AdjustVolume(baseVolume);

		var qty = riskValue / stopDistance;
		var adjusted = AdjustVolume(qty);

		return adjusted > 0m ? adjusted : AdjustVolume(baseVolume);
	}

	private decimal AdjustVolume(decimal volume)
	{
		var security = Security;
		if (security != null)
		{
			var step = security.VolumeStep;
			if (step is decimal s && s > 0m)
			{
				volume = Math.Floor(volume / s) * s;
			}

			var min = security.MinVolume;
			if (min is decimal minVol && volume < minVol)
				volume = minVol;

			var max = security.MaxVolume;
			if (max is decimal maxVol && maxVol > 0m && volume > maxVol)
				volume = maxVol;

			if (volume <= 0m)
				volume = step is decimal stepVal && stepVal > 0m ? stepVal : 0m;
		}

		if (volume <= 0m)
			volume = volume == 0m ? 1m : Math.Abs(volume);

		return volume;
	}

	private void UpdatePreviousClose(decimal close)
	{
		_previousClose = close;
	}

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