GitHub で見る

YenTrader051戦略(C#)

概要

YenTrader051戦略は、3つの通貨ペア間の関係を裁定取引するオリジナルのMetaTraderエキスパートアドバイザーを再現します:

  • 取引クロスペア – 戦略インスタンスをホストする銘柄(例:GBPJPY)。
  • メジャーペア – クロスペアの基軸通貨対USD(例:GBPUSD)。
  • USDJPY – 三角形の円の脚を確認するために使用されます。

メジャーペアのブレイクアウトとUSDJPYからの確認を組み合わせることで取引シグナルを生成します。オプションのRSI、CCI、RVI、移動平均フィルターでエントリーを絞り込みます。ポジション管理はアベレージングとピラミッディングの両方をサポートし、リスク管理はEAからのpip/ATRベースのストップ処理を再現します。

トレーディングロジック

  1. ブレイクアウト検出
    • LoopBackBarsはルックバックウィンドウを制御します。1より大きい場合、戦略は以下を確認します:
      • 最近の高値/安値(PriceReference = HighLow)、または
      • LoopBackBarsバー前の終値(PriceReference = Close)。
    • MajorDirectionは、クロスペアがメジャー/円(Left)または円/メジャー(Right)として見積もられた場合に、メジャーペアと円の脚がどのように相互に動くべきかを定義します。
  2. エントリーフィルター
    • UseRsiFilterは期待されるトレンドアラインメントに応じてRSIが50を上回る/下回ることを要求します。
    • UseCciFilterはCCIが正/負であることを強制します。
    • UseRviFilterはRVIがそのシグナルラインをクロスするのを待ちます。シグナルラインはMT4の実装と同様に、RVI値の4期間SMAです。
    • UseMovingAverageFilterは設定可能な移動平均(MaModeMaPeriod)とエントリーを揃えます。
  3. エントリースタイル
    • EntryMode = Bothは任意のブレイクアウトを許可します。
    • EntryMode = Pyramidingは取引方向の強気/弱気ローソク足のみ追加します。
    • EntryMode = Averagingは前のローソク足がポジションに逆行してクローズした場合のみ追加して平均を取ります。
  4. 注文サイジング
    • FixedLotSizeは一定のボリュームを配置します。
    • 固定ロットがゼロの場合、戦略はBalancePercentLotSizeと現在のポートフォリオ価値を使用して取引をサイジングします。
    • MaxOpenPositionsは累積サイズ(加算エントリーの数)を制限します。
  5. リスク管理
    • pip距離(StopLossPipsTakeProfitPipsBreakEvenPipsProfitLockPipsTrailingStopPipsTrailingStepPips)はSecurity.MinPriceStepを通じて変換されます。
    • EnableAtrLevelsが有効な場合、ATR距離は日次ATR(AtrCandleTypeAtrPeriod)と各乗数を使用してpipsを置き換えます。
    • ストップ、テイクプロフィット、ブレークイーブン、利益ロック、トレーリングレベルは完成したローソク足から更新され、MQL実装と同様です。
    • CloseOnOppositeは反対のブレイクアウトが現れたとき、新しいものを積み重ねる代わりに既存のポジションをクローズします。
    • AllowHedgingは反対のポジションがまだオープンであっても、ポジションに追加することを許可します。StockSharpの戦略はネットポジションを使用するため、ロング/ショートの同時ポジションはサポートされません。フラグは現在のネットポジションが反対方向を向いているときに、戦略がエクスポージャーを増やすことができるかどうかを実質的に制御します。

パラメーター

グループ 名前 説明
銘柄 MajorSecurity ブレイクアウト確認に使用するメジャーペア。
UsdJpySecurity 円の脚確認のためのUSDJPY銘柄。
データ CandleType 3つのペアすべてのシグナル時間軸。
フィルター MajorDirection メジャーペアと取引クロスの整合(Left = メジャー/円、Right = 円/メジャー)。
PriceReference 高値/安値ブレイクアウトまたは遅延終値比較。
LoopBackBars ブレイクアウト評価のための履歴バー数。
EntryMode アベレージング、ピラミッディング、またはその両方。
インジケーター UseRsiFilterUseCciFilterUseRviFilterUseMovingAverageFilter 追加確認フィルターの有効/無効。
MaPeriodMaMode 移動平均の設定。
リスク FixedLotSizeBalancePercentLotSize ボリューム制御。
MaxOpenPositions 加算エントリーの最大数。
StopLossPipsTakeProfitPipsBreakEvenPipsProfitLockPipsTrailingStopPipsTrailingStepPips pipベースのリスク距離。
EnableAtrLevelsAtrCandleTypeAtrPeriodAtrStopLossMultiplierAtrTakeProfitMultiplierAtrTrailingMultiplierAtrBreakEvenMultiplierAtrProfitLockMultiplier ATRベースのリスク設定。
動作 CloseOnOpposite 反対のシグナルでポジションをクローズまたは反転。
AllowHedging 反対のネットポジションが存在する場合のエントリーを許可。

使用上の注意

  • 取引するクロス銘柄を戦略のSecurityプロパティに割り当て、次にサポート銘柄としてMajorSecurityUsdJpySecurityを設定します。
  • ポートフォリオが接続されていることを確認してください。変動ロットサイジングにはPortfolio.CurrentValueが必要です。
  • 戦略は3つの銘柄すべての同期したローソク足データを期待します。異なる取引所がセッションカレンダーの異なるデータを提供する場合、共通の時間軸へのリサンプリングを検討してください。
  • ATR計算は設定されたAtrCandleTypeを購読します。比較可能な動作のためにオリジナルEAのデフォルト(日次、21期間)と揃えてください。
  • リスクロジックはクローズしたローソク足で動作するため、保護注文はしきい値が次のローソク足中に破られたときに市場決済によって実行されます。

MT4バージョンとの違い

  • StockSharpは集計されたネットポジションを使用します。真のヘッジング(ロングとショートを同時に保持)は利用できません。AllowHedgingは新しいシグナルが現れたときに戦略がポジションを自動的に反転できるかどうかを単純に制御します。
  • ストップ/リミット管理は、ローソク足データでしきい値がトリガーされた後の市場決済で実装されます。オリジナルEAはティックレベルで動作するため、注文ストップを直接変更します。
  • RVIシグナルラインはRVI値の4期間SMAとして実装され、MT4のMODE_SIGNALの動作に一致します。
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>
/// Port of the Yen Trader expert advisor that trades a JPY cross with confirmation from a major pair and USDJPY.
/// </summary>
public class YenTrader051Strategy : Strategy
{
	private readonly StrategyParam<Security> _majorSecurity;
	private readonly StrategyParam<Security> _usdJpySecurity;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<YenTraderMajorDirections> _majorDirection;
	private readonly StrategyParam<YenTraderEntryModes> _entryMode;
	private readonly StrategyParam<YenTraderPriceReferences> _priceReference;
	private readonly StrategyParam<int> _loopBackBars;
	private readonly StrategyParam<bool> _useRsiFilter;
	private readonly StrategyParam<bool> _useCciFilter;
	private readonly StrategyParam<bool> _useRviFilter;
	private readonly StrategyParam<bool> _useMovingAverageFilter;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<MovingAverageModes> _maMode;
	private readonly StrategyParam<decimal> _fixedLotSize;
	private readonly StrategyParam<decimal> _balancePercentLotSize;
	private readonly StrategyParam<int> _maxOpenPositions;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _breakEvenPips;
	private readonly StrategyParam<int> _profitLockPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<bool> _closeOnOpposite;
	private readonly StrategyParam<bool> _allowHedging;
	private readonly StrategyParam<bool> _enableAtrLevels;
	private readonly StrategyParam<DataType> _atrCandleType;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _atrStopLossMultiplier;
	private readonly StrategyParam<decimal> _atrTakeProfitMultiplier;
	private readonly StrategyParam<decimal> _atrTrailingMultiplier;
	private readonly StrategyParam<decimal> _atrBreakEvenMultiplier;
	private readonly StrategyParam<decimal> _atrProfitLockMultiplier;

	private Highest _majorHighest = null!;
	private Lowest _majorLowest = null!;
	private RelativeStrengthIndex _majorRsi = null!;
	private CommodityChannelIndex _majorCci = null!;
	private RelativeVigorIndex _majorRvi = null!;
	private SimpleMovingAverage _majorRviSignal = null!;
	private IIndicator _majorMa = null!;

	private Highest _usdJpyHighest = null!;
	private Lowest _usdJpyLowest = null!;
	private RelativeStrengthIndex _usdJpyRsi = null!;
	private CommodityChannelIndex _usdJpyCci = null!;
	private RelativeVigorIndex _usdJpyRvi = null!;
	private SimpleMovingAverage _usdJpyRviSignal = null!;
	private IIndicator _usdJpyMa = null!;

	private AverageTrueRange _atr;

	private readonly Queue<decimal> _majorCloses = new();
	private readonly Queue<decimal> _usdJpyCloses = new();

	private decimal? _majorLastClose;
	private decimal? _majorLookbackClose;
	private decimal? _majorHighestValue;
	private decimal? _majorLowestValue;
	private decimal? _majorRsiValue;
	private decimal? _majorCciValue;
	private decimal? _majorRviValue;
	private decimal? _majorRviSignalValue;
	private decimal? _majorMaValue;

	private decimal? _usdJpyLastClose;
	private decimal? _usdJpyLookbackClose;
	private decimal? _usdJpyHighestValue;
	private decimal? _usdJpyLowestValue;
	private decimal? _usdJpyRsiValue;
	private decimal? _usdJpyCciValue;
	private decimal? _usdJpyRviValue;
	private decimal? _usdJpyRviSignalValue;
	private decimal? _usdJpyMaValue;

	private decimal? _atrValue;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private bool _breakEvenActivated;
	private bool _profitLockActivated;
	private decimal _highestSinceEntry;
	private decimal _lowestSinceEntry;

	/// <summary>
	/// Initializes a new instance of the <see cref="YenTrader051Strategy"/> class.
	/// </summary>
	public YenTrader051Strategy()
	{
		_majorSecurity = Param(nameof(MajorSecurity), default(Security))
			.SetDisplay("Major Security", "Major currency pair used for confirmation", "Instruments");
		_usdJpySecurity = Param(nameof(UsdJpySecurity), default(Security))
			.SetDisplay("USDJPY Security", "USDJPY pair used for confirmation", "Instruments");
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Signal Candles", "Primary timeframe for signals", "Data");
		_majorDirection = Param(nameof(MajorDirection), YenTraderMajorDirections.Left)
			.SetDisplay("Major Direction", "Alignment between major and cross", "Filters");
		_entryMode = Param(nameof(EntryMode), YenTraderEntryModes.Both)
			.SetDisplay("Entry Mode", "Control averaging or pyramiding behaviour", "Filters");
		_priceReference = Param(nameof(PriceReference), YenTraderPriceReferences.Close)
			.SetDisplay("Price Reference", "Breakout reference for loop back bars", "Filters");
		_loopBackBars = Param(nameof(LoopBackBars), 40)
			.SetDisplay("Loop Back Bars", "Number of historical bars for breakout logic", "Filters");
		_useRsiFilter = Param(nameof(UseRsiFilter), true)
			.SetDisplay("Use RSI", "Enable RSI confirmation filter", "Indicators");
		_useCciFilter = Param(nameof(UseCciFilter), false)
			.SetDisplay("Use CCI", "Enable CCI confirmation filter", "Indicators");
		_useRviFilter = Param(nameof(UseRviFilter), false)
			.SetDisplay("Use RVI", "Enable RVI confirmation filter", "Indicators");
		_useMovingAverageFilter = Param(nameof(UseMovingAverageFilter), true)
			.SetDisplay("Use Moving Average", "Enable moving average confirmation filter", "Indicators");
		_maPeriod = Param(nameof(MaPeriod), 34)
			.SetGreaterThanZero()
			.SetDisplay("MA Period", "Moving average period", "Indicators");
		_maMode = Param(nameof(MaMode), MovingAverageModes.Smoothed)
			.SetDisplay("MA Mode", "Moving average calculation mode", "Indicators");
		_fixedLotSize = Param(nameof(FixedLotSize), 0m)
			.SetDisplay("Fixed Volume", "Fixed volume per trade (0 = disabled)", "Risk");
		_balancePercentLotSize = Param(nameof(BalancePercentLotSize), 1m)
			.SetDisplay("Balance Percent Volume", "Portfolio percent used to size trades when fixed volume is disabled", "Risk");
		_maxOpenPositions = Param(nameof(MaxOpenPositions), 1)
			.SetDisplay("Max Positions", "Maximum number of additive entries", "Risk");
		_stopLossPips = Param(nameof(StopLossPips), 1000)
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk");
		_takeProfitPips = Param(nameof(TakeProfitPips), 5000)
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");
		_breakEvenPips = Param(nameof(BreakEvenPips), 200)
			.SetDisplay("Break Even (pips)", "Distance before moving stop to break even", "Risk");
		_profitLockPips = Param(nameof(ProfitLockPips), 200)
			.SetDisplay("Profit Lock (pips)", "Distance before locking additional profit", "Risk");
		_trailingStopPips = Param(nameof(TrailingStopPips), 200)
			.SetDisplay("Trailing Stop (pips)", "Trailing stop distance in pips", "Risk");
		_trailingStepPips = Param(nameof(TrailingStepPips), 10)
			.SetDisplay("Trailing Step (pips)", "Minimum trailing stop step in pips", "Risk");
		_closeOnOpposite = Param(nameof(CloseOnOpposite), false)
			.SetDisplay("Close On Opposite", "Close current position when opposite signal appears", "Risk");
		_allowHedging = Param(nameof(AllowHedging), true)
			.SetDisplay("Allow Hedging", "Allow simultaneous trades without closing existing ones", "Risk");
		_enableAtrLevels = Param(nameof(EnableAtrLevels), false)
			.SetDisplay("Use ATR Levels", "Use ATR based distances instead of pips", "Risk");
		_atrCandleType = Param(nameof(AtrCandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("ATR Candles", "Timeframe for ATR calculations", "Risk");
		_atrPeriod = Param(nameof(AtrPeriod), 21)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR lookback period", "Risk");
		_atrStopLossMultiplier = Param(nameof(AtrStopLossMultiplier), 2m)
			.SetDisplay("ATR SL Multiplier", "ATR multiplier for stop loss", "Risk");
		_atrTakeProfitMultiplier = Param(nameof(AtrTakeProfitMultiplier), 4m)
			.SetDisplay("ATR TP Multiplier", "ATR multiplier for take profit", "Risk");
		_atrTrailingMultiplier = Param(nameof(AtrTrailingMultiplier), 1m)
			.SetDisplay("ATR Trail Multiplier", "ATR multiplier for trailing stop", "Risk");
		_atrBreakEvenMultiplier = Param(nameof(AtrBreakEvenMultiplier), 0.5m)
			.SetDisplay("ATR BE Multiplier", "ATR multiplier for break even distance", "Risk");
		_atrProfitLockMultiplier = Param(nameof(AtrProfitLockMultiplier), 2m)
			.SetDisplay("ATR PL Multiplier", "ATR multiplier for profit lock distance", "Risk");
	}

	/// <summary>
	/// Major pair used for confirmation.
	/// </summary>
	public Security MajorSecurity
	{
		get => _majorSecurity.Value;
		set => _majorSecurity.Value = value;
	}

	/// <summary>
	/// USDJPY pair used for confirmation.
	/// </summary>
	public Security UsdJpySecurity
	{
		get => _usdJpySecurity.Value;
		set => _usdJpySecurity.Value = value;
	}

	/// <summary>
	/// Main candle type used for trading signals.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Relationship between the major pair and the traded cross.
	/// </summary>
	public YenTraderMajorDirections MajorDirection
	{
		get => _majorDirection.Value;
		set => _majorDirection.Value = value;
	}

	/// <summary>
	/// Entry behaviour when stacking orders.
	/// </summary>
	public YenTraderEntryModes EntryMode
	{
		get => _entryMode.Value;
		set => _entryMode.Value = value;
	}

	/// <summary>
	/// Price reference used for breakout detection.
	/// </summary>
	public YenTraderPriceReferences PriceReference
	{
		get => _priceReference.Value;
		set => _priceReference.Value = value;
	}

	/// <summary>
	/// Number of bars used for breakout checks.
	/// </summary>
	public int LoopBackBars
	{
		get => _loopBackBars.Value;
		set => _loopBackBars.Value = value;
	}

	/// <summary>
	/// Enable RSI confirmation.
	/// </summary>
	public bool UseRsiFilter
	{
		get => _useRsiFilter.Value;
		set => _useRsiFilter.Value = value;
	}

	/// <summary>
	/// Enable CCI confirmation.
	/// </summary>
	public bool UseCciFilter
	{
		get => _useCciFilter.Value;
		set => _useCciFilter.Value = value;
	}

	/// <summary>
	/// Enable RVI confirmation.
	/// </summary>
	public bool UseRviFilter
	{
		get => _useRviFilter.Value;
		set => _useRviFilter.Value = value;
	}

	/// <summary>
	/// Enable moving average confirmation.
	/// </summary>
	public bool UseMovingAverageFilter
	{
		get => _useMovingAverageFilter.Value;
		set => _useMovingAverageFilter.Value = value;
	}

	/// <summary>
	/// Moving average period.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Moving average calculation mode.
	/// </summary>
	public MovingAverageModes MaMode
	{
		get => _maMode.Value;
		set => _maMode.Value = value;
	}

	/// <summary>
	/// Fixed volume per trade.
	/// </summary>
	public decimal FixedLotSize
	{
		get => _fixedLotSize.Value;
		set => _fixedLotSize.Value = value;
	}

	/// <summary>
	/// Percentage of portfolio balance used when variable sizing is active.
	/// </summary>
	public decimal BalancePercentLotSize
	{
		get => _balancePercentLotSize.Value;
		set => _balancePercentLotSize.Value = value;
	}

	/// <summary>
	/// Maximum number of additive entries.
	/// </summary>
	public int MaxOpenPositions
	{
		get => _maxOpenPositions.Value;
		set => _maxOpenPositions.Value = value;
	}

	/// <summary>
	/// Stop loss distance in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take profit distance in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Break even trigger distance in pips.
	/// </summary>
	public int BreakEvenPips
	{
		get => _breakEvenPips.Value;
		set => _breakEvenPips.Value = value;
	}

	/// <summary>
	/// Profit lock trigger distance in pips.
	/// </summary>
	public int ProfitLockPips
	{
		get => _profitLockPips.Value;
		set => _profitLockPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public int TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Minimum trailing stop update step in pips.
	/// </summary>
	public int TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Close current position when an opposite signal is generated.
	/// </summary>
	public bool CloseOnOpposite
	{
		get => _closeOnOpposite.Value;
		set => _closeOnOpposite.Value = value;
	}

	/// <summary>
	/// Allow adding positions even if an opposite trade is still open.
	/// </summary>
	public bool AllowHedging
	{
		get => _allowHedging.Value;
		set => _allowHedging.Value = value;
	}

	/// <summary>
	/// Use ATR based levels instead of pip distances.
	/// </summary>
	public bool EnableAtrLevels
	{
		get => _enableAtrLevels.Value;
		set => _enableAtrLevels.Value = value;
	}

	/// <summary>
	/// Candle type used for ATR calculations.
	/// </summary>
	public DataType AtrCandleType
	{
		get => _atrCandleType.Value;
		set => _atrCandleType.Value = value;
	}

	/// <summary>
	/// ATR lookback period.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// ATR multiplier for stop loss.
	/// </summary>
	public decimal AtrStopLossMultiplier
	{
		get => _atrStopLossMultiplier.Value;
		set => _atrStopLossMultiplier.Value = value;
	}

	/// <summary>
	/// ATR multiplier for take profit.
	/// </summary>
	public decimal AtrTakeProfitMultiplier
	{
		get => _atrTakeProfitMultiplier.Value;
		set => _atrTakeProfitMultiplier.Value = value;
	}

	/// <summary>
	/// ATR multiplier for trailing stop distance.
	/// </summary>
	public decimal AtrTrailingMultiplier
	{
		get => _atrTrailingMultiplier.Value;
		set => _atrTrailingMultiplier.Value = value;
	}

	/// <summary>
	/// ATR multiplier for break even activation.
	/// </summary>
	public decimal AtrBreakEvenMultiplier
	{
		get => _atrBreakEvenMultiplier.Value;
		set => _atrBreakEvenMultiplier.Value = value;
	}

	/// <summary>
	/// ATR multiplier for profit lock activation.
	/// </summary>
	public decimal AtrProfitLockMultiplier
	{
		get => _atrProfitLockMultiplier.Value;
		set => _atrProfitLockMultiplier.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		if (Security != null)
			yield return (Security, CandleType);

		if (MajorSecurity != null)
			yield return (MajorSecurity, CandleType);

		if (UsdJpySecurity != null)
			yield return (UsdJpySecurity, CandleType);

		if (EnableAtrLevels && Security != null)
			yield return (Security, AtrCandleType);
	}

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

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

		InitializeIndicators();

		var useSingleSecurity = MajorSecurity == null && UsdJpySecurity == null;

		var tradingSubscription = SubscribeCandles(CandleType);

		if (useSingleSecurity)
		{
			// When no separate securities are configured, use the primary security data for all streams.
			tradingSubscription.Bind(c =>
			{
				ProcessMajorCandle(c);
				ProcessUsdJpyCandle(c);
				ProcessTradingCandle(c);
			}).Start();
		}
		else
		{
			tradingSubscription.Bind(ProcessTradingCandle).Start();

			if (MajorSecurity != null)
			{
				var majorSubscription = SubscribeCandles(CandleType, true, MajorSecurity);
				majorSubscription.Bind(ProcessMajorCandle).Start();
			}

			if (UsdJpySecurity != null)
			{
				var usdJpySubscription = SubscribeCandles(CandleType, true, UsdJpySecurity);
				usdJpySubscription.Bind(ProcessUsdJpyCandle).Start();
			}
		}

		if (EnableAtrLevels)
		{
			_atr = new AverageTrueRange { Length = AtrPeriod };
			var atrSubscription = SubscribeCandles(AtrCandleType, true, Security);
			atrSubscription.Bind(ProcessAtrCandle).Start();
		}
		else
		{
			_atr = null;
		}

		StartProtection(null, null);

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

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

		UpdateBreakoutIndicators(candle, _majorHighest, _majorLowest, ref _majorHighestValue, ref _majorLowestValue);
		UpdateOscillators(candle, _majorRsi, ref _majorRsiValue, _majorCci, ref _majorCciValue, _majorRvi, _majorRviSignal, ref _majorRviValue, ref _majorRviSignalValue);
		_majorMaValue = UpdateMovingAverage(_majorMa, candle);

		_majorLastClose = candle.ClosePrice;
		UpdateLookbackQueue(_majorCloses, LoopBackBars, candle.ClosePrice, ref _majorLookbackClose);
	}

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

		UpdateBreakoutIndicators(candle, _usdJpyHighest, _usdJpyLowest, ref _usdJpyHighestValue, ref _usdJpyLowestValue);
		UpdateOscillators(candle, _usdJpyRsi, ref _usdJpyRsiValue, _usdJpyCci, ref _usdJpyCciValue, _usdJpyRvi, _usdJpyRviSignal, ref _usdJpyRviValue, ref _usdJpyRviSignalValue);
		_usdJpyMaValue = UpdateMovingAverage(_usdJpyMa, candle);

		_usdJpyLastClose = candle.ClosePrice;
		UpdateLookbackQueue(_usdJpyCloses, LoopBackBars, candle.ClosePrice, ref _usdJpyLookbackClose);
	}

	private void ProcessAtrCandle(ICandleMessage candle)
	{
		if (!EnableAtrLevels || _atr == null)
			return;

		if (candle.State != CandleStates.Finished)
			return;

		var atrValue = _atr.Process(candle);
		if (atrValue.IsFinal)
		{
			var v = TryGetDecimal(atrValue);
			if (v.HasValue)
				_atrValue = v.Value;
		}
	}

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

		UpdateRiskManagement(candle);

		if (!IsFormed)
			return;

		if (!IsSignalReady())
			return;

		var longSignal = CalculateBreakoutSignal(true);
		var shortSignal = CalculateBreakoutSignal(false);

		ApplyEntryMode(candle, ref longSignal, ref shortSignal);
		ApplyIndicatorFilters(ref longSignal, ref shortSignal);

		if (longSignal)
			TryEnterLong(candle);

		if (shortSignal)
			TryEnterShort(candle);
	}

	private void ApplyEntryMode(ICandleMessage candle, ref bool longSignal, ref bool shortSignal)
	{
		if (EntryMode == YenTraderEntryModes.Averaging)
		{
			longSignal &= candle.ClosePrice < candle.OpenPrice;
			shortSignal &= candle.ClosePrice > candle.OpenPrice;
		}
		else if (EntryMode == YenTraderEntryModes.Pyramiding)
		{
			longSignal &= candle.ClosePrice > candle.OpenPrice;
			shortSignal &= candle.ClosePrice < candle.OpenPrice;
		}
	}

	private void ApplyIndicatorFilters(ref bool longSignal, ref bool shortSignal)
	{
		if (!longSignal && !shortSignal)
			return;

		if (UseRsiFilter)
		{
			if (!_majorRsiValue.HasValue || !_usdJpyRsiValue.HasValue)
			{
				longSignal = false;
				shortSignal = false;
			}
			else if (MajorDirection == YenTraderMajorDirections.Left)
			{
				longSignal &= _majorRsiValue > 50m && _usdJpyRsiValue > 50m;
				shortSignal &= _majorRsiValue < 50m && _usdJpyRsiValue < 50m;
			}
			else
			{
				longSignal &= _majorRsiValue < 50m && _usdJpyRsiValue > 50m;
				shortSignal &= _majorRsiValue > 50m && _usdJpyRsiValue < 50m;
			}
		}

		if (UseCciFilter && (longSignal || shortSignal))
		{
			if (!_majorCciValue.HasValue || !_usdJpyCciValue.HasValue)
			{
				longSignal = false;
				shortSignal = false;
			}
			else if (MajorDirection == YenTraderMajorDirections.Left)
			{
				longSignal &= _majorCciValue > 0m && _usdJpyCciValue > 0m;
				shortSignal &= _majorCciValue < 0m && _usdJpyCciValue < 0m;
			}
			else
			{
				longSignal &= _majorCciValue < 0m && _usdJpyCciValue > 0m;
				shortSignal &= _majorCciValue > 0m && _usdJpyCciValue < 0m;
			}
		}

		if (UseRviFilter && (longSignal || shortSignal))
		{
			if (!_majorRviValue.HasValue || !_usdJpyRviValue.HasValue || !_majorRviSignalValue.HasValue || !_usdJpyRviSignalValue.HasValue)
			{
				longSignal = false;
				shortSignal = false;
			}
			else if (MajorDirection == YenTraderMajorDirections.Left)
			{
				longSignal &= _majorRviValue > _majorRviSignalValue && _usdJpyRviValue > _usdJpyRviSignalValue;
				shortSignal &= _majorRviValue < _majorRviSignalValue && _usdJpyRviValue < _usdJpyRviSignalValue;
			}
			else
			{
				longSignal &= _majorRviValue < _majorRviSignalValue && _usdJpyRviValue > _usdJpyRviSignalValue;
				shortSignal &= _majorRviValue > _majorRviSignalValue && _usdJpyRviValue < _usdJpyRviSignalValue;
			}
		}

		if (UseMovingAverageFilter && (longSignal || shortSignal))
		{
			if (!_majorMaValue.HasValue || !_usdJpyMaValue.HasValue || !_majorLastClose.HasValue || !_usdJpyLastClose.HasValue)
			{
				longSignal = false;
				shortSignal = false;
			}
			else if (MajorDirection == YenTraderMajorDirections.Left)
			{
				longSignal &= _majorLastClose > _majorMaValue && _usdJpyLastClose > _usdJpyMaValue;
				shortSignal &= _majorLastClose < _majorMaValue && _usdJpyLastClose < _usdJpyMaValue;
			}
			else
			{
				longSignal &= _majorLastClose < _majorMaValue && _usdJpyLastClose > _usdJpyMaValue;
				shortSignal &= _majorLastClose > _majorMaValue && _usdJpyLastClose < _usdJpyMaValue;
			}
		}
	}

	private bool CalculateBreakoutSignal(bool isLong)
	{
		if (_majorLastClose == null || _usdJpyLastClose == null)
			return false;

		if (LoopBackBars <= 1)
			return true;

		if (PriceReference == YenTraderPriceReferences.HighLow)
		{
			if (_majorHighestValue == null || _majorLowestValue == null || _usdJpyHighestValue == null || _usdJpyLowestValue == null)
				return false;

			return MajorDirection == YenTraderMajorDirections.Left
				? isLong
					? _majorLastClose > _majorHighestValue && _usdJpyLastClose > _usdJpyHighestValue
					: _majorLastClose < _majorLowestValue && _usdJpyLastClose < _usdJpyLowestValue
				: isLong
					? _majorLastClose < _majorLowestValue && _usdJpyLastClose > _usdJpyHighestValue
					: _majorLastClose > _majorHighestValue && _usdJpyLastClose < _usdJpyLowestValue;
		}

		if (_majorLookbackClose == null || _usdJpyLookbackClose == null)
			return false;

		return MajorDirection == YenTraderMajorDirections.Left
			? isLong
				? _majorLastClose > _majorLookbackClose && _usdJpyLastClose > _usdJpyLookbackClose
				: _majorLastClose < _majorLookbackClose && _usdJpyLastClose < _usdJpyLookbackClose
			: isLong
				? _majorLastClose < _majorLookbackClose && _usdJpyLastClose > _usdJpyLookbackClose
				: _majorLastClose > _majorLookbackClose && _usdJpyLastClose < _usdJpyLookbackClose;
	}

	private void TryEnterLong(ICandleMessage candle)
	{
		if (Position > 0 && MaxOpenPositions <= 1)
			return;

		if (!AllowHedging && Position < 0)
			return;

		var orderVolume = GetOrderVolume(candle.ClosePrice, Sides.Buy);
		if (orderVolume <= 0m)
			return;

		var totalVolume = orderVolume;
		if (CloseOnOpposite && Position < 0)
			totalVolume += Math.Abs(Position);

		if (totalVolume <= 0m)
			return;

		BuyMarket(totalVolume);
		InitializePositionState(candle, Sides.Buy);
	}

	private void TryEnterShort(ICandleMessage candle)
	{
		if (Position < 0 && MaxOpenPositions <= 1)
			return;

		if (!AllowHedging && Position > 0)
			return;

		var orderVolume = GetOrderVolume(candle.ClosePrice, Sides.Sell);
		if (orderVolume <= 0m)
			return;

		var totalVolume = orderVolume;
		if (CloseOnOpposite && Position > 0)
			totalVolume += Math.Abs(Position);

		if (totalVolume <= 0m)
			return;

		SellMarket(totalVolume);
		InitializePositionState(candle, Sides.Sell);
	}

	private void InitializePositionState(ICandleMessage candle, Sides side)
	{
		_entryPrice = candle.ClosePrice;
		_stopPrice = null;
		_takeProfitPrice = null;
		_breakEvenActivated = false;
		_profitLockActivated = false;
		_highestSinceEntry = candle.HighPrice;
		_lowestSinceEntry = candle.LowPrice;

		var atrDistance = EnableAtrLevels ? _atrValue : null;
		var stopDistance = GetDistance(StopLossPips, AtrStopLossMultiplier, atrDistance);
		var takeDistance = GetDistance(TakeProfitPips, AtrTakeProfitMultiplier, atrDistance);

		if (side == Sides.Buy)
		{
			if (stopDistance > 0m)
				_stopPrice = _entryPrice - stopDistance;

			if (takeDistance > 0m)
				_takeProfitPrice = _entryPrice + takeDistance;
		}
		else
		{
			if (stopDistance > 0m)
				_stopPrice = _entryPrice + stopDistance;

			if (takeDistance > 0m)
				_takeProfitPrice = _entryPrice - takeDistance;
		}
	}

	private void UpdateRiskManagement(ICandleMessage candle)
	{
		if (Position == 0)
		{
			ResetPositionState();
			return;
		}

		if (_entryPrice == null)
			return;

		if (Position > 0)
		{
			_highestSinceEntry = Math.Max(_highestSinceEntry, candle.HighPrice);

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(Position);
				ResetPositionState();
				return;
			}

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

			ApplyTrailingRules(candle, Sides.Buy);
		}
		else
		{
			_lowestSinceEntry = Math.Min(_lowestSinceEntry, candle.LowPrice);

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

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

			ApplyTrailingRules(candle, Sides.Sell);
		}
	}

	private void ApplyTrailingRules(ICandleMessage candle, Sides side)
	{
		if (_entryPrice == null)
			return;

		var atrDistance = EnableAtrLevels ? _atrValue : null;
		var breakEvenDistance = GetDistance(BreakEvenPips, AtrBreakEvenMultiplier, atrDistance);
		var profitLockDistance = GetDistance(ProfitLockPips, AtrProfitLockMultiplier, atrDistance);
		var trailingDistance = GetDistance(TrailingStopPips, AtrTrailingMultiplier, atrDistance);
		var trailingStep = ConvertPipsToPrice(TrailingStepPips);

		if (side == Sides.Buy)
		{
			if (!_breakEvenActivated && breakEvenDistance > 0m && candle.HighPrice >= _entryPrice + breakEvenDistance)
			{
				var newStop = (_entryPrice + breakEvenDistance).Value;
				_stopPrice = _stopPrice.HasValue ? Math.Max(_stopPrice.Value, newStop) : newStop;
				_breakEvenActivated = true;
			}

			if (!_profitLockActivated && profitLockDistance > 0m && candle.HighPrice >= _entryPrice + profitLockDistance)
			{
				var newStop = (_entryPrice + profitLockDistance).Value;
				_stopPrice = _stopPrice.HasValue ? Math.Max(_stopPrice.Value, newStop) : newStop;
				_profitLockActivated = true;
			}

			if (trailingDistance > 0m)
			{
				var desiredStop = Math.Max(_entryPrice.Value, candle.HighPrice - trailingDistance);
				if (!_stopPrice.HasValue || desiredStop > _stopPrice.Value + trailingStep)
					_stopPrice = desiredStop;
			}
		}
		else
		{
			if (!_breakEvenActivated && breakEvenDistance > 0m && candle.LowPrice <= _entryPrice - breakEvenDistance)
			{
				var newStop = (_entryPrice - breakEvenDistance).Value;
				_stopPrice = _stopPrice.HasValue ? Math.Min(_stopPrice.Value, newStop) : newStop;
				_breakEvenActivated = true;
			}

			if (!_profitLockActivated && profitLockDistance > 0m && candle.LowPrice <= _entryPrice - profitLockDistance)
			{
				var newStop = (_entryPrice - profitLockDistance).Value;
				_stopPrice = _stopPrice.HasValue ? Math.Min(_stopPrice.Value, newStop) : newStop;
				_profitLockActivated = true;
			}

			if (trailingDistance > 0m)
			{
				var desiredStop = Math.Min(_entryPrice.Value, candle.LowPrice + trailingDistance);
				if (!_stopPrice.HasValue || desiredStop < _stopPrice.Value - trailingStep)
					_stopPrice = desiredStop;
			}
		}
	}

	private decimal GetOrderVolume(decimal price, Sides side)
	{
		var baseVolume = FixedLotSize > 0m ? FixedLotSize : Volume;

		if (FixedLotSize <= 0m && BalancePercentLotSize > 0m && Portfolio != null && price > 0m)
		{
			var portfolioValue = Portfolio.CurrentValue ?? 0m;
			if (portfolioValue > 0m)
				baseVolume = portfolioValue * BalancePercentLotSize / 100m / price;
		}

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m && baseVolume > 0m)
			baseVolume = Math.Max(step, Math.Round(baseVolume / step) * step);

		if (MaxOpenPositions > 0 && Security != null)
		{
			var current = side == Sides.Buy ? Math.Max(0m, Position) : Math.Max(0m, -Position);
			var maxVolume = baseVolume * MaxOpenPositions;
			var available = maxVolume - current;
			if (available <= 0m)
				return 0m;

			baseVolume = Math.Min(baseVolume, available);
		}

		return Math.Max(0m, baseVolume);
	}

	private decimal GetDistance(int pips, decimal multiplier, decimal? atrValue)
	{
		if (EnableAtrLevels && atrValue.HasValue)
			return atrValue.Value * multiplier;

		if (pips <= 0)
			return 0m;

		return ConvertPipsToPrice(pips);
	}

	private decimal ConvertPipsToPrice(int pips)
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? pips * step : pips;
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_breakEvenActivated = false;
		_profitLockActivated = false;
		_highestSinceEntry = 0m;
		_lowestSinceEntry = 0m;
	}

	private void ResetState()
	{
		_majorCloses.Clear();
		_usdJpyCloses.Clear();

		_majorLastClose = null;
		_majorLookbackClose = null;
		_majorHighestValue = null;
		_majorLowestValue = null;
		_majorRsiValue = null;
		_majorCciValue = null;
		_majorRviValue = null;
		_majorRviSignalValue = null;
		_majorMaValue = null;

		_usdJpyLastClose = null;
		_usdJpyLookbackClose = null;
		_usdJpyHighestValue = null;
		_usdJpyLowestValue = null;
		_usdJpyRsiValue = null;
		_usdJpyCciValue = null;
		_usdJpyRviValue = null;
		_usdJpyRviSignalValue = null;
		_usdJpyMaValue = null;

		_atrValue = null;

		ResetPositionState();
	}

	private bool IsSignalReady()
	{
		if (_majorLastClose == null || _usdJpyLastClose == null)
			return false;

		if (LoopBackBars > 1)
		{
			if (PriceReference == YenTraderPriceReferences.HighLow)
			{
				if (_majorHighestValue == null || _majorLowestValue == null || _usdJpyHighestValue == null || _usdJpyLowestValue == null)
					return false;
			}
			else
			{
				if (_majorLookbackClose == null || _usdJpyLookbackClose == null)
					return false;
			}
		}

		if (UseRsiFilter && (!_majorRsiValue.HasValue || !_usdJpyRsiValue.HasValue))
			return false;

		if (UseCciFilter && (!_majorCciValue.HasValue || !_usdJpyCciValue.HasValue))
			return false;

		if (UseRviFilter && (!_majorRviValue.HasValue || !_majorRviSignalValue.HasValue || !_usdJpyRviValue.HasValue || !_usdJpyRviSignalValue.HasValue))
			return false;

		if (UseMovingAverageFilter && (!_majorMaValue.HasValue || !_usdJpyMaValue.HasValue))
			return false;

		return true;
	}

	private void InitializeIndicators()
	{
		var breakoutLength = Math.Max(LoopBackBars, 2);

		_majorHighest = new Highest { Length = breakoutLength };
		_majorLowest = new Lowest { Length = breakoutLength };
		_majorRsi = new RelativeStrengthIndex { Length = 14 };
		_majorCci = new CommodityChannelIndex { Length = 14 };
		_majorRvi = new RelativeVigorIndex();
		_majorRviSignal = new SimpleMovingAverage { Length = 4 };
		_majorMa = CreateMovingAverage(MaMode, MaPeriod);

		_usdJpyHighest = new Highest { Length = breakoutLength };
		_usdJpyLowest = new Lowest { Length = breakoutLength };
		_usdJpyRsi = new RelativeStrengthIndex { Length = 14 };
		_usdJpyCci = new CommodityChannelIndex { Length = 14 };
		_usdJpyRvi = new RelativeVigorIndex();
		_usdJpyRviSignal = new SimpleMovingAverage { Length = 4 };
		_usdJpyMa = CreateMovingAverage(MaMode, MaPeriod);
	}

	private static void UpdateBreakoutIndicators(ICandleMessage candle, Highest highest, Lowest lowest, ref decimal? highValue, ref decimal? lowValue)
	{
		var highVal = highest.Process(candle);
		if (highVal.IsFinal)
		{
			var v = TryGetDecimal(highVal);
			if (v.HasValue)
				highValue = v.Value;
		}

		var lowVal = lowest.Process(candle);
		if (lowVal.IsFinal)
		{
			var v = TryGetDecimal(lowVal);
			if (v.HasValue)
				lowValue = v.Value;
		}
	}

	private static decimal? TryGetDecimal(IIndicatorValue value)
	{
		if (value == null || value.IsEmpty)
			return null;

		try
		{
			return value.ToDecimal();
		}
		catch
		{
			return null;
		}
	}

	private static void UpdateOscillators(
		ICandleMessage candle,
		RelativeStrengthIndex rsi,
		ref decimal? rsiValue,
		CommodityChannelIndex cci,
		ref decimal? cciValue,
		RelativeVigorIndex rvi,
		SimpleMovingAverage rviSignal,
		ref decimal? rviMain,
		ref decimal? rviSignalValue)
	{
		var rsiInput = new DecimalIndicatorValue(rsi, candle.ClosePrice, candle.CloseTime) { IsFinal = true };
		var rsiVal = rsi.Process(rsiInput);
		if (rsiVal.IsFinal)
		{
			var v = TryGetDecimal(rsiVal);
			if (v.HasValue)
				rsiValue = v.Value;
		}

		var cciVal = cci.Process(candle);
		if (cciVal.IsFinal)
		{
			var v = TryGetDecimal(cciVal);
			if (v.HasValue)
				cciValue = v.Value;
		}

		var rviVal = rvi.Process(candle);
		if (rviVal.IsFinal)
		{
			var v = TryGetDecimal(rviVal);
			if (v.HasValue)
			{
				rviMain = v.Value;

				var signalVal = rviSignal.Process(new DecimalIndicatorValue(rviSignal, v.Value, candle.CloseTime) { IsFinal = true });
				if (signalVal.IsFinal)
				{
					var sv = TryGetDecimal(signalVal);
					if (sv.HasValue)
						rviSignalValue = sv.Value;
				}
			}
		}
	}

	private static decimal? UpdateMovingAverage(IIndicator indicator, ICandleMessage candle)
	{
		var input = new DecimalIndicatorValue(indicator, candle.ClosePrice, candle.CloseTime) { IsFinal = true };
		var value = indicator.Process(input);
		return value.IsFinal ? TryGetDecimal(value) : null;
	}

	private static void UpdateLookbackQueue(Queue<decimal> queue, int loopBackBars, decimal close, ref decimal? lookback)
	{
		queue.Enqueue(close);

		var maxCount = Math.Max(loopBackBars + 1, 2);
		while (queue.Count > maxCount)
			queue.Dequeue();

		if (loopBackBars > 0 && queue.Count > loopBackBars)
		{
			var values = queue.ToArray();
			var index = values.Length - 1 - loopBackBars;
			if (index >= 0)
				lookback = values[index];
		}
		else
		{
			lookback = null;
		}
	}

	private static IIndicator CreateMovingAverage(MovingAverageModes mode, int period)
	{
		var length = Math.Max(1, period);

		return mode switch
		{
			MovingAverageModes.Simple => new SimpleMovingAverage { Length = length },
			MovingAverageModes.Exponential => new ExponentialMovingAverage { Length = length },
			MovingAverageModes.Smoothed => new SmoothedMovingAverage { Length = length },
			MovingAverageModes.LinearWeighted => new WeightedMovingAverage { Length = length },
			_ => new SimpleMovingAverage { Length = length },
		};
	}

	/// <summary>
	/// Entry stacking behaviour.
	/// </summary>
	public enum YenTraderEntryModes
	{
		/// <summary>Allow both averaging and pyramiding entries.</summary>
		Both,
		/// <summary>Only add to profitable trades.</summary>
		Pyramiding,
		/// <summary>Only add to losing trades.</summary>
		Averaging
	}

	/// <summary>
	/// Mapping between major pair and traded cross.
	/// </summary>
	public enum YenTraderMajorDirections
	{
		/// <summary>Major pair acts as the left component.</summary>
		Left,
		/// <summary>Major pair acts as the right component.</summary>
		Right
	}

	/// <summary>
	/// Breakout reference type.
	/// </summary>
	public enum YenTraderPriceReferences
	{
		/// <summary>Use delayed close values.</summary>
		Close,
		/// <summary>Use highest highs and lowest lows.</summary>
		HighLow
	}

	/// <summary>
	/// Moving average calculation modes supported by the strategy.
	/// </summary>
	public enum MovingAverageModes
	{
		/// <summary>Simple moving average.</summary>
		Simple,
		/// <summary>Exponential moving average.</summary>
		Exponential,
		/// <summary>Smoothed moving average.</summary>
		Smoothed,
		/// <summary>Linear weighted moving average.</summary>
		LinearWeighted
	}
}