在 GitHub 上查看

Liquidex Keltner

Liquidex Keltner 策略结合移动平均线和 Keltner 通道来交易突破。 仅在指定的时间段内交易,并可选择使用 RSI 方向过滤。 止损和止盈通过固定百分比进行管理。

详情

  • 入场条件
    • 价格突破上轨并收于移动平均线上方。
    • 价格突破下轨并收于移动平均线下方。
    • K线实体必须大于 RangeFilter
    • 启用 UseRsiFilter 时,多头需 RSI > 50,空头需 RSI < 50。
    • 当前时间需在 EntryHourFromEntryHourTo 之间,且周五需早于 FridayEndHour
  • 多头/空头:双向。
  • 出场条件:止损或止盈。
  • 止损:是,通过 StartProtection 的百分比。
  • 默认值
    • MaPeriod = 7
    • RangeFilter = 10m
    • StopLoss = 1m
    • TakeProfit = 2m
    • UseKeltnerFilter = true
    • KeltnerPeriod = 6
    • KeltnerMultiplier = 1m
    • UseRsiFilter = false
    • RsiPeriod = 14
    • EntryHourFrom = 2
    • EntryHourTo = 24
    • FridayEndHour = 22
    • CandleType = TimeSpan.FromMinutes(15).TimeFrame()
  • 过滤器
    • 类别: 突破
    • 方向: 双向
    • 指标: MA, Keltner, RSI
    • 止损: 是
    • 复杂度: 中等
    • 时间框架: 日内 (15m)
    • 季节性: 否
    • 神经网络: 否
    • 背离: 否
    • 风险等级: 中等
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>
/// Liquidex strategy using moving average and Keltner Channel filter.
/// Trades are executed only during configured hours and can be confirmed by RSI direction.
/// </summary>
public class LiquidexKeltnerStrategy : Strategy
{
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<decimal> _rangeFilter;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<bool> _useKeltnerFilter;
	private readonly StrategyParam<int> _keltnerPeriod;
	private readonly StrategyParam<decimal> _keltnerMultiplier;
	private readonly StrategyParam<bool> _useRsiFilter;
	private readonly StrategyParam<int> _rsiPeriod;
	private readonly StrategyParam<int> _entryHourFrom;
	private readonly StrategyParam<int> _entryHourTo;
	private readonly StrategyParam<int> _fridayEndHour;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevPrice;
	private SimpleMovingAverage _ma;
	private RelativeStrengthIndex _rsi;

	/// <summary>
	/// Moving average period.
	/// </summary>
	public int MaPeriod { get => _maPeriod.Value; set => _maPeriod.Value = value; }

	/// <summary>
	/// Minimum candle body size to trade.
	/// </summary>
	public decimal RangeFilter { get => _rangeFilter.Value; set => _rangeFilter.Value = value; }

	/// <summary>
	/// Stop-loss in percent.
	/// </summary>
	public decimal StopLoss { get => _stopLoss.Value; set => _stopLoss.Value = value; }

	/// <summary>
	/// Take-profit in percent.
	/// </summary>
	public decimal TakeProfit { get => _takeProfit.Value; set => _takeProfit.Value = value; }

	/// <summary>
	/// Enable Keltner Channel filter.
	/// </summary>
	public bool UseKeltnerFilter { get => _useKeltnerFilter.Value; set => _useKeltnerFilter.Value = value; }

	/// <summary>
	/// Period for Keltner Channels.
	/// </summary>
	public int KeltnerPeriod { get => _keltnerPeriod.Value; set => _keltnerPeriod.Value = value; }

	/// <summary>
	/// Width multiplier for Keltner Channels.
	/// </summary>
	public decimal KeltnerMultiplier { get => _keltnerMultiplier.Value; set => _keltnerMultiplier.Value = value; }

	/// <summary>
	/// Enable RSI direction filter.
	/// </summary>
	public bool UseRsiFilter { get => _useRsiFilter.Value; set => _useRsiFilter.Value = value; }

	/// <summary>
	/// RSI indicator period.
	/// </summary>
	public int RsiPeriod { get => _rsiPeriod.Value; set => _rsiPeriod.Value = value; }

	/// <summary>
	/// Start trading hour.
	/// </summary>
	public int EntryHourFrom { get => _entryHourFrom.Value; set => _entryHourFrom.Value = value; }

	/// <summary>
	/// End trading hour.
	/// </summary>
	public int EntryHourTo { get => _entryHourTo.Value; set => _entryHourTo.Value = value; }

	/// <summary>
	/// Last trading hour on Friday.
	/// </summary>
	public int FridayEndHour { get => _fridayEndHour.Value; set => _fridayEndHour.Value = value; }

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

	/// <summary>
	/// Initializes a new instance of <see cref="LiquidexKeltnerStrategy"/>.
	/// </summary>
	public LiquidexKeltnerStrategy()
	{
		_maPeriod = Param(nameof(MaPeriod), 7)
			.SetRange(1, 100)
			.SetDisplay("MA Period", "Moving average period", "General");

		_rangeFilter = Param(nameof(RangeFilter), 0m)
			.SetRange(0m, 100m)
			.SetDisplay("Range Filter", "Minimum candle body", "General");

		_stopLoss = Param(nameof(StopLoss), 1m)
			.SetRange(0m, 10m)
			.SetDisplay("Stop Loss %", "Stop loss percent", "Risk Management");

		_takeProfit = Param(nameof(TakeProfit), 2m)
			.SetRange(0m, 20m)
			.SetDisplay("Take Profit %", "Take profit percent", "Risk Management");

		_useKeltnerFilter = Param(nameof(UseKeltnerFilter), true)
			.SetDisplay("Use Keltner", "Enable Keltner filter", "Filters");

		_keltnerPeriod = Param(nameof(KeltnerPeriod), 6)
			.SetRange(1, 100)
			.SetDisplay("Keltner Period", "Keltner period", "Filters");

		_keltnerMultiplier = Param(nameof(KeltnerMultiplier), 1m)
			.SetRange(0.5m, 5m)
			.SetDisplay("Keltner Multiplier", "Keltner width", "Filters");

		_useRsiFilter = Param(nameof(UseRsiFilter), false)
			.SetDisplay("Use RSI", "Enable RSI filter", "Filters");

		_rsiPeriod = Param(nameof(RsiPeriod), 14)
			.SetRange(1, 100)
			.SetDisplay("RSI Period", "RSI period", "Filters")
			;

		_entryHourFrom = Param(nameof(EntryHourFrom), 2)
			.SetRange(0, 23)
			.SetDisplay("Entry From", "Start hour", "Time");

		_entryHourTo = Param(nameof(EntryHourTo), 24)
			.SetRange(0, 24)
			.SetDisplay("Entry To", "End hour", "Time");

		_fridayEndHour = Param(nameof(FridayEndHour), 22)
			.SetRange(0, 24)
			.SetDisplay("Friday End", "Friday closing hour", "Time");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_prevPrice = 0m;
	}

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

		_ma = new SimpleMovingAverage
		{
			Length = MaPeriod,
		};

		var keltner = new KeltnerChannels
		{
			Length = KeltnerPeriod,
			Multiplier = KeltnerMultiplier,
		};

		_rsi = new RelativeStrengthIndex
		{
			Length = RsiPeriod,
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(keltner, ProcessCandle)
			.Start();

		StartProtection(
			stopLoss: new Unit(StopLoss, UnitTypes.Percent),
			takeProfit: new Unit(TakeProfit, UnitTypes.Percent)
		);

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _ma);
			if (UseKeltnerFilter)
				DrawIndicator(area, keltner);
			if (UseRsiFilter)
				DrawIndicator(area, _rsi);
			DrawOwnTrades(area);
		}
	}

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

		var price = candle.ClosePrice;
		var time = candle.CloseTime;

		// process MA and RSI manually
		var maResult = _ma.Process(price, candle.OpenTime, true);
		var rsiResult = _rsi.Process(price, candle.OpenTime, true);

		if (!IsTradingTime(time))
		{
			_prevPrice = price;
			return;
		}

		var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);
		if (body < RangeFilter)
		{
			_prevPrice = price;
			return;
		}

		if (!maResult.IsFinal || !maResult.IsFormed)
		{
			_prevPrice = price;
			return;
		}

		var ma = maResult.ToDecimal();

		var rsiVal = rsiResult.IsFormed ? rsiResult.ToDecimal() : 50m;

		if (UseKeltnerFilter)
		{
			var kc = (KeltnerChannelsValue)keltnerValue;

			if (kc.Upper is not decimal upper || kc.Lower is not decimal lower)
			{
				_prevPrice = price;
				return;
			}

			var crossAbove = _prevPrice > 0 && _prevPrice <= upper && price > upper;
			var crossBelow = _prevPrice > 0 && _prevPrice >= lower && price < lower;

			if (crossAbove && price > ma && (!UseRsiFilter || rsiVal > 50m) && Position <= 0)
			{
				if (Position < 0) BuyMarket();
				BuyMarket();
			}
			else if (crossBelow && price < ma && (!UseRsiFilter || rsiVal < 50m) && Position >= 0)
			{
				if (Position > 0) SellMarket();
				SellMarket();
			}
		}
		else
		{
			if (price > ma && (!UseRsiFilter || rsiVal > 50m) && Position <= 0)
			{
				if (Position < 0) BuyMarket();
				BuyMarket();
			}
			else if (price < ma && (!UseRsiFilter || rsiVal < 50m) && Position >= 0)
			{
				if (Position > 0) SellMarket();
				SellMarket();
			}
		}

		_prevPrice = price;
	}

	private bool IsTradingTime(DateTime time)
	{
		var hour = time.Hour;

		if (time.DayOfWeek == DayOfWeek.Friday && hour >= FridayEndHour)
			return false;

		if (EntryHourFrom <= EntryHourTo)
			return hour >= EntryHourFrom && hour <= EntryHourTo;

		return hour >= EntryHourFrom || hour <= EntryHourTo;
	}
}