Liquidex Keltner
Liquidex Keltner 策略结合移动平均线和 Keltner 通道来交易突破。 仅在指定的时间段内交易,并可选择使用 RSI 方向过滤。 止损和止盈通过固定百分比进行管理。
详情
- 入场条件:
- 价格突破上轨并收于移动平均线上方。
- 价格突破下轨并收于移动平均线下方。
- K线实体必须大于
RangeFilter。 - 启用
UseRsiFilter时,多头需 RSI > 50,空头需 RSI < 50。 - 当前时间需在
EntryHourFrom与EntryHourTo之间,且周五需早于FridayEndHour。
- 多头/空头:双向。
- 出场条件:止损或止盈。
- 止损:是,通过
StartProtection的百分比。 - 默认值:
MaPeriod = 7RangeFilter = 10mStopLoss = 1mTakeProfit = 2mUseKeltnerFilter = trueKeltnerPeriod = 6KeltnerMultiplier = 1mUseRsiFilter = falseRsiPeriod = 14EntryHourFrom = 2EntryHourTo = 24FridayEndHour = 22CandleType = 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, DayOfWeek
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import SimpleMovingAverage, KeltnerChannels, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class liquidex_keltner_strategy(Strategy):
def __init__(self):
super(liquidex_keltner_strategy, self).__init__()
self._ma_period = self.Param("MaPeriod", 7) \
.SetDisplay("MA Period", "Moving average period", "General")
self._range_filter = self.Param("RangeFilter", 0.0) \
.SetDisplay("Range Filter", "Minimum candle body", "General")
self._stop_loss_param = self.Param("StopLoss", 1.0) \
.SetDisplay("Stop Loss %", "Stop loss percent", "Risk Management")
self._take_profit_param = self.Param("TakeProfit", 2.0) \
.SetDisplay("Take Profit %", "Take profit percent", "Risk Management")
self._use_keltner_filter = self.Param("UseKeltnerFilter", True) \
.SetDisplay("Use Keltner", "Enable Keltner filter", "Filters")
self._keltner_period = self.Param("KeltnerPeriod", 6) \
.SetDisplay("Keltner Period", "Keltner period", "Filters")
self._keltner_multiplier = self.Param("KeltnerMultiplier", 1.0) \
.SetDisplay("Keltner Multiplier", "Keltner width", "Filters")
self._use_rsi_filter = self.Param("UseRsiFilter", False) \
.SetDisplay("Use RSI", "Enable RSI filter", "Filters")
self._rsi_period = self.Param("RsiPeriod", 14) \
.SetDisplay("RSI Period", "RSI period", "Filters")
self._entry_hour_from = self.Param("EntryHourFrom", 2) \
.SetDisplay("Entry From", "Start hour", "Time")
self._entry_hour_to = self.Param("EntryHourTo", 24) \
.SetDisplay("Entry To", "End hour", "Time")
self._friday_end_hour = self.Param("FridayEndHour", 22) \
.SetDisplay("Friday End", "Friday closing hour", "Time")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Type of candles", "General")
self._prev_price = 0.0
self._ma = None
self._rsi = None
@property
def ma_period(self):
return self._ma_period.Value
@property
def range_filter(self):
return self._range_filter.Value
@property
def stop_loss(self):
return self._stop_loss_param.Value
@property
def take_profit(self):
return self._take_profit_param.Value
@property
def use_keltner_filter(self):
return self._use_keltner_filter.Value
@property
def keltner_period(self):
return self._keltner_period.Value
@property
def keltner_multiplier(self):
return self._keltner_multiplier.Value
@property
def use_rsi_filter(self):
return self._use_rsi_filter.Value
@property
def rsi_period(self):
return self._rsi_period.Value
@property
def entry_hour_from(self):
return self._entry_hour_from.Value
@property
def entry_hour_to(self):
return self._entry_hour_to.Value
@property
def friday_end_hour(self):
return self._friday_end_hour.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(liquidex_keltner_strategy, self).OnReseted()
self._prev_price = 0.0
def OnStarted2(self, time):
super(liquidex_keltner_strategy, self).OnStarted2(time)
self._ma = SimpleMovingAverage()
self._ma.Length = self.ma_period
keltner = KeltnerChannels()
keltner.Length = self.keltner_period
keltner.Multiplier = float(self.keltner_multiplier)
self._rsi = RelativeStrengthIndex()
self._rsi.Length = self.rsi_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(keltner, self.process_candle).Start()
self.StartProtection(
Unit(float(self.take_profit), UnitTypes.Percent),
Unit(float(self.stop_loss), UnitTypes.Percent))
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._ma)
if self.use_keltner_filter:
self.DrawIndicator(area, keltner)
if self.use_rsi_filter:
self.DrawIndicator(area, self._rsi)
self.DrawOwnTrades(area)
def _process_indicator(self, indicator, price, open_time):
return process_float(indicator, price, open_time, True)
def process_candle(self, candle, keltner_value):
if candle.State != CandleStates.Finished:
return
price = float(candle.ClosePrice)
t = candle.CloseTime
# process MA and RSI manually
ma_result = self._process_indicator(self._ma, candle.ClosePrice, candle.OpenTime)
rsi_result = self._process_indicator(self._rsi, candle.ClosePrice, candle.OpenTime)
if not self._is_trading_time(t):
self._prev_price = price
return
body = abs(float(candle.ClosePrice) - float(candle.OpenPrice))
if body < float(self.range_filter):
self._prev_price = price
return
if not ma_result.IsFinal or not ma_result.IsFormed:
self._prev_price = price
return
ma = float(ma_result)
rsi_val = float(rsi_result) if rsi_result.IsFormed else 50.0
if self.use_keltner_filter:
upper_val = keltner_value.Upper
lower_val = keltner_value.Lower
if upper_val is None or lower_val is None:
self._prev_price = price
return
upper = float(upper_val)
lower = float(lower_val)
cross_above = self._prev_price > 0 and self._prev_price <= upper and price > upper
cross_below = self._prev_price > 0 and self._prev_price >= lower and price < lower
if cross_above and price > ma and (not self.use_rsi_filter or rsi_val > 50.0) and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif cross_below and price < ma and (not self.use_rsi_filter or rsi_val < 50.0) and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
else:
if price > ma and (not self.use_rsi_filter or rsi_val > 50.0) and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif price < ma and (not self.use_rsi_filter or rsi_val < 50.0) and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._prev_price = price
def _is_trading_time(self, t):
hour = t.Hour
if t.DayOfWeek == DayOfWeek.Friday and hour >= self.friday_end_hour:
return False
if self.entry_hour_from <= self.entry_hour_to:
return hour >= self.entry_hour_from and hour <= self.entry_hour_to
return hour >= self.entry_hour_from or hour <= self.entry_hour_to
def CreateClone(self):
return liquidex_keltner_strategy()