Ver no GitHub

Blau TStoch Indicator Strategy

Overview

  • Port of the MetaTrader 5 expert advisor Exp_BlauTStochI to the StockSharp high level API.
  • Trades the Blau Triple Stochastic Index (William Blau) on configurable timeframes.
  • Supports two execution modes: Breakdown (zero line breakouts) and Twist (slope reversals).
  • Position permissions reproduce the original expert advisor flags (independent toggles for opening/closing long and short trades).

Indicator construction

  • Calculates a momentum series as applied price - lowest over MomentumLength bars and its range highest - lowest.
  • Applies three consecutive smoothing stages to both numerator and denominator.
  • Supported smoothing methods: Exponential (EMA), Simple (SMA), Smoothed/Running (SMMA), and Linear Weighted (LWMA).
  • The original MQL options (JJMA, JurX, ParMA, T3, VIDYA, AMA) are not reproduced; the Phase parameter is retained for compatibility but ignored.
  • Applied price options match the MQL enumerations (close, open, high, low, median, typical, weighted, simple, quarted, trend-following variants, DeMark).
  • Final indicator value: 100 * smoothedStoch / smoothedRange - 50.

Trading rules

Breakdown mode

  • Inspect the indicator on the bar defined by SignalBar (default 1, i.e. the last closed candle).
  • Long entry: previous value (SignalBar+1) above zero and current value (SignalBar) crosses below or equals zero.
  • Short entry: previous value below zero and current value crosses above or equals zero.
  • Long exit: previous value below zero and long exits permitted.
  • Short exit: previous value above zero and short exits permitted.

Twist mode

  • Long entry: indicator rising (value[SignalBar+1] < value[SignalBar+2]) and the latest value not lower than the previous one.
  • Short entry: indicator falling (value[SignalBar+1] > value[SignalBar+2]) and the latest value not higher than the previous one.
  • Long exit: indicator slope turns downward (value[SignalBar+1] > value[SignalBar+2]).
  • Short exit: indicator slope turns upward (value[SignalBar+1] < value[SignalBar+2]).

Position management

  • Entries reverse existing opposite positions by adding the absolute position size to the configured Volume.
  • Exits close the full existing position with market orders.
  • Trade processing is performed only on finished candles and after the indicator is fully formed.

Risk management

  • Optional stop-loss and take-profit measured in price steps (StopLossPoints, TakeProfitPoints).
  • Both are implemented via StartProtection and can be disabled by setting the distance to zero.

Parameters

Parameter Description Default
CandleType Data type/timeframe for calculations. 4-hour candles
Smoothing Smoothing method (EMA/SMA/SMMA/LWMA). EMA
MomentumLength Lookback for highest/lowest detection. 20
FirstSmoothing Length of smoothing stage 1. 5
SecondSmoothing Length of smoothing stage 2. 8
ThirdSmoothing Length of smoothing stage 3. 3
Phase Kept for compatibility (ignored). 15
PriceType Applied price constant. Close
SignalBar Bar shift used for signal evaluation (>= 1). 1
Mode Trading mode (Breakdown/Twist). Twist
AllowLongEntries Enable long entries. true
AllowShortEntries Enable short entries. true
AllowLongExits Enable closing long trades. true
AllowShortExits Enable closing short trades. true
TakeProfitPoints Take-profit distance in steps (0 disables). 2000
StopLossPoints Stop-loss distance in steps (0 disables). 1000

Differences from the MT5 expert

  • Advanced smoothing algorithms from SmoothAlgorithms.mqh are not implemented; choose among EMA/SMA/SMMA/LWMA.
  • Money management (lot sizing) is simplified: the strategy relies on the StockSharp Volume property.
  • Signal evaluation occurs on finished candles only; there is no intra-bar execution.

Usage notes

  • Ensure SignalBar remains at least 1; the implementation maintains sufficient indicator history automatically.
  • Increasing the smoothing lengths increases formation time because each stage requires the full window to complete.
  • For reversal trading on higher timeframes, consider widening stop/take distances or disabling one side via permissions.
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>
/// Strategy that ports the Blau Triple Stochastic Index expert advisor.
/// Supports zero breakdown and trend twist entry modes with optional position permissions.
/// </summary>
public class BlauTStochIndicatorStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<SmoothingMethods> _smoothingMethod;
	private readonly StrategyParam<int> _momentumLength;
	private readonly StrategyParam<int> _firstSmoothing;
	private readonly StrategyParam<int> _secondSmoothing;
	private readonly StrategyParam<int> _thirdSmoothing;
	private readonly StrategyParam<int> _phase;
	private readonly StrategyParam<AppliedPriceTypes> _priceType;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<BlauEntryModes> _mode;
	private readonly StrategyParam<bool> _allowLongEntries;
	private readonly StrategyParam<bool> _allowShortEntries;
	private readonly StrategyParam<bool> _allowLongExits;
	private readonly StrategyParam<bool> _allowShortExits;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;

	private BlauTripleStochastic _indicator;
	private readonly List<decimal> _indicatorValues = new();

	/// <summary>
	/// Available entry algorithms.
	/// </summary>
	public enum BlauEntryModes
	{
		/// <summary>
		/// Trade zero line breakdown of the Blau Triple Stochastic Index.
		/// </summary>
		Breakdown,

		/// <summary>
		/// Trade twists of the indicator slope.
		/// </summary>
		Twist
	}

	/// <summary>
	/// Supported smoothing techniques.
	/// </summary>
	public enum SmoothingMethods
	{
		/// <summary>
		/// Exponential moving average.
		/// </summary>
		Ema,

		/// <summary>
		/// Simple moving average.
		/// </summary>
		Sma,

		/// <summary>
		/// Smoothed (RMA) moving average.
		/// </summary>
		Smma,

		/// <summary>
		/// Weighted moving average.
		/// </summary>
		Lwma
	}

	/// <summary>
	/// Applied price options.
	/// </summary>
	public enum AppliedPriceTypes
	{
		/// <summary>
		/// Closing price.
		/// </summary>
		Close,

		/// <summary>
		/// Opening price.
		/// </summary>
		Open,

		/// <summary>
		/// Highest price.
		/// </summary>
		High,

		/// <summary>
		/// Lowest price.
		/// </summary>
		Low,

		/// <summary>
		/// Median price (high + low) / 2.
		/// </summary>
		Median,

		/// <summary>
		/// Typical price (close + high + low) / 3.
		/// </summary>
		Typical,

		/// <summary>
		/// Weighted price (2 * close + high + low) / 4.
		/// </summary>
		Weighted,

		/// <summary>
		/// Simple price (open + close) / 2.
		/// </summary>
		Simple,

		/// <summary>
		/// Quarted price (open + close + high + low) / 4.
		/// </summary>
		Quarted,

		/// <summary>
		/// Trend-following price variant #0.
		/// </summary>
		TrendFollow0,

		/// <summary>
		/// Trend-following price variant #1.
		/// </summary>
		TrendFollow1,

		/// <summary>
		/// DeMark price.
		/// </summary>
		Demark
	}

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

	/// <summary>
	/// Smoothing algorithm applied to stochastic and range series.
	/// </summary>
	public SmoothingMethods Smoothing
	{
		get => _smoothingMethod.Value;
		set => _smoothingMethod.Value = value;
	}

	/// <summary>
	/// Momentum lookback for the Blau Triple Stochastic Index.
	/// </summary>
	public int MomentumLength
	{
		get => _momentumLength.Value;
		set => _momentumLength.Value = value;
	}

	/// <summary>
	/// Length of the first smoothing stage.
	/// </summary>
	public int FirstSmoothing
	{
		get => _firstSmoothing.Value;
		set => _firstSmoothing.Value = value;
	}

	/// <summary>
	/// Length of the second smoothing stage.
	/// </summary>
	public int SecondSmoothing
	{
		get => _secondSmoothing.Value;
		set => _secondSmoothing.Value = value;
	}

	/// <summary>
	/// Length of the third smoothing stage.
	/// </summary>
	public int ThirdSmoothing
	{
		get => _thirdSmoothing.Value;
		set => _thirdSmoothing.Value = value;
	}

	/// <summary>
	/// Phase parameter retained for compatibility.
	/// </summary>
	public int Phase
	{
		get => _phase.Value;
		set => _phase.Value = value;
	}

	/// <summary>
	/// Applied price selection.
	/// </summary>
	public AppliedPriceTypes PriceType
	{
		get => _priceType.Value;
		set => _priceType.Value = value;
	}

	/// <summary>
	/// Bar shift used to evaluate the indicator.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	/// <summary>
	/// Entry algorithm.
	/// </summary>
	public BlauEntryModes Mode
	{
		get => _mode.Value;
		set => _mode.Value = value;
	}

	/// <summary>
	/// Allow long entries.
	/// </summary>
	public bool AllowLongEntries
	{
		get => _allowLongEntries.Value;
		set => _allowLongEntries.Value = value;
	}

	/// <summary>
	/// Allow short entries.
	/// </summary>
	public bool AllowShortEntries
	{
		get => _allowShortEntries.Value;
		set => _allowShortEntries.Value = value;
	}

	/// <summary>
	/// Allow closing long positions.
	/// </summary>
	public bool AllowLongExits
	{
		get => _allowLongExits.Value;
		set => _allowLongExits.Value = value;
	}

	/// <summary>
	/// Allow closing short positions.
	/// </summary>
	public bool AllowShortExits
	{
		get => _allowShortExits.Value;
		set => _allowShortExits.Value = value;
	}

	/// <summary>
	/// Take profit distance in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop loss distance in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Initialize strategy parameters.
	/// </summary>
	public BlauTStochIndicatorStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe for indicator calculation", "General");

		_smoothingMethod = Param(nameof(Smoothing), SmoothingMethods.Ema)
		.SetDisplay("Smoothing", "Moving average type for smoothing", "Indicator");

		_momentumLength = Param(nameof(MomentumLength), 5)
		.SetGreaterThanZero()
		.SetDisplay("Momentum Length", "Lookback for highest and lowest prices", "Indicator");

		_firstSmoothing = Param(nameof(FirstSmoothing), 5)
		.SetGreaterThanZero()
		.SetDisplay("First Smoothing", "Length of the first smoothing stage", "Indicator");

		_secondSmoothing = Param(nameof(SecondSmoothing), 8)
		.SetGreaterThanZero()
		.SetDisplay("Second Smoothing", "Length of the second smoothing stage", "Indicator");

		_thirdSmoothing = Param(nameof(ThirdSmoothing), 3)
		.SetGreaterThanZero()
		.SetDisplay("Third Smoothing", "Length of the third smoothing stage", "Indicator");

		_phase = Param(nameof(Phase), 15)
		.SetDisplay("Phase", "Compatibility phase parameter", "Indicator");

		_priceType = Param(nameof(PriceType), AppliedPriceTypes.Close)
		.SetDisplay("Applied Price", "Price source for momentum calculation", "Indicator");

		_signalBar = Param(nameof(SignalBar), 1)
		.SetGreaterThanZero()
		.SetDisplay("Signal Bar", "Closed bar index used for signals", "Signals");

		_mode = Param(nameof(Mode), BlauEntryModes.Twist)
		.SetDisplay("Mode", "Entry algorithm", "Signals");

		_allowLongEntries = Param(nameof(AllowLongEntries), true)
		.SetDisplay("Allow Long Entries", "Enable opening long positions", "Permissions");

		_allowShortEntries = Param(nameof(AllowShortEntries), true)
		.SetDisplay("Allow Short Entries", "Enable opening short positions", "Permissions");

		_allowLongExits = Param(nameof(AllowLongExits), true)
		.SetDisplay("Allow Long Exits", "Enable closing long positions", "Permissions");

		_allowShortExits = Param(nameof(AllowShortExits), true)
		.SetDisplay("Allow Short Exits", "Enable closing short positions", "Permissions");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
		.SetNotNegative()
		.SetDisplay("Take Profit Points", "Take profit distance in price steps", "Risk");

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
		.SetNotNegative()
		.SetDisplay("Stop Loss Points", "Stop loss distance in price steps", "Risk");
	}

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

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

		_indicatorValues.Clear();
		_indicator = null;
		_entryPrice = 0m;
	}

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

		_indicator = new BlauTripleStochastic
		{
			Method = Smoothing,
			Period = MomentumLength,
			Smooth1 = FirstSmoothing,
			Smooth2 = SecondSmoothing,
			Smooth3 = ThirdSmoothing,
			PriceType = PriceType
		};

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

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

	private decimal _entryPrice;

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (trade?.Trade == null) return;
		if (Position != 0m && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;
		if (Position == 0m)
			_entryPrice = 0m;
	}

	private void HandleProtectiveLevels(ICandleMessage candle)
	{
		var step = Security?.PriceStep ?? 1m;
		if (step <= 0m) return;

		if (Position > 0m)
		{
			if (StopLossPoints > 0 && candle.LowPrice <= _entryPrice - StopLossPoints * step)
			{
				SellMarket(Position);
				return;
			}
			if (TakeProfitPoints > 0 && candle.HighPrice >= _entryPrice + TakeProfitPoints * step)
			{
				SellMarket(Position);
				return;
			}
		}
		else if (Position < 0m)
		{
			var abs = Math.Abs(Position);
			if (StopLossPoints > 0 && candle.HighPrice >= _entryPrice + StopLossPoints * step)
			{
				BuyMarket(abs);
				return;
			}
			if (TakeProfitPoints > 0 && candle.LowPrice <= _entryPrice - TakeProfitPoints * step)
			{
				BuyMarket(abs);
				return;
			}
		}
	}

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

		HandleProtectiveLevels(candle);

		var indicatorValue = _indicator.Process(candle);

		if (_indicator is null || !_indicator.IsFormed)
		{
			var rawValue = indicatorValue.ToDecimal();
			_indicatorValues.Add(rawValue);
			TrimHistory();
			return;
		}

		var currentValue = indicatorValue.ToDecimal();
		_indicatorValues.Add(currentValue);
		TrimHistory();

		var maxOffset = Mode == BlauEntryModes.Twist ? 2 : 1;
		if (_indicatorValues.Count < SignalBar + maxOffset)
		return;

		var value0 = GetShiftValue(0);
		var value1 = GetShiftValue(1);
		var value2 = maxOffset >= 2 ? GetShiftValue(2) : 0m;

		var openLong = false;
		var openShort = false;
		var closeLong = false;
		var closeShort = false;

		switch (Mode)
		{
			case BlauEntryModes.Breakdown:
			{
				if (value1 > 0m)
				{
					if (AllowLongEntries && value0 <= 0m)
					openLong = true;

					if (AllowShortExits)
					closeShort = true;
				}

				if (value1 < 0m)
				{
					if (AllowShortEntries && value0 >= 0m)
					openShort = true;

					if (AllowLongExits)
					closeLong = true;
				}
				break;
			}

			case BlauEntryModes.Twist:
			{
				if (value1 < value2)
				{
					if (AllowLongEntries && value0 >= value1)
					openLong = true;

					if (AllowShortExits)
					closeShort = true;
				}

				if (value1 > value2)
				{
					if (AllowShortEntries && value0 <= value1)
					openShort = true;

					if (AllowLongExits)
					closeLong = true;
				}
				break;
			}
		}

		if (closeLong && Position > 0m)
		SellMarket(Position);

		if (closeShort && Position < 0m)
		BuyMarket(-Position);

		if (openLong && Position <= 0m)
		{
			var volume = Volume + (Position < 0m ? -Position : 0m);
			if (volume > 0m)
			BuyMarket(volume);
		}

		if (openShort && Position >= 0m)
		{
			var volume = Volume + (Position > 0m ? Position : 0m);
			if (volume > 0m)
			SellMarket(volume);
		}
	}

	private void TrimHistory()
	{
		var maxLength = Math.Max(SignalBar + 5, 10);
		while (_indicatorValues.Count > maxLength)
		_indicatorValues.RemoveAt(0);
	}

	private decimal GetShiftValue(int offset)
	{
		var index = _indicatorValues.Count - SignalBar - offset;
		if (index < 0 || index >= _indicatorValues.Count)
		throw new InvalidOperationException("Insufficient indicator history for the requested shift.");

		return _indicatorValues[index];
	}

	private class BlauTripleStochastic : BaseIndicator
	{
		public SmoothingMethods Method { get; set; } = SmoothingMethods.Ema;
		public int Period { get; set; } = 20;
		public int Smooth1 { get; set; } = 5;
		public int Smooth2 { get; set; } = 8;
		public int Smooth3 { get; set; } = 3;
		public AppliedPriceTypes PriceType { get; set; } = AppliedPriceTypes.Close;

		private Highest _highest;
		private Lowest _lowest;
		private IIndicator _stoch1;
		private IIndicator _stoch2;
		private IIndicator _stoch3;
		private IIndicator _range1;
		private IIndicator _range2;
		private IIndicator _range3;

		protected override IIndicatorValue OnProcess(IIndicatorValue input)
		{
			EnsureInitialized();
			_lastInputTime = input.Time;

			var candle = input.GetValue<ICandleMessage>();
			var price = GetPrice(candle);

			var highestValue = _highest!.Process(new DecimalIndicatorValue(_highest, candle.HighPrice, input.Time) { IsFinal = input.IsFinal }).ToDecimal();
			var lowestValue = _lowest!.Process(new DecimalIndicatorValue(_lowest, candle.LowPrice, input.Time) { IsFinal = input.IsFinal }).ToDecimal();

			if (!_highest.IsFormed || !_lowest.IsFormed)
			{
				IsFormed = false;
				return new DecimalIndicatorValue(this, 0m, input.Time);
			}

			var stoch = price - lowestValue;
			var range = highestValue - lowestValue;

			var stage1Stoch = ProcessStage(_stoch1!, stoch);
			var stage1Range = ProcessStage(_range1!, range);

			if (!_stoch1!.IsFormed || !_range1!.IsFormed)
			{
				IsFormed = false;
				return new DecimalIndicatorValue(this, 0m, input.Time);
			}

			var stage2Stoch = ProcessStage(_stoch2!, stage1Stoch);
			var stage2Range = ProcessStage(_range2!, stage1Range);

			if (!_stoch2!.IsFormed || !_range2!.IsFormed)
			{
				IsFormed = false;
				return new DecimalIndicatorValue(this, 0m, input.Time);
			}

			var stage3Stoch = ProcessStage(_stoch3!, stage2Stoch);
			var stage3Range = ProcessStage(_range3!, stage2Range);

			if (!_stoch3!.IsFormed || !_range3!.IsFormed || stage3Range == 0m)
			{
				IsFormed = false;
				return new DecimalIndicatorValue(this, 0m, input.Time);
			}

			IsFormed = true;
			var value = 100m * stage3Stoch / stage3Range - 50m;
			return new DecimalIndicatorValue(this, value, input.Time);
		}

		public override void Reset()
		{
			base.Reset();

			_highest = null;
			_lowest = null;
			_stoch1 = _stoch2 = _stoch3 = null;
			_range1 = _range2 = _range3 = null;
			IsFormed = false;
		}

		private void EnsureInitialized()
		{
			if (_highest != null)
			return;

			_highest = new Highest { Length = Math.Max(1, Period) };
			_lowest = new Lowest { Length = Math.Max(1, Period) };

			_stoch1 = CreateSmoother(Smooth1);
			_stoch2 = CreateSmoother(Smooth2);
			_stoch3 = CreateSmoother(Smooth3);
			_range1 = CreateSmoother(Smooth1);
			_range2 = CreateSmoother(Smooth2);
			_range3 = CreateSmoother(Smooth3);
		}

		private DateTime _lastInputTime;

		private decimal ProcessStage(IIndicator indicator, decimal value)
		{
			return indicator.Process(new DecimalIndicatorValue(indicator, value, _lastInputTime) { IsFinal = true }).ToDecimal();
		}

		private IIndicator CreateSmoother(int length)
		{
			var len = Math.Max(1, length);

			return Method switch
			{
				SmoothingMethods.Sma => new SimpleMovingAverage { Length = len },
				SmoothingMethods.Smma => new SmoothedMovingAverage { Length = len },
				SmoothingMethods.Lwma => new WeightedMovingAverage { Length = len },
				_ => new ExponentialMovingAverage { Length = len },
			};
		}

		private decimal GetPrice(ICandleMessage candle)
		{
			var open = candle.OpenPrice;
			var high = candle.HighPrice;
			var low = candle.LowPrice;
			var close = candle.ClosePrice;

			return PriceType switch
			{
				AppliedPriceTypes.Open => open,
				AppliedPriceTypes.High => high,
				AppliedPriceTypes.Low => low,
				AppliedPriceTypes.Median => (high + low) / 2m,
				AppliedPriceTypes.Typical => (close + high + low) / 3m,
				AppliedPriceTypes.Weighted => (2m * close + high + low) / 4m,
				AppliedPriceTypes.Simple => (open + close) / 2m,
				AppliedPriceTypes.Quarted => (open + close + high + low) / 4m,
				AppliedPriceTypes.TrendFollow0 => close > open ? high : close < open ? low : close,
				AppliedPriceTypes.TrendFollow1 => close > open ? (high + close) / 2m : close < open ? (low + close) / 2m : close,
				AppliedPriceTypes.Demark => CalculateDemark(open, high, low, close),
				_ => close,
			};
		}

		private static decimal CalculateDemark(decimal open, decimal high, decimal low, decimal close)
		{
			var res = high + low + close;
			if (close < open)
			res = (res + low) / 2m;
			else if (close > open)
			res = (res + high) / 2m;
			else
			res = (res + close) / 2m;

			return ((res - low) + (res - high)) / 2m;
		}
	}
}