Стратегия RRS Tangled EA
Обзор
RRS Tangled EA — порт MetaTrader 4 советника на платформу StockSharp. Оригинальная версия случайным образом выбирала направление и торговый инструмент, ограничивала количество одновременно открытых сделок и защищала плавающий результат при помощи трейлинг-стопа и управления риском. Перенос использует текущий инструмент стратегии и полностью реализует случайные входы, трейлинг и контроль капитала через высокоуровневый API StockSharp.
Алгоритм работы
- Стратегия подписывается на выбранную серию свечей и обрабатывает только закрытые бары.
- Для каждой свечи выполняются шаги:
- Обновление уровней трейлинг-стопа для действующих длинных и коротких корзин.
- Проверка условия срабатывания стоп-лосса и тейк-профита по максимуму и минимуму свечи.
- Расчет текущего плавающего результата; если убыток превышает допустимое значение, все позиции закрываются.
- При разрешенной торговле, допустимом спреде и свободном лимите сделок генерируется случайное число
[0, 3]. - Значение
1инициирует покупку,2— продажу. Объем выбирается случайно в заданных границах и корректируется под шаг объема инструмента.
- Когда цена проходит активационную дистанцию, трейлинг-стоп подтягивается к лучшему бид/аск и фиксирует прибыль при откате на величину зазора.
- Управление риском работает либо в режиме фиксированной суммы, либо как процент от текущего баланса. После превышения лимита убытка стратегия немедленно закрывает все позиции.
Параметры
| Имя | Описание |
|---|---|
MinVolume |
Нижняя граница случайного объема сделки. |
MaxVolume |
Верхняя граница случайного объема. |
TakeProfitPips |
Расстояние тейк-профита в пунктах относительно средней цены корзины. |
StopLossPips |
Расстояние стоп-лосса в пунктах от средней цены входа. |
TrailingStartPips |
Прибыль, после которой активируется трейлинг-стоп. |
TrailingGapPips |
Отступ между трейлинг-стопом и лучшим бид/аск. |
MaxSpreadPips |
Максимальный допустимый спред для открытия новой сделки. |
MaxOpenTrades |
Лимит одновременно открытых случайных ордеров. |
RiskManagementMode |
Режим управления риском: фиксированная сумма или процент от баланса. |
RiskAmount |
Допустимый уровень плавающего убытка (в деньгах или процентах). |
TradeComment |
Комментарий к сделкам, оставлен для совместимости с оригиналом. |
Notes |
Произвольная заметка, отображается в статусе стратегии. |
CandleType |
Тип свечей, используемых для расчетов. |
Отличия от версии MQL
- Торговля ведется по инструменту, назначенному стратегии, без случайного выбора тикера из Market Watch.
- Управление ордерами ведется корзинами покупок и продаж, что соответствует раздельным magic number в MetaTrader.
- Контроль спреда использует последние котировки bid/ask вместо вызовов
MarketInfo.
Рекомендации по использованию
- Необходим источник данных с котировками bid и ask, чтобы корректно рассчитывать спред и трейлинг.
- Подбирайте
MinVolumeиMaxVolumeв пределах допустимого диапазона инструмента — стратегия сама подгонит объем под шаг. - При срабатывании риск-менеджмента все позиции закрываются и новые сделки не открываются до следующей свечи.
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()