RRS Tangled EA Strategy
Overview
The RRS Tangled EA Strategy is a StockSharp port of the MetaTrader 4 expert advisor "RRS Tangled EA". The original system randomly chooses trade direction and symbol, while capping the number of simultaneous orders and protecting floating profit through trailing stops and strict risk limits. The converted version focuses on the currently selected instrument, reproducing the random entry, trailing, and risk management behaviour using the high-level StockSharp API.
Core Logic
- Subscribe to the configured candle series and wait for completed candles.
- On each bar:
- Update trailing stop levels for existing long and short baskets.
- Check stop-loss and take-profit distances using candle highs and lows.
- Evaluate the floating profit of all open entries; close everything if it breaches the money-at-risk threshold.
- If trading is allowed, spread is acceptable, and the number of entries is below the limit, draw a random integer in
[0, 3]. - Open a new long when the random value is
1, or a new short when the value is2, using a random volume between the configured bounds.
- Trailing stops follow the best bid/ask once price moves by the activation distance, locking in profits if price retraces by the trailing gap.
- Risk management can work in fixed-money mode or as a percentage of the current account balance. When floating loss exceeds the configured amount, all positions are flattened immediately.
Parameters
| Name | Description |
|---|---|
MinVolume |
Lower bound for the randomly generated trade volume. |
MaxVolume |
Upper bound for the random trade volume. |
TakeProfitPips |
Target distance in pips, applied to the average entry price of the basket. |
StopLossPips |
Protective stop distance in pips, measured from the average entry price. |
TrailingStartPips |
Profit distance needed before the trailing logic activates. |
TrailingGapPips |
Gap maintained between the trailing stop and the best bid/ask price. |
MaxSpreadPips |
Maximum allowed spread before opening a new random entry. |
MaxOpenTrades |
Maximum number of simultaneous entries across both directions. |
RiskManagementMode |
Switches between fixed-money and balance-percentage risk handling. |
RiskAmount |
Amount of risk (currency or percentage) monitored against floating PnL. |
TradeComment |
Optional comment for bookkeeping, kept for compatibility with the source EA. |
Notes |
Informational text displayed inside the strategy status string. |
CandleType |
Candle series used for decision making. |
Differences from the MQL Version
- Trades are executed on the strategy's assigned instrument instead of randomly selecting symbols from the MetaTrader market watch. This keeps the implementation compatible with StockSharp's single-security strategies.
- Order management is performed on aggregated long/short baskets, mirroring how the original EA grouped positions with the same magic numbers.
- Spread control relies on the latest best bid/ask from the order book instead of MetaTrader's
MarketInfocalls.
Usage Notes
- Ensure that the connected broker or simulator provides both bid and ask quotes so that spread and trailing calculations remain accurate.
- Set
MinVolumeandMaxVolumewithin the instrument's allowed volume range. The strategy automatically snaps the random volume to the symbol's volume step and limits. - The risk management logic closes all trades immediately once the floating loss exceeds the configured threshold; no new positions are opened until the next candle.
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>
/// Randomized hedging strategy converted from the MetaTrader "RRS Tangled EA" advisor.
/// </summary>
public class RrsTangledEaStrategy : Strategy
{
/// <summary>
/// Risk handling modes that mirror the original MetaTrader inputs.
/// </summary>
public enum RiskModes
{
/// <summary>
/// Risk a fixed monetary amount.
/// </summary>
FixedMoney,
/// <summary>
/// Risk a percentage of the account balance.
/// </summary>
BalancePercentage,
}
private readonly StrategyParam<decimal> _minVolume;
private readonly StrategyParam<decimal> _maxVolume;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _trailingStartPips;
private readonly StrategyParam<decimal> _trailingGapPips;
private readonly StrategyParam<decimal> _maxSpreadPips;
private readonly StrategyParam<int> _maxOpenTrades;
private readonly StrategyParam<RiskModes> _riskMode;
private readonly StrategyParam<decimal> _riskAmount;
private readonly StrategyParam<string> _tradeComment;
private readonly StrategyParam<string> _notes;
private readonly StrategyParam<DataType> _candleType;
private readonly List<TradeEntry> _buyEntries = new();
private readonly List<TradeEntry> _sellEntries = new();
private int _tradeCounter;
private decimal _point;
private decimal? _buyTrailingStop;
private decimal? _sellTrailingStop;
private decimal? _lastSpread;
private decimal _initialBalance;
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public RrsTangledEaStrategy()
{
_minVolume = Param(nameof(MinVolume), 0.01m)
.SetDisplay("Minimum Volume", "Lower bound for random position sizing", "Money Management")
.SetGreaterThanZero();
_maxVolume = Param(nameof(MaxVolume), 0.50m)
.SetDisplay("Maximum Volume", "Upper bound for random position sizing", "Money Management")
.SetGreaterThanZero();
_takeProfitPips = Param(nameof(TakeProfitPips), 50000m)
.SetDisplay("Take Profit (pips)", "Distance in pips for profit targets", "Risk")
.SetRange(0m, 10_000m);
_stopLossPips = Param(nameof(StopLossPips), 50000m)
.SetDisplay("Stop Loss (pips)", "Distance in pips for protective stops", "Risk")
.SetRange(0m, 10_000m);
_trailingStartPips = Param(nameof(TrailingStartPips), 50000m)
.SetDisplay("Trailing Start (pips)", "Activation distance for the trailing logic", "Risk")
.SetRange(0m, 10_000m);
_trailingGapPips = Param(nameof(TrailingGapPips), 50m)
.SetDisplay("Trailing Gap (pips)", "Gap maintained by the trailing stop", "Risk")
.SetRange(0m, 10_000m);
_maxSpreadPips = Param(nameof(MaxSpreadPips), 100m)
.SetDisplay("Max Spread (pips)", "Maximum allowed spread before opening new trades", "Filters")
.SetRange(0m, 10_000m);
_maxOpenTrades = Param(nameof(MaxOpenTrades), 10)
.SetDisplay("Max Open Trades", "Maximum simultaneous random entries", "General")
.SetRange(1, 1000);
_riskMode = Param(nameof(RiskManagementMode), RiskModes.BalancePercentage)
.SetDisplay("Risk Mode", "Select fixed risk or balance percentage", "Risk");
_riskAmount = Param(nameof(RiskAmount), 5m)
.SetDisplay("Risk Amount", "Money risk (fixed or percentage)", "Risk")
.SetGreaterThanZero();
_tradeComment = Param(nameof(TradeComment), "RRS")
.SetDisplay("Trade Comment", "Comment stored with each order", "General");
_notes = Param(nameof(Notes), "Note For Your Reference")
.SetDisplay("Notes", "Informational note shown in the status string", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candle series used for processing", "Data");
}
/// <summary>
/// Minimum random volume.
/// </summary>
public decimal MinVolume
{
get => _minVolume.Value;
set => _minVolume.Value = value;
}
/// <summary>
/// Maximum random volume.
/// </summary>
public decimal MaxVolume
{
get => _maxVolume.Value;
set => _maxVolume.Value = value;
}
/// <summary>
/// Take-profit distance expressed in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Trailing start distance expressed in pips.
/// </summary>
public decimal TrailingStartPips
{
get => _trailingStartPips.Value;
set => _trailingStartPips.Value = value;
}
/// <summary>
/// Trailing gap distance expressed in pips.
/// </summary>
public decimal TrailingGapPips
{
get => _trailingGapPips.Value;
set => _trailingGapPips.Value = value;
}
/// <summary>
/// Maximum allowed spread in pips.
/// </summary>
public decimal MaxSpreadPips
{
get => _maxSpreadPips.Value;
set => _maxSpreadPips.Value = value;
}
/// <summary>
/// Maximum number of simultaneous open trades.
/// </summary>
public int MaxOpenTrades
{
get => _maxOpenTrades.Value;
set => _maxOpenTrades.Value = value;
}
/// <summary>
/// Risk handling mode.
/// </summary>
public RiskModes RiskManagementMode
{
get => _riskMode.Value;
set => _riskMode.Value = value;
}
/// <summary>
/// Risk amount (fixed money or percentage).
/// </summary>
public decimal RiskAmount
{
get => _riskAmount.Value;
set => _riskAmount.Value = value;
}
/// <summary>
/// Optional trade comment stored for reference.
/// </summary>
public string TradeComment
{
get => _tradeComment.Value;
set => _tradeComment.Value = value;
}
/// <summary>
/// Informational note displayed in the status string.
/// </summary>
public string Notes
{
get => _notes.Value;
set => _notes.Value = value;
}
/// <summary>
/// Candle data type used for processing.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_tradeCounter = 0;
_buyEntries.Clear();
_sellEntries.Clear();
_buyTrailingStop = null;
_sellTrailingStop = null;
_lastSpread = null;
_initialBalance = 0m;
_point = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_point = GetPointValue();
_initialBalance = GetCurrentBalance();
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
UpdateSpread();
UpdateTrailing(candle);
CheckStopsAndTargets(candle);
var price = candle.ClosePrice;
var floating = CalculateUnrealizedPnL(price);
var riskLimit = CalculateRiskLimit();
if (floating <= riskLimit && (_buyEntries.Count > 0 || _sellEntries.Count > 0))
{
CloseAllTrades();
LogInfo($"Risk management triggered. Floating={floating:F2} Threshold={riskLimit:F2}");
return;
}
UpdateStatus(price, floating);
if (!IsSpreadAcceptable())
return;
if (Position != 0m)
return;
if (_tradeCounter % 2 == 0)
{
OpenBuy(price);
}
else
{
OpenSell(price);
}
_tradeCounter++;
}
private void UpdateSpread()
{
var bid = GetSecurityValue<decimal?>(Level1Fields.BestBidPrice);
var ask = GetSecurityValue<decimal?>(Level1Fields.BestAskPrice);
if (bid.HasValue && ask.HasValue)
_lastSpread = ask.Value - bid.Value;
}
private void UpdateTrailing(ICandleMessage candle)
{
if (TrailingStartPips <= 0m || TrailingGapPips <= 0m)
{
_buyTrailingStop = null;
_sellTrailingStop = null;
return;
}
var bid = GetSecurityValue<decimal?>(Level1Fields.BestBidPrice) ?? candle.ClosePrice;
var ask = GetSecurityValue<decimal?>(Level1Fields.BestAskPrice) ?? candle.ClosePrice;
var startDistance = TrailingStartPips * _point;
var gapDistance = TrailingGapPips * _point;
if (_buyEntries.Count > 0)
{
var avgBuy = GetAveragePrice(_buyEntries);
if (bid - avgBuy >= startDistance)
{
var desiredStop = bid - gapDistance;
if (_buyTrailingStop == null || desiredStop > _buyTrailingStop.Value)
_buyTrailingStop = desiredStop;
if (_buyTrailingStop != null && bid <= _buyTrailingStop.Value)
CloseBuys();
}
}
else
{
_buyTrailingStop = null;
}
if (_sellEntries.Count > 0)
{
var avgSell = GetAveragePrice(_sellEntries);
if (avgSell - ask >= startDistance)
{
var desiredStop = ask + gapDistance;
if (_sellTrailingStop == null || desiredStop < _sellTrailingStop.Value)
_sellTrailingStop = desiredStop;
if (_sellTrailingStop != null && ask >= _sellTrailingStop.Value)
CloseSells();
}
}
else
{
_sellTrailingStop = null;
}
}
private void CheckStopsAndTargets(ICandleMessage candle)
{
var stopDistance = StopLossPips * _point;
var takeDistance = TakeProfitPips * _point;
if (_buyEntries.Count > 0)
{
var avgBuy = GetAveragePrice(_buyEntries);
if (StopLossPips > 0m && avgBuy - candle.LowPrice >= stopDistance)
{
CloseBuys();
}
else if (TakeProfitPips > 0m && candle.HighPrice - avgBuy >= takeDistance)
{
CloseBuys();
}
}
else
{
_buyTrailingStop = null;
}
if (_sellEntries.Count > 0)
{
var avgSell = GetAveragePrice(_sellEntries);
if (StopLossPips > 0m && candle.HighPrice - avgSell >= stopDistance)
{
CloseSells();
}
else if (TakeProfitPips > 0m && avgSell - candle.LowPrice >= takeDistance)
{
CloseSells();
}
}
else
{
_sellTrailingStop = null;
}
}
private void OpenBuy(decimal price)
{
var volume = Volume > 0m ? Volume : 1m;
BuyMarket(volume);
_buyEntries.Add(new TradeEntry(price, volume));
}
private void OpenSell(decimal price)
{
var volume = Volume > 0m ? Volume : 1m;
SellMarket(volume);
_sellEntries.Add(new TradeEntry(price, volume));
}
private bool IsSpreadAcceptable()
{
if (MaxSpreadPips <= 0m)
return true;
if (!_lastSpread.HasValue)
return true;
return _lastSpread.Value <= MaxSpreadPips * _point;
}
private void CloseAllTrades()
{
CloseBuys();
CloseSells();
}
private void CloseBuys()
{
var total = GetTotalVolume(_buyEntries);
if (total <= 0m)
return;
SellMarket(total);
_buyEntries.Clear();
_buyTrailingStop = null;
}
private void CloseSells()
{
var total = GetTotalVolume(_sellEntries);
if (total <= 0m)
return;
BuyMarket(total);
_sellEntries.Clear();
_sellTrailingStop = null;
}
private decimal CalculateUnrealizedPnL(decimal price)
{
if (_point <= 0m)
return 0m;
var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 1m;
decimal total = 0m;
for (var i = 0; i < _buyEntries.Count; i++)
{
var entry = _buyEntries[i];
var difference = price - entry.Price;
var steps = difference / _point;
total += steps * stepPrice * entry.Volume;
}
for (var i = 0; i < _sellEntries.Count; i++)
{
var entry = _sellEntries[i];
var difference = entry.Price - price;
var steps = difference / _point;
total += steps * stepPrice * entry.Volume;
}
return total;
}
private decimal CalculateRiskLimit()
{
var mode = RiskManagementMode;
var risk = Math.Abs(RiskAmount);
return mode switch
{
RiskModes.BalancePercentage => -GetCurrentBalance() * risk / 100m,
_ => -risk,
};
}
private decimal GetCurrentBalance()
{
var portfolio = Portfolio;
if ((portfolio?.CurrentValue ?? 0m) > 0m)
return portfolio.CurrentValue.Value;
if ((portfolio?.BeginValue ?? 0m) > 0m)
return portfolio.BeginValue.Value;
return _initialBalance;
}
private void UpdateStatus(decimal price, decimal floating)
{
var balance = GetCurrentBalance();
var modeDescription = RiskManagementMode == RiskModes.BalancePercentage
? $"Balance % ({RiskAmount:F2})"
: $"Fixed ({RiskAmount:F2})";
var spreadText = _lastSpread.HasValue ? (_lastSpread.Value / _point).ToString("F2") : "n/a";
LogInfo($"Balance={balance:F2} FloatingPnL={floating:F2} Trades(Buy={_buyEntries.Count}, Sell={_sellEntries.Count}) " +
$"Risk={modeDescription} Spread(pips)={spreadText} Notes={Notes}");
}
private decimal GetPointValue()
{
var point = Security?.PriceStep;
if (point == null || point == 0m)
return 0.0001m;
return point.Value;
}
private static decimal GetTotalVolume(List<TradeEntry> entries)
{
decimal total = 0m;
for (var i = 0; i < entries.Count; i++)
total += entries[i].Volume;
return total;
}
private static decimal GetAveragePrice(List<TradeEntry> entries)
{
decimal volume = 0m;
decimal weighted = 0m;
for (var i = 0; i < entries.Count; i++)
{
var entry = entries[i];
volume += entry.Volume;
weighted += entry.Price * entry.Volume;
}
return volume > 0m ? weighted / volume : 0m;
}
private readonly struct TradeEntry
{
public TradeEntry(decimal price, decimal volume)
{
Price = price;
Volume = volume;
}
public decimal Price { get; }
public decimal Volume { get; }
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class rrs_tangled_ea_strategy(Strategy):
"""Randomized hedging strategy - alternates buy/sell on each candle with trailing stop management."""
def __init__(self):
super(rrs_tangled_ea_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candle series used for processing", "Data")
self._take_profit_pips = self.Param("TakeProfitPips", 50000.0) \
.SetDisplay("Take Profit (pips)", "Distance in pips for profit targets", "Risk")
self._stop_loss_pips = self.Param("StopLossPips", 50000.0) \
.SetDisplay("Stop Loss (pips)", "Distance in pips for protective stops", "Risk")
self._trailing_start_pips = self.Param("TrailingStartPips", 50000.0) \
.SetDisplay("Trailing Start (pips)", "Activation distance for trailing", "Risk")
self._trailing_gap_pips = self.Param("TrailingGapPips", 50.0) \
.SetDisplay("Trailing Gap (pips)", "Gap maintained by the trailing stop", "Risk")
self._buy_entries = []
self._sell_entries = []
self._trade_counter = 0
self._point = 0.0
self._buy_trailing_stop = None
self._sell_trailing_stop = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TrailingStartPips(self):
return self._trailing_start_pips.Value
@property
def TrailingGapPips(self):
return self._trailing_gap_pips.Value
def OnReseted(self):
super(rrs_tangled_ea_strategy, self).OnReseted()
self._trade_counter = 0
self._buy_entries = []
self._sell_entries = []
self._buy_trailing_stop = None
self._sell_trailing_stop = None
self._point = 0.0
def OnStarted2(self, time):
super(rrs_tangled_ea_strategy, self).OnStarted2(time)
self._point = self._get_point_value()
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _get_point_value(self):
if self.Security is not None and self.Security.PriceStep is not None:
p = float(self.Security.PriceStep)
if p > 0:
return p
return 0.0001
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
price = float(candle.ClosePrice)
self._update_trailing(candle)
self._check_stops_and_targets(candle)
if self.Position != 0:
return
if self._trade_counter % 2 == 0:
volume = float(self.Volume) if self.Volume > 0 else 1.0
self.BuyMarket(volume)
self._buy_entries.append([price, volume])
else:
volume = float(self.Volume) if self.Volume > 0 else 1.0
self.SellMarket(volume)
self._sell_entries.append([price, volume])
self._trade_counter += 1
def _update_trailing(self, candle):
if self.TrailingStartPips <= 0 or self.TrailingGapPips <= 0:
self._buy_trailing_stop = None
self._sell_trailing_stop = None
return
bid = float(candle.ClosePrice)
ask = float(candle.ClosePrice)
start_dist = float(self.TrailingStartPips) * self._point
gap_dist = float(self.TrailingGapPips) * self._point
if len(self._buy_entries) > 0:
avg_buy = self._get_avg_price(self._buy_entries)
if bid - avg_buy >= start_dist:
desired = bid - gap_dist
if self._buy_trailing_stop is None or desired > self._buy_trailing_stop:
self._buy_trailing_stop = desired
if self._buy_trailing_stop is not None and bid <= self._buy_trailing_stop:
self._close_buys()
else:
self._buy_trailing_stop = None
if len(self._sell_entries) > 0:
avg_sell = self._get_avg_price(self._sell_entries)
if avg_sell - ask >= start_dist:
desired = ask + gap_dist
if self._sell_trailing_stop is None or desired < self._sell_trailing_stop:
self._sell_trailing_stop = desired
if self._sell_trailing_stop is not None and ask >= self._sell_trailing_stop:
self._close_sells()
else:
self._sell_trailing_stop = None
def _check_stops_and_targets(self, candle):
stop_dist = float(self.StopLossPips) * self._point
take_dist = float(self.TakeProfitPips) * self._point
if len(self._buy_entries) > 0:
avg_buy = self._get_avg_price(self._buy_entries)
if self.StopLossPips > 0 and avg_buy - float(candle.LowPrice) >= stop_dist:
self._close_buys()
elif self.TakeProfitPips > 0 and float(candle.HighPrice) - avg_buy >= take_dist:
self._close_buys()
if len(self._sell_entries) > 0:
avg_sell = self._get_avg_price(self._sell_entries)
if self.StopLossPips > 0 and float(candle.HighPrice) - avg_sell >= stop_dist:
self._close_sells()
elif self.TakeProfitPips > 0 and avg_sell - float(candle.LowPrice) >= take_dist:
self._close_sells()
def _close_buys(self):
total = sum(e[1] for e in self._buy_entries)
if total > 0:
self.SellMarket(total)
self._buy_entries = []
self._buy_trailing_stop = None
def _close_sells(self):
total = sum(e[1] for e in self._sell_entries)
if total > 0:
self.BuyMarket(total)
self._sell_entries = []
self._sell_trailing_stop = None
@staticmethod
def _get_avg_price(entries):
total_vol = 0.0
weighted = 0.0
for e in entries:
weighted += e[0] * e[1]
total_vol += e[1]
return weighted / total_vol if total_vol > 0 else 0.0
def CreateClone(self):
return rrs_tangled_ea_strategy()