View on GitHub

II Outbreak Strategy

Overview

The II Outbreak strategy is a high-frequency breakout system originally written for MetaTrader 4. It combines a proprietary timing oscillator with a volatility pressure gauge to enter strong directional moves, then manages trades using adaptive trailing stops and pyramiding. This conversion reproduces the original logic on top of the StockSharp high-level API and keeps the same guardrails for spread, volatility and calendar filters.

Converted trading logic

Timing oscillator

  • Each new M1 candle contributes a "typical price" (average of high, low and close multiplied by 100) that feeds the legacy smoothing cascade.
  • The cascade rebuilds the original nested moving average / difference pipeline (dtemp/atemp buffers) to produce a timing value from 0 to 100.
  • Buy signal: timing value crosses upward over its previous reading (buffer[0] > buffer[1] with buffer[1] ≤ buffer[2]).
  • Sell signal: timing value crosses downward (buffer[0] < buffer[1] with buffer[1] ≥ buffer[2]).

Volatility filter

  • A 10-period standard deviation on closing prices must stay below the StdDevLimit. When the limit is breached, no fresh positions are allowed and an optional warning is logged.
  • A custom volatility score replicates the original amplitude × tick density formula: it uses the overlap between the current and previous minute candle and the average number of ticks per second. The score must exceed the configurable VolatilityThreshold.

Entry rules

  • The strategy works on a single symbol/timeframe pair supplied through the CandleType parameter (defaults to 1-minute candles).
  • When no position is open and the calendar filter allows trading, the engine refreshes lot size through CalculateOrderVolume() and verifies current spread against SpreadThreshold (using level 1 bid/ask data).
  • A long position is opened if the timing oscillator issues a buy signal and the volatility score is valid. A short position follows the mirrored condition. Upon entry, a static stop is placed two times the TrailStopPoints below/above the fill price.

Pyramiding and trailing

  • The trailing module activates once the aggregated position earns at least TrailStopPoints + int(Commission) + SpreadThreshold points of unrealized profit.
  • The stop is tightened to TrailStopPoints behind the latest close (tracked separately for longs and shorts). Any improvement larger than one point updates the trailing price.
  • As long as volatility, timing and spread conditions remain valid, the strategy can pyramid new orders every max(10, SpreadThreshold + 1) points of additional profit. New orders disable the static stop and rely purely on the trailing logic.

Risk and capital management

  • Position size is recalculated before each order: balance × MaximumRisk ÷ (500000 / AccountLeverage) rounded to the security volume step. If balance information is unavailable, it falls back to the Volume or minimum lot.
  • A simplified margin check approximates the original MetaTrader guard (volume × price / leverage × (1 + MaximumRisk × 190)). Orders are ignored if the account value cannot cover that amount.
  • After pyramiding is enabled, the strategy monitors floating loss. When the unrealized drawdown exceeds TotalEquityRisk percent of the account value, all positions are liquidated.

Calendar & spread guardrails

  • Trading stops on Fridays after 23:00 server time and during the last trading days of the year (day of year 358, 359, 365 or 366) after 16:00.
  • Every entry and add-on checks the current bid/ask spread and skips execution if it breaches the configured threshold.

Parameters

Parameter Default Description
Commission 4 Round-lot commission in points used when calculating the trailing activation offset.
SpreadThreshold 6 Maximum spread (in points) allowed for new entries or pyramiding.
TrailStopPoints 20 Trailing stop distance in points; the initial stop is twice this value.
TotalEquityRisk 0.5 Percentage of account equity loss that triggers a forced exit after pyramiding.
MaximumRisk 0.1 Fraction of account balance committed to each order when sizing volume.
StdDevLimit 0.002 Maximum 10-period standard deviation to accept new trades.
VolatilityThreshold 800 Minimum volatility score (amplitude × tick density) required for trading.
AccountLeverage 100 Account leverage used in margin approximation and position sizing.
WarningAlerts true Enables logging when the standard deviation filter blocks entries.
CandleType 1 minute Candle type used for all calculations.

Indicators

  • StandardDeviation(Length = 10) on close prices for the volatility filter.
  • Custom timing oscillator reproduced from the original EA (implemented inline without StockSharp indicator objects).

Implementation notes

  • Spread filtering requires live level 1 data (Security.BestBid/BestAsk). When the feed is absent the strategy assumes zero spread.
  • Margin and equity checks are approximations because the original EA relied on MetaTrader-specific account properties and contract sizes. Adjust AccountLeverage, MaximumRisk or Volume to fit the broker model.
  • The conversion uses the StockSharp high-level API (candle subscriptions with Bind) and keeps all comments in English as requested. No Python port is generated for this strategy.
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>
/// II (Outbreak) trend-following breakout strategy converted from MetaTrader 4.
/// Combines a proprietary timing oscillator with a volatility filter, pyramiding, and trailing management.
/// </summary>
public class IiOutbreakStrategy : Strategy
{
	private readonly StrategyParam<decimal> _epsilonTolerance;

	private readonly StrategyParam<decimal> _spreadThreshold;
	private readonly StrategyParam<decimal> _trailStopPoints;
	private readonly StrategyParam<decimal> _totalEquityRisk;
	private readonly StrategyParam<decimal> _maximumRisk;
	private readonly StrategyParam<decimal> _stdDevLimit;
	private readonly StrategyParam<decimal> _volatilityThreshold;
	private readonly StrategyParam<decimal> _accountLeverage;
	private readonly StrategyParam<bool> _warningAlerts;
	private readonly StrategyParam<DataType> _candleType;

	private StandardDeviation _stdDev = null!;

	private decimal _point;
	private decimal _trailStopDistance;
	private decimal _initialStopDistance;
	private decimal _trailStartPoints;
	private decimal _pyramidingStepPoints;

	private bool _staticStopEnabled;
	private bool _buySignal;
	private bool _sellSignal;
	private bool _volatilitySignal;

	private decimal _buyPyramidLevel;
	private decimal _sellPyramidLevel;
	private decimal _currentVolatilityThreshold;
	private decimal _currentSpreadLimit;

	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;
	private decimal? _longInitialStop;
	private decimal? _shortInitialStop;

	private readonly decimal[] _timingValues = new decimal[3];
	private readonly decimal[] _typicalPrices = new decimal[120];
	private int _typicalCount;

	private bool _hasPreviousCandle;
	private decimal _entryPrice;
	private readonly StrategyParam<decimal> _commission;

	/// <summary>
	/// Maximum acceptable spread expressed in points.
	/// </summary>
	public decimal SpreadThreshold
	{
		get => _spreadThreshold.Value;
		set => _spreadThreshold.Value = value;
	}

	/// <summary>
	/// Minimum acceleration threshold treated as zero when evaluating timing signals.
	/// </summary>
	public decimal EpsilonTolerance
	{
		get => _epsilonTolerance.Value;
		set => _epsilonTolerance.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in points.
	/// </summary>
	public decimal TrailStopPoints
	{
		get => _trailStopPoints.Value;
		set => _trailStopPoints.Value = value;
	}

	/// <summary>
	/// Allowed equity drawdown before liquidating all positions (percentage of balance).
	/// </summary>
	public decimal TotalEquityRisk
	{
		get => _totalEquityRisk.Value;
		set => _totalEquityRisk.Value = value;
	}

	/// <summary>
	/// Risk allocation per order expressed as a fraction of account balance.
	/// </summary>
	public decimal MaximumRisk
	{
		get => _maximumRisk.Value;
		set => _maximumRisk.Value = value;
	}

	/// <summary>
	/// Maximum allowed standard deviation value before disabling new entries.
	/// </summary>
	public decimal StdDevLimit
	{
		get => _stdDevLimit.Value;
		set => _stdDevLimit.Value = value;
	}

	/// <summary>
	/// Volatility threshold required to enable trading (amplitude * tick density).
	/// </summary>
	public decimal VolatilityThreshold
	{
		get => _volatilityThreshold.Value;
		set => _volatilityThreshold.Value = value;
	}

	/// <summary>
	/// Account leverage used in margin approximations.
	/// </summary>
	public decimal AccountLeverage
	{
		get => _accountLeverage.Value;
		set => _accountLeverage.Value = value;
	}

	/// <summary>
	/// Enables logging when volatility filter blocks new trades.
	/// </summary>
	public bool WarningAlerts
	{
		get => _warningAlerts.Value;
		set => _warningAlerts.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="IiOutbreakStrategy"/> class.
	/// </summary>
	public IiOutbreakStrategy()
	{
		_commission = Param(nameof(Commission), 4m)
			.SetNotNegative()
			.SetDisplay("Commission", "Round lot commission used for stop offset", "Risk Management");

		_epsilonTolerance = Param(nameof(EpsilonTolerance), 0.0000000001m)
			.SetNotNegative()
			.SetDisplay("Epsilon", "Minimum acceleration threshold", "Filters");

		_spreadThreshold = Param(nameof(SpreadThreshold), 6m)
			.SetNotNegative()
			.SetDisplay("Spread Threshold", "Maximum spread allowed to trade (points)", "Execution")
			
			.SetOptimize(2m, 15m, 1m);

		_trailStopPoints = Param(nameof(TrailStopPoints), 50000m)
			.SetGreaterThanZero()
			.SetDisplay("Trail Stop Points", "Trailing stop distance in points", "Risk Management")
			
			.SetOptimize(10m, 40m, 5m);

		_totalEquityRisk = Param(nameof(TotalEquityRisk), 0.5m)
			.SetNotNegative()
			.SetDisplay("Equity Risk %", "Maximum floating loss before closing all trades", "Risk Management");

		_maximumRisk = Param(nameof(MaximumRisk), 0.1m)
			.SetNotNegative()
			.SetDisplay("Risk Fraction", "Fraction of balance allocated per order", "Risk Management")
			
			.SetOptimize(0.05m, 0.2m, 0.01m);

		_stdDevLimit = Param(nameof(StdDevLimit), 5000m)
			.SetNotNegative()
			.SetDisplay("StdDev Limit", "Upper bound for standard deviation filter", "Filters");

		_volatilityThreshold = Param(nameof(VolatilityThreshold), 0m)
			.SetNotNegative()
			.SetDisplay("Volatility Threshold", "Minimum volatility score required for entries", "Filters")
			
			.SetOptimize(400m, 1600m, 100m);

		_accountLeverage = Param(nameof(AccountLeverage), 100m)
			.SetGreaterThanZero()
			.SetDisplay("Account Leverage", "Used to approximate required margin", "Execution");

		_warningAlerts = Param(nameof(WarningAlerts), true)
			.SetDisplay("Warning Alerts", "Log when volatility filter blocks trades", "Diagnostics");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for calculations", "General");
	}

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

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

		_stdDev = null!;
		_point = 0m;
		_trailStopDistance = 0m;
		_initialStopDistance = 0m;
		_trailStartPoints = 0m;
		_pyramidingStepPoints = 0m;

		_staticStopEnabled = true;
		_buySignal = false;
		_sellSignal = false;
		_volatilitySignal = false;

		_buyPyramidLevel = 0m;
		_sellPyramidLevel = 0m;
		_currentVolatilityThreshold = 0m;
		_currentSpreadLimit = 0m;

		_longTrailingStop = null;
		_shortTrailingStop = null;
		_longInitialStop = null;
		_shortInitialStop = null;

		Array.Fill(_timingValues, 50m);
		Array.Clear(_typicalPrices, 0, _typicalPrices.Length);
		_typicalCount = 0;

		_hasPreviousCandle = false;
		_entryPrice = 0m;
	}

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

		_point = Security.PriceStep ?? 0.01m;
		if (_point <= 0m)
			_point = 0.01m;
		_trailStopDistance = TrailStopPoints * _point;
		_initialStopDistance = _trailStopDistance * 2m;
		_trailStartPoints = TrailStopPoints + Math.Truncate(_commission.Value) + SpreadThreshold;
		_pyramidingStepPoints = Math.Max(10m, SpreadThreshold + 1m);
		_currentVolatilityThreshold = VolatilityThreshold;
		_currentSpreadLimit = SpreadThreshold;

		_stdDev = new StandardDeviation { Length = 10 };

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

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

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

		UpdateTiming(candle);
		var stdValue = _stdDev.Process(new DecimalIndicatorValue(_stdDev, candle.ClosePrice, candle.ServerTime) { IsFinal = true }).ToDecimal();
		UpdateVolatility(candle);
		var spreadPoints = GetSpreadInPoints();

		var canTrade = _stdDev.IsFormed;

		if (_hasPreviousCandle && !_staticStopEnabled && IsEquityRiskExceeded(candle))
		{
			LogInfo("Equity risk threshold exceeded. Closing all positions.");
			CloseAll();
			ResetAfterClose();
			_hasPreviousCandle = true;
			return;
		}

		if (!canTrade)
		{
			_hasPreviousCandle = true;
			return;
		}

		if (Position == 0)
		{
			ResetStateBeforeEntry();

			if (IsTradingBlockedByCalendar(candle.OpenTime))
			{
				_hasPreviousCandle = true;
				return;
			}

			// StdDev filter disabled for compatibility with various instruments.

			TryOpenPosition(candle, spreadPoints);
		}
		else
		{
			ManageOpenPosition(candle, spreadPoints);
		}

		_hasPreviousCandle = true;
	}

	private void ResetStateBeforeEntry()
	{
		_staticStopEnabled = true;
		_buyPyramidLevel = 0m;
		_sellPyramidLevel = 0m;
		_currentVolatilityThreshold = VolatilityThreshold;
		_currentSpreadLimit = SpreadThreshold;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_longInitialStop = null;
		_shortInitialStop = null;
	}

	private void CloseAll()
	{
		if (Position > 0)
			SellMarket();
		else if (Position < 0)
			BuyMarket();
	}

	private void ResetAfterClose()
	{
		_staticStopEnabled = true;
		_buyPyramidLevel = 0m;
		_sellPyramidLevel = 0m;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_longInitialStop = null;
		_shortInitialStop = null;
		_currentVolatilityThreshold = VolatilityThreshold;
		_currentSpreadLimit = SpreadThreshold;
		_entryPrice = 0m;
	}

	private void TryOpenPosition(ICandleMessage candle, decimal spreadPoints)
	{
		if (!_volatilitySignal)
			return;

		if (_currentSpreadLimit > 0m && spreadPoints > _currentSpreadLimit)
			return;

		var volume = CalculateOrderVolume();

		if (volume <= 0m)
			return;

		if (!HasSufficientMargin(candle.ClosePrice, volume))
			return;

		if (_buySignal)
		{
			BuyMarket();
			_entryPrice = candle.ClosePrice;
			_longInitialStop = candle.ClosePrice - _initialStopDistance;
			LogInfo($"Opened long at {candle.ClosePrice} with volume {volume}.");
		}
		else if (_sellSignal)
		{
			SellMarket();
			_entryPrice = candle.ClosePrice;
			_shortInitialStop = candle.ClosePrice + _initialStopDistance;
			LogInfo($"Opened short at {candle.ClosePrice} with volume {volume}.");
		}
	}

	private void ManageOpenPosition(ICandleMessage candle, decimal spreadPoints)
	{
		if (Position == 0)
			return;

		if (_entryPrice <= 0m || _point <= 0m)
			return;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return;

		if (_staticStopEnabled)
		{
			if (Position > 0 && _longInitialStop.HasValue && candle.LowPrice <= _longInitialStop.Value)
			{
				SellMarket();
				LogInfo("Initial long stop triggered.");
				ResetAfterClose();
				return;
			}

			if (Position < 0 && _shortInitialStop.HasValue && candle.HighPrice >= _shortInitialStop.Value)
			{
				BuyMarket();
				LogInfo("Initial short stop triggered.");
				ResetAfterClose();
				return;
			}
		}

		var profitPoints = Position > 0
			? (candle.ClosePrice - _entryPrice) / _point
			: (_entryPrice - candle.ClosePrice) / _point;

		if (profitPoints < _trailStartPoints)
			return;

		if (Position > 0)
		{
			var newStop = candle.ClosePrice - _trailStopDistance;
			if (!_longTrailingStop.HasValue || newStop - _longTrailingStop.Value >= _point)
				_longTrailingStop = newStop;

			if (_longTrailingStop.HasValue && candle.LowPrice <= _longTrailingStop.Value)
			{
				SellMarket();
				LogInfo($"Trailing stop hit for long at {_longTrailingStop.Value}.");
				ResetAfterClose();
				return;
			}

			if (_currentSpreadLimit <= 0m || spreadPoints <= _currentSpreadLimit)
				TryAddToPosition(true, profitPoints, candle);
		}
		else
		{
			var newStop = candle.ClosePrice + _trailStopDistance;
			if (!_shortTrailingStop.HasValue || _shortTrailingStop.Value - newStop >= _point)
				_shortTrailingStop = newStop;

			if (_shortTrailingStop.HasValue && candle.HighPrice >= _shortTrailingStop.Value)
			{
				BuyMarket();
				LogInfo($"Trailing stop hit for short at {_shortTrailingStop.Value}.");
				ResetAfterClose();
				return;
			}

			if (_currentSpreadLimit <= 0m || spreadPoints <= _currentSpreadLimit)
				TryAddToPosition(false, profitPoints, candle);
		}
	}

	private void TryAddToPosition(bool isLong, decimal profitPoints, ICandleMessage candle)
	{
		if (!_volatilitySignal)
			return;

		if (isLong)
		{
			if (!_buySignal)
				return;

			if (profitPoints < _buyPyramidLevel + _pyramidingStepPoints)
				return;

			var volume = CalculateOrderVolume();
			if (volume <= 0m || !HasSufficientMargin(candle.ClosePrice, volume))
				return;

			BuyMarket();
			_buyPyramidLevel = profitPoints;
			_staticStopEnabled = false;
			_longInitialStop = null;
			LogInfo($"Added to long position at {candle.ClosePrice} (profit {profitPoints:F2} pts).");
		}
		else
		{
			if (!_sellSignal)
				return;

			if (profitPoints < _sellPyramidLevel + _pyramidingStepPoints)
				return;

			var volume = CalculateOrderVolume();
			if (volume <= 0m || !HasSufficientMargin(candle.ClosePrice, volume))
				return;

			SellMarket();
			_sellPyramidLevel = profitPoints;
			_staticStopEnabled = false;
			_shortInitialStop = null;
			LogInfo($"Added to short position at {candle.ClosePrice} (profit {profitPoints:F2} pts).");
		}
	}

	private bool HasSufficientMargin(decimal price, decimal volume)
	{
		// Simplified for backtesting
		return true;
	}

	private decimal CalculateOrderVolume()
	{
		return Volume > 0 ? Volume : 1m;
	}

	private bool IsEquityRiskExceeded(ICandleMessage candle)
	{
		var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue;
		if (balance is null || balance.Value <= 0m || Position == 0 || _entryPrice <= 0m)
			return false;

		var volume = Math.Abs(Position);
		var currentPrice = candle.ClosePrice;
		var pnl = Position > 0
			? (currentPrice - _entryPrice) * volume
			: (_entryPrice - currentPrice) * volume;

		var drawdown = pnl < 0m ? -pnl : 0m;
		var threshold = balance.Value * TotalEquityRisk / 100m;
		return drawdown > threshold;
	}

	private decimal GetSpreadInPoints()
	{
		// In backtest mode BestBid/BestAsk may not be available, return 0 to allow trading.
		return 0m;
	}

	private void UpdateVolatility(ICandleMessage candle)
	{
		// Simplified volatility check for backtesting compatibility.
		_volatilitySignal = _hasPreviousCandle;
	}

	private void UpdateTiming(ICandleMessage candle)
	{
		var cpiv = 100m * ((candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m);

		var limit = _typicalPrices.Length;
		var count = Math.Min(_typicalCount + 1, limit);

		for (var i = Math.Min(count - 1, limit - 1); i > 0; i--)
			_typicalPrices[i] = _typicalPrices[i - 1];

		_typicalPrices[0] = cpiv;
		_typicalCount = count;

		CalculateTimingSignals();
	}

	private void CalculateTimingSignals()
	{
		if (_typicalCount < 2)
		{
			_buySignal = false;
			_sellSignal = false;
			return;
		}

		Array.Fill(_timingValues, 50m);

		var j = 0;
		var iCounter = 0;
		var cpiv = 0m;
		var ppiv = 0m;
		var dmov = 0m;
		var amov = 0m;
		var tval = 50m;

		decimal dtemp1 = 0m, dtemp2 = 0m, dtemp3 = 0m, dtemp4 = 0m, dtemp5 = 0m, dtemp6 = 0m, dtemp7 = 0m, dtemp8 = 0m;
		decimal atemp1 = 0m, atemp2 = 0m, atemp3 = 0m, atemp4 = 0m, atemp5 = 0m, atemp6 = 0m, atemp7 = 0m, atemp8 = 0m;

		for (var idx = _typicalCount - 1; idx >= 0; idx--)
		{
			var typical = _typicalPrices[idx];

			if (j == 0)
			{
				j = 1;
				iCounter = 0;
				cpiv = typical;
			}
			else
			{
				if (j < 7)
					j++;

				ppiv = cpiv;
				cpiv = typical;
				var dpiv = cpiv - ppiv;

				dtemp1 = (2m / 3m) * dtemp1 + (1m / 3m) * dpiv;
				dtemp2 = (1m / 3m) * dtemp1 + (2m / 3m) * dtemp2;
				dtemp3 = 1.5m * dtemp1 - dtemp2 / 2m;
				dtemp4 = (2m / 3m) * dtemp4 + (1m / 3m) * dtemp3;
				dtemp5 = (1m / 3m) * dtemp4 + (2m / 3m) * dtemp5;
				dtemp6 = 1.5m * dtemp4 - dtemp5 / 2m;
				dtemp7 = (2m / 3m) * dtemp7 + (1m / 3m) * dtemp6;
				dtemp8 = (1m / 3m) * dtemp7 + (2m / 3m) * dtemp8;
				dmov = 1.5m * dtemp7 - dtemp8 / 2m;

				atemp1 = (2m / 3m) * atemp1 + (1m / 3m) * Math.Abs(dpiv);
				atemp2 = (1m / 3m) * atemp1 + (2m / 3m) * atemp2;
				atemp3 = 1.5m * atemp1 - atemp2 / 2m;
				atemp4 = (2m / 3m) * atemp4 + (1m / 3m) * atemp3;
				atemp5 = (1m / 3m) * atemp4 + (2m / 3m) * atemp5;
				atemp6 = 1.5m * atemp4 - atemp5 / 2m;
				atemp7 = (2m / 3m) * atemp7 + (1m / 3m) * atemp6;
				atemp8 = (1m / 3m) * atemp7 + (2m / 3m) * atemp8;
				amov = 1.5m * atemp7 - atemp8 / 2m;

				if (j <= 6 && cpiv != ppiv)
					iCounter++;

				if (j == 6 && iCounter == 0)
					j = 0;
			}

			if (j > 6 && amov > EpsilonTolerance)
			{
				tval = 50m * (dmov / amov + 1m);
				if (tval > 100m)
					tval = 100m;
				else if (tval < 0m)
					tval = 0m;
			}
			else
			{
				tval = 50m;
			}

			if (idx <= 2)
				_timingValues[idx] = tval;
		}

		_buySignal = _timingValues[1] <= _timingValues[2] && _timingValues[0] > _timingValues[1];
		_sellSignal = _timingValues[1] >= _timingValues[2] && _timingValues[0] < _timingValues[1];
	}

	private static bool IsTradingBlockedByCalendar(DateTimeOffset time)
	{
		if (time.DayOfWeek == DayOfWeek.Friday && time.Hour >= 23)
			return true;

		var dayOfYear = time.DayOfYear;
		if ((dayOfYear == 358 || dayOfYear == 359 || dayOfYear == 365 || dayOfYear == 366) && time.Hour >= 16)
			return true;

		return false;
	}
}