Открыть на GitHub

Стратегия Exp XWPR Histogram Vol

Обзор

Данная стратегия — перенос на C# эксперта MetaTrader Exp_XWPR_Histogram_Vol. Алгоритм торгует по смене цветов пользовательского индикатора XWPR Histogram Vol, который умножает Williams %R на объём свечи и сглаживает результат. В версии для StockSharp сохранены два независимых торговых слота (основной и дополнительный объём) и полностью воспроизведены правила входа и выхода, основанные на цветах индикатора.

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

Логика индикатора

  1. Значение Williams %R (WprPeriod) сдвигается на +50 и умножается на выбранный тип объёма (VolumeMode).
  2. Сглаживанию подвергаются и полученный произведение, и исходный объём с помощью одинаковых фильтров (SmoothingMethod, SmoothingLength, SmoothingPhase).
  3. Из сглаженного объёма вычисляются четыре динамических уровня: HighLevel2, HighLevel1, LowLevel1, LowLevel2.
  4. Цвета гистограммы соответствуют следующим зонам:
    • 0 – выше HighLevel2 (сильная бычья зона).
    • 1 – между HighLevel1 и HighLevel2 (умеренно бычья зона).
    • 2 – между LowLevel1 и HighLevel1 (нейтральная зона).
    • 3 – между LowLevel2 и LowLevel1 (умеренно медвежья зона).
    • 4 – ниже LowLevel2 (сильная медвежья зона).

Правила сигналов

Для расчёта берутся два цвета: бар SignalBar + 1 (более старый) и бар SignalBar (более новый).

  • Открыть основной лонг (PrimaryVolume) — если старый цвет равен 1, а новый переходит в 2, 3 или 4. Одновременно формируется запрос на закрытие шортов.
  • Открыть дополнительный лонг (SecondaryVolume) — если старый цвет 0, а новый отличается от 0. Шорты закрываются.
  • Открыть основной шорт (PrimaryVolume) — если старый цвет 3, а новый поднимается в 0, 1 или 2. Лонги закрываются.
  • Открыть дополнительный шорт (SecondaryVolume) — если старый цвет 4, а новый становится 0, 1, 2 или 3, также закрывая лонги.
  • Закрыть лонги — когда старый цвет 3 или 4.
  • Закрыть шорты — когда старый цвет 0 или 1.

Для каждого направления поддерживаются две независимые позиции. Сигнал приводит к заявке только тогда, когда соответствующий слот свободен и разрешён вход (AllowLongEntry, AllowShortEntry).

Управление рисками

  • StopLossSteps и TakeProfitSteps переводятся в защитные заявки StockSharp через StartProtection и задаются в шагах цены.
  • DeviationSteps сохранён ради совместимости с оригиналом и не влияет на выставление рыночных ордеров.

Параметры

Имя Описание
CandleType Таймфрейм свечей, которые поступают на индикатор.
PrimaryVolume, SecondaryVolume Объёмы основного и дополнительного слота.
AllowLongEntry, AllowShortEntry Разрешение на открытие длинных / коротких позиций.
AllowLongExit, AllowShortExit Разрешение на закрытие длинных / коротких позиций.
StopLossSteps, TakeProfitSteps Дистанции защитных ордеров в шагах цены (0 отключает).
DeviationSteps Параметр совместимости, не используемый при заявках.
SignalBar Сдвиг по закрытым свечам при чтении цвета (0 — последняя закрытая свеча).
WprPeriod Период расчёта Williams %R.
VolumeMode Источник объёма: Tick — количество тиков, Real — реальный объём.
HighLevel2, HighLevel1 Множители верхних бычьих уровней.
LowLevel1, LowLevel2 Множители нижних медвежьих уровней.
SmoothingMethod Тип сглаживания гистограммы и базового объёма.
SmoothingLength Длина сглаживающих фильтров.
SmoothingPhase Фаза для сглаживателей на основе Jurik (для остальных методов игнорируется).

Примечания по использованию

  • Стратегия торгует единственным инструментом из GetWorkingSecurities() и использует рыночные заявки.
  • Сигналы рассчитываются один раз на закрытой свече, буфер истории предотвращает повторные заявки на том же баре.
  • Основной и дополнительный слоты работают независимо; чтобы отключить слот, установите его объём в 0 или снимите флаг Allow*Entry.
  • В переносе не используются MetaTrader magic number и режимы маржи. Размер позиции определяется параметрами PrimaryVolume и SecondaryVolume.
namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>
/// Strategy converted from the MetaTrader expert Exp_XWPR_Histogram_Vol.
/// Computes a volume-weighted Williams %R histogram inline and trades on strong colour transitions.
/// </summary>
public class ExpXwprHistogramVolStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _wprPeriod;
	private readonly StrategyParam<int> _smoothingLength;
	private readonly StrategyParam<decimal> _highLevel2;
	private readonly StrategyParam<decimal> _lowLevel2;
	private readonly StrategyParam<int> _signalCooldownBars;

	private WilliamsR _wpr;
	private SimpleMovingAverage _histSma;
	private SimpleMovingAverage _volSma;
	private int? _prevColor;
	private int _cooldownRemaining;
	private DateTimeOffset? _lastEntryTime;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int WprPeriod { get => _wprPeriod.Value; set => _wprPeriod.Value = value; }
	public int SmoothingLength { get => _smoothingLength.Value; set => _smoothingLength.Value = value; }
	public decimal HighLevel2 { get => _highLevel2.Value; set => _highLevel2.Value = value; }
	public decimal LowLevel2 { get => _lowLevel2.Value; set => _lowLevel2.Value = value; }
	public int SignalCooldownBars { get => _signalCooldownBars.Value; set => _signalCooldownBars.Value = value; }

	public ExpXwprHistogramVolStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe", "General");

		_wprPeriod = Param(nameof(WprPeriod), 7)
			.SetGreaterThanZero()
			.SetDisplay("WPR Period", "Williams %R lookback", "Indicator");

		_smoothingLength = Param(nameof(SmoothingLength), 5)
			.SetGreaterThanZero()
			.SetDisplay("Smoothing", "Smoothing length", "Indicator");

		_highLevel2 = Param(nameof(HighLevel2), 17m)
			.SetDisplay("High Level 2", "Strong bullish zone", "Indicator");

		_lowLevel2 = Param(nameof(LowLevel2), -17m)
			.SetDisplay("Low Level 2", "Strong bearish zone", "Indicator");

		_signalCooldownBars = Param(nameof(SignalCooldownBars), 48)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait after a new entry", "Trading");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_wpr = null;
		_histSma = null;
		_volSma = null;
		_prevColor = null;
		_cooldownRemaining = 0;
		_lastEntryTime = null;
	}

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

		_prevColor = null;
		_cooldownRemaining = 0;
		_lastEntryTime = null;

		_wpr = new WilliamsR { Length = WprPeriod };
		_histSma = new SimpleMovingAverage { Length = SmoothingLength };
		_volSma = new SimpleMovingAverage { Length = SmoothingLength };

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

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

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

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		var wprValue = _wpr.Process(candle);
		if (!wprValue.IsFormed)
			return;

		var wpr = wprValue.ToDecimal();
		var volume = candle.TotalVolume > 0m ? candle.TotalVolume : 1m;
		var histRaw = (wpr + 50m) * volume;
		var histSmoothed = _histSma.Process(new DecimalIndicatorValue(_histSma, histRaw, candle.OpenTime) { IsFinal = true });
		var volSmoothed = _volSma.Process(new DecimalIndicatorValue(_volSma, volume, candle.OpenTime) { IsFinal = true });

		if (!histSmoothed.IsFormed || !volSmoothed.IsFormed)
			return;

		var baseline = volSmoothed.ToDecimal();
		if (baseline == 0m)
			return;

		var hist = histSmoothed.ToDecimal();
		var strongBullLevel = HighLevel2 * baseline;
		var strongBearLevel = LowLevel2 * baseline;

		var color = hist >= strongBullLevel ? 0 : hist <= strongBearLevel ? 4 : 2;

		if (_prevColor == null)
		{
			_prevColor = color;
			return;
		}

		var previousColor = _prevColor.Value;
		_prevColor = color;

		if (_cooldownRemaining > 0 || HasRecentEntry(candle))
			return;

		if (previousColor != 0 && color == 0 && Position <= 0)
		{
			var volumeToBuy = Volume + Math.Abs(Position);
			BuyMarket(volumeToBuy);
			_cooldownRemaining = SignalCooldownBars;
			_lastEntryTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;
		}
		else if (previousColor != 4 && color == 4 && Position >= 0)
		{
			var volumeToSell = Volume + Math.Abs(Position);
			SellMarket(volumeToSell);
			_cooldownRemaining = SignalCooldownBars;
			_lastEntryTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;
		}
	}

	private bool HasRecentEntry(ICandleMessage candle)
	{
		if (!_lastEntryTime.HasValue)
			return false;

		var candleTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime;
		return candleTime.Date == _lastEntryTime.Value.Date;
	}
}