Auf GitHub ansehen

Self-Optimizing RSI or MFI Trader v3

Overview

This strategy ports the MetaTrader "Self Optimizing RSI or MFI Trader" expert advisor to StockSharp's high level API. On every finished candle the algorithm backtests a sliding window of historical bars and finds the most profitable overbought and oversold thresholds for the selected oscillator. Live trades are only taken when the current oscillator value crosses the best performing threshold in the same direction as the historical edge, optionally without requiring a cross in "aggressive" mode. Position exits rely on ATR-based or fixed-distance stops and targets with an optional breakeven step.

Market Data

  • Works with any instrument that provides OHLC candles and volume (MFI requires volume).
  • Uses the timeframe specified by the CandleType parameter. The default is 15-minute candles but you can attach any time frame supported by the venue adapter.

Indicators

  • Relative Strength Index (RSI) or Money Flow Index (MFI) depending on the IndicatorChoice parameter. Both share the same averaging length.
  • Average True Range (ATR) for ATR-based stop-loss / take-profit sizing when UseDynamicTargets is enabled.

Trading Logic

  1. Maintain a rolling history of OptimizingPeriods + 1 finished candles with their oscillator values and close prices.
  2. For each integer level between IndicatorBottomValue and IndicatorTopValue the strategy simulates trades in the historical window:
    • Short simulation: count how many times the oscillator crossed below the level and whether a short stop-loss or take-profit would have been hit first.
    • Long simulation: count how many times the oscillator crossed above the level and how profitable the trades would have been.
  3. Choose the threshold that delivered the highest simulated profit for each direction. If TradeReverse is enabled the profitability scores are swapped so that the opposite direction becomes favoured.
  4. When the live oscillator crosses the best level in the profitable direction (or immediately when UseAggressiveEntries is true) the strategy opens a position, respecting OneOrderAtATime.
  5. Exit management:
    • Stop-loss and take-profit levels are calculated either from ATR multiples (StopLossAtrMultiplier, TakeProfitAtrMultiplier) or from fixed point distances (StaticStopLossPoints, StaticTakeProfitPoints).
    • UseBreakEven moves the stop to the entry price plus BreakEvenPaddingPoints once unrealised profit reaches BreakEvenTriggerPoints.
    • Positions are closed when either stop-loss or take-profit prices are crossed.

Risk Management

  • Dynamic sizing: when UseDynamicVolume is true the strategy risks RiskPercent of the current portfolio value. The calculation converts the stop distance into monetary risk using the security's PriceStep and StepPrice.
  • Static sizing: when disabled, BaseVolume lots are traded on every entry.
  • Breakeven guard: ensures winning trades are protected once sufficient profit has accrued.

Parameters

Parameter Description
OptimizingPeriods Number of bars used for the rolling in-sample optimisation (default 144).
IndicatorChoice Chooses RSI or MFI as the driving oscillator.
IndicatorPeriod Averaging period for the oscillator and ATR.
IndicatorTopValue / IndicatorBottomValue Search bounds for threshold levels (typically 0–100).
UseAggressiveEntries If true, allows entries without a confirmed cross.
TradeReverse Swaps profitability scores to trade the historically losing side.
OneOrderAtATime Prevents opening a new position while another is active.
UseDynamicTargets Switch between ATR-based and fixed-point stops/targets.
StopLossAtrMultiplier, TakeProfitAtrMultiplier ATR multipliers for dynamic exits.
StaticStopLossPoints, StaticTakeProfitPoints Point distances for fixed exits.
UseBreakEven, BreakEvenTriggerPoints, BreakEvenPaddingPoints Configure the breakeven stop behaviour.
UseDynamicVolume, RiskPercent, BaseVolume Control the position sizing logic.
CandleType Timeframe for optimisation and trading.

Implementation Notes

  • The strategy uses StockSharp's SubscribeCandles().Bind(...) pipeline, so it only runs on completed candles.
  • OneOrderAtATime should remain enabled when trading in a netted account, because the implementation tracks a single aggregated position.
  • ATR-based exits require a valid ATR value; the strategy will skip trading until the indicator is fully formed.
  • When using MFI ensure the data feed supplies volume, otherwise the indicator returns zero and no trades will be generated.

Optimisation Tips

  • Optimise OptimizingPeriods, oscillator period, and ATR multipliers together to match the instrument's volatility regime.
  • Different assets may benefit from narrower level ranges (e.g., 20–80) to reduce noise.
  • Consider forward-testing with walk-forward analysis because the strategy adapts thresholds continuously.

Usage

  1. Add the strategy to a connector in the Designer or run it programmatically.
  2. Set the desired security, portfolio, and parameter values.
  3. Start the strategy; it will begin trading once enough candles are accumulated for optimisation.

Limitations

  • Historic optimisation occurs on every bar and may be CPU intensive for very large OptimizingPeriods or wide level ranges.
  • Because levels are integers, fine-grained thresholds (e.g., 70.5) are not tested.
  • The approach assumes the recent past remains predictive; sudden regime shifts can degrade performance, so monitor live results and adjust configuration when necessary.
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;

using StockSharp.Algo.Candles;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Strategy that dynamically optimizes RSI or MFI threshold levels over a rolling history window.
/// Chooses the most profitable overbought/oversold levels and executes trades with ATR or point based risk control.
/// </summary>
public class SelfOptimizingRsiOrMfiTraderV3Strategy : Strategy
{
	private readonly StrategyParam<int> _optimizingPeriods;
	private readonly StrategyParam<bool> _useAggressiveEntries;
	private readonly StrategyParam<bool> _tradeReverse;
	private readonly StrategyParam<bool> _oneOrderAtATime;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<bool> _useDynamicVolume;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<IndicatorSources> _indicatorChoice;
	private readonly StrategyParam<int> _indicatorTopValue;
	private readonly StrategyParam<int> _indicatorBottomValue;
	private readonly StrategyParam<int> _indicatorPeriod;
	private readonly StrategyParam<bool> _useDynamicTargets;
	private readonly StrategyParam<int> _staticStopLossPoints;
	private readonly StrategyParam<int> _staticTakeProfitPoints;
	private readonly StrategyParam<decimal> _stopLossAtrMultiplier;
	private readonly StrategyParam<decimal> _takeProfitAtrMultiplier;
	private readonly StrategyParam<bool> _useBreakEven;
	private readonly StrategyParam<int> _breakEvenTriggerPoints;
	private readonly StrategyParam<int> _breakEvenPaddingPoints;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<(decimal indicator, decimal close)> _history = new();

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;

	private IIndicator _indicator;
	private AverageTrueRange _atr;

	/// <summary>
	/// Indicator source used for optimization.
	/// </summary>
	public enum IndicatorSources
	{
		/// <summary>
		/// Use Relative Strength Index values.
		/// </summary>
		RelativeStrengthIndex,

		/// <summary>
		/// Use Money Flow Index values.
		/// </summary>
		MoneyFlowIndex,
	}

	/// <summary>
	/// Number of bars evaluated when searching for best thresholds.
	/// </summary>
	public int OptimizingPeriods
	{
		get => _optimizingPeriods.Value;
		set => _optimizingPeriods.Value = value;
	}

	/// <summary>
	/// Allow entries without waiting for indicator crosses.
	/// </summary>
	public bool UseAggressiveEntries
	{
		get => _useAggressiveEntries.Value;
		set => _useAggressiveEntries.Value = value;
	}

	/// <summary>
	/// Invert profitability preference to trade opposite direction.
	/// </summary>
	public bool TradeReverse
	{
		get => _tradeReverse.Value;
		set => _tradeReverse.Value = value;
	}

	/// <summary>
	/// Restrict strategy to a single open position at a time.
	/// </summary>
	public bool OneOrderAtATime
	{
		get => _oneOrderAtATime.Value;
		set => _oneOrderAtATime.Value = value;
	}

	/// <summary>
	/// Static volume used when dynamic sizing is disabled.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Enable risk based position sizing.
	/// </summary>
	public bool UseDynamicVolume
	{
		get => _useDynamicVolume.Value;
		set => _useDynamicVolume.Value = value;
	}

	/// <summary>
	/// Percentage of portfolio risked per trade when sizing dynamically.
	/// </summary>
	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

	/// <summary>
	/// Oscillator used for optimization.
	/// </summary>
	public IndicatorSources IndicatorChoice
	{
		get => _indicatorChoice.Value;
		set => _indicatorChoice.Value = value;
	}

	/// <summary>
	/// Highest threshold tested when searching for overbought levels.
	/// </summary>
	public int IndicatorTopValue
	{
		get => _indicatorTopValue.Value;
		set => _indicatorTopValue.Value = value;
	}

	/// <summary>
	/// Lowest threshold tested when searching for oversold levels.
	/// </summary>
	public int IndicatorBottomValue
	{
		get => _indicatorBottomValue.Value;
		set => _indicatorBottomValue.Value = value;
	}

	/// <summary>
	/// Period used for the selected indicator.
	/// </summary>
	public int IndicatorPeriod
	{
		get => _indicatorPeriod.Value;
		set => _indicatorPeriod.Value = value;
	}

	/// <summary>
	/// Enable ATR based stop-loss and take-profit levels.
	/// </summary>
	public bool UseDynamicTargets
	{
		get => _useDynamicTargets.Value;
		set => _useDynamicTargets.Value = value;
	}

	/// <summary>
	/// Static stop-loss distance expressed in points when dynamic targets are disabled.
	/// </summary>
	public int StaticStopLossPoints
	{
		get => _staticStopLossPoints.Value;
		set => _staticStopLossPoints.Value = value;
	}

	/// <summary>
	/// Static take-profit distance expressed in points when dynamic targets are disabled.
	/// </summary>
	public int StaticTakeProfitPoints
	{
		get => _staticTakeProfitPoints.Value;
		set => _staticTakeProfitPoints.Value = value;
	}

	/// <summary>
	/// ATR multiplier applied to stop-loss when dynamic targets are enabled.
	/// </summary>
	public decimal StopLossAtrMultiplier
	{
		get => _stopLossAtrMultiplier.Value;
		set => _stopLossAtrMultiplier.Value = value;
	}

	/// <summary>
	/// ATR multiplier applied to take-profit when dynamic targets are enabled.
	/// </summary>
	public decimal TakeProfitAtrMultiplier
	{
		get => _takeProfitAtrMultiplier.Value;
		set => _takeProfitAtrMultiplier.Value = value;
	}

	/// <summary>
	/// Enable stop adjustment to breakeven once profit target is reached.
	/// </summary>
	public bool UseBreakEven
	{
		get => _useBreakEven.Value;
		set => _useBreakEven.Value = value;
	}

	/// <summary>
	/// Profit threshold in points required to arm the breakeven stop.
	/// </summary>
	public int BreakEvenTriggerPoints
	{
		get => _breakEvenTriggerPoints.Value;
		set => _breakEvenTriggerPoints.Value = value;
	}

	/// <summary>
	/// Additional padding in points applied once breakeven triggers.
	/// </summary>
	public int BreakEvenPaddingPoints
	{
		get => _breakEvenPaddingPoints.Value;
		set => _breakEvenPaddingPoints.Value = value;
	}

	/// <summary>
	/// Candle type processed by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="SelfOptimizingRsiOrMfiTraderV3Strategy"/>.
	/// </summary>
	public SelfOptimizingRsiOrMfiTraderV3Strategy()
	{
		_optimizingPeriods = Param(nameof(OptimizingPeriods), 30)
			.SetGreaterThanZero()
			.SetDisplay("Optimization Bars", "Number of bars used for optimization", "General")

			.SetOptimize(20, 100, 10);

		_useAggressiveEntries = Param(nameof(UseAggressiveEntries), false)
			.SetDisplay("Aggressive Entries", "Allow entries without indicator crosses", "Trading");

		_tradeReverse = Param(nameof(TradeReverse), false)
			.SetDisplay("Reverse Trading", "Swap profitability preference for opposite trades", "Trading");

		_oneOrderAtATime = Param(nameof(OneOrderAtATime), true)
			.SetDisplay("One Position", "Permit only one open position", "Trading");

		_baseVolume = Param(nameof(BaseVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Base Volume", "Static order volume when sizing manually", "Risk");

		_useDynamicVolume = Param(nameof(UseDynamicVolume), true)
			.SetDisplay("Dynamic Volume", "Use risk percentage for position sizing", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 2m)
			.SetRange(0.1m, 10m)
			.SetDisplay("Risk %", "Percent of capital risked per trade", "Risk");

		_indicatorChoice = Param(nameof(IndicatorChoice), IndicatorSources.RelativeStrengthIndex)
			.SetDisplay("Indicator", "Oscillator optimized by the strategy", "Indicator");

		_indicatorTopValue = Param(nameof(IndicatorTopValue), 100)
			.SetDisplay("Top Level", "Upper bound for level search", "Indicator");

		_indicatorBottomValue = Param(nameof(IndicatorBottomValue), 0)
			.SetDisplay("Bottom Level", "Lower bound for level search", "Indicator");

		_indicatorPeriod = Param(nameof(IndicatorPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("Indicator Period", "Averaging period for RSI or MFI", "Indicator");

		_useDynamicTargets = Param(nameof(UseDynamicTargets), true)
			.SetDisplay("Dynamic Targets", "Use ATR based stop-loss and take-profit", "Risk");

		_staticStopLossPoints = Param(nameof(StaticStopLossPoints), 1000)
			.SetGreaterThanZero()
			.SetDisplay("Static Stop", "Stop-loss in points when dynamic targets disabled", "Risk");

		_staticTakeProfitPoints = Param(nameof(StaticTakeProfitPoints), 2000)
			.SetGreaterThanZero()
			.SetDisplay("Static Take", "Take-profit in points when dynamic targets disabled", "Risk");

		_stopLossAtrMultiplier = Param(nameof(StopLossAtrMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("ATR Stop Mult", "Stop-loss multiplier applied to ATR", "Risk");

		_takeProfitAtrMultiplier = Param(nameof(TakeProfitAtrMultiplier), 7m)
			.SetGreaterThanZero()
			.SetDisplay("ATR Take Mult", "Take-profit multiplier applied to ATR", "Risk");

		_useBreakEven = Param(nameof(UseBreakEven), true)
			.SetDisplay("Use Breakeven", "Move stop to breakeven after trigger", "Risk");

		_breakEvenTriggerPoints = Param(nameof(BreakEvenTriggerPoints), 200)
			.SetGreaterThanZero()
			.SetDisplay("Breakeven Trigger", "Profit in points required to arm breakeven", "Risk");

		_breakEvenPaddingPoints = Param(nameof(BreakEvenPaddingPoints), 100)
			.SetGreaterThanZero()
			.SetDisplay("Breakeven Padding", "Padding in points applied after trigger", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for analysis", "General");

		Volume = 1m;
	}

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

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

		_history.Clear();
		_entryPrice = null;
		_stopPrice = null;
		_takeProfitPrice = null;
		_indicator = null;
		_atr = null;
	}

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

		_indicator = IndicatorChoice switch
		{
			IndicatorSources.MoneyFlowIndex => new MoneyFlowIndex { Length = IndicatorPeriod },
			_ => new RelativeStrengthIndex { Length = IndicatorPeriod }
		};

		_atr = new AverageTrueRange { Length = IndicatorPeriod };

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

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

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

		_history.Add((indicatorValue, candle.ClosePrice));

		var maxNeeded = Math.Max(OptimizingPeriods + 1, 3);
		while (_history.Count > maxNeeded)
		{
			_history.RemoveAt(0);
		}

		var priceStep = Security?.PriceStep ?? 1m;
		if (priceStep <= 0m)
			priceStep = 1m;

		var stepPrice = priceStep;

		var triggerDiff = UseBreakEven ? BreakEvenTriggerPoints * priceStep : 0m;
		var paddingPoints = BreakEvenPaddingPoints > BreakEvenTriggerPoints ? 0 : BreakEvenPaddingPoints;
		var paddingDiff = UseBreakEven ? paddingPoints * priceStep : 0m;

		ManageOpenPosition(candle, triggerDiff, paddingDiff);

		if (_history.Count < maxNeeded)
			return;

		var indicatorValues = new decimal[_history.Count];
		var closeValues = new decimal[_history.Count];
		for (var i = 0; i < _history.Count; i++)
		{
			var source = _history[_history.Count - 1 - i];
			indicatorValues[i] = source.indicator;
			closeValues[i] = source.close;
		}

		decimal stopLossDiff;
		decimal takeProfitDiff;

		if (UseDynamicTargets)
		{
			if (atrValue <= 0m)
				return;

			stopLossDiff = atrValue * StopLossAtrMultiplier;
			takeProfitDiff = atrValue * TakeProfitAtrMultiplier;
		}
		else
		{
			stopLossDiff = StaticStopLossPoints * priceStep;
			takeProfitDiff = StaticTakeProfitPoints * priceStep;
		}

		if (stopLossDiff <= 0m || takeProfitDiff <= 0m)
			return;

		var volume = CalculateVolume(stopLossDiff);
		if (volume <= 0m)
			return;

		var stepMultiplier = priceStep > 0m ? stepPrice / priceStep : 1m;

		var (sellLevel, sellProfit) = CalculateBestSellLevel(indicatorValues, closeValues, stopLossDiff, takeProfitDiff, volume, stepMultiplier);
		var (buyLevel, buyProfit) = CalculateBestBuyLevel(indicatorValues, closeValues, stopLossDiff, takeProfitDiff, volume, stepMultiplier);

		var adjustedSellProfit = sellProfit;
		var adjustedBuyProfit = buyProfit;
		if (TradeReverse)
		{
			adjustedSellProfit = buyProfit;
			adjustedBuyProfit = sellProfit;
		}

		var canEnter = !OneOrderAtATime || Position == 0m;
		var currentIndicator = indicatorValues[0];
		var previousIndicator = indicatorValues[1];

		if (adjustedSellProfit > adjustedBuyProfit)
		{
			if (canEnter && ((currentIndicator < sellLevel && previousIndicator > sellLevel) || UseAggressiveEntries))
			{
				EnterShort(candle, volume, stopLossDiff, takeProfitDiff);
			}
		}
		else if (adjustedSellProfit < adjustedBuyProfit)
		{
			if (canEnter && ((currentIndicator > buyLevel && previousIndicator < buyLevel) || UseAggressiveEntries))
			{
				EnterLong(candle, volume, stopLossDiff, takeProfitDiff);
			}
		}
	}

	private (int level, decimal profit) CalculateBestSellLevel(decimal[] indicatorValues, decimal[] closeValues, decimal stopLossDiff, decimal takeProfitDiff, decimal volume, decimal stepMultiplier)
	{
		var bottom = Math.Min(IndicatorBottomValue, IndicatorTopValue);
		var top = Math.Max(IndicatorBottomValue, IndicatorTopValue);
		var bestProfit = 0m;
		var bestLevel = bottom;
		var updated = false;

		for (var level = bottom; level <= top; level++)
		{
			var profit = EvaluateSellLevel(indicatorValues, closeValues, level, stopLossDiff, takeProfitDiff, volume, stepMultiplier);
			if (profit > bestProfit)
			{
				bestProfit = profit;
				bestLevel = level;
				updated = true;
			}
		}

		return (bestLevel, updated ? bestProfit : 0m);
	}

	private (int level, decimal profit) CalculateBestBuyLevel(decimal[] indicatorValues, decimal[] closeValues, decimal stopLossDiff, decimal takeProfitDiff, decimal volume, decimal stepMultiplier)
	{
		var bottom = Math.Min(IndicatorBottomValue, IndicatorTopValue);
		var top = Math.Max(IndicatorBottomValue, IndicatorTopValue);
		var bestProfit = 0m;
		var bestLevel = top;
		var updated = false;

		for (var level = top; level >= bottom; level--)
		{
			var profit = EvaluateBuyLevel(indicatorValues, closeValues, level, stopLossDiff, takeProfitDiff, volume, stepMultiplier);
			if (profit > bestProfit)
			{
				bestProfit = profit;
				bestLevel = level;
				updated = true;
			}
		}

		return (bestLevel, updated ? bestProfit : 0m);
	}

	private decimal EvaluateSellLevel(decimal[] indicatorValues, decimal[] closeValues, int level, decimal stopLossDiff, decimal takeProfitDiff, decimal volume, decimal stepMultiplier)
	{
		var totalProfit = 0m;
		if (indicatorValues.Length < 3)
			return 0m;

		var threshold = (decimal)level;
		for (var i = indicatorValues.Length - 2; i >= 2; i--)
		{
			if (indicatorValues[i] < threshold && indicatorValues[i + 1] > threshold)
			{
				var entryPrice = closeValues[i];
				for (var j = i - 1; j >= 1; j--)
				{
					var price = closeValues[j];
					if (price >= entryPrice + stopLossDiff)
					{
						var loss = (price - entryPrice) * stepMultiplier * volume;
						totalProfit -= loss;
						i = j;
						break;
					}

					if (price <= entryPrice - takeProfitDiff)
					{
						var gain = (entryPrice - price) * stepMultiplier * volume;
						totalProfit += gain;
						i = j;
						break;
					}
				}
			}
		}

		return totalProfit;
	}

	private decimal EvaluateBuyLevel(decimal[] indicatorValues, decimal[] closeValues, int level, decimal stopLossDiff, decimal takeProfitDiff, decimal volume, decimal stepMultiplier)
	{
		var totalProfit = 0m;
		if (indicatorValues.Length < 3)
			return 0m;

		var threshold = (decimal)level;
		for (var i = indicatorValues.Length - 2; i >= 2; i--)
		{
			if (indicatorValues[i] > threshold && indicatorValues[i + 1] < threshold)
			{
				var entryPrice = closeValues[i];
				for (var j = i - 1; j >= 1; j--)
				{
					var price = closeValues[j];
					if (price <= entryPrice - stopLossDiff)
					{
						var loss = (entryPrice - price) * stepMultiplier * volume;
						totalProfit -= loss;
						i = j;
						break;
					}

					if (price >= entryPrice + takeProfitDiff)
					{
						var gain = (price - entryPrice) * stepMultiplier * volume;
						totalProfit += gain;
						i = j;
						break;
					}
				}
			}
		}

		return totalProfit;
	}

	private decimal CalculateVolume(decimal stopLossDiff)
	{
		var volume = BaseVolume;

		if (UseDynamicVolume && stopLossDiff > 0m && Security != null)
		{
			var priceStep = Security.PriceStep ?? 0m;
			var stepPrice = priceStep;
			if (priceStep > 0m && stepPrice > 0m)
			{
				var stopPoints = stopLossDiff / priceStep;
				var riskPerUnit = stopPoints * stepPrice;
				var capital = Portfolio?.CurrentValue ?? 0m;
				var riskBudget = capital * (RiskPercent / 100m);
				if (riskPerUnit > 0m && riskBudget > 0m)
				{
					var rawVolume = riskBudget / riskPerUnit;
					if (rawVolume > 0m)
						volume = rawVolume;
				}
			}
		}

		return AdjustVolume(volume);
	}

	private decimal AdjustVolume(decimal volume)
	{
		if (Security == null)
			return Math.Max(volume, 0.01m);

		var step = Security.VolumeStep ?? 0m;
		var min = Security.MinVolume ?? 0m;
		var max = Security.MaxVolume ?? decimal.MaxValue;

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

		if (min <= 0m)
			min = step;

		if (volume < min)
			volume = min;

		if (volume > max)
			volume = max;

		volume = Math.Floor(volume / step) * step;
		if (volume <= 0m)
			volume = min;

		return volume;
	}

	private void EnterLong(ICandleMessage candle, decimal volume, decimal stopLossDiff, decimal takeProfitDiff)
	{
		var orderVolume = volume;
		if (Position < 0m)
			orderVolume += Math.Abs(Position);

		BuyMarket();

		_entryPrice = candle.ClosePrice;
		_stopPrice = _entryPrice - stopLossDiff;
		_takeProfitPrice = _entryPrice + takeProfitDiff;
	}

	private void EnterShort(ICandleMessage candle, decimal volume, decimal stopLossDiff, decimal takeProfitDiff)
	{
		var orderVolume = volume;
		if (Position > 0m)
			orderVolume += Position;

		SellMarket();

		_entryPrice = candle.ClosePrice;
		_stopPrice = _entryPrice + stopLossDiff;
		_takeProfitPrice = _entryPrice - takeProfitDiff;
	}

	private void ManageOpenPosition(ICandleMessage candle, decimal triggerDiff, decimal paddingDiff)
	{
		if (Position > 0m)
		{
			if (UseBreakEven && _entryPrice is decimal entry && _stopPrice is decimal currentStop)
			{
				var triggerPrice = entry + triggerDiff;
				var targetStop = entry + paddingDiff;
				if (triggerDiff > 0m && candle.HighPrice >= triggerPrice && currentStop < targetStop)
					_stopPrice = targetStop;
			}

			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket();
				ResetPositionState();
				return;
			}

			if (_takeProfitPrice is decimal target && candle.HighPrice >= target)
			{
				SellMarket();
				ResetPositionState();
				return;
			}
		}
		else if (Position < 0m)
		{
			if (UseBreakEven && _entryPrice is decimal entry && _stopPrice is decimal currentStop)
			{
				var triggerPrice = entry - triggerDiff;
				var targetStop = entry - paddingDiff;
				if (triggerDiff > 0m && candle.LowPrice <= triggerPrice && currentStop > targetStop)
					_stopPrice = targetStop;
			}

			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket();
				ResetPositionState();
				return;
			}

			if (_takeProfitPrice is decimal target && candle.LowPrice <= target)
			{
				BuyMarket();
				ResetPositionState();
				return;
			}
		}
		else
		{
			ResetPositionState();
		}
	}

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