A Estratégia Multi Arbitragem é um port StockSharp do consultor especialista MetaTrader "Multi_arbitration 1.000". O script original avalia continuamente as posições compradas e vendidas existentes, adiciona novos trades na direção com menor lucro flutuante, e realiza uma liquidação global assim que os objetivos gerais de lucro são atingidos. Esta implementação em C# mantém a lógica de decisão central, adaptando-a ao modelo de portfolio de compensação do StockSharp e à API de estratégias de alto nível.
A estratégia:
Abre uma posição comprada inicial assim que a primeira vela finalizada chega.
Compara o lucro não realizado da direção ativa com a direção alternativa para decidir se uma reversão é necessária.
Força uma posição plana quando o objetivo de lucro configurado é excedido ou quando a pressão de posição cresce além de um limite configurável.
Usa apenas ordens de mercado (BuyMarket / SellMarket) para manter simplicidade e execução rápida.
Lógica de Trading
Ordem inicial – A primeira vela finalizada aciona uma ordem de mercado comprada com o volume de operação configurado. Isso reproduz a entrada imediata no mercado do consultor especialista MetaTrader.
Comparação de lucros – Em cada vela finalizada, a estratégia calcula o PnL flutuante da direção atual:
Lucro comprado = (close - entry) * volume
Lucro vendido = (entry - close) * volume
Seleção de posição – Se a direção alternativa funcionaria melhor atualmente que a ativa, a estratégia inverte a posição enviando uma ordem de mercado dimensionada para cobrir a exposição existente e abrir uma nova posição na nova direção. Quando nenhuma posição está aberta, o algoritmo padroniza para uma entrada comprada, correspondendo ao consultor especialista original.
Guarda de limite de posições – Um parâmetro configurável MaxOpenPositions espelha a verificação MetaTrader contra LimitOrders(). Quando a exposição combinada comprada/vendida atinge esse limite e a estratégia é lucrativa, ela aplaina o livro para evitar alavancagem excessiva.
Saída por objetivo de lucro – Quando o PnL da conta (realizado + não realizado) excede o limite ProfitForClose, a estratégia fecha todas as posições, exatamente como a verificação original Equity - Balance.
Parâmetros
Nome
Descrição
Padrão
TradeVolume
Volume usado para cada ordem de mercado. Representa o tamanho mínimo de lote no EA original.
1
ProfitForClose
Limite de lucro que aciona uma saída global quando excedido.
300
MaxOpenPositions
Número máximo de posições simultâneas permitidas antes de a estratégia forçar um aplainamento. Equivale a limit - 15.
15
CandleType
Tipo de dados de vela que sincroniza as decisões de operação. Padrão é período de 1 minuto.
velas de 1 minuto
Notas de Implementação
StockSharp usa um modelo de posição de compensação, portanto a estratégia só pode manter uma direção líquida por vez. As reversões são tratadas dimensionando ordens de mercado para fechar a exposição existente e abrir uma nova posição na direção oposta.
A chamada StartProtection() é usada para herdar o gerenciamento de risco integrado (por exemplo, stop-out em posições diferentes de zero quando a estratégia é parada).
Todas as variáveis de estado (_entryPrice, _currentSide, _initialOrderPlaced) são redefinidas em OnReseted para suportar reinicializações e simulações repetidas sem dados obsoletos.
A estratégia só reage a velas finalizadas para evitar a contagem dupla de lucros em barras parcialmente formadas.
Recomendações de Uso
Alinhe o parâmetro TradeVolume com o tamanho do lote do instrumento ou o multiplicador do contrato.
O valor ProfitForClose deve ser definido usando a mesma moeda que o PnL da conta (por exemplo, USD para contas FX).
Aumente ou diminua MaxOpenPositions dependendo de quão agressivamente você quer que a estratégia acumule exposição antes de forçar um aplainamento.
Como a estratégia sempre começa com uma operação comprada, considere iniciá-la manualmente quando entradas compradas são aceitáveis para o instrumento negociado.
Diferenças da Versão MetaTrader
O modo de cobertura do MetaTrader permite posições compradas e vendidas simultâneas, enquanto este port opera em um ambiente de compensação. A lógica de decisão ainda compara a rentabilidade direcional, mas apenas uma posição líquida é mantida a qualquer momento.
Verificações específicas da plataforma (permissões de trading do terminal, seleção do tipo de preenchimento, números mágicos da conta) são substituídas por equivalentes StockSharp como StartProtection() e assinaturas de velas.
Diagnósticos comentados do arquivo MQL não são reproduzidos; confie no log do StockSharp se informações em tempo de execução forem necessárias.
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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class multi_arbitration_strategy(Strategy):
"""Multi-direction arbitration strategy with profit-based flattening."""
def __init__(self):
super(multi_arbitration_strategy, self).__init__()
self._profit_for_close = self.Param("ProfitForClose", 300.0) \
.SetDisplay("Profit Threshold", "Profit required before flattening all positions.", "Risk")
self._max_open_positions = self.Param("MaxOpenPositions", 15) \
.SetGreaterThanZero() \
.SetDisplay("Max Open Positions", "Maximum simultaneous positions allowed before closing everything.", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle type used to synchronize trading decisions.", "Data")
self._initial_order_placed = False
self._entry_price = 0.0
self._current_side = 0 # 0=none, 1=buy, -1=sell
@property
def ProfitForClose(self):
return self._profit_for_close.Value
@property
def MaxOpenPositions(self):
return self._max_open_positions.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(multi_arbitration_strategy, self).OnStarted2(time)
self._initial_order_placed = False
self._entry_price = 0.0
self._current_side = 0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
vol = float(self.Volume) if self.Volume > 0 else 1.0
if not self._initial_order_placed:
self._open_long(close)
self._initial_order_placed = True
long_count = 1 if self._current_side == 1 else 0
short_count = 1 if self._current_side == -1 else 0
long_profit = (close - self._entry_price) * vol if self._current_side == 1 else 0.0
short_profit = (self._entry_price - close) * vol if self._current_side == -1 else 0.0
if long_count + short_count < self.MaxOpenPositions:
if long_profit < short_profit and self._current_side != 1:
self._open_long(close)
elif short_profit < long_profit and self._current_side != -1:
self._open_short(close)
elif long_profit == 0.0 and short_profit == 0.0 and self.Position == 0 and self._current_side == 0:
self._open_long(close)
elif float(self.PnL) > 0.0 and self.Position != 0:
self._flatten(close)
if float(self.PnL) > float(self.ProfitForClose) and self.Position != 0:
self._flatten(close)
def _open_long(self, close):
if self.Position > 0:
self._entry_price = close
self._current_side = 1
return
self.BuyMarket()
self._entry_price = close
self._current_side = 1
def _open_short(self, close):
if self.Position < 0:
self._entry_price = close
self._current_side = -1
return
self.SellMarket()
self._entry_price = close
self._current_side = -1
def _flatten(self, close):
if self._current_side == 0:
return
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._current_side = 0
self._entry_price = 0.0
def OnReseted(self):
super(multi_arbitration_strategy, self).OnReseted()
self._initial_order_placed = False
self._entry_price = 0.0
self._current_side = 0
def CreateClone(self):
return multi_arbitration_strategy()