kraken_client 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/Gemfile +7 -0
  4. data/Gemfile.lock +61 -0
  5. data/LICENSE.md +24 -0
  6. data/Rakefile +1 -0
  7. data/fixtures/vcr_cassettes/add_order.yml +41 -0
  8. data/fixtures/vcr_cassettes/assets.yml +45 -0
  9. data/fixtures/vcr_cassettes/assets_pairs.yml +45 -0
  10. data/fixtures/vcr_cassettes/balance.yml +41 -0
  11. data/fixtures/vcr_cassettes/cancel_order.yml +41 -0
  12. data/fixtures/vcr_cassettes/closed_orders.yml +134 -0
  13. data/fixtures/vcr_cassettes/ledgers.yml +41 -0
  14. data/fixtures/vcr_cassettes/open_orders.yml +43 -0
  15. data/fixtures/vcr_cassettes/order_book.yml +43 -0
  16. data/fixtures/vcr_cassettes/query_ledgers.yml +41 -0
  17. data/fixtures/vcr_cassettes/query_orders.yml +43 -0
  18. data/fixtures/vcr_cassettes/query_trades.yml +41 -0
  19. data/fixtures/vcr_cassettes/server_time.yml +44 -0
  20. data/fixtures/vcr_cassettes/spread.yml +43 -0
  21. data/fixtures/vcr_cassettes/ticker.yml +43 -0
  22. data/fixtures/vcr_cassettes/trade_balance.yml +41 -0
  23. data/fixtures/vcr_cassettes/trade_volume.yml +41 -0
  24. data/fixtures/vcr_cassettes/trades.yml +43 -0
  25. data/fixtures/vcr_cassettes/trades_history.yml +41 -0
  26. data/kraken_rb.gemspec +33 -0
  27. data/lib/configurable.rb +13 -0
  28. data/lib/configuration.rb +19 -0
  29. data/lib/exceptions.rb +6 -0
  30. data/lib/kraken_client/application.rb +45 -0
  31. data/lib/kraken_client/endpoints/base.rb +43 -0
  32. data/lib/kraken_client/endpoints/private.rb +45 -0
  33. data/lib/kraken_client/endpoints/public.rb +33 -0
  34. data/lib/kraken_client/requests/base.rb +35 -0
  35. data/lib/kraken_client/requests/content/body.rb +18 -0
  36. data/lib/kraken_client/requests/content/header.rb +61 -0
  37. data/lib/kraken_client/requests/get.rb +13 -0
  38. data/lib/kraken_client/requests/limiter.rb +78 -0
  39. data/lib/kraken_client/requests/post.rb +38 -0
  40. data/lib/kraken_client.rb +15 -0
  41. data/lib/version.rb +3 -0
  42. data/readme.md +280 -0
  43. data/spec/requests/private_spec.rb +104 -0
  44. data/spec/requests/public_spec.rb +57 -0
  45. metadata +229 -0
@@ -0,0 +1,18 @@
1
+ module KrakenClient
2
+ module Requests
3
+ module Content
4
+ class Body
5
+
6
+ attr_reader :post_data
7
+
8
+ def initialize(post_data)
9
+ @post_data = post_data
10
+ end
11
+
12
+ def call
13
+ post_data
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,61 @@
1
+ module KrakenClient
2
+ module Requests
3
+ module Content
4
+ class Header
5
+
6
+ attr_accessor :config, :endpoint_name, :options, :url
7
+
8
+ def initialize(config, endpoint_name, options, url)
9
+ @config = config
10
+ @endpoint_name = endpoint_name
11
+ @url = url
12
+
13
+ @options = options
14
+ @options[:nonce] = nonce
15
+ end
16
+
17
+ def call
18
+ {
19
+ 'API-Key' => config.api_key,
20
+ 'API-Sign' => generate_signature,
21
+ }
22
+ end
23
+
24
+ private
25
+
26
+ ## Security
27
+
28
+ # Generate a 64-bit nonce where the 48 high bits come directly from the current
29
+ # timestamp and the low 16 bits are pseudorandom. We can't use a pure [P]RNG here
30
+ # because the Kraken API requires every request within a given session to use a
31
+ # monotonically increasing nonce value. This approach splits the difference.
32
+ def nonce
33
+ high_bits = (Time.now.to_f * 10000).to_i << 16
34
+ low_bits = SecureRandom.random_number(2 ** 16) & 0xffff
35
+ (high_bits | low_bits).to_s
36
+ end
37
+
38
+ def encoded_options
39
+ uri = Addressable::URI.new
40
+ uri.query_values = options
41
+ uri.query
42
+ end
43
+
44
+ def generate_signature
45
+ key = Base64.decode64(config.api_secret)
46
+ message = generate_message
47
+ generate_hmac(key, message)
48
+ end
49
+
50
+ def generate_message
51
+ digest = OpenSSL::Digest.new('sha256', options[:nonce] + encoded_options).digest
52
+ url.split('.com').last + digest
53
+ end
54
+
55
+ def generate_hmac(key, message)
56
+ Base64.strict_encode64(OpenSSL::HMAC.digest('sha512', key, message))
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,13 @@
1
+ module KrakenClient
2
+ module Requests
3
+ class Get < Base
4
+
5
+ def call(url, params, *)
6
+ super
7
+
8
+ HTTParty.get(url, query: params)
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,78 @@
1
+ module KrakenClient
2
+ module Requests
3
+ class Limiter
4
+
5
+ attr_reader :config, :previous_timestamps, :endpoint_name, :current_count
6
+
7
+ def initialize(config)
8
+ @config = config
9
+ @previous_timestamps = Time.now
10
+ @current_count = counter_total
11
+ end
12
+
13
+ def update(endpoint_name)
14
+ return unless config.limiter
15
+ @endpoint_name = endpoint_name
16
+
17
+ decrement_current_count
18
+ end
19
+
20
+ private
21
+
22
+ # Adds the number of seconds depending of the current tier value
23
+ def refresh_current_count
24
+ @current_count += ((Time.now - previous_timestamps) / seconds_to_decrement).to_int
25
+ @current_count = counter_total if current_count > counter_total
26
+
27
+ current_count
28
+ end
29
+
30
+ def decrement_current_count
31
+ @current_count -= value_to_decrement
32
+
33
+ if current_count < 0
34
+ sleep value_to_decrement
35
+
36
+ @current_count = value_to_decrement
37
+
38
+ update(endpoint_name)
39
+ else
40
+ refresh_current_count
41
+
42
+ @previous_timestamps = Time.now
43
+ end
44
+ end
45
+
46
+ def value_to_decrement
47
+
48
+ case endpoint_name
49
+ when 'Ledger' then 2
50
+ when 'TradeHistory' then 2
51
+ when 'AddOrder' then 0
52
+ when 'CancelOrder' then 0
53
+ else 1
54
+ end
55
+ end
56
+
57
+ def counter_total
58
+ @counter ||= case config.tier
59
+ when 0 then 10
60
+ when 1 then 10
61
+ when 2 then 10
62
+ when 3 then 20
63
+ when 4 then 20
64
+ end
65
+ end
66
+
67
+ def seconds_to_decrement
68
+ @decrement ||= case config.tier
69
+ when 0 then 5
70
+ when 1 then 5
71
+ when 2 then 5
72
+ when 3 then 2
73
+ when 4 then 1
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,38 @@
1
+ module KrakenClient
2
+ module Requests
3
+ class Post < Base
4
+
5
+ def call(url, endpoint_name, options)
6
+ super
7
+
8
+ @url = url
9
+ @endpoint_name = endpoint_name
10
+
11
+ response = HTTParty.post(url, params(options)).parsed_response
12
+ response['error'].empty? ? response['result'] : response['error']
13
+ end
14
+
15
+ private
16
+
17
+ def params(options = {})
18
+ params = {}
19
+
20
+ header_content = content_manager::Header.new(config, endpoint_name, options, url)
21
+
22
+ params[:headers] = header_content.call
23
+ params[:body] = body_content(header_content).call
24
+
25
+ params
26
+ end
27
+
28
+ def content_manager
29
+ KrakenClient::Requests::Content
30
+ end
31
+
32
+ def body_content(header_content)
33
+ content_manager::Body.new(header_content.send(:encoded_options))
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ require 'find'
2
+ require 'active_support/inflector'
3
+
4
+ Find.find('./lib').select { |p| /.*\.rb$/ =~ p }.each do |path|
5
+ require_relative("../" + path)
6
+ end
7
+
8
+ module KrakenClient
9
+ extend KrakenClient::Configurable
10
+
11
+ def self.load(params = {})
12
+ KrakenClient::Application.new(params)
13
+ end
14
+
15
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module KrakenClient
2
+ VERSION = "0.2.0"
3
+ end
data/readme.md ADDED
@@ -0,0 +1,280 @@
1
+
2
+ # **KrakenClient**
3
+
4
+ `KrakenClient` is a **Ruby** wrapper of the Kraken API. Kraken is a market exchange site serving those trading with Crypto-Currencies, such as **Bitcoin**.
5
+
6
+ This gem tends to be a replacement for [kraken_ruby](https://github.com/leishman/kraken_ruby), another wrapper, from which it is originally forked from.
7
+
8
+ It has every features of the latter gem, with improved readability, usability, maintenability, and speced using the Awesome [Spectus gem](https://github.com/fixrb/spectus).
9
+
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'kraken_client'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install kraken_ruby
26
+
27
+ And require it in your application:
28
+
29
+ irb(main):001:0> require 'kraken_client'
30
+ => true
31
+
32
+
33
+ ## Usage
34
+
35
+ ### Configuration ###
36
+ You can pass multiple variables that will be used in the gem.
37
+
38
+ ```ruby
39
+ KrakenClient.configure do |config|
40
+ config.api_key = ENV['KRAKEN_API_KEY']
41
+ config.api_secret = ENV['KRAKEN_API_SECRET']
42
+ config.base_uri = 'https://api.kraken.com'
43
+ config.api_version = 0
44
+ config.limiter = true
45
+ config.tier = 2
46
+ end
47
+ ```
48
+
49
+ By default, the default values are the ones described in the above example.
50
+
51
+ You can also pass any of those options inline when loading an instance of KrakenClient.
52
+
53
+ ```ruby
54
+ KrakenClient.load({base_uri: 'https://api.kraken.com', tier: 3}).config.tier
55
+ ```
56
+
57
+
58
+ **/!\\ Important Note /!\\** If you wish to use the Private Endpoints, you need to specify an API Key, or an exception will be raised.
59
+
60
+ ### Call Rate Limiter ###
61
+
62
+ Kraken has implemented a security which limit API users to make too much requests to the server. Each user has a counter (which is bigger depending on your tier). Each call increments your counter, and if your counter reaches its limit, you are blocked for 15 minutes.
63
+
64
+ To prevent this, `KrakenClient` has a safeguard, which queue the request which should in theory be blocked and is executed two second later.
65
+
66
+ If you want to disable this option, pass the `limiter` variable in the configuration to false.
67
+
68
+ ```ruby
69
+ KrakenClient.load({limiter: false}).config.tier
70
+ ```
71
+
72
+ Also, this limiter is activated by default. You would like to specify your tier, and `KrakenClient` will automatically make the required adjustments. The default `tier` is 2.
73
+
74
+ ```ruby
75
+ KrakenClient.load({tier: 3}).config.tier
76
+ ```
77
+
78
+ For more information, please consult the [Kraken official documentation](https://support.kraken.com/hc/en-us/articles/206548367-What-is-the-API-call-rate-limit-).
79
+
80
+ ### Requests ###
81
+
82
+ In all our examples henceforward, we consider this variable to be a loaded instance of `KrakenClient`
83
+
84
+ ```ruby
85
+ client = KrakenClient.load
86
+ ```
87
+
88
+ If you ever need to see the full documentation for the possible parameters, please take a look at the official [Kraken API docs](https://www.kraken.com/help/api).
89
+
90
+ A `KrakenClient::MissingParameter` exception will be raised along with the missing parameters if a required parameter is not passed.
91
+
92
+ ### *Public Endpoints* ###
93
+
94
+
95
+ ##### Server Time
96
+
97
+ This functionality is provided by Kraken to to aid in approximating the skew time between the server and client.
98
+
99
+ ```ruby
100
+ time = client.public.server_time
101
+
102
+ time.unixtime #=> 1393056191
103
+ time.rfc1123 #=> "Sat, 22 Feb 2014 08:28:04 GMT"
104
+ ```
105
+
106
+ ##### Asset Info
107
+
108
+ Returns the assets that can be traded on the exchange. This method can be passed ```info```, ```aclass``` (asset class), and ```asset``` options. An example below is given for each:
109
+
110
+ ```ruby
111
+ assets = client.public.assets
112
+ ```
113
+
114
+ ##### Asset Pairs
115
+
116
+ ```ruby
117
+ pairs = client.public.asset_pairs
118
+ ```
119
+
120
+ ##### Ticker Information
121
+
122
+ ```ruby
123
+ ticker_data = client.public.ticker('XLTCXXDG, ZUSDXXVN')
124
+ ```
125
+
126
+ ##### Order Book
127
+
128
+ Get market depth information for given asset pairs
129
+
130
+ ```ruby
131
+ depth_data = client.public.order_book('LTCXRP')
132
+ ```
133
+
134
+ ##### Trades
135
+
136
+ Get recent trades
137
+
138
+ ```ruby
139
+ trades = client.public.trades('LTCXRP')
140
+ ```
141
+
142
+ ##### Spread
143
+
144
+ Get spread data for a given asset pair
145
+
146
+ ```ruby
147
+ spread = client.public.spread('LTCXRP')
148
+ ```
149
+
150
+
151
+ #### Private Endpoints ####
152
+
153
+ ##### Balance
154
+
155
+ Get account balance for each asset
156
+ Note: Rates used for the floating valuation is the midpoint of the best bid and ask prices
157
+
158
+ ```ruby
159
+ balance = client.private.balance
160
+ ```
161
+
162
+ ##### Trade Balance
163
+
164
+ Get account trade balance
165
+
166
+ ```ruby
167
+ trade_balance = client.private.trade_balance
168
+ ```
169
+
170
+ ##### Open Orders
171
+
172
+ ```ruby
173
+ open_orders = client.private.open_orders
174
+ ```
175
+
176
+ ##### Closed Orders
177
+
178
+ ```ruby
179
+ closed_orders = client.private.closed_orders
180
+ ```
181
+
182
+ ##### Query Orders
183
+
184
+ **Input:** Comma delimited list of transaction ids (txid)
185
+
186
+ See all orders
187
+
188
+ ```ruby
189
+ orders = client.private.query_orders(txid: ids)
190
+ ```
191
+
192
+ ##### Trades History
193
+
194
+ Get array of all trades
195
+
196
+ ```ruby
197
+ trades = client.private.trade_history
198
+ ```
199
+
200
+ ##### Query Trades
201
+
202
+ **Input:** Comma delimited list of transaction ids (txid)
203
+
204
+ See all orders
205
+
206
+ ```ruby
207
+ orders = client.private.query_orders(txid: ids)
208
+ ```
209
+
210
+ ##### Open Positions
211
+
212
+ **Input:** Comma delimited list of transaction (txid) ids
213
+
214
+ ```ruby
215
+ positions = client.private.open_positions(txid)
216
+ ```
217
+
218
+ ##### Ledgers Info
219
+
220
+ ```ruby
221
+ ledgers = client.private.ledgers
222
+ ```
223
+
224
+ ##### Query Ledgers
225
+
226
+ **Input:** Comma delimited list of ledger ids
227
+
228
+ ```ruby
229
+ ledgers = client.private.query_ledgers(id: ledger_ids)
230
+ ```
231
+
232
+ ##### Trade Volume
233
+
234
+ ```ruby
235
+ ledgers = client.private.trade_volume
236
+ ```
237
+
238
+ ##### Add Order
239
+
240
+ There are 4 required parameters for buying an order. The example below illustrates the most basic order. Please see the [Kraken documentation](https://www.kraken.com/help/api#add-standard-order) for the parameters required for more advanced order types.
241
+
242
+ ```ruby
243
+ # buying 0.01 XBT (bitcoin) for XRP (ripple) at market price
244
+ opts = {
245
+ pair: 'XBTXRP',
246
+ type: 'buy',
247
+ ordertype: 'market',
248
+ volume: 0.01
249
+ }
250
+
251
+ client.private.add_order(opts)
252
+ ```
253
+
254
+ ##### Cancel Order
255
+
256
+ ```ruby
257
+ client.private.cancel_order("UKIYSP-9VN27-AJWWYC")
258
+ ```
259
+
260
+
261
+ ## Credits
262
+
263
+ This gem has been made by [Sidney SISSAOUI (shideneyu)](https://github.com/shideneyu).
264
+
265
+ Special credits goes to [Alexander LEISHMAN](http://alexleishman.com/) and other [kraken_ruby](https://github.com/leishman/kraken_ruby) contributors for their gem, which helped me to have a nice base to begin **KrakenClient**. It would have been difficult for me to sign the requests if it wasn't thanks to their work.
266
+ If you want to be part of those credits, do not hesitate to contribute by doing some pull requests ;) !
267
+
268
+
269
+ ## Contributing
270
+
271
+ 1. Fork it ( https://github.com/[my-github-username]/swiffer/fork )
272
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
273
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
274
+ 4. Push to the branch (`git push origin my-new-feature`)
275
+ 5. Create a new Pull Request
276
+
277
+
278
+ ## License
279
+
280
+ See `LICENSE.md` file.
@@ -0,0 +1,104 @@
1
+ require_relative File.join '..', '..', 'lib', 'kraken_client'
2
+ require 'spectus'
3
+ require 'vcr'
4
+ require 'webmock'
5
+
6
+ include WebMock::API
7
+
8
+ VCR.configure do |config|
9
+ config.cassette_library_dir = "fixtures/vcr_cassettes"
10
+ config.hook_into :webmock # or :fakeweb
11
+ end
12
+
13
+ # Testing Private Endpoints
14
+ KrakenClient.configure do |config|
15
+ config.api_key = ENV['KRAKEN_API_KEY'] || 'COMPUTED'
16
+ config.api_secret = ENV['KRAKEN_API_SECRET'] ||'COMPUTED'
17
+ end
18
+
19
+ kraken = KrakenClient.load
20
+ client = kraken.private
21
+
22
+ # User Balance
23
+ VCR.use_cassette("balance") do
24
+ Spectus.this { client.balance.class }.MUST Equal: Hashie::Mash
25
+ end
26
+
27
+ # Trade Balance
28
+ VCR.use_cassette("trade_balance") do
29
+ Spectus.this { client.trade_balance.c }.MUST Eql: '0.0000'
30
+ end
31
+
32
+ # Open Orders
33
+ VCR.use_cassette("open_orders") do
34
+ Spectus.this { client.open_orders.open.class }.MUST Equal: Hashie::Mash
35
+ end
36
+
37
+ # Closed Orders
38
+ VCR.use_cassette("closed_orders") do
39
+ Spectus.this { client.closed_orders.closed.class }.MUST Equal: Hashie::Mash
40
+ end
41
+
42
+ # Query Orders
43
+ VCR.use_cassette("query_orders") do
44
+ Spectus.this do
45
+ order = client.query_orders({txid: 'OKRRJ6-MH3UH-DV6IKT'})
46
+ order['OKRRJ6-MH3UH-DV6IKT'].status
47
+ end.MUST Eql: 'canceled'
48
+ end
49
+
50
+ # Trades History
51
+ VCR.use_cassette("trades_history") do
52
+ Spectus.this { client.trades_history.trades.class }.MUST Equal: Hashie::Mash
53
+ end
54
+
55
+ # Query Trades
56
+ VCR.use_cassette("query_trades") do
57
+ Spectus.this do
58
+ order = client.query_trades({txid: 'THZPTW-BMF6X-VWMN5P'})
59
+ order['THZPTW-BMF6X-VWMN5P'].pair
60
+ end.MUST Eql: 'XETHZEUR'
61
+ end
62
+
63
+ # Open Positions
64
+ #CANT TEST, PARAMS DO NOT WORK
65
+
66
+ # Ledgers Info
67
+ VCR.use_cassette("ledgers") do
68
+ Spectus.this { client.ledgers.ledger.class }.MUST Equal: Hashie::Mash
69
+ end
70
+
71
+ # Query Ledgers
72
+ VCR.use_cassette("query_ledgers") do
73
+ Spectus.this do
74
+ ledger = client.query_ledgers(id: 'LRSNYS-DICDD-3QM34P')
75
+ ledger['LRSNYS-DICDD-3QM34P'].class
76
+ end.MUST Equal: Hashie::Mash
77
+ end
78
+
79
+ # Trade Volume
80
+ VCR.use_cassette("trade_volume") do
81
+ Spectus.this { client.trade_volume(pair: 'XETHZEUR').count }.MUST Equal: 4
82
+ end
83
+
84
+ # Add Order
85
+ VCR.use_cassette("add_order") do
86
+ Spectus.this do
87
+
88
+ opts = {
89
+ pair: 'ETHEUR',
90
+ type: 'buy',
91
+ ordertype: 'market',
92
+ volume: 0.01
93
+ }
94
+
95
+ client.add_order(opts).txid
96
+ end.MUST Eql: ['OEDIZV-VDAW3-RHLJVB']
97
+ end
98
+
99
+ # Cancel Order
100
+ VCR.use_cassette("cancel_order") do
101
+ Spectus.this do
102
+ client.cancel_order(txid: 'ODEC3J-QAMVD-NSF7XD').count
103
+ end.MUST Eql: 1
104
+ end
@@ -0,0 +1,57 @@
1
+ require_relative File.join '..', '..', 'lib', 'kraken_client'
2
+ require 'spectus'
3
+ require 'vcr'
4
+ require 'webmock'
5
+
6
+ include WebMock::API
7
+
8
+ VCR.configure do |config|
9
+ config.cassette_library_dir = "fixtures/vcr_cassettes"
10
+ config.hook_into :webmock # or :fakeweb
11
+ end
12
+
13
+ # Testing Public Endpoints
14
+ kraken = KrakenClient.load
15
+ client = kraken.public
16
+
17
+ # Server Time
18
+ VCR.use_cassette("server_time") do
19
+ kraken_time = DateTime.parse(client.server_time.rfc1123)
20
+ utc_time = Time.now.getutc
21
+ Spectus.this { kraken_time.day.class }.MUST Equal: Fixnum
22
+ Spectus.this { kraken_time.hour.class }.MUST Equal: Fixnum
23
+ end
24
+
25
+ # Assets
26
+ VCR.use_cassette("assets") do
27
+ Spectus.this { client.assets.XETH.aclass }.MUST Eql: 'currency'
28
+ end
29
+
30
+ # Assets Pairs
31
+ VCR.use_cassette("assets_pairs") do
32
+ Spectus.this { client.asset_pairs.XETHXXBT.altname }.MUST Eql: 'ETHXBT'
33
+ end
34
+
35
+ # Ticker
36
+ VCR.use_cassette("ticker") do
37
+ result = client.ticker(pair: 'XXBTZEUR, XXBTZGBP')
38
+ Spectus.this { result.XXBTZGBP.a.class }.MUST Equal: Array
39
+ end
40
+
41
+ # Order Book
42
+ VCR.use_cassette("order_book") do
43
+ order_book = client.order_book(pair: 'XXBTZEUR')
44
+ Spectus.this { order_book.XXBTZEUR.asks.class }.MUST Equal: Array
45
+ end
46
+
47
+ # Trades
48
+ VCR.use_cassette("trades") do
49
+ trades = client.trades(pair: 'XXBTZEUR')
50
+ Spectus.this { trades.XXBTZEUR.class }.MUST Equal: Array
51
+ end
52
+
53
+ # Spread
54
+ VCR.use_cassette("spread") do
55
+ spread = client.spread(pair: 'XXBTZEUR')
56
+ Spectus.this { spread.XXBTZEUR.class }.MUST Equal: Array
57
+ end