gekko 0.5.2 → 0.8.1

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