orderbook 0.1.0 → 1.0.0

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: 84269d46b54589bbc0b4ef3cc68427ea1f41160d
4
- data.tar.gz: 355f66f391af0c02d0eea84d9edbcff19c9761ae
3
+ metadata.gz: cf56caee6a0354c98d4db2a50d4b19c0d31f6ca7
4
+ data.tar.gz: 3da66fed2eeeb1c3e9b0171ded4bdd58d805caa1
5
5
  SHA512:
6
- metadata.gz: 978af2908c7c1c0be124d3fff39fb1f43cb0bb3d980b4252842e50d1059617c82ffeeab34997e3d30513eb14c1ddb89992d4730f98761d6660cd9608172634b5
7
- data.tar.gz: 5540ecd2f5cd99f9d68648d934d8d4b226aa4f66f7646294b72d6673c9f41a71a3f3c536b3af364af4d22fa1020eb0840674f0fd2641eac03198ee189d1a28c9
6
+ metadata.gz: 424e6bd0efea8a60866f2688d58b5578a698415708277c88a8091f9391bedcec4bee7344d467a977f9a625ed9d5542c5a95f5351934d92cdd5975342a03ddeec
7
+ data.tar.gz: 92594ee583a883d8a116701fbef362ab785e8d0f47d9768041c410acb3b5c53f4df72185d2408971f78ac0492b397de95c1ae0a1402b806da5ae26a3c11e9dc1
data/.gitignore CHANGED
@@ -16,3 +16,4 @@ mkmf.log
16
16
  *config.yaml
17
17
  *config.rb
18
18
  *.gem
19
+ bin
data/Gemfile CHANGED
@@ -2,6 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in orderbook.gemspec
4
4
  gemspec
5
- gem 'coinbase_exchange', :git=> 'git://github.com/mikerodrigues/coinbase_exchange.git'
5
+ gem 'coinbase/exchange'
6
6
  gem 'json'
7
- gem 'websocket-client-simple'
data/README.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  A gem for creating a realtime order book for the Coinbase Exchange.
4
4
 
5
+ Version 1.0.0 and greater now use the official Coinbase Exchange Ruby Gem's
6
+ EventMachine-driven client. It should be more reliable than the previous socket
7
+ code.
8
+
9
+ Also, the gem now uses BigDecimal in place of Float when dealing with sizes and
10
+ prices.
11
+
5
12
  ## Installation
6
13
 
7
14
  Add this line to your application's Gemfile:
@@ -24,26 +31,69 @@ Or install it yourself as:
24
31
  require 'orderbook'
25
32
  ```
26
33
 
27
- * To create a RealTimeBook:
34
+ * Create a live updating Orderbook:
35
+ ```ruby
36
+ ob = Orderbook.new
28
37
  ```
29
- rtb = Orderbook::RealTimeBook.new
30
38
 
31
- rtb.bids # Returns an array of bids.
39
+ * Create an Orderbook object but don't fetch an orderbook or start live
40
+ updating.
41
+ ```ruby
42
+ ob = Orderbook.new(false)
32
43
 
33
- rtb.asks # Returns an array of asks.
44
+ # When you want it to go live:
34
45
 
35
- # Setup a callback that runs after the message is applied to the book. Each
36
- message received on the WebSocket is passed to this block. Running this method
37
- again will redefine the callback.
46
+ ob.live!
47
+ ```
38
48
 
39
- rtb.set_callback do |msg|
40
- puts msg.fetch('type')
49
+ * Create a live Orderbook with a callback to fire on each message:
50
+ ```ruby
51
+ ob = Orderbook.new do |message|
52
+ puts message.fetch 'type'
41
53
  end
42
54
  ```
43
55
 
56
+ * Create or reset the callback:
57
+ ```ruby
58
+ ob.callback = lambda do |message|
59
+ puts message.fetch 'callback'
60
+ end
61
+ ```
62
+
63
+ * The old class name is still supported and is equivalent to an Orderbook:
64
+ ```ruby
65
+ rtb = Orderbook::RealTimeBook.new
66
+ ```
67
+
68
+ * List current bids:
69
+ ```ruby
70
+ ob.bids
71
+ ```
72
+
73
+ * List current asks:
74
+ ```ruby
75
+ ob.asks
76
+ ```
77
+
78
+ * Show sequence number for initial level 3 snapshot:
79
+ ```ruby
80
+ ob.first_sequence
81
+ ```
82
+
83
+ * Show sequence number for the last message received
84
+ ```ruby
85
+ ob.last_sequence
86
+ ```
87
+
88
+ * Show the last Time a pong was received after a ping (ensures the connection is
89
+ still alive):
90
+ ```ruby
91
+ ob.last_pong
92
+ ```
93
+
44
94
  ## Contributing
45
95
 
46
- 1. Fork it ( https://github.com/[my-github-username]/orderbook/fork )
96
+ 1. Fork it ( https://github.com/mikerodrigues/orderbook/fork )
47
97
  2. Create your feature branch (`git checkout -b my-new-feature`)
48
98
  3. Commit your changes (`git commit -am 'Add some feature'`)
49
99
  4. Push to the branch (`git push origin my-new-feature`)
data/Rakefile CHANGED
@@ -1,2 +1,36 @@
1
- require "bundler/gem_tasks"
1
+ require 'rake/testtask'
2
+ require 'bundler'
3
+ require_relative './lib/orderbook/version.rb'
2
4
 
5
+ task :build do
6
+ begin
7
+ puts 'building gem...'
8
+ `gem build orderbook.gemspec`
9
+ rescue
10
+ puts 'build failed.'
11
+ end
12
+ end
13
+
14
+ task :install do
15
+ begin
16
+ puts 'installing gem...'
17
+ `gem install --local orderbook`
18
+ rescue
19
+ puts 'install failed.'
20
+ end
21
+ end
22
+
23
+ task :console do
24
+ require 'rubygems'
25
+ require 'pry'
26
+ ARGV.clear
27
+ PRY.start
28
+ end
29
+
30
+ task :default => ['build', 'install']
31
+
32
+ Rake::TestTask.new do |t|
33
+ t.libs << 'test'
34
+ t.test_files = FileList['test/tc*.rb']
35
+ t.verbose = true
36
+ end
data/lib/orderbook.rb CHANGED
@@ -1,8 +1,117 @@
1
- require 'coinbase_exchange'
2
- require 'orderbook/real_time_book'
1
+ require 'coinbase/exchange'
3
2
  require 'orderbook/book_methods'
3
+ require 'orderbook/book_analysis'
4
+ require 'orderbook/real_time_book'
4
5
  require 'orderbook/version'
5
6
 
6
- module Orderbook
7
- # Your code goes here...
7
+ # This class represents the current state of the CoinBase Exchange orderbook.
8
+ #
9
+ class Orderbook
10
+ include BookMethods
11
+ include BookAnalysis
12
+
13
+ PING_INTERVAL = 15 # seconds in between pinging the connection.
14
+
15
+ # Array of bids
16
+ #
17
+ attr_reader :bids
18
+
19
+ # Array of asks
20
+ #
21
+ attr_reader :asks
22
+
23
+ # Sequence number from the initial level 3 snapshot
24
+ #
25
+ attr_reader :first_sequence
26
+
27
+ # Sequence number of most recently received message
28
+ #
29
+ attr_reader :last_sequence
30
+
31
+ # Coinbase::Exchange::Websocket object
32
+ #
33
+ attr_reader :websocket
34
+
35
+ # Coinbase::Exchange::AsyncClient object
36
+ #
37
+ attr_reader :client
38
+
39
+ # Thread running the EM loop
40
+ #
41
+ attr_reader :thread
42
+
43
+ # Last time a pong was received after a ping
44
+ #
45
+ attr_reader :last_pong
46
+
47
+ # Callback to pass each received message to
48
+ #
49
+ attr_accessor :callback
50
+
51
+ # Creates a new live copy of the orderbook.
52
+ #
53
+ # If +live+ is set to false, the orderbook will not start automatically.
54
+ #
55
+ # If a +block+ is given it is passed each message as it is received.
56
+ #
57
+ def initialize(live = true, &block)
58
+ @bids = [[nil, nil]]
59
+ @asks = [[nil, nil]]
60
+ @first_sequence = 0
61
+ @last_sequence = 0
62
+ @websocket = Coinbase::Exchange::Websocket.new(keepalive: true)
63
+ @client = Coinbase::Exchange::AsyncClient.new
64
+ @callback = block if block_given?
65
+ live && live!
66
+ end
67
+
68
+ # Used to start the thread that listens to updates on the websocket and
69
+ # applies them to the current orderbook to create a live book.
70
+ #
71
+ def live!
72
+ setup_websocket
73
+ start_thread
74
+ end
75
+
76
+ private
77
+
78
+ def setup_websocket
79
+ @websocket.message do |message|
80
+ apply(message)
81
+ @callback && @callback.call(message)
82
+ end
83
+ end
84
+
85
+ def fetch_current_orderbook
86
+ @client.orderbook(level: 3) do |resp|
87
+ @bids = resp['bids']
88
+ @asks = resp['asks']
89
+ @first_sequence = resp['sequence']
90
+ end
91
+ end
92
+
93
+ def ping
94
+ EM.add_periodic_timer(PING_INTERVAL) do
95
+ @websocket.ping do
96
+ @last_pong = Time.now
97
+ end
98
+ end
99
+ end
100
+
101
+ def handle_errors
102
+ EM.error_handler do |e|
103
+ print "Websocket Error: #{e.message} - #{e.backtrace.join("\n")}"
104
+ end
105
+ end
106
+
107
+ def start_thread
108
+ @thread = Thread.new do
109
+ EM.run do
110
+ fetch_current_orderbook
111
+ @websocket.start!
112
+ ping
113
+ handle_errors
114
+ end
115
+ end
116
+ end
8
117
  end
@@ -1,33 +1,57 @@
1
- module Orderbook
1
+ class Orderbook
2
2
  module BookAnalysis
3
- def size
4
- puts "Bids: #{@bids.count}"
5
- puts "Asks: #{@asks.count}"
3
+ def bid_count
4
+ @bids.count
6
5
  end
7
6
 
8
- def volume
9
- puts "Bid volume: #{@bids.map {|x| x.fetch(1).to_f}.inject(:+)}"
10
- puts "Ask volume: #{@asks.map {|x| x.fetch(1).to_f}.inject(:+)}"
7
+ def ask_count
8
+ @asks.count
11
9
  end
12
10
 
13
- def average
11
+ def bid_volume
12
+ @bids.map {|x| BigDecimal.new(x.fetch(1))}.inject(:+)
13
+ end
14
+
15
+ def ask_volume
16
+ @asks.map {|x| BigDecimal.new(x.fetch(1))}.inject(:+)
17
+ end
18
+
19
+ def average_bid
14
20
  array = @bids.map do |price, amount, id|
15
- price.to_f
21
+ BigDecimal.new price
16
22
  end
17
23
  avg_bid = array.inject(:+) / array.count
18
- puts "Avg. Bid: #{avg_bid}"
24
+ avg_bid
25
+ end
19
26
 
27
+ def average_ask
20
28
  array = @asks.map do |price, amount, id|
21
- price.to_f
29
+ BigDecimal.new price
22
30
  end
23
31
  avg_ask = array.inject(:+) / array.count
24
- puts "Avg. Asks: #{avg_ask}"
32
+ avg_ask
25
33
  end
26
34
 
27
- def best
28
- puts "Best Bid: #{(@bids.sort_by {|x| x.fetch(0).to_f}).last}"
29
- puts "Best Ask: #{@asks.sort_by {|x| x.fetch(0).to_f}.first}"
35
+ def best_bid
36
+ BigDecimal.new @bids.sort_by {|x| BigDecimal.new(x.fetch(0))}.last.first
30
37
  end
31
38
 
39
+ def best_ask
40
+ BigDecimal.new @asks.sort_by {|x| BigDecimal.new(x.fetch(0))}.first.first
41
+ end
42
+
43
+ def spread
44
+ best_ask - best_bid
45
+ end
46
+
47
+ def summarize
48
+ 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"
49
+ $stdout.flush
50
+ # puts "Avg. ask: #{average_ask}"
51
+ # puts "Avg. bid: #{average_bid}"
52
+ # puts "Best ask: #{best_bid}"
53
+ # puts "Best bid: #{best_ask}"
54
+ # puts "Spread: #{spread}"
55
+ end
32
56
  end
33
57
  end
@@ -1,8 +1,17 @@
1
- module Orderbook
1
+ require 'bigdecimal'
2
+
3
+ class Orderbook
4
+ # This class provides methods to apply updates to the state of the orderbook
5
+ # as they come in as individual messages.
6
+ #
2
7
  module BookMethods
3
8
 
9
+ # Applies a message to an Orderbook object by making relevant changes to
10
+ # @bids, @asks, and @last_sequence.
11
+ #
4
12
  def apply(msg)
5
- unless msg.fetch('sequence') <= @sequence
13
+ unless msg.fetch('sequence') <= @first_sequence
14
+ @last_sequence = msg.fetch('sequence')
6
15
  __send__(msg.fetch('type'), msg)
7
16
  end
8
17
  end
@@ -20,36 +29,36 @@ module Orderbook
20
29
  end
21
30
 
22
31
  def match(msg)
23
- match_size = msg.fetch("size").to_f
32
+ match_size = BigDecimal.new(msg.fetch("size"))
24
33
  case msg.fetch("side")
25
34
  when "sell"
26
35
  @asks.map do |ask|
27
36
  if ask.include? msg.fetch("maker_order_id")
28
- old_size = ask.fetch(1).to_f
37
+ old_size = BigDecimal.new(ask.fetch(1))
29
38
  new_size = old_size - match_size
30
- ask[1] = new_size.to_s
39
+ ask[1] = new_size.to_s('F')
31
40
  end
32
41
  end
33
42
  @bids.map do |bid|
34
43
  if bid.include? msg.fetch("taker_order_id")
35
- old_size = bid.fetch(1).to_f
44
+ old_size = BigDecimal.net(bid.fetch(1))
36
45
  new_size = old_size - match_size
37
- bid[1] = new_size.to_s
46
+ bid[1] = new_size.to_s('F')
38
47
  end
39
48
  end
40
49
  when "buy"
41
50
  @bids.map do |bid|
42
51
  if bid.include? msg.fetch("maker_order_id")
43
- old_size = bid.fetch(1).to_f
52
+ old_size = BigDecimal.new(bid.fetch(1))
44
53
  new_size = old_size - match_size
45
- bid[1] = new_size.to_s
54
+ bid[1] = new_size.to_s('F')
46
55
  end
47
56
  end
48
57
  @asks.map do |ask|
49
58
  if ask.include? msg.fetch("taker_order_id")
50
- old_size = ask.fetch(1).to_f
59
+ old_size = BigDecimal.new(ask.fetch(1))
51
60
  new_size = old_size - match_size
52
- ask[1] = new_size.to_s
61
+ ask[1] = new_size.to_s('F')
53
62
  end
54
63
  end
55
64
  end
@@ -82,6 +91,7 @@ module Orderbook
82
91
  end
83
92
 
84
93
  def received(msg)
94
+ # The book doesn't change for this message type.
85
95
  end
86
96
 
87
97
  end
@@ -1,116 +1,7 @@
1
- require 'orderbook/book_methods'
2
- require 'orderbook/book_analysis'
1
+ class Orderbook
2
+ class RealTimeBook < Orderbook
3
+ # For backwards compatability
3
4
 
4
- module Orderbook
5
- class RealTimeBook
6
- include BookMethods
7
- include BookAnalysis
8
-
9
- # Time to wait until considering skipped messages lost.
10
- #
11
- LOST_TIMEOUT = 5
12
-
13
- # Array of bids
14
- #
15
- attr_reader :bids
16
-
17
- # Array of asks
18
- #
19
- attr_reader :asks
20
-
21
- # Sequence number of snapshot
22
- #
23
- attr_reader :sequence
24
-
25
- # Most recently processed sequence
26
- #
27
- attr_reader :last_sequence
28
-
29
- # Hash of missing sequence numbers and their timeout threads.
30
- #
31
- attr_reader :missing
32
-
33
- # CoinbaseExchange::Feed object
34
- #
35
- attr_reader :feed
36
-
37
- # Queue of messages to be processed
38
- #
39
- attr_reader :queue
40
-
41
- def initialize(&block)
42
- if block_given?
43
- @callback = block
44
- end
45
- subscribe
46
- snapshot
47
- process_queue
48
- end
49
-
50
- def set_callback(&block)
51
- @callback = block
52
- end
53
-
54
- def refresh_snapshot
55
- @thread.kill
56
- snapshot
57
- process_queue
58
- end
59
-
60
- private
61
-
62
- def subscribe
63
- @queue = Queue.new
64
- @missing = {}
65
- on_msg = lambda {|msg| @queue << msg}
66
- on_close = lambda {|close| puts close}
67
- on_err = lambda {|err| puts err}
68
- @feed = ::CoinbaseExchange::Feed.new(on_msg, on_close, on_err)
69
- end
70
-
71
- def snapshot
72
- @cb || @cb = ::CoinbaseExchange.new
73
- @snapshot = @cb.orderbook(3, 'BTC-USD')
74
- @sequence = @snapshot.fetch('sequence').to_i
75
- @bids = @snapshot.fetch('bids')
76
- @asks = @snapshot.fetch('asks')
77
- end
78
-
79
- def timeout(sequence)
80
- warn "Missing sequence #{sequence} timed out. Refreshingn snapshot."
81
- refresh_snapshot
82
- end
83
-
84
- def process_queue
85
- @thread && @thread.kill
86
- @thread = Thread.new do
87
- @last_sequence = 'start'
88
- loop do
89
- msg = @queue.shift
90
- sequence = msg.fetch('sequence').to_i
91
- if sequence > @sequence
92
- unless @last_sequence == 'start'
93
- expected_sequence = @last_sequence + 1
94
- if sequence != expected_sequence
95
- if @missing.keys.include? (expected_sequence)
96
- @missing.fetch(expected_sequence).kill
97
- @missing.delete(expected_sequence)
98
- else
99
- @missing[expected_sequence] = Thread.new do
100
- sleep LOST_TIMEOUT
101
- timeout expected_sequence
102
- end
103
- end
104
- @missing < expected_sequence
105
- end
106
- end
107
- apply(msg)
108
- @callback && @callback.call(msg)
109
- end
110
- @last_sequence = sequence
111
- end
112
- end
113
- end
114
5
  end
115
6
  end
116
7
 
@@ -1,3 +1,3 @@
1
- module Orderbook
2
- VERSION = "0.1.0"
1
+ class Orderbook
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: orderbook
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Rodrigues
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-15 00:00:00.000000000 Z
11
+ date: 2015-06-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -79,8 +79,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
79
  version: '0'
80
80
  requirements: []
81
81
  rubyforge_project:
82
- rubygems_version: 2.4.5
82
+ rubygems_version: 2.2.2
83
83
  signing_key:
84
84
  specification_version: 4
85
85
  summary: Maintains an real-time copy of the Coinbase Exchange order book.
86
86
  test_files: []
87
+ has_rdoc: