Оригинальный советник MetaTrader использует индикатор «VIDYA N Bars Borders» и модуль мартингейла для наращивания объёма после убыточных сделок. В версии для StockSharp сохраняется базовая идея: покупки выполняются при уходе цены ниже адаптивной нижней границы канала, продажи — при выходе выше верхней. Центральная линия формируется адаптивным скользящим средним (аналогом VIDYA), ширина коридора задаётся индикатором ATR. Блок управления капиталом увеличивает размер следующей позиции после убытка и контролирует лимиты по объёму.
Логика работы
Подписка на свечи выбранного таймфрейма.
Расчёт KaufmanAdaptiveMovingAverage в качестве замены VIDYA и построение ATR-оболочки вокруг него.
Если закрытие завершённой свечи падает ниже нижней границы, стратегия открывает/разворачивает длинную позицию (или короткую, если активирован флаг Reverse).
Если закрытие поднимается выше верхней границы, открывается/разворачивается короткая позиция (или длинная при Reverse = true).
Между подряд идущими входами соблюдается минимальная ценовая дистанция, чтобы исключить повторный вход «в ту же точку».
При достижении заданной денежной цели по плавающей прибыли позиция полностью закрывается.
После закрытия сделки объём следующего ордера либо возвращается к базовому значению (если результат положительный), либо умножается на коэффициент мартингейла. Объём дополнительно выравнивается по шагу инструмента и ограничивается максимальными значениями.
Параметры
Имя
Описание
Candle Type
Тип свечей для торговли.
CMO Period
Окно расчёта коэффициента эффективности для адаптивного среднего.
EMA Period
Период сглаживания адаптивного среднего.
ATR Period
Количество баров для половины ширины канала ATR.
Profit Target
Денежная цель, при достижении которой позиция закрывается.
Increase Ratio
Множитель объёма после убыточной сделки.
Max Position Volume
Жёсткий предел объёма одной позиции.
Max Total Volume
Предел суммарной экспозиции стратегии.
Max Positions
Максимум одновременных позиций (в порте ведётся одна нетто-позиция).
Minimum Step
Минимальная дистанция между входами в пунктах.
Base Volume
Исходный объём ордера до применения мартингейла.
Reverse Signals
Инверсия сигналов на покупку/продажу.
Особенности реализации
В библиотеке StockSharp отсутствует прямой индикатор VIDYA. Используется KaufmanAdaptiveMovingAverage с настраиваемыми окнами эффективности и сглаживания, что позволяет приблизить адаптивное поведение оригинала без стороннего кода.
Управляется только одна нетто-позиция. В MetaTrader можно было ставить очередь ордеров; в StockSharp каждый сигнал открывает новую позицию или разворачивает текущую. Мартингейл применяется к размеру следующей сделки.
Минимальный шаг и приведение объёма опираются на свойства инструмента (PriceStep, VolumeStep, MinVolume, MaxVolume). Убедитесь, что эти параметры заданы при запуске стратегии.
Контроль прибыли выполняется по показателю PnL стратегии и последнему закрытию свечи. Для реальной торговли требуется подключить стратегию к портфелю, который предоставляет фактическую реализацию PnL.
Файлы
CS/VidyaNBarsBordersMartingaleStrategy.cs — реализация стратегии на C#.
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>
/// Simplified from "VIDYA N Bars Borders Martingale" MetaTrader expert.
/// Uses EMA as adaptive MA proxy and a range-based channel from recent N bars.
/// Buys when price closes below lower band, sells when above upper band.
/// Includes simple martingale volume increase on losing trades.
/// </summary>
public class VidyaNBarsBordersMartingaleStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _emaPeriod;
private readonly StrategyParam<int> _rangePeriod;
private readonly StrategyParam<decimal> _martingaleMultiplier;
private ExponentialMovingAverage _ema;
private readonly Queue<decimal> _highHistory = new();
private readonly Queue<decimal> _lowHistory = new();
private decimal _currentVolume;
private decimal _entryPrice;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int EmaPeriod
{
get => _emaPeriod.Value;
set => _emaPeriod.Value = value;
}
public int RangePeriod
{
get => _rangePeriod.Value;
set => _rangePeriod.Value = value;
}
public decimal MartingaleMultiplier
{
get => _martingaleMultiplier.Value;
set => _martingaleMultiplier.Value = value;
}
public VidyaNBarsBordersMartingaleStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
.SetDisplay("Candle Type", "Trading candle type", "General");
_emaPeriod = Param(nameof(EmaPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("EMA Period", "Smoothing period for adaptive MA proxy", "Indicators");
_rangePeriod = Param(nameof(RangePeriod), 10)
.SetGreaterThanZero()
.SetDisplay("Range Period", "Number of bars for high/low range channel", "Indicators");
_martingaleMultiplier = Param(nameof(MartingaleMultiplier), 1.25m)
.SetDisplay("Martingale Multiplier", "Volume multiplier after losing trade", "Risk");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ema = new ExponentialMovingAverage { Length = EmaPeriod };
_highHistory.Clear();
_lowHistory.Clear();
_currentVolume = Volume > 0 ? Volume : 1;
_entryPrice = 0;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_ema, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal emaValue)
{
if (candle.State != CandleStates.Finished)
return;
// Track high/low for range calculation
_highHistory.Enqueue(candle.HighPrice);
_lowHistory.Enqueue(candle.LowPrice);
if (_highHistory.Count > RangePeriod)
{
_highHistory.Dequeue();
_lowHistory.Dequeue();
}
if (!_ema.IsFormed || _highHistory.Count < RangePeriod)
return;
// Compute range from recent bars
decimal highest = decimal.MinValue;
decimal lowest = decimal.MaxValue;
var highs = _highHistory.ToArray();
var lows = _lowHistory.ToArray();
foreach (var h in highs)
if (h > highest) highest = h;
foreach (var l in lows)
if (l < lowest) lowest = l;
var range = (highest - lowest) * 0.75m;
if (range <= 0)
return;
var upper = emaValue + range;
var lower = emaValue - range;
var close = candle.ClosePrice;
var baseVolume = Volume > 0 ? Volume : 1;
var maxVolume = baseVolume * 8;
var nextVolume = _currentVolume;
if (close < lower)
{
// Price below lower band -> buy signal
if (Position < 0)
{
var wasLoss = close > _entryPrice;
nextVolume = wasLoss
? Math.Min(_currentVolume * MartingaleMultiplier, maxVolume)
: baseVolume;
}
if (Position <= 0)
{
BuyMarket(Position < 0 ? Math.Abs(Position) + nextVolume : nextVolume);
_currentVolume = nextVolume;
_entryPrice = close;
}
}
else if (close > upper)
{
// Price above upper band -> sell signal
if (Position > 0)
{
var wasLoss = close < _entryPrice;
nextVolume = wasLoss
? Math.Min(_currentVolume * MartingaleMultiplier, maxVolume)
: baseVolume;
}
if (Position >= 0)
{
SellMarket(Position > 0 ? Math.Abs(Position) + nextVolume : nextVolume);
_currentVolume = nextVolume;
_entryPrice = close;
}
}
}
/// <inheritdoc />
protected override void OnReseted()
{
_ema = null;
_highHistory.Clear();
_lowHistory.Clear();
_currentVolume = 0;
_entryPrice = 0;
base.OnReseted();
}
}
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 vidya_n_bars_borders_martingale_strategy(Strategy):
def __init__(self):
super(vidya_n_bars_borders_martingale_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(60)))
self._ema_period = self.Param("EmaPeriod", 20)
self._range_period = self.Param("RangePeriod", 10)
self._high_history = []
self._low_history = []
self._entry_price = 0.0
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def EmaPeriod(self):
return self._ema_period.Value
@EmaPeriod.setter
def EmaPeriod(self, value):
self._ema_period.Value = value
@property
def RangePeriod(self):
return self._range_period.Value
@RangePeriod.setter
def RangePeriod(self, value):
self._range_period.Value = value
def OnReseted(self):
super(vidya_n_bars_borders_martingale_strategy, self).OnReseted()
self._high_history = []
self._low_history = []
self._entry_price = 0.0
def OnStarted2(self, time):
super(vidya_n_bars_borders_martingale_strategy, self).OnStarted2(time)
self._high_history = []
self._low_history = []
self._entry_price = 0.0
ema = ExponentialMovingAverage()
ema.Length = self.EmaPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ema, self._process_candle).Start()
def _process_candle(self, candle, ema_value):
if candle.State != CandleStates.Finished:
return
ema_val = float(ema_value)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
rng_period = self.RangePeriod
self._high_history.append(high)
self._low_history.append(low)
if len(self._high_history) > rng_period:
self._high_history.pop(0)
self._low_history.pop(0)
if len(self._high_history) < rng_period:
return
highest = max(self._high_history)
lowest = min(self._low_history)
rng = (highest - lowest) * 0.75
if rng <= 0:
return
upper = ema_val + rng
lower = ema_val - rng
if close < lower and self.Position <= 0:
self.BuyMarket()
self._entry_price = close
elif close > upper and self.Position >= 0:
self.SellMarket()
self._entry_price = close
def CreateClone(self):
return vidya_n_bars_borders_martingale_strategy()