Открыть на GitHub

Стратегия Swaper (API 3751)

Обзор

Swaper Strategy — это перенос советника MetaTrader "Swaper 1.1" на высокий уровень API StockSharp. Оригинальная система зарабатывает на свапах, постоянно перестраивая синтетический портфель между длинными и короткими позициями. Конверсия воссоздаёт денежный поток советника: вычисляет справедливую стоимость базового инструмента и подстраивает открытую позицию под полученное значение.

Основная логика

  1. Реконструкция синтетического капитала. Переменная money из MQL воспроизводится через стартовый баланс (BaseUnits * BeginPrice), реализованную прибыль по сделкам и нереализованный результат текущей позиции, умноженный на ContractMultiplier.
  2. Знаменатель справедливой цены. Переменная com из оригинала зависит от величины активных позиций. Здесь она рассчитывается как BaseUnits + ContractMultiplier * Position, что повторяет изменение знаменателя при росте или сокращении объёма.
  3. Расчёт целевого объёма. Берётся максимум из двух последних максимумов свечей (с поправкой на спред) и минимум из двух последних минимумов. Коэффициент Experts / (Experts + 1) задаёт скорость подстройки к справедливой цене.
  4. Коррекция позиции. В зависимости от dt стратегия:
    • закрывает позицию, если требуемая корректировка меньше одной десятой лота;
    • наращивает продажи, когда dt < 0;
    • наращивает покупки, когда dt >= 0.
  5. Учёт маржи. Метод GetTradableVolume аппроксимирует AccountFreeMargin(), сравнивая параметр MarginPerLot со стоимостью портфеля. Если доступной маржи не хватает, объём округляется вниз до ближайшей десятой доли лота.

Весь цикл исполняется по завершённым свечам, что заменяет тиковый start() и при этом сохраняет экономическую идею.

Параметры

Параметр Значение по умолчанию Описание
Experts 1 Вес, с которым стратегия приближает позицию к справедливой цене.
BeginPrice 1.8014 Стартовая цена для восстановления виртуального баланса.
MagicNumber 777 Сохранённый идентификатор из версии MetaTrader (можно использовать в комментариях к заявкам).
BaseUnits 1000 Базовые единицы капитала в знаменателе справедливой цены.
ContractMultiplier 10 Множитель, переводящий ценовые изменения в валюту счёта.
MarginPerLot 1000 Приближённая потребность в капитале для удержания одного лота.
FallbackSpreadSteps 1 Спред в шагах цены, когда данные стакана недоступны.
CandleType 1 час Таймфрейм, на котором выполняется перебалансировка.

Последовательность работы

  1. Подписаться на выбранные свечи и поток Level 1, чтобы фиксировать спред.
  2. При отсутствии котировок использовать значение FallbackSpreadSteps * PriceStep.
  3. На каждой завершённой свече пересчитывать синтетический капитал и знаменатель com.
  4. Сначала анализировать верхнюю ветку (dt по максимумам). Если dt < 0, переходить к ветке минимумов, повторяя защиту из оригинала.
  5. Вызывать AdjustShort или AdjustLong для уменьшения/увеличения позиции. Если требуемый объём меньше 0.1 лота — закрывать позицию полностью, как это делает closeby в MetaTrader.
  6. В OnOwnTradeReceived обновлять реализованную прибыль, чтобы следующие итерации использовали актуальный баланс.

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

  • Тиковый цикл start() заменён обработкой свечей. Это исключает пустой опрос и оставляет стратегию идейно неизменной.
  • История ордеров и активные сделки анализируются через собственные сделки стратегии, поскольку в StockSharp нет прямых аналогов OrdersHistoryTotal() и OrdersTotal().
  • Проверка маржи основана на Portfolio.CurrentValue и настраиваемом MarginPerLot, так как брокерские функции MarketInfo недоступны.
  • Закрытие встречных ордеров (OrderCloseBy) имитируется принудительным выравниванием нетто-позиции, что соответствует модели неттинга в большинстве коннекторов StockSharp.

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

  • Подберите MarginPerLot под спецификацию брокера, чтобы стратегия не пыталась открыть недоступный объём.
  • Используйте таймфрейм, близкий к исходному, чтобы экстремумы свечей совпадали с данными MetaTrader.
  • Убедитесь, что подписки на свечи и Level 1 активны одновременно: так спред будет рассчитываться корректно, а реакция стратегии останется синхронной с оригиналом.
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// Swap-based mean reversion strategy converted from the MetaTrader expert "Swaper 1.1".
/// Calculates a synthetic fair value using closed trades, adjusts the open position, and keeps the volume within the available margin.
/// </summary>
public class SwaperStrategy : Strategy
{
	private readonly StrategyParam<decimal> _experts;
	private readonly StrategyParam<decimal> _beginPrice;
	private readonly StrategyParam<int> _magicNumber;
	private readonly StrategyParam<decimal> _baseUnits;
	private readonly StrategyParam<decimal> _contractMultiplier;
	private readonly StrategyParam<decimal> _marginPerLot;
	private readonly StrategyParam<decimal> _fallbackSpreadSteps;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _initialCapital;
	private decimal _realizedPnL;
	private decimal _positionVolume;
	private decimal _averagePrice;
	private decimal? _bestBid;
	private decimal? _bestAsk;
	private ICandleMessage _previousCandle;

	/// <summary>
	/// Initializes a new instance of the <see cref="SwaperStrategy"/> class.
	/// </summary>
	public SwaperStrategy()
	{
		_experts = Param(nameof(Experts), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Experts", "Weighting coefficient applied to the synthetic fair value.", "General");

		_beginPrice = Param(nameof(BeginPrice), 1.8014m)
		.SetGreaterThanZero()
		.SetDisplay("Begin Price", "Initial price used to recreate the historical balance.", "General");

		_magicNumber = Param(nameof(MagicNumber), 777)
		.SetDisplay("Magic Number", "Identifier kept for compatibility with the MetaTrader expert.", "General");

		_baseUnits = Param(nameof(BaseUnits), 1000m)
		.SetGreaterThanZero()
		.SetDisplay("Base Units", "Synthetic account units used when calculating the fair value denominator.", "Money Management");

		_contractMultiplier = Param(nameof(ContractMultiplier), 10m)
		.SetGreaterThanZero()
		.SetDisplay("Contract Multiplier", "Value multiplier applied to realized and unrealized profit.", "Money Management");

		_marginPerLot = Param(nameof(MarginPerLot), 1000m)
		.SetGreaterThanZero()
		.SetDisplay("Margin Per Lot", "Approximate capital required to keep one lot open.", "Money Management");

		_fallbackSpreadSteps = Param(nameof(FallbackSpreadSteps), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Fallback Spread (steps)", "Spread expressed in price steps when level-one data is unavailable.", "General");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Primary timeframe that replaces the tick-based loop of the original expert.", "Data");
	}

	/// <summary>
	/// Weighting coefficient applied to the synthetic fair value.
	/// </summary>
	public decimal Experts
	{
		get => _experts.Value;
		set => _experts.Value = value;
	}

	/// <summary>
	/// Initial price used to recreate the historical balance.
	/// </summary>
	public decimal BeginPrice
	{
		get => _beginPrice.Value;
		set => _beginPrice.Value = value;
	}

	/// <summary>
	/// Identifier kept for compatibility with the MetaTrader expert.
	/// </summary>
	public int MagicNumber
	{
		get => _magicNumber.Value;
		set => _magicNumber.Value = value;
	}

	/// <summary>
	/// Synthetic account units used when calculating the fair value denominator.
	/// </summary>
	public decimal BaseUnits
	{
		get => _baseUnits.Value;
		set => _baseUnits.Value = value;
	}

	/// <summary>
	/// Value multiplier applied to realized and unrealized profit.
	/// </summary>
	public decimal ContractMultiplier
	{
		get => _contractMultiplier.Value;
		set => _contractMultiplier.Value = value;
	}

	/// <summary>
	/// Approximate capital required to keep one lot open.
	/// </summary>
	public decimal MarginPerLot
	{
		get => _marginPerLot.Value;
		set => _marginPerLot.Value = value;
	}

	/// <summary>
	/// Spread expressed in price steps when level-one data is unavailable.
	/// </summary>
	public decimal FallbackSpreadSteps
	{
		get => _fallbackSpreadSteps.Value;
		set => _fallbackSpreadSteps.Value = value;
	}

	/// <summary>
	/// Primary timeframe that replaces the tick-based loop of the original expert.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

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

		_initialCapital = 0m;
		_realizedPnL = 0m;
		_positionVolume = 0m;
		_averagePrice = 0m;
		_bestBid = null;
		_bestAsk = null;
		_previousCandle = null;
	}

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

		_initialCapital = BaseUnits * BeginPrice;
		_realizedPnL = 0m;
		_positionVolume = 0m;
		_averagePrice = 0m;
		_bestBid = null;
		_bestAsk = null;
		_previousCandle = null;

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

	private void ProcessLevel1(Level1ChangeMessage message)
	{
		if (message.TryGetDecimal(Level1Fields.BestBidPrice) is decimal bid)
		_bestBid = bid;

		if (message.TryGetDecimal(Level1Fields.BestAskPrice) is decimal ask)
		_bestAsk = ask;
	}

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

		if (_previousCandle == null)
		{
		_previousCandle = candle;
		return;
		}

		var security = Security;
		var priceStep = security?.PriceStep ?? 0.0001m;
		var spread = GetSpread(priceStep);
		var high = Math.Max(candle.HighPrice, _previousCandle.HighPrice);
		var low = Math.Min(candle.LowPrice, _previousCandle.LowPrice);

		if (high <= 0m || low <= 0m)
		{
		_previousCandle = candle;
		return;
		}

		var denominator = high + spread;
		if (denominator <= 0m)
		{
		_previousCandle = candle;
		return;
		}

		var com = CalculateDenominator();
		if (com == 0m)
		{
		_previousCandle = candle;
		return;
		}

		var money = CalculateSyntheticCapital(candle.ClosePrice);
		var expertsWeight = Experts;
		var dt = (money / denominator - com) * expertsWeight / (expertsWeight + 1m);

		if (dt < 0m)
		{
		var altDenominator = money / low;
		var dtAlt = (com - altDenominator) * expertsWeight / (expertsWeight + 1m);

		if (dtAlt < 1m)
		{
		ClosePositionIfExists();
		_previousCandle = candle;
		return;
		}

		var lots = (decimal)Math.Floor((double)dtAlt) / 10m;
		AdjustShort(lots);
		}
		else
		{
		if (dt < 1m)
		{
		ClosePositionIfExists();
		_previousCandle = candle;
		return;
		}

		var lots = (decimal)Math.Floor((double)dt) / 10m;
		AdjustLong(lots);
		}

		_previousCandle = candle;
	}

	private decimal CalculateSyntheticCapital(decimal currentPrice)
	{
		var multiplier = ContractMultiplier;
		var unrealized = _positionVolume * currentPrice * multiplier;
		return _initialCapital + _realizedPnL + unrealized;
	}

	private decimal CalculateDenominator()
	{
		return BaseUnits + ContractMultiplier * _positionVolume;
	}

	private decimal GetSpread(decimal priceStep)
	{
		if (_bestBid is decimal bid && _bestAsk is decimal ask && ask > bid)
		return ask - bid;

		var steps = FallbackSpreadSteps;
		return (steps <= 0m ? 1m : steps) * priceStep;
	}

	private void AdjustShort(decimal targetLots)
	{
		if (targetLots <= 0m)
		return;

		if (Position > 0m)
		{
		var reduce = Math.Min(Position, targetLots);
		if (reduce > 0m)
		SellMarket(reduce);
		return;
		}

		var currentShort = Position < 0m ? Math.Abs(Position) : 0m;
		if (currentShort >= targetLots)
		return;

		var additional = targetLots - currentShort;
		var tradable = GetTradableVolume(additional);
		if (tradable > 0m)
		SellMarket(tradable);
	}

	private void AdjustLong(decimal targetLots)
	{
		if (targetLots <= 0m)
		return;

		if (Position < 0m)
		{
		var reduce = Math.Min(Math.Abs(Position), targetLots);
		if (reduce > 0m)
		BuyMarket(reduce);
		return;
		}

		var currentLong = Position > 0m ? Position : 0m;
		if (currentLong >= targetLots)
		return;

		var additional = targetLots - currentLong;
		var tradable = GetTradableVolume(additional);
		if (tradable > 0m)
		BuyMarket(tradable);
	}

	private void ClosePositionIfExists()
	{
		var volume = Math.Abs(Position);
		if (volume <= 0m)
		return;

		if (Position > 0m)
		SellMarket(volume);
		else
		BuyMarket(volume);
	}

	private decimal GetTradableVolume(decimal desiredLots)
	{
		if (desiredLots <= 0m)
		return 0m;

		var marginPerLot = MarginPerLot;
		var availableCapital = Portfolio?.CurrentValue ?? (_initialCapital + _realizedPnL);

		if (marginPerLot <= 0m || availableCapital <= 0m)
		return (decimal)Math.Floor((double)(desiredLots * 10m)) / 10m;

		var maxLots = (decimal)Math.Floor((double)((availableCapital / marginPerLot) * 10m)) / 10m;
		if (maxLots <= 0m)
		return 0m;

		return Math.Min(desiredLots, (decimal)maxLots);
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		var order = trade.Order;
		if (order == null || order.Security != Security)
		return;

		var tradeInfo = trade.Trade;
		var volume = tradeInfo.Volume;
		if (volume <= 0m)
		return;

		var signedVolume = order.Side == Sides.Buy ? volume : -volume;
		var price = tradeInfo.Price;

		if (_positionVolume == 0m || Math.Sign(_positionVolume) == Math.Sign(signedVolume))
		{
		var totalVolume = _positionVolume + signedVolume;
		if (totalVolume == 0m)
		{
		_positionVolume = 0m;
		_averagePrice = 0m;
		}
		else
		{
		var weightedPrice = _averagePrice * _positionVolume + price * signedVolume;
		_positionVolume = totalVolume;
		_averagePrice = weightedPrice / totalVolume;
		}
		return;
		}

		var closingVolume = Math.Min(Math.Abs(signedVolume), Math.Abs(_positionVolume));
		var realized = (price - _averagePrice) * closingVolume * Math.Sign(_positionVolume) * ContractMultiplier;
		_realizedPnL += realized;

		var remainingVolume = _positionVolume + signedVolume;

		if (remainingVolume == 0m)
		{
		_positionVolume = 0m;
		_averagePrice = 0m;
		return;
		}

		if (Math.Sign(_positionVolume) == Math.Sign(remainingVolume))
		{
		_positionVolume = remainingVolume;
		return;
		}

		_positionVolume = remainingVolume;
		_averagePrice = price;
	}
}