Открыть на GitHub

Стратегия RangeEA Weekly Grid

Обзор

RangeEA Weekly Grid — это сеточная стратегия с отложенными лимитными заявками, портированная из оригинального эксперт-советника MetaTrader. Алгоритм определяет актуальный недельный ценовой диапазон и распределяет внутри него заданное количество отложенных заявок. Для каждой заявки рассчитываются динамические уровни стоп-лосса и тейк-профита: они масштабируются относительно расстояния до текущей цены и дополнительно ограничиваются минимальными значениями в пунктах. При достижении целевой доходности стратегия закрывает все позиции и заявки, фиксируя прибыль.

Реализация опирается на высокоуровневый API StockSharp: логика запускается по свечам, управление заявками осуществляется через встроенные методы стратегии, а параметры риска доступны для оптимизации.

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

  1. Подписка на два потока свечей:
    • Пользовательский таймфрейм (по умолчанию 1 час) для обслуживания сетки.
    • Недельные свечи для оценки диапазона.
  2. После закрытия каждой недельной свечи обновляются максимум и минимум за последние две недели. Разница между ними задаёт рабочий диапазон.
  3. На закрытии каждой рабочей свечи:
    • Проверяется торговое окно (StartTradeHourEndTradeHour).
    • При необходимости сетка сбрасывается в начале нового торгового дня.
    • При отсутствии активных лимитных заявок формируется новая сетка, равномерно покрывающая диапазон.
    • Если уже исполнено минимум две заявки, то при сокращении сетки до NumberOfOrders - 2 заявок возобновляется заявка по цене предпоследнего исполнения.
    • Контролируется прирост капитала, и при достижении целевого процента выполняется полная ликвидация позиций.
  4. По окончании торгового окна и при включённом CloseAllAtEndTrade стратегия отменяет все отложенные заявки и закрывает открытые позиции.

Параметры

Имя Описание Значение по умолчанию
CandleType Таймфрейм для управления сеткой. Часовые свечи
WeeklyCandleType Таймфрейм для расчёта диапазона. Недельные свечи
StartTradeHour Час начала выставления заявок. 0
EndTradeHour Час завершения торговли. 24
CloseAllAtEndTrade Закрывать позиции и заявки вне торгового окна. true
MaxOpenOrders Максимум одновременно открытых заявок и позиций. 5
NumberOfOrders Количество лимитных заявок в сетке. 10
OrderVolume Объём каждой заявки. 0.01
ResetOrdersDaily Перестраивать сетку в начале дня. true
StopLossPoints Минимальный стоп-лосс в пунктах. 60
TakeProfitPoints Минимальный тейк-профит в пунктах. 60
StopLossMultiplier Множитель динамического стоп-лосса. 3
TakeProfitMultiplier Множитель динамического тейк-профита. 1
TargetPercentage Рост капитала, после которого выполняется фиксация. 8

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

  • Ограничение MaxOpenOrders удерживает число заявок и позиций под контролем.
  • Стоп-лосс и тейк-профит всегда находятся не ближе минимального числа пунктов и могут быть увеличены множителями.
  • Ежедневный сброс не позволяет переносить устаревшие заявки на следующий день.
  • Целевой процент прибыли фиксирует накопленный результат и предотвращает переразмеренный риск.

Примечания

  • Необходимо, чтобы по инструменту поставлялись недельные свечи, иначе диапазон не будет рассчитан.
  • Для инструментов с нестандартным шагом цены следует скорректировать параметры в пунктах под фактический тик.
  • Оптимизация NumberOfOrders, OrderVolume и множителей стопов/тейков помогает адаптировать стратегию к различной волатильности.
using System;
using System.Collections.Generic;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Range based grid strategy that detects the trading range from recent price action
/// and places buy/sell limit orders at grid levels within the range.
/// Buys at lower grid levels, sells at upper grid levels.
/// </summary>
public class RangeWeeklyGridStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rangePeriod;
	private readonly StrategyParam<int> _gridLevels;

	private decimal _rangeHigh;
	private decimal _rangeLow;
	private bool _rangeSet;
	private decimal _entryPrice;
	private DateTimeOffset _lastTradeTime;

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

	public int RangePeriod
	{
		get => _rangePeriod.Value;
		set => _rangePeriod.Value = value;
	}

	public int GridLevels
	{
		get => _gridLevels.Value;
		set => _gridLevels.Value = value;
	}

	public RangeWeeklyGridStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle type", "General");

		_rangePeriod = Param(nameof(RangePeriod), 100)
			.SetDisplay("Range Period", "Number of candles to determine range", "Logic");

		_gridLevels = Param(nameof(GridLevels), 5)
			.SetDisplay("Grid Levels", "Number of grid levels within the range", "Logic");
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_rangeHigh = 0;
		_rangeLow = 0;
		_rangeSet = false;
		_entryPrice = 0;
		_lastTradeTime = default;
	}

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

		var highest = new Highest { Length = RangePeriod };
		var lowest = new Lowest { Length = RangePeriod };

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

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

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

		if (highestValue <= 0 || lowestValue <= 0 || highestValue <= lowestValue)
			return;

		_rangeHigh = highestValue;
		_rangeLow = lowestValue;
		_rangeSet = true;

		if (!_rangeSet)
			return;

		var range = _rangeHigh - _rangeLow;
		if (range <= 0)
			return;

		// Cooldown: at least 1 day between trades
		if (_lastTradeTime != default && candle.CloseTime - _lastTradeTime < TimeSpan.FromDays(1))
			return;

		var gridStep = range / (GridLevels + 1);
		var close = candle.ClosePrice;
		var mid = (_rangeHigh + _rangeLow) / 2;

		// Buy when price is in lower portion of range
		if (close <= _rangeLow + gridStep && Position <= 0)
		{
			BuyMarket();
			_entryPrice = close;
			_lastTradeTime = candle.CloseTime;
		}
		// Sell when price is in upper portion of range
		else if (close >= _rangeHigh - gridStep && Position >= 0)
		{
			SellMarket();
			_entryPrice = close;
			_lastTradeTime = candle.CloseTime;
		}
		// Take profit at mid-range
		else if (Position > 0 && close >= mid)
		{
			SellMarket();
			_lastTradeTime = candle.CloseTime;
		}
		else if (Position < 0 && close <= mid)
		{
			BuyMarket();
			_lastTradeTime = candle.CloseTime;
		}
	}
}