Ketty Channel Breakout — это прямая конвертация эксперта Ketty.mq5 в C#. Стратегия строит краткосрочный ценовой канал в заданное время перед началом торгов и ожидает резкого выхода цены за его пределы. После такого импульса выставляется стоп-заявка на противоположной стороне канала. Дополнительно создаются защитные стоп-лосс и тейк-профит ордера, что полностью повторяет механику отложенных заявок оригинального советника.
Логика работы
Ежедневный сброс. На первой свече каждого торгового дня стратегия удаляет активные отложенные заявки (а при отсутствии позиции — и защитные ордера) и обнуляет статистику по каналу.
Построение канала. В промежутке между ChannelStartHour:ChannelStartMinute и ChannelEndHour:ChannelEndMinute вычисляются максимальные и минимальные значения выбранного таймфрейма CandleType. Полученный диапазон используется как канал пробоя до конца текущего дня.
Цены заявок. Планируемый buy stop равен channelHigh + OrderPriceShiftPips, а sell stop — channelLow - OrderPriceShiftPips. Пересчёт пипсов в цену совпадает с алгоритмом из MQL5: если у инструмента 3 или 5 знаков после запятой, один pip соответствует десяти ценовым шагам, иначе используется один шаг.
Обнаружение сигнала. Как только канал сформирован и текущее время попадает в диапазон PlacingStartHour–PlacingEndHour, анализируется последняя завершённая свеча. Сигнал на покупку появляется, если минимум свечи пробил канал вниз не менее чем на ChannelBreakthroughPips. Сигнал на продажу возникает, когда максимум свечи превысил верхнюю границу на ту же величину.
Управление отложенными ордерами. Одновременно активна только одна заявка. При появлении нового сигнала предыдущая заявка отменяется и регистрируется новая. После наступления PlacingEndHour все отложенные ордера автоматически снимаются.
Защитные ордера. После исполнения отложенной заявки стратегия немедленно отправляет соответствующий стоп-лосс (если StopLossPips больше нуля) и тейк-профит (если TakeProfitPips больше нуля). Эти ордера удаляются при полном закрытии позиции.
Параметры
EntryVolume — базовый объём заявок.
StopLossPips — расстояние от цены входа до стоп-лосса; ноль отключает защиту.
TakeProfitPips — расстояние от цены входа до тейк-профита; ноль отключает защиту.
ChannelStartHour / ChannelStartMinute — начало периода расчёта канала.
ChannelEndHour / ChannelEndMinute — окончание периода расчёта канала. Реализация корректно обрабатывает переход через полночь.
PlacingStartHour — час, с которого допускается размещение отложенных заявок.
PlacingEndHour — час, после которого все отложенные заявки снимаются.
ChannelBreakthroughPips — буфер пробоя, который должна преодолеть последняя свеча перед постановкой заявки.
OrderPriceShiftPips — дополнительный сдвиг цены при размещении стоп-ордера.
VisualizeChannel — при включении на графике отображаются две горизонтальные линии канала.
CandleType — таймфрейм, используемый для расчёта и мониторинга канала.
Дополнительные замечания
Стратегия рассчитана на непрерывный поток данных. При отсутствии свечей внутри временного окна канал будет достраиваться по мере появления новых данных.
Защитные приказы регистрируются отдельными стоп/лимит ордерами уже после входа в позицию, потому что в StockSharp нельзя прикрепить SL/TP к отложенной заявке так же, как в MetaTrader.
Значение EntryVolume должно соответствовать шагу лота у брокера. Рекомендуется выбирать насыщенный таймфрейм — исходная версия использовала минутные свечи.
using System;
using System.Collections.Generic;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Ketty Channel Breakout strategy. Uses Highest/Lowest channel breakout (period 15).
/// </summary>
public class KettyChannelBreakoutStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _period;
private decimal? _prevHigh;
private decimal? _prevLow;
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public int Period { get => _period.Value; set => _period.Value = value; }
public KettyChannelBreakoutStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame()).SetDisplay("Candle Type", "Timeframe", "General");
_period = Param(nameof(Period), 15).SetGreaterThanZero().SetDisplay("Channel Period", "Highest/Lowest lookback", "Indicators");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_prevHigh = null;
_prevLow = null;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_prevHigh = null; _prevLow = null;
var highest = new Highest { Length = Period };
var lowest = new Lowest { Length = Period };
var subscription = SubscribeCandles(CandleType);
subscription.Bind(highest, lowest, ProcessCandle).Start();
var area = CreateChartArea();
if (area != null) { DrawCandles(area, subscription); DrawOwnTrades(area); }
}
private void ProcessCandle(ICandleMessage candle, decimal high, decimal low)
{
if (candle.State != CandleStates.Finished) return;
if (!IsFormedAndOnlineAndAllowTrading()) { _prevHigh = high; _prevLow = low; return; }
if (_prevHigh == null || _prevLow == null) { _prevHigh = high; _prevLow = low; return; }
var close = candle.ClosePrice;
if (close > _prevHigh.Value && Position <= 0) { if (Position < 0) BuyMarket(); BuyMarket(); }
else if (close < _prevLow.Value && Position >= 0) { if (Position > 0) SellMarket(); SellMarket(); }
_prevHigh = high; _prevLow = low;
}
}
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 Highest, Lowest
from StockSharp.Algo.Strategies import Strategy
class ketty_channel_breakout_strategy(Strategy):
def __init__(self):
super(ketty_channel_breakout_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe", "General")
self._period = self.Param("Period", 15) \
.SetDisplay("Channel Period", "Highest/Lowest lookback", "Indicators")
self._prev_high = None
self._prev_low = None
@property
def CandleType(self):
return self._candle_type.Value
@property
def Period(self):
return self._period.Value
def OnReseted(self):
super(ketty_channel_breakout_strategy, self).OnReseted()
self._prev_high = None
self._prev_low = None
def OnStarted2(self, time):
super(ketty_channel_breakout_strategy, self).OnStarted2(time)
self._prev_high = None
self._prev_low = None
highest = Highest()
highest.Length = self.Period
lowest = Lowest()
lowest.Length = self.Period
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(highest, lowest, self._on_process).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _on_process(self, candle, high_value, low_value):
if candle.State != CandleStates.Finished:
return
hv = float(high_value)
lv = float(low_value)
if self._prev_high is None or self._prev_low is None:
self._prev_high = hv
self._prev_low = lv
return
close = float(candle.ClosePrice)
if close > self._prev_high and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
elif close < self._prev_low and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._prev_high = hv
self._prev_low = lv
def CreateClone(self):
return ketty_channel_breakout_strategy()