Ver en GitHub

Estrategia de Open Close

Descripción general

Open Close es un port del asesor experto MetaTrader 5 Open Close.mq5 (ticket 23090). La estrategia observa la relación entre las aperturas y cierres de las dos velas finalizadas más recientes. Opera una sola posición a la vez: cuando la vela más nueva se revierte en relación con la anterior, la estrategia entra; y cuando ambas velas apuntan en la misma dirección, sale. La versión en C# reproduce el dimensionamiento adaptativo de lotes original que reduce la exposición tras una racha de operaciones perdedoras.

Lógica de la estrategia

Filtro de patrón de velas

  • La estrategia trabaja exclusivamente con velas completadas suministradas por el parámetro configurable CandleType.
  • Mantiene una ventana deslizante de las dos últimas velas finalizadas (denominadas previous y older).
  • El patrón compara tanto las aperturas como los cierres de estas velas:
    • Reversión alcistaprevious.Open > older.Open y previous.Close < older.Close.
    • Reversión bajistaprevious.Open < older.Open y previous.Close > older.Close.

Reglas de entrada

  • Si no hay posición abierta y aparece el patrón de reversión alcista, la estrategia envía una orden de compra de mercado.
  • Si no hay posición abierta y aparece el patrón de reversión bajista, envía una orden de venta de mercado.
  • Solo se permite una posición. Las señales opuestas se ignoran hasta que se cierra la operación activa.

Reglas de salida

  • Cuando se mantiene una posición larga, la estrategia sale si las dos velas rastreadas se mueven más bajas (previous.Open < older.Open y previous.Close < older.Close).
  • Cuando se mantiene una posición corta, el desencadenante de salida es simétrico (previous.Open > older.Open y previous.Close > older.Close).
  • No hay órdenes de stop-loss o take-profit en el asesor original, por lo que el port depende únicamente de la relación de velas para cerrar operaciones.

Dimensionamiento de posición y manejo de racha de pérdidas

  • El volumen de la orden se determina principalmente por MaximumRiskPercent – la fracción deseada del valor de la cartera invertida por operación. El tamaño bruto es Portfolio.CurrentValue × MaximumRiskPercent ÷ referencePrice usando el último cierre como proxy de precio.
  • Si la valoración de la cartera o el precio no están disponibles, el parámetro FallbackVolume actúa como valor predeterminado seguro.
  • Tras cada operación completamente cerrada, el PnL realizado se almacena. La racha de pérdidas consecutivas se cuenta durante los últimos HistoryDays días.
    • Cuando la racha es mayor a una operación, el tamaño de la siguiente orden se reduce en volume × losses ÷ DecreaseFactor, imitando la lógica de MT5.
  • El volumen final respeta el paso de volumen del instrumento así como los límites mínimos y máximos de volumen.

Notas adicionales de implementación

  • La estrategia reacciona solo a CandleStates.Finished, asegurando que el patrón use datos de mercado completos.
  • Las comprobaciones de entrada y salida ocurren al cierre de la vela más nueva. En MetaTrader, la orden se envía a la apertura de la siguiente barra; la diferencia es insignificante para marcos temporales más altos pero debe considerarse para intervalos muy cortos.
  • Las métricas de cartera en StockSharp aproximan la información de cuenta de MetaTrader. Ajuste MaximumRiskPercent o FallbackVolume si el broker usa multiplicadores de contrato diferentes.

Parámetros

Parámetro Tipo Predeterminado Descripción
MaximumRiskPercent decimal 0.02 Fracción del valor de cartera usada para dimensionar una nueva posición (0.02 = 2%).
DecreaseFactor decimal 3 Divisor aplicado al tamaño del lote tras operaciones perdedoras consecutivas. Valores mayores suavizan la reducción.
HistoryDays int 60 Número de días de calendario escaneados al contar la última racha de pérdidas.
FallbackVolume decimal 0.1 Volumen de orden usado cuando no se puede realizar el cálculo basado en riesgo.
CandleType DataType TimeFrame(15m) Serie de velas que proporciona los valores de apertura/cierre para la generación de señales.

Diferencias con la versión MetaTrader

  • Las comprobaciones de margen de cuenta dependen del Portfolio.CurrentValue de StockSharp; MetaTrader usaba AccountFreeMargin. El comportamiento coincide con la regla de riesgo original solo cuando ambas plataformas informan valoraciones similares.
  • El historial de operaciones se recopila de las propias ejecuciones de la estrategia en lugar del historial de toda la terminal. Asegúrese de que la estrategia funcione suficiente tiempo para acumular estadísticas de racha.
  • El port mantiene el modelo de posición única (sin pirámide) y refleja la falta original de órdenes protectoras. Agregue stops externamente si es necesario para el control de riesgo.
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Open Close strategy using EMA crossover.
/// Buys when fast EMA crosses above slow EMA, sells on reverse.
/// </summary>
public class OpenClose23090Strategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public OpenClose23090Strategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 15).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 60).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}