gekko 1.1.0 → 1.2.0

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