orderbook 0.1.0 → 1.0.0

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