Открыть на GitHub

Стратегия PriceChannel Signal v2

Общее описание

PriceChannel Signal v2 — трендовая стратегия на основе модифицированного канала Дончиана. Оригинальный советник MQL5 отслеживает смену тренда по каналу, дополнительные сигналы на повторный вход при повторном пробое диапазона и защитные уровни выхода, построенные из того же диапазона. Порт на StockSharp полностью сохраняет логику: работает с одной позицией, анализирует только завершённые свечи и при необходимости ограничивает торговлю временным окном.

Логика работы

  1. Верхняя и нижняя границы канала Дончиана рассчитываются на интервале ChannelPeriod.
  2. Полученный диапазон смещается двумя коэффициентами:
    • Risk Factor — сдвигает входные полосы к центру диапазона.
    • Exit Level — формирует внутренние защитные полосы для выходов.
  3. Поддерживается состояние тренда:
    • Закрытие выше верхней входной полосы переводит тренд в бычий режим.
    • Закрытие ниже нижней входной полосы переводит тренд в медвежий режим.
    • Если пробоя нет, сохраняется предыдущее состояние.
  4. Сигналы:
    • Покупка — переход тренда из медвежьего в бычий.
    • Продажа — переход тренда из бычьего в медвежий.
    • Повторный вход вверх — при включённой опции цена закрывается выше верхней полосы, когда тренд уже бычий.
    • Повторный вход вниз — при включённой опции цена закрывается ниже нижней полосы, когда тренд уже медвежий.
    • Выход из лонга — при включённой опции закрытие опускается ниже защитной полосы после нахождения выше неё на предыдущей свече.
    • Выход из шорта — при включённой опции закрытие поднимается выше защитной полосы после нахождения ниже неё на предыдущей свече.
  5. На одну свечу допускается не более одной заявки в каждом направлении. Новая позиция не открывается, если предыдущая ещё активна.
  6. При включённом временном фильтре все сигналы игнорируются вне заданного торгового интервала.

Параметры

Параметр Описание
ChannelPeriod Длина истории для канала Дончиана и защитных полос.
RiskFactor Сдвиг входных полос (0–10). Меньшие значения расширяют диапазон, большие — сужают.
ExitLevel Сдвиг защитных полос. Должен быть больше RiskFactor, чтобы располагаться внутри диапазона входа.
UseReEntry Включает повторные входы при пробое активной полосы.
UseExitSignals Включает выходы по внутренним защитным полосам.
CandleType Тип свечей, на которых строятся расчёты.
UseTimeControl Переключатель ограничения по времени.
StartHour / StartMinute Начало торгового окна (включительно) при включённом фильтре.
EndHour / EndMinute Конец торгового окна (исключительно) при включённом фильтре.

Правила входа и выхода

  • Вход в лонг: тренд стал бычьим или выполнено условие повторного входа, позиция отсутствует, свеча внутри торгового окна.
  • Вход в шорт: тренд стал медвежьим или выполнено условие повторного входа вниз, позиция отсутствует, свеча внутри торгового окна.
  • Выход из лонга: включены защитные сигналы и закрытие опустилось ниже защитной полосы после нахождения выше неё на предыдущей свече.
  • Выход из шорта: включены защитные сигналы и закрытие поднялось выше защитной полосы после нахождения ниже неё на предыдущей свече.

Дополнительные замечания

  • Стратегия использует рыночные заявки и не наращивает объём позиции.
  • Обработка выполняется только по завершённым свечам, что исключает перерисовку внутри бара.
  • Объём по умолчанию — 1 контракт, если не задан пользователем.
  • Временной фильтр повторяет поведение оригинала: конечное время задаётся как исключительное, допускается переход через полночь.
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Price Channel Signal v2 strategy that reacts to Donchian channel breakouts.
/// </summary>
public class PriceChannelSignalV2Strategy : Strategy
{
	private readonly StrategyParam<int> _channelPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Queue<decimal> _highHistory = new();
	private readonly Queue<decimal> _lowHistory = new();
	private int _previousTrend;
	private decimal? _previousClose;

	/// <summary>
	/// Channel lookback length.
	/// </summary>
	public int ChannelPeriod
	{
		get => _channelPeriod.Value;
		set => _channelPeriod.Value = value;
	}

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

	/// <summary>
	/// Initialize a new instance of <see cref="PriceChannelSignalV2Strategy"/>.
	/// </summary>
	public PriceChannelSignalV2Strategy()
	{
		_channelPeriod = Param(nameof(ChannelPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Channel Period", "Donchian lookback used for Price Channel", "Price Channel");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for Price Channel", "General");
	}

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

		_previousTrend = 0;
		_previousClose = null;
		_highHistory.Clear();
		_lowHistory.Clear();

		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 (_highHistory.Count < ChannelPeriod)
		{
			EnqueueCandle(candle);
			return;
		}

		var highs = _highHistory.ToArray();
		var lows = _lowHistory.ToArray();
		var channelHigh = GetMax(highs);
		var channelLow = GetMin(lows);
		var range = channelHigh - channelLow;
		if (range <= 0m)
		{
			_previousClose = candle.ClosePrice;
			EnqueueCandle(candle);
			return;
		}

		var mid = (channelHigh + channelLow) / 2m;

		// Update trend state based on channel breakout
		var trend = _previousTrend;
		if (candle.ClosePrice > channelHigh + range * 0.05m)
			trend = 1;
		else if (candle.ClosePrice < channelLow - range * 0.05m)
			trend = -1;

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

		// Trend reversal signals
		var changedPosition = false;

		if (trend > 0 && _previousTrend <= 0)
		{
			if (Position <= 0)
			{
				BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
				changedPosition = true;
			}
		}
		else if (trend < 0 && _previousTrend >= 0)
		{
			if (Position >= 0)
			{
				SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
				changedPosition = true;
			}
		}

		// Exit on mid-line cross
		if (!changedPosition && Position > 0 && _previousClose is decimal pc1 && pc1 >= mid && candle.ClosePrice < mid)
		{
			SellMarket(Math.Abs(Position));
		}
		else if (!changedPosition && Position < 0 && _previousClose is decimal pc2 && pc2 <= mid && candle.ClosePrice > mid)
		{
			BuyMarket(Math.Abs(Position));
		}

		_previousTrend = trend;
		_previousClose = candle.ClosePrice;
		EnqueueCandle(candle);
	}

	private void EnqueueCandle(ICandleMessage candle)
	{
		_highHistory.Enqueue(candle.HighPrice);
		_lowHistory.Enqueue(candle.LowPrice);

		while (_highHistory.Count > ChannelPeriod)
			_highHistory.Dequeue();

		while (_lowHistory.Count > ChannelPeriod)
			_lowHistory.Dequeue();
	}

	private static decimal GetMax(IEnumerable<decimal> values)
	{
		var max = decimal.MinValue;

		foreach (var value in values)
		{
			if (value > max)
				max = value;
		}

		return max;
	}

	private static decimal GetMin(IEnumerable<decimal> values)
	{
		var min = decimal.MaxValue;

		foreach (var value in values)
		{
			if (value < min)
				min = value;
		}

		return min;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_previousTrend = 0;
		_previousClose = null;
		_highHistory.Clear();
		_lowHistory.Clear();

		base.OnReseted();
	}
}