在 GitHub 上查看

日元交易者 05.1 策略(C#)

概述

日元交易者 05.1 策略重现了原始 MetaTrader 智能交易系统,通过三条货币对之间的三角关系寻找突破机会:

  • 交易交叉盘 —— 本策略实例绑定的证券(例如 GBPJPY)。
  • 主要货币对 —— 交叉盘基准货币与美元的组合(例如 GBPUSD),用于捕捉主驱动趋势。
  • USDJPY —— 用于确认日元腿的动量。

当主要货币对出现突破并得到 USDJPY 的同步确认时,策略会在交叉盘上建立头寸。可选的 RSI、CCI、RVI 与均线过滤器帮助筛选信号。仓位管理同时支持金字塔加仓与摊平补仓,风险控制部分完整复刻了 EA 中基于点差和 ATR 的止损逻辑。

交易逻辑

  1. 突破检测
    • LoopBackBars 指定回溯窗口。当大于 1 时,策略会检查:
      • 最近的最高价/最低价(PriceReference = HighLow),或
      • LoopBackBars 根之前的收盘价(PriceReference = Close)。
    • MajorDirection 决定主要货币对与日元腿应当如何协同(Left 表示交叉盘报价为主要货币/日元,Right 表示日元/主要货币)。
  2. 过滤器
    • UseRsiFilter 要求 RSI 位于 50 上方或下方以确认趋势方向。
    • UseCciFilter 强制 CCI 为正或为负。
    • UseRviFilter 等待 RVI 上穿或下穿其信号线。信号线使用 4 周期简单移动平均,与 MT4 中的 MODE_SIGNAL 一致。
    • UseMovingAverageFilter 通过指定周期与类型的均线确保入场方向顺应趋势。
  3. 入场模式
    • EntryMode = Both 允许任意突破信号。
    • EntryMode = Pyramiding 仅在顺势 K 线后加仓。
    • EntryMode = Averaging 仅在逆势 K 线后补仓摊平。
  4. 仓位管理
    • FixedLotSize 设定固定下单量。
    • 当固定手数为 0 时,使用 BalancePercentLotSize 与组合市值计算动态手数。
    • MaxOpenPositions 限制最大加仓次数(累积仓位规模)。
  5. 风险控制
    • 所有点差参数(StopLossPipsTakeProfitPipsBreakEvenPipsProfitLockPipsTrailingStopPipsTrailingStepPips)都会通过 Security.MinPriceStep 转换为价格距离。
    • 启用 EnableAtrLevels 后,策略改用 ATR 距离,ATR 通过 AtrCandleTypeAtrPeriod 订阅的 K 线计算,倍数由各个乘数参数控制。
    • 止损、止盈、保本、锁盈与跟踪止损均基于收盘数据更新,保持与原 EA 相同的节奏。
    • CloseOnOpposite 在出现反向信号时会平掉已有仓位并按需要反向建仓。
    • AllowHedging 允许在持有反向仓位时继续加仓。由于 StockSharp 采用净仓位模型,无法真正同时持有多空仓,该选项仅决定策略是否允许在当前净仓与信号方向相反时直接翻仓。

参数一览

组别 名称 说明
仪表 MajorSecurity 用于确认的主要货币对。
UsdJpySecurity 用于确认日元腿的 USDJPY。
数据 CandleType 三个货币对共享的信号级别。
过滤 MajorDirection 主要货币对与交叉盘的方向关系。
PriceReference 突破参考类型:高低点或延迟收盘价。
LoopBackBars 回溯计算的历史根数。
EntryMode 加仓模式:同时支持、仅金字塔或仅摊平。
指标 UseRsiFilterUseCciFilterUseRviFilterUseMovingAverageFilter 是否启用各类过滤器。
MaPeriodMaMode 均线周期与类型。
风险 FixedLotSizeBalancePercentLotSize 下单量控制。
MaxOpenPositions 最大加仓次数。
StopLossPips 基于点差的风险参数。
EnableAtrLevels 及 ATR 相关参数 基于 ATR 的风险设置。
行为 CloseOnOpposite 是否在反向信号时平仓。
AllowHedging 是否允许在反向净仓位情况下继续入场。

使用建议

  • 将交易交叉盘赋给策略的 Security 属性,同时配置 MajorSecurityUsdJpySecurity
  • 动态手数需要组合估值,运行前确保投资组合已连接并更新 Portfolio.CurrentValue
  • 请保证三个货币对的 K 线时间轴一致,如来自不同市场,可预先统一到同一时间框架。
  • ATR 订阅使用 AtrCandleType 指定的级别,保持默认(日线、21 周期)即可与原 EA 行为一致。
  • 风险控制基于收盘价执行,触发后通过市价单平仓,模拟 EA 中的止损移动逻辑。

与 MT4 版本的差异

  • StockSharp 以净仓位计算,不支持真正的对冲持仓。AllowHedging 仅决定策略是否允许自动翻仓。
  • 原 EA 在 tick 级别修改挂单止损,本策略在收盘时检测阈值后以市价单离场。
  • RVI 信号线通过对 RVI 值进行四周期 SMA 计算,与 MT4 的实现保持一致。
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
	}
}