gekko 0.1.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +7 -0
- data/lib/gekko/book.rb +31 -15
- data/lib/gekko/errors.rb +0 -3
- data/lib/gekko/tape.rb +95 -15
- 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: 95df87c9c162b780e408b2761062ff2230542f22
|
4
|
+
data.tar.gz: 4468b3d3bc4f53aa31316321ce9908f63c899704
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5d66764b99cc061c8fc8f6c9750fbb64f478c4d817b04cefbff70a718589225eb5fc3df0fbd935b9f06b36fa5618817eda6c685d0a764e959377b71d7795fc95
|
7
|
+
data.tar.gz: dffa0324039b7c5aa10772087af12e763a5de8f25e1f9ff7ff906ecd31002d005b723e0b736a690c71a62c60f734226bc252b052c5b89444c4f84cce5cbaeacf
|
data/README.md
CHANGED
@@ -7,3 +7,10 @@ Gekko is intended to become a high-performance in-memory trade order matching en
|
|
7
7
|
## What Gekko is not
|
8
8
|
Gekko is not intended to maintain an accounting database, it just matches trade orders associated to accounts, and returns the necessary data for an accounting system to record the trades (usually against some sort of RDBMS).
|
9
9
|
|
10
|
+
## Left to do
|
11
|
+
The following items are left to do and will need to be implemented before gekko is considered production-ready.
|
12
|
+
|
13
|
+
- Persistence and failure recovery
|
14
|
+
- Add and enforce minimum and maximum order sizes
|
15
|
+
- Correctly handle order expiration
|
16
|
+
|
data/lib/gekko/book.rb
CHANGED
@@ -9,25 +9,15 @@ module Gekko
|
|
9
9
|
#
|
10
10
|
class Book
|
11
11
|
|
12
|
-
|
13
|
-
# TODO: Add order expiration
|
14
|
-
# TODO: Test for rounding issues
|
15
|
-
|
16
|
-
# The default minimum price increment accepted for placed orders
|
17
|
-
DEFAULT_TICK_SIZE = 1000
|
18
|
-
|
19
|
-
attr_accessor :pair, :bids, :asks, :tape, :tick_size, :received, :base_precision
|
12
|
+
attr_accessor :pair, :bids, :asks, :tape, :received, :base_precision
|
20
13
|
|
21
14
|
def initialize(pair, opts = {})
|
22
15
|
self.pair = pair
|
23
16
|
self.bids = BookSide.new(:bid)
|
24
17
|
self.asks = BookSide.new(:ask)
|
25
18
|
self.tape = Tape.new(opts[:logger])
|
26
|
-
self.base_precision = 8
|
19
|
+
self.base_precision = opts[:base_precision] || 8
|
27
20
|
self.received = {}
|
28
|
-
|
29
|
-
self.tick_size = opts[:tick_size] || DEFAULT_TICK_SIZE
|
30
|
-
raise "Tick size must be a positive integer if provided" if tick_size && (!tick_size.is_a?(Fixnum) || tick_size <= 0)
|
31
21
|
end
|
32
22
|
|
33
23
|
#
|
@@ -39,12 +29,12 @@ module Gekko
|
|
39
29
|
|
40
30
|
raise 'Order must be a Gekko::LimitOrder or a Gekko::MarketOrder' unless [LimitOrder, MarketOrder].include?(order.class)
|
41
31
|
|
42
|
-
raise Gekko::TickSizeMismatch unless (order.is_a?(MarketOrder) || (order.price % tick_size).zero?)
|
43
|
-
|
44
32
|
if received.has_key?(order.id.to_s)
|
45
33
|
tape << order.message(:reject, reason: "Duplicate ID <#{order.id.to_s}>")
|
46
34
|
|
47
35
|
else
|
36
|
+
old_ticker = ticker
|
37
|
+
|
48
38
|
self.received[order.id.to_s] = order
|
49
39
|
tape << order.message(:received)
|
50
40
|
|
@@ -70,7 +60,6 @@ module Gekko
|
|
70
60
|
end
|
71
61
|
end
|
72
62
|
|
73
|
-
|
74
63
|
tape << {
|
75
64
|
type: :execution,
|
76
65
|
price: trade_price,
|
@@ -99,9 +88,27 @@ module Gekko
|
|
99
88
|
order_side.insert_order(order)
|
100
89
|
tape << order.message(:open)
|
101
90
|
end
|
91
|
+
|
92
|
+
tick! unless (ticker == old_ticker)
|
102
93
|
end
|
103
94
|
end
|
104
95
|
|
96
|
+
#
|
97
|
+
# Cancels an order given an ID
|
98
|
+
#
|
99
|
+
# @param order_id [String] The ID of the order to cancel
|
100
|
+
#
|
101
|
+
def cancel(order_id)
|
102
|
+
prev_bid = bid
|
103
|
+
prev_ask = ask
|
104
|
+
|
105
|
+
order = received[order_id.to_s]
|
106
|
+
dels = order.bid? ? bids.delete(order) : asks.delete(order)
|
107
|
+
dels && tape << order.message(:done, reason: :cancelled)
|
108
|
+
|
109
|
+
tick! if (prev_bid != bid) || (prev_ask != ask)
|
110
|
+
end
|
111
|
+
|
105
112
|
#
|
106
113
|
# Returns the current best ask price or +nil+ if there
|
107
114
|
# are currently no asks
|
@@ -126,6 +133,13 @@ module Gekko
|
|
126
133
|
ask && bid && (ask - bid)
|
127
134
|
end
|
128
135
|
|
136
|
+
#
|
137
|
+
# Emits a ticker on the tape
|
138
|
+
#
|
139
|
+
def tick!
|
140
|
+
tape << { type: :ticker }.merge(ticker)
|
141
|
+
end
|
142
|
+
|
129
143
|
#
|
130
144
|
# Returns the current ticker
|
131
145
|
#
|
@@ -137,6 +151,8 @@ module Gekko
|
|
137
151
|
last: tape.last_trade_price,
|
138
152
|
bid: bid,
|
139
153
|
ask: ask,
|
154
|
+
high_24h: tape.high_24h,
|
155
|
+
low_24h: tape.low_24h,
|
140
156
|
spread: spread,
|
141
157
|
volume_24h: v24h,
|
142
158
|
vwap_24h: (v24h > 0) && (tape.quote_volume_24h * (10 ** base_precision)/ v24h)
|
data/lib/gekko/errors.rb
CHANGED
data/lib/gekko/tape.rb
CHANGED
@@ -24,17 +24,11 @@ module Gekko
|
|
24
24
|
message[:sequence] = length
|
25
25
|
logger && logger.info(message)
|
26
26
|
|
27
|
-
|
28
|
-
# Keep last price up to date
|
29
|
-
@last_trade_price = message[:price]
|
27
|
+
super(message)
|
30
28
|
|
31
|
-
|
32
|
-
|
33
|
-
@quote_volume_24h += message[:quote_size]
|
34
|
-
move_24h_cursor!
|
29
|
+
if message[:type] == :execution
|
30
|
+
update_ticker(message)
|
35
31
|
end
|
36
|
-
|
37
|
-
super(message)
|
38
32
|
end
|
39
33
|
|
40
34
|
#
|
@@ -60,6 +54,47 @@ module Gekko
|
|
60
54
|
@volume_24h
|
61
55
|
end
|
62
56
|
|
57
|
+
#
|
58
|
+
# Returns the highest trade price that occurred during the last 24h
|
59
|
+
#
|
60
|
+
# @return [Fixnum] The last 24h high
|
61
|
+
#
|
62
|
+
def high_24h
|
63
|
+
move_24h_cursor!
|
64
|
+
@high_24h
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Returns the lowest trade price that occurred during the last 24h
|
69
|
+
#
|
70
|
+
# @return [Fixnum] The last 24h low
|
71
|
+
#
|
72
|
+
def low_24h
|
73
|
+
move_24h_cursor!
|
74
|
+
@low_24h
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Recalculates the previous 24h high and low
|
79
|
+
#
|
80
|
+
def recalc_high_low_24h!
|
81
|
+
@high_24h = nil
|
82
|
+
@low_24h = nil
|
83
|
+
|
84
|
+
# Work backwards from current position until the cursor points to an event
|
85
|
+
# that's older than 24h
|
86
|
+
tmp_cursor = (length - 1)
|
87
|
+
evt = self[tmp_cursor]
|
88
|
+
|
89
|
+
while (evt && (evt[:time] >= time_24h_ago)) do
|
90
|
+
@high_24h = ((@high_24h.nil? || (evt[:price] > @high_24h)) && evt[:price]) || @high_24h
|
91
|
+
@low_24h = ((@low_24h.nil? || (evt[:price] < @low_24h)) && evt[:price]) || @low_24h
|
92
|
+
|
93
|
+
tmp_cursor -= 1
|
94
|
+
evt = (tmp_cursor >= 0) && self[tmp_cursor]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
63
98
|
#
|
64
99
|
# Returns the traded amount of quote currency in the last 24h
|
65
100
|
#
|
@@ -70,22 +105,67 @@ module Gekko
|
|
70
105
|
@quote_volume_24h
|
71
106
|
end
|
72
107
|
|
108
|
+
#
|
109
|
+
# Updates the ticker after an execution has been recorded
|
110
|
+
#
|
111
|
+
def update_ticker(execution)
|
112
|
+
price = execution[:price]
|
113
|
+
|
114
|
+
# Keep last price up to date
|
115
|
+
@last_trade_price = price
|
116
|
+
|
117
|
+
# Keep 24h volume up to date
|
118
|
+
@volume_24h += execution[:base_size]
|
119
|
+
@quote_volume_24h += execution[:quote_size]
|
120
|
+
|
121
|
+
# Record new high/lows
|
122
|
+
if @high_24h.nil? || (@high_24h < price)
|
123
|
+
@high_24h = price
|
124
|
+
end
|
125
|
+
|
126
|
+
if @low_24h.nil? || (price < @low_24h)
|
127
|
+
@low_24h = price
|
128
|
+
end
|
129
|
+
|
130
|
+
move_24h_cursor!
|
131
|
+
end
|
132
|
+
|
133
|
+
#
|
134
|
+
# Returns the float timestamp of 24h ago
|
135
|
+
#
|
136
|
+
# @return [Float] Yesterday's cut-off timestamp
|
137
|
+
#
|
138
|
+
def time_24h_ago
|
139
|
+
Time.now.to_f - 24*3600
|
140
|
+
end
|
141
|
+
|
73
142
|
#
|
74
143
|
# Moves the cursor pointing to the first trade that happened during
|
75
|
-
# the last 24h
|
144
|
+
# the last 24h. Every execution getting out of the 24h rolling window is
|
145
|
+
# passed to Tape#fall_out_of_24h_window
|
76
146
|
#
|
77
147
|
def move_24h_cursor!
|
78
|
-
time_24h_ago = Time.now.to_f - 24*3600
|
79
|
-
|
80
148
|
while(self[@cursor_24h] && ((self[@cursor_24h][:type] != :execution) || (self[@cursor_24h][:time] < time_24h_ago)))
|
81
|
-
if self[@cursor_24h]
|
82
|
-
|
83
|
-
@quote_volume_24h -= self[@cursor_24h][:quote_size]
|
149
|
+
if self[@cursor_24h][:type] == :execution
|
150
|
+
fall_out_of_24h_window(self[@cursor_24h])
|
84
151
|
end
|
85
152
|
|
86
153
|
@cursor_24h += 1
|
87
154
|
end
|
88
155
|
end
|
156
|
+
|
157
|
+
#
|
158
|
+
# Updates the low, high, and volumes when an execution falls out of the rolling
|
159
|
+
# previous 24h window
|
160
|
+
#
|
161
|
+
def fall_out_of_24h_window(execution)
|
162
|
+
@volume_24h -= execution[:base_size]
|
163
|
+
@quote_volume_24h -= execution[:quote_size]
|
164
|
+
|
165
|
+
if [@high_24h, @low_24h].include?(execution[:price])
|
166
|
+
recalc_high_low_24h!
|
167
|
+
end
|
168
|
+
end
|
89
169
|
end
|
90
170
|
end
|
91
171
|
|
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: 0.1
|
4
|
+
version: 0.2.1
|
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-02-
|
11
|
+
date: 2015-02-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: uuidtools
|