Стратегия представляет собой перевод на C# эксперта MetaTrader Exp_i-KlPrice_Vol.mq5. Она воссоздаёт осциллятор KlPrice, измеряющий удалённость цены от волатильной полосы, умножает его на объём свечи и отслеживает цветовые переходы, формируемые адаптивными порогами. Для каждой стороны моделируются два независимых слота позиций, что соответствует работе оригинального советника с двумя magic-номерами.
Логика индикатора
Цена преобразуется в соответствии с выбранным режимом AppliedPrice (close, open, median, Demark и т.д.).
Преобразованная цена сглаживается средним по методу PriceMaMethod с периодом PriceMaLength.
Диапазон свечи (High - Low) сглаживается методом RangeMaMethod/RangeMaLength и служит шириной динамического канала.
Базовый осциллятор KlPrice рассчитывается по формуле 100 * (Price - (MA - RangeMA)) / (2 * RangeMA) - 50.
Осциллятор умножается на выбранный источник объёма (AppliedVolume.Tick или AppliedVolume.Real).
Юриковское сглаживание длиной SmoothingLength применяется как к осциллятору, так и к самому объёму, формируя две адаптивные серии.
Текущий цвет определяется сравнением сглаженного осциллятора с порогами:
4 – значение выше HighLevel2 * volume (сильное бычье давление).
3 – между верхним умеренным и экстремальным уровнями.
2 – зона нейтральности между порогами.
1 – между нижним порогом и нулевой линией.
0 – ниже LowLevel2 * volume (сильное медвежье давление).
Торговые правила
Анализируются цвета на свече с отступом SignalBar (обычно предыдущая завершённая свеча) и ещё на одну свечу старше.
Входы в лонг:
Слот 1 открывается, когда цвет меняется с 4 на значение ниже 4, и разрешён параметр AllowLongEntry.
Слот 2 открывается при переходе с 3 на значение ниже 3.
Входы в шорт:
Слот 1 открывается при переходе цвета с 0 на значение выше 0, если AllowShortEntry активен.
Слот 2 открывается при переходе с 1 на значение выше 1.
Закрытие длинных позиций происходит, когда предыдущий цвет был 0 или 1, и включён AllowLongExit.
Закрытие коротких позиций выполняется при предыдущих цветах 4 или 3 и активном AllowShortExit.
Каждый слот запоминает время последнего сигнала, чтобы избежать повторных заявок на той же свече. Если StopLossPoints или TakeProfitPoints больше нуля, для защиты вызывается StartProtection.
Параметры
Имя
Тип
Значение по умолчанию
Описание
PrimaryVolume
decimal
0.1
Объём заявок для первого слота.
SecondaryVolume
decimal
0.2
Объём заявок для второго слота.
StopLossPoints
int
1000
Расстояние защитного стопа в шагах цены.
TakeProfitPoints
int
2000
Расстояние тейк-профита в шагах цены.
AllowLongEntry
bool
true
Разрешение на открытие длинных позиций.
AllowShortEntry
bool
true
Разрешение на открытие коротких позиций.
AllowLongExit
bool
true
Закрытие длинных позиций при медвежьих цветах.
AllowShortExit
bool
true
Закрытие коротких позиций при бычьих цветах.
CandleType
DataType
H8
Таймфрейм свечей для расчётов.
PriceMaMethod
SmoothMethod
Sma
Тип сглаживания применённого значения цены.
PriceMaLength
int
100
Период сглаживания цены.
PriceMaPhase
int
15
Фазовый параметр для фильтров Юрика.
RangeMaMethod
SmoothMethod
Jjma
Тип сглаживания диапазона свечи.
RangeMaLength
int
20
Период сглаживания диапазона.
RangeMaPhase
int
100
Фазовый параметр диапазонного фильтра.
SmoothingLength
int
20
Длина Юриковского сглаживания осциллятора и объёма.
AppliedPrice
AppliedPrice
Close
Источник цены для вычислений осциллятора.
VolumeType
AppliedVolume
Tick
Источник объёма, умножаемого на осциллятор.
HighLevel2
int
150
Верхний экстремальный множитель.
HighLevel1
int
20
Верхний умеренный множитель.
LowLevel1
int
-20
Нижний умеренный множитель.
LowLevel2
int
-150
Нижний экстремальный множитель.
SignalBar
int
1
Отступ по истории для оценки цветов.
Рекомендации
Подключайте стратегию к инструментам, где доступны данные по цене и объёму; при отсутствии реального объёма используется тиковой счётчик.
Параметры PrimaryVolume и SecondaryVolume позволяют имитировать две независимые схемы управления капиталом оригинального советника.
Меняйте SignalBar, если требуется сместить анализ на более ранние свечи или при синхронизации истории.
Методы сглаживания поддерживают фильтры Jurik через отражение, что максимально приближает расчёты к библиотеке SmoothAlgorithms из MQL.
Защитные ордера активируются только при положительных значениях StopLossPoints или TakeProfitPoints; нулевые значения полностью отключают защиту.
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>
/// KlPrice-Vol strategy using EMA crossover with volume confirmation.
/// Buys on fast EMA crossing above slow EMA with rising volume.
/// Sells on reverse crossover.
/// </summary>
public class ExpIKlPriceVolStrategy : Strategy
{
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private ExponentialMovingAverage _fast;
private ExponentialMovingAverage _slow;
private decimal _prevFast;
private decimal _prevSlow;
private decimal _entryPrice;
private int _cooldown;
/// <summary>
/// Fast EMA period.
/// </summary>
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Slow EMA period.
/// </summary>
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// Stop-loss distance in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public ExpIKlPriceVolStrategy()
{
_fastPeriod = Param(nameof(FastPeriod), 50)
.SetGreaterThanZero()
.SetDisplay("Fast Period", "Fast EMA period", "Indicator");
_slowPeriod = Param(nameof(SlowPeriod), 200)
.SetGreaterThanZero()
.SetDisplay("Slow Period", "Slow EMA period", "Indicator");
_stopLossPoints = Param(nameof(StopLossPoints), 200)
.SetNotNegative()
.SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 400)
.SetNotNegative()
.SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fast = null;
_slow = null;
_prevFast = 0;
_prevSlow = 0;
_entryPrice = 0;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fast = new ExponentialMovingAverage { Length = FastPeriod };
_slow = new ExponentialMovingAverage { Length = SlowPeriod };
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.Bind(_fast, _slow, ProcessCandle);
subscription.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_fast.IsFormed || !_slow.IsFormed)
{
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (_cooldown > 0)
{
_cooldown--;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
var close = candle.ClosePrice;
var step = Security?.PriceStep ?? 1m;
// Check SL/TP
if (Position > 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 60;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 60;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
}
else if (Position < 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 60;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 60;
_prevFast = fastValue;
_prevSlow = slowValue;
return;
}
}
// EMA crossover
if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
_entryPrice = close;
_cooldown = 60;
}
else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
_entryPrice = close;
_cooldown = 60;
}
_prevFast = fastValue;
_prevSlow = slowValue;
}
}
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 ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class exp_i_kl_price_vol_strategy(Strategy):
def __init__(self):
super(exp_i_kl_price_vol_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 50) \
.SetDisplay("Fast Period", "Fast EMA period", "Indicator")
self._slow_period = self.Param("SlowPeriod", 200) \
.SetDisplay("Slow Period", "Slow EMA period", "Indicator")
self._stop_loss_points = self.Param("StopLossPoints", 200) \
.SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 400) \
.SetDisplay("Take Profit", "Take-profit in price steps", "Risk")
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
@property
def fast_period(self):
return self._fast_period.Value
@property
def slow_period(self):
return self._slow_period.Value
@property
def stop_loss_points(self):
return self._stop_loss_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
def OnReseted(self):
super(exp_i_kl_price_vol_strategy, self).OnReseted()
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(exp_i_kl_price_vol_strategy, self).OnStarted2(time)
self._fast = ExponentialMovingAverage()
self._fast.Length = self.fast_period
self._slow = ExponentialMovingAverage()
self._slow.Length = self.slow_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(5)))
subscription.Bind(self._fast, self._slow, self._process_candle)
subscription.Start()
def _process_candle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
slow_val = float(slow_value)
if not self._fast.IsFormed or not self._slow.IsFormed:
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self._cooldown > 0:
self._cooldown -= 1
self._prev_fast = fast_val
self._prev_slow = slow_val
return
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
# Check SL/TP
if self.Position > 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close <= self._entry_price - self.stop_loss_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 60
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close >= self._entry_price + self.take_profit_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 60
self._prev_fast = fast_val
self._prev_slow = slow_val
return
elif self.Position < 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close >= self._entry_price + self.stop_loss_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 60
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close <= self._entry_price - self.take_profit_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 60
self._prev_fast = fast_val
self._prev_slow = slow_val
return
# EMA crossover
if self._prev_fast <= self._prev_slow and fast_val > slow_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._cooldown = 60
elif self._prev_fast >= self._prev_slow and fast_val < slow_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._cooldown = 60
self._prev_fast = fast_val
self._prev_slow = slow_val
def CreateClone(self):
return exp_i_kl_price_vol_strategy()