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