Ver en GitHub

Comerciante WSS

Puerto del asesor experto "Wss_trader" MetaTrader 4 publicado en forex-instruments.info. El EA original combina niveles de reversión estilo Camarilla con distancias de pivote clásicas y abre una única operación por barra cada vez que el precio rompe las bandas configuradas durante la sesión de Londres.

Lógica estratégica

  1. Al comienzo de cada nuevo día de negociación, la estrategia lee el máximo, mínimo y cierre diario anterior para construir una escalera de pivote:
    • Pivot = (High + Low + Close) / 3
    • Long entry = Pivot + Metric × point
    • Short entry = Pivot − Metric × point
    • Long stop = Short entry
    • Short stop = Long entry
    • Los objetivos reflejan las fórmulas MetaTrader Close ± (High − Low) × 1.1 / 2 con la misma abrazadera de seguridad que el código original.
  2. Solo se permite operar entre Start Hour y End Hour (inclusive). Fuera de la ventana, todas las posiciones abiertas se cierran inmediatamente.
  3. Cuando una vela terminada cruza por encima del nivel de entrada largo (cierre >= nivel y cierre anterior <nivel), la estrategia compra una vez con el volumen configurado, adjunta el stop y el objetivo precalculados y bloquea cualquier entrada adicional para esa barra. Se aplica una regla simétrica para los pantalones cortos.
  4. Si la posición se mueve a favor al menos Trailing Points pasos de precio, se sigue el stop para mantener la misma distancia con respecto al precio de cierre. La parada nunca retrocede.

Parámetros

Nombre Descripción Predeterminado
Working Candle Tipo de vela principal utilizado para cálculos intradiarios. 15 Minute
Daily Candle Tipo de vela utilizada para leer el día anterior para los niveles de pivote. 1 Day
Start Hour Hora (0-23) en la que el comercio está habilitado. 8
End Hour Hora (0-23) en la que la negociación deja de aceptar nuevas entradas. 16
Metric Points Distancia desde el pivote hasta los niveles de ruptura medidos en pasos de precios. 20
Trailing Points Distancia del trailing stop en pasos de precio. Establezca en 0 para deshabilitar el seguimiento. 20
Order Volume Tamaño del pedido que refleja el parámetro lots original. 0.1

Notas

  • La estrategia cierra la posición actual tan pronto como finaliza la ventana de negociación, coincidiendo con el comportamiento del EA original.
  • El arrastre se procesa en velas terminadas. El seguimiento intrabar no se reproduce porque StockSharp opera con cierres de velas en este puerto.
  • Solo se permite una operación por vela, replicando la bandera tenb de la versión MQL.

using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Port of the "Wss_trader" MetaTrader strategy built around Camarilla and classic pivot levels.
/// Reproduces the time-filtered breakout entries, fixed targets, and optional trailing stop.
/// </summary>
public class WssTraderStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<DataType> _dailyCandleType;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<int> _metricPoints;
	private readonly StrategyParam<int> _trailingPoints;
	private readonly StrategyParam<decimal> _orderVolume;

	private ICandleMessage _previousDailyCandle;
	private decimal _priceStep;

	private decimal _longEntryLevel;
	private decimal _shortEntryLevel;
	private decimal _longStopLevel;
	private decimal _shortStopLevel;
	private decimal _longTargetLevel;
	private decimal _shortTargetLevel;

	private decimal _previousClose;
	private bool _hasPreviousClose;
	private bool _levelsReady;
	private bool _canTrade;
	private DateTimeOffset? _lastCandleOpenTime;

	private decimal _longEntryPrice;
	private decimal _shortEntryPrice;
	private decimal _longStop;
	private decimal _shortStop;
	private decimal _longTarget;
	private decimal _shortTarget;

	/// <summary>
	/// Initializes a new instance of the <see cref="WssTraderStrategy"/> class.
	/// </summary>
	public WssTraderStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Working Candle", "Primary candle type for trading logic.", "General");

		_dailyCandleType = Param(nameof(DailyCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Daily Candle", "Daily candle type used for pivot calculation.", "General");

		_startHour = Param(nameof(StartHour), 0)
			.SetDisplay("Start Hour", "Hour of day when trading becomes active (0-23).", "Session");

		_endHour = Param(nameof(EndHour), 23)
			.SetDisplay("End Hour", "Hour of day after which trading is disabled (0-23).", "Session");

		_metricPoints = Param(nameof(MetricPoints), 20)
			.SetGreaterThanZero()
			.SetDisplay("Metric Points", "Distance from the pivot to entry levels expressed in price steps.", "Levels");

		_trailingPoints = Param(nameof(TrailingPoints), 20)
			.SetDisplay("Trailing Points", "Trailing stop offset in price steps (0 disables trailing).", "Risk");

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Order volume replicated from the original lots parameter.", "Orders");
	}

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

	/// <summary>
	/// Daily candle type responsible for pivot extraction.
	/// </summary>
	public DataType DailyCandleType
	{
		get => _dailyCandleType.Value;
		set => _dailyCandleType.Value = value;
	}

	/// <summary>
	/// Hour of day when the strategy begins accepting signals.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Hour of day when the strategy stops opening trades.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Distance from the pivot to breakout levels in price steps.
	/// </summary>
	public int MetricPoints
	{
		get => _metricPoints.Value;
		set => _metricPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in price steps.
	/// </summary>
	public int TrailingPoints
	{
		get => _trailingPoints.Value;
		set => _trailingPoints.Value = value;
	}

	/// <summary>
	/// Order volume used for entries.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		if (Security == null)
			yield break;

		yield return (Security, CandleType);

		if (DailyCandleType != null && !Equals(DailyCandleType, CandleType))
			yield return (Security, DailyCandleType);
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_previousDailyCandle = null;
		_priceStep = 0m;

		_longEntryLevel = 0m;
		_shortEntryLevel = 0m;
		_longStopLevel = 0m;
		_shortStopLevel = 0m;
		_longTargetLevel = 0m;
		_shortTargetLevel = 0m;

		_previousClose = 0m;
		_hasPreviousClose = false;
		_levelsReady = false;
		_canTrade = true;
		_lastCandleOpenTime = null;

		_longEntryPrice = 0m;
		_shortEntryPrice = 0m;
		_longStop = 0m;
		_shortStop = 0m;
		_longTarget = 0m;
		_shortTarget = 0m;
	}

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

		_priceStep = Security?.PriceStep ?? 0m;
		if (_priceStep <= 0m)
			_priceStep = 0.0001m;

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

		var dailySubscription = SubscribeCandles(DailyCandleType);
		dailySubscription.Bind(ProcessDailyCandle).Start();

		StartProtection(null, null);

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

	private void ProcessDailyCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_previousDailyCandle != null)
		{
			CalculatePivotLevels(_previousDailyCandle);
			_levelsReady = true;
		}

		_previousDailyCandle = candle;
	}

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

		if (_lastCandleOpenTime != candle.OpenTime)
		{
			_canTrade = true;
			_lastCandleOpenTime = candle.OpenTime;
		}

		var withinHours = IsWithinTradingHours(candle.CloseTime);
		if (!withinHours)
		{
			if (Position != 0m)
			{
				if (Position > 0) SellMarket(); else BuyMarket();
				ResetPositionState();
			}

			_previousClose = candle.ClosePrice;
			_hasPreviousClose = true;
			return;
		}

		ManagePositions(candle);

		if (!_levelsReady)
		{
			_previousClose = candle.ClosePrice;
			_hasPreviousClose = true;
			return;
		}

		if (!_hasPreviousClose)
		{
			_previousClose = candle.ClosePrice;
			_hasPreviousClose = true;
			return;
		}

		if (!_canTrade || Position != 0m)
		{
			_previousClose = candle.ClosePrice;
			return;
		}

		var close = candle.ClosePrice;
		var volume = AdjustVolume(OrderVolume);

		if (volume > 0m && _previousClose < _longEntryLevel && close >= _longEntryLevel)
		{
			BuyMarket();
			_canTrade = false;
			_longEntryPrice = close;
			_longStop = RoundPrice(_longStopLevel);
			_longTarget = RoundPrice(_longTargetLevel);
			_previousClose = close;
			return;
		}

		if (volume > 0m && _previousClose > _shortEntryLevel && close <= _shortEntryLevel)
		{
			SellMarket();
			_canTrade = false;
			_shortEntryPrice = close;
			_shortStop = RoundPrice(_shortStopLevel);
			_shortTarget = RoundPrice(_shortTargetLevel);
			_previousClose = close;
			return;
		}

		_previousClose = close;
	}

	private void ManagePositions(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			ManageLongPosition(candle);
		}
		else if (Position < 0m)
		{
			ManageShortPosition(candle);
		}
	}

	private void ManageLongPosition(ICandleMessage candle)
	{
		var stop = _longStop;
		var target = _longTarget;
		var volume = Position;

		if (volume <= 0m)
			return;

		if (stop > 0m && candle.LowPrice <= stop)
		{
			SellMarket();
			ResetLongState();
			return;
		}

		if (target > 0m && candle.HighPrice >= target)
		{
			SellMarket();
			ResetLongState();
			return;
		}

		var trailingDistance = ConvertPointsToPrice(TrailingPoints);
		if (trailingDistance <= 0m || _longEntryPrice <= 0m)
			return;

		if (candle.ClosePrice - _longEntryPrice >= trailingDistance)
		{
			var newStop = RoundPrice(candle.ClosePrice - trailingDistance);
			if (newStop > _longStop)
			{
				_longStop = newStop;

				if (candle.LowPrice <= _longStop)
				{
					SellMarket();
					ResetLongState();
				}
			}
		}
	}

	private void ManageShortPosition(ICandleMessage candle)
	{
		var stop = _shortStop;
		var target = _shortTarget;
		var volume = Math.Abs(Position);

		if (volume <= 0m)
			return;

		if (stop > 0m && candle.HighPrice >= stop)
		{
			BuyMarket();
			ResetShortState();
			return;
		}

		if (target > 0m && candle.LowPrice <= target)
		{
			BuyMarket();
			ResetShortState();
			return;
		}

		var trailingDistance = ConvertPointsToPrice(TrailingPoints);
		if (trailingDistance <= 0m || _shortEntryPrice <= 0m)
			return;

		if (_shortEntryPrice - candle.ClosePrice >= trailingDistance)
		{
			var newStop = RoundPrice(candle.ClosePrice + trailingDistance);
			if (_shortStop == 0m || newStop < _shortStop)
			{
				_shortStop = newStop;

				if (candle.HighPrice >= _shortStop)
				{
					BuyMarket();
					ResetShortState();
				}
			}
		}
	}

	private void ResetPositionState()
	{
		ResetLongState();
		ResetShortState();
	}

	private void ResetLongState()
	{
		_longEntryPrice = 0m;
		_longStop = 0m;
		_longTarget = 0m;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = 0m;
		_shortStop = 0m;
		_shortTarget = 0m;
	}

	private void CalculatePivotLevels(ICandleMessage dailyCandle)
	{
		var high = dailyCandle.HighPrice;
		var low = dailyCandle.LowPrice;
		var close = dailyCandle.ClosePrice;

		var pivot = (high + low + close) / 3m;
		var metricDistance = MetricPoints * _priceStep;
		var doubleMetric = 2m * metricDistance;
		var twentyPoints = 20m * _priceStep;
		var range = (high - low) * 1.1m / 2m;

		var lwb = RoundPrice(pivot + metricDistance);
		var lwr = RoundPrice(pivot - metricDistance);
		var lrr = RoundPrice(pivot - doubleMetric);

		var rtl = RoundPrice(Math.Max(close + range, lrr - twentyPoints));
		var rts = RoundPrice(Math.Min(close - range, lrr - twentyPoints));

		_longEntryLevel = lwb;
		_shortEntryLevel = lwr;
		_longStopLevel = lwr;
		_shortStopLevel = lwb;
		_longTargetLevel = rtl;
		_shortTargetLevel = rts;
	}

	private decimal AdjustVolume(decimal volume)
	{
		if (Security == null)
			return volume;

		var step = Security.VolumeStep;
		if (step is decimal volumeStep && volumeStep > 0m)
		{
			var steps = Math.Ceiling(volume / volumeStep);
			if (steps < 1m)
				steps = 1m;
			volume = steps * volumeStep;
		}

		var minVolume = Security.MinVolume;
		if (minVolume is decimal min && volume < min)
			volume = min;

		var maxVolume = Security.MaxVolume;
		if (maxVolume is decimal max && max > 0m && volume > max)
			volume = max;

		return volume;
	}

	private decimal RoundPrice(decimal price)
	{
		if (_priceStep > 0m)
			return Math.Round(price / _priceStep) * _priceStep;
		return price;
	}

	private decimal ConvertPointsToPrice(int points)
	{
		if (points <= 0)
			return 0m;

		return points * _priceStep;
	}

	private bool IsWithinTradingHours(DateTimeOffset time)
	{
		var hour = time.Hour;
		var start = Math.Clamp(StartHour, 0, 23);
		var end = Math.Clamp(EndHour, 0, 23);

		if (start <= end)
			return hour >= start && hour <= end;

		return hour >= start || hour <= end;
	}
}