Ver en GitHub

Estrategia PriceChannel Signal v2

Descripción general

PriceChannel Signal v2 es un sistema de seguimiento de tendencias creado alrededor de un canal Donchian modificado. El asesor experto original MQL5 observa las transiciones en la tendencia del canal, las condiciones de reingreso opcionales cuando el precio retrocede a través de las bandas y los niveles de salida protectores derivados del mismo rango. El puerto StockSharp mantiene el comportamiento original: negocia una sola posición a la vez, reacciona solo ante velas completadas y puede restringirse a una ventana intradiaria.

Lógica comercial

  1. El nivel alto y bajo del canal Donchian se calcula sobre el ChannelPeriod configurado.
  2. El rango bruto se desplaza mediante dos multiplicadores:
    • Factor de Riesgo – comprime las bandas de entrada hacia la mediana del canal.
    • Nivel de salida: construye un segundo par de bandas internas que activan las salidas.
  3. Se mantiene un estado de tendencia:
    • Cuando el cierre supera la banda de entrada superior, la tendencia se vuelve alcista.
    • Cuando el cierre cae por debajo de la banda de entrada inferior, la tendencia se vuelve bajista.
    • En caso contrario se mantiene la tendencia anterior.
  4. Señales generadas a partir de ese estado:
    • Entrada larga – la tendencia cambia de bajista a alcista.
    • Entrada corta – la tendencia cambia de alcista a bajista.
    • Reentrada larga – opcional, el precio cierra nuevamente por encima de la banda superior mientras la tendencia ya es alcista.
    • Reentrada corta – opcional, el precio cierra nuevamente por debajo de la banda inferior mientras la tendencia ya es bajista.
    • Salida larga – opcional, el precio cierra por debajo de la banda de salida alcista después de estar por encima de ella en la barra anterior.
    • Salida corta – opcional, el precio cierra por encima de la banda de salida bajista después de estar por debajo de ella en la barra anterior.
  5. Sólo se permite un pedido por barra y por dirección. La estrategia se niega a abrir una nueva posición si ya hay otra activa.
  6. Si el filtro de tiempo intradiario está habilitado, todas las señales anteriores se ignoran fuera de la ventana configurada.

Parámetros

Parámetro Descripción
ChannelPeriod Donchian longitud retrospectiva utilizada para calcular el canal de precios y las bandas de salida.
RiskFactor Desplazamiento de las bandas de entrada (0–10). Los valores más bajos ensanchan las bandas, los valores más altos las estrechan.
ExitLevel Desplazamiento de las bandas de salida. Debe ser mayor que RiskFactor para permanecer dentro del rango de entrada.
UseReEntry Permite operaciones de reingreso cuando el precio retrocede a través de la banda activa.
UseExitSignals Habilita la lógica de salida basada en las bandas protectoras internas.
CandleType Marco de tiempo utilizado para construir velas y ejecutar los indicadores.
UseTimeControl Alterna la ventana de negociación intradía.
StartHour / StartMinute Inicio inclusivo de la ventana de negociación cuando el control de tiempo está activo.
EndHour / EndMinute Fin exclusivo de la ventana de negociación cuando el control de tiempo está activo.

Reglas de entrada y salida.

  • Entrar en largo: la tendencia cambia a alcista o se activa la condición de reingreso, la posición actual es plana y la barra está dentro de la ventana de tiempo permitida.
  • Entrar en corto: la tendencia cambia a bajista o se dispara una condición de reingreso en corto, la posición actual es plana y la barra está dentro de la ventana de tiempo permitida.
  • Salida larga: UseExitSignals está habilitado y el cierre cae por debajo de la banda de salida después de estar por encima de ella en la barra anterior.
  • Salida corta: UseExitSignals está habilitado y el cierre se eleva por encima de la banda de salida después de estar por debajo de ella en la barra anterior.

Notas adicionales

  • La estrategia funciona con órdenes de mercado y no piramidal.
  • Los valores del indicador se procesan únicamente en velas terminadas para evitar el repintado dentro de la barra.
  • El volumen predeterminado es 1 contrato si no se proporciona explícitamente.
  • El control de tiempo sigue el comportamiento original de EA: la hora de finalización es exclusiva y se admite el ajuste hasta la medianoche.
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();
	}
}