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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b6a24e973cb010d9b1f8067d9bea9cb30a1c3bc4
4
- data.tar.gz: f120b77ebf50b3a973a886dc4b8561249c395169
3
+ metadata.gz: 4a6c74d9cd4db77db4b045b3cbef970af72ad93b
4
+ data.tar.gz: 2cc81dd40f090dbe5b1e512a8e973daff6e0310c
5
5
  SHA512:
6
- metadata.gz: 0d14739c0de58e63f2ce1039018911f3689a59c833f1871e136ef6076730337493d709b2efe9e43ff4883228ae795a76a8a66c7c89a3d68dad0c0229132e29ea
7
- data.tar.gz: 396fb3177eb9c2fbcdf289fe36ee1cb6abb1658aa4db4eda8b7f3bcbf5e08ec60ee799f487cc3793f8f57f740f06b147d50680208438b4007f8bbd399d672cef
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 'Order must be a Gekko::LimitOrder or a Gekko::MarketOrder' unless [LimitOrder, MarketOrder].include?(order.class)
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
- elsif order.expired?
44
- tape << order.message(:reject, reason: :expired)
60
+ else
61
+ self.received[order.id.to_s] = order
45
62
 
46
- elsif order.post_only && order.crosses?(opposite_side.first)
47
- tape << order.message(:reject, reason: :would_execute)
63
+ if order.expired?
64
+ tape << order.message(:reject, reason: :expired)
48
65
 
49
- else
50
- old_ticker = ticker
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
- self.received[order.id.to_s] = order
53
- tape << order.message(:received)
69
+ elsif order.post_only && order.crosses?(opposite_side.first)
70
+ tape << order.message(:reject, reason: :would_execute)
54
71
 
55
- order_side = order.bid? ? bids : asks
56
- next_match = opposite_side.first
57
- prev_match_id = nil
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
- while !order.done? && order.crosses?(next_match)
60
- # If we match against the same order twice in a row, something went seriously
61
- # wrong, we'd rather noisily die at this point.
62
- raise 'Infinite matching loop detected !!' if (prev_match_id == next_match.id)
63
- prev_match_id = next_match.id
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
- if next_match.expired?
66
- tape << opposite_side.shift.message(:done, reason: :expired)
67
- next_match = opposite_side.first
86
+ if next_match.expired?
87
+ tape << opposite_side.shift.message(:done, reason: :expired)
88
+ next_match = opposite_side.first
68
89
 
69
- elsif order.uid == next_match.uid
70
- # Same user/account associated to order, we cancel the next match
71
- tape << opposite_side.shift.message(:done, reason: :canceled)
72
- next_match = opposite_side.first
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
- execute_trade(next_match, order)
110
+ order_side.insert_order(order)
111
+ tape << order.message(:open)
112
+ end
76
113
 
77
- if next_match.filled?
78
- tape << opposite_side.shift.message(:done, reason: :filled)
79
- next_match = opposite_side.first
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
- if order.filled?
85
- tape << order.message(:done, reason: :filled)
86
- elsif order.fill_or_kill?
87
- tape << order.message(:done, reason: :killed)
88
- else
89
- order_side.insert_order(order)
90
- tape << order.message(:open)
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
- tick! unless (ticker == old_ticker)
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
- tick! if (prev_bid != bid) || (prev_ask != ask)
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.reject! do |order|
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.new(:bid, orders: hsh[:bids].map { |o| symbolize_keys(o) }),
260
- asks: BookSide.new(:ask, orders: hsh[:asks].map { |o| symbolize_keys(o) })
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 } }
@@ -5,16 +5,23 @@ module Gekko
5
5
  #
6
6
  class BookSide < Array
7
7
 
8
- attr_accessor :side
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 = 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
- attr_accessor :id, :uid, :side, :size, :remaining, :price, :expiration, :created_at, :post_only
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 = id
16
- @uid = uid
17
- @side = side && side.to_sym
18
- @size = size
19
- @remaining = @size
20
- @expiration = opts[:expiration]
21
- @created_at = Time.now.to_f
22
- @post_only = opts[:post_only]
23
-
24
- raise 'Orders must have an UUID' unless @id && @id.is_a?(UUID)
25
- raise 'Orders must have a user ID' unless @uid && @uid.is_a?(UUID)
26
- raise 'Side must be either :bid or :ask' unless [:bid, :ask].include?(@side)
27
- raise 'Size must be a positive integer' if (@size && (!@size.is_a?(Fixnum) || @size <= 0))
28
- raise 'Expiration must be omitted or be an integer' unless (@expiration.nil? || (@expiration.is_a?(Fixnum) && @expiration > 0))
29
- raise 'The order creation timestamp can''t be nil' if !@created_at
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
@@ -1,6 +1,6 @@
1
1
  module Gekko
2
2
 
3
3
  # The Gekko version string
4
- VERSION = '1.1.0'
4
+ VERSION = '1.2.0'
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: 1.1.0
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-17 00:00:00.000000000 Z
11
+ date: 2016-02-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: uuidtools