Открыть на GitHub

VIDYA N Bars Borders Martingale

Обзор

Оригинальный советник MetaTrader использует индикатор «VIDYA N Bars Borders» и модуль мартингейла для наращивания объёма после убыточных сделок. В версии для StockSharp сохраняется базовая идея: покупки выполняются при уходе цены ниже адаптивной нижней границы канала, продажи — при выходе выше верхней. Центральная линия формируется адаптивным скользящим средним (аналогом VIDYA), ширина коридора задаётся индикатором ATR. Блок управления капиталом увеличивает размер следующей позиции после убытка и контролирует лимиты по объёму.

Логика работы

  1. Подписка на свечи выбранного таймфрейма.
  2. Расчёт KaufmanAdaptiveMovingAverage в качестве замены VIDYA и построение ATR-оболочки вокруг него.
  3. Если закрытие завершённой свечи падает ниже нижней границы, стратегия открывает/разворачивает длинную позицию (или короткую, если активирован флаг Reverse).
  4. Если закрытие поднимается выше верхней границы, открывается/разворачивается короткая позиция (или длинная при Reverse = true).
  5. Между подряд идущими входами соблюдается минимальная ценовая дистанция, чтобы исключить повторный вход «в ту же точку».
  6. При достижении заданной денежной цели по плавающей прибыли позиция полностью закрывается.
  7. После закрытия сделки объём следующего ордера либо возвращается к базовому значению (если результат положительный), либо умножается на коэффициент мартингейла. Объём дополнительно выравнивается по шагу инструмента и ограничивается максимальными значениями.

Параметры

Имя Описание
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();
	}
}