Class: Gekko::Book

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Serialization
Defined in:
lib/gekko/book.rb

Overview

An order book consisting of a bid side and an ask side

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Serialization

included, #serialize

Constructor Details

#initialize(pair, opts = {}) ⇒ Book

Returns a new instance of Book.



19
20
21
22
23
24
25
26
27
# File 'lib/gekko/book.rb', line 19

def initialize(pair, opts = {})
  self.pair           = opts[:pair] || pair
  self.bids           = opts[:bids] || BookSide.new(:bid)
  self.asks           = opts[:asks] || BookSide.new(:ask)
  self.tape           = opts[:tape] || Tape.new({ logger: opts[:logger] })
  self.base_precision = opts[:base_precision] || 8
  self.multiplier     = BigDecimal(10 ** base_precision)
  self.received       = opts[:received] || {}
end

Instance Attribute Details

#asksObject

Returns the value of attribute asks.



15
16
17
# File 'lib/gekko/book.rb', line 15

def asks
  @asks
end

#base_precisionObject

Returns the value of attribute base_precision.



15
16
17
# File 'lib/gekko/book.rb', line 15

def base_precision
  @base_precision
end

#bidsObject

Returns the value of attribute bids.



15
16
17
# File 'lib/gekko/book.rb', line 15

def bids
  @bids
end

#multiplierObject

Returns the value of attribute multiplier.



15
16
17
# File 'lib/gekko/book.rb', line 15

def multiplier
  @multiplier
end

#pairObject

Returns the value of attribute pair.



15
16
17
# File 'lib/gekko/book.rb', line 15

def pair
  @pair
end

#receivedObject

Returns the value of attribute received.



15
16
17
# File 'lib/gekko/book.rb', line 15

def received
  @received
end

#tapeObject

Returns the value of attribute tape.



15
16
17
# File 'lib/gekko/book.rb', line 15

def tape
  @tape
end

Class Method Details

.from_hash(hsh) ⇒ Gekko::Book

Loads the book from a hash

Parameters:

  • hsh (Hash)

    A Book hash

Returns:



252
253
254
255
256
257
258
259
260
261
262
# File 'lib/gekko/book.rb', line 252

def self.from_hash(hsh)
  book = Book.new(hsh[:pair], {
    bids: BookSide.new(:bid, orders: hsh[:bids].map { |o| symbolize_keys(o) }),
    asks: BookSide.new(:ask, orders: hsh[:asks].map { |o| symbolize_keys(o) })
  })

  [:bids, :asks].each { |s| book.send(s).each { |ord| book.received[ord.id.to_s] = ord } }
  book.tape = Tape.from_hash(symbolize_keys(hsh[:tape])) if hsh[:tape]

  book
end

Instance Method Details

#askObject

Returns the current best ask price or nil if there are currently no asks



181
182
183
# File 'lib/gekko/book.rb', line 181

def ask
  asks.top
end

#bidObject

Returns the current best bid price or nil if there are currently no bids



189
190
191
# File 'lib/gekko/book.rb', line 189

def bid
  bids.top
end

#cancel(order_id) ⇒ Object

Cancels an order given an ID

Parameters:

  • order_id (UUID)

    The ID of the order to cancel



147
148
149
150
151
152
153
154
155
156
# File 'lib/gekko/book.rb', line 147

def cancel(order_id)
  prev_bid = bid
  prev_ask = ask

  order = received[order_id.to_s]
  dels = order.bid? ? bids.delete(order) : asks.delete(order)
  dels && tape << order.message(:done, reason: :canceled)

  tick! if (prev_bid != bid) || (prev_ask != ask)
end

#execute_trade(maker, taker) ⇒ Object

Executes a trade between two orders

Parameters:



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/gekko/book.rb', line 98

def execute_trade(maker, taker)
  trade_price     = maker.price
  max_quote_size  = nil

  # Rounding direction depends on the takers direction
  rounding = (taker.bid? ? :floor : :ceil)

  if taker.is_a?(MarketOrder)
    max_size_with_quote_margin = taker.remaining_quote_margin &&
      (taker.remaining_quote_margin * multiplier / trade_price).send(rounding)
  end

  base_size = [
    maker.remaining,
    taker.remaining,
    max_size_with_quote_margin
  ].compact.min

  if taker.is_a?(LimitOrder)
    quote_size = (base_size * trade_price) / multiplier

  elsif taker.is_a?(MarketOrder)
    if base_size == max_size_with_quote_margin
      taker.max_precision = true
    end

    quote_size = [(trade_price * base_size / multiplier).round, taker.remaining_quote_margin].compact.min
    taker.remaining_quote_margin -= quote_size if taker.quote_margin
  end

  tape << {
    type:       :execution,
    price:      trade_price,
    base_size:  base_size,
    quote_size: quote_size,
    maker_id:   maker.id.to_s,
    taker_id:   taker.id.to_s,
    tick:       taker.bid? ? :up : :down
  }

  taker.remaining  -= base_size if taker.remaining
  maker.remaining  -= base_size
end

#receive_order(order) ⇒ Object

Receives an order and executes it

Parameters:

  • order (Order)

    The order to execute



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/gekko/book.rb', line 34

def receive_order(order)
  raise 'Order must be a Gekko::LimitOrder or a Gekko::MarketOrder' unless [LimitOrder, MarketOrder].include?(order.class)

  if received.has_key?(order.id.to_s)
    tape << order.message(:reject, reason: :duplicate_id)

  elsif order.expired?
    tape << order.message(:reject, reason: :expired)

  else
    old_ticker = ticker

    self.received[order.id.to_s] = order
    tape << order.message(:received)

    order_side    = order.bid? ? bids : asks
    opposite_side = order.bid? ? asks : bids
    next_match    = opposite_side.first
    prev_match_id = nil

    while !order.done? && order.crosses?(next_match)
      # If we match against the same order twice in a row, something went seriously
      # wrong, we'd rather noisily die at this point.
      raise 'Infinite matching loop detected !!' if (prev_match_id == next_match.id)
      prev_match_id = next_match.id

      if next_match.expired?
        tape << opposite_side.shift.message(:done, reason: :expired)
        next_match = opposite_side.first

      elsif order.uid == next_match.uid
        # Same user/account associated to order, we cancel the next match
        tape << opposite_side.shift.message(:done, reason: :canceled)
        next_match = opposite_side.first

      else
        execute_trade(next_match, order)

        if next_match.filled?
          tape << opposite_side.shift.message(:done, reason: :filled)
          next_match = opposite_side.first
        end
      end
    end

    if order.filled?
      tape << order.message(:done, reason: :filled)
    elsif order.fill_or_kill?
      tape << order.message(:done, reason: :killed)
    else
      order_side.insert_order(order)
      tape << order.message(:open)
    end

    tick! unless (ticker == old_ticker)
  end
end

#remove_expired!Object

Removes all expired orders from the book



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/gekko/book.rb', line 161

def remove_expired!
  prev_bid = bid
  prev_ask = ask

  [bids, asks].each do |bs|
    bs.reject! do |order| 
      if order.expired?
        tape << order.message(:done, reason: :expired)
        true
      end
    end
  end

  tick! if (prev_bid != bid) || (prev_ask != ask)
end

#spreadObject

Returns the current spread if at least a bid and an ask are present, returns nil otherwise



197
198
199
# File 'lib/gekko/book.rb', line 197

def spread
  ask && bid && (ask - bid)
end

#tick!Object

Emits a ticker on the tape



204
205
206
# File 'lib/gekko/book.rb', line 204

def tick!
  tape << { type: :ticker }.merge(ticker)
end

#tickerHash

Returns the current ticker

Returns:

  • (Hash)

    The current ticker



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/gekko/book.rb', line 213

def ticker
  v24h = tape.volume_24h
  {
    last:       tape.last_trade_price,
    bid:        bid,
    ask:        ask,
    high_24h:   tape.high_24h,
    low_24h:    tape.low_24h,
    spread:     spread,
    volume_24h: v24h,

    # We'd like to return +nil+, not +false+ when we don't have any volume
    vwap_24h:   ((v24h > 0) && (tape.quote_volume_24h * multiplier / v24h).to_i) || nil
  }
end

#to_hashHash

Returns a Hash representation of this Book instance

Returns:

  • (Hash)

    The serializable representation



234
235
236
237
238
239
240
241
242
243
244
# File 'lib/gekko/book.rb', line 234

def to_hash
  {
    time:             Time.now.to_f,
    bids:             bids.to_hash,
    asks:             asks.to_hash,
    pair:             pair,
    tape:             tape.to_hash,
    received:         received,
    base_precision:   base_precision
  }
end