Открыть на GitHub

Стратегия Weight Oscillator Direct

Общая идея

Стратегия переносит эксперта MetaTrader Exp_WeightOscillator_Direct на высокоуровневый API StockSharp. Она объединяет четыре классических осциллятора (RSI, Money Flow Index, Williams %R и DeMarker) в единый взвешенный показатель, сглаженный выбранной скользящей средней. В режиме «Direct» стратегия следует за наклоном результирующего осциллятора, а в режиме «Against» работает как контртрендовая, открывая противоположные сделки.

Цепочка индикаторов

  1. RSI – нормализован к диапазону 0..100.
  2. MFI – оценивает денежные потоки в том же диапазоне 0..100.
  3. Williams %R – смещён на +100, чтобы привести значения к положительной шкале.
  4. DeMarker – умножается на 100 для согласования с остальными источниками.
  5. Сглаживание – одна из доступных скользящих средних: Simple, Exponential, Smoothed (RMA), Weighted, Jurik, Kaufman.
  6. Композитный осциллятор – взвешенное среднее нормализованных значений, прошедшее дополнительное сглаживание.

Значение композитного осциллятора фиксируется на каждой завершённой свече. Параметр Signal Bar позволяет, как и в оригинальном советнике, пропустить несколько последних баров при вычислении сигналов.

Логика торговли

  1. Дождаться формирования всех индикаторов и сглаживающей средней.
  2. Рассчитать сглаженный композитный осциллятор на текущей завершённой свече и сохранить его в историю.
  3. Взять три исторических значения current, previous, prior, отстоящих на Signal Bar, Signal Bar + 1 и Signal Bar + 2 баров назад.
  4. Найти изменение наклона:
    • Рост: previous < prior и одновременно current > previous.
    • Падение: previous > prior и current < previous.
  5. В режиме Direct рост генерирует сигнал на покупку, падение – на продажу. В режиме Against сигналы инвертируются.
  6. Выполнить соответствующие действия:
    • Закрыть противоположные позиции, если разрешены переключатели Close Shorts/Longs on Signal.
    • Открыть новую позицию, если включены Allow Long/Short Entries. Объём заявки равен Volume + |Position|, что позволяет моментально переворачиваться.
  7. Параметры Stop Loss Points и Take Profit Points активируют защиту через StartProtection, расстояния задаются в шагах цены инструмента.

Параметры

Группа Название Описание
General Candle Type Таймфрейм свечей для расчёта индикаторов.
Trading Trend Mode Direct – следование наклону, Against – контртренд.
Trading Signal Bar Количество закрытых баров, которые нужно пропустить (1 = последний закрытый бар).
Oscillator RSI / MFI / WPR / DeMarker Weight Вес каждого осциллятора во взвешенном среднем. Ноль отключает компонент.
Oscillator RSI / MFI / WPR / DeMarker Period Периоды расчёта соответствующих осцилляторов.
Oscillator Smoothing Method Тип сглаживания (Simple, Exponential, Smoothed, Weighted, Jurik, Kaufman).
Oscillator Smoothing Length Период сглаживающей средней.
Risk Management Stop Loss Points Стоп-лосс в шагах цены; 0 отключает.
Risk Management Take Profit Points Тейк-профит в шагах цены; 0 отключает.
Trading Allow Long/Short Entries Разрешить открытие длинных/коротких позиций.
Trading Close Shorts/Longs on Signal Разрешить закрытие текущей позиции при противоположном сигнале.

Все параметры оформлены через StrategyParam, поэтому доступны для оптимизации в Designer.

Особенности использования

  • Перед запуском задайте свойство Volume. При перевороте позиции заявка увеличивается на величину текущего объёма, чтобы закрыть старую и открыть новую позицию одной сделкой.
  • Стратегия подписывается только на один поток свечей, возвращаемый GetWorkingSecurities().
  • Защитные стопы пересчитываются из шагов цены (PriceStep) в абсолютные уровни.
  • Режим Against меняет только направление сигналов, а остальная логика полностью совпадает с оригиналом.
  • Williams %R и DeMarker нормализуются к диапазону 0..100, как и в MQL-версии.

Отличия от MQL-советника

  • В оригинале присутствовали дополнительные методы сглаживания (ParMA, JurX, VIDYA, T3). В StockSharp реализованы надёжные аналоги (Jurik и Kaufman), по умолчанию используется Jurik.
  • Money Flow Index всегда использует объём свечи. В MetaTrader можно выбирать тип объёма, здесь всё зависит от поставщика данных.
  • Управление рисками выполняется через StartProtection, что удобнее в экосистеме StockSharp и обеспечивает сопоставимое поведение при корректном PriceStep.

Быстрый старт

  1. Подключите стратегию к нужному портфелю и инструменту.
  2. Настройте веса, периоды и переключатели входов/выходов.
  3. Выберите тип и длину сглаживания под волатильность инструмента.
  4. Укажите защитные стопы и тейк-профиты (при необходимости).
  5. Запустите стратегию – сигналы будут срабатывать только на завершённых свечах, что делает поведение детерминированным.
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>
/// Trading strategy that combines several oscillators into a weighted composite signal.
/// </summary>
public class WeightOscillatorDirectStrategy : Strategy
{
	/// <summary>
	/// Defines how the strategy reacts to the oscillator slope.
	/// </summary>
	public enum WeightOscillatorTrendModes
	{
		/// <summary>
		/// Trade in the direction of the oscillator slope.
		/// </summary>
		Direct,

		/// <summary>
		/// Trade against the oscillator slope.
		/// </summary>
		Against,
	}

	/// <summary>
	/// Available smoothing methods for the blended oscillator.
	/// </summary>
	public enum WeightOscillatorSmoothingMethods
	{
		/// <summary>
		/// Simple moving average.
		/// </summary>
		Simple,

		/// <summary>
		/// Exponential moving average.
		/// </summary>
		Exponential,

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

		/// <summary>
		/// Linear weighted moving average.
		/// </summary>
		Weighted,

		/// <summary>
		/// Jurik moving average.
		/// </summary>
		Jurik,

		/// <summary>
		/// Kaufman adaptive moving average.
		/// </summary>
		Kaufman,
	}
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<WeightOscillatorTrendModes> _trendMode;
	private readonly StrategyParam<int> _signalBar;

	private readonly StrategyParam<decimal> _rsiWeight;
	private readonly StrategyParam<int> _rsiPeriod;

	private readonly StrategyParam<decimal> _mfiWeight;
	private readonly StrategyParam<int> _mfiPeriod;

	private readonly StrategyParam<decimal> _wprWeight;
	private readonly StrategyParam<int> _wprPeriod;

	private readonly StrategyParam<decimal> _deMarkerWeight;
	private readonly StrategyParam<int> _deMarkerPeriod;

	private readonly StrategyParam<WeightOscillatorSmoothingMethods> _smoothingMethod;
	private readonly StrategyParam<int> _smoothingLength;

	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private readonly StrategyParam<bool> _buyOpenEnabled;
	private readonly StrategyParam<bool> _sellOpenEnabled;
	private readonly StrategyParam<bool> _buyCloseEnabled;
	private readonly StrategyParam<bool> _sellCloseEnabled;

	private RelativeStrengthIndex _rsi = null!;
	private MoneyFlowIndex _mfi = null!;
	private WilliamsR _wpr = null!;
	private DeMarker _deMarker = null!;
	private IIndicator _smoothing = null!;

	private readonly List<decimal> _oscillatorHistory = new();

	/// <summary>
	/// Initializes a new instance of the <see cref="WeightOscillatorDirectStrategy"/> class.
	/// </summary>
	public WeightOscillatorDirectStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe used for indicator calculations", "General");

		_trendMode = Param(nameof(TrendMode), WeightOscillatorTrendModes.Direct)
		.SetDisplay("Trend Mode", "Trade with the oscillator slope or against it", "Trading");

		_signalBar = Param(nameof(SignalBar), 2)
		.SetDisplay("Signal Bar", "Number of closed bars to skip before evaluating signals", "Trading")
		.SetRange(1, 5)
		;

		_rsiWeight = Param(nameof(RsiWeight), 1m)
		.SetDisplay("RSI Weight", "Weight of RSI in the composite score", "Oscillator")
		.SetRange(0m, 5m)
		;

		_rsiPeriod = Param(nameof(RsiPeriod), 14)
		.SetDisplay("RSI Period", "Number of bars used for RSI", "Oscillator")
		.SetRange(2, 200)
		;

		_mfiWeight = Param(nameof(MfiWeight), 1m)
		.SetDisplay("MFI Weight", "Weight of Money Flow Index", "Oscillator")
		.SetRange(0m, 5m)
		;

		_mfiPeriod = Param(nameof(MfiPeriod), 14)
		.SetDisplay("MFI Period", "Number of bars used for MFI", "Oscillator")
		.SetRange(2, 200)
		;

		_wprWeight = Param(nameof(WprWeight), 1m)
		.SetDisplay("WPR Weight", "Weight of Williams %R", "Oscillator")
		.SetRange(0m, 5m)
		;

		_wprPeriod = Param(nameof(WprPeriod), 14)
		.SetDisplay("WPR Period", "Number of bars used for Williams %R", "Oscillator")
		.SetRange(2, 200)
		;

		_deMarkerWeight = Param(nameof(DeMarkerWeight), 1m)
		.SetDisplay("DeMarker Weight", "Weight of DeMarker oscillator", "Oscillator")
		.SetRange(0m, 5m)
		;

		_deMarkerPeriod = Param(nameof(DeMarkerPeriod), 14)
		.SetDisplay("DeMarker Period", "Number of bars used for DeMarker", "Oscillator")
		.SetRange(2, 200)
		;

		_smoothingMethod = Param(nameof(SmoothingMethod), WeightOscillatorSmoothingMethods.Jurik)
		.SetDisplay("Smoothing Method", "Moving average applied to the blended oscillator", "Oscillator");

		_smoothingLength = Param(nameof(SmoothingLength), 10)
		.SetDisplay("Smoothing Length", "Length of the smoothing moving average", "Oscillator")
		.SetRange(1, 200)
		;

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
		.SetDisplay("Stop Loss Points", "Protective stop in price steps (0 disables)", "Risk Management")
		.SetRange(0, 10000)
		;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
		.SetDisplay("Take Profit Points", "Profit target in price steps (0 disables)", "Risk Management")
		.SetRange(0, 20000)
		;

		_buyOpenEnabled = Param(nameof(BuyOpenEnabled), true)
		.SetDisplay("Allow Long Entries", "Enable opening long positions", "Trading");

		_sellOpenEnabled = Param(nameof(SellOpenEnabled), true)
		.SetDisplay("Allow Short Entries", "Enable opening short positions", "Trading");

		_buyCloseEnabled = Param(nameof(BuyCloseEnabled), true)
		.SetDisplay("Close Shorts on Long Signal", "Allow closing shorts when a long signal appears", "Trading");

		_sellCloseEnabled = Param(nameof(SellCloseEnabled), true)
		.SetDisplay("Close Longs on Short Signal", "Allow closing longs when a short signal appears", "Trading");
	}

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

	/// <summary>
	/// Defines whether the strategy trades with or against the oscillator direction.
	/// </summary>
	public WeightOscillatorTrendModes TrendMode
	{
		get => _trendMode.Value;
		set => _trendMode.Value = value;
	}

	/// <summary>
	/// Number of closed bars to skip when evaluating the composite oscillator.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	/// <summary>
	/// Weight assigned to RSI.
	/// </summary>
	public decimal RsiWeight
	{
		get => _rsiWeight.Value;
		set => _rsiWeight.Value = value;
	}

	/// <summary>
	/// RSI lookback period.
	/// </summary>
	public int RsiPeriod
	{
		get => _rsiPeriod.Value;
		set => _rsiPeriod.Value = value;
	}

	/// <summary>
	/// Weight assigned to MFI.
	/// </summary>
	public decimal MfiWeight
	{
		get => _mfiWeight.Value;
		set => _mfiWeight.Value = value;
	}

	/// <summary>
	/// MFI lookback period.
	/// </summary>
	public int MfiPeriod
	{
		get => _mfiPeriod.Value;
		set => _mfiPeriod.Value = value;
	}

	/// <summary>
	/// Weight assigned to Williams %R.
	/// </summary>
	public decimal WprWeight
	{
		get => _wprWeight.Value;
		set => _wprWeight.Value = value;
	}

	/// <summary>
	/// Williams %R lookback period.
	/// </summary>
	public int WprPeriod
	{
		get => _wprPeriod.Value;
		set => _wprPeriod.Value = value;
	}

	/// <summary>
	/// Weight assigned to DeMarker oscillator.
	/// </summary>
	public decimal DeMarkerWeight
	{
		get => _deMarkerWeight.Value;
		set => _deMarkerWeight.Value = value;
	}

	/// <summary>
	/// DeMarker lookback period.
	/// </summary>
	public int DeMarkerPeriod
	{
		get => _deMarkerPeriod.Value;
		set => _deMarkerPeriod.Value = value;
	}

	/// <summary>
	/// Smoothing method applied to the blended oscillator.
	/// </summary>
	public WeightOscillatorSmoothingMethods SmoothingMethod
	{
		get => _smoothingMethod.Value;
		set => _smoothingMethod.Value = value;
	}

	/// <summary>
	/// Length of the smoothing moving average.
	/// </summary>
	public int SmoothingLength
	{
		get => _smoothingLength.Value;
		set => _smoothingLength.Value = value;
	}

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

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

	/// <summary>
	/// Enables opening long positions.
	/// </summary>
	public bool BuyOpenEnabled
	{
		get => _buyOpenEnabled.Value;
		set => _buyOpenEnabled.Value = value;
	}

	/// <summary>
	/// Enables opening short positions.
	/// </summary>
	public bool SellOpenEnabled
	{
		get => _sellOpenEnabled.Value;
		set => _sellOpenEnabled.Value = value;
	}

	/// <summary>
	/// Enables closing short positions on a long signal.
	/// </summary>
	public bool BuyCloseEnabled
	{
		get => _buyCloseEnabled.Value;
		set => _buyCloseEnabled.Value = value;
	}

	/// <summary>
	/// Enables closing long positions on a short signal.
	/// </summary>
	public bool SellCloseEnabled
	{
		get => _sellCloseEnabled.Value;
		set => _sellCloseEnabled.Value = value;
	}

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

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

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

		_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
		_mfi = new MoneyFlowIndex { Length = MfiPeriod };
		_wpr = new WilliamsR { Length = WprPeriod };
		_deMarker = new DeMarker { Length = DeMarkerPeriod };
		_smoothing = CreateSmoothingIndicator();

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_rsi, _mfi, _wpr, _deMarker, ProcessCandle)
			.Start();

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

		var step = Security?.PriceStep ?? 1m;
		var takeProfit = TakeProfitPoints > 0 ? new Unit(TakeProfitPoints * step, UnitTypes.Absolute) : null;
		var stopLoss = StopLossPoints > 0 ? new Unit(StopLossPoints * step, UnitTypes.Absolute) : null;

		StartProtection(stopLoss, takeProfit);
	}

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

		var totalWeight = RsiWeight + MfiWeight + WprWeight + DeMarkerWeight;
		if (totalWeight <= 0)
		{
		this.LogInfo("Total oscillator weight must be positive to generate signals.");
		return;
		}

		// Williams %R is negative in StockSharp, so shift it into the 0..100 range.
		var normalizedWpr = wprValue + 100m;
		// DeMarker returns 0..1; scale to match other oscillators.
		var normalizedDeMarker = deMarkerValue * 100m;

		var blended = (RsiWeight * rsiValue + MfiWeight * mfiValue + WprWeight * normalizedWpr + DeMarkerWeight * normalizedDeMarker) / totalWeight;

		var smoothedValue = _smoothing.Process(new DecimalIndicatorValue(_smoothing, blended, candle.OpenTime) { IsFinal = true });
		if (!smoothedValue.IsFinal)
		return;

		var oscillator = smoothedValue.ToDecimal();

		_oscillatorHistory.Add(oscillator);
		if (_oscillatorHistory.Count > 512)
		_oscillatorHistory.RemoveAt(0);

		var requiredCount = SignalBar + 2;
		if (_oscillatorHistory.Count < requiredCount)
		return;

		var current = GetHistoryValue(SignalBar);
		var previous = GetHistoryValue(SignalBar + 1);
		var prior = GetHistoryValue(SignalBar + 2);

		// Rising when slope turns up over the last two steps.
		var rising = previous < prior && current > previous;
		// Falling when slope turns down over the last two steps.
		var falling = previous > prior && current < previous;

		bool longSignal;
		bool shortSignal;

		if (TrendMode == WeightOscillatorTrendModes.Direct)
		{
		longSignal = rising;
		shortSignal = falling;
		}
		else
		{
		longSignal = falling;
		shortSignal = rising;
		}

		if (longSignal)
		{
		if (BuyCloseEnabled && Position < 0)
		{
		BuyMarket(Math.Abs(Position));
		}

		if (BuyOpenEnabled && Position <= 0)
		{
		BuyMarket(Volume > 0m ? Volume : 1m);
		}
		}

		if (shortSignal)
		{
		if (SellCloseEnabled && Position > 0)
		{
		SellMarket(Math.Abs(Position));
		}

		if (SellOpenEnabled && Position >= 0)
		{
		SellMarket(Volume > 0m ? Volume : 1m);
		}
		}
	}

	private IIndicator CreateSmoothingIndicator()
	{
		return SmoothingMethod switch
		{
			WeightOscillatorSmoothingMethods.Simple => new SMA { Length = SmoothingLength },
			WeightOscillatorSmoothingMethods.Exponential => new EMA { Length = SmoothingLength },
			WeightOscillatorSmoothingMethods.Smoothed => new SmoothedMovingAverage { Length = SmoothingLength },
			WeightOscillatorSmoothingMethods.Weighted => new WeightedMovingAverage { Length = SmoothingLength },
			WeightOscillatorSmoothingMethods.Kaufman => new KaufmanAdaptiveMovingAverage { Length = SmoothingLength },
			_ => new JurikMovingAverage { Length = SmoothingLength },
		};
	}

	private decimal GetHistoryValue(int shift)
	{
		return _oscillatorHistory[_oscillatorHistory.Count - shift];
	}
}