Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/CryptoExchanges.Net.Bybit/Streaming/BybitStreamProtocol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,27 @@ public StreamFrame Classify(ReadOnlySpan<byte> frame)
{
StreamKind.Ticker => $"tickers.{request.WireSymbol}",
StreamKind.Trade => $"publicTrade.{request.WireSymbol}",
StreamKind.OrderBook when request.Depth.HasValue => $"orderbook.{request.Depth}.{request.WireSymbol}",
StreamKind.OrderBook => $"orderbook.{DefaultOrderBookDepth}.{request.WireSymbol}",
StreamKind.OrderBook => $"orderbook.{MapOrderBookDepth(request.Depth)}.{request.WireSymbol}",
StreamKind.Kline when request.Interval is not null =>
$"kline.{MapInterval(request.Interval)}.{request.WireSymbol}",
StreamKind.Kline => $"kline.1.{request.WireSymbol}",
_ => throw new ArgumentOutOfRangeException(nameof(request), request.Kind,
$"Unsupported stream kind: {request.Kind}")
};

// Bybit v5 spot publishes order books only at tiers 1/50/200/1000; round each request UP to
// the nearest tier, throwing above 1000 (deepest tier) rather than under-delivering silently.
private static int MapOrderBookDepth(int? requested) => requested switch
{
null => DefaultOrderBookDepth,
<= 1 => 1,
<= 50 => 50,
<= 200 => 200,
<= 1000 => 1000,
_ => throw new ArgumentOutOfRangeException(nameof(requested), requested,
$"Bybit v5 spot supports order-book depths 1, 50, 200, or 1000; requested {requested}.")
};
Comment thread
morpheums marked this conversation as resolved.

private static string MapInterval(string intervalToken) => intervalToken switch
{
nameof(KlineInterval.OneMinute) => "1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ public void BuildSubscribe_Trade_ProducesCorrectTopic()
[Fact]
public void BuildSubscribe_OrderBook_DefaultDepth50()
{
// Confirmed default depth = 50 (Bybit v5 spot available levels: 1, 50, 200).
// Confirmed default depth = 50 (Bybit v5 spot available levels: 1, 50, 200, 1000).
var protocol = MakeProtocol();
var request = new StreamRequest(StreamKind.OrderBook, "BTCUSDT");

Expand Down Expand Up @@ -269,6 +269,73 @@ public void BuildSubscribe_OrderBook_ExplicitDepth200()
doc.RootElement.GetProperty("args")[0].GetString().Should().Be("orderbook.200.BTCUSDT");
}

[Theory]
[InlineData(20, "orderbook.50.BTCUSDT")]
[InlineData(5, "orderbook.50.BTCUSDT")]
[InlineData(1, "orderbook.1.BTCUSDT")]
[InlineData(50, "orderbook.50.BTCUSDT")]
[InlineData(200, "orderbook.200.BTCUSDT")]
[InlineData(500, "orderbook.1000.BTCUSDT")]
[InlineData(1000, "orderbook.1000.BTCUSDT")]
public void BuildSubscribe_OrderBook_RoundsDepthUpToSupportedTier(int requestedDepth, string expectedTopic)
{
// Bybit v5 spot publishes order books only at depths 1/50/200/1000; a non-tier depth
// would yield an invalid topic the venue rejects, so each request rounds UP to a tier.
var protocol = MakeProtocol();
var request = new StreamRequest(StreamKind.OrderBook, "BTCUSDT", Depth: requestedDepth);

var wire = protocol.BuildSubscribe(request);
using var doc = JsonDocument.Parse(wire);

doc.RootElement.GetProperty("args")[0].GetString().Should().Be(expectedTopic);
}

[Theory]
[InlineData(1001)]
[InlineData(5000)]
public void BuildSubscribe_OrderBook_DepthAbove1000_Throws(int requestedDepth)
{
// 1000 is the deepest Bybit v5 spot tier; a deeper request is unsatisfiable and must
// throw rather than silently clamp DOWN to a shallower book than asked for.
var protocol = MakeProtocol();
var request = new StreamRequest(StreamKind.OrderBook, "BTCUSDT", Depth: requestedDepth);

var act = () => protocol.BuildSubscribe(request);

act.Should().Throw<ArgumentOutOfRangeException>();
}

[Fact]
public void BuildSubscribe_OrderBook_NullDepth_MapsToDefaultTier50()
{
var protocol = MakeProtocol();
var request = new StreamRequest(StreamKind.OrderBook, "BTCUSDT", Depth: null);

var wire = protocol.BuildSubscribe(request);
using var doc = JsonDocument.Parse(wire);

doc.RootElement.GetProperty("args")[0].GetString().Should().Be("orderbook.50.BTCUSDT");
}

[Fact]
public void RoutingKeyFor_OrderBook_NonTierDepth_MatchesBuildSubscribeTopic()
{
// Depth 20 is not a Bybit tier; both RoutingKeyFor and the subscribe topic must map it
// to the same tier (50) or data frames would never route back to the subscription.
var protocol = MakeProtocol();
var request = new StreamRequest(StreamKind.OrderBook, "BTCUSDT", Depth: 20);

var routingKey = protocol.RoutingKeyFor(request);
var wire = protocol.BuildSubscribe(request);
using var doc = JsonDocument.Parse(wire);
var subscribeTopic = doc.RootElement.GetProperty("args")[0].GetString();

routingKey.Should().Be("orderbook.50.BTCUSDT");
subscribeTopic.Should().Be("orderbook.50.BTCUSDT");
routingKey.Should().Be(subscribeTopic,
"RoutingKeyFor and BuildSubscribe must share one mapped keyspace so frames reach their subscription");
}

[Fact]
public void BuildSubscribe_Kline_OneMinute()
{
Expand Down