Liquidex Strategy
Breakout strategy that enters when price moves outside Keltner Channel bands and manages risk with stop loss, take profit, break-even and trailing stop.
Details
- Entry Criteria:
- Long: close above the upper Keltner band.
- Short: close below the lower Keltner band.
- Long/Short: Both.
- Exit Criteria:
- Stop loss or take profit level reached.
- Stop moved to break-even after profit target.
- Trailing stop activated.
- Stops: Yes.
- Default Values:
KcPeriod= 10UseKcFilter= trueStopLoss= 30TakeProfit= 0MoveToBe= 15MoveToBeOffset= 2TrailingDistance= 5CandleType= TimeSpan.FromMinutes(15).TimeFrame()
- Filters:
- Category: Channel
- Direction: Both
- Indicators: Keltner
- Stops: Yes
- Complexity: Basic
- Timeframe: Intraday
- Seasonality: No
- Neural networks: No
- Divergence: No
- Risk level: Medium
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>
/// Liquidex breakout strategy using Keltner Channels.
/// </summary>
public class LiquidexStrategy : Strategy
{
private readonly StrategyParam<int> _kcPeriod;
private readonly StrategyParam<bool> _useKcFilter;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _moveToBe;
private readonly StrategyParam<decimal> _moveToBeOffset;
private readonly StrategyParam<decimal> _trailingDistance;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _breakoutPercent;
private readonly StrategyParam<int> _cooldownBars;
private decimal _entryPrice;
private decimal _stopPrice;
private int _cooldownRemaining;
/// <summary>
/// Keltner Channels period.
/// </summary>
public int KcPeriod
{
get => _kcPeriod.Value;
set => _kcPeriod.Value = value;
}
/// <summary>
/// Use Keltner Channel breakout filter.
/// </summary>
public bool UseKcFilter
{
get => _useKcFilter.Value;
set => _useKcFilter.Value = value;
}
/// <summary>
/// Stop loss distance in price units.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Take profit distance in price units.
/// </summary>
public decimal TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
/// <summary>
/// Profit to move stop to break-even.
/// </summary>
public decimal MoveToBe
{
get => _moveToBe.Value;
set => _moveToBe.Value = value;
}
/// <summary>
/// Offset from entry when moving stop to break-even.
/// </summary>
public decimal MoveToBeOffset
{
get => _moveToBeOffset.Value;
set => _moveToBeOffset.Value = value;
}
/// <summary>
/// Trailing stop distance in price units.
/// </summary>
public decimal TrailingDistance
{
get => _trailingDistance.Value;
set => _trailingDistance.Value = value;
}
/// <summary>
/// Candle type to subscribe.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Minimum breakout percentage beyond the Keltner boundary.
/// </summary>
public decimal BreakoutPercent
{
get => _breakoutPercent.Value;
set => _breakoutPercent.Value = value;
}
/// <summary>
/// Number of completed candles to wait after a position change.
/// </summary>
public int CooldownBars
{
get => _cooldownBars.Value;
set => _cooldownBars.Value = value;
}
/// <summary>
/// Initialize Liquidex strategy.
/// </summary>
public LiquidexStrategy()
{
_kcPeriod = Param(nameof(KcPeriod), 10)
.SetDisplay("KC Period", "Keltner Channels period", "Parameters");
_useKcFilter = Param(nameof(UseKcFilter), true)
.SetDisplay("Use KC Filter", "Enable Keltner Channels breakout filter", "Parameters");
_stopLoss = Param(nameof(StopLoss), 60m)
.SetDisplay("Stop Loss", "Stop loss in price units", "Risk");
_takeProfit = Param(nameof(TakeProfit), 120m)
.SetDisplay("Take Profit", "Take profit in price units, 0 disables", "Risk");
_moveToBe = Param(nameof(MoveToBe), 30m)
.SetDisplay("Move To BE", "Profit to move stop to break-even, 0 disables", "Risk");
_moveToBeOffset = Param(nameof(MoveToBeOffset), 4m)
.SetDisplay("BE Offset", "Offset when moving stop to break-even", "Risk");
_trailingDistance = Param(nameof(TrailingDistance), 15m)
.SetDisplay("Trailing", "Trailing stop distance, 0 disables", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle", "Candle type", "General");
_breakoutPercent = Param(nameof(BreakoutPercent), 0.0025m)
.SetDisplay("Breakout %", "Minimum breakout beyond Keltner boundary", "Filters");
_cooldownBars = Param(nameof(CooldownBars), 6)
.SetDisplay("Cooldown Bars", "Completed candles to wait after a position change", "Trading");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0m;
_stopPrice = 0m;
_cooldownRemaining = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var keltner = new KeltnerChannels { Length = KcPeriod };
var subscription = SubscribeCandles(CandleType);
subscription.BindEx(keltner, ProcessCandle).Start();
StartProtection(null, null);
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, keltner);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue keltnerValue)
{
if (candle.State != CandleStates.Finished || !keltnerValue.IsFinal)
return;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
var kc = (KeltnerChannelsValue)keltnerValue;
if (kc.Upper is not decimal upper || kc.Lower is not decimal lower || kc.Middle is not decimal middle)
return;
var price = candle.ClosePrice;
var longBreakout = price > upper && price >= upper * (1m + BreakoutPercent);
var shortBreakout = price < lower && price <= lower * (1m - BreakoutPercent);
if (Position == 0 && _cooldownRemaining == 0)
{
if (!UseKcFilter || longBreakout)
{
BuyMarket();
_entryPrice = price;
_stopPrice = price - StopLoss;
_cooldownRemaining = CooldownBars;
}
else if (!UseKcFilter || shortBreakout)
{
SellMarket();
_entryPrice = price;
_stopPrice = price + StopLoss;
_cooldownRemaining = CooldownBars;
}
}
else if (Position > 0)
{
if (TakeProfit > 0m && price >= _entryPrice + TakeProfit)
{
SellMarket();
_cooldownRemaining = CooldownBars;
}
else if (price <= _stopPrice)
{
SellMarket();
_cooldownRemaining = CooldownBars;
}
else
{
if (MoveToBe > 0m && price - _entryPrice >= MoveToBe)
_stopPrice = Math.Max(_stopPrice, _entryPrice + MoveToBeOffset);
if (TrailingDistance > 0m)
_stopPrice = Math.Max(_stopPrice, price - TrailingDistance);
}
}
else if (Position < 0)
{
if (TakeProfit > 0m && price <= _entryPrice - TakeProfit)
{
BuyMarket();
_cooldownRemaining = CooldownBars;
}
else if (price >= _stopPrice)
{
BuyMarket();
_cooldownRemaining = CooldownBars;
}
else
{
if (MoveToBe > 0m && _entryPrice - price >= MoveToBe)
_stopPrice = Math.Min(_stopPrice, _entryPrice - MoveToBeOffset);
if (TrailingDistance > 0m)
_stopPrice = Math.Min(_stopPrice, price + TrailingDistance);
}
}
}
}
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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import KeltnerChannels
from StockSharp.Algo.Strategies import Strategy
class liquidex_strategy(Strategy):
def __init__(self):
super(liquidex_strategy, self).__init__()
self._kc_period = self.Param("KcPeriod", 10) \
.SetDisplay("KC Period", "Keltner Channels period", "Parameters")
self._use_kc_filter = self.Param("UseKcFilter", True) \
.SetDisplay("Use KC Filter", "Enable Keltner Channels breakout filter", "Parameters")
self._stop_loss = self.Param("StopLoss", 60.0) \
.SetDisplay("Stop Loss", "Stop loss in price units", "Risk")
self._take_profit = self.Param("TakeProfit", 120.0) \
.SetDisplay("Take Profit", "Take profit in price units, 0 disables", "Risk")
self._move_to_be = self.Param("MoveToBe", 30.0) \
.SetDisplay("Move To BE", "Profit to move stop to break-even, 0 disables", "Risk")
self._move_to_be_offset = self.Param("MoveToBeOffset", 4.0) \
.SetDisplay("BE Offset", "Offset when moving stop to break-even", "Risk")
self._trailing_distance = self.Param("TrailingDistance", 15.0) \
.SetDisplay("Trailing", "Trailing stop distance, 0 disables", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle", "Candle type", "General")
self._breakout_percent = self.Param("BreakoutPercent", 0.0025) \
.SetDisplay("Breakout %", "Minimum breakout beyond Keltner boundary", "Filters")
self._cooldown_bars = self.Param("CooldownBars", 6) \
.SetDisplay("Cooldown Bars", "Completed candles to wait after a position change", "Trading")
self._entry_price = 0.0
self._stop_price = 0.0
self._cooldown_remaining = 0
@property
def kc_period(self):
return self._kc_period.Value
@property
def use_kc_filter(self):
return self._use_kc_filter.Value
@property
def stop_loss(self):
return self._stop_loss.Value
@property
def take_profit(self):
return self._take_profit.Value
@property
def move_to_be(self):
return self._move_to_be.Value
@property
def move_to_be_offset(self):
return self._move_to_be_offset.Value
@property
def trailing_distance(self):
return self._trailing_distance.Value
@property
def candle_type(self):
return self._candle_type.Value
@property
def breakout_percent(self):
return self._breakout_percent.Value
@property
def cooldown_bars(self):
return self._cooldown_bars.Value
def OnReseted(self):
super(liquidex_strategy, self).OnReseted()
self._entry_price = 0.0
self._stop_price = 0.0
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(liquidex_strategy, self).OnStarted2(time)
keltner = KeltnerChannels()
keltner.Length = self.kc_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(keltner, self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, keltner)
self.DrawOwnTrades(area)
def process_candle(self, candle, keltner_value):
if candle.State != CandleStates.Finished or not keltner_value.IsFinal:
return
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
upper = keltner_value.Upper
lower = keltner_value.Lower
middle = keltner_value.Middle
if upper is None or lower is None or middle is None:
return
upper = float(upper)
lower = float(lower)
price = float(candle.ClosePrice)
bp = float(self.breakout_percent)
long_breakout = price > upper and price >= upper * (1.0 + bp)
short_breakout = price < lower and price <= lower * (1.0 - bp)
sl = float(self.stop_loss)
tp = float(self.take_profit)
mtb = float(self.move_to_be)
mtbo = float(self.move_to_be_offset)
td = float(self.trailing_distance)
if self.Position == 0 and self._cooldown_remaining == 0:
if not self.use_kc_filter or long_breakout:
self.BuyMarket()
self._entry_price = price
self._stop_price = price - sl
self._cooldown_remaining = self.cooldown_bars
elif not self.use_kc_filter or short_breakout:
self.SellMarket()
self._entry_price = price
self._stop_price = price + sl
self._cooldown_remaining = self.cooldown_bars
elif self.Position > 0:
if tp > 0 and price >= self._entry_price + tp:
self.SellMarket()
self._cooldown_remaining = self.cooldown_bars
elif price <= self._stop_price:
self.SellMarket()
self._cooldown_remaining = self.cooldown_bars
else:
if mtb > 0 and price - self._entry_price >= mtb:
self._stop_price = max(self._stop_price, self._entry_price + mtbo)
if td > 0:
self._stop_price = max(self._stop_price, price - td)
elif self.Position < 0:
if tp > 0 and price <= self._entry_price - tp:
self.BuyMarket()
self._cooldown_remaining = self.cooldown_bars
elif price >= self._stop_price:
self.BuyMarket()
self._cooldown_remaining = self.cooldown_bars
else:
if mtb > 0 and self._entry_price - price >= mtb:
self._stop_price = min(self._stop_price, self._entry_price - mtbo)
if td > 0:
self._stop_price = min(self._stop_price, price + td)
def CreateClone(self):
return liquidex_strategy()