趋势 RDS 策略
概述
趋势 RDS 最初是 MetaTrader 平台上的一个会话型反转策略。它会在指定的开盘时间开始扫描最近的 100 根已收盘 K 线,寻找连续三根同向动量结构,并逆向建仓。移植到 StockSharp 后保留了原始的资金管理功能,包括可选的信号反向、固定止损与止盈、保本移动以及带步长的追踪止损。
交易逻辑
- 信号窗口:到达
Start Time后,策略检查最多 100 根最近收盘的 K 线。 - 形态识别:寻找第一个满足以下任一条件的三根 K 线组合:
- 高点连续上移且低点连续上移(
High[n] < High[n+1] < High[n+2]且Low[n] > Low[n+1] > Low[n+2])。 - 高点连续下移且低点连续下移(
High[n] > High[n+1] > High[n+2]且Low[n] < Low[n+1] < Low[n+2])。 如果同时出现高点上移和低点下移(内外包结构),则视为冲突信号并忽略。当Reverse Signals为真时,买卖方向会被反向处理。
- 高点连续上移且低点连续上移(
- 入场:若当前没有持仓,则按
Trade Volume下市价单;如持有反向仓位,会先平仓再重新判断信号。 - 强制平仓窗口:
Close Time起的 15 分钟内,若还有持仓将被全部平仓。 - 风控组件:
- 按参数下达止损与止盈委托,可在成交量变化时自动刷新。
- 达到
Break-Even (pips)所设阈值后,止损上移至开仓价实现保本。 - 追踪止损始终保持
Trailing Stop (pips)的距离,只有当价格继续移动超过Trailing Step (pips)时才会再次推进。
参数说明
| 名称 | 说明 |
|---|---|
| Trade Volume | 每次下单的合约或手数。 |
| Stop Loss (pips) | 止损距离,设为 0 表示关闭。 |
| Take Profit (pips) | 止盈距离,设为 0 表示关闭。 |
| Start Time | 开始扫描形态的时间(交易所时间)。 |
| Close Time | 强制平仓时间(交易所时间),策略会在之后 15 分钟内平掉仓位。 |
| Reverse Signals | 反向处理买卖信号。 |
| Trailing Stop (pips) | 追踪止损的基础距离,为 0 则不启用。 |
| Trailing Step (pips) | 追踪止损再次推进所需的额外价格移动。 |
| Break-Even (pips) | 触发保本移动的利润阈值,为 0 则关闭功能。 |
| Candle Type | 用于分析的蜡烛图类型。 |
使用建议
- 策略基于合约的
PriceStep或MinPriceStep计算点值,请确保交易品种提供正确的最小跳动。 - 仅处理已收盘的 K 线,因此每个时间框架每天最多触发一次信号。
- 当仓位数量发生变化时,止损/止盈委托会同步调整,确保保护水平一致。
- 追踪止损与保本逻辑只在存在有效入场价时生效。
文件结构
CS/TrendRdsStrategy.cs:策略的 C# 实现。README.md:英文说明。README_ru.md:俄文说明。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Port of the MetaTrader Trend_RDS expert advisor.
/// Detects three-bar momentum patterns and reverses into the move.
/// Includes configurable stop-loss, take-profit, and trailing management.
/// </summary>
public class TrendRdsReversalStrategy : Strategy
{
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<bool> _reverseSignals;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _maxPatternDepth;
private readonly List<(decimal High, decimal Low)> _recentExtremes = new();
/// <summary>
/// Trading volume for market entries.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Stop-loss distance in absolute price units.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance in absolute price units.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Inverts the buy and sell conditions when enabled.
/// </summary>
public bool ReverseSignals
{
get => _reverseSignals.Value;
set => _reverseSignals.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Maximum number of swings tracked when validating the pattern.
/// </summary>
public int MaxPatternDepth
{
get => _maxPatternDepth.Value;
set => _maxPatternDepth.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="TrendRdsReversalStrategy"/> class.
/// </summary>
public TrendRdsReversalStrategy()
{
_tradeVolume = Param(nameof(TradeVolume), 1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Market order volume", "General");
_stopLossPips = Param(nameof(StopLossPips), 500m)
.SetDisplay("Stop Loss", "Stop-loss distance", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 500m)
.SetDisplay("Take Profit", "Take-profit distance", "Risk");
_reverseSignals = Param(nameof(ReverseSignals), false)
.SetDisplay("Reverse Signals", "Invert buy and sell signals", "Filters");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(8).TimeFrame())
.SetDisplay("Candle Type", "Working timeframe", "General");
_maxPatternDepth = Param(nameof(MaxPatternDepth), 10)
.SetGreaterThanZero()
.SetDisplay("Max Pattern Depth", "Maximum candles tracked for pattern detection", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return new[] { (Security, CandleType) };
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_recentExtremes.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
// Use a dummy EMA to ensure candle callbacks fire in the backtester
var ema = new EMA { Length = 5 };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ema, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, ema);
DrawOwnTrades(area);
}
// Use StartProtection for SL/TP
var tp = TakeProfitPips > 0 ? new Unit(TakeProfitPips, UnitTypes.Absolute) : null;
var sl = StopLossPips > 0 ? new Unit(StopLossPips, UnitTypes.Absolute) : null;
StartProtection(tp, sl);
base.OnStarted2(time);
}
private void ProcessCandle(ICandleMessage candle, decimal emaValue)
{
if (candle.State != CandleStates.Finished)
return;
// Track recent highs and lows
_recentExtremes.Insert(0, (candle.HighPrice, candle.LowPrice));
if (_recentExtremes.Count > MaxPatternDepth + 2)
_recentExtremes.RemoveAt(_recentExtremes.Count - 1);
if (!IsFormedAndOnlineAndAllowTrading())
return;
// Need at least 3 bars for the pattern
if (_recentExtremes.Count < 3)
return;
var (buySignal, sellSignal) = DetectSignals();
if (buySignal)
{
if (Position < 0)
BuyMarket(Math.Abs(Position));
if (Position <= 0)
BuyMarket(Volume);
}
else if (sellSignal)
{
if (Position > 0)
SellMarket(Position);
if (Position >= 0)
SellMarket(Volume);
}
}
private (bool Buy, bool Sell) DetectSignals()
{
var depth = Math.Min(_recentExtremes.Count - 2, MaxPatternDepth);
if (depth <= 0)
return (false, false);
for (var index = 0; index < depth; index++)
{
if (index + 2 >= _recentExtremes.Count)
break;
var first = _recentExtremes[index];
var second = _recentExtremes[index + 1];
var third = _recentExtremes[index + 2];
// Conflict: both highs and lows rising simultaneously
var conflict = first.High < second.High && second.High < third.High &&
first.Low > second.Low && second.Low > third.Low;
// Rising lows pattern -> buy
if (!conflict && first.Low > second.Low && second.Low > third.Low)
{
return ReverseSignals ? (false, true) : (true, false);
}
// Rising highs pattern -> sell
if (!conflict && first.High < second.High && second.High < third.High)
{
return ReverseSignals ? (true, false) : (false, true);
}
}
return (false, false);
}
}
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, UnitTypes, Unit
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class trend_rds_reversal_strategy(Strategy):
"""Three-bar momentum reversal with EMA filter and StartProtection SL/TP."""
def __init__(self):
super(trend_rds_reversal_strategy, self).__init__()
self._sl = self.Param("StopLoss", 500.0).SetDisplay("Stop Loss", "Stop-loss distance", "Risk")
self._tp = self.Param("TakeProfit", 500.0).SetDisplay("Take Profit", "Take-profit distance", "Risk")
self._depth = self.Param("MaxPatternDepth", 10).SetGreaterThanZero().SetDisplay("Pattern Depth", "Max candles for pattern", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(8))).SetDisplay("Candle Type", "Timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(trend_rds_reversal_strategy, self).OnReseted()
self._extremes = []
def OnStarted2(self, time):
super(trend_rds_reversal_strategy, self).OnStarted2(time)
self._extremes = []
ema = ExponentialMovingAverage()
ema.Length = 5
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(ema, self.OnProcess).Start()
tp_val = float(self._tp.Value)
sl_val = float(self._sl.Value)
tp = Unit(tp_val, UnitTypes.Absolute) if tp_val > 0 else None
sl = Unit(sl_val, UnitTypes.Absolute) if sl_val > 0 else None
self.StartProtection(tp, sl)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawIndicator(area, ema)
self.DrawOwnTrades(area)
def OnProcess(self, candle, ema_val):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
depth = self._depth.Value
self._extremes.insert(0, (high, low))
if len(self._extremes) > depth + 2:
self._extremes.pop()
if not self.IsFormedAndOnlineAndAllowTrading():
return
if len(self._extremes) < 3:
return
buy_signal, sell_signal = self._detect_signals()
if buy_signal:
if self.Position < 0:
self.BuyMarket(abs(self.Position))
if self.Position <= 0:
self.BuyMarket(self.Volume)
elif sell_signal:
if self.Position > 0:
self.SellMarket(self.Position)
if self.Position >= 0:
self.SellMarket(self.Volume)
def _detect_signals(self):
depth = min(len(self._extremes) - 2, self._depth.Value)
if depth <= 0:
return False, False
for i in range(depth):
if i + 2 >= len(self._extremes):
break
first = self._extremes[i]
second = self._extremes[i + 1]
third = self._extremes[i + 2]
conflict = (first[0] < second[0] and second[0] < third[0] and
first[1] > second[1] and second[1] > third[1])
# Rising lows -> buy
if not conflict and first[1] > second[1] and second[1] > third[1]:
return True, False
# Rising highs -> sell
if not conflict and first[0] < second[0] and second[0] < third[0]:
return False, True
return False, False
def CreateClone(self):
return trend_rds_reversal_strategy()