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 +4 -4
- data/README.md +0 -1
- data/lib/gekko.rb +1 -0
- data/lib/gekko/book.rb +42 -33
- data/lib/gekko/book_side.rb +1 -0
- data/lib/gekko/limit_order.rb +1 -0
- data/lib/gekko/market_order.rb +14 -6
- data/lib/gekko/order.rb +52 -4
- data/lib/gekko/serialization.rb +4 -43
- data/lib/gekko/tape.rb +50 -34
- data/lib/gekko/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 981305466dff1fb589c97f96fb4be28ca1b4e2ce
|
4
|
+
data.tar.gz: 123b650f9b0774614710cec547990609786f1dc2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
data/lib/gekko.rb
CHANGED
data/lib/gekko/book.rb
CHANGED
@@ -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
|
-
|
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
|
61
|
-
|
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) /
|
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 > (
|
68
|
-
quote_size = (
|
69
|
-
order.remaining_quote_margin -= quote_size if order.
|
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
|
-
|
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: :
|
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
|
-
|
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
|
-
#
|
204
|
+
# Returns a +Hash+ representation of this +Book+ instance
|
197
205
|
#
|
198
|
-
# @return [
|
206
|
+
# @return [Hash] The serializable representation
|
199
207
|
#
|
200
|
-
def
|
201
|
-
|
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
|
221
|
+
# Loads the book from a hash
|
214
222
|
#
|
215
|
-
# @param
|
216
|
-
# @return [Gekko::Book] The
|
223
|
+
# @param hsh [Hash] A Book hash
|
224
|
+
# @return [Gekko::Book] The loaded book instance
|
217
225
|
#
|
218
|
-
def self.
|
219
|
-
|
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
|
-
|
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
|
-
|
234
|
+
book
|
226
235
|
end
|
227
236
|
|
228
237
|
end
|
data/lib/gekko/book_side.rb
CHANGED
data/lib/gekko/limit_order.rb
CHANGED
data/lib/gekko/market_order.rb
CHANGED
@@ -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
|
-
|
18
|
-
|
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
|
-
|
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? ||
|
40
|
+
filled? ||
|
41
|
+
(bid? && remaining_quote_margin.zero?)
|
34
42
|
end
|
35
43
|
|
36
44
|
end
|
data/lib/gekko/order.rb
CHANGED
@@ -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
|
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
|
data/lib/gekko/serialization.rb
CHANGED
@@ -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
|
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
|
|
data/lib/gekko/tape.rb
CHANGED
@@ -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
|
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
|
-
@
|
23
|
-
@
|
24
|
-
@
|
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
|
|
data/lib/gekko/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2016-02-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: uuidtools
|