The Karpenko Channel strategy builds a dynamic price channel using two moving averages. The base line is an average of closing prices, while the upper and lower bounds are derived from the average high-low range scaled by the golden ratio 1.618. The channel expands until it envelopes the current bar.
A signal to go long appears when the upper bound, previously above the base line, crosses below it. A short signal arises when the upper bound crosses above the base line after staying below. Existing positions in the opposite direction are closed when the regime changes.
Only finished candles are processed. Fixed stop-loss and take-profit levels protect each trade.
Details
Entry Criteria:
Long: Previous upper bound above base line and current value below or equal to it.
Short: Previous upper bound below base line and current value above or equal to it.
Exit Criteria:
Close long when previous upper bound was below the base line.
Close short when previous upper bound was above the base line.
Stops: Fixed stop-loss and take-profit distances in price units.
Default Values:
Base MA = 144
History = 500
Stop Loss = 1000
Take Profit = 2000
Candle Type = 4 hour
Filters:
Category: Trend following
Direction: Both
Indicators: Custom
Stops: Yes
Complexity: Intermediate
Timeframe: Medium-term
Seasonality: No
Neural networks: No
Divergence: No
Risk level: Medium
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>
/// Karpenko Channel strategy.
/// Generates signals based on dynamic channel and SMA baseline crossover.
/// Long when price is below channel baseline, short when above.
/// </summary>
public class KarpenkoChannelStrategy : Strategy
{
private readonly StrategyParam<int> _basicMa;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private decimal _prevClose;
private decimal _prevMa;
private bool _initialized;
private int _cooldownRemaining;
/// <summary>
/// Period for base moving average.
/// </summary>
public int BasicMa { get => _basicMa.Value; set => _basicMa.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>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public KarpenkoChannelStrategy()
{
_basicMa = Param(nameof(BasicMa), 20)
.SetGreaterThanZero()
.SetDisplay("Base MA", "Length of base moving average", "Parameters");
_cooldownBars = Param(nameof(CooldownBars), 8)
.SetDisplay("Cooldown Bars", "Completed candles to wait after a signal", "Signal");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Type of candles to use", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var sma = new SimpleMovingAverage { Length = BasicMa };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(sma, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevClose = 0m;
_prevMa = 0m;
_initialized = false;
_cooldownRemaining = 0;
}
private void ProcessCandle(ICandleMessage candle, decimal maValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_initialized)
{
_prevClose = candle.ClosePrice;
_prevMa = maValue;
_initialized = true;
return;
}
// Cross above MA -> buy signal
var crossUp = _prevClose <= _prevMa && candle.ClosePrice > maValue;
// Cross below MA -> sell signal
var crossDown = _prevClose >= _prevMa && candle.ClosePrice < maValue;
if (_cooldownRemaining > 0)
_cooldownRemaining--;
if (crossUp && _cooldownRemaining == 0 && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
_cooldownRemaining = CooldownBars;
}
else if (crossDown && _cooldownRemaining == 0 && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
_cooldownRemaining = CooldownBars;
}
_prevClose = candle.ClosePrice;
_prevMa = maValue;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class karpenko_channel_strategy(Strategy):
def __init__(self):
super(karpenko_channel_strategy, self).__init__()
self._basic_ma = self.Param("BasicMa", 20) \
.SetDisplay("Base MA", "Length of base moving average", "Parameters")
self._cooldown_bars = self.Param("CooldownBars", 8) \
.SetDisplay("Cooldown Bars", "Completed candles to wait after a signal", "Signal")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Type of candles to use", "General")
self._prev_close = 0.0
self._prev_ma = 0.0
self._initialized = False
self._cooldown_remaining = 0
@property
def basic_ma(self):
return self._basic_ma.Value
@property
def cooldown_bars(self):
return self._cooldown_bars.Value
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(karpenko_channel_strategy, self).OnReseted()
self._prev_close = 0.0
self._prev_ma = 0.0
self._initialized = False
self._cooldown_remaining = 0
def OnStarted2(self, time):
super(karpenko_channel_strategy, self).OnStarted2(time)
sma = SimpleMovingAverage()
sma.Length = self.basic_ma
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(sma, self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def process_candle(self, candle, ma_value):
if candle.State != CandleStates.Finished:
return
ma_value = float(ma_value)
close = float(candle.ClosePrice)
if not self._initialized:
self._prev_close = close
self._prev_ma = ma_value
self._initialized = True
return
cross_up = self._prev_close <= self._prev_ma and close > ma_value
cross_down = self._prev_close >= self._prev_ma and close < ma_value
if self._cooldown_remaining > 0:
self._cooldown_remaining -= 1
if cross_up and self._cooldown_remaining == 0 and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._cooldown_remaining = self.cooldown_bars
elif cross_down and self._cooldown_remaining == 0 and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._cooldown_remaining = self.cooldown_bars
self._prev_close = close
self._prev_ma = ma_value
def CreateClone(self):
return karpenko_channel_strategy()