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 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