Стратегия CBC с подтверждением тренда и отдельным стоп-лоссом
Эта стратегия использует состояние смены цвета свечи (CBC) для фиксации пробоя предыдущего максимума или минимума. Вход выполняется при подтверждении тренда через EMA и VWAP и ограничен заданным временным окном. Выход использует тейк-профит на основе ATR и стоп-лосс по экстремумам предыдущей свечи.
Подробности
- Условия входа: смена CBC, опциональный фильтр сильных сигналов, сравнение медленной EMA с VWAP, торговля в заданные часы.
- Лонг/Шорт: Оба направления.
- Условия выхода: тейк-профит по множителю ATR, стоп-лосс на пробой прошлого экстремума.
- Стопы: Да.
- Значения по умолчанию:
AtrLength= 14ProfitTargetMultiplier= 1.0mStrongFlipsOnly= trueEntryStartHour= 10EntryEndHour= 15CandleType= TimeSpan.FromMinutes(5)
- Фильтры:
- Категория: Тренд
- Направление: Оба
- Индикаторы: EMA, VWAP, ATR
- Стопы: Да
- Сложность: Средняя
- Таймфрейм: Внутридневной
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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>
/// CBC with Trend Confirmation and Separate Stop Loss strategy.
/// The color bar change (CBC) state turns bullish when the close breaks above the previous candle's high and bearish when it breaks
/// below the previous candle's low. A flip to bullish goes long when the slow EMA is above the daily VWAP, a flip to bearish goes short
/// when it is below, reversing an opposite position; with StrongFlipsOnly the flip candle also has to close in its direction. Entries
/// are taken only between EntryStartHour and EntryEndHour (UTC). Each entry freezes a target ProfitTargetMultiplier ATRs away and a
/// stop at the previous candle's low (long) or high (short).
/// </summary>
public class CbcWithTrendConfirmationAndSeparateStopLossStrategy : Strategy
{
private readonly StrategyParam<int> _atrLength;
private readonly StrategyParam<decimal> _profitTargetMultiplier;
private readonly StrategyParam<bool> _strongFlipsOnly;
private readonly StrategyParam<int> _entryStartHour;
private readonly StrategyParam<int> _entryEndHour;
private readonly StrategyParam<int> _slowEmaLength;
private readonly StrategyParam<DataType> _candleType;
private ICandleMessage _prevCandle;
private bool? _cbcBullish;
private DateTime _vwapDay;
private decimal _vwapPriceVolume;
private decimal _vwapVolume;
private decimal? _stopPrice;
private decimal? _targetPrice;
/// <summary>
/// ATR period.
/// </summary>
public int AtrLength
{
get => _atrLength.Value;
set => _atrLength.Value = value;
}
/// <summary>
/// ATR multiple of the profit target.
/// </summary>
public decimal ProfitTargetMultiplier
{
get => _profitTargetMultiplier.Value;
set => _profitTargetMultiplier.Value = value;
}
/// <summary>
/// Trade only flips whose candle closes in the flip direction.
/// </summary>
public bool StrongFlipsOnly
{
get => _strongFlipsOnly.Value;
set => _strongFlipsOnly.Value = value;
}
/// <summary>
/// First entry hour (UTC).
/// </summary>
public int EntryStartHour
{
get => _entryStartHour.Value;
set => _entryStartHour.Value = value;
}
/// <summary>
/// Hour entries stop (UTC, exclusive).
/// </summary>
public int EntryEndHour
{
get => _entryEndHour.Value;
set => _entryEndHour.Value = value;
}
/// <summary>
/// Period of the slow trend EMA.
/// </summary>
public int SlowEmaLength
{
get => _slowEmaLength.Value;
set => _slowEmaLength.Value = value;
}
/// <summary>
/// Candle type.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public CbcWithTrendConfirmationAndSeparateStopLossStrategy()
{
_atrLength = Param(nameof(AtrLength), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Length", "ATR period", "Risk");
_profitTargetMultiplier = Param(nameof(ProfitTargetMultiplier), 1.0m)
.SetGreaterThanZero()
.SetDisplay("Profit Target Multiplier", "ATR multiple of the profit target", "Risk");
_strongFlipsOnly = Param(nameof(StrongFlipsOnly), true)
.SetDisplay("Strong Flips Only", "Trade only flips whose candle closes in the flip direction", "Signals");
_entryStartHour = Param(nameof(EntryStartHour), 10)
.SetRange(0, 23)
.SetDisplay("Entry Start Hour", "First entry hour (UTC)", "Session");
_entryEndHour = Param(nameof(EntryEndHour), 15)
.SetRange(1, 24)
.SetDisplay("Entry End Hour", "Hour entries stop (UTC, exclusive)", "Session");
_slowEmaLength = Param(nameof(SlowEmaLength), 21)
.SetGreaterThanZero()
.SetDisplay("Slow EMA Length", "Period of the slow trend EMA", "Trend");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).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 OnReseted()
{
base.OnReseted();
ResetState();
}
private void ResetState()
{
_prevCandle = null;
_cbcBullish = null;
_vwapDay = default;
_vwapPriceVolume = 0m;
_vwapVolume = 0m;
_stopPrice = null;
_targetPrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
ResetState();
var ema = new ExponentialMovingAverage { Length = SlowEmaLength };
var atr = new AverageTrueRange { Length = AtrLength };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(ema, atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue emaValue, IIndicatorValue atrValue)
{
if (candle.State != CandleStates.Finished)
return;
// Session VWAP restarts every UTC day.
var day = candle.OpenTime.Date;
if (day != _vwapDay)
{
_vwapDay = day;
_vwapPriceVolume = 0m;
_vwapVolume = 0m;
}
var typical = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 3m;
_vwapPriceVolume += typical * candle.TotalVolume;
_vwapVolume += candle.TotalVolume;
var prev = _prevCandle;
_prevCandle = candle;
if (prev == null)
return;
var previousState = _cbcBullish;
if (candle.ClosePrice > prev.HighPrice)
_cbcBullish = true;
else if (candle.ClosePrice < prev.LowPrice)
_cbcBullish = false;
if (!emaValue.IsFormed || !atrValue.IsFormed || _vwapVolume <= 0)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
// Stop and target frozen at entry.
if (Position > 0 && _stopPrice is decimal longStop && _targetPrice is decimal longTarget && (candle.LowPrice <= longStop || candle.HighPrice >= longTarget))
{
SellMarket(Position);
_stopPrice = null;
_targetPrice = null;
return;
}
if (Position < 0 && _stopPrice is decimal shortStop && _targetPrice is decimal shortTarget && (candle.HighPrice >= shortStop || candle.LowPrice <= shortTarget))
{
BuyMarket(-Position);
_stopPrice = null;
_targetPrice = null;
return;
}
if (previousState is not bool wasBullish || _cbcBullish is not bool isBullish || wasBullish == isBullish)
return;
var hour = candle.OpenTime.Hour;
if (hour < EntryStartHour || hour >= EntryEndHour)
return;
var ema = emaValue.GetValue<decimal>();
var vwap = _vwapPriceVolume / _vwapVolume;
var atr = atrValue.GetValue<decimal>();
var close = candle.ClosePrice;
if (isBullish && (!StrongFlipsOnly || close > candle.OpenPrice) && ema > vwap && Position <= 0 && prev.LowPrice < close)
{
BuyMarket(Volume + Math.Abs(Position));
_stopPrice = prev.LowPrice;
_targetPrice = close + atr * ProfitTargetMultiplier;
}
else if (!isBullish && (!StrongFlipsOnly || close < candle.OpenPrice) && ema < vwap && Position >= 0 && prev.HighPrice > close)
{
SellMarket(Volume + Math.Abs(Position));
_stopPrice = prev.HighPrice;
_targetPrice = close - atr * ProfitTargetMultiplier;
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage, AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class cbc_with_trend_confirmation_and_separate_stop_loss_strategy(Strategy):
"""
CBC with Trend Confirmation and Separate Stop Loss strategy.
The color bar change (CBC) state turns bullish when the close breaks above the previous candle's high and bearish when it breaks
below the previous candle's low. A flip to bullish goes long when the slow EMA is above the daily VWAP, a flip to bearish goes short
when it is below, reversing an opposite position; with StrongFlipsOnly the flip candle also has to close in its direction. Entries
are taken only between EntryStartHour and EntryEndHour (UTC). Each entry freezes a target ProfitTargetMultiplier ATRs away and a
stop at the previous candle's low (long) or high (short).
"""
def __init__(self):
super(cbc_with_trend_confirmation_and_separate_stop_loss_strategy, self).__init__()
self._atr_length = self.Param("AtrLength", 14).SetGreaterThanZero().SetDisplay("ATR Length", "ATR period", "Risk")
self._profit_target_multiplier = self.Param("ProfitTargetMultiplier", 1.0).SetGreaterThanZero().SetDisplay("Profit Target Multiplier", "ATR multiple of the profit target", "Risk")
self._strong_flips_only = self.Param("StrongFlipsOnly", True).SetDisplay("Strong Flips Only", "Trade only flips whose candle closes in the flip direction", "Signals")
self._entry_start_hour = self.Param("EntryStartHour", 10).SetRange(0, 23).SetDisplay("Entry Start Hour", "First entry hour (UTC)", "Session")
self._entry_end_hour = self.Param("EntryEndHour", 15).SetRange(1, 24).SetDisplay("Entry End Hour", "Hour entries stop (UTC, exclusive)", "Session")
self._slow_ema_length = self.Param("SlowEmaLength", 21).SetGreaterThanZero().SetDisplay("Slow EMA Length", "Period of the slow trend EMA", "Trend")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))).SetDisplay("Candle Type", "Type of candles to use", "General")
self._reset_state()
@property
def candle_type(self):
return self._candle_type.Value
def _reset_state(self):
self._prev_candle = None
self._cbc_bullish = None
self._vwap_day = None
self._vwap_price_volume = Decimal(0)
self._vwap_volume = Decimal(0)
self._stop_price = None
self._target_price = None
def OnReseted(self):
super(cbc_with_trend_confirmation_and_separate_stop_loss_strategy, self).OnReseted()
self._reset_state()
def OnStarted2(self, time):
super(cbc_with_trend_confirmation_and_separate_stop_loss_strategy, self).OnStarted2(time)
self._reset_state()
ema = ExponentialMovingAverage()
ema.Length = self._slow_ema_length.Value
atr = AverageTrueRange()
atr.Length = self._atr_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(ema, atr, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def _process_candle(self, candle, ema_value, atr_value):
if candle.State != CandleStates.Finished:
return
# Session VWAP restarts every UTC day.
day = candle.OpenTime.Date
if day != self._vwap_day:
self._vwap_day = day
self._vwap_price_volume = Decimal(0)
self._vwap_volume = Decimal(0)
typical = (candle.HighPrice + candle.LowPrice + candle.ClosePrice) / Decimal(3)
self._vwap_price_volume += typical * candle.TotalVolume
self._vwap_volume += candle.TotalVolume
prev = self._prev_candle
self._prev_candle = candle
if prev is None:
return
previous_state = self._cbc_bullish
if candle.ClosePrice > prev.HighPrice:
self._cbc_bullish = True
elif candle.ClosePrice < prev.LowPrice:
self._cbc_bullish = False
if not ema_value.IsFormed or not atr_value.IsFormed or self._vwap_volume <= 0:
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
# Stop and target frozen at entry.
if self.Position > 0 and self._stop_price is not None and self._target_price is not None and (candle.LowPrice <= self._stop_price or candle.HighPrice >= self._target_price):
self.SellMarket(self.Position)
self._stop_price = None
self._target_price = None
return
if self.Position < 0 and self._stop_price is not None and self._target_price is not None and (candle.HighPrice >= self._stop_price or candle.LowPrice <= self._target_price):
self.BuyMarket(-self.Position)
self._stop_price = None
self._target_price = None
return
is_bullish = self._cbc_bullish
if previous_state is None or is_bullish is None or previous_state == is_bullish:
return
hour = candle.OpenTime.Hour
if hour < self._entry_start_hour.Value or hour >= self._entry_end_hour.Value:
return
ema = ema_value.GetValue[Decimal](None)
vwap = self._vwap_price_volume / self._vwap_volume
atr = atr_value.GetValue[Decimal](None)
close = candle.ClosePrice
strong_only = self._strong_flips_only.Value
multiplier = Decimal(self._profit_target_multiplier.Value)
if is_bullish and (not strong_only or close > candle.OpenPrice) and ema > vwap and self.Position <= 0 and prev.LowPrice < close:
self.BuyMarket(self.Volume + abs(self.Position))
self._stop_price = prev.LowPrice
self._target_price = close + atr * multiplier
elif not is_bullish and (not strong_only or close < candle.OpenPrice) and ema < vwap and self.Position >= 0 and prev.HighPrice > close:
self.SellMarket(self.Volume + abs(self.Position))
self._stop_price = prev.HighPrice
self._target_price = close - atr * multiplier
def CreateClone(self):
return cbc_with_trend_confirmation_and_separate_stop_loss_strategy()