BB RSI Trailing Stop
ボリンジャーバンド逸脱後、RSI が極端域から戻った場合のみ新規建てし、条件付きトレーリングストップを使います。p は直前の指標形成済み足、RSI は Wilder 平滑です。
取引ルール
買い:Low < lowerBand かつ RSI[p] <= RsiOversold && RSI > RsiOversold。売り:High > upperBand かつ RSI[p] >= RsiOverbought && RSI < RsiOverbought。極端域に留まるだけでは不十分です。ノーポジション時にのみ Volume で発注し、隠れた待機期間はありません。
加重平均実約定価格が基準で、買いは執行可能な bid、売りは ask を使います。初期距離は StopLossPoints。有利な気配値極値が TrailOffsetPoints 進んだ後に追従を開始し、極値から TrailStopPoints 離して初期ストップを緩めず更新します。気配値到達で実保有数量を全て成行決済します。距離は価格単位で、ティックや百分率ではありません。足の過去の高安を、その後に作ったストップ判定へ遡及利用しません。
指標のウォームアップ完了後、確定足だけで新規シグナルを生成します。保有ポジションは決済まで管理し、未完了の成行注文がある間は重複発注しません。
ボリンジャーバンドは現在の確定足を含む終値の移動窓から母標準偏差を計算します。
パラメーター
| Parameter | Default |
|---|---|
BollingerPeriod |
25 |
BollingerDeviation |
2 |
RsiPeriod |
14 |
RsiOversold |
33 |
RsiOverbought |
60 |
StopLossPoints |
50 |
TrailOffsetPoints |
99 |
TrailStopPoints |
40 |
CandleType |
15m |
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>Bollinger excursions with RSI recovery and fill-anchored conditional quote trailing.</summary>
public class BbRsiTrailingStopStrategy : Strategy
{
private readonly StrategyParam<int> _bollingerPeriod;
private readonly StrategyParam<decimal> _bollingerDeviation;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<decimal> _rsiOverbought;
private readonly StrategyParam<decimal> _rsiOversold;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _trailOffsetPoints;
private readonly StrategyParam<decimal> _trailStopPoints;
private readonly StrategyParam<DataType> _candleType;
private Order _pending;
private decimal? _previousRsi;
private decimal _entryCost, _entryQuantity, _extreme, _stop;
private decimal _initialDistance, _activationDistance, _trailingDistance;
private bool _trailing;
/// <summary>Initializes the published signal and risk parameters.</summary>
public BbRsiTrailingStopStrategy()
{
_bollingerPeriod = Param(nameof(BollingerPeriod), 25).SetGreaterThanZero().SetOptimize(10, 50, 5)
.SetDisplay("BollingerPeriod", "Bollinger close window", "Parameters");
_bollingerDeviation = Param(nameof(BollingerDeviation), 2m).SetGreaterThanZero().SetOptimize(1m, 3m, 0.5m)
.SetDisplay("BollingerDeviation", "Bollinger standard deviation multiplier", "Parameters");
_rsiPeriod = Param(nameof(RsiPeriod), 14).SetGreaterThanZero().SetOptimize(7, 21, 7)
.SetDisplay("RsiPeriod", "Wilder RSI window", "Parameters");
_rsiOverbought = Param(nameof(RsiOverbought), 60m).SetGreaterThanZero().SetOptimize(50m, 80m, 5m)
.SetDisplay("RsiOverbought", "RSI upper recovery threshold", "Parameters");
_rsiOversold = Param(nameof(RsiOversold), 33m).SetGreaterThanZero().SetOptimize(20m, 40m, 5m)
.SetDisplay("RsiOversold", "RSI lower recovery threshold", "Parameters");
_stopLossPoints = Param(nameof(StopLossPoints), 50m).SetGreaterThanZero().SetOptimize(20m, 100m, 10m)
.SetDisplay("StopLossPoints", "Initial stop distance in price units", "Parameters");
_trailOffsetPoints = Param(nameof(TrailOffsetPoints), 99m).SetGreaterThanZero().SetOptimize(50m, 150m, 10m)
.SetDisplay("TrailOffsetPoints", "Favorable price movement required to activate trailing", "Parameters");
_trailStopPoints = Param(nameof(TrailStopPoints), 40m).SetGreaterThanZero().SetOptimize(20m, 80m, 10m)
.SetDisplay("TrailStopPoints", "Trailing distance in price units", "Parameters");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
.SetDisplay("CandleType", "Finished signal candle interval", "Parameters");
}
/// <summary>Bollinger close window.</summary>
public int BollingerPeriod
{
get => _bollingerPeriod.Value;
set => _bollingerPeriod.Value = value;
}
/// <summary>Bollinger standard deviation multiplier.</summary>
public decimal BollingerDeviation
{
get => _bollingerDeviation.Value;
set => _bollingerDeviation.Value = value;
}
/// <summary>Wilder RSI window.</summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>RSI upper recovery threshold.</summary>
public decimal RsiOverbought
{
get => _rsiOverbought.Value;
set => _rsiOverbought.Value = value;
}
/// <summary>RSI lower recovery threshold.</summary>
public decimal RsiOversold
{
get => _rsiOversold.Value;
set => _rsiOversold.Value = value;
}
/// <summary>Initial stop distance in price units.</summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>Favorable price movement required to activate trailing.</summary>
public decimal TrailOffsetPoints
{
get => _trailOffsetPoints.Value;
set => _trailOffsetPoints.Value = value;
}
/// <summary>Trailing distance in price units.</summary>
public decimal TrailStopPoints
{
get => _trailStopPoints.Value;
set => _trailStopPoints.Value = value;
}
/// <summary>Finished signal candle interval.</summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
private bool IsPending => _pending is not null && _pending.State is not (OrderStates.Done or OrderStates.Failed);
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType), (Security, DataType.Level1)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pending = null;
_previousRsi = null;
_entryCost = _entryQuantity = _extreme = _stop = 0m;
_initialDistance = _activationDistance = _trailingDistance = 0m;
_trailing = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
if (RsiOversold >= RsiOverbought || RsiOverbought >= 100m)
throw new InvalidOperationException("RSI thresholds must satisfy 0 < oversold < overbought < 100.");
base.OnStarted2(time);
foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
{
var quote = new Subscription(DataType.Level1, Security);
quote.MarketData.BuildField = field;
SubscribeLevel1(quote).Bind(ProcessQuote).Start();
}
var bb = new BollingerBands { Length = BollingerPeriod, Width = BollingerDeviation };
var rsi = new RelativeStrengthIndex { Length = RsiPeriod };
var candles = SubscribeCandles(CandleType);
candles.BindEx([bb, rsi], ProcessCandle).Start();
var area = CreateChartArea();
if (area is not null)
{
DrawCandles(area, candles);
DrawIndicator(area, bb);
DrawIndicator(area, rsi);
DrawOwnTrades(area);
}
}
private void Submit(Sides side, decimal quantity, string reason)
{
_pending = CreateOrder(side, 0m, quantity);
_pending.Type = OrderTypes.Market;
_pending.Comment = "0551" + reason;
RegisterOrder(_pending);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue[] values)
{
if (candle.State != CandleStates.Finished || !values[0].IsFormed || !values[1].IsFormed
|| values[0] is not BollingerBandsValue bb || bb.LowBand is not decimal lower || bb.UpBand is not decimal upper)
return;
var rsi = values[1].GetValue<decimal>();
var previous = _previousRsi;
_previousRsi = rsi;
if (previous is null || Position != 0m || IsPending || !IsFormedAndOnlineAndAllowTrading())
return;
Sides? side = candle.LowPrice < lower && previous <= RsiOversold && rsi > RsiOversold ? Sides.Buy
: candle.HighPrice > upper && previous >= RsiOverbought && rsi < RsiOverbought ? Sides.Sell : null;
if (side is null)
return;
_entryCost = _entryQuantity = 0m;
_trailing = false;
_initialDistance = StopLossPoints;
_activationDistance = TrailOffsetPoints;
_trailingDistance = TrailStopPoints;
Submit(side.Value, Volume, "Entry");
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade.Order.Comment == "0551Entry")
{
_entryCost += trade.Trade.Price * trade.Trade.Volume;
_entryQuantity += trade.Trade.Volume;
var entry = _entryCost / _entryQuantity;
_extreme = entry;
_stop = trade.Order.Side == Sides.Buy ? entry - _initialDistance : entry + _initialDistance;
}
}
private void ProcessQuote(Level1ChangeMessage quote)
{
if (Position == 0m || _entryQuantity == 0m || IsPending || !IsFormedAndOnlineAndAllowTrading())
return;
var field = Position > 0m ? Level1Fields.BestBidPrice : Level1Fields.BestAskPrice;
if (!quote.Changes.TryGetValue(field, out var raw) || raw is not decimal price || price <= 0m)
return;
var entry = _entryCost / _entryQuantity;
_extreme = Position > 0m ? Math.Max(_extreme, price) : Math.Min(_extreme, price);
if (Position > 0m ? _extreme - entry >= _activationDistance : entry - _extreme >= _activationDistance)
_trailing = true;
if (_trailing)
_stop = Position > 0m ? Math.Max(_stop, _extreme - _trailingDistance) : Math.Min(_stop, _extreme + _trailingDistance);
if (Position > 0m ? price <= _stop : price >= _stop)
Submit(Position > 0m ? Sides.Sell : Sides.Buy, Math.Abs(Position), "Stop");
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import Decimal, Math, TimeSpan, Convert, InvalidOperationException
from StockSharp.Algo.Indicators import BollingerBands, RelativeStrengthIndex, ExponentialMovingAverage, AverageTrueRange, DecimalIndicatorValue
from StockSharp.Algo.Strategies import Strategy
from StockSharp.BusinessEntities import Subscription
from StockSharp.Messages import DataType, CandleStates, Level1Fields, OrderStates, OrderTypes, Sides, Unit, UnitTypes, ITickTradeMessage
class bb_rsi_trailing_stop_strategy(Strategy):
"""Bollinger excursions, RSI recovery, and executable-quote conditional trailing."""
def __init__(self):
super(bb_rsi_trailing_stop_strategy, self).__init__()
self._bollingerPeriod = self.Param("BollingerPeriod", 25).SetGreaterThanZero() \
.SetDisplay("BollingerPeriod", "Bollinger close window", "Parameters")
self._bollingerDeviation = self.Param("BollingerDeviation", Decimal(2)).SetGreaterThanZero() \
.SetDisplay("BollingerDeviation", "Bollinger standard deviation multiplier", "Parameters")
self._rsiPeriod = self.Param("RsiPeriod", 14).SetGreaterThanZero() \
.SetDisplay("RsiPeriod", "Wilder RSI window", "Parameters")
self._rsiOverbought = self.Param("RsiOverbought", Decimal(60)).SetGreaterThanZero() \
.SetDisplay("RsiOverbought", "RSI upper recovery threshold", "Parameters")
self._rsiOversold = self.Param("RsiOversold", Decimal(33)).SetGreaterThanZero() \
.SetDisplay("RsiOversold", "RSI lower recovery threshold", "Parameters")
self._stopLossPoints = self.Param("StopLossPoints", Decimal(50)).SetGreaterThanZero() \
.SetDisplay("StopLossPoints", "Initial stop distance in price units", "Parameters")
self._trailOffsetPoints = self.Param("TrailOffsetPoints", Decimal(99)).SetGreaterThanZero() \
.SetDisplay("TrailOffsetPoints", "Favorable price movement required to activate trailing", "Parameters")
self._trailStopPoints = self.Param("TrailStopPoints", Decimal(40)).SetGreaterThanZero() \
.SetDisplay("TrailStopPoints", "Trailing distance in price units", "Parameters")
self._candleType = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(15))) \
.SetDisplay("CandleType", "Finished signal candle interval", "Parameters")
self._clear_state()
@property
def BollingerPeriod(self):
return self._bollingerPeriod.Value
@BollingerPeriod.setter
def BollingerPeriod(self, value):
self._bollingerPeriod.Value = value
@property
def BollingerDeviation(self):
return self._bollingerDeviation.Value
@BollingerDeviation.setter
def BollingerDeviation(self, value):
self._bollingerDeviation.Value = value
@property
def RsiPeriod(self):
return self._rsiPeriod.Value
@RsiPeriod.setter
def RsiPeriod(self, value):
self._rsiPeriod.Value = value
@property
def RsiOverbought(self):
return self._rsiOverbought.Value
@RsiOverbought.setter
def RsiOverbought(self, value):
self._rsiOverbought.Value = value
@property
def RsiOversold(self):
return self._rsiOversold.Value
@RsiOversold.setter
def RsiOversold(self, value):
self._rsiOversold.Value = value
@property
def StopLossPoints(self):
return self._stopLossPoints.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stopLossPoints.Value = value
@property
def TrailOffsetPoints(self):
return self._trailOffsetPoints.Value
@TrailOffsetPoints.setter
def TrailOffsetPoints(self, value):
self._trailOffsetPoints.Value = value
@property
def TrailStopPoints(self):
return self._trailStopPoints.Value
@TrailStopPoints.setter
def TrailStopPoints(self, value):
self._trailStopPoints.Value = value
@property
def CandleType(self):
return self._candleType.Value
@CandleType.setter
def CandleType(self, value):
self._candleType.Value = value
def _clear_state(self):
self._pending = None
self._previous_rsi = None
self._entry_cost = Decimal(0)
self._entry_quantity = Decimal(0)
self._extreme = Decimal(0)
self._stop = Decimal(0)
self._trailing = False
def OnReseted(self):
super(bb_rsi_trailing_stop_strategy, self).OnReseted()
self._clear_state()
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType), (self.Security, DataType.Level1)]
def OnStarted2(self, time):
if self.RsiOversold >= self.RsiOverbought or self.RsiOverbought >= 100:
raise InvalidOperationException("RSI thresholds must satisfy 0 < oversold < overbought < 100.")
super(bb_rsi_trailing_stop_strategy, self).OnStarted2(time)
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quote = Subscription(DataType.Level1, self.Security)
quote.MarketData.BuildField = field
self.SubscribeLevel1(quote).Bind(self._process_quote).Start()
self._bb = BollingerBands()
self._bb.Length = self.BollingerPeriod
self._bb.Width = self.BollingerDeviation
self._rsi = RelativeStrengthIndex()
self._rsi.Length = self.RsiPeriod
candles = self.SubscribeCandles(self.CandleType)
candles.BindEx(self._bb, self._rsi, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, candles)
self.DrawIndicator(area, self._bb)
self.DrawIndicator(area, self._rsi)
self.DrawOwnTrades(area)
def _is_pending(self):
return self._pending is not None and self._pending.State not in (OrderStates.Done, OrderStates.Failed)
def _submit(self, side, quantity, reason):
self._pending = self.CreateOrder(side, Decimal(0), quantity)
self._pending.Type = OrderTypes.Market
self._pending.Comment = "0551" + reason
self.RegisterOrder(self._pending)
def _process_candle(self, candle, bb, rsi_value):
if candle.State != CandleStates.Finished or not self._bb.IsFormed or not self._rsi.IsFormed or bb.LowBand is None or bb.UpBand is None:
return
rsi = rsi_value.GetValue[Decimal](None)
previous = self._previous_rsi
self._previous_rsi = rsi
if previous is None or self.Position != 0 or self._is_pending() or not self.IsFormedAndOnlineAndAllowTrading():
return
side = Sides.Buy if candle.LowPrice < bb.LowBand and previous <= self.RsiOversold and rsi > self.RsiOversold else Sides.Sell if candle.HighPrice > bb.UpBand and previous >= self.RsiOverbought and rsi < self.RsiOverbought else None
if side is None:
return
self._entry_cost = Decimal(0)
self._entry_quantity = Decimal(0)
self._trailing = False
self._initial_distance = self.StopLossPoints
self._activation_distance = self.TrailOffsetPoints
self._trailing_distance = self.TrailStopPoints
self._submit(side, self.Volume, "Entry")
def OnOwnTradeReceived(self, trade):
super(bb_rsi_trailing_stop_strategy, self).OnOwnTradeReceived(trade)
if trade.Order.Comment == "0551Entry":
price = ITickTradeMessage.Price.__get__(trade.Trade)
quantity = ITickTradeMessage.Volume.__get__(trade.Trade)
self._entry_cost += price * quantity
self._entry_quantity += quantity
entry = self._entry_cost / self._entry_quantity
self._extreme = entry
self._stop = entry - self._initial_distance if trade.Order.Side == Sides.Buy else entry + self._initial_distance
def _process_quote(self, quote):
if self.Position == 0 or self._entry_quantity == 0 or self._is_pending() or not self.IsFormedAndOnlineAndAllowTrading():
return
field = Level1Fields.BestBidPrice if self.Position > 0 else Level1Fields.BestAskPrice
if not quote.Changes.ContainsKey(field):
return
price = Convert.ToDecimal(quote.Changes[field])
if price <= 0:
return
entry = self._entry_cost / self._entry_quantity
self._extreme = max(self._extreme, price) if self.Position > 0 else min(self._extreme, price)
if (self._extreme - entry >= self._activation_distance if self.Position > 0 else entry - self._extreme >= self._activation_distance):
self._trailing = True
if self._trailing:
self._stop = max(self._stop, self._extreme - self._trailing_distance) if self.Position > 0 else min(self._stop, self._extreme + self._trailing_distance)
if (price <= self._stop if self.Position > 0 else price >= self._stop):
self._submit(Sides.Sell if self.Position > 0 else Sides.Buy, abs(self.Position), "Stop")
def CreateClone(self):
return bb_rsi_trailing_stop_strategy()