rtcbx 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|