View on GitHub

Vlado Williams %R Threshold Strategy

Overview

The Vlado Williams %R Threshold Strategy is a direct conversion of the MetaTrader 4 expert advisor Vlado_www_forex-instruments_info.mq4. The original robot trades a single Williams %R oscillator and flips its market exposure whenever the indicator crosses a user-defined level. This StockSharp port reproduces the same regime-switching behaviour while exposing each tunable value as a strategy parameter for optimisation and UI control.

Key Concepts

  • Trades the direction of the Williams %R oscillator relative to a threshold (default -50).
  • Holds at most one market position at a time and reverses only after the previous trade is closed.
  • Optional risk-based position sizing that mimics the MetaTrader money management formula AccountFreeMargin * MaximumRisk / price.
  • Works with any candle timeframe through the CandleType parameter (default 15-minute bars).

Trading Logic

  1. Subscribe to the configured candle stream and calculate a Williams %R of length WprLength (default 100).
  2. When Williams %R rises above WprLevel, the strategy marks a bullish bias:
    • If no position is open and the previous trade was not long, send a market buy order.
    • If a short position exists, it is closed immediately; fresh longs are considered on the next candle after the position is flat.
  3. When Williams %R falls below WprLevel, the bias flips to bearish:
    • If no position is open and the previous trade was not short, send a market sell order.
    • If a long position exists, it is flattened right away.
  4. Position size is determined by CalculateOrderVolume:
    • When UseRiskMoneyManagement is true, the strategy estimates the tradable volume from the current portfolio value: Portfolio.CurrentValue × MaximumRiskPercent ÷ 100 ÷ ClosePrice.
    • Otherwise the base Strategy.Volume is used.
    • Resulting lots are aligned to the instrument VolumeStep and clamped by MinVolume / MaxVolume if these bounds are available.

The strategy intentionally avoids opening a reversal position in the same candle that triggered the exit, matching the original EA flow (CheckForClose runs before CheckForOpen).

Conversion Notes

  • Money management defaults follow the MT4 script: MaximumRiskPercent starts at 10, matching the original MaximumRisk = 10 constant that targeted roughly one mini-lot per trade.
  • MetaTrader's shift parameter (indicator shift) is always zero in the source file; therefore it was omitted.
  • MT4 colour arguments (e.g., Red, Blue) have no StockSharp equivalent and are ignored.
  • Slippage inputs are not required because StockSharp market orders already use the current best price.

Parameters

Parameter Type Default Description
CandleType DataType 15-minute timeframe Timeframe for both signal calculation and order triggers.
WprLength int 100 Lookback period of the Williams %R oscillator.
WprLevel decimal -50 Threshold separating bullish and bearish regimes.
UseRiskMoneyManagement bool false Toggles risk-based position sizing.
MaximumRiskPercent decimal 10 Percentage of portfolio equity deployed per trade when risk management is on.

Tip: Combine the strategy with StartProtection() or external risk controls if you need automatic stop-loss handling. The original EA also relied on manual supervision and did not define hard stops.

Usage Guidelines

  1. Attach the strategy to a security that exposes accurate PriceStep, StepPrice, VolumeStep, and volume limits so the position-sizing helper can normalise orders correctly.
  2. Set Volume to your desired fallback lot size. It will be used whenever portfolio equity is unavailable or UseRiskMoneyManagement is disabled.
  3. Optimise WprLevel and WprLength to adapt the system to different markets. Narrow levels (e.g., -20 / -80) make the strategy more selective, while wide thresholds (-50) ensure it is almost always invested.
  4. The strategy is trend-following: it will reverse frequently in ranging conditions. Consider combining it with filters such as higher-timeframe trend checks or volatility thresholds when necessary.

Differences vs. MetaTrader Version

  • Uses candle subscriptions and indicator bindings from the StockSharp high-level API; there is no manual order loop or history scanning.
  • Risk sizing relies on Portfolio.CurrentValue. When account valuation is missing, the logic falls back to the static Volume, matching the MT4 behaviour where mm=0 forced a fixed lot size.
  • All comments and parameter descriptions are in English for consistency with the repository guidelines.

Validation Checklist

  • ✅ Strategy file compiled with the StockSharp strategy template conventions (tabs, file-scoped namespace, XML inheritdoc).
  • ✅ Parameters created via Param() and marked for optimisation where appropriate.
  • ✅ Williams %R values consumed through Bind, without any direct GetValue() access.
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 "Vlado" MetaTrader expert advisor that trades Williams %R level breakouts.
/// </summary>
public class VladoWilliamsPercentRangeStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _wprLength;
	private readonly StrategyParam<decimal> _wprLevel;
	private readonly StrategyParam<bool> _useRiskMoneyManagement;
	private readonly StrategyParam<decimal> _maximumRiskPercent;

	private bool _buySignal;
	private bool _sellSignal;
	private int _lastSignal;

	private WilliamsR _williamsR;

	/// <summary>
	/// Initializes a new instance of the <see cref="VladoWilliamsPercentRangeStrategy"/> class.
	/// </summary>
	public VladoWilliamsPercentRangeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for the strategy", "General");

		_wprLength = Param(nameof(WprLength), 100)
			.SetGreaterThanZero()
			.SetDisplay("Williams %R Period", "Lookback period for Williams %R", "Indicators")
			;

		_wprLevel = Param(nameof(WprLevel), -50m)
			.SetDisplay("Williams %R Level", "Threshold that flips the bias", "Signals")
			;

		_useRiskMoneyManagement = Param(nameof(UseRiskMoneyManagement), false)
			.SetDisplay("Risk Money Management", "Recalculate volume from equity before entries", "Risk")
			;

		_maximumRiskPercent = Param(nameof(MaximumRiskPercent), 10m)
			.SetDisplay("Maximum Risk Percent", "Equity percentage used when sizing orders", "Risk")
			;
	}

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

	/// <summary>
	/// Williams %R lookback length.
	/// </summary>
	public int WprLength
	{
		get => _wprLength.Value;
		set => _wprLength.Value = value;
	}

	/// <summary>
	/// Threshold that toggles bullish or bearish bias.
	/// </summary>
	public decimal WprLevel
	{
		get => _wprLevel.Value;
		set => _wprLevel.Value = value;
	}

	/// <summary>
	/// Enables risk based volume sizing similar to the MetaTrader version.
	/// </summary>
	public bool UseRiskMoneyManagement
	{
		get => _useRiskMoneyManagement.Value;
		set => _useRiskMoneyManagement.Value = value;
	}

	/// <summary>
	/// Fraction of the current equity used to size entries when risk management is enabled.
	/// </summary>
	public decimal MaximumRiskPercent
	{
		get => _maximumRiskPercent.Value;
		set => _maximumRiskPercent.Value = value;
	}

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

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

		_williamsR = null;
		_buySignal = false;
		_sellSignal = false;
		_lastSignal = 0;
	}

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

		_williamsR = new WilliamsR
		{
			Length = WprLength
		};

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

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

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

		UpdateSignals(wprValue);

		if (Position != 0)
		{
			if (Position > 0 && _sellSignal)
			{
				// Exit long positions when the bearish Williams %R regime appears.
				if (Position > 0) SellMarket(); else BuyMarket();
				return;
			}

			if (Position < 0 && _buySignal)
			{
				// Exit short positions when the bullish Williams %R regime appears.
				if (Position > 0) SellMarket(); else BuyMarket();
				return;
			}

			return;
		}

		// No open position - evaluate fresh entries.
		var volume = CalculateOrderVolume(candle.ClosePrice);
		if (volume <= 0m)
			return;

		if (_sellSignal && _lastSignal != -1)
		{
			// Enter short once Williams %R falls below the chosen level.
			SellMarket();
			_lastSignal = -1;
			return;
		}

		if (_buySignal && _lastSignal != 1)
		{
			// Enter long once Williams %R rises above the chosen level.
			BuyMarket();
			_lastSignal = 1;
		}
	}

	private void UpdateSignals(decimal wprValue)
	{
		// Williams %R values are negative: less negative indicates bullish momentum.
		if (wprValue > WprLevel)
		{
			_buySignal = true;
			_sellSignal = false;
		}
		else if (wprValue < WprLevel)
		{
			_sellSignal = true;
			_buySignal = false;
		}
	}

	private decimal CalculateOrderVolume(decimal referencePrice)
	{
		var volume = Volume;

		if (UseRiskMoneyManagement && MaximumRiskPercent > 0m && referencePrice > 0m)
		{
			var equity = Portfolio?.CurrentValue ?? 0m;
			if (equity > 0m)
			{
				// Convert risk capital to volume using the latest close price as approximation.
				volume = equity * (MaximumRiskPercent / 100m) / referencePrice;
			}
		}

		return NormalizeVolume(volume);
	}

	private decimal NormalizeVolume(decimal volume)
	{
		var normalized = volume;

		if (Security?.VolumeStep is decimal step && step > 0m)
		{
			var steps = decimal.Floor(normalized / step);
			normalized = steps * step;

			if (normalized <= 0m)
				normalized = step;
		}

		if (Security?.MinVolume is decimal minVolume && minVolume > 0m && normalized < minVolume)
			normalized = minVolume;

		if (Security?.MaxVolume is decimal maxVolume && maxVolume > 0m && normalized > maxVolume)
			normalized = maxVolume;

		if (normalized <= 0m && volume > 0m)
			normalized = volume;

		return normalized;
	}
}