Открыть на GitHub

Стратегия Multi Arbitration

Общее описание

Multi Arbitration Strategy — это порт экспертного советника MetaTrader "Multi_arbitration 1.000" на платформу StockSharp. Оригинальный советник постоянно анализирует уже открытые длинные и короткие позиции, добавляет сделки в сторону с меньшей плавающей прибылью и закрывает все позиции, когда совокупный результат достигает целевого значения. Версия на C# сохраняет эту идею и адаптирует её к неттинговой модели позиций и высокоуровневому API StockSharp.

Ключевые особенности:

  • Первая завершённая свеча после запуска приводит к открытию длинной позиции с заданным объёмом.
  • На каждой свече стратегия сравнивает плавающий результат текущего направления с потенциальным результатом противоположного направления.
  • При достижении целевой прибыли или при превышении лимита по количеству позиций стратегия полностью закрывает портфель.
  • Используются только рыночные заявки (BuyMarket / SellMarket), что упрощает логику и ускоряет исполнение.

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

  1. Начальная сделка. Как только поступает первая завершённая свеча, стратегия отправляет рыночную заявку на покупку с заданным объёмом — так воспроизводится поведение MQL5-советника.
  2. Сравнение прибыли. Для каждой завершённой свечи рассчитываются плавающие результаты:
    • Длинная позиция: (Close - Entry) * Volume
    • Короткая позиция: (Entry - Close) * Volume
  3. Выбор направления. Если альтернативное направление выглядит выгоднее текущего, стратегия разворачивает позицию — заявка выставляется таким объёмом, чтобы закрыть текущую позицию и открыть новую в обратную сторону. При отсутствии открытых позиций алгоритм по умолчанию открывает длинную сделку.
  4. Ограничение по позициям. Параметр MaxOpenPositions имитирует проверку LimitOrders() в MetaTrader. Если суммарное количество длинных и коротких позиций достигает этого порога и стратегия находится в плюсе, портфель закрывается.
  5. Выход по прибыли. Когда прибыль по счёту (реализованная и плавающая) превышает ProfitForClose, стратегия закрывает все позиции — аналог условия Equity - Balance > ProfitFoClose в исходном коде.

Параметры

Параметр Описание Значение по умолчанию
TradeVolume Объём каждой рыночной заявки. Аналог минимального лота в исходном советнике. 1
ProfitForClose Порог прибыли, при превышении которого портфель закрывается полностью. 300
MaxOpenPositions Максимально допустимое количество одновременно открытых позиций. После достижения лимита стратегия закрывается. 15
CandleType Тип свечей, используемый для синхронизации решений. По умолчанию — минутные свечи. 1 minute candles

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

  • В StockSharp используется неттинговая система, поэтому в любой момент времени может существовать только одна чистая позиция. Разворот реализован через отправку заявки достаточного объёма, чтобы закрыть текущую позицию и открыть новую в противоположном направлении.
  • Вызов StartProtection() подключает встроенные механизмы защиты (например, автоматическое закрытие позиции при остановке стратегии).
  • Переменные _entryPrice, _currentSide и _initialOrderPlaced сбрасываются в OnReseted, что позволяет повторно запускать стратегию без артефактов.
  • Обработка ведётся только по завершённым свечам (CandleStates.Finished), чтобы избежать двойного учёта прибыли.

Рекомендации по использованию

  • Настройте TradeVolume в соответствии с минимальным шагом объёма/лотностью инструмента.
  • Порог ProfitForClose должен измеряться в валюте счёта, чтобы совпадать с расчётом PnL.
  • Уменьшение MaxOpenPositions делает стратегию более консервативной, увеличение — позволяет дольше удерживать позицию до принудительного закрытия.
  • Стратегия всегда начинает с покупки, поэтому запускайте её в тех условиях рынка, когда такая сделка допустима.

Отличия от версии MetaTrader

  • В MetaTrader разрешены одновременные длинные и короткие позиции (режим хеджирования). В StockSharp стратегия работает в неттинговом режиме и удерживает только одно чистое направление, но продолжает сравнивать потенциальную прибыль обеих сторон.
  • Проверки терминала (разрешение алгоритмической торговли, типы исполнения, magic number) заменены на механизмы StockSharp: подписка на свечи и защиту через StartProtection().
  • Текстовые комментарии и вывод Comment() из оригинала не перенесены. Для мониторинга используйте систему логирования StockSharp.
using System;
using System.Linq;
using System.Collections.Generic;

using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Multi-direction arbitration strategy adapted from MetaTrader logic.
/// </summary>
public class MultiArbitrationStrategy : Strategy
{
	private readonly StrategyParam<decimal> _profitForClose;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _maxOpenPositions;
	private readonly StrategyParam<DataType> _candleType;

	private bool _initialOrderPlaced;
	private decimal _entryPrice;
	private Sides? _currentSide;

	/// <summary>
	/// Target profit that triggers a full position exit.
	/// </summary>
	public decimal ProfitForClose
	{
		get => _profitForClose.Value;
		set => _profitForClose.Value = value;
	}

	/// <summary>
	/// Volume used when sending market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Maximum simultaneous positions allowed before forcing a flatten.
	/// </summary>
	public int MaxOpenPositions
	{
		get => _maxOpenPositions.Value;
		set => _maxOpenPositions.Value = value;
	}

	/// <summary>
	/// Candle type used for synchronization and decision making.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="MultiArbitrationStrategy"/> class.
	/// </summary>
	public MultiArbitrationStrategy()
	{
		_profitForClose = Param(nameof(ProfitForClose), 300m)
			.SetDisplay("Profit Threshold", "Profit required before flattening all positions.", "Risk");

		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Volume used when opening new positions.", "Trading");

		_maxOpenPositions = Param(nameof(MaxOpenPositions), 15)
			.SetGreaterThanZero()
			.SetDisplay("Max Open Positions", "Maximum simultaneous positions allowed before closing everything.", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used to synchronize trading decisions.", "Data");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

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

		_initialOrderPlaced = false;
		_entryPrice = 0m;
		_currentSide = null;
	}

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

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
	}

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

		if (!_initialOrderPlaced)
		{
			OpenLong(candle);
			_initialOrderPlaced = true;
		}

		var longCount = _currentSide == Sides.Buy ? 1 : 0;
		var shortCount = _currentSide == Sides.Sell ? 1 : 0;

		var longProfit = _currentSide == Sides.Buy ? (candle.ClosePrice - _entryPrice) * Volume : 0m;
		var shortProfit = _currentSide == Sides.Sell ? (_entryPrice - candle.ClosePrice) * Volume : 0m;

		if (longCount + shortCount < MaxOpenPositions)
		{
			if (longProfit < shortProfit && _currentSide != Sides.Buy)
			{
				OpenLong(candle);
			}
			else if (shortProfit < longProfit && _currentSide != Sides.Sell)
			{
				OpenShort(candle);
			}
			else if (longProfit == 0m && shortProfit == 0m && Position == 0 && _currentSide is null)
			{
				OpenLong(candle);
			}
		}
		else if (PnL > 0m && Position != 0)
		{
			FlattenPosition(candle);
		}

		if (PnL > ProfitForClose && Position != 0)
		{
			FlattenPosition(candle);
		}
	}

	private void OpenLong(ICandleMessage candle)
	{
		if (Position > 0)
		{
			// Already holding a long position, so only refresh the entry reference.
			_entryPrice = candle.ClosePrice;
			_currentSide = Sides.Buy;
			return;
		}

		BuyMarket();
		_entryPrice = candle.ClosePrice;
		_currentSide = Sides.Buy;
	}

	private void OpenShort(ICandleMessage candle)
	{
		if (Position < 0)
		{
			// Already holding a short position, so only refresh the entry reference.
			_entryPrice = candle.ClosePrice;
			_currentSide = Sides.Sell;
			return;
		}

		SellMarket();
		_entryPrice = candle.ClosePrice;
		_currentSide = Sides.Sell;
	}

	private void FlattenPosition(ICandleMessage candle)
	{
		if (_currentSide is null)
			return;

		if (Position > 0)
		{
			SellMarket();
		}
		else if (Position < 0)
		{
			BuyMarket();
		}

		_currentSide = null;
		_entryPrice = 0m;
	}
}