gekko 1.1.0 → 1.2.0
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/lib/gekko/book.rb +87 -51
- data/lib/gekko/book_side.rb +38 -3
- data/lib/gekko/order.rb +92 -17
- 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: 4a6c74d9cd4db77db4b045b3cbef970af72ad93b
|
4
|
+
data.tar.gz: 2cc81dd40f090dbe5b1e512a8e973daff6e0310c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1fed15e7db3bc630c741eed42d7e191ce1390894abc9dbfed90e4bcb5161dac6d680ba8bd8aab3198854574abd35e94676d89237ae28685c64565bf4dda43c51
|
7
|
+
data.tar.gz: b66b9ad7f75edea38f2391572a2dbb3d6172008ca96338f4799745d6f22c959112a16406e40c8b31235f7c16c7454358e91d2204b9e320cce633465b628af6ca
|
data/lib/gekko/book.rb
CHANGED
@@ -24,73 +24,110 @@ module Gekko
|
|
24
24
|
self.base_precision = opts[:base_precision] || 8
|
25
25
|
self.multiplier = BigDecimal(10 ** base_precision)
|
26
26
|
self.received = opts[:received] || {}
|
27
|
+
|
28
|
+
@triggered_stops = []
|
27
29
|
end
|
28
30
|
|
29
31
|
#
|
30
32
|
# Receives an order and executes it
|
31
33
|
#
|
32
34
|
# @param order [Order] The order to execute
|
35
|
+
# @param execute_triggered_stops [Boolean] Whether to also execute the STOP orders triggered
|
36
|
+
# by the current received order executions
|
33
37
|
#
|
34
|
-
def receive_order(order)
|
35
|
-
raise
|
38
|
+
def receive_order(order, stop_order_execution = false)
|
39
|
+
raise "Order must be a Gekko::LimitOrder or a Gekko::MarketOrder." unless [LimitOrder, MarketOrder].include?(order.class)
|
40
|
+
raise "Can't receive a new STOP before a first trade has taken place." if order.stop? && ticker[:last].nil?
|
41
|
+
|
42
|
+
# We need to initialize the stop_price for trailing stops if necessary
|
43
|
+
if order.stop? && !order.stop_price
|
44
|
+
if order.stop_percent
|
45
|
+
order.stop_price = ticker[:last] + (ticker[:last] * order.stop_percent / Gekko::Order::TRL_STOP_PCT_MULTIPLIER * (order.bid? ? 1 : -1)).round
|
46
|
+
elsif order.stop_offset
|
47
|
+
order.stop_price = ticker[:last] + order.stop_offset * (order.bid? ? 1 : -1)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# The side of the received order
|
52
|
+
current_side = (order.ask? ? asks : bids)
|
36
53
|
|
37
54
|
# The side from which we'll pop orders
|
38
55
|
opposite_side = order.bid? ? asks : bids
|
39
56
|
|
40
|
-
if received.has_key?(order.id.to_s)
|
57
|
+
if received.has_key?(order.id.to_s) && !stop_order_execution
|
41
58
|
tape << order.message(:reject, reason: :duplicate_id)
|
42
59
|
|
43
|
-
|
44
|
-
|
60
|
+
else
|
61
|
+
self.received[order.id.to_s] = order
|
45
62
|
|
46
|
-
|
47
|
-
|
63
|
+
if order.expired?
|
64
|
+
tape << order.message(:reject, reason: :expired)
|
48
65
|
|
49
|
-
|
50
|
-
|
66
|
+
elsif order.stop? && !order.should_trigger?(tape.last_trade_price)
|
67
|
+
current_side.stops << order # Add the STOP to the list of currently active STOPs
|
51
68
|
|
52
|
-
|
53
|
-
|
69
|
+
elsif order.post_only && order.crosses?(opposite_side.first)
|
70
|
+
tape << order.message(:reject, reason: :would_execute)
|
54
71
|
|
55
|
-
|
56
|
-
|
57
|
-
|
72
|
+
else
|
73
|
+
old_ticker = ticker
|
74
|
+
tape << order.message(:received)
|
75
|
+
|
76
|
+
order_side = order.bid? ? bids : asks
|
77
|
+
next_match = opposite_side.first
|
78
|
+
prev_match_id = nil
|
58
79
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
80
|
+
while !order.done? && order.crosses?(next_match)
|
81
|
+
# If we match against the same order twice in a row, something went seriously
|
82
|
+
# wrong, we'd rather noisily die at this point.
|
83
|
+
raise 'Infinite matching loop detected !!' if (prev_match_id == next_match.id)
|
84
|
+
prev_match_id = next_match.id
|
64
85
|
|
65
|
-
|
66
|
-
|
67
|
-
|
86
|
+
if next_match.expired?
|
87
|
+
tape << opposite_side.shift.message(:done, reason: :expired)
|
88
|
+
next_match = opposite_side.first
|
68
89
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
90
|
+
elsif order.uid == next_match.uid
|
91
|
+
# Same user/account associated to order, we cancel the next match
|
92
|
+
tape << opposite_side.shift.message(:done, reason: :canceled)
|
93
|
+
next_match = opposite_side.first
|
94
|
+
|
95
|
+
else
|
96
|
+
execute_trade(next_match, order)
|
97
|
+
|
98
|
+
if next_match.filled?
|
99
|
+
tape << opposite_side.shift.message(:done, reason: :filled)
|
100
|
+
next_match = opposite_side.first
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
73
104
|
|
105
|
+
if order.filled?
|
106
|
+
tape << order.message(:done, reason: :filled)
|
107
|
+
elsif order.fill_or_kill?
|
108
|
+
tape << order.message(:done, reason: :killed)
|
74
109
|
else
|
75
|
-
|
110
|
+
order_side.insert_order(order)
|
111
|
+
tape << order.message(:open)
|
112
|
+
end
|
76
113
|
|
77
|
-
|
78
|
-
|
79
|
-
|
114
|
+
current_side.stops.each do |stop|
|
115
|
+
if stop.should_trigger?(tape.last_trade_price)
|
116
|
+
@triggered_stops << current_side.stops.delete(stop)
|
80
117
|
end
|
81
118
|
end
|
82
|
-
end
|
83
119
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
120
|
+
opposite_side.stops.each { |s| s.update_trailing_stop(tape.last_trade_price) }
|
121
|
+
|
122
|
+
if !stop_order_execution
|
123
|
+
# We only want to execute triggered stops at the top level as to correctly order them
|
124
|
+
while t = @triggered_stops.shift
|
125
|
+
receive_order(t, true)
|
126
|
+
end
|
92
127
|
|
93
|
-
|
128
|
+
tick! unless (ticker == old_ticker)
|
129
|
+
end
|
130
|
+
end
|
94
131
|
end
|
95
132
|
end
|
96
133
|
|
@@ -154,10 +191,14 @@ module Gekko
|
|
154
191
|
prev_ask = ask
|
155
192
|
|
156
193
|
order = received[order_id.to_s]
|
157
|
-
dels = order.bid? ? bids.delete(order) : asks.delete(order)
|
158
|
-
dels && tape << order.message(:done, reason: :canceled)
|
159
194
|
|
160
|
-
|
195
|
+
if order
|
196
|
+
s = order.bid? ? bids : asks
|
197
|
+
dels = s.delete(order) || s.stops.delete(order)
|
198
|
+
dels && tape << order.message(:done, reason: :canceled)
|
199
|
+
|
200
|
+
tick! if (prev_bid != bid) || (prev_ask != ask)
|
201
|
+
end
|
161
202
|
end
|
162
203
|
|
163
204
|
#
|
@@ -168,12 +209,7 @@ module Gekko
|
|
168
209
|
prev_ask = ask
|
169
210
|
|
170
211
|
[bids, asks].each do |bs|
|
171
|
-
bs.
|
172
|
-
if order.expired?
|
173
|
-
tape << order.message(:done, reason: :expired)
|
174
|
-
true
|
175
|
-
end
|
176
|
-
end
|
212
|
+
bs.remove_expired! { |tape_msg| tape << tape_msg }
|
177
213
|
end
|
178
214
|
|
179
215
|
tick! if (prev_bid != bid) || (prev_ask != ask)
|
@@ -256,8 +292,8 @@ module Gekko
|
|
256
292
|
#
|
257
293
|
def self.from_hash(hsh)
|
258
294
|
book = Book.new(hsh[:pair], {
|
259
|
-
bids: BookSide.
|
260
|
-
asks: BookSide.
|
295
|
+
bids: BookSide.from_hash(:bid, hsh[:bids]),
|
296
|
+
asks: BookSide.from_hash(:ask, hsh[:asks])
|
261
297
|
})
|
262
298
|
|
263
299
|
[:bids, :asks].each { |s| book.send(s).each { |ord| book.received[ord.id.to_s] = ord } }
|
data/lib/gekko/book_side.rb
CHANGED
@@ -5,16 +5,23 @@ module Gekko
|
|
5
5
|
#
|
6
6
|
class BookSide < Array
|
7
7
|
|
8
|
-
|
8
|
+
include Serialization
|
9
|
+
|
10
|
+
attr_accessor :side, :stops
|
9
11
|
|
10
12
|
def initialize(side, opts = {})
|
13
|
+
super()
|
14
|
+
|
11
15
|
raise "Incorrect side <#{side}>" unless [:bid, :ask].include?(side)
|
12
|
-
@side
|
16
|
+
@side = side
|
17
|
+
@stops = []
|
13
18
|
|
14
19
|
if opts[:orders]
|
15
20
|
opts[:orders].each_with_index { |obj, idx| self[idx] = Order.from_hash(obj) }
|
16
21
|
sort!
|
17
22
|
end
|
23
|
+
|
24
|
+
opts[:stops].each_with_index { |obj, idx| stops[idx] = Order.from_hash(obj) } if opts[:stops]
|
18
25
|
end
|
19
26
|
|
20
27
|
#
|
@@ -23,7 +30,21 @@ module Gekko
|
|
23
30
|
# @return [Hash] The serializable representation
|
24
31
|
#
|
25
32
|
def to_hash
|
26
|
-
map(&:to_hash)
|
33
|
+
{ orders: map(&:to_hash), stops: stops.map(&:to_hash) }
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Returns a +Gekko::BookSide+ instance from a hash
|
38
|
+
#
|
39
|
+
# @param side [Symbol] Either +:bid+ or +:ask+
|
40
|
+
# @param h [Hash] The hash representation of a book side
|
41
|
+
# @return [Gekko::BookSide] The represented book side
|
42
|
+
#
|
43
|
+
def self.from_hash(side, h)
|
44
|
+
h = symbolize_keys(h)
|
45
|
+
bs = new(side, orders: h[:orders].map { |o| symbolize_keys(o) })
|
46
|
+
bs.stops = h[:stops].map { |o| Gekko::Order.from_hash(symbolize_keys(o)) }
|
47
|
+
bs
|
27
48
|
end
|
28
49
|
|
29
50
|
#
|
@@ -64,5 +85,19 @@ module Gekko
|
|
64
85
|
side == :bid
|
65
86
|
end
|
66
87
|
|
88
|
+
#
|
89
|
+
# Removes the expired book orders and STOPs from this side
|
90
|
+
#
|
91
|
+
def remove_expired!
|
92
|
+
[self, stops].each do |orders|
|
93
|
+
orders.reject! do |order|
|
94
|
+
if order.expired?
|
95
|
+
yield(order.message(:done, reason: :expired))
|
96
|
+
true
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
67
102
|
end
|
68
103
|
end
|
data/lib/gekko/order.rb
CHANGED
@@ -9,24 +9,40 @@ module Gekko
|
|
9
9
|
|
10
10
|
include Serialization
|
11
11
|
|
12
|
-
|
12
|
+
#
|
13
|
+
# Multiplier applied to the trailing STOP percentage to be able to pass it around as integer
|
14
|
+
#
|
15
|
+
TRL_STOP_PCT_MULTIPLIER = BigDecimal(10000)
|
16
|
+
|
17
|
+
attr_accessor :id, :uid, :side, :size, :remaining, :price, :expiration, :created_at, :post_only,
|
18
|
+
:stop_price, :stop_percent, :stop_offset
|
13
19
|
|
14
20
|
def initialize(side, id, uid, size, opts = {})
|
15
|
-
@id
|
16
|
-
@uid
|
17
|
-
@side
|
18
|
-
@size
|
19
|
-
@remaining
|
20
|
-
@expiration
|
21
|
-
@created_at
|
22
|
-
@post_only
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
raise '
|
28
|
-
raise '
|
29
|
-
raise '
|
21
|
+
@id = id
|
22
|
+
@uid = uid
|
23
|
+
@side = side && side.to_sym
|
24
|
+
@size = size
|
25
|
+
@remaining = @size
|
26
|
+
@expiration = opts[:expiration]
|
27
|
+
@created_at = Time.now.to_f
|
28
|
+
@post_only = opts[:post_only]
|
29
|
+
@stop_price = opts[:stop_price]
|
30
|
+
@stop_percent = opts[:stop_percent]
|
31
|
+
@stop_offset = opts[:stop_offset]
|
32
|
+
|
33
|
+
raise 'Orders must have an UUID' unless @id && @id.is_a?(UUID)
|
34
|
+
raise 'Orders must have a user ID' unless @uid && @uid.is_a?(UUID)
|
35
|
+
raise 'Side must be either :bid or :ask' unless [:bid, :ask].include?(@side)
|
36
|
+
raise 'Size must be a positive integer' if (@size && (!@size.is_a?(Fixnum) || @size <= 0))
|
37
|
+
raise 'Stop price must be a positive integer' if (@stop_price && (!@stop_price.is_a?(Fixnum) || @stop_price <= 0))
|
38
|
+
raise 'Trailing percentage must be a positive integer' if (@stop_percent && (!@stop_percent.is_a?(Fixnum) || @stop_percent <= 0 || @stop_percent >= TRL_STOP_PCT_MULTIPLIER))
|
39
|
+
raise 'Trailing offset must be a positive integer' if (@stop_offset && (!@stop_offset.is_a?(Fixnum) || @stop_offset <= 0))
|
40
|
+
raise 'Expiration must be omitted or be an integer' unless (@expiration.nil? || (@expiration.is_a?(Fixnum) && @expiration > 0))
|
41
|
+
raise 'The order creation timestamp can''t be nil' if !@created_at
|
42
|
+
|
43
|
+
if (((@stop_price && 1 )|| 0) + ((@stop_percent && 1 ) || 0) + ((@stop_offset && 1) || 0)) > 1
|
44
|
+
raise 'Stop orders must specify exactly one of either stop price, trailing percentage, or trailing offset.'
|
45
|
+
end
|
30
46
|
end
|
31
47
|
|
32
48
|
#
|
@@ -45,6 +61,28 @@ module Gekko
|
|
45
61
|
end
|
46
62
|
end
|
47
63
|
|
64
|
+
#
|
65
|
+
# Returns +true+ if the order is a STOP order, +false+ otherwise
|
66
|
+
#
|
67
|
+
# @return [Boolean] Whether this order is a STOP
|
68
|
+
#
|
69
|
+
def stop?
|
70
|
+
!!(stop_price || stop_percent || stop_offset)
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Returns +true+ if the given price should trigger this order's execution.
|
75
|
+
#
|
76
|
+
# @param p [Fixnum] The price to which we want to compare the STOP price
|
77
|
+
# @return [Boolean] Whether this order should trigger
|
78
|
+
#
|
79
|
+
def should_trigger?(p)
|
80
|
+
p || raise("Provided price can't be nil")
|
81
|
+
stop? || raise("Called Order#should_trigger? on a non-stop order")
|
82
|
+
|
83
|
+
(bid? && (stop_price <= p)) || (ask? && (stop_price >= p))
|
84
|
+
end
|
85
|
+
|
48
86
|
#
|
49
87
|
# Creates a message in order to print it ont the tape
|
50
88
|
#
|
@@ -126,6 +164,16 @@ module Gekko
|
|
126
164
|
hsh[:remaining_quote_margin] = remaining_quote_margin
|
127
165
|
end
|
128
166
|
|
167
|
+
if stop?
|
168
|
+
hsh.merge!({
|
169
|
+
stop: {
|
170
|
+
price: stop_price,
|
171
|
+
offset: stop_offset,
|
172
|
+
percent: stop_percent
|
173
|
+
}
|
174
|
+
})
|
175
|
+
end
|
176
|
+
|
129
177
|
hsh
|
130
178
|
end
|
131
179
|
|
@@ -136,7 +184,34 @@ module Gekko
|
|
136
184
|
# @return [Gekko::Order] A trade order
|
137
185
|
#
|
138
186
|
def self.from_hash(hsh)
|
139
|
-
(hsh[:price] ? LimitOrder : MarketOrder).from_hash(hsh)
|
187
|
+
order = (hsh[:price] ? LimitOrder : MarketOrder).from_hash(hsh)
|
188
|
+
|
189
|
+
if hsh[:stop]
|
190
|
+
hsh[:stop] = symbolize_keys(hsh[:stop])
|
191
|
+
order.stop_price = hsh[:stop][:price]
|
192
|
+
order.stop_offset = hsh[:stop][:offset]
|
193
|
+
order.stop_percent = hsh[:stop][:percent]
|
194
|
+
end
|
195
|
+
|
196
|
+
order
|
197
|
+
end
|
198
|
+
|
199
|
+
#
|
200
|
+
# Updates the stop price according to either the +stop_percent+ or
|
201
|
+
# or the +stop_offset+ attributes
|
202
|
+
#
|
203
|
+
# @param p [Fixnum] The price used to update the +stop_price+
|
204
|
+
#
|
205
|
+
def update_trailing_stop(p)
|
206
|
+
sign = bid? ? 1 : -1
|
207
|
+
|
208
|
+
if stop_percent
|
209
|
+
new_price = p + (p * stop_percent * sign / TRL_STOP_PCT_MULTIPLIER).round
|
210
|
+
self.stop_price = new_price if (bid? && (new_price < stop_price)) || (ask? && (new_price > stop_price))
|
211
|
+
elsif stop_offset
|
212
|
+
new_price = p + stop_offset * sign
|
213
|
+
self.stop_price = new_price if (bid? && (new_price < stop_price)) || (ask? && (new_price > stop_price))
|
214
|
+
end
|
140
215
|
end
|
141
216
|
|
142
217
|
end
|
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: 1.
|
4
|
+
version: 1.2.0
|
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-02-
|
11
|
+
date: 2016-02-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: uuidtools
|