bitflyer 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +3 -3
- data/README.md +36 -4
- data/lib/bitflyer.rb +2 -2
- data/lib/bitflyer/realtime/client.rb +8 -4
- data/lib/bitflyer/realtime/websocket.rb +63 -26
- data/lib/bitflyer/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0afb7c55d8bf1c0c68fdb043881e446ad9481b932a328c25cc68dadabbd935c4
|
4
|
+
data.tar.gz: 0d58c2aebb84b56f15bcc4bca7da2a2ace9f8c39ec6a6b87fa9460d03087d61f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af27099352002ac7983bb1655c6f7d04e606fa06494d76e37943c1c1343c4d6fdf987d235d36a863e63365e667ff7a61a7564fa81800f8ef6f9798b362c6df76
|
7
|
+
data.tar.gz: 6e5f176afd23d3c438a1a5ec679a9e5dbd0b1a30784ce91968ce29f2914d7cd5dcd3eec1b5a96e260a203aef4375c4a90b31b2a77b2fb78c3e6fdfe28ec70a19
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
bitflyer (1.
|
4
|
+
bitflyer (1.2.0)
|
5
5
|
faraday (>= 0.14, < 1.4)
|
6
6
|
faraday_middleware (>= 0.12, < 1.1)
|
7
7
|
websocket-client-simple (~> 0.3.0)
|
@@ -30,7 +30,7 @@ GEM
|
|
30
30
|
method_source (~> 1.0)
|
31
31
|
rainbow (3.0.0)
|
32
32
|
rake (13.0.3)
|
33
|
-
regexp_parser (2.
|
33
|
+
regexp_parser (2.1.1)
|
34
34
|
rexml (3.2.4)
|
35
35
|
rspec (3.10.0)
|
36
36
|
rspec-core (~> 3.10.0)
|
@@ -45,7 +45,7 @@ GEM
|
|
45
45
|
diff-lcs (>= 1.2.0, < 2.0)
|
46
46
|
rspec-support (~> 3.10.0)
|
47
47
|
rspec-support (3.10.2)
|
48
|
-
rubocop (1.
|
48
|
+
rubocop (1.11.0)
|
49
49
|
parallel (~> 1.10)
|
50
50
|
parser (>= 3.0.0.0)
|
51
51
|
rainbow (>= 2.2.2, < 4.0)
|
data/README.md
CHANGED
@@ -37,20 +37,52 @@ p private_client.positions # will print your positions
|
|
37
37
|
|
38
38
|
### Realtime API
|
39
39
|
|
40
|
+
#### Public events
|
41
|
+
|
40
42
|
Accessor format is like `{event_name}_{product_code}`.
|
41
43
|
You can set lambda to get realtime events.
|
42
44
|
|
43
45
|
`{event_name}` and `{product_code}` is defined at [client.rb](./lib/bitflyer/realtime/client.rb).
|
44
46
|
|
45
|
-
####
|
47
|
+
#### Private events
|
48
|
+
|
49
|
+
To subscribe to the private `child_order_events` and `parent_order_events`, pass your API key and secret when creating the `realtime_client`.
|
50
|
+
|
51
|
+
#### Connection status monitoring
|
46
52
|
|
53
|
+
The `ready` callback is called when the `realtime_client` is ready to receive events (after the socket connection is established, the optional authentication has succeeded, and the channels have been subscribed). If connection is lost, the `disconnected` callback is called, and reconnection is attempted automatically. When connection is restored, `ready` is called again.
|
54
|
+
|
55
|
+
#### Examples
|
56
|
+
|
57
|
+
For public events only:
|
47
58
|
```ruby
|
48
59
|
client = Bitflyer.realtime_client
|
49
|
-
client.ticker_btc_jpy = ->(json){ p json } # will print json object
|
60
|
+
client.ticker_btc_jpy = ->(json){ p json } # will print json object
|
61
|
+
client.executions_btc_jpy = ->(json){ p json }
|
62
|
+
# ...
|
63
|
+
```
|
64
|
+
|
65
|
+
For both public and private events:
|
66
|
+
```ruby
|
67
|
+
client = Bitflyer.realtime_client('YOUR_API_KEY', 'YOUR_API_SECRET')
|
68
|
+
# Private events:
|
69
|
+
client.child_order_events = ->(json){ p json }
|
70
|
+
client.parent_order_events = ->(json){ p json }
|
71
|
+
# Public events:
|
72
|
+
client.ticker_btc_jpy = ->(json){ p json }
|
50
73
|
client.executions_btc_jpy = ->(json){ p json }
|
51
|
-
# ...
|
74
|
+
# ...
|
75
|
+
```
|
76
|
+
|
77
|
+
Connection monitoring:
|
78
|
+
```ruby
|
79
|
+
client = Bitflyer.realtime_client
|
80
|
+
client.ready = -> { p "Client is ready to receive events" }
|
81
|
+
client.disconnected = ->(error) { p "Client got disconnected" }
|
52
82
|
```
|
53
83
|
|
84
|
+
|
85
|
+
|
54
86
|
## Contributing
|
55
87
|
|
56
88
|
Bug reports and pull requests are welcome on GitHub at https://github.com/unhappychoice/bitflyer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
@@ -80,4 +112,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|
80
112
|
|
81
113
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
82
114
|
|
83
|
-
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
115
|
+
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
data/lib/bitflyer.rb
CHANGED
@@ -4,13 +4,17 @@ require_relative './websocket'
|
|
4
4
|
|
5
5
|
module Bitflyer
|
6
6
|
module Realtime
|
7
|
-
|
7
|
+
PUBLIC_EVENT_NAMES = %w[lightning_board_snapshot lightning_board lightning_ticker lightning_executions].freeze
|
8
8
|
MARKETS = %w[BTC_JPY FX_BTC_JPY ETH_BTC BCH_BTC BTCJPY_MAT3M BTCJPY_MAT1WK BTCJPY_MAT2WK].freeze
|
9
|
-
|
9
|
+
PUBLIC_CHANNEL_NAMES = PUBLIC_EVENT_NAMES.product(MARKETS).map { |e, m| "#{e}_#{m}" }.freeze
|
10
|
+
PRIVATE_CHANNEL_NAMES = %w[child_order_events parent_order_events].freeze
|
11
|
+
CHANNEL_NAMES = (PUBLIC_CHANNEL_NAMES + PRIVATE_CHANNEL_NAMES).freeze
|
10
12
|
|
11
13
|
SOCKET_HOST = 'https://io.lightstream.bitflyer.com'
|
12
14
|
|
13
15
|
class Client
|
16
|
+
extend Forwardable
|
17
|
+
def_delegators :@websocket_client, :ready=, :disconnected=
|
14
18
|
attr_accessor :websocket_client, :ping_interval, :ping_timeout, :last_ping_at, :last_pong_at
|
15
19
|
|
16
20
|
Realtime::CHANNEL_NAMES.each do |channel_name|
|
@@ -19,8 +23,8 @@ module Bitflyer
|
|
19
23
|
end
|
20
24
|
end
|
21
25
|
|
22
|
-
def initialize
|
23
|
-
@websocket_client = Bitflyer::Realtime::WebSocketClient.new(host: SOCKET_HOST)
|
26
|
+
def initialize(key = nil, secret = nil)
|
27
|
+
@websocket_client = Bitflyer::Realtime::WebSocketClient.new(host: SOCKET_HOST, key: key, secret: secret)
|
24
28
|
end
|
25
29
|
end
|
26
30
|
end
|
@@ -2,54 +2,55 @@
|
|
2
2
|
|
3
3
|
require 'websocket-client-simple'
|
4
4
|
require 'json'
|
5
|
+
require 'openssl'
|
5
6
|
|
6
7
|
module Bitflyer
|
7
8
|
module Realtime
|
8
9
|
class WebSocketClient
|
9
|
-
attr_accessor :websocket_client, :
|
10
|
-
:last_ping_at, :last_pong_at, :
|
10
|
+
attr_accessor :websocket_client, :channel_names, :channel_callbacks, :ping_interval, :ping_timeout,
|
11
|
+
:last_ping_at, :last_pong_at, :ready, :disconnected
|
11
12
|
|
12
|
-
def initialize(host:, debug: false)
|
13
|
+
def initialize(host:, key:, secret:, debug: false)
|
13
14
|
@host = host
|
15
|
+
@key = key
|
16
|
+
@secret = secret
|
14
17
|
@debug = debug
|
15
|
-
@error = nil
|
16
18
|
@channel_names = []
|
17
19
|
@channel_callbacks = {}
|
18
20
|
connect
|
21
|
+
start_monitoring
|
19
22
|
end
|
20
23
|
|
21
24
|
def subscribe(channel_name:, &block)
|
22
25
|
debug_log "Subscribe #{channel_name}"
|
23
26
|
@channel_names = (@channel_names + [channel_name]).uniq
|
24
27
|
@channel_callbacks[channel_name] = block
|
25
|
-
websocket_client.send "42#{['subscribe', channel_name].to_json}"
|
28
|
+
@websocket_client.send "42#{['subscribe', channel_name].to_json}"
|
26
29
|
end
|
27
30
|
|
28
31
|
def connect
|
29
32
|
@websocket_client = WebSocket::Client::Simple.connect "#{@host}/socket.io/?transport=websocket"
|
30
33
|
this = self
|
34
|
+
@websocket_client.on(:message) { |payload| this.handle_message(payload: payload) }
|
35
|
+
@websocket_client.on(:error) { |error| this.handle_error(error: error) }
|
36
|
+
@websocket_client.on(:close) { |error| this.handle_close(error: error) }
|
37
|
+
rescue SocketError => e
|
38
|
+
puts e
|
39
|
+
puts e.backtrace.join("\n")
|
40
|
+
end
|
31
41
|
|
42
|
+
def start_monitoring
|
32
43
|
Thread.new do
|
33
44
|
loop do
|
34
45
|
sleep 1
|
35
46
|
if @websocket_client&.open?
|
36
47
|
send_ping
|
37
48
|
wait_pong
|
49
|
+
else
|
50
|
+
reconnect
|
38
51
|
end
|
39
52
|
end
|
40
53
|
end
|
41
|
-
|
42
|
-
Thread.new do
|
43
|
-
loop do
|
44
|
-
sleep 1
|
45
|
-
next unless @error
|
46
|
-
|
47
|
-
reconnect
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
@websocket_client.on(:message) { |payload| this.handle_message(payload: payload) }
|
52
|
-
@websocket_client.on(:error) { |error| this.handle_error(error: error) }
|
53
54
|
end
|
54
55
|
|
55
56
|
def send_ping
|
@@ -70,23 +71,19 @@ module Bitflyer
|
|
70
71
|
end
|
71
72
|
|
72
73
|
def reconnect
|
73
|
-
return
|
74
|
+
return if @websocket_client&.open?
|
74
75
|
|
75
76
|
debug_log 'Reconnecting...'
|
76
77
|
|
77
|
-
@error = nil
|
78
78
|
@websocket_client.close if @websocket_client.open?
|
79
79
|
connect
|
80
|
-
@channel_names.each do |channel_name|
|
81
|
-
debug_log "42#{{ subscribe: channel_name }.to_json}"
|
82
|
-
websocket_client.send "42#{['subscribe', channel_name].to_json}"
|
83
|
-
end
|
84
80
|
end
|
85
81
|
|
86
82
|
def handle_error(error:)
|
87
83
|
debug_log error
|
88
84
|
return unless error.is_a? Errno::ECONNRESET
|
89
85
|
|
86
|
+
@disconnected&.call(error)
|
90
87
|
reconnect
|
91
88
|
end
|
92
89
|
|
@@ -101,20 +98,60 @@ module Bitflyer
|
|
101
98
|
when 3 then receive_pong
|
102
99
|
when 41 then disconnect
|
103
100
|
when 42 then emit_message(json: body)
|
101
|
+
when 430 then authenticated(json: body)
|
104
102
|
end
|
105
103
|
rescue StandardError => e
|
106
104
|
puts e
|
107
105
|
puts e.backtrace.join("\n")
|
108
106
|
end
|
109
107
|
|
108
|
+
def handle_close(error:)
|
109
|
+
debug_log error
|
110
|
+
@disconnected&.call(error)
|
111
|
+
end
|
112
|
+
|
110
113
|
def setup_by_response(json:)
|
111
114
|
body = JSON.parse json
|
112
115
|
@ping_interval = body['pingInterval'].to_i || 25_000
|
113
116
|
@ping_timeout = body['pingTimeout'].to_i || 60_000
|
114
117
|
@last_ping_at = Time.now.to_i
|
115
118
|
@last_pong_at = Time.now.to_i
|
116
|
-
|
117
|
-
|
119
|
+
if @key && @secret
|
120
|
+
authenticate
|
121
|
+
else
|
122
|
+
subscribe_channels
|
123
|
+
@ready&.call
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def authenticate
|
128
|
+
debug_log 'Authenticate'
|
129
|
+
timestamp = Time.now.to_i
|
130
|
+
nonce = Random.new.bytes(16).unpack('H*').first
|
131
|
+
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), @secret, timestamp.to_s + nonce)
|
132
|
+
auth_params = {
|
133
|
+
api_key: @key,
|
134
|
+
timestamp: timestamp,
|
135
|
+
nonce: nonce,
|
136
|
+
signature: signature
|
137
|
+
}
|
138
|
+
@websocket_client.send "420#{['auth', auth_params].to_json}"
|
139
|
+
end
|
140
|
+
|
141
|
+
def authenticated(json:)
|
142
|
+
if json == '[null]'
|
143
|
+
debug_log 'Authenticated'
|
144
|
+
subscribe_channels
|
145
|
+
@ready&.call
|
146
|
+
else
|
147
|
+
raise "Authentication failed: #{json}"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def subscribe_channels
|
152
|
+
@channel_callbacks.each do |channel_name, _|
|
153
|
+
debug_log "42#{{ subscribe: channel_name }.to_json}"
|
154
|
+
@websocket_client.send "42#{['subscribe', channel_name].to_json}"
|
118
155
|
end
|
119
156
|
end
|
120
157
|
|
@@ -125,7 +162,7 @@ module Bitflyer
|
|
125
162
|
|
126
163
|
def disconnect
|
127
164
|
debug_log 'Disconnecting from server...'
|
128
|
-
@
|
165
|
+
@websocket_client.close
|
129
166
|
end
|
130
167
|
|
131
168
|
def emit_message(json:)
|
data/lib/bitflyer/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bitflyer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yuji Ueki
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-03-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|