gekko 0.0.1 → 0.1.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: 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