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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 82a2a059f832e80857a14fa93ca724c7231f9fe9
4
- data.tar.gz: 5f8346117ed15e6b2d7b2391796104c928c4fe64
3
+ metadata.gz: 618bb22197148e139a847b21a5c41f049b4b974e
4
+ data.tar.gz: 8ae41e97883da4fffd728625215b210dcc1115ef
5
5
  SHA512:
6
- metadata.gz: 5029d67e9ac25a3e8dba7f9f718d53589555f62bf99ca1296ff1296ae2a303fd558b2f0f9a6d87165a78883bc08105656da12225046a2611f289e4f4de96f197
7
- data.tar.gz: a288a6bb4d241a13ae5ca4d195b421572f506ff4486e487a4c48e3e870f642e9e900155026bd2df36db579f3a3b8a1e69f9dd531d0df3813c2bd4bd38b29acd3
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 )
@@ -8,20 +8,52 @@ require 'eventmachine'
8
8
  class RTCBX
9
9
  # seconds in between pinging the connection.
10
10
  #
11
- PING_INTERVAL = 15
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")}"
@@ -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
- attr_reader :history_queue
8
- attr_reader :update_thread
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
- attr_reader :start_minute
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
- begin
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 = []
@@ -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),
@@ -47,11 +47,13 @@ class RTCBX
47
47
  #
48
48
  def start!
49
49
  super
50
- sleep 0.3
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
@@ -1,5 +1,5 @@
1
1
  # Orderbook version number. I try to keep it semantic.
2
2
  #
3
3
  class RTCBX
4
- VERSION = '0.0.3'
4
+ VERSION = '0.0.4'
5
5
  end
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.3
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-19 00:00:00.000000000 Z
11
+ date: 2017-12-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler