BladeRunner — перевод советника MetaTrader, который объединяет пробой фракталов с фильтрами тренда и импульса. В версии для StockSharp сохранена многофреймовая структура оригинала: базовая серия свечей управляет сделками, серия старшего таймфрейма проверяет импульс, а самая медленная серия формирует фильтр MACD. Сделки открываются рыночными приказами с масштабированием позиции и автоматической установкой стоп-лосса и тейк-профита в шагах цены.
Логика торговли
Фрактальный фильтр. Алгоритм отслеживает завершённые свечи на наличие фракталов Билла Вильямса. Бычий фрактал принимается, если свеча два бара назад обновила максимум, а подтверждающая свеча открылась ниже уровня фрактала и ниже 20-периодной LWMA типичной цены. Для медвежьих сигналов условия зеркальные.
Трендовый фильтр. Быстрая и медленная LWMA на основной серии свечей определяют направление тенденции. Для покупок быстая LWMA должна находиться выше медленной, для продаж — ниже.
Импульсный фильтр. Осциллятор Momentum на старшем таймфрейме должен отклониться от уровня 100 минимум на заданный порог в одном из трёх последних значений. Так воспроизводится проверка Momentum из MQL-версии.
MACD-фильтр. MACD на медленной серии свечей требует, чтобы основная линия была выше (для лонга) или ниже (для шорта) сигнальной линии, что соответствует месячному фильтру оригинала.
Пробой уровня. Закрытие последней основной свечи должно преодолеть запомненный уровень фрактала до открытия позиции.
Когда все фильтры совпадают, стратегия открывает позицию заданным объёмом. Текущая противоположная позиция закрывается перед разворотом. Дополнительные входы разрешены до достижения максимального числа масштабирований.
Реализация
Используются три подписки на свечи через high-level API. Каждая подписка сразу связывается с нужными индикаторами без добавления в коллекцию стратегии.
Все LWMA рассчитываются по типичной цене (HLC/3), чтобы воспроизвести логику MQL. MACD также использует типичную цену.
Для обнаружения фракталов хранится скользящее окно завершённых свечей и соответствующих значений фильтра. Запоминается только последнее валидированное направление фрактала, что исключает повторные сигналы по той же формации.
История Momentum хранится в фиксированном массиве из трёх значений, без динамических выделений памяти.
Перед размещением ордера объём корректируется под шаг, минимум и максимум инструмента.
Вызов StartProtection автоматически навешивает стоп-лосс и тейк-профит в шагах цены, что соответствует фиксированным пунктам из MetaTrader.
Параметры
Параметр
Описание
Значение по умолчанию
CandleType
Основная серия свечей для генерации сигналов.
Свечи 15 минут
MomentumCandleType
Старший таймфрейм для фильтра Momentum.
Свечи 1 час
MacdCandleType
Серия свечей для MACD-фильтра.
Дневные свечи
FastMaPeriod
Период быстрой LWMA.
6
SlowMaPeriod
Период медленной LWMA.
85
FilterMaPeriod
LWMA, используемая при проверке фрактала.
20
MomentumPeriod
Период расчёта индикатора Momentum.
14
MomentumThreshold
Минимальное отклонение Momentum от 100.
0.3
FractalLookback
Размер окна для поиска фракталов.
200
MaxTrades
Максимум доливок в одном направлении.
3
OrderVolume
Базовый объём рыночного ордера.
1 контракт
TakeProfitSteps
Тейк-профит в шагах цены.
50
StopLossSteps
Стоп-лосс в шагах цены.
20
Управление рисками
StartProtection автоматически выставляет стоп-лосс и тейк-профит для каждой позиции.
Перед открытием новой сделки стратегия закрывает противоположную позицию, исключая хеджирование.
Ограничение MaxTrades удерживает суммарное количество доливок, а проверка шага объёма не позволяет нарушить требования биржи.
Отличия от оригинального советника
Функции остановки по балансу, трейлинг-стоп и перенос в безубыток из MQL не перенесены. Их можно реализовать отдельными компонентами StockSharp.
Денежный трейлинг и пуш-уведомления исключены, поскольку платформа поддерживает собственные уведомления.
По умолчанию MACD рассчитывается на дневных свечах. При необходимости можно переключить MacdCandleType на месячный таймфрейм, если он доступен у поставщика данных.
Валидация фракталов опирается на последнее подтверждающее значение в окне, что повторяет эффект оригинального цикла без трудоёмкого пересчёта.
Рекомендации по использованию
Настройте таймфреймы свечей в соответствии с доступными потоками данных.
Подберите OrderVolume, TakeProfitSteps и StopLossSteps с учётом шага цены и минимального объёма инструмента.
Оптимизируйте порог Momentum и периоды LWMA на исторических данных перед боевым запуском.
При необходимости включите отображение графиков, чтобы визуально контролировать расположение LWMA и подтверждение фракталов.
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;
public class BladeRunnerStrategy : 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;
public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }
public BladeRunnerStrategy()
{
_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
_slowPeriod = Param(nameof(SlowPeriod), 50).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");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
}
protected override void OnReseted()
{
base.OnReseted();
_fast = null; _slow = null;
_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
}
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;
if (Position > 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
}
else if (Position < 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
}
if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }
_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 blade_runner_strategy(Strategy):
def __init__(self):
super(blade_runner_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 14) \
.SetDisplay("Fast Period", "Fast MA period", "Indicator")
self._slow_period = self.Param("SlowPeriod", 50) \
.SetDisplay("Slow Period", "Slow MA 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(blade_runner_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(blade_runner_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
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 = 100
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 = 100
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 = 100
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 = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
return
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 = 100
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 = 100
self._prev_fast = fast_val
self._prev_slow = slow_val
def CreateClone(self):
return blade_runner_strategy()