Ver en GitHub

Estrategia Falcon Liquidity Grab

Esta estrategia opera capturas de liquidez durante las principales sesiones del mercado utilizando una media móvil simple para definir la tendencia. Entra cuando el precio traspasa niveles de swing recientes y revierte con la tendencia. Cada operación utiliza stop loss y take profit fijos medidos en ticks.

Detalles

  • Condiciones de entrada:
    • Largo: Low < lowest(swing period) && Close > SMA && session filter
    • Corto: High > highest(swing period) && Close < SMA && session filter
  • Condiciones de salida: stop loss y take profit fijos.
  • Tipo: Reversión
  • Indicadores: SMA, Highest, Lowest
  • Marco temporal: 15 minutos (por defecto)
  • Stops: StopLossPoints ticks, TakeProfitMultiplier× distancia del stop

Regla precisa y valores predeterminados: SwingPeriod=20, TrendLength=50, StopLossPoints=50, TakeProfitMultiplier=2, UseSession=true, SessionStart=07:00, SessionEnd=21:00 UTC, CandleType=15 minutos. El mínimo/máximo de la vela cerrada se compara con los extremos de las N velas anteriores (sin incluir la actual). Largo si Low está estrictamente bajo el mínimo previo y Close sobre la SMA; corto si High supera el máximo previo y Close está bajo SMA. La SMA sí incluye la vela de señal. Solo se entra sin posición dentro de la ventana UTC (inicio incluido, fin excluido; admite cruce de medianoche). La protección nativa mediante órdenes de mercado usa bid/ask Level1 entre velas: stop StopLossPoints × Security.PriceStep, objetivo TakeProfitMultiplier × distancia del stop. No hay salida por señal contraria al cierre.

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

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Looks for wick sweeps of preceding swing extremes with an SMA trend filter.
/// Protective stop and target distances are measured in the security's minimum price step.
/// </summary>
public class FalconLiquidityGrabStrategy : Strategy
{
	private readonly StrategyParam<int> _swingPeriod;
	private readonly StrategyParam<int> _trendLength;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitMultiplier;
	private readonly StrategyParam<bool> _useSession;
	private readonly StrategyParam<TimeSpan> _sessionStart;
	private readonly StrategyParam<TimeSpan> _sessionEnd;
	private readonly StrategyParam<DataType> _candleType;
	private readonly Queue<decimal> _previousHighs = new();
	private readonly Queue<decimal> _previousLows = new();
	private Order _pendingOrder;

	public int SwingPeriod { get => _swingPeriod.Value; set => _swingPeriod.Value = value; }
	public int TrendLength { get => _trendLength.Value; set => _trendLength.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public decimal TakeProfitMultiplier { get => _takeProfitMultiplier.Value; set => _takeProfitMultiplier.Value = value; }
	public bool UseSession { get => _useSession.Value; set => _useSession.Value = value; }
	public TimeSpan SessionStart { get => _sessionStart.Value; set => _sessionStart.Value = value; }
	public TimeSpan SessionEnd { get => _sessionEnd.Value; set => _sessionEnd.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public FalconLiquidityGrabStrategy()
	{
		_swingPeriod = Param(nameof(SwingPeriod), 20).SetGreaterThanZero()
			.SetDisplay("Swing Period", "Lookback of previous highs and lows; excludes the signal candle.", "Signal");
		_trendLength = Param(nameof(TrendLength), 50).SetGreaterThanZero()
			.SetDisplay("Trend Length", "SMA trend-filter period.", "Signal");
		_stopLossPoints = Param(nameof(StopLossPoints), 50).SetGreaterThanZero()
			.SetDisplay("Stop Loss Points", "Number of minimum price steps from the entry.", "Protection");
		_takeProfitMultiplier = Param(nameof(TakeProfitMultiplier), 2m).SetGreaterThanZero()
			.SetDisplay("Take Profit Multiplier", "Target distance divided by the stop distance.", "Protection");
		_useSession = Param(nameof(UseSession), true)
			.SetDisplay("Use Session", "Limit entries to the UTC trading window.", "Session");
		_sessionStart = Param(nameof(SessionStart), TimeSpan.FromHours(7))
			.SetDisplay("Session Start", "UTC inclusive session start.", "Session");
		_sessionEnd = Param(nameof(SessionEnd), TimeSpan.FromHours(21))
			.SetDisplay("Session End", "UTC exclusive session end.", "Session");
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe.", "General");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType), (Security, DataType.Level1)];

	protected override void OnReseted()
	{
		base.OnReseted();
		_previousHighs.Clear();
		_previousLows.Clear();
		_pendingOrder = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
			throw new InvalidOperationException("The instrument must supply a positive price step for tick-based protection.");

		var stopDistance = StopLossPoints * priceStep;
		StartProtection(
			new Unit(stopDistance * TakeProfitMultiplier, UnitTypes.Absolute),
			new Unit(stopDistance, UnitTypes.Absolute),
			useMarketOrders: true,
			isLocalStop: true);

		foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
		{
			var quotes = new Subscription(DataType.Level1, Security);
			quotes.MarketData.BuildField = field;
			SubscribeLevel1(quotes).Bind(ObserveProtectionQuote).Start();
		}

		OrderRegistering += order => _pendingOrder = order;
		var trend = new SimpleMovingAverage { Length = TrendLength };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(trend, (candle, value) => ProcessCandle(candle, value, trend)).Start();

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

	private void ObserveProtectionQuote(Level1ChangeMessage quote)
	{
		// Native protection reacts to both quote sides before the next finished signal candle.
	}

	private void ProcessCandle(ICandleMessage candle, decimal trendValue, SimpleMovingAverage trend)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var pendingActive = _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed);
		if (trend.IsFormed && _previousHighs.Count == SwingPeriod && _previousLows.Count == SwingPeriod
			&& Position == 0m && !pendingActive)
		{
			var utcTime = candle.OpenTime.ToUniversalTime().TimeOfDay;
			var inSession = !UseSession || (SessionStart <= SessionEnd
				? utcTime >= SessionStart && utcTime < SessionEnd
				: utcTime >= SessionStart || utcTime < SessionEnd);
			if (inSession)
			{
				if (candle.LowPrice < _previousLows.Min() && candle.ClosePrice > trendValue)
					BuyMarket(Volume);
				else if (candle.HighPrice > _previousHighs.Max() && candle.ClosePrice < trendValue)
					SellMarket(Volume);
			}
		}
		_previousHighs.Enqueue(candle.HighPrice);
		_previousLows.Enqueue(candle.LowPrice);
		while (_previousHighs.Count > SwingPeriod) _previousHighs.Dequeue();
		while (_previousLows.Count > SwingPeriod) _previousLows.Dequeue();
	}
}