Открыть на GitHub

Стратегия WPR Custom Cloud Simple

Обзор

WPR Custom Cloud Simple — это порт эксперта MetaTrader WPR Custom Cloud Simple.mq5 на платформу StockSharp. Робот отслеживает осциллятор Larry Williams %R и открывает сделки, когда индикатор выходит из зон перепроданности или перекупленности. В версии на C# сохранены все ключевые особенности оригинала: анализ только на открытии новой свечи, разворот позиции при обратном сигнале и полный отказ от стоп-лоссов, тейк-профитов и трейлинг-стопов.

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

  1. Подписаться на выбранный таймфрейм (CandleType) и передавать свечи в индикатор WilliamsR.
  2. Ждать закрытия свечи — стратегия не реагирует на незавершённые бары.
  3. Хранить два последних завершённых значения %R, аналогичные обращениям wpr[1] и wpr[2] в MetaTrader.
  4. Формировать сигналы при пересечениях порогов:
    • Покупка: предыдущая свеча закрылась выше OversoldLevel, а свеча до неё находилась ниже уровня — полный аналог условия wpr[2] < уровень и wpr[1] > уровень в исходном коде.
    • Продажа: предыдущая свеча закрылась ниже OverboughtLevel, а ещё одна свеча назад была выше порога — соответствует проверке wpr[2] > уровень и wpr[1] < уровень.
  5. При покупке стратегия сначала закрывает все короткие позиции и покупает базовый объём. При продаже — закрывает длинную позицию и продаёт такой же объём. Благодаря неттинговой модели StockSharp достаточно отправить BuyMarket/SellMarket с объёмом Volume + |Position|, чтобы повторить метатрейдеровский сценарий «закрыть и развернуться».
  6. Никаких дополнительных выходов не используется: только новый противоположный сигнал завершает открытую позицию, что полностью соответствует оригинальному эксперту.

Параметры

Имя Тип Значение по умолчанию Аналог в MetaTrader Описание
WprPeriod int 14 Inp_WPR_Period Длина расчёта индикатора Williams %R.
OverboughtLevel decimal -20 Inp_WPR_Level1 Граница перекупленности; пробой вниз инициирует продажи.
OversoldLevel decimal -80 Inp_WPR_Level2 Граница перепроданности; пробой вверх инициирует покупки.
CandleType DataType Таймфрейм 1 час InpWorkingPeriod Тип свечей, используемый для расчёта и сигналов.
Volume decimal Базовый объём стратегии InpLots Лот для рыночных ордеров; перед открытием позиции стратегия автоматически сводит нетто-позицию к нулю.

Отличия от оригинального эксперта

  • В StockSharp используется неттинговый учёт позиций. Закрытие встречного плеча реализовано увеличением объёма рыночного ордера, поэтому необходимость в структурах STRUCT_POSITION отпала.
  • Все вспомогательные классы MetaTrader (CTrade, CPositionInfo, проверки маржи и т. п.) заменены встроенными механизмами StockSharp. Управление объёмом полностью базируется на Strategy.Volume и параметрах инструмента.
  • Логирование упрощено: высокоуровневый API и так сообщает о статусах заявок, поэтому дополнительные Print не требуются.
  • Отсутствуют защитные заявки, что подчёркивает идею «закрываемся только по обратному сигналу» из оригинала.

Рекомендации по использованию

  • Подберите CandleType в соответствии с таймфреймом в MetaTrader, чтобы получить сопоставимую частоту сигналов.
  • Пороговые значения %R отрицательны. Чем ближе OverboughtLevel к нулю, тем реже появляются продажи; чем ближе OversoldLevel к -100, тем реже возникают покупки.
  • Убедитесь, что Volume соответствует минимальному шагу и лотности инструмента. Перед запуском на реальном счёте скорректируйте базовый объём через интерфейс или код.
using System;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Williams %R cloud breakout strategy converted from the MetaTrader expert advisor.
/// Looks for %R crossings above -80 to go long and below -20 to go short.
/// Positions are reversed when an opposite signal appears.
/// </summary>
public class WprCustomCloudSimpleStrategy : Strategy
{
	private readonly StrategyParam<int> _wprPeriod;
	private readonly StrategyParam<decimal> _overboughtLevel;
	private readonly StrategyParam<decimal> _oversoldLevel;
	private readonly StrategyParam<DataType> _candleType;

	private WilliamsR _williamsR;
	private decimal? _previousWpr;
	private decimal? _olderWpr;

	public int WprPeriod
	{
		get => _wprPeriod.Value;
		set => _wprPeriod.Value = value;
	}

	public decimal OverboughtLevel
	{
		get => _overboughtLevel.Value;
		set => _overboughtLevel.Value = value;
	}

	public decimal OversoldLevel
	{
		get => _oversoldLevel.Value;
		set => _oversoldLevel.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public WprCustomCloudSimpleStrategy()
	{
		_wprPeriod = Param(nameof(WprPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("WPR Period", "Williams %R lookback length", "Williams %R");

		_overboughtLevel = Param(nameof(OverboughtLevel), -10m)
			.SetDisplay("Overbought Level", "%R level that marks overbought conditions", "Williams %R");

		_oversoldLevel = Param(nameof(OversoldLevel), -90m)
			.SetDisplay("Oversold Level", "%R level that marks oversold conditions", "Williams %R");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for Williams %R", "Data");
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_williamsR = new WilliamsR { Length = WprPeriod };
		_previousWpr = null;
		_olderWpr = null;

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

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

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

		if (!_williamsR.IsFormed)
		{
			_olderWpr = _previousWpr;
			_previousWpr = wprValue;
			return;
		}

		if (_previousWpr is not null && _olderWpr is not null)
		{
			var prev = _previousWpr.Value;
			var prevPrev = _olderWpr.Value;

			var crossedAboveOversold = prevPrev < OversoldLevel && prev > OversoldLevel;
			var crossedBelowOverbought = prevPrev > OverboughtLevel && prev < OverboughtLevel;

			var volume = Volume;
			if (volume <= 0)
				volume = 1;

			if (crossedAboveOversold)
			{
				if (Position <= 0)
					BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
			}
			else if (crossedBelowOverbought)
			{
				if (Position >= 0)
					SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
			}
		}

		_olderWpr = _previousWpr;
		_previousWpr = wprValue;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_williamsR = null;
		_previousWpr = null;
		_olderWpr = null;

		base.OnReseted();
	}
}