ig_markets 0.16 → 0.17

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: 26ec69035134c7762495091e87dd4794c9aaebdf
4
- data.tar.gz: 01373bb017a39473a812c60fa599ee0b4f082ec4
3
+ metadata.gz: 4c696f25e6b960d65896474cf519c7492f8ad195
4
+ data.tar.gz: 54dcc2e3caa92373344c3faf7c3a1ea8f916dcf2
5
5
  SHA512:
6
- metadata.gz: 6bad0ef7b8a20a769177bfbf8bb59454aa11f31c836efb62a8d9b18ce929a5aff7d59b5a2f51691d5f3862daa40bf00110136bc7a9d331336125c73335c96bd8
7
- data.tar.gz: ac90eab61abcfa36d8c022f9d89514eee691e49feafd7aecbf579e710ba1880fea613b0d069a1cb5c2a0bd041a1d0d8a01a31186160ebb7f8c29436a0f446e00
6
+ metadata.gz: 3b0052a80212495c796d6ab5c75188690f259d640fa96982b7c0cb7f7b636014bc43cea9e5102a4bb17e8c07cb0f671d634db469165ab83b002d66d7bdde53b6
7
+ data.tar.gz: 588dd121edd94b04546afa353802f39ccdde3c972684f17d297d6d2d649baa2cce25968dfc82a6876e06ce7e0e86093236ba4a13720d9db98531a437c057cb17
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # IG Markets Changelog
2
2
 
3
+ ### 0.17 — July 27, 2016
4
+
5
+ - Switched to the `excon` HTTP library
6
+ - Replaced `IGMarkets::RequestFailedError` with a new `IGMarkets::IGMarketsError` class which is used as a base class
7
+ for all errors raised by this gem
8
+ - Added a full set of error subclassses to give more accurate reporting of errors generated by the IG Markets API
9
+ - Documentation improvements
10
+
3
11
  ### 0.16 — July 20, 2016
4
12
 
5
13
  - Switched to YAML for the `.ig_markets` config files and added support for storing multiple authentication profiles in
data/lib/ig_markets.rb CHANGED
@@ -1,13 +1,15 @@
1
1
  require 'base64'
2
- require 'colorize'
3
2
  require 'date'
4
3
  require 'json'
5
- require 'pry'
6
- require 'rest-client'
7
4
  require 'securerandom'
5
+ require 'time'
6
+ require 'yaml'
7
+
8
+ require 'colorize'
9
+ require 'excon'
10
+ require 'pry'
8
11
  require 'terminal-table'
9
12
  require 'thor'
10
- require 'yaml'
11
13
 
12
14
  require 'ig_markets/boolean'
13
15
  require 'ig_markets/model'
@@ -29,6 +31,7 @@ require 'ig_markets/dealing_platform/position_methods'
29
31
  require 'ig_markets/dealing_platform/sprint_market_position_methods'
30
32
  require 'ig_markets/dealing_platform/watchlist_methods'
31
33
  require 'ig_markets/dealing_platform/working_order_methods'
34
+ require 'ig_markets/errors'
32
35
  require 'ig_markets/format'
33
36
  require 'ig_markets/instrument'
34
37
  require 'ig_markets/historical_price_result'
@@ -38,7 +41,6 @@ require 'ig_markets/market_hierarchy_result'
38
41
  require 'ig_markets/password_encryptor'
39
42
  require 'ig_markets/payload_formatter'
40
43
  require 'ig_markets/position'
41
- require 'ig_markets/request_failed_error'
42
44
  require 'ig_markets/request_printer'
43
45
  require 'ig_markets/response_parser'
44
46
  require 'ig_markets/session'
@@ -1,10 +1,16 @@
1
1
  module IGMarkets
2
2
  # Named constant used to target version 1 of the IG Markets API.
3
+ #
4
+ # @private
3
5
  API_V1 = 1
4
6
 
5
7
  # Named constant used to target version 2 of the IG Markets API.
8
+ #
9
+ # @private
6
10
  API_V2 = 2
7
11
 
8
12
  # Named constant used to target version 3 of the IG Markets API.
13
+ #
14
+ # @private
9
15
  API_V3 = 3
10
16
  end
@@ -26,8 +26,6 @@ module IGMarkets
26
26
 
27
27
  # Turns the `:days` and `:from` options into a hash with `:from` and `:to` keys that can be passed to
28
28
  # {AccountMethods#activities} and {AccountMethods#transactions}.
29
- #
30
- # @return [Hash]
31
29
  def history_options
32
30
  if options[:from]
33
31
  from = Date.strptime options[:from], '%F'
@@ -68,10 +66,8 @@ module IGMarkets
68
66
  @dealing_platform.sign_in options[:username], options[:password], options[:api_key], platform
69
67
 
70
68
  yield @dealing_platform
71
- rescue IGMarkets::RequestFailedError => request_failed_error
72
- error "Request error (HTTP #{request_failed_error.http_code}): #{request_failed_error.error}"
73
- rescue ArgumentError => argument_error
74
- error "Argument error: #{argument_error}"
69
+ rescue IGMarketsError => error
70
+ warn_and_exit error
75
71
  end
76
72
 
77
73
  # Requests and displays the deal confirmation for the passed deal reference. If the request for the deal
@@ -86,8 +82,8 @@ module IGMarkets
86
82
  5.times do |index|
87
83
  begin
88
84
  return print_deal_confirmation @dealing_platform.deal_confirmation(deal_reference)
89
- rescue IGMarkets::RequestFailedError => request_failed_error
90
- raise if request_failed_error.error != 'error.confirms.deal-not-found' || index == 4
85
+ rescue Errors::DealNotFoundError
86
+ raise if index == 4
91
87
 
92
88
  puts 'Deal not found, retrying ...'
93
89
  sleep 2
@@ -130,30 +126,30 @@ module IGMarkets
130
126
  options.each_with_object({}) do |(key, value), new_hash|
131
127
  next unless whitelist.include? key.to_sym
132
128
 
133
- new_hash[key.to_sym] = (value == key) ? nil : value
129
+ new_hash[key.to_sym] = value == key ? nil : value
134
130
  end
135
131
  end
136
132
 
137
133
  private
138
134
 
139
- # Writes the passed message to `stderr` and then exits the application.
140
- #
141
- # @param [String] message The error message.
142
- def error(message)
143
- warn message
135
+ # Prints the passed error to `stderr` and then exits the application.
136
+ def warn_and_exit(error)
137
+ class_name = error.class.name.split('::').last
138
+
139
+ message = error.message.to_s
140
+ message = nil if message.empty? || message == error.class.to_s
141
+
142
+ warn ["IG Markets: #{class_name}", message].compact.join(', ')
143
+
144
144
  exit 1
145
145
  end
146
146
 
147
147
  # Returns the config file to use for this invocation.
148
- #
149
- # @return [ConfigFile]
150
148
  def config_file
151
149
  ConfigFile.find "#{Dir.pwd}/.ig_markets.yml", "#{Dir.home}/.ig_markets.yml"
152
150
  end
153
151
 
154
152
  # Prints out details of the passed deal confirmation.
155
- #
156
- # @param [DealConfirmation] deal_confirmation The deal confirmation to print out.
157
153
  def print_deal_confirmation(deal_confirmation)
158
154
  puts <<-END
159
155
  Deal ID: #{deal_confirmation.deal_id}
@@ -167,8 +163,6 @@ END
167
163
  end
168
164
 
169
165
  # Prints out the profit/loss for the passed deal confirmation if applicable.
170
- #
171
- # @param [DealConfirmation] deal_confirmation The deal confirmation to print out the profit/loss for.
172
166
  def print_deal_confirmation_profit_loss(deal_confirmation)
173
167
  return unless deal_confirmation.profit
174
168
 
@@ -12,33 +12,51 @@ module IGMarkets
12
12
  #
13
13
  # See `README.md` for examples.
14
14
  #
15
- # If any errors occur while executing requests to the IG Markets API then {RequestFailedError} will be raised.
15
+ # If any errors occur while executing requests to the IG Markets API then an {IGMarketsError} will be raised.
16
16
  class DealingPlatform
17
- # @return [Session] The session used by this dealing platform.
17
+ # The session used by this dealing platform.
18
+ #
19
+ # @return [Session]
18
20
  attr_reader :session
19
21
 
20
- # @return [ClientAccountSummary] The summary of the client account that is returned as part of a successful sign in.
22
+ # The summary of the client account that is returned as part of a successful sign in.
23
+ #
24
+ # @return [ClientAccountSummary]
21
25
  attr_reader :client_account_summary
22
26
 
23
- # @return [AccountMethods] Methods for working with the logged in account.
27
+ # Methods for working with the logged in account.
28
+ #
29
+ # @return [AccountMethods]
24
30
  attr_reader :account
25
31
 
26
- # @return [ClientSentimentMethods] Methods for working with client sentiment.
32
+ # Methods for working with client sentiment.
33
+ #
34
+ # @return [ClientSentimentMethods]
27
35
  attr_reader :client_sentiment
28
36
 
29
- # @return [MarketMethods] Methods for working with markets.
37
+ # Methods for working with markets.
38
+ #
39
+ # @return [MarketMethods]
30
40
  attr_reader :markets
31
41
 
32
- # @return [PositionMethods] Methods for working with positions.
42
+ # Methods for working with positions.
43
+ #
44
+ # @return [PositionMethods]
33
45
  attr_reader :positions
34
46
 
35
- # @return [SprintMarketPositionMethods] Methods for working with sprint market positions.
47
+ # Methods for working with sprint market positions.
48
+ #
49
+ # @return [SprintMarketPositionMethods]
36
50
  attr_reader :sprint_market_positions
37
51
 
38
- # @return [WatchlistMethods] Methods for working with watchlists.
52
+ # Methods for working with watchlists.
53
+ #
54
+ # @return [WatchlistMethods]
39
55
  attr_reader :watchlists
40
56
 
41
- # @return [WorkingOrderMethods] Methods for working with working orders.
57
+ # Methods for working with working orders.
58
+ #
59
+ # @return [WorkingOrderMethods]
42
60
  attr_reader :working_orders
43
61
 
44
62
  def initialize
@@ -0,0 +1,323 @@
1
+ module IGMarkets
2
+ # Base class for all errors raised by this gem.
3
+ class IGMarketsError < StandardError
4
+ end
5
+
6
+ # This module contains all the error classes for this gem. They all subclass {IGMarketsError}.
7
+ module Errors
8
+ # This error is raised when the specified account is invalid.
9
+ class InvalidClientAccountError < IGMarketsError
10
+ end
11
+
12
+ # This error is raised when the API key can't be used for this endpoint.
13
+ class APIKeyRejectedError < IGMarketsError
14
+ end
15
+
16
+ # This error is raised when a specified deal can't be found.
17
+ class DealNotFoundError < IGMarketsError
18
+ end
19
+
20
+ # This error is raised when an invalid date range is specified.
21
+ class InvalidDateRangeError < IGMarketsError
22
+ end
23
+
24
+ # This error is raised when an invalid watchlist name is specified.
25
+ class InvalidWatchlistError < IGMarketsError
26
+ end
27
+
28
+ # This error is raised when a malformed date is specified.
29
+ class MalformedDateError < IGMarketsError
30
+ end
31
+
32
+ # This error is raised when a specified position can't be found.
33
+ class PositionNotFoundError < IGMarketsError
34
+ end
35
+
36
+ # This error is raised when a generic position error occurs.
37
+ class PositionError < IGMarketsError
38
+ end
39
+
40
+ # This error is raised when a specified EPIC can't be found.
41
+ class EPICNotFoundError < IGMarketsError
42
+ end
43
+
44
+ # This error is raised when the account's traffic allowance is exceeded.
45
+ class ExceededAccountAllowanceError < IGMarketsError
46
+ end
47
+
48
+ # This error is raised when the account's historical data allowance is exceeded.
49
+ class ExceededAccountHistoricalDataAllowanceError < IGMarketsError
50
+ end
51
+
52
+ # This error is raised when the account's trading allowance is exceeded.
53
+ class ExceededAccountTradingAllowanceError < IGMarketsError
54
+ end
55
+
56
+ # This error is raised when the API key's allowance is exceeded.
57
+ class ExceededAPIKeyAllowanceError < IGMarketsError
58
+ end
59
+
60
+ # This error is raised when trying to login with an unencrypted password to a server that requires encryption.
61
+ class EncryptionRequiredError < IGMarketsError
62
+ end
63
+
64
+ # This error is raised when required KYC (Know Your Client) authorization for this account is incomplete.
65
+ class KYCRequiredForAccountError < IGMarketsError
66
+ end
67
+
68
+ # This error is raised when relevant credentials are not supplied.
69
+ class MissingCredentialsError < IGMarketsError
70
+ end
71
+
72
+ # This error is raised when the account has pending agreements that must be agreed to before using the account.
73
+ class PendingAgreementsError < IGMarketsError
74
+ end
75
+
76
+ # This error is raised when the preferred account is disabled.
77
+ class PreferredAccountDisabledError < IGMarketsError
78
+ end
79
+
80
+ # This error is raised when the preferred account is not set.
81
+ class PreferredAccountNotSetError < IGMarketsError
82
+ end
83
+
84
+ # This error is raised when stockbroking is not supported.
85
+ class StockbrokingNotSupportedError < IGMarketsError
86
+ end
87
+
88
+ # This error is raised when too many EPICs are specified for a request.
89
+ class TooManyEPICSError < IGMarketsError
90
+ end
91
+
92
+ # This error is raised when the account has been denied login privileges.
93
+ class AccountAccessDeniedError < IGMarketsError
94
+ end
95
+
96
+ # This error is raised when the account has been migrated to the client-account model and should be authenticated
97
+ # with the relevant client credentials.
98
+ class AccountMigratedError < IGMarketsError
99
+ end
100
+
101
+ # This error is raised when the account has not yet been activated.
102
+ class AccountNotYetActivatedError < IGMarketsError
103
+ end
104
+
105
+ # This error is raised when the the account has been suspended.
106
+ class AccountSuspendedError < IGMarketsError
107
+ end
108
+
109
+ # This error is raised when the service requires an account token but the one given was invalid.
110
+ class AccountTokenInvalidError < IGMarketsError
111
+ end
112
+
113
+ # This error is raised when the service requires an account token but none was specified.
114
+ class AccountTokenMissingError < IGMarketsError
115
+ end
116
+
117
+ # This error is raised when all accounts are in the 'pending' state.
118
+ class AllAccountsPendingError < IGMarketsError
119
+ end
120
+
121
+ # This error is raised when all accounts are in the 'suspended' state.
122
+ class AllAccountsSuspendedError < IGMarketsError
123
+ end
124
+
125
+ # This error is raised when the specified API key is not enabled.
126
+ class APIKeyDisabledError < IGMarketsError
127
+ end
128
+
129
+ # This error is raised when the specified API key is invalid.
130
+ class APIKeyInvalidError < IGMarketsError
131
+ end
132
+
133
+ # This error is raised when no API key was specified.
134
+ class APIKeyMissingError < IGMarketsError
135
+ end
136
+
137
+ # This error is raised when the specified API key is not valid for the requested account.
138
+ class APIKeyRestrictedError < IGMarketsError
139
+ end
140
+
141
+ # This error is raised when the specified API key has been revoked.
142
+ class APIKeyRevokedError < IGMarketsError
143
+ end
144
+
145
+ # This error is raised when the client has been suspended from using the platform.
146
+ class ClientSuspendedError < IGMarketsError
147
+ end
148
+
149
+ # This error is raised when the specified client token is invalid.
150
+ class ClientTokenInvalidError < IGMarketsError
151
+ end
152
+
153
+ # This error is raised when no client token was specified.
154
+ class ClientTokenMissingError < IGMarketsError
155
+ end
156
+
157
+ # This error is raised when there is a generic security error.
158
+ class SecurityError < IGMarketsError
159
+ end
160
+
161
+ # This error is raised when the provided user agent string is not valid.
162
+ class InvalidApplicationError < IGMarketsError
163
+ end
164
+
165
+ # This error is raised when the specified user credentials are not valid.
166
+ class InvalidCredentialsError < IGMarketsError
167
+ end
168
+
169
+ # This error is raised when the requested site is not accessible through the API.
170
+ class InvalidWebsiteError < IGMarketsError
171
+ end
172
+
173
+ # This error is raised when there have been too many failed login attempts.
174
+ class TooManyFailedLoginAttemptsError < IGMarketsError
175
+ end
176
+
177
+ # This error is raised when an invalid EPIC was used when interacting with a watchlist.
178
+ class WatchlistInvalidEPICError < IGMarketsError
179
+ end
180
+
181
+ # This error is raised when trying to set the current account to the current account.
182
+ class AccountAlreadyCurrentError < IGMarketsError
183
+ end
184
+
185
+ # This error is raised when setting the default account is not allowed.
186
+ class CannotSetDefaultAccountError < IGMarketsError
187
+ end
188
+
189
+ # This error is raised when an invalid account ID was specified.
190
+ class InvalidAccountIDError < IGMarketsError
191
+ end
192
+
193
+ # This error is raised when an unsupported EPIC was specified.
194
+ class UnsupportedEPICError < IGMarketsError
195
+ end
196
+
197
+ # This error is raised when trying to delete a watchlist that can't be deleted.
198
+ class CannotDeleteWatchlistError < IGMarketsError
199
+ end
200
+
201
+ # This error is raised when trying to set two watchlists to have the same name.
202
+ class DuplicateWatchlistNameError < IGMarketsError
203
+ end
204
+
205
+ # This error is raised when a generic watchlist error occurs.
206
+ class WatchlistError < IGMarketsError
207
+ end
208
+
209
+ # This error is raised when the specified watchlist could not be found.
210
+ class WatchlistNotFoundError < IGMarketsError
211
+ end
212
+
213
+ # This error is raised when invalid input was supplied.
214
+ class InvalidInputError < IGMarketsError
215
+ end
216
+
217
+ # This error is raised when an invalid URL was specified.
218
+ class InvalidURLError < IGMarketsError
219
+ end
220
+
221
+ # This error is raised when a generic system error occurs.
222
+ class SystemError < IGMarketsError
223
+ end
224
+
225
+ # This error is raised when trying attempting unauthorised access to an equity.
226
+ class UnauthorisedAccessToEquityError < IGMarketsError
227
+ end
228
+
229
+ # This error is raised when the specified API key is not valid for the client.
230
+ class InvalidAPIKeyForClientError < IGMarketsError
231
+ end
232
+
233
+ # This error is raised when invalid JSON is returned by the IG Markets API.
234
+ class InvalidJSONError < IGMarketsError
235
+ end
236
+
237
+ # This error is raised when an HTTP connection error occurs.
238
+ class ConnectionError < IGMarketsError
239
+ end
240
+ end
241
+
242
+ # Base class for all errors raised by this gem.
243
+ class IGMarketsError
244
+ # Takes an IG Markets error code and returns an instance of the relevant error class that should be raised in
245
+ # response to the error.
246
+ #
247
+ # @param [String] error_code The error code.
248
+ #
249
+ # @return [LightstreamerError]
250
+ #
251
+ # @private
252
+ def self.build(error_code)
253
+ if API_ERROR_CODE_TO_CLASS.key? error_code
254
+ API_ERROR_CODE_TO_CLASS[error_code].new ''
255
+ else
256
+ new error_code
257
+ end
258
+ end
259
+
260
+ API_ERROR_CODE_TO_CLASS = {
261
+ 'authentication.failure.not-a-client-account' => Errors::InvalidClientAccountError,
262
+ 'endpoint.unavailable.for.api-key' => Errors::APIKeyRejectedError,
263
+ 'error.confirms.deal-not-found' => Errors::DealNotFoundError,
264
+ 'error.invalid.daterange' => Errors::InvalidDateRangeError,
265
+ 'error.invalid.watchlist' => Errors::InvalidWatchlistError,
266
+ 'error.malformed.date' => Errors::MalformedDateError,
267
+ 'error.position.notfound' => Errors::PositionNotFoundError,
268
+ 'error.positions.generic' => Errors::PositionError,
269
+ 'error.public-api.epic-not-found' => Errors::EPICNotFoundError,
270
+ 'error.public-api.exceeded-account-allowance' => Errors::ExceededAccountAllowanceError,
271
+ 'error.public-api.exceeded-account-historical-data-allowance' =>
272
+ Errors::ExceededAccountHistoricalDataAllowanceError,
273
+ 'error.public-api.exceeded-account-trading-allowance' => Errors::ExceededAccountTradingAllowanceError,
274
+ 'error.public-api.exceeded-api-key-allowance' => Errors::ExceededAPIKeyAllowanceError,
275
+ 'error.public-api.failure.encryption.required' => Errors::EncryptionRequiredError,
276
+ 'error.public-api.failure.kyc.required' => Errors::KYCRequiredForAccountError,
277
+ 'error.public-api.failure.missing.credentials' => Errors::MissingCredentialsError,
278
+ 'error.public-api.failure.pending.agreements.required' => Errors::PendingAgreementsError,
279
+ 'error.public-api.failure.preferred.account.disabled' => Errors::PreferredAccountDisabledError,
280
+ 'error.public-api.failure.preferred.account.not.set' => Errors::PreferredAccountNotSetError,
281
+ 'error.public-api.failure.stockbroking-not-supported' => Errors::StockbrokingNotSupportedError,
282
+ 'error.public-api.too-many-epics' => Errors::TooManyEPICSError,
283
+ 'error.security.account-access-denied' => Errors::AccountAccessDeniedError,
284
+ 'error.security.account-migrated' => Errors::AccountMigratedError,
285
+ 'error.security.account-not-yet-activated' => Errors::AccountNotYetActivatedError,
286
+ 'error.security.account-suspended' => Errors::AccountSuspendedError,
287
+ 'error.security.account-token-invalid' => Errors::AccountTokenInvalidError,
288
+ 'error.security.account-token-missing' => Errors::AccountTokenMissingError,
289
+ 'error.security.all-accounts-pending' => Errors::AllAccountsPendingError,
290
+ 'error.security.all-accounts-suspended' => Errors::AllAccountsSuspendedError,
291
+ 'error.security.api-key-disabled' => Errors::APIKeyDisabledError,
292
+ 'error.security.api-key-invalid' => Errors::APIKeyInvalidError,
293
+ 'error.security.api-key-missing' => Errors::APIKeyMissingError,
294
+ 'error.security.api-key-restricted' => Errors::APIKeyRestrictedError,
295
+ 'error.security.api-key-revoked' => Errors::APIKeyRevokedError,
296
+ 'error.security.client-suspended' => Errors::ClientSuspendedError,
297
+ 'error.security.client-token-invalid' => Errors::ClientTokenInvalidError,
298
+ 'error.security.client-token-missing' => Errors::ClientTokenMissingError,
299
+ 'error.security.generic' => Errors::SecurityError,
300
+ 'error.security.invalid-application' => Errors::InvalidApplicationError,
301
+ 'error.security.invalid-details' => Errors::InvalidCredentialsError,
302
+ 'error.security.invalid-website' => Errors::InvalidWebsiteError,
303
+ 'error.security.too-many-failed-attempts' => Errors::TooManyFailedLoginAttemptsError,
304
+ 'error.service.watchlists.add-instrument.invalid-epic' => Errors::WatchlistInvalidEPICError,
305
+ 'error.switch.accountId-must-be-different' => Errors::AccountAlreadyCurrentError,
306
+ 'error.switch.cannot-set-default-account' => Errors::CannotSetDefaultAccountError,
307
+ 'error.switch.invalid-accountId' => Errors::InvalidAccountIDError,
308
+ 'error.unsupported.epic' => Errors::UnsupportedEPICError,
309
+ 'error.watchlists.management.cannot-delete-watchlist' => Errors::CannotDeleteWatchlistError,
310
+ 'error.watchlists.management.duplicate-name' => Errors::DuplicateWatchlistNameError,
311
+ 'error.watchlists.management.error' => Errors::WatchlistError,
312
+ 'error.watchlists.management.watchlist-not-found' => Errors::WatchlistNotFoundError,
313
+ 'invalid.input' => Errors::InvalidInputError,
314
+ 'invalid.url' => Errors::InvalidURLError,
315
+ 'system.error' => Errors::SystemError,
316
+ 'unauthorised.access.to.equity.exception' => Errors::UnauthorisedAccessToEquityError,
317
+ 'unauthorised.api-key.revoked' => Errors::APIKeyRevokedError,
318
+ 'unauthorised.clientId.api-key.mismatch' => Errors::InvalidAPIKeyForClientError
319
+ }.freeze
320
+
321
+ private_constant :API_ERROR_CODE_TO_CLASS
322
+ end
323
+ end
@@ -1,5 +1,7 @@
1
1
  module IGMarkets
2
2
  # This module contains shared methods for formatting different content types for display.
3
+ #
4
+ # @private
3
5
  module Format
4
6
  module_function
5
7
 
@@ -3,7 +3,9 @@ module IGMarkets
3
3
  # attribute is defined by a call to {attribute}. Attributes have standard getter and setter methods and can also
4
4
  # be subject to a variety of constraints and validations, see {attribute} for further details.
5
5
  class Model
6
- # @return [Hash] The current attribute values set on this model.
6
+ # The current attribute values set on this model.
7
+ #
8
+ # @return [Hash]
7
9
  attr_reader :attributes
8
10
 
9
11
  # Initializes this new model with the given attribute values. Attributes not known to this model will raise
@@ -74,10 +76,14 @@ module IGMarkets
74
76
  end
75
77
 
76
78
  class << self
77
- # @return [Hash] A hash containing details of all attributes that have been defined on this model.
79
+ # A hash containing details of all attributes that have been defined on this model.
80
+ #
81
+ # @return [Hash]
78
82
  attr_accessor :defined_attributes
79
83
 
80
- # @return [Array] The names of the deprecated attributes on this model.
84
+ # The names of the deprecated attributes on this model.
85
+ #
86
+ # @return [Array]
81
87
  attr_accessor :deprecated_attributes
82
88
 
83
89
  # Returns the names of all currently defined attributes for this model.
@@ -1,10 +1,16 @@
1
1
  module IGMarkets
2
2
  # Encrypts account passwords in the manner required for authentication with the IG Markets API.
3
+ #
4
+ # @private
3
5
  class PasswordEncryptor
4
- # @return [OpenSSL::PKey::RSA] The public key used by {#encrypt}, can also be set using {#encoded_public_key=}.
6
+ # The public key used by {#encrypt}, can also be set using {#encoded_public_key=}.
7
+ #
8
+ # @return [OpenSSL::PKey::RSA]
5
9
  attr_accessor :public_key
6
10
 
7
- # @return [String] The time stamp used by {#encrypt}.
11
+ # The time stamp used by {#encrypt}.
12
+ #
13
+ # @return [String]
8
14
  attr_accessor :time_stamp
9
15
 
10
16
  # Initializes this password encryptor with the specified encoded public key and timestamp.
@@ -1,5 +1,7 @@
1
1
  module IGMarkets
2
2
  # Contains methods for formatting payloads that can be passed to the IG Markets API.
3
+ #
4
+ # @private
3
5
  module PayloadFormatter
4
6
  module_function
5
7
 
@@ -1,5 +1,7 @@
1
1
  module IGMarkets
2
2
  # Contains regex's for validating specific types of strings.
3
+ #
4
+ # @private
3
5
  module Regex
4
6
  # Regex used to validate an ISO currency code.
5
7
  CURRENCY = /\A[A-Z]{3}\Z/
@@ -1,9 +1,13 @@
1
1
  module IGMarkets
2
2
  # This class contains methods for printing a REST request and its JSON response for inspection and debugging.
3
3
  # Request printing is enabled by setting {enabled} to `true`.
4
+ #
5
+ # @private
4
6
  class RequestPrinter
5
7
  class << self
6
- # @return [Boolean] Whether the request printer is enabled.
8
+ # Whether the request printer is enabled.
9
+ #
10
+ # @return [Boolean]
7
11
  attr_accessor :enabled
8
12
 
9
13
  # Prints out an options hash that is ready to be passed to `RestClient::Request.execute`.
@@ -1,25 +1,16 @@
1
1
  module IGMarkets
2
2
  # Contains methods for parsing responses received from the IG Markets API.
3
+ #
4
+ # @private
3
5
  module ResponseParser
4
6
  module_function
5
7
 
6
- # Parses the passed JSON string and then passes it on to {parse}. If `json` is not valid JSON then `{}` is returned.
7
- #
8
- # @param [String] json The JSON string.
9
- #
10
- # @return [Hash]
11
- def parse_json(json)
12
- parse JSON.parse(json)
13
- rescue JSON::ParserError
14
- {}
15
- end
16
-
17
8
  # Parses the specified value that was returned from a call to the IG Markets API.
18
9
  #
19
- # @param [Hash, Array, Object] response The response or part of a reponse that should be parsed. If this is of type
10
+ # @param [Hash, Array, Object] response The response or part of a response that should be parsed. If this is of type
20
11
  # `Hash` then all hash keys will converted from camel case into snake case and their values each to parsed
21
- # individually by a recursive call. If this is of type `Array` then each item will be parsed indidiaully by a
22
- # recursive call. All other types are passed through unchanged.
12
+ # individually by a recursive call. If this is of type `Array` then each item will be parsed individually by
13
+ # a recursive call. All other types are passed through unchanged.
23
14
  #
24
15
  # @return [Hash, Array, Object] The parsed object, the type depends on the type of the `response` parameter.
25
16
  def parse(response)
@@ -3,45 +3,55 @@ module IGMarkets
3
3
  # In order to sign in, {#username}, {#password}, {#api_key} and {#platform} must be set. {#platform} must be
4
4
  # either `:demo` or `:live` depending on which platform is being targeted.
5
5
  class Session
6
- # @return [String] The username to use to authenticate this session.
6
+ # The username to use to authenticate this session.
7
+ #
8
+ # @return [String]
7
9
  attr_accessor :username
8
10
 
9
- # @return [String] The password to use to authenticate this session.
11
+ # The password to use to authenticate this session.
12
+ #
13
+ # @return [String]
10
14
  attr_accessor :password
11
15
 
12
- # @return [String] The API key to use to authenticate this session.
16
+ # The API key to use to authenticate this session.
17
+ #
18
+ # @return [String]
13
19
  attr_accessor :api_key
14
20
 
15
- # @return [:demo, :live] The platform variant to log into for this session.
21
+ # The platform variant to log into for this session.
22
+ #
23
+ # @return [:demo, :live]
16
24
  attr_accessor :platform
17
25
 
18
- # @return [String] The client session security access token for the currently logged in session, or `nil` if there
19
- # is no active session.
26
+ # The client session security access token for the currently logged in session, or `nil` if there is no active
27
+ # session.
28
+ #
29
+ # @return [String]
20
30
  attr_reader :client_security_token
21
31
 
22
- # @return [String] The account session security access token for the currently logged in session, or `nil` if there
23
- # is no active session.
32
+ # The account session security access token for the currently logged in session, or `nil` if there is no active
33
+ # session.
34
+ #
35
+ # @return [String]
24
36
  attr_reader :x_security_token
25
37
 
26
38
  # Signs in to IG Markets using the values of {#username}, {#password}, {#api_key} and {#platform}. If an error
27
- # occurs then {RequestFailedError} will be raised.
39
+ # occurs then an {IGMarketsError} will be raised.
28
40
  #
29
41
  # @return [Hash] The data returned in the body of the sign in request.
30
42
  def sign_in
31
- validate_authentication
43
+ body = { identifier: username, password: password_encryptor.encrypt(password), encryptedPassword: true }
32
44
 
33
- payload = { identifier: username, password: password_encryptor.encrypt(password), encryptedPassword: true }
34
-
35
- sign_in_result = request method: :post, url: 'session', payload: payload, api_version: API_V2
45
+ sign_in_result = request method: :post, url: 'session', body: body, api_version: API_V2
36
46
 
37
47
  headers = sign_in_result.fetch(:response).headers
38
- @client_security_token = headers.fetch :cst
39
- @x_security_token = headers.fetch :x_security_token
48
+ @client_security_token = headers.fetch 'CST'
49
+ @x_security_token = headers.fetch 'X-SECURITY-TOKEN'
40
50
 
41
- sign_in_result.fetch :result
51
+ sign_in_result.fetch :body
42
52
  end
43
53
 
44
- # Signs out of IG Markets, ending the current session (if any). If an error occurs then {RequestFailedError} will be
54
+ # Signs out of IG Markets, ending the current session (if any). If an error occurs then an {IGMarketsError} will be
45
55
  # raised.
46
56
  def sign_out
47
57
  delete 'session' if alive?
@@ -56,61 +66,53 @@ module IGMarkets
56
66
  !client_security_token.nil? && !x_security_token.nil?
57
67
  end
58
68
 
59
- # Sends a POST request to the IG Markets API. If an error occurs then {RequestFailedError} will be raised.
69
+ # Sends a POST request to the IG Markets API. If an error occurs then an {IGMarketsError} will be raised.
60
70
  #
61
71
  # @param [String] url The URL to send the POST request to.
62
- # @param [nil, String, Hash] payload The payload to include with the POST request, this will be encoded as JSON.
72
+ # @param [Hash, nil] body The body to include with the POST request, this will be encoded as JSON.
63
73
  # @param [Fixnum] api_version The API version to target.
64
74
  #
65
75
  # @return [Hash] The response from the IG Markets API.
66
- def post(url, payload, api_version = API_V1)
67
- request(method: :post, url: url, payload: payload, api_version: api_version).fetch :result
76
+ def post(url, body, api_version = API_V1)
77
+ request(method: :post, url: url, body: body, api_version: api_version).fetch :body
68
78
  end
69
79
 
70
- # Sends a GET request to the IG Markets API. If an error occurs then {RequestFailedError} will be raised.
80
+ # Sends a GET request to the IG Markets API. If an error occurs then an {IGMarketsError} will be raised.
71
81
  #
72
82
  # @param [String] url The URL to send the GET request to.
73
83
  # @param [Fixnum] api_version The API version to target.
74
84
  #
75
85
  # @return [Hash] The response from the IG Markets API.
76
86
  def get(url, api_version = API_V1)
77
- request(method: :get, url: url, api_version: api_version).fetch :result
87
+ request(method: :get, url: url, api_version: api_version).fetch :body
78
88
  end
79
89
 
80
- # Sends a PUT request to the IG Markets API. If an error occurs then {RequestFailedError} will be raised.
90
+ # Sends a PUT request to the IG Markets API. If an error occurs then an {IGMarketsError} will be raised.
81
91
  #
82
92
  # @param [String] url The URL to send the PUT request to.
83
- # @param [nil, String, Hash] payload The payload to include with the PUT request, this will be encoded as JSON.
93
+ # @param [Hash, nil] body The body to include with the PUT request, this will be encoded as JSON.
84
94
  # @param [Fixnum] api_version The API version to target.
85
95
  #
86
96
  # @return [Hash] The response from the IG Markets API.
87
- def put(url, payload = nil, api_version = API_V1)
88
- request(method: :put, url: url, payload: payload, api_version: api_version).fetch :result
97
+ def put(url, body = nil, api_version = API_V1)
98
+ request(method: :put, url: url, body: body, api_version: api_version).fetch :body
89
99
  end
90
100
 
91
- # Sends a DELETE request to the IG Markets API. If an error occurs then {RequestFailedError} will be raised.
101
+ # Sends a DELETE request to the IG Markets API. If an error occurs then an {IGMarketsError} will be raised.
92
102
  #
93
103
  # @param [String] url The URL to send the DELETE request to.
94
- # @param [nil, String, Hash] payload The payload to include with the DELETE request, this will be encoded as JSON.
104
+ # @param [Hash, nil] body The body to include with the DELETE request, this will be encoded as JSON.
95
105
  # @param [Fixnum] api_version The API version to target.
96
106
  #
97
107
  # @return [Hash] The response from the IG Markets API.
98
- def delete(url, payload = nil, api_version = API_V1)
99
- request(method: :delete, url: url, payload: payload, api_version: api_version).fetch :result
108
+ def delete(url, body = nil, api_version = API_V1)
109
+ request(method: :delete, url: url, body: body, api_version: api_version).fetch :body
100
110
  end
101
111
 
102
112
  private
103
113
 
104
114
  HOST_URLS = { demo: 'https://demo-api.ig.com/gateway/deal/', live: 'https://api.ig.com/gateway/deal/' }.freeze
105
115
 
106
- def validate_authentication
107
- %i(username password api_key).each do |attribute|
108
- raise ArgumentError, "#{attribute} is not set" if send(attribute).to_s.empty?
109
- end
110
-
111
- raise ArgumentError, 'platform is invalid' unless HOST_URLS.key? platform
112
- end
113
-
114
116
  def password_encryptor
115
117
  result = get 'session/encryptionKey'
116
118
 
@@ -120,76 +122,73 @@ module IGMarkets
120
122
  def request(options)
121
123
  options[:url] = "#{HOST_URLS.fetch(platform)}#{URI.escape(options[:url])}"
122
124
  options[:headers] = request_headers(options)
123
- options[:payload] = options[:payload] && options[:payload].to_json
124
-
125
- RequestPrinter.print_options options
126
-
127
- response = execute_request options
128
125
 
129
- RequestPrinter.print_response_body response.body
126
+ # The IG Markets API requires that DELETE requests with a body are sent as POST requests with a special header
127
+ if options[:method] == :delete && options[:body]
128
+ options[:headers]['_method'] = 'DELETE'
129
+ options[:method] = :post
130
+ end
130
131
 
131
- result = process_response response
132
+ options[:body] = options[:body] && options[:body].to_json
132
133
 
133
- { response: response, result: result }
134
+ execute_request options
134
135
  end
135
136
 
136
137
  def request_headers(options)
137
138
  headers = {}
138
139
 
139
- headers[:content_type] = headers[:accept] = 'application/json; charset=UTF-8'
140
- headers[:'X-IG-API-KEY'] = api_key
141
- headers[:version] = options.delete :api_version
140
+ headers['Content-Type'] = headers['Accept'] = 'application/json; charset=UTF-8'
141
+ headers['X-IG-API-KEY'] = api_key
142
+ headers['Version'] = options.delete :api_version
142
143
 
143
- headers[:cst] = client_security_token if client_security_token
144
- headers[:x_security_token] = x_security_token if x_security_token
144
+ headers['CST'] = client_security_token if client_security_token
145
+ headers['X-SECURITY-TOKEN'] = x_security_token if x_security_token
145
146
 
146
147
  headers
147
148
  end
148
149
 
149
- def use_post_for_delete_with_payload(options)
150
- if options[:method] == :delete && options[:payload]
151
- options[:headers]['_method'] = :delete
152
- options[:method] = :post
153
- end
154
- end
155
-
156
150
  def execute_request(options)
157
- use_post_for_delete_with_payload options
151
+ RequestPrinter.print_options options
158
152
 
159
- RestClient::Request.execute options
160
- rescue RestClient::Exception => exception
161
- raise RequestFailedError, exception.message unless exception.response
153
+ response = Excon.send options[:method], options[:url], headers: options[:headers], body: options[:body]
162
154
 
163
- return exception.response unless should_retry_request? exception.response, options
155
+ RequestPrinter.print_response_body response.body
164
156
 
165
- execute_request options.merge(retry: true)
166
- rescue SocketError => socket_error
167
- raise RequestFailedError, socket_error
157
+ process_response response, options
158
+ rescue Excon::Error => error
159
+ raise Errors::ConnectionError, error.message
168
160
  end
169
161
 
170
- def should_retry_request?(response, options)
171
- error_code = ResponseParser.parse_json(response.body)[:error_code]
162
+ def process_response(response, options)
163
+ body = parse_body response
172
164
 
173
- if error_code == 'error.security.client-token-invalid' && !options[:retry]
174
- sign_in
175
- return true
176
- end
165
+ if body.key? :error_code
166
+ error = IGMarketsError.build body[:error_code]
177
167
 
178
- if error_code =~ /^error\.public-api\.exceeded-(api-key|account)-allowance/
179
- sleep 5
180
- return true
181
- end
168
+ raise error unless should_retry_request? error, options
182
169
 
183
- false
170
+ execute_request options.merge(retry: true)
171
+ else
172
+ { response: response, body: body }
173
+ end
184
174
  end
185
175
 
186
- def process_response(response)
187
- result = ResponseParser.parse_json response.body
188
- http_code = response.code
176
+ def parse_body(response)
177
+ return {} if response.body == ''
189
178
 
190
- raise RequestFailedError.new(result[:error_code], http_code) unless http_code >= 200 && http_code < 300
179
+ ResponseParser.parse JSON.parse(response.body)
180
+ rescue JSON::ParserError
181
+ raise Errors::InvalidJSONError, response.body
182
+ end
191
183
 
192
- result
184
+ def should_retry_request?(error, options)
185
+ if error.is_a?(Errors::ClientTokenInvalidError) && !options[:retry]
186
+ sign_in
187
+ true
188
+ elsif error.is_a?(Errors::ExceededAPIKeyAllowanceError) || error.is_a?(Errors::ExceededAccountAllowanceError)
189
+ sleep 5
190
+ true
191
+ end
193
192
  end
194
193
  end
195
194
  end
@@ -1,4 +1,4 @@
1
1
  module IGMarkets
2
2
  # The version of this gem.
3
- VERSION = '0.16'.freeze
3
+ VERSION = '0.17'.freeze
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ig_markets
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.16'
4
+ version: '0.17'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Viney
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-07-20 00:00:00.000000000 Z
11
+ date: 2016-07-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -25,33 +25,33 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.8'
27
27
  - !ruby/object:Gem::Dependency
28
- name: pry
28
+ name: excon
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.10.4
33
+ version: '0.51'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.10.4
40
+ version: '0.51'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rest-client
42
+ name: pry
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '2.0'
47
+ version: 0.10.4
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '2.0'
54
+ version: 0.10.4
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: terminal-table
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -184,14 +184,14 @@ dependencies:
184
184
  requirements:
185
185
  - - "~>"
186
186
  - !ruby/object:Gem::Version
187
- version: '0.41'
187
+ version: '0.42'
188
188
  type: :development
189
189
  prerelease: false
190
190
  version_requirements: !ruby/object:Gem::Requirement
191
191
  requirements:
192
192
  - - "~>"
193
193
  - !ruby/object:Gem::Version
194
- version: '0.41'
194
+ version: '0.42'
195
195
  - !ruby/object:Gem::Dependency
196
196
  name: yard
197
197
  requirement: !ruby/object:Gem::Requirement
@@ -263,6 +263,7 @@ files:
263
263
  - lib/ig_markets/dealing_platform/sprint_market_position_methods.rb
264
264
  - lib/ig_markets/dealing_platform/watchlist_methods.rb
265
265
  - lib/ig_markets/dealing_platform/working_order_methods.rb
266
+ - lib/ig_markets/errors.rb
266
267
  - lib/ig_markets/format.rb
267
268
  - lib/ig_markets/historical_price_result.rb
268
269
  - lib/ig_markets/instrument.rb
@@ -275,7 +276,6 @@ files:
275
276
  - lib/ig_markets/payload_formatter.rb
276
277
  - lib/ig_markets/position.rb
277
278
  - lib/ig_markets/regex.rb
278
- - lib/ig_markets/request_failed_error.rb
279
279
  - lib/ig_markets/request_printer.rb
280
280
  - lib/ig_markets/response_parser.rb
281
281
  - lib/ig_markets/session.rb
@@ -307,6 +307,5 @@ rubyforge_project:
307
307
  rubygems_version: 2.5.1
308
308
  signing_key:
309
309
  specification_version: 4
310
- summary: Ruby library and command-line client for accessing the IG Markets dealing
311
- platform.
310
+ summary: Library and command-line client for accessing the IG Markets dealing platform.
312
311
  test_files: []
@@ -1,21 +0,0 @@
1
- module IGMarkets
2
- # This error class is raised by {Session} when a request to the IG Markets API fails.
3
- class RequestFailedError < StandardError
4
- # @return [String] A description of the error that occurred when the request was attempted.
5
- attr_reader :error
6
-
7
- # @return [Fixnum] The HTTP code that was returned, or `nil` if unknown.
8
- attr_reader :http_code
9
-
10
- # Initializes this request failure error with a message and an HTTP code.
11
- #
12
- # @param [String] error The error description.
13
- # @param [Integer] http_code The HTTP code for the request failure, if known.
14
- def initialize(error, http_code = nil)
15
- @error = error.to_s
16
- @http_code = http_code ? http_code.to_i : nil
17
-
18
- super "#<#{self.class.name} error: #{@error}#{http_code ? ", http_code: #{http_code}" : ''}>"
19
- end
20
- end
21
- end