gekko 0.5.2 → 0.8.1

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: 486109d7779f0dad9f4295d86f7c4e04a190aaea
4
- data.tar.gz: 2b7855c833b96c746d9877145d5df44078cd306e
3
+ metadata.gz: 981305466dff1fb589c97f96fb4be28ca1b4e2ce
4
+ data.tar.gz: 123b650f9b0774614710cec547990609786f1dc2
5
5
  SHA512:
6
- metadata.gz: 2374b2b91e9873fc91d61395dac62face02d86644949b82a1fe775745836e78f58695a8b2ab2ead6cdc3eb60a5b093de5ace2fef59fc49d930a9e3b11d234d73
7
- data.tar.gz: 2bdbb61d58115c18f5cdb0907f6f4f2a6fb6bae2ccc7275950c1a1a7faab4495fd7850dd20ea72ae8eff10f3781837057ee2ca11d688b2d76d99a9ab91cc6bad
6
+ metadata.gz: bf435111f321867abc6d3dc3028e54a8da6791c2dc6b2bd216c45279f5bcc750be54332765d411d11a2486db735e67767d4fb5b0c1291893e8468d5eb9f5843b
7
+ data.tar.gz: 33210a4936da0df11359588b54773570a9f12f0002e85ae516931efdc0f763adb236074a7abea5869ac45eb34bd3f38d5763ef89461860af088b2965c23ba6ab
data/README.md CHANGED
@@ -10,7 +10,6 @@ Gekko is not intended to maintain an accounting database, it just matches trade
10
10
  ## Left to do
11
11
  The following items are left to do and will need to be implemented before gekko is considered production-ready.
12
12
 
13
- - Persistence and failure recovery
14
13
  - Add and enforce minimum and maximum order sizes
15
14
  - Correctly handle order expiration
16
15
 
@@ -3,6 +3,7 @@ require 'uuidtools'
3
3
  # UUID shortcut
4
4
  UUID = UUIDTools::UUID
5
5
 
6
+ require 'gekko/serialization'
6
7
  require 'gekko/order'
7
8
  require 'gekko/limit_order'
8
9
  require 'gekko/market_order'
@@ -1,10 +1,6 @@
1
- require 'oj'
2
- Oj.default_options = { mode: :compat }
3
-
4
1
  require 'gekko/book_side'
5
2
  require 'gekko/tape'
6
3
  require 'gekko/errors'
7
- require 'gekko/symbolize_keys'
8
4
 
9
5
  module Gekko
10
6
 
@@ -13,9 +9,9 @@ module Gekko
13
9
  #
14
10
  class Book
15
11
 
16
- extend SymbolizeKeys
12
+ include Serialization
17
13
 
18
- attr_accessor :pair, :bids, :asks, :tape, :received, :base_precision
14
+ attr_accessor :pair, :bids, :asks, :tape, :received, :base_precision, :multiplier
19
15
 
20
16
  def initialize(pair, opts = {})
21
17
  self.pair = opts[:pair] || pair
@@ -23,16 +19,16 @@ module Gekko
23
19
  self.asks = opts[:asks] || BookSide.new(:ask)
24
20
  self.tape = opts[:tape] || Tape.new({ logger: opts[:logger] })
25
21
  self.base_precision = opts[:base_precision] || 8
22
+ self.multiplier = BigDecimal(10 ** base_precision)
26
23
  self.received = opts[:received] || {}
27
24
  end
28
25
 
29
26
  #
30
- # Receives an order and executes it
27
+ # Receives an order and executes it
31
28
  #
32
29
  # @param order [Order] The order to execute
33
30
  #
34
31
  def receive_order(order)
35
-
36
32
  raise 'Order must be a Gekko::LimitOrder or a Gekko::MarketOrder' unless [LimitOrder, MarketOrder].include?(order.class)
37
33
 
38
34
  if received.has_key?(order.id.to_s)
@@ -57,20 +53,30 @@ module Gekko
57
53
  next_match = opposite_side.first
58
54
 
59
55
  else
60
- trade_price = next_match.price
61
- base_size = [next_match.remaining, order.remaining].min
56
+ trade_price = next_match.price
57
+ max_quote_size = nil
58
+
59
+ if order.is_a?(MarketOrder)
60
+ max_size_possible_with_quote_margin = order.remaining_quote_margin && (order.remaining_quote_margin * multiplier / trade_price).round
61
+ end
62
+
63
+ base_size = [
64
+ next_match.remaining,
65
+ order.remaining,
66
+ max_size_possible_with_quote_margin
67
+ ].compact.min
62
68
 
63
69
  if order.is_a?(LimitOrder)
64
- quote_size = (base_size * trade_price) / (10 ** base_precision)
70
+ quote_size = (base_size * trade_price) / multiplier
65
71
 
66
72
  elsif order.is_a?(MarketOrder)
67
- if order.ask? || (order.remaining_quote_margin > ((trade_price * base_size) / (10 ** base_precision)))
68
- quote_size = ((trade_price * base_size) / (10 ** base_precision))
69
- order.remaining_quote_margin -= quote_size if order.bid?
73
+ if order.ask? || (order.remaining_quote_margin > (trade_price * base_size / multiplier))
74
+ quote_size = (trade_price * base_size / multiplier).round
75
+ order.remaining_quote_margin -= quote_size if order.quote_margin
76
+
70
77
  elsif order.bid?
71
78
  quote_size = order.remaining_quote_margin
72
- base_size = (order.remaining_quote_margin * (10 ** base_precision)) / trade_price
73
- order.remaining_quote_margin -= quote_size
79
+ order.remaining_quote_margin = 0
74
80
  end
75
81
  end
76
82
 
@@ -84,7 +90,7 @@ module Gekko
84
90
  tick: order.bid? ? :up : :down
85
91
  }
86
92
 
87
- order.remaining -= base_size
93
+ order.remaining -= base_size if order.remaining
88
94
  next_match.remaining -= base_size
89
95
 
90
96
  if next_match.filled?
@@ -118,7 +124,7 @@ module Gekko
118
124
 
119
125
  order = received[order_id.to_s]
120
126
  dels = order.bid? ? bids.delete(order) : asks.delete(order)
121
- dels && tape << order.message(:done, reason: :cancelled)
127
+ dels && tape << order.message(:done, reason: :canceled)
122
128
 
123
129
  tick! if (prev_bid != bid) || (prev_ask != ask)
124
130
  end
@@ -188,17 +194,19 @@ module Gekko
188
194
  low_24h: tape.low_24h,
189
195
  spread: spread,
190
196
  volume_24h: v24h,
191
- vwap_24h: (v24h > 0) && (tape.quote_volume_24h * (10 ** base_precision)/ v24h)
197
+
198
+ # We'd like to return +nil+, not +false+ when we don't have any volume
199
+ vwap_24h: ((v24h > 0) && (tape.quote_volume_24h * multiplier / v24h).to_i) || nil
192
200
  }
193
201
  end
194
202
 
195
203
  #
196
- # Dumps the book to a JSON string
204
+ # Returns a +Hash+ representation of this +Book+ instance
197
205
  #
198
- # @return [String] The serialized order book
206
+ # @return [Hash] The serializable representation
199
207
  #
200
- def dump
201
- Oj.dump({
208
+ def to_hash
209
+ {
202
210
  time: Time.now.to_f,
203
211
  bids: bids.to_hash,
204
212
  asks: asks.to_hash,
@@ -206,23 +214,24 @@ module Gekko
206
214
  tape: tape.to_hash,
207
215
  received: received,
208
216
  base_precision: base_precision
209
- })
217
+ }
210
218
  end
211
219
 
212
220
  #
213
- # Loads the book from a JSON string
221
+ # Loads the book from a hash
214
222
  #
215
- # @param serialized [String] A serialized book
216
- # @return [Gekko::Book] The deserialized book instance
223
+ # @param hsh [Hash] A Book hash
224
+ # @return [Gekko::Book] The loaded book instance
217
225
  #
218
- def self.load(serialized)
219
- hsh = symbolize_keys(Oj.load(serialized))
226
+ def self.from_hash(hsh)
227
+ book = Book.new(hsh[:pair], {
228
+ bids: BookSide.new(:bid, orders: hsh[:bids].map { |o| symbolize_keys(o) }.sort { |a, b| b[:price] <=> a[:price] }),
229
+ asks: BookSide.new(:ask, orders: hsh[:asks].map { |o| symbolize_keys(o) }.sort { |a, b| a[:price] <=> b[:price] }),
230
+ })
220
231
 
221
- hsh[:tape] = Tape.new(symbolize_keys(hsh[:tape]))
222
- hsh[:bids] = BookSide.new(:bid, orders: hsh[:bids].map { |o| symbolize_keys(o) })
223
- hsh[:asks] = BookSide.new(:ask, orders: hsh[:asks].map { |o| symbolize_keys(o) })
232
+ book.tape = Tape.from_hash(symbolize_keys(hsh[:tape])) if hsh[:tape]
224
233
 
225
- Book.new(hsh[:pair], hsh)
234
+ book
226
235
  end
227
236
 
228
237
  end
@@ -10,6 +10,7 @@ module Gekko
10
10
  attr_accessor :side
11
11
 
12
12
  def initialize(side, opts = {})
13
+ # TODO "WARNING: Sort orders ?"
13
14
  raise "Incorrect side <#{side}>" unless [:bid, :ask].include?(side)
14
15
  @side = side
15
16
 
@@ -10,6 +10,7 @@ module Gekko
10
10
  def initialize(side, id, size, price, expiration = nil)
11
11
  super(side, id, size, expiration)
12
12
  @price = price
13
+
13
14
  raise 'Price must be a positive integer' if @price.nil? || (!@price.is_a?(Fixnum) || (@price <= 0))
14
15
  end
15
16
 
@@ -8,21 +8,28 @@ module Gekko
8
8
 
9
9
  attr_accessor :quote_margin, :remaining_quote_margin
10
10
 
11
- def initialize(side, id, size, quote_margin)
12
- super(side, id, size)
11
+ def initialize(side, id, size, quote_margin, expiration = nil)
12
+ super(side, id, size, expiration)
13
13
 
14
14
  @quote_margin = quote_margin
15
15
  @remaining_quote_margin = @quote_margin
16
16
 
17
- raise 'Quote currency margin must be provided for a market bid' if quote_margin.nil? && bid?
18
- raise 'Quote currency margin can not be specified for a market ask' if quote_margin && ask?
17
+ if bid?
18
+ quote_margin.nil? &&
19
+ raise('Quote currency margin must be provided for a market bid')
20
+ elsif ask?
21
+ (quote_margin.nil? ^ size.nil?) ||
22
+ raise('Quote currency margin and size can not be both specified for a market ask')
23
+ end
19
24
  end
20
25
 
21
26
  #
22
27
  # Returns +true+ if the order is filled
23
28
  #
24
29
  def filled?
25
- remaining.zero?
30
+ #binding.pry
31
+ (!size.nil? && remaining.zero?) ||
32
+ (!quote_margin.nil? && remaining_quote_margin.zero?)
26
33
  end
27
34
 
28
35
  #
@@ -30,7 +37,8 @@ module Gekko
30
37
  # executing further due to quote currency margin constraints
31
38
  #
32
39
  def done?
33
- filled? || (bid? && remaining_quote_margin.zero?)
40
+ filled? ||
41
+ (bid? && remaining_quote_margin.zero?)
34
42
  end
35
43
 
36
44
  end
@@ -1,5 +1,3 @@
1
- require 'gekko/serialization'
2
-
3
1
  module Gekko
4
2
 
5
3
  #
@@ -9,7 +7,7 @@ module Gekko
9
7
  #
10
8
  class Order
11
9
 
12
- include Gekko::Serialization
10
+ include Serialization
13
11
 
14
12
  attr_accessor :id, :side, :size, :remaining, :price, :expiration, :created_at
15
13
 
@@ -54,7 +52,7 @@ module Gekko
54
52
  # @return [Hash] The message we'll print on the tape
55
53
  #
56
54
  def message(type, extra_attrs = {})
57
- {
55
+ hsh = {
58
56
  type: type,
59
57
  order_id: id.to_s,
60
58
  side: side,
@@ -63,6 +61,14 @@ module Gekko
63
61
  price: price,
64
62
  expiration: expiration
65
63
  }.merge(extra_attrs)
64
+
65
+ if is_a?(Gekko::MarketOrder)
66
+ hsh.delete(:price)
67
+ hsh[:quote_margin] = quote_margin
68
+ hsh[:remaining_quote_margin] = remaining_quote_margin
69
+ end
70
+
71
+ hsh
66
72
  end
67
73
 
68
74
  #
@@ -94,5 +100,47 @@ module Gekko
94
100
  expiration && (expiration <= Time.now.to_i)
95
101
  end
96
102
 
103
+ #
104
+ # Returns a +Hash+ representation of this +Order+ instance
105
+ #
106
+ # @return [Hash] The serializable representation
107
+ #
108
+ def to_hash
109
+ hsh = {
110
+ id: id.to_s,
111
+ side: side,
112
+ size: size,
113
+ price: price,
114
+ remaining: remaining,
115
+ expiration: expiration,
116
+ created_at: created_at
117
+ }
118
+
119
+ if is_a?(Gekko::MarketOrder)
120
+ hsh.delete(:price)
121
+ hsh[:quote_margin] = quote_margin
122
+ hsh[:remaining_quote_margin] = remaining_quote_margin
123
+ end
124
+
125
+ hsh
126
+ end
127
+
128
+ #
129
+ # Initializes a +Gekko::Order+ subclass from a +Hash+ instance
130
+ #
131
+ # @param hsh [Hash] The order data
132
+ # @return [Gekko::Order] A trade order
133
+ #
134
+ def self.from_hash(hsh)
135
+ order = if hsh[:price]
136
+ LimitOrder.new(hsh[:side], UUID.parse(hsh[:id]), hsh[:size], hsh[:price], hsh[:expiration])
137
+ else
138
+ MarketOrder.new(hsh[:side], UUID.parse(hsh[:id]), hsh[:size], hsh[:quote_margin], hsh[:expiration])
139
+ end
140
+
141
+ order.created_at = hsh[:created_at] if hsh[:created_at]
142
+ order
143
+ end
144
+
97
145
  end
98
146
  end
@@ -1,9 +1,12 @@
1
1
  require 'gekko/symbolize_keys'
2
2
 
3
+ require 'oj'
4
+ Oj.default_options = { mode: :compat }
5
+
3
6
  module Gekko
4
7
 
5
8
  #
6
- # Handles JSON serialization and deserialization of trade orders
9
+ # Handles JSON serialization and deserialization
7
10
  #
8
11
  module Serialization
9
12
 
@@ -30,23 +33,6 @@ module Gekko
30
33
  def deserialize(serialized)
31
34
  from_hash(symbolize_keys(Oj.load(serialized)))
32
35
  end
33
-
34
- #
35
- # Initializes a +Gekko::Order+ subclass from a +Hash+ instance
36
- #
37
- # @param hsh [Hash] The order data
38
- # @return [Gekko::Order] A trade order
39
- #
40
- def from_hash(hsh)
41
- order = if hsh[:price]
42
- LimitOrder.new(hsh[:side], UUID.parse(hsh[:id]), hsh[:size], hsh[:price], hsh[:expiration])
43
- else
44
- MarketOrder.new(hsh[:side], UUID.parse(hsh[:id]), hsh[:size], hsh[:quote_margin])
45
- end
46
-
47
- order.created_at = hsh[:created_at] if hsh[:created_at]
48
- order
49
- end
50
36
  end
51
37
 
52
38
  #
@@ -59,31 +45,6 @@ module Gekko
59
45
  Oj.dump(to_hash)
60
46
  end
61
47
 
62
- #
63
- # Returns a +Hash+ representation of this +Order+ instance
64
- #
65
- # @return [Hash] The serializable representation
66
- #
67
- def to_hash
68
- hsh = {
69
- id: id.to_s,
70
- side: side,
71
- size: size,
72
- price: price,
73
- remaining: remaining,
74
- expiration: expiration,
75
- created_at: created_at
76
- }
77
-
78
- if is_a?(Gekko::MarketOrder)
79
- hsh.delete(:price)
80
- hsh[:quote_margin] = quote_margin
81
- hsh[:remaining_quote_margin] = remaining_quote_margin
82
- end
83
-
84
- hsh
85
- end
86
-
87
48
  end
88
49
  end
89
50
 
@@ -5,46 +5,23 @@ module Gekko
5
5
  #
6
6
  class Tape < Array
7
7
 
8
+ include Serialization
9
+
8
10
  # The number of seconds in 24h
9
11
  SECONDS_IN_24H = 60 * 60 * 24
10
12
 
11
13
  attr_accessor :logger, :last_trade_price
12
- attr_reader :volume_24h, :high_24h, :low_24h
14
+ attr_reader :volume_24h, :high_24h, :low_24h, :open_24h, :var_24h
13
15
 
14
16
  def initialize(opts = {})
15
- @logger = opts[:logger]
16
-
17
- @cursor = opts[:cursor] || 0
18
- @cursor_24h = opts[:cursor_24h] || 0
19
- @volume_24h = opts[:volume_24h] || 0
20
- @quote_volume_24h = opts[:quote_volume_24h] || 0
17
+ @logger = opts[:logger]
21
18
 
22
- @low_24h = opts[:low_24h]
23
- @high_24h = opts[:high_24h]
24
- @last_trade_price = opts[:last_trade_price]
25
-
26
- opts[:events] && opts[:events].each_with_index { |obj, idx| self[idx] = obj }
19
+ @cursor = 0
20
+ @cursor_24h = 0
21
+ @volume_24h = 0
22
+ @quote_volume_24h = 0
27
23
  end
28
24
 
29
- #
30
- # Returns this +Tape+ object as a +Hash+ for the purpose of serialization
31
- #
32
- # @return [Hash] The JSON-friendly +Hash+ representation
33
- #
34
- def to_hash
35
- {
36
- cursor: @cursor,
37
- cursor_24h: @cursor_24h,
38
- volume_24h: @volume_24h,
39
- high_24h: @high_24h,
40
- low_24h: @low_24h,
41
- quote_volume_24h: @quote_volume_24h,
42
- last_trade_price: @last_trade_price,
43
- events: self
44
- }
45
- end
46
-
47
-
48
25
  #
49
26
  # Prints a message on the tape
50
27
  #
@@ -107,7 +84,6 @@ module Gekko
107
84
  # @return [Fixnum] The last 24h quote currency volume
108
85
  #
109
86
  def quote_volume_24h
110
- #move_24h_cursor!
111
87
  @quote_volume_24h
112
88
  end
113
89
 
@@ -148,7 +124,7 @@ module Gekko
148
124
  #
149
125
  # Moves the cursor pointing to the first trade that happened during
150
126
  # the last 24h. Every execution getting out of the 24h rolling window is
151
- # passed to Tape#fall_out_of_24h_window
127
+ # passed to Tape#fall_out_of_24h_window
152
128
  #
153
129
  def move_24h_cursor!
154
130
  while(self[@cursor_24h] && (self[@cursor_24h][:time] < time_24h_ago))
@@ -163,15 +139,55 @@ module Gekko
163
139
  #
164
140
  # Updates the low, high, and volumes when an execution falls out of the rolling
165
141
  # previous 24h window
166
- #
142
+ #
167
143
  def fall_out_of_24h_window(execution)
168
144
  @volume_24h -= execution[:base_size]
169
145
  @quote_volume_24h -= execution[:quote_size]
146
+ @open_24h = execution[:price]
147
+ @var_24h = @last_trade_price && ((@last_trade_price - @open_24h) / @open_24h.to_f)
170
148
 
171
149
  if [@high_24h, @low_24h].include?(execution[:price])
172
150
  recalc_high_low_24h!
173
151
  end
174
152
  end
153
+
154
+ #
155
+ # Returns this +Tape+ object as a +Hash+ for the purpose of serialization
156
+ #
157
+ # @return [Hash] The JSON-friendly +Hash+ representation
158
+ #
159
+ def to_hash
160
+ {
161
+ cursor: @cursor,
162
+ cursor_24h: @cursor_24h,
163
+ volume_24h: @volume_24h,
164
+ high_24h: @high_24h,
165
+ low_24h: @low_24h,
166
+ open_24h: @open_24h,
167
+ var_24h: @var_24h,
168
+ quote_volume_24h: @quote_volume_24h,
169
+ last_trade_price: @last_trade_price,
170
+ events: self
171
+ }
172
+ end
173
+
174
+ #
175
+ # Loads a +Tape+ object from a hash
176
+ #
177
+ # @param hsh [Hash] The +Tape+ data
178
+ #
179
+ def self.from_hash(hsh)
180
+ tape = Tape.new
181
+
182
+ hsh[:events].each do |evt|
183
+ e = symbolize_keys(evt)
184
+ e[:type] = e[:type].to_sym
185
+ tape << e
186
+ end
187
+
188
+ tape
189
+ end
190
+
175
191
  end
176
192
  end
177
193
 
@@ -1,6 +1,6 @@
1
1
  module Gekko
2
2
 
3
3
  # The Gekko version string
4
- VERSION = '0.5.2'
4
+ VERSION = '0.8.1'
5
5
 
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gekko
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David FRANCOIS
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-14 00:00:00.000000000 Z
11
+ date: 2016-02-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: uuidtools