Ver no GitHub

Estratégia de troca (API 3751)

Visão geral

A Estratégia Swaper replica o consultor especialista MetaTrader "Swaper 1.1" usando a estratégia de alto nível de StockSharp API. O O sistema original acumula ganhos de swap reequilibrando constantemente uma carteira sintética entre exposições longas e curtas. Isto a conversão preserva a lógica do fluxo de dinheiro, reconstruindo o equilíbrio virtual do especialista, calculando um valor justo para o instrumento subjacente e alinhando a posição aberta com esse valor alvo.

Lógica principal

  1. Reconstrução de capital sintético. A estratégia recria o acumulador MetaTrader money combinando o acumulador inicial saldo (BaseUnits * BeginPrice), lucro realizado de pedidos atendidos e a parte não realizada da posição atual dimensionado por ContractMultiplier.
  2. Denominador do valor justo. O especialista MQL mantém uma variável com que aumenta ou diminui com o volume ativo. O StockSharp port espelha esse comportamento por meio de BaseUnits + ContractMultiplier * Position.
  3. Cálculo do volume alvo. O algoritmo avalia o máximo das duas últimas máximas do candle (ajustado pelo spread do mercado) e o mínimo dos dois últimos mínimos para reproduzir o guard-rail MetaTrader. Um fator Experts / (Experts + 1) controla como agressivamente a estratégia caminha em direção ao valor justo.
  4. Ajustes de posição. Dependendo do valor dt calculado, a estratégia também
    • fecha posições quando o ajuste calculado é inferior a um décimo do lote, ou
    • vende volume adicional quando dt < 0, ou
    • compra volume adicional quando dt >= 0.
  5. Dimensionamento de lote com reconhecimento de margem. O método auxiliar GetTradableVolume aproxima as verificações de AccountFreeMargin() comparando o configurado MarginPerLot com o capital do portfólio disponível. Se o tamanho solicitado exceder a margem disponível, o lote o valor é calculado até o décimo mais próximo.

Todo o loop é executado em velas finalizadas, substituindo a função original baseada em ticks, mantendo a lógica econômica intacto.

Parâmetros

Parâmetro Padrão Descrição
Experts 1 Peso aplicado ao ajuste sintético de valor justo.
BeginPrice 1.8014 Preço inicial usado para reconstruir o saldo virtual.
MagicNumber 777 Identificador preservado para compatibilidade com a versão MetaTrader (registrado nos pedidos, se necessário).
BaseUnits 1000 Unidades de capital inicial utilizadas pelo denominador da equação do valor justo.
ContractMultiplier 10 Multiplicador que converte as diferenças de preço na moeda da conta.
MarginPerLot 1000 Capital aproximado necessário para suportar um lote; rege a lógica de redução de lote.
FallbackSpreadSteps 1 Spread em etapas de preço quando faltam cotações de nível um.
CandleType 1 Hour Período primário que alimenta o ciclo de rebalanceamento.

Fluxo de trabalho de negociação

  1. Assine a série de velas configurada e os dados de nível um.
  2. Acompanhe as melhores cotações de compra/venda para obter um spread preciso. Se o feed estiver silencioso, volte para FallbackSpreadSteps * PriceStep.
  3. Recalcule o capital sintético e o denominador de cada vela acabada.
  4. Calcule dt usando o caminho de preço alto. Quando dt < 0, mude para o ramo de preço baixo para emular a proteção original lógica.
  5. Use AdjustShort ou AdjustLong para diminuir ou expandir a posição. Quando o tamanho do alvo for menor que um décimo do lote, feche a posição completamente para copiar o comportamento closeby de MetaTrader.
  6. Atualize o PnL realizado dentro de OnOwnTradeReceived para que as iterações subsequentes usem o saldo mais recente.

Diferenças versus a versão MQL4

  • O loop start() acionado por ticks é substituído pelo processamento de velas, o que evita espera ocupada enquanto preserva a estratégia intenção.
  • O histórico de pedidos e a varredura de negociações abertas são aproximados por meio do fluxo de negociações da própria estratégia, em vez de OrdersHistoryTotal() e OrdersTotal().
  • As verificações de margem usam Portfolio.CurrentValue com uma constante MarginPerLot configurável porque a margem específica do corretor funções não estão disponíveis em StockSharp.
  • O fechamento do par via OrderCloseBy é emulado simplesmente achatando a posição líquida, consistente com o modelo de compensação da maioria Conectores StockSharp.

Notas de uso

  • Configure MarginPerLot de acordo com as especificações do contrato do conector para evitar que a estratégia solicite um volume inviável.
  • A estratégia espera que os dados das velas forneçam máximos e mínimos confiáveis; use um período que corresponda ao feed do corretor usado pelo Versão MetaTrader se desejar um comportamento idêntico.
  • Como as cotações de nível um podem chegar de forma assíncrona, a estratégia armazena o spread mais recente. Certifique-se de que as velas e o nível uma assinatura é habilitada para replicação precisa.
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;
	}
}