Class: Bybit::WebSocket::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/bybit/websocket/client.rb

Overview

Bybit V5 WebSocket client — public streams (spot / linear / inverse / option) and the private user-data stream.

Design goals mirror the REST session:

  • one class, channel: selects endpoint (avoids Public/Private class duplication for ping / subscribe / dispatch logic)
  • explicit callbacks (on_message, on_open, on_close, on_error) passed as procs; no global registry / singleton
  • HMAC-SHA256 auth for :private is identical to REST signing except the payload is "GET/realtime" + expires — the docs are explicit
  • ping every ping_interval seconds via a lightweight background thread; server terminates connections after 20s silence
  • automatic resubscribe on reconnect (topic list is retained across #connect calls) so callers don't lose subscriptions on transient network blips

This class depends on websocket-client-simple (a runtime dep of the gem). If missing, requiring it raises a clear LoadError below.

Constant Summary collapse

PUBLIC_CHANNELS =

Channel → WS path (public streams are category-scoped; private has one).

%i[spot linear inverse option].freeze
PRIVATE_CHANNEL =
:private
TRADE_CHANNEL =
:trade
PATHS =
{
  spot: '/v5/public/spot',
  linear: '/v5/public/linear',
  inverse: '/v5/public/inverse',
  option: '/v5/public/option',
  private: '/v5/private',
  trade: '/v5/trade'
}.freeze
HOST_MAINNET =
'stream.bybit.com'
HOST_TESTNET =
'stream-testnet.bybit.com'
DEFAULT_PING_INTERVAL =
20
DEFAULT_AUTH_EXPIRY_MS =

Bybit rejects auth expiry timestamps in the past AND >30s in the future.

10_000

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(channel:, testnet: false, api_key: nil, api_secret: nil, ping_interval: DEFAULT_PING_INTERVAL, url: nil, on_open: nil, on_message: nil, on_close: nil, on_error: nil) ⇒ Client

Returns a new instance of Client.

Parameters:

  • channel (Symbol)

    one of :spot, :linear, :inverse, :option, :private, :trade.

  • testnet (Boolean) (defaults to: false)

    hit stream-testnet.bybit.com instead of mainnet

  • api_key (String) (defaults to: nil)

    required for :private / :trade

  • api_secret (String) (defaults to: nil)

    required for :private / :trade

  • ping_interval (Integer) (defaults to: DEFAULT_PING_INTERVAL)

    seconds between ping frames (default 20)

  • url (String) (defaults to: nil)

    override the entire WS URL (skips host/path build)

  • on_open,

    on_message, on_close, on_error [Proc]



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/bybit/websocket/client.rb', line 67

def initialize(channel:, testnet: false,
               api_key: nil, api_secret: nil,
               ping_interval: DEFAULT_PING_INTERVAL,
               url: nil,
               on_open: nil, on_message: nil, on_close: nil, on_error: nil)
  validate_channel!(channel)
  @channel        = channel
  @testnet        = testnet
  @api_key        = api_key
  @api_secret     = api_secret
  @ping_interval  = ping_interval
  @url            = url || build_url
  @on_open        = on_open
  @on_message     = on_message
  @on_close       = on_close
  @on_error       = on_error
  @subscriptions  = []
  @ws             = nil
  @ping_thread    = nil
  @connected      = false
  @auth_ok        = false
  @closing        = false
end

Instance Attribute Details

#channelObject (readonly)

Returns the value of attribute channel.



57
58
59
# File 'lib/bybit/websocket/client.rb', line 57

def channel
  @channel
end

#subscriptionsObject (readonly)

Returns the value of attribute subscriptions.



57
58
59
# File 'lib/bybit/websocket/client.rb', line 57

def subscriptions
  @subscriptions
end

#testnetObject (readonly)

Returns the value of attribute testnet.



57
58
59
# File 'lib/bybit/websocket/client.rb', line 57

def testnet
  @testnet
end

#urlObject (readonly)

Returns the value of attribute url.



57
58
59
# File 'lib/bybit/websocket/client.rb', line 57

def url
  @url
end

Instance Method Details

#connectObject

Open the WebSocket, authenticate (if private/trade), and start pinging. Calling connect a second time closes the prior socket first, then opens a fresh one and replays any topics buffered in @subscriptions (call disconnect explicitly if you want to drop them). No automatic reconnect on network drop — the caller decides when to re-open.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/bybit/websocket/client.rb', line 96

def connect
  require_auth! if requires_auth?
  # Idempotent: tear down any prior socket / ping thread so a second
  # `connect` call doesn't leak the previous ws or leave two pingers.
  disconnect if @ws
  @closing = false
  client = self
  @ws = ::WebSocket::Client::Simple.connect(@url)

  @ws.on(:open)    { client.send(:handle_open) }
  @ws.on(:message) { |msg| client.send(:handle_message, msg) }
  @ws.on(:close)   { |ev|  client.send(:handle_close, ev) }
  @ws.on(:error)   { |err| client.send(:handle_error, err) }
  self
end

#connected?Boolean

Returns:

  • (Boolean)


156
157
158
# File 'lib/bybit/websocket/client.rb', line 156

def connected?
  @connected
end

#disconnectObject



142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/bybit/websocket/client.rb', line 142

def disconnect
  @closing = true
  stop_pinger
  @connected = false
  @auth_ok = false
  begin
    @ws&.close
  rescue StandardError
    # Socket may already be half-closed by the peer or in a bad state —
    # we only care about detaching, not about clean protocol shutdown.
  end
  @ws = nil
end

#ready?Boolean

Returns:

  • (Boolean)


160
161
162
163
164
# File 'lib/bybit/websocket/client.rb', line 160

def ready?
  return false unless @connected

  requires_auth? ? @auth_ok : true
end

#send_raw(payload) ⇒ Object

Send a WS message directly (escape hatch for placing orders over the /v5/trade stream; the docs specify per-op payloads).



136
137
138
139
140
# File 'lib/bybit/websocket/client.rb', line 136

def send_raw(payload)
  return unless @ws

  @ws.send(payload.is_a?(String) ? payload : JSON.generate(payload))
end

#subscribe(*topics) ⇒ Object

Subscribe to one or more topics. Bybit expects an array under args:

{"op":"subscribe","args":["orderbook.1.BTCUSDT","tickers.BTCUSDT"]}

Callers can call subscribe before connect; topics are buffered and sent once the socket opens (private streams also wait for auth OK).



116
117
118
119
120
121
122
123
# File 'lib/bybit/websocket/client.rb', line 116

def subscribe(*topics)
  topics = topics.flatten.compact.uniq
  return if topics.empty?

  @subscriptions |= topics
  flush_subscriptions(topics) if ready?
  self
end

#unsubscribe(*topics) ⇒ Object



125
126
127
128
129
130
131
132
# File 'lib/bybit/websocket/client.rb', line 125

def unsubscribe(*topics)
  topics = topics.flatten.compact.uniq
  return if topics.empty?

  @subscriptions -= topics
  send_frame(op: 'unsubscribe', args: topics) if ready?
  self
end