Открыть на GitHub

Стратегия BARS Alligator

Стратегия BARS Alligator является прямым портом одноимённого эксперта MetaTrader. Она использует индикатор Alligator Билла Уильямса для определения момента «пробуждения» тренда: пересечение зелёной линии Lips выше синей линии Jaw трактуется как зарождение бычьего импульса, а обратное пересечение сигнализирует о медвежьем движении. Выходы выполняются при пересечении Lips и красной линии Teeth, чтобы закрывать позицию при ослаблении импульса. Стоп-лосс, тейк-профит и трейлинг-стоп задаются в пунктах и автоматически переводятся в цену с учётом шага цены инструмента и числа знаков после запятой.

Логика торговли

  1. Формирование индикаторов
    • Три скользящих средних с настраиваемыми периодами, смещениями и типом сглаживания (простая, экспоненциальная, сглаженная или взвешенная) формируют линии Alligator.
    • В качестве источника цены может использоваться close, open, high, low, медианная, типичная или взвешенная цена свечи.
    • Чтобы сохранить поведение MetaTrader с положительным смещением, для каждой линии ведётся небольшой буфер значений, и сигналы рассчитываются по тем же точкам, что отображались бы на графике.
  2. Условия входа
    • Лонг: на предыдущей свече Lips находится выше Jaw, а на позапрошлой — ниже (бычий крест вверх).
    • Шорт: на предыдущей свече Lips ниже Jaw, а на позапрошлой — выше (медвежий крест вниз).
    • Новые заявки разрешены только при отсутствии позиции или при совпадении направления сигнала с текущей позицией. Суммарный объём не должен превышать MaxPositions × OrderVolume (или рассчитанный по риску эквивалент).
  3. Условия выхода
    • Лонг: Lips пересекает Teeth сверху вниз, и позиция остаётся прибыльной относительно средневзвешенной цены входа.
    • Шорт: Lips пересекает Teeth снизу вверх, и позиция находится в прибыли.
    • Дополнительно выполняется закрытие при срабатывании заданных стоп-лоссов и тейк-профитов.
  4. Трейлинг-стоп
    • При включении стоп переносится, как только прибыль превышает TrailingStopPips + TrailingStepPips. Новый стоп удерживается на расстоянии TrailingStopPips пунктов от текущей цены и продвигается дальше только при новом движении минимум на TrailingStepPips.
  5. Управление капиталом
    • В режиме FixedVolume сделки выставляются объёмом OrderVolume.
    • В режиме RiskPercent объём рассчитывается так, чтобы при срабатывании стоп-лосса потери составили MoneyValue процентов от стоимости портфеля. Риск на единицу равен стоп-дистанции в ценовых единицах; итог округляется вниз к ближайшему шагу объёма (или к 1, если шаг неизвестен).

Параметры

Параметр Тип Значение по умолчанию Описание
CandleType DataType TimeSpan.FromHours(1).TimeFrame() Таймфрейм, используемый для расчётов.
OrderVolume decimal 0.1 Фиксированный объём сделки в режиме FixedVolume.
MoneyMode MoneyManagementMode FixedVolume Выбор между фиксированным объёмом и расчётом по риску.
MoneyValue decimal 1 Процент риска при MoneyMode = RiskPercent; в фиксированном режиме игнорируется.
MaxPositions int 1 Максимальное число добавочных входов в одном направлении (в кратных рассчитанному объёму единицах).
StopLossPips int 150 Расстояние до стоп-лосса в пунктах. Ноль отключает стоп.
TakeProfitPips int 150 Расстояние до тейк-профита. Ноль отключает цель.
TrailingStopPips int 5 Дистанция трейлинг-стопа. Ноль отключает сопровождение.
TrailingStepPips int 5 Минимальный дополнительный ход цены перед переносом трейлинга; должен быть > 0 при активном трейлинге.
JawPeriod int 13 Период линии Jaw.
JawShift int 8 Сдвиг линии Jaw вперёд на указанное число свечей.
TeethPeriod int 8 Период линии Teeth.
TeethShift int 5 Сдвиг линии Teeth вперёд.
LipsPeriod int 5 Период линии Lips.
LipsShift int 3 Сдвиг линии Lips вперёд.
MaType MovingAverageType Smoothed Тип скользящей средней для всех линий Alligator.
AppliedPrice AppliedPriceType Median Тип цены, подаваемой на скользящие (close, open, high, low, median, typical, weighted).

Перевод пунктов в цену

Стратегия умножает значения в пунктах на PriceStep инструмента. Для инструментов с 3 или 5 знаками после запятой дистанция дополнительно умножается на 10, чтобы соответствовать определению пипса в MetaTrader. Если шаг цены неизвестен, используется значение 1.

Особенности реализации

  • В StockSharp используется неттинговый режим, поэтому MaxPositions ограничивает суммарный объём позиции: дополнительные входы изменяют среднюю цену, а не создают отдельные позиции.
  • Стоп-лосс и тейк-профит контролируются во внутреннем состоянии стратегии и исполняются рыночными заявками на первой свече, пересекающей уровень, что повторяет логику оригинального советника.
  • Риск-процентный режим требует ненулевой дистанции стоп-лосса; иначе стратегия возвращается к фиксированному объёму.
  • Индикаторы обновляются только на закрытых свечах (CandleStates.Finished), чтобы исключить преждевременные сигналы.
namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Candles;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>
/// Bill Williams Alligator strategy: trades on lips/jaw crossover and exits on lips/teeth crossover.
/// </summary>
public class BarsAlligatorStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _cooldownBars;
	private readonly StrategyParam<decimal> _stopLossPercent;
	private readonly StrategyParam<decimal> _takeProfitPercent;

	private readonly SmoothedMovingAverage _jaw = new() { Length = 13 };
	private readonly SmoothedMovingAverage _teeth = new() { Length = 8 };
	private readonly SmoothedMovingAverage _lips = new() { Length = 5 };

	private decimal _previousJaw;
	private decimal _previousTeeth;
	private decimal _previousLips;
	private bool _hasPrevious;
	private decimal? _entryPrice;
	private int _cooldownLeft;

	public BarsAlligatorStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame()).SetDisplay("Candle Type", "Timeframe", "General");
		_cooldownBars = Param(nameof(CooldownBars), 6).SetNotNegative().SetDisplay("Cooldown Bars", "Bars between completed trades", "Trading");
		_stopLossPercent = Param(nameof(StopLossPercent), 3m).SetDisplay("Stop Loss %", "Stop distance as percentage of entry price", "Risk");
		_takeProfitPercent = Param(nameof(TakeProfitPercent), 3m).SetDisplay("Take Profit %", "Take-profit distance as percentage of entry price", "Risk");
	}

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int CooldownBars { get => _cooldownBars.Value; set => _cooldownBars.Value = value; }
	public decimal StopLossPercent { get => _stopLossPercent.Value; set => _stopLossPercent.Value = value; }
	public decimal TakeProfitPercent { get => _takeProfitPercent.Value; set => _takeProfitPercent.Value = value; }

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_previousJaw = 0m;
		_previousTeeth = 0m;
		_previousLips = 0m;
		_hasPrevious = false;
		_entryPrice = null;
		_cooldownLeft = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		OnReseted();

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(_jaw, _teeth, _lips, ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal jaw, decimal teeth, decimal lips)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_cooldownLeft > 0)
			_cooldownLeft--;

		if (Position != 0 && _entryPrice is null)
			_entryPrice = candle.ClosePrice;

		if (TryExitByRisk(candle))
		{
			UpdatePrevious(jaw, teeth, lips);
			return;
		}

		if (!_hasPrevious)
		{
			UpdatePrevious(jaw, teeth, lips);
			return;
		}

		// Exit conditions: lips crosses teeth against position
		var closeLong = lips < teeth && _previousLips >= _previousTeeth && Position > 0;
		var closeShort = lips > teeth && _previousLips <= _previousTeeth && Position < 0;

		if (closeLong)
		{
			SellMarket(Position);
			_entryPrice = null;
			_cooldownLeft = CooldownBars;
			UpdatePrevious(jaw, teeth, lips);
			return;
		}

		if (closeShort)
		{
			BuyMarket(Math.Abs(Position));
			_entryPrice = null;
			_cooldownLeft = CooldownBars;
			UpdatePrevious(jaw, teeth, lips);
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading() || _cooldownLeft > 0)
		{
			UpdatePrevious(jaw, teeth, lips);
			return;
		}

		// Entry: lips crosses jaw with proper Alligator ordering
		var buySignal = lips > jaw && _previousLips <= _previousJaw && lips > teeth;
		var sellSignal = lips < jaw && _previousLips >= _previousJaw && lips < teeth;

		if (buySignal && Position <= 0)
		{
			if (Position < 0)
			{
				BuyMarket(Math.Abs(Position));
				_entryPrice = null;
				_cooldownLeft = CooldownBars;
			}
			else
			{
				BuyMarket();
				_entryPrice = candle.ClosePrice;
				_cooldownLeft = CooldownBars;
			}
		}
		else if (sellSignal && Position >= 0)
		{
			if (Position > 0)
			{
				SellMarket(Position);
				_entryPrice = null;
				_cooldownLeft = CooldownBars;
			}
			else
			{
				SellMarket();
				_entryPrice = candle.ClosePrice;
				_cooldownLeft = CooldownBars;
			}
		}

		UpdatePrevious(jaw, teeth, lips);
	}

	private bool TryExitByRisk(ICandleMessage candle)
	{
		if (_entryPrice is not decimal entryPrice || Position == 0 || entryPrice == 0)
			return false;

		var stopDistance = entryPrice * StopLossPercent / 100m;
		var takeDistance = entryPrice * TakeProfitPercent / 100m;

		if (Position > 0)
		{
			if ((stopDistance > 0 && candle.LowPrice <= entryPrice - stopDistance) ||
				(takeDistance > 0 && candle.HighPrice >= entryPrice + takeDistance))
			{
				SellMarket(Position);
				_entryPrice = null;
				_cooldownLeft = CooldownBars;
				return true;
			}
		}
		else if (Position < 0)
		{
			var volume = Math.Abs(Position);

			if ((stopDistance > 0 && candle.HighPrice >= entryPrice + stopDistance) ||
				(takeDistance > 0 && candle.LowPrice <= entryPrice - takeDistance))
			{
				BuyMarket(volume);
				_entryPrice = null;
				_cooldownLeft = CooldownBars;
				return true;
			}
		}

		return false;
	}

	private void UpdatePrevious(decimal jaw, decimal teeth, decimal lips)
	{
		_previousJaw = jaw;
		_previousTeeth = teeth;
		_previousLips = lips;
		_hasPrevious = true;
	}
}