Перенос советника MetaTrader 5 Spasm (barabashkakvn's edition) на высокоуровневый API StockSharp.
Отслеживает адаптивный ценовой канал, построенный на основе текущей волатильности, и переключается между бычьим и медвежьим режимами.
Работает на любых инструментах и таймфреймах, задаваемых параметром CandleType (по умолчанию — часовые свечи).
Подготовка данных
Подписка на поток свечей, определяемых параметром CandleType, для выбранного инструмента.
Построение оценки волатильности по последним VolatilityPeriod свечам:
При отключенном UseWeightedVolatility используется простое скользящее среднее диапазона свечей.
При включенном UseWeightedVolatility применяется линейно-взвешенное среднее, усиливающее влияние свежих наблюдений.
Источник диапазона — High - Low. Если UseOpenCloseRange включён, вместо этого берётся абсолютная разница между ценами открытия и закрытия, как в оригинальном эксперте.
Среднее значение диапазона переводится в количество минимальных шагов цены, умножается на VolatilityMultiplier, результат округляется вниз до целого числа шагов и снова умножается на шаг цены — так получается порог пробоя.
В течение первых VolatilityPeriod * 3 завершённых свечей стратегия собирает последние экстремумы и их метки времени, чтобы определить, какая волна была сформирована позже, и зафиксировать начальное направление тренда.
Количество свечей для расчёта волатильности и стартового анализа экстремумов.
UseWeightedVolatility
false
Переключает среднее волатильности с простого на линейно-взвешенное.
UseOpenCloseRange
false
Использует абсолютное изменение между открытием и закрытием вместо диапазона High-Low.
StopLossFraction
0.5
Доля волатильностного порога, применяемая для расчёта дистанции стоп-лосса (не менее трёх шагов цены).
CandleType
часовой таймфрейм
Тип и таймфрейм свечей, которые используются в расчётах.
Логика торговли
Отслеживание тренда
Переменные _highestPrice и _lowestPrice хранят актуальные экстремумы текущего движения.
Если цена поднимается выше _highestPrice + threshold, максимум обновляется до текущего значения High; аналогично при падении ниже _lowestPrice - threshold минимум обновляется до текущего Low.
Флаг _isTrendUp показывает активный режим: true — бычий, false — медвежий.
Правила входа
Когда _isTrendUp == false и закрытие свечи превышает _lowestPrice + threshold, стратегия переходит в бычий режим и отправляет BuyMarket(Volume + Math.Abs(Position)), закрывая короткие позиции и открывая длинную на заданный объём.
Когда _isTrendUp == true и закрытие падает ниже _highestPrice - threshold, режим меняется на медвежий, вызывается SellMarket(Volume + Math.Abs(Position)), что переворачивает позицию в шорт.
Управление стоп-лоссом
Для длинной позиции стоп рассчитывается как entry - max(threshold * StopLossFraction, 3 * priceStep).
Для короткой позиции стоп равен entry + max(threshold * StopLossFraction, 3 * priceStep).
Если минимум свечи достигает длинного стопа или максимум — короткого, позиция закрывается рыночной заявкой. При StopLossFraction = 0 защитный стоп не используется.
Контроль рисков и инфраструктура
В методе OnStarted вызывается StartProtection(), чтобы активировать встроенную защиту позиции.
Обработка ведётся только по завершённым свечам, что исключает шум внутри бара и повторяет логику исходного советника.
Комментарии и имена параметров оставлены на английском языке согласно требованиям проекта.
Отличия от версии MQL
В оригинале перерасчёт порога выполнялся на каждом тике; в портированной версии расчёт производится на закрытых свечах, поскольку высокоуровневый API работает со свечными подписками.
Исполнение стоп-лоссов контролируется по данным свечей, поэтому внутрисвечные проколы оцениваются на границе бара.
В StockSharp нет прямых аналогов свойств символа MetaTrader (спред, минимальный стоп). При слишком малой дистанции стопа используется минимум в три шага цены, что повторяет защитный механизм MT5-реализации.
Рекомендации по использованию
Убедитесь, что у инструмента задан корректный PriceStep; при отсутствии значения стратегия принимает шаг, равный 1.
Подходит для спот-рынка, фьючерсов и CFD при условии наличия свечей выбранного таймфрейма.
Целей по прибыли не предусмотрено — выход осуществляется только при смене режима или срабатывании стоп-лосса.
namespace StockSharp.Samples.Strategies;
using System;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;
/// <summary>
/// Volatility breakout strategy converted from the MetaTrader Spasm expert advisor.
/// Tracks directional swings using adaptive thresholds derived from ATR.
/// Buys when price breaks above recent high + ATR*multiplier, sells when price breaks below recent low - ATR*multiplier.
/// </summary>
public class SpasmStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _volatilityMultiplier;
private decimal _highestPrice;
private decimal _lowestPrice;
private decimal _prevRange;
private bool _initialized;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public decimal VolatilityMultiplier
{
get => _volatilityMultiplier.Value;
set => _volatilityMultiplier.Value = value;
}
public SpasmStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for analysis", "General");
_volatilityMultiplier = Param(nameof(VolatilityMultiplier), 2m)
.SetGreaterThanZero()
.SetDisplay("Volatility Multiplier", "Multiplier applied to ATR for breakout bands", "Trading");
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_highestPrice = 0m;
_lowestPrice = decimal.MaxValue;
_prevRange = 0m;
_initialized = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_highestPrice = 0m;
_lowestPrice = decimal.MaxValue;
_prevRange = 0m;
_initialized = false;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(OnProcess)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void OnProcess(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (!_initialized)
{
_highestPrice = candle.HighPrice;
_lowestPrice = candle.LowPrice;
_prevRange = candle.HighPrice - candle.LowPrice;
_initialized = true;
return;
}
// Update extremes
if (candle.HighPrice > _highestPrice)
_highestPrice = candle.HighPrice;
if (candle.LowPrice < _lowestPrice)
_lowestPrice = candle.LowPrice;
var threshold = _prevRange * VolatilityMultiplier;
if (threshold <= 0)
return;
// Breakout above lowest + threshold => buy
if (candle.ClosePrice > _lowestPrice + threshold && Position <= 0)
{
BuyMarket();
// Reset extremes after entry
_highestPrice = candle.HighPrice;
_lowestPrice = candle.LowPrice;
}
// Breakout below highest - threshold => sell
else if (candle.ClosePrice < _highestPrice - threshold && Position >= 0)
{
SellMarket();
// Reset extremes after entry
_highestPrice = candle.HighPrice;
_lowestPrice = candle.LowPrice;
}
_prevRange = candle.HighPrice - candle.LowPrice;
}
}
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.Strategies import Strategy
class spasm_strategy(Strategy):
def __init__(self):
super(spasm_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe for analysis", "General")
self._volatility_multiplier = self.Param("VolatilityMultiplier", 2.0) \
.SetDisplay("Volatility Multiplier", "Multiplier applied to ATR for breakout bands", "Trading")
self._highest_price = 0.0
self._lowest_price = float('inf')
self._prev_range = 0.0
self._initialized = False
@property
def CandleType(self):
return self._candle_type.Value
@property
def VolatilityMultiplier(self):
return self._volatility_multiplier.Value
def OnReseted(self):
super(spasm_strategy, self).OnReseted()
self._highest_price = 0.0
self._lowest_price = float('inf')
self._prev_range = 0.0
self._initialized = False
def OnStarted2(self, time):
super(spasm_strategy, self).OnStarted2(time)
self._highest_price = 0.0
self._lowest_price = float('inf')
self._prev_range = 0.0
self._initialized = False
subscription = self.SubscribeCandles(self.CandleType)
subscription \
.Bind(self._on_process) \
.Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _on_process(self, candle):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if not self._initialized:
self._highest_price = high
self._lowest_price = low
self._prev_range = high - low
self._initialized = True
return
if high > self._highest_price:
self._highest_price = high
if low < self._lowest_price:
self._lowest_price = low
threshold = self._prev_range * float(self.VolatilityMultiplier)
if threshold <= 0:
self._prev_range = high - low
return
if close > self._lowest_price + threshold and self.Position <= 0:
self.BuyMarket()
self._highest_price = high
self._lowest_price = low
elif close < self._highest_price - threshold and self.Position >= 0:
self.SellMarket()
self._highest_price = high
self._lowest_price = low
self._prev_range = high - low
def CreateClone(self):
return spasm_strategy()