ig_markets 0.16 → 0.17

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: 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