RRS Tangled EA 策略
概述
RRS Tangled EA 策略 是 MetaTrader 4 专家顾问的 StockSharp 移植版本。原始程序会随机选择交易方向与品种,并限制同时持有的订单数量,同时通过移动止损和资金风险控制来保护浮动收益。移植版在当前选定的品种上复现这一行为,利用 StockSharp 的高级 API 完成随机进场、跟踪止损和资金管理。
运行流程
- 订阅设定的 K 线类型,仅处理收盘完成的 K 线。
- 每根 K 线到来时:
- 根据最新的买卖价更新多头、空头持仓的跟踪止损位置。
- 通过当根最高价/最低价检查止损和止盈是否被触发。
- 计算所有持仓的浮动盈亏,当亏损超过可承受金额时立即清仓。
- 在允许交易、点差符合要求且持仓数量未达到上限的情况下,生成
[0, 3]的随机整数;值为1时开多,值为2时开空,手数从设定区间内随机选择。
- 当价格走出激活距离后,跟踪止损会贴着买价或卖价推进,一旦价格回撤超出设定间距即锁定利润。
- 风险控制支持固定金额或按账户余额百分比两种模式,只要浮亏超过阈值就平掉全部仓位。
参数说明
| 名称 | 说明 |
|---|---|
MinVolume |
随机手数的下限。 |
MaxVolume |
随机手数的上限。 |
TakeProfitPips |
相对平均持仓价的止盈距离(点)。 |
StopLossPips |
相对平均持仓价的止损距离(点)。 |
TrailingStartPips |
启动跟踪止损所需的盈利距离。 |
TrailingGapPips |
跟踪止损与最佳买/卖价之间保持的间隔。 |
MaxSpreadPips |
开仓前允许的最大点差。 |
MaxOpenTrades |
同时持有的最大随机订单数。 |
RiskManagementMode |
固定金额或余额百分比风险模式。 |
RiskAmount |
监控浮亏的金额或百分比阈值。 |
TradeComment |
可选的订单备注,与原 EA 保持一致。 |
Notes |
显示在策略状态字符串中的备注信息。 |
CandleType |
用于分析的 K 线类型。 |
与 MQL 版本的差异
- 由于 StockSharp 策略通常针对单一品种,移植版不再随机切换交易品种,而是使用策略绑定的证券。
- 持仓按照多头和空头篮子聚合管理,对应原 EA 使用不同 magic number 的做法。
- 点差限制使用最新的买卖价计算,替代 MetaTrader 的
MarketInfo查询。
使用提示
- 连接的撮合或仿真环境应提供买价和卖价,以保证点差和跟踪止损计算准确。
- 请确保
MinVolume与MaxVolume位于交易品种允许的手数范围内,策略会自动按照最小变动手数对齐。 - 一旦触发风险控制,策略会立即清空所有持仓,并在下一根 K 线前不再开仓。
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()