The Multi Arbitration Strategy is a StockSharp port of the MetaTrader "Multi_arbitration 1.000" expert advisor. The original script continuously evaluates existing buy and sell positions, adds new trades in the direction with weaker floating profit, and performs a global liquidation once overall profit targets are met. This C# implementation keeps the core decision logic while adapting it to StockSharp's netting portfolio model and high-level strategy API.
The strategy:
Opens an initial long position as soon as the first finished candle arrives.
Compares the unrealized profit of the active direction with the alternative direction to decide whether a reversal is required.
Forces a flat position when the configured profit target is exceeded or when position pressure grows beyond a configurable limit.
Uses only market orders (BuyMarket / SellMarket) to maintain simplicity and fast execution.
Trading Logic
Initial order – The very first finished candle triggers a long market order with the configured trade volume. This reproduces the MetaTrader expert's immediate market entry.
Profit comparison – On every finished candle the strategy calculates the floating PnL of the current direction:
Long profit = (close - entry) * volume
Short profit = (entry - close) * volume
Position selection – If the alternative direction would currently perform better than the active one, the strategy flips the position by sending a market order sized to cover the existing exposure and open a new position in the new direction. When no position is open, the algorithm defaults to a long entry, matching the original expert advisor.
Position limit guard – A configurable MaxOpenPositions parameter mirrors the MetaTrader check against LimitOrders(). When the combined long/short exposure reaches this cap and the strategy is profitable, it flattens the book to avoid over-leverage.
Profit target exit – When the account PnL (realized + unrealized) exceeds the ProfitForClose threshold the strategy closes all positions, exactly like the original Equity - Balance check.
Parameters
Name
Description
Default
TradeVolume
Volume used for every market order. Represents the minimum lot size in the original EA.
1
ProfitForClose
Profit threshold that triggers a global exit once exceeded.
300
MaxOpenPositions
Maximum number of simultaneous positions allowed before the strategy forces a flatten. Acts as limit - 15 equivalent.
15
CandleType
Candle data type that synchronizes trade decisions. Default is 1-minute time frame.
1 minute candles
Implementation Notes
StockSharp uses a netting position model, so the strategy can hold only one net direction at a time. Reversals are handled by sizing market orders to both close the existing exposure and open a new position in the opposite direction.
The StartProtection() call is used to inherit built-in risk handling (e.g., stop-out on non-zero positions when the strategy is stopped).
All state variables (_entryPrice, _currentSide, _initialOrderPlaced) are reset on OnReseted to support restarts and repeated simulations without stale data.
The strategy only reacts to finished candles to avoid double-counting profits on partially formed bars.
Usage Recommendations
Align the TradeVolume parameter with the instrument's lot size or contract multiplier.
The ProfitForClose value should be set using the same currency as the account PnL (e.g., USD for FX accounts).
Increase or decrease MaxOpenPositions depending on how aggressively you want the strategy to accumulate exposure before forcing a flatten.
Because the strategy always begins with a long trade, consider manually starting it when long entries are acceptable for the traded instrument.
Differences from the MetaTrader Version
MetaTrader's hedging mode allows simultaneous long and short positions, while this port operates in a netting environment. The decision logic still compares directional profitability, but only one net position is kept at any moment.
Platform-specific checks (terminal trading permissions, filling type selection, account magic numbers) are replaced with StockSharp equivalents such as StartProtection() and candle subscriptions.
Commented diagnostics from the MQL file are not reproduced; rely on StockSharp logging if runtime information is required.
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()