rtcbx 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +16 -0
- data/lib/rtcbx.rb +44 -4
- data/lib/rtcbx/candles.rb +33 -10
- data/lib/rtcbx/candles/candle.rb +4 -0
- data/lib/rtcbx/orderbook.rb +5 -3
- data/lib/rtcbx/orderbook/book_analysis.rb +62 -0
- data/lib/rtcbx/orderbook/book_methods.rb +10 -0
- data/lib/rtcbx/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 618bb22197148e139a847b21a5c41f049b4b974e
|
4
|
+
data.tar.gz: 8ae41e97883da4fffd728625215b210dcc1115ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fdfe5368c8e704e0a99d64892662345a7c2d19bd2c1c7fca35bc5b7f9197d219f28962416f56c0413bb5ed1d3e89aa8b01c35b4b1197cb1a54ddc84e7db5420a
|
7
|
+
data.tar.gz: efcae4d468599dcabd22a0fd166e025f75b6860150c22ea78b1ed5babf4a515a87cbe39ccb4fbd14faa8118024484859f3c7f185f1ef9d53972b4acc8a5cd28e
|
data/README.md
CHANGED
@@ -129,6 +129,22 @@ ob.last_sequence
|
|
129
129
|
ob.last_pong
|
130
130
|
```
|
131
131
|
|
132
|
+
* Aggregates the top N bids in the Orderbook (optional parameter defaults to aggregate all available bids):
|
133
|
+
```ruby
|
134
|
+
ob.aggregate_bids(10)
|
135
|
+
```
|
136
|
+
|
137
|
+
* Aggregates the first N asks in the Orderbook (optional parameter defaults to aggregate all available asks):
|
138
|
+
```ruby
|
139
|
+
ob.aggregate_asks(10)
|
140
|
+
```
|
141
|
+
|
142
|
+
* Aggregates the top N bids and the first N asks in the Orderbook (optional parameter defaults to aggregate the entire Orderbook):
|
143
|
+
```ruby
|
144
|
+
# Will perform the aggregation on each call. Avoid abusing this function since it may degrade performance.
|
145
|
+
ob.aggregate(10)
|
146
|
+
```
|
147
|
+
|
132
148
|
## Contributing
|
133
149
|
|
134
150
|
1. Fork it ( https://github.com/mikerodrigues/orderbook/fork )
|
data/lib/rtcbx.rb
CHANGED
@@ -8,20 +8,52 @@ require 'eventmachine'
|
|
8
8
|
class RTCBX
|
9
9
|
# seconds in between pinging the connection.
|
10
10
|
#
|
11
|
-
PING_INTERVAL =
|
12
|
-
|
13
|
-
Thread.abort_on_exception = true
|
11
|
+
PING_INTERVAL = 2
|
14
12
|
|
13
|
+
# The GDAX product being tracked (eg. "BTC-USD")
|
15
14
|
attr_reader :product_id
|
15
|
+
|
16
|
+
# Boolean, whether the orderbook goes live on creation or not
|
17
|
+
# If +false+, +#start!+ must be called to initiate tracking.
|
16
18
|
attr_reader :start
|
19
|
+
|
20
|
+
# API key used to authenticate to the API
|
21
|
+
# Not required for Orderbook or Candles
|
17
22
|
attr_reader :api_key
|
23
|
+
|
24
|
+
# An array of blocks to be run each time a message comes in on the Websocket
|
18
25
|
attr_reader :message_callbacks
|
26
|
+
|
27
|
+
# The Websocket object
|
19
28
|
attr_reader :websocket
|
29
|
+
|
30
|
+
# The GDAX Client object
|
31
|
+
# You can use this if you need to make API calls
|
20
32
|
attr_reader :client
|
33
|
+
|
34
|
+
# The message queue from the Websocket.
|
35
|
+
# The +websocket_thread+ processes this queue
|
21
36
|
attr_reader :queue
|
37
|
+
|
38
|
+
# Epoch time indicating the last time we received a pong from GDAX in response
|
39
|
+
# to one of our pings
|
22
40
|
attr_reader :last_pong
|
41
|
+
|
42
|
+
# The thread that consumes the websocket data
|
23
43
|
attr_reader :websocket_thread
|
24
44
|
|
45
|
+
# Create a new RTCBX object with options and an optional block to be run when
|
46
|
+
# each message is called.
|
47
|
+
#
|
48
|
+
# Generally you won't call this directly. You'll use +RTCBX::Orderbook.new+,
|
49
|
+
# +RTCBX::Trader.new+, or +RTCBX::Candles.new+.
|
50
|
+
#
|
51
|
+
# You can also subclass RTCBX and call this method through +super+, as the
|
52
|
+
# classes mentioned above do.
|
53
|
+
#
|
54
|
+
# RTCBX handles connecting to the Websocket, setting up the client, and
|
55
|
+
# managing the thread that consumes the Websocket feed.
|
56
|
+
#
|
25
57
|
def initialize(options = {}, &block)
|
26
58
|
@product_id = options.fetch(:product_id, 'BTC-USD')
|
27
59
|
@start = options.fetch(:start, true)
|
@@ -44,15 +76,18 @@ class RTCBX
|
|
44
76
|
start! if start
|
45
77
|
end
|
46
78
|
|
79
|
+
# Starts the thread to consume the Websocket feed
|
47
80
|
def start!
|
48
81
|
start_websocket_thread
|
49
82
|
end
|
50
83
|
|
84
|
+
# Stops the thread and disconnects from the Websocket
|
51
85
|
def stop!
|
52
86
|
websocket_thread.kill
|
53
87
|
websocket.stop!
|
54
88
|
end
|
55
89
|
|
90
|
+
# Stops, then starts the thread that consumes the Websocket feed
|
56
91
|
def reset!
|
57
92
|
stop!
|
58
93
|
start!
|
@@ -63,6 +98,8 @@ class RTCBX
|
|
63
98
|
attr_reader :api_secret
|
64
99
|
attr_reader :api_passphrase
|
65
100
|
|
101
|
+
# Configures the websocket to pass each message to each of the defined message
|
102
|
+
# callbacks
|
66
103
|
def setup_websocket_callback
|
67
104
|
websocket.message do |message|
|
68
105
|
queue.push(message)
|
@@ -70,6 +107,7 @@ class RTCBX
|
|
70
107
|
end
|
71
108
|
end
|
72
109
|
|
110
|
+
# Starts the thread that consumes the websocket
|
73
111
|
def start_websocket_thread
|
74
112
|
@websocket_thread = Thread.new do
|
75
113
|
setup_websocket_callback
|
@@ -81,14 +119,16 @@ class RTCBX
|
|
81
119
|
end
|
82
120
|
end
|
83
121
|
|
122
|
+
# Configures the websocket to periodically ping GDAX and confirm connection
|
84
123
|
def setup_ping_timer
|
85
124
|
EM.add_periodic_timer(PING_INTERVAL) do
|
86
125
|
websocket.ping do
|
87
|
-
last_pong = Time.now
|
126
|
+
@last_pong = Time.now
|
88
127
|
end
|
89
128
|
end
|
90
129
|
end
|
91
130
|
|
131
|
+
# Configures the Websocket object to print any errors to the console
|
92
132
|
def setup_error_handler
|
93
133
|
EM.error_handler do |e|
|
94
134
|
print "Websocket Error: #{e.message} - #{e.backtrace.join("\n")}"
|
data/lib/rtcbx/candles.rb
CHANGED
@@ -3,25 +3,51 @@ require 'rtcbx/candles/candle'
|
|
3
3
|
class RTCBX
|
4
4
|
class Candles < RTCBX
|
5
5
|
|
6
|
+
# A hash of buckets
|
7
|
+
# Each key is an epoch which stores every +match+ message for that minute
|
8
|
+
# (The epoch plus 60 seconds)
|
9
|
+
# Each minute interval is a bucket, which is used to calculate that minute's
|
10
|
+
# +Candle+
|
6
11
|
attr_reader :buckets
|
7
|
-
|
8
|
-
|
12
|
+
|
13
|
+
|
14
|
+
# This thread monitors the websocket object and puts each +match+ object
|
15
|
+
# into the proper bucket. This thread maintains the +buckets+ object.
|
9
16
|
attr_reader :bucket_thread
|
17
|
+
|
18
|
+
# The +candle_thread+ consumes the buckets created by the +bucket_thread+ in
|
19
|
+
# +buckets+ and turns them into +Candle+ objects. These are then appended to
|
20
|
+
# the +candles+ array. This functionality could be improved. Ideally you're
|
21
|
+
# consuming this array into a database to keep history in realtime.
|
10
22
|
attr_reader :candle_thread
|
23
|
+
|
24
|
+
# The epoch representing the current bucket
|
11
25
|
attr_reader :current_bucket
|
12
|
-
|
26
|
+
|
27
|
+
# An array of generated candles. You should process these by putting them
|
28
|
+
# into a database and removing them from the array. If you want to help me
|
29
|
+
# abstract this to a pluggable database system, open an issue.
|
13
30
|
attr_reader :candles
|
14
31
|
|
32
|
+
# The first full minute that we can collect for. (+Time+ object)
|
15
33
|
attr_reader :initial_time
|
34
|
+
|
35
|
+
# The epoch of the first bucket
|
16
36
|
attr_reader :first_bucket
|
17
|
-
attr_reader :bucket_lock
|
18
37
|
|
38
|
+
# Mutex to allow our two threads to produce and consume +buckets+
|
39
|
+
attr_reader :buckets_lock
|
19
40
|
|
41
|
+
|
42
|
+
# Create a new +Candles+ object to start and track candles
|
43
|
+
# Pass a block to run a block whenever a candle is created.
|
44
|
+
#
|
20
45
|
def initialize(options = {}, &block)
|
21
46
|
super(options, &block)
|
22
47
|
@buckets_lock = Mutex.new
|
23
48
|
end
|
24
49
|
|
50
|
+
# Start tracking candles
|
25
51
|
def start!
|
26
52
|
super
|
27
53
|
#
|
@@ -30,7 +56,6 @@ class RTCBX
|
|
30
56
|
#
|
31
57
|
@initial_time = Time.now
|
32
58
|
@first_bucket = initial_time.to_i + (60 - initial_time.sec)
|
33
|
-
@history_queue = Queue.new
|
34
59
|
|
35
60
|
start_bucket_thread
|
36
61
|
start_candle_thread
|
@@ -38,6 +63,7 @@ class RTCBX
|
|
38
63
|
|
39
64
|
private
|
40
65
|
|
66
|
+
# Start the thread to create buckets
|
41
67
|
def start_bucket_thread
|
42
68
|
@bucket_thread = Thread.new do
|
43
69
|
@buckets = {}
|
@@ -56,11 +82,7 @@ class RTCBX
|
|
56
82
|
@buckets[current_bucket.to_i] = []
|
57
83
|
@buckets[current_bucket.to_i] << message
|
58
84
|
else
|
59
|
-
|
60
|
-
@buckets[current_bucket.to_i] << message
|
61
|
-
rescue
|
62
|
-
binding.pry
|
63
|
-
end
|
85
|
+
@buckets[current_bucket.to_i] << message
|
64
86
|
end
|
65
87
|
end
|
66
88
|
end
|
@@ -69,6 +91,7 @@ class RTCBX
|
|
69
91
|
end
|
70
92
|
end
|
71
93
|
|
94
|
+
# Start the thread to consume buckets to +Candle+ objects
|
72
95
|
def start_candle_thread
|
73
96
|
@candle_thread = Thread.new do
|
74
97
|
@candles = []
|
data/lib/rtcbx/candles/candle.rb
CHANGED
@@ -2,8 +2,11 @@ class RTCBX
|
|
2
2
|
class Candles < RTCBX
|
3
3
|
class Candle
|
4
4
|
|
5
|
+
# Candle values, this is standard
|
5
6
|
attr_reader :time, :low, :high, :open, :close, :volume
|
6
7
|
|
8
|
+
# Create a new +Candle+ from an epoch, and all the messages sent during
|
9
|
+
# the interval of the candle
|
7
10
|
def initialize(epoch, matches)
|
8
11
|
@time = Time.at(epoch)
|
9
12
|
@low = matches.map {|message| BigDecimal.new(message.fetch('price'))}.min
|
@@ -13,6 +16,7 @@ class RTCBX
|
|
13
16
|
@volume = matches.reduce(BigDecimal(0)) {|sum, message| sum + BigDecimal.new(message.fetch('size'))}
|
14
17
|
end
|
15
18
|
|
19
|
+
# Return a +Hash+ representation of the +Candle+
|
16
20
|
def to_h
|
17
21
|
{
|
18
22
|
start: Time.at(@time),
|
data/lib/rtcbx/orderbook.rb
CHANGED
@@ -47,11 +47,13 @@ class RTCBX
|
|
47
47
|
#
|
48
48
|
def start!
|
49
49
|
super
|
50
|
-
sleep
|
50
|
+
sleep 1
|
51
51
|
apply_orderbook_snapshot
|
52
52
|
start_update_thread
|
53
53
|
end
|
54
54
|
|
55
|
+
# Stop the thread that listens to updates on the websocket
|
56
|
+
#
|
55
57
|
def stop!
|
56
58
|
super
|
57
59
|
update_thread.kill
|
@@ -79,6 +81,8 @@ class RTCBX
|
|
79
81
|
end
|
80
82
|
end
|
81
83
|
|
84
|
+
# Private method to actually start the thread that reads from the que and
|
85
|
+
# updates the Orderbook state
|
82
86
|
def start_update_thread
|
83
87
|
@update_thread = Thread.new do
|
84
88
|
begin
|
@@ -92,7 +96,5 @@ class RTCBX
|
|
92
96
|
end
|
93
97
|
end
|
94
98
|
end
|
95
|
-
|
96
|
-
# apply(message)
|
97
99
|
end
|
98
100
|
end
|
@@ -4,64 +4,126 @@ class RTCBX
|
|
4
4
|
# methods for calculating whatever it is you feel like calculating.
|
5
5
|
#
|
6
6
|
module BookAnalysis
|
7
|
+
# Number of all current bids
|
7
8
|
def bid_count
|
8
9
|
@bids.count
|
9
10
|
end
|
10
11
|
|
12
|
+
# Number of all current asks
|
11
13
|
def ask_count
|
12
14
|
@asks.count
|
13
15
|
end
|
14
16
|
|
17
|
+
# Number of all current orders
|
15
18
|
def count
|
16
19
|
{ bid: bid_count, ask: ask_count }
|
17
20
|
end
|
18
21
|
|
22
|
+
# The total volume of product across all current bids
|
19
23
|
def bid_volume
|
20
24
|
@bids.map { |x| x.fetch(:size) }.inject(:+)
|
21
25
|
end
|
22
26
|
|
27
|
+
# The total volume of product across all current asks
|
23
28
|
def ask_volume
|
24
29
|
@asks.map { |x| x.fetch(:size) }.inject(:+)
|
25
30
|
end
|
26
31
|
|
32
|
+
# The total volume of all product across current asks and bids
|
27
33
|
def volume
|
28
34
|
{ bid: bid_volume, ask: ask_volume }
|
29
35
|
end
|
30
36
|
|
37
|
+
# The average bid price across all bids
|
31
38
|
def average_bid
|
32
39
|
bids = @bids.map { |x| x.fetch(:price) }
|
33
40
|
bids.inject(:+) / bids.count
|
34
41
|
end
|
35
42
|
|
43
|
+
# The average ask price across all asks
|
36
44
|
def average_ask
|
37
45
|
asks = @asks.map { |x| x.fetch(:price) }
|
38
46
|
asks.inject(:+) / asks.count
|
39
47
|
end
|
40
48
|
|
49
|
+
# The average price across all orders
|
41
50
|
def average
|
42
51
|
{ bid: average_bid, ask: average_ask }
|
43
52
|
end
|
44
53
|
|
54
|
+
# The price of the best current bid
|
45
55
|
def best_bid
|
46
56
|
@bids.sort_by { |x| x.fetch(:price) }.last
|
47
57
|
end
|
48
58
|
|
59
|
+
# The price of the best current ask
|
49
60
|
def best_ask
|
50
61
|
@asks.sort_by { |x| x.fetch(:price) }.first
|
51
62
|
end
|
52
63
|
|
64
|
+
# The prices of the best current bid and ask
|
53
65
|
def best
|
54
66
|
{ bid: best_bid, ask: best_ask }
|
55
67
|
end
|
56
68
|
|
69
|
+
# The price difference between the best current bid and ask
|
57
70
|
def spread
|
58
71
|
best_ask.fetch(:price) - best_bid.fetch(:price)
|
59
72
|
end
|
60
73
|
|
74
|
+
# Aggregates the +top_n+ current bids. Pass `50` and you'll get the same
|
75
|
+
# thing tht GDAX calls a "Level 2 Orderbook"
|
76
|
+
def aggregate_bids(top_n = nil)
|
77
|
+
aggregate = {}
|
78
|
+
@bids.each do |bid|
|
79
|
+
aggregate[bid[:price]] ||= aggregate_base
|
80
|
+
aggregate[bid[:price]][:size] += bid[:size]
|
81
|
+
aggregate[bid[:price]][:num_orders] += 1
|
82
|
+
end
|
83
|
+
top_n ||= aggregate.keys.count
|
84
|
+
aggregate.keys.sort.reverse.first(top_n).map do |price|
|
85
|
+
{ price: price,
|
86
|
+
size: aggregate[price][:size],
|
87
|
+
num_orders: aggregate[price][:num_orders]
|
88
|
+
}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Aggregates the +top_n+ current asks. Pass `50` and you'll get the same
|
93
|
+
# thing tht GDAX calls a "Level 2 Orderbook"
|
94
|
+
def aggregate_asks(top_n = nil)
|
95
|
+
aggregate = {}
|
96
|
+
@asks.each do |ask|
|
97
|
+
aggregate[ask[:price]] ||= aggregate_base
|
98
|
+
aggregate[ask[:price]][:size] += ask[:size]
|
99
|
+
aggregate[ask[:price]][:num_orders] += 1
|
100
|
+
end
|
101
|
+
top_n ||= aggregate.keys.count
|
102
|
+
aggregate.keys.sort.first(top_n).map do |price|
|
103
|
+
{ price: price,
|
104
|
+
size: aggregate[price][:size],
|
105
|
+
num_orders: aggregate[price][:num_orders]
|
106
|
+
}
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Aggregates the +top_n+ current asks and bids. Pass `50` and you'll get the same
|
111
|
+
# thing tht GDAX calls a "Level 2 Orderbook"
|
112
|
+
def aggregate(top_n = nil)
|
113
|
+
{ bids: aggregate_bids(top_n), asks: aggregate_asks(top_n) }
|
114
|
+
end
|
115
|
+
|
116
|
+
# print a quick summary of the +Orderbook+
|
61
117
|
def summarize
|
62
118
|
print "# of asks: #{ask_count}\n# of bids: #{bid_count}\nAsk volume: #{ask_volume.to_s('F')}\nBid volume: #{bid_volume.to_s('F')}\n"
|
63
119
|
$stdout.flush
|
64
120
|
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def aggregate_base
|
125
|
+
{ size: BigDecimal.new(0), num_orders: 0 }
|
126
|
+
end
|
65
127
|
end
|
66
128
|
end
|
67
129
|
end
|
@@ -6,6 +6,8 @@ class RTCBX
|
|
6
6
|
# as they are received by the websocket.
|
7
7
|
#
|
8
8
|
module BookMethods
|
9
|
+
|
10
|
+
# Names of attributes that should be converted to +BigDecimal+
|
9
11
|
BIGDECIMAL_KEYS = %w(size old_size new_size remaining_size price)
|
10
12
|
|
11
13
|
# Applies a message to an Orderbook object by making relevant changes to
|
@@ -71,6 +73,14 @@ class RTCBX
|
|
71
73
|
def received(_)
|
72
74
|
# The book doesn't change for this message type.
|
73
75
|
end
|
76
|
+
|
77
|
+
def margin_profile_update(_)
|
78
|
+
# The book doesn't change for this message type.
|
79
|
+
end
|
80
|
+
|
81
|
+
def activate(_)
|
82
|
+
# The book doesn't change for this message type.
|
83
|
+
end
|
74
84
|
end
|
75
85
|
end
|
76
86
|
end
|
data/lib/rtcbx/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rtcbx
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Rodrigues
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-12-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|