Открыть на GitHub

Стратегия Fluctuate

Fluctuate Strategy — это порт эксперта MetaTrader "Fluctuate" на StockSharp. Алгоритм полностью реализован через высокоуровневое API: анализ свечей ведётся через SubscribeCandles, входы выполняются командами BuyMarket / SellMarket, а восстановительные ордера выставляются в виде стоп-заявок. Для имитации хеджевого режима MetaTrader внутри стратегии отдельно ведутся длинная и короткая экспозиции, хотя фактическая позиция в StockSharp остаётся нетто.

Ключевая идея

  1. При закрытии каждой свечи сравниваются два последних закрытия. Более высокое закрытие даёт сигнал на покупку, более низкое — на продажу. Равные значения пропускаются.
  2. Каждой сделке назначаются фиксированные стоп-лосс и тейк-профит (в пунктах). Запоминаются точная цена исполнения и объём, добавленный этой сделкой.
  3. После входа активируется противоположный стоп-ордер на расстоянии StepPips от цены последней сделки (с небольшим запасом на спрэд). Его объём вычисляется из предыдущей сделки и коэффициента LotCoefficient; при MultiplyLotCoefficient = true используется суммарная экспозиция.
  4. При срабатывании стоп-ордера предыдущая заявка отменяется, внутренние счётчики объёмов обновляются, и мгновенно готовится новый восстановительный ордер уже в другую сторону. Это повторяет сеточную/мартингальную логику исходного советника.
  5. Защитный трейлинг подтягивает стоп, когда цена проходит минимум TrailingStopPips + TrailingStepPips в прибыльную сторону, то есть точно как в MQL-версии, где требовался дополнительный запас до переноса стопа.

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

  • Формирование сигналов. Используется только закрытие свечей (CandleStates.Finished). Торговля блокируется вне диапазона часов [StartHour, EndHour) либо при срабатывании ограничения по капиталу.
  • Первичный объём. Режим PositionSizingMode = FixedVolume даёт фиксированный лот. В режиме RiskPercent риск (процент текущего капитала) делится на денежный убыток при срабатывании стопа; для перевода пунктов в деньги применяются PriceStep и StepPrice инструмента.
  • Учёт экспозиции. Отдельные накопители ведут объём, среднюю цену и экстремумы для лонга и шорта. Благодаря этому стратегия "помнит" обе стороны, даже если брокер ведёт нетто-позицию.
  • Восстановительные ордера. После каждой сделки пересчитывается объём следующего стоп-ордера:
    • При MultiplyLotCoefficient = false используется формула LastVolume × LotCoefficient.
    • При true умножается общий абсолютный объём.
    • Объём нормируется по биржевым ограничениям (шаг, минимум, максимум) и отклоняется, если превышает MaxTotalVolume или лимит по количеству позиций MaxPositions.
  • Цель по прибыли и контроль капитала. Нереализованный результат пересчитывается в валюту счёта через PriceStep/StepPrice. Достижение ProfitTarget приводит к закрытию всех позиций и отмене ордеров. Если же капитал падает ниже MinEquityPercent от стартового, стратегия переходит в режим ожидания и допускает только выходы.
  • Трейлинг. Для лонга хранится максимум цены с момента входа. Как только он превышает цену входа на TrailingStopPips + TrailingStepPips, стоп переносится на расстояние TrailingStopPips от максимума. Для шорта действует зеркальное правило относительно минимума.

Управление рисками

  • Стоп и тейк. Оба параметра можно отключить установкой нуля. При добавлении объёма уровни пересчитываются для совокупной позиции соответствующего направления.
  • MaxPositions. Считает количество активных направлений (лонг/шорт) плюс текущий восстановительный ордер. Достигнутый лимит блокирует постановку новых стоп-заявок.
  • MaxTotalVolume. Ограничивает сумму абсолютного открытого объёма и объёма активного стоп-ордера.
  • CloseAllAtStart. Опционально закрывает все позиции при старте стратегии.

Параметры

Имя Описание Значение по умолчанию
CandleType Таймфрейм, по которому формируются сигналы. 1 минута
StopLossPips Дистанция стоп-лосса (в пунктах). 0 — без стопа. 50
TakeProfitPips Дистанция тейк-профита (в пунктах). 0 — без тейка. 50
TrailingStopPips Основная дистанция трейлинг-стопа (в пунктах). Работает только с положительным TrailingStepPips. 5
TrailingStepPips Дополнительный ход цены перед сдвигом трейлинг-стопа (в пунктах). 5
StepPips Отступ между последней сделкой и противоположным восстановительным стопом (в пунктах). 30
LotCoefficient Коэффициент умножения объёма для восстановительных ордеров. 2.0
MultiplyLotCoefficient Если true, коэффициент применяется к суммарному объёму, а не к последней сделке. false
MaxPositions Максимум одновременно открытых направлений плюс активный стоп-ордер. 9
MaxTotalVolume Лимит на сумму абсолютного объёма и объёма стоп-ордера. 50
ProfitTarget Цель по нереализованной прибыли (в валюте счёта). 0 — без цели. 50
MinEquityPercent Минимальный процент капитала от стартового баланса для продолжения торговли. 30
CloseAllAtStart Закрыть все позиции при запуске стратегии. false
StartHour Час начала торговли (включительно, по времени площадки). 10
EndHour Час окончания торговли (не включительно, по времени площадки). 20
PositionSizingMode FixedVolume — фиксированный лот, RiskPercent — доля капитала под риск. FixedVolume
VolumeOrRisk Фиксированный лот (в режиме FixedVolume) или процент риска (в режиме RiskPercent). 1.0

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

  • Цены стоп-ордеров дополнительно сдвигаются на минимальный спрэд (используется PriceStep, если доступен), чтобы повторить логику MetaTrader с freeze-level.
  • После исполнения любой заявки оставшийся стоп-ордер отменяется — как и в оригинальном советнике.
  • Из-за неттинга в StockSharp хеджирование моделируется внутренними переменными, фактическая биржевая позиция всегда нетто.
  • Режим RiskPercent требует корректных PriceStep и StepPrice в описании инструмента.

Практические рекомендации

  1. Подберите таймфрейм, близкий к тому, на котором советник тестировался (чаще всего M5 или M15).
  2. Убедитесь, что биржевые ограничения по объёму позволяют выставлять восстановительные ордера; если нормированный объём стал равен нулю, стратегия прекратит набор сетки.
  3. В режиме RiskPercent следите за актуальностью данных по капиталу портфеля, иначе стратегия вернётся к фиксированному объёму.
  4. При необходимости подключите встроенную защиту StartProtection() для дополнительного контроля рисков на уровне счёта.
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>
/// Fluctuate strategy using EMA crossover with stop-loss and take-profit.
/// Buys when fast EMA crosses above slow EMA, sells on reverse cross.
/// </summary>
public class FluctuateStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	/// <summary>
	/// Fast EMA period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="FluctuateStrategy"/>.
	/// </summary>
	public FluctuateStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("Fast Period", "Fast EMA period", "Indicator");

		_slowPeriod = Param(nameof(SlowPeriod), 200)
			.SetGreaterThanZero()
			.SetDisplay("Slow Period", "Slow EMA period", "Indicator");

		_stopLossPoints = Param(nameof(StopLossPoints), 200)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

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

		_fast = null;
		_slow = null;
		_prevFast = 0;
		_prevSlow = 0;
		_entryPrice = 0;
		_cooldown = 0;
	}

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

		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };

		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (!_fast.IsFormed || !_slow.IsFormed)
		{
			_prevFast = fastValue;
			_prevSlow = slowValue;
			return;
		}

		if (_cooldown > 0)
		{
			_cooldown--;
			_prevFast = fastValue;
			_prevSlow = slowValue;
			return;
		}

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		// Check SL/TP
		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step)
			{
				SellMarket();
				_entryPrice = 0;
				_cooldown = 50;
				_prevFast = fastValue;
				_prevSlow = slowValue;
				return;
			}

			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step)
			{
				SellMarket();
				_entryPrice = 0;
				_cooldown = 50;
				_prevFast = fastValue;
				_prevSlow = slowValue;
				return;
			}
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step)
			{
				BuyMarket();
				_entryPrice = 0;
				_cooldown = 50;
				_prevFast = fastValue;
				_prevSlow = slowValue;
				return;
			}

			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step)
			{
				BuyMarket();
				_entryPrice = 0;
				_cooldown = 50;
				_prevFast = fastValue;
				_prevSlow = slowValue;
				return;
			}
		}

		// EMA crossover buy signal
		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();

			BuyMarket();
			_entryPrice = close;
			_cooldown = 50;
		}
		// EMA crossover sell signal
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{
			if (Position > 0)
				SellMarket();

			SellMarket();
			_entryPrice = close;
			_cooldown = 50;
		}

		_prevFast = fastValue;
		_prevSlow = slowValue;
	}
}