gekko 0.0.1 → 0.1.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: 5d4ac3dfd5190f9f36de0f900b461fa2c5d77682
4
- data.tar.gz: 8e19f4f203d0ffbc4444a516d89476bd12ab9328
3
+ metadata.gz: c22a43388f516eb29f65b1c7439018b7d0b62b2d
4
+ data.tar.gz: f5b5b3011d40a9dfa97b3573b166af0c772023cd
5
5
  SHA512:
6
- metadata.gz: 4c398ee4e020fc3f5d3f35786a4d4161d884fb10830a220ad425ae62ad93069196ec3d842daab673b3ae5f4b0b37ef7679fadc7224592c5e3f467e8531e8ae4f
7
- data.tar.gz: 6c45b8eef3dfc3b4d479b6ca0ea0aa29adee4bb80da1b9e6e28846e073ff8b9559f9abf2131bf21c5db234a06315055bd4d3d48ba22a6e8ed6521104de42a176
6
+ metadata.gz: 2c486804dc2109f896f2d98cb160b099e73dbe157c465f21bf61f7cfa56f36794ceac3886677ebd802102bc90d0c5e0059aeaa354c590047b152a39746dd6bac
7
+ data.tar.gz: 5c303f98396a43ba09640630f8bb8f649b064819f05a5b76c4fc1e75f7bdc0d2e58411bb8fc186d18b96006e5b1aa916750ce60d9915fb5e9fb64eae5baceed3
@@ -4,6 +4,8 @@ require 'uuidtools'
4
4
  UUID = UUIDTools::UUID
5
5
 
6
6
  require 'gekko/order'
7
+ require 'gekko/limit_order'
8
+ require 'gekko/market_order'
7
9
  require 'gekko/book'
8
10
 
9
11
  # The Gekko top-level module
@@ -16,13 +16,15 @@ module Gekko
16
16
  # The default minimum price increment accepted for placed orders
17
17
  DEFAULT_TICK_SIZE = 1000
18
18
 
19
- attr_accessor :pair, :bids, :asks, :tape, :tick_size
19
+ attr_accessor :pair, :bids, :asks, :tape, :tick_size, :received, :base_precision
20
20
 
21
21
  def initialize(pair, opts = {})
22
- self.pair = pair
23
- self.bids = BookSide.new(:bid)
24
- self.asks = BookSide.new(:ask)
25
- self.tape = Tape.new(opts[:logger])
22
+ self.pair = pair
23
+ self.bids = BookSide.new(:bid)
24
+ self.asks = BookSide.new(:ask)
25
+ self.tape = Tape.new(opts[:logger])
26
+ self.base_precision = 8
27
+ self.received = {}
26
28
 
27
29
  self.tick_size = opts[:tick_size] || DEFAULT_TICK_SIZE
28
30
  raise "Tick size must be a positive integer if provided" if tick_size && (!tick_size.is_a?(Fixnum) || tick_size <= 0)
@@ -35,44 +37,68 @@ module Gekko
35
37
  #
36
38
  def receive_order(order)
37
39
 
38
- raise Gekko::TickSizeMismatch unless (order.price % tick_size).zero?
40
+ raise 'Order must be a Gekko::LimitOrder or a Gekko::MarketOrder' unless [LimitOrder, MarketOrder].include?(order.class)
39
41
 
40
- order_side = order.bid? ? bids : asks
41
- opposite_side = order.bid? ? asks : bids
42
- next_match = opposite_side.first
42
+ raise Gekko::TickSizeMismatch unless (order.is_a?(MarketOrder) || (order.price % tick_size).zero?)
43
43
 
44
- while !order.filled? && order.crosses?(next_match)
44
+ if received.has_key?(order.id.to_s)
45
+ tape << order.message(:reject, reason: "Duplicate ID <#{order.id.to_s}>")
45
46
 
46
- trade_price = next_match.price
47
- base_size = [next_match.remaining, order.remaining].min
48
-
49
- quoted_size = base_size / trade_price
50
-
51
- tape << {
52
- type: :execution,
53
- price: trade_price,
54
- base_size: base_size,
55
- quoted_size: quoted_size,
56
- maker_order_id: next_match.id,
57
- taker_order_id: order.id,
58
- time: Time.now.to_f,
59
- tick: order.bid? ? :up : :down
60
- }
61
-
62
- order.remaining -= base_size
63
- next_match.remaining -= base_size
64
-
65
- if next_match.filled?
66
- tape << opposite_side.shift.message(:done, reason: :filled)
67
- next_match = opposite_side.first
47
+ else
48
+ self.received[order.id.to_s] = order
49
+ tape << order.message(:received)
50
+
51
+ order_side = order.bid? ? bids : asks
52
+ opposite_side = order.bid? ? asks : bids
53
+ next_match = opposite_side.first
54
+
55
+ while !order.done? && order.crosses?(next_match)
56
+ trade_price = next_match.price
57
+ base_size = [next_match.remaining, order.remaining].min
58
+
59
+ if order.is_a?(LimitOrder)
60
+ quote_size = (base_size * trade_price) / (10 ** base_precision)
61
+
62
+ elsif order.is_a?(MarketOrder)
63
+ if order.ask? || (order.remaining_quote_margin > ((trade_price * base_size) / (10 ** base_precision)))
64
+ quote_size = ((trade_price * base_size) / (10 ** base_precision))
65
+ order.remaining_quote_margin -= quote_size if order.bid?
66
+ elsif order.bid?
67
+ quote_size = order.remaining_quote_margin
68
+ base_size = (order.remaining_quote_margin * (10 ** base_precision)) / trade_price
69
+ order.remaining_quote_margin -= quote_size
70
+ end
71
+ end
72
+
73
+
74
+ tape << {
75
+ type: :execution,
76
+ price: trade_price,
77
+ base_size: base_size,
78
+ quote_size: quote_size,
79
+ maker_order_id: next_match.id.to_s,
80
+ taker_order_id: order.id.to_s,
81
+ time: Time.now.to_f,
82
+ tick: order.bid? ? :up : :down
83
+ }
84
+
85
+ order.remaining -= base_size
86
+ next_match.remaining -= base_size
87
+
88
+ if next_match.filled?
89
+ tape << opposite_side.shift.message(:done, reason: :filled)
90
+ next_match = opposite_side.first
91
+ end
68
92
  end
69
- end
70
93
 
71
- if order.filled?
72
- tape << order.message(:done, reason: :filled)
73
- else
74
- order_side.insert_order(order)
75
- tape << order.message(:open)
94
+ if order.filled?
95
+ tape << order.message(:done, reason: :filled)
96
+ elsif order.fill_or_kill?
97
+ tape << order.message(:done, reason: :killed)
98
+ else
99
+ order_side.insert_order(order)
100
+ tape << order.message(:open)
101
+ end
76
102
  end
77
103
  end
78
104
 
@@ -100,6 +126,23 @@ module Gekko
100
126
  ask && bid && (ask - bid)
101
127
  end
102
128
 
129
+ #
130
+ # Returns the current ticker
131
+ #
132
+ # @return [Hash] The current ticker
133
+ #
134
+ def ticker
135
+ v24h = tape.volume_24h
136
+ {
137
+ last: tape.last_trade_price,
138
+ bid: bid,
139
+ ask: ask,
140
+ spread: spread,
141
+ volume_24h: v24h,
142
+ vwap_24h: (v24h > 0) && (tape.quote_volume_24h * (10 ** base_precision)/ v24h)
143
+ }
144
+ end
145
+
103
146
  end
104
147
  end
105
148
 
@@ -0,0 +1,29 @@
1
+ module Gekko
2
+
3
+ #
4
+ # Represents a limit order. These order must specify a price.
5
+ #
6
+ class LimitOrder < Order
7
+
8
+ attr_accessor :price
9
+
10
+ def initialize(side, id, size, price, expiration = nil)
11
+ super(side, id, size, expiration)
12
+ @price = price
13
+ raise 'Price must be a positive integer' if @price.nil? || (!@price.is_a?(Fixnum) || (@price <= 0))
14
+ end
15
+
16
+ #
17
+ # Returns +true+ if the order is filled
18
+ #
19
+ def filled?
20
+ remaining.zero?
21
+ end
22
+
23
+ def done?
24
+ filled?
25
+ end
26
+
27
+ end
28
+ end
29
+
@@ -0,0 +1,36 @@
1
+ module Gekko
2
+
3
+ #
4
+ # Represents a market order. If a bid, it must specify the maximum spendable quote
5
+ # currency as remaining quote margin
6
+ #
7
+ class MarketOrder < Order
8
+
9
+ attr_accessor :quote_margin, :remaining_quote_margin
10
+
11
+ def initialize(side, id, size, quote_margin, expiration = nil)
12
+ super(side, id, size, expiration)
13
+ @quote_margin = quote_margin
14
+ @remaining_quote_margin = @quote_margin
15
+ raise 'Quote currency margin must be provided for a market bid' if quote_margin.nil? && bid?
16
+ raise 'Quote currency margin can not be specified for a market ask' if quote_margin && ask?
17
+ end
18
+
19
+ #
20
+ # Returns +true+ if the order is filled
21
+ #
22
+ def filled?
23
+ remaining.zero?
24
+ end
25
+
26
+ #
27
+ # Returns +true+ if the order has been filled or can not keep
28
+ # executing further due to quote currency margin constraints
29
+ #
30
+ def done?
31
+ filled? || (bid? && remaining_quote_margin.zero?)
32
+ end
33
+
34
+ end
35
+ end
36
+
@@ -2,7 +2,7 @@ module Gekko
2
2
 
3
3
  #
4
4
  # Represents a trade order. Trade orders can be either buy (bid) or sell (ask) orders.
5
- # All orders are identified by an UUID, must specify a size, a price, and an optional
5
+ # All orders are identified by an UUID, and must specify a size, and an optional
6
6
  # expiration timestamp.
7
7
  #
8
8
  class Order
@@ -11,38 +11,33 @@ module Gekko
11
11
 
12
12
  def initialize(side, id, size, price, expiration = nil)
13
13
  @id = id
14
- @side = side
15
- @size = size
14
+ @side = side && side.to_sym
15
+ @size = size
16
16
  @remaining = @size
17
- @price = price
18
17
  @expiration = expiration
19
18
  @created_at = Time.now.to_f
20
19
 
21
- raise 'Orders must have an UUID' unless @id && @id.is_a?(UUID)
22
- raise 'Side must be either :bid or :ask' unless [:bid, :ask].include?(side)
23
- raise 'Price must be a positive integer or be omitted' if (@price && (!@price.is_a?(Fixnum) || (@price <= 0)))
24
- raise 'Size must be a positive integer' if (@size && (!@size.is_a?(Fixnum) || @size <= 0))
25
- raise 'Expiration must be omitted or be an integer' unless (@expiration.nil? || (@expiration.is_a?(Fixnum) && @expiration > 0))
26
- raise 'The order creation timestamp can''t be nil' if !@created_at
20
+ raise 'Orders must have an UUID' unless @id && @id.is_a?(UUID)
21
+ raise 'Side must be either :bid or :ask' unless [:bid, :ask].include?(@side)
22
+ raise 'Size must be a positive integer' if (@size && (!@size.is_a?(Fixnum) || @size <= 0))
23
+ raise 'Expiration must be omitted or be an integer' unless (@expiration.nil? || (@expiration.is_a?(Fixnum) && @expiration > 0))
24
+ raise 'The order creation timestamp can''t be nil' if !@created_at
27
25
  end
28
26
 
29
27
  #
30
- # Returns +true+ if this order can execute against +other_order+
28
+ # Returns +true+ if this order can execute against +limit_order+
31
29
  #
32
- # @param other [Order] The other order against which we want
30
+ # @param limit_order [LimitOrder] The limit order against which we want
33
31
  # to know if an execution is possible
34
32
  #
35
- def crosses?(other)
36
- if other && (bid? ^ other.bid?)
37
- (bid? && (price >= other.price)) || (ask? && (price <= other.price))
38
- end
39
- end
33
+ def crosses?(limit_order)
34
+ if limit_order
35
+ raise 'Can not test againt a market order' unless limit_order.is_a?(LimitOrder)
40
36
 
41
- #
42
- # Returns +true+ if the order is filled
43
- #
44
- def filled?
45
- remaining.zero?
37
+ if bid? ^ limit_order.bid?
38
+ is_a?(MarketOrder) || (bid? && (price >= limit_order.price)) || (ask? && (price <= limit_order.price))
39
+ end
40
+ end
46
41
  end
47
42
 
48
43
  #
@@ -57,7 +52,7 @@ module Gekko
57
52
  def message(type, extra_attrs = {})
58
53
  {
59
54
  type: type,
60
- order_id: id,
55
+ order_id: id.to_s,
61
56
  side: side,
62
57
  size: size,
63
58
  remaining: remaining,
@@ -79,5 +74,13 @@ module Gekko
79
74
  !bid?
80
75
  end
81
76
 
77
+ #
78
+ # Returns +true+ if this order isn't supposed to stick around in
79
+ # the order book
80
+ #
81
+ def fill_or_kill?
82
+ is_a?(Gekko::MarketOrder)
83
+ end
84
+
82
85
  end
83
86
  end
@@ -3,13 +3,16 @@ module Gekko
3
3
  #
4
4
  # Records the trading engine messages sequentially
5
5
  #
6
- class Tape
6
+ class Tape < Array
7
7
 
8
- attr_accessor :events, :logger
8
+ attr_accessor :logger, :last_trade_price
9
9
 
10
10
  def initialize(logger = nil)
11
- @events = []
12
- @logger = logger
11
+ @logger = logger
12
+ @cursor = 0
13
+ @cursor_24h = 0
14
+ @volume_24h = 0
15
+ @quote_volume_24h = 0
13
16
  end
14
17
 
15
18
  #
@@ -18,10 +21,71 @@ module Gekko
18
21
  # @param message [Hash] The message to record
19
22
  #
20
23
  def <<(message)
21
- message[:sequence] = events.length
24
+ message[:sequence] = length
22
25
  logger && logger.info(message)
23
- events << message
26
+
27
+ if message[:type] == :execution
28
+ # Keep last price up to date
29
+ @last_trade_price = message[:price]
30
+
31
+ # Keep 24h volume up to date
32
+ @volume_24h += message[:base_size]
33
+ @quote_volume_24h += message[:quote_size]
34
+ move_24h_cursor!
35
+ end
36
+
37
+ super(message)
38
+ end
39
+
40
+ #
41
+ # Returns the next unread element from the tape
42
+ #
43
+ # @return [Hash] The next unread element
44
+ #
45
+ def next
46
+ if @cursor < length
47
+ n = self[@cursor]
48
+ @cursor += 1
49
+ n
50
+ end
51
+ end
52
+
53
+ #
54
+ # Returns the traded volume for the last 24h
55
+ #
56
+ # @return [Fixnum] The last 24h volume
57
+ #
58
+ def volume_24h
59
+ move_24h_cursor!
60
+ @volume_24h
61
+ end
62
+
63
+ #
64
+ # Returns the traded amount of quote currency in the last 24h
65
+ #
66
+ # @return [Fixnum] The last 24h quote currency volume
67
+ #
68
+ def quote_volume_24h
69
+ move_24h_cursor!
70
+ @quote_volume_24h
24
71
  end
25
72
 
73
+ #
74
+ # Moves the cursor pointing to the first trade that happened during
75
+ # the last 24h and updates the volume along the way
76
+ #
77
+ def move_24h_cursor!
78
+ time_24h_ago = Time.now.to_f - 24*3600
79
+
80
+ while(self[@cursor_24h] && ((self[@cursor_24h][:type] != :execution) || (self[@cursor_24h][:time] < time_24h_ago)))
81
+ if self[@cursor_24h] && self[@cursor_24h][:type] == :execution
82
+ @volume_24h -= self[@cursor_24h][:base_size]
83
+ @quote_volume_24h -= self[@cursor_24h][:quote_size]
84
+ end
85
+
86
+ @cursor_24h += 1
87
+ end
88
+ end
26
89
  end
27
90
  end
91
+
@@ -1,6 +1,6 @@
1
1
  module Gekko
2
2
 
3
3
  # The Gekko version string
4
- VERSION = '0.0.1'
4
+ VERSION = '0.1.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: 0.0.1
4
+ version: 0.1.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: 2015-01-30 00:00:00.000000000 Z
11
+ date: 2015-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: uuidtools
@@ -24,48 +24,118 @@ dependencies:
24
24
  - - ~>
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '0.10'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '0.10'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rspec
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - ~>
32
46
  - !ruby/object:Gem::Version
33
- version: '0'
47
+ version: '3.1'
34
48
  type: :development
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - ~>
39
53
  - !ruby/object:Gem::Version
40
- version: '0'
54
+ version: '3.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '10.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '10.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: yard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '0.8'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '0.8'
83
+ - !ruby/object:Gem::Dependency
84
+ name: timecop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '0.7'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '0.7'
97
+ - !ruby/object:Gem::Dependency
98
+ name: redcarpet
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: '3.1'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: '3.1'
41
111
  - !ruby/object:Gem::Dependency
42
112
  name: simplecov
43
113
  requirement: !ruby/object:Gem::Requirement
44
114
  requirements:
45
115
  - - ~>
46
116
  - !ruby/object:Gem::Version
47
- version: '0'
117
+ version: '0.9'
48
118
  type: :development
49
119
  prerelease: false
50
120
  version_requirements: !ruby/object:Gem::Requirement
51
121
  requirements:
52
122
  - - ~>
53
123
  - !ruby/object:Gem::Version
54
- version: '0'
124
+ version: '0.9'
55
125
  - !ruby/object:Gem::Dependency
56
- name: pry
126
+ name: coveralls
57
127
  requirement: !ruby/object:Gem::Requirement
58
128
  requirements:
59
129
  - - ~>
60
130
  - !ruby/object:Gem::Version
61
- version: '0'
131
+ version: '0.7'
62
132
  type: :development
63
133
  prerelease: false
64
134
  version_requirements: !ruby/object:Gem::Requirement
65
135
  requirements:
66
136
  - - ~>
67
137
  - !ruby/object:Gem::Version
68
- version: '0'
138
+ version: '0.7'
69
139
  description: Gekko is a bare-bones order matcher whose task is to accept orders and
70
140
  maintain an order book.
71
141
  email:
@@ -80,6 +150,8 @@ files:
80
150
  - lib/gekko/book.rb
81
151
  - lib/gekko/book_side.rb
82
152
  - lib/gekko/errors.rb
153
+ - lib/gekko/limit_order.rb
154
+ - lib/gekko/market_order.rb
83
155
  - lib/gekko/order.rb
84
156
  - lib/gekko/tape.rb
85
157
  - lib/gekko/version.rb