Backbone — перенос эксперта MetaTrader 4 "Backbone.mq4" на высокоуровневый API StockSharp. Стратегия сначала накапливает экстремальные значения бид/аск, чтобы определить исходное направление, затем последовательно формирует корзину ордеров в выбранную сторону. На каждой завершённой свече допускается не более одной новой заявки. Корзина закрывается достижением лимита MaxTrades или срабатыванием защитных ордеров. Расчёт объёма основан на доле риска от капитала и расстоянии до стоп-лосса, как в оригинальном роботе.
Поток данных
Свечи (CandleType) — решения принимаются только после закрытия свечи, полностью повторяя условие MT4 Bars > PrevBars.
Стакан котировок — лучшие цены покупателя/продавца используются для поиска начальных экстремумов и обновления трейлинг-стопа.
Состояние стратегии — базовый класс Strategy предоставляет текущую позицию, среднюю цену входа и PnL, что позволяет управлять защитными ордерами без дополнительных структур.
Логика торговли
Инициализация — пока направление не определено, фиксируется максимальный бид и минимальный аск. При откате от экстремума на TrailingStopPoints * PriceStep выбирается первоначальная сторона.
Очередность ордеров:
Если последний совершённый трейд был коротким (_lastPositionDirection == -1) и позиция отсутствует, выставляется рыночная покупка.
Если предыдущий трейд был длинным (_lastPositionDirection == 1) и число сделок меньше MaxTrades, на следующих свечах добавляются новые покупки.
Для продаж действует симметричное правило.
Размер позиции — каждая заявка использует формулу Vol() из MT4: берётся доступный капитал (CurrentValue → CurrentBalance → BeginBalance), умножается на MaxRisk и делится на стоимость стоп-лосса в деньгах (PriceStepCost * количество шагов). Объём приводится к VolumeStep, ограничивается MinVolume/MaxVolume и отклоняется, если после округления меньше минимального шага.
Защитные ордера — после исполнения заявки размещаются единые стоп-лосс и тейк-профит на всю корзину. Дистанции задаются в пунктах (шаги цены), как и в исходном коде.
Трейлинг-стоп — при положительных StopLossPoints и TrailingStopPoints стоп переносится всякий раз, когда цена уходит дальше заданного смещения от средней цены входа. Для длинных позиций опорной величиной служит лучший бид, для коротких — лучший аск.
Завершение корзины — при срабатывании стопа или тейк-профита счётчики сбрасываются, однако _lastPositionDirection сохраняет направление последней сделки, поэтому следующая свеча откроет корзину в противоположную сторону, как в MQL-версии.
Управление рисками
Используется оригинальная формула 1 / (MaxTrades / MaxRisk - openTrades).
Капитал для риска берётся из Portfolio.CurrentValue, затем CurrentBalance, затем BeginBalance.
Если после учёта VolumeStep объём меньше MinVolume, сделка не отправляется.
При изменении позиции стоп/тейк-профит регистрируются заново, чтобы всегда покрывать полный объём.
Параметры
Параметр
Значение по умолчанию
Описание
CandleType
15 минут
Таймфрейм, по закрытию которого принимаются решения.
MaxRisk
0.5
Доля капитала, используемая для расчёта нового объёма. Значение должно быть > 0.
MaxTrades
10
Максимальное число сделок в одной корзине.
TakeProfitPoints
170
Дистанция до тейк-профита в шагах цены. 0 отключает тейк-профит.
StopLossPoints
40
Дистанция до стоп-лосса в шагах цены. Обязательна для расчёта объёма и трейлинга.
TrailingStopPoints
300
Дистанция трейлинг-стопа в шагах цены. При 0 стоп остаётся статичным.
Особенности конверсии
В MT4 стопы/тейки привязывались к каждой заявке отдельно. В StockSharp используется один агрегированный защитный ордер на весь нетто-объём, что соответствует модели позиций платформы.
Размер позиции зависит от Security.PriceStepCost. Если поставщик данных не предоставляет стоимость шага, стратегия использует значение Volume как запасной вариант.
Трейлинг обновляется лишь на закрытии свечей, повторяя одноразовую обработку бара в исходном коде.
_lastPositionDirection сохраняет направление последнего трейда, обеспечивая чередование длинных и коротких корзин после закрытия.
В каталоге присутствует только C#-реализация; версия на Python не создавалась.
Рекомендации
Выбирайте инструменты с корректно заполненными PriceStep, PriceStepCost, VolumeStep, иначе расчёт объёма может быть некорректным.
При тестировании включайте поток стакана, чтобы механизм трейлинг-стопа получал актуальные бид/аск.
Для снижения агрессивности набора позиций уменьшите MaxRisk или увеличьте MaxTrades, что уменьшит расчётный объём Vol().
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Backbone strategy that alternates between long and short based on retracements from recent extremes.
/// Uses ATR to detect when price has pulled back enough from its high/low to enter a new position.
/// </summary>
public class BackboneBasketStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _retraceMultiplier;
private decimal _highestPrice;
private decimal _lowestPrice;
private decimal _entryPrice;
private int _lastDirection; // 1 = long, -1 = short, 0 = none
public BackboneBasketStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for analysis.", "General");
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetDisplay("ATR Period", "Period for ATR indicator.", "Indicators");
_retraceMultiplier = Param(nameof(RetraceMultiplier), 2m)
.SetDisplay("Retrace Multiplier", "ATR multiplier for retracement threshold.", "Signals");
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int AtrPeriod
{
get => _atrPeriod.Value;
set => _atrPeriod.Value = value;
}
public decimal RetraceMultiplier
{
get => _retraceMultiplier.Value;
set => _retraceMultiplier.Value = value;
}
/// <inheritdoc />
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highestPrice = 0;
_lowestPrice = decimal.MaxValue;
_entryPrice = 0;
_lastDirection = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_highestPrice = 0;
_lowestPrice = decimal.MaxValue;
_entryPrice = 0;
_lastDirection = 0;
var atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, atr);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (atrValue <= 0)
return;
var close = candle.ClosePrice;
// Track extremes
if (close > _highestPrice)
_highestPrice = close;
if (close < _lowestPrice)
_lowestPrice = close;
var threshold = atrValue * RetraceMultiplier;
// Exit existing positions on retracement
if (Position > 0 && close < _highestPrice - threshold)
{
SellMarket();
_entryPrice = 0;
_lastDirection = 1;
_lowestPrice = close;
}
else if (Position < 0 && close > _lowestPrice + threshold)
{
BuyMarket();
_entryPrice = 0;
_lastDirection = -1;
_highestPrice = close;
}
// Entry logic - alternate direction
if (Position == 0)
{
if (_lastDirection != 1 && close < _highestPrice - threshold)
{
// Price pulled back from high - sell
SellMarket();
_entryPrice = close;
_lastDirection = -1;
_lowestPrice = close;
}
else if (_lastDirection != -1 && close > _lowestPrice + threshold)
{
// Price bounced from low - buy
BuyMarket();
_entryPrice = close;
_lastDirection = 1;
_highestPrice = close;
}
}
}
}
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 AverageTrueRange
from StockSharp.Algo.Strategies import Strategy
class backbone_basket_strategy(Strategy):
def __init__(self):
super(backbone_basket_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Timeframe used for analysis.", "General")
self._atr_period = self.Param("AtrPeriod", 14) \
.SetDisplay("ATR Period", "Period for ATR indicator.", "Indicators")
self._retrace_multiplier = self.Param("RetraceMultiplier", 2.0) \
.SetDisplay("Retrace Multiplier", "ATR multiplier for retracement threshold.", "Signals")
self._highest_price = 0.0
self._lowest_price = float('inf')
self._entry_price = 0.0
self._last_direction = 0
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
@property
def atr_period(self):
return self._atr_period.Value
@atr_period.setter
def atr_period(self, value):
self._atr_period.Value = value
@property
def retrace_multiplier(self):
return self._retrace_multiplier.Value
@retrace_multiplier.setter
def retrace_multiplier(self, value):
self._retrace_multiplier.Value = value
def OnReseted(self):
super(backbone_basket_strategy, self).OnReseted()
self._highest_price = 0.0
self._lowest_price = float('inf')
self._entry_price = 0.0
self._last_direction = 0
def OnStarted2(self, time):
super(backbone_basket_strategy, self).OnStarted2(time)
self._highest_price = 0.0
self._lowest_price = float('inf')
self._entry_price = 0.0
self._last_direction = 0
atr = AverageTrueRange()
atr.Length = self.atr_period
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(atr, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, atr)
self.DrawOwnTrades(area)
def OnProcess(self, candle, atr_value):
if candle.State != CandleStates.Finished:
return
if atr_value <= 0:
return
close = float(candle.ClosePrice)
if close > self._highest_price:
self._highest_price = close
if close < self._lowest_price:
self._lowest_price = close
threshold = float(atr_value) * self.retrace_multiplier
# Exit existing positions on retracement
if self.Position > 0 and close < self._highest_price - threshold:
self.SellMarket()
self._entry_price = 0.0
self._last_direction = 1
self._lowest_price = close
elif self.Position < 0 and close > self._lowest_price + threshold:
self.BuyMarket()
self._entry_price = 0.0
self._last_direction = -1
self._highest_price = close
# Entry logic - alternate direction
if self.Position == 0:
if self._last_direction != 1 and close < self._highest_price - threshold:
self.SellMarket()
self._entry_price = close
self._last_direction = -1
self._lowest_price = close
elif self._last_direction != -1 and close > self._lowest_price + threshold:
self.BuyMarket()
self._entry_price = close
self._last_direction = 1
self._highest_price = close
def CreateClone(self):
return backbone_basket_strategy()