GitHub で見る

Karpenko Channel Strategy

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;
	}
}