ig_markets 0.18 → 0.19

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: 2b3fab7b62a9c929a8262f9ef104c200278e03b2
4
- data.tar.gz: 1fe0c83b896f00e493a2e7a4257fb2f43a80188f
3
+ metadata.gz: 6e6fa29281db18808e948435c5dd1dcb5da3b003
4
+ data.tar.gz: ef3c666877d55b0c85236e1982b6d6d55713ad90
5
5
  SHA512:
6
- metadata.gz: e3325e6ea4704dc3bc75f333d82718115a82febc8aa4f814bd10f3495bdf27a3fc43bafa59b39fe0a7c01a454c8417478ef4aa9588551cfbe5875ab4927e2ae6
7
- data.tar.gz: e4578ffa1bb86eab2cfb2b32a0223e9792db69ce50f433e6da2936415fac0cc248d51a605b37a1a777434da01697e6e5def084e5911faad5c1ad565b9fade536
6
+ metadata.gz: 01471a39066771f1bbbb4df8faff8c500072af4bd93eef58a246c2669f495a3307d136b190b67c45e27bb4774515aada15d40099caf388ee4459e2c9ec76fab9
7
+ data.tar.gz: 314ec4b9d9005ae4dd5117dd2ec5380f5338a6a53a4be1d32f0a3592327dc331fa5153f97e5197565badec61dfd6c54863b7449c45f9559670ca0ccb32687179
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # IG Markets Changelog
2
2
 
3
+ ### 0.19 — August 5, 2016
4
+
5
+ - Added `ig_markets stream` command that displays live streaming data for accounts, markets and trading activity
6
+ - Added `IGMarkets::DealingPlatform#lightstreamer_session` that returns a `Lightstreamer::Session` instance that is
7
+ ready to connect and start streaming data
8
+ - Improved parsing of expiry dates on instruments and dealing confirmations
9
+ - If the API returns an unexpected enum value then a one-time warning is now emitted and the value is treated as `nil`,
10
+ this replaces the previous behavior of raising an exception and makes the library more future-proof
11
+ - When a traffic allowance is exceeded the time waited before retrying is now ten seconds instead of five
12
+ - HTTP response headers are now shown when in verbose mode
13
+
3
14
  ### 0.18 — August 2, 2016
4
15
 
5
16
  - Added support for the recently added `#date` attribute on `IGMarkets::DealConfirmation`
data/README.md CHANGED
@@ -13,14 +13,15 @@ provided command-line client. Written against the [official REST API](http://lab
13
13
 
14
14
  Includes support for:
15
15
 
16
- * Activity and transaction history
17
- * Positions
18
- * Sprint market positions
19
- * Working orders
20
- * Market navigation, searches and snapshots
21
- * Historical prices
22
- * Watchlists
23
- * Client sentiment
16
+ - Activity and transaction history
17
+ - Positions
18
+ - Sprint market positions
19
+ - Working orders
20
+ - Market navigation, searches and snapshots
21
+ - Historical prices
22
+ - Watchlists
23
+ - Client sentiment
24
+ - Authentication profiles
24
25
 
25
26
  An IG Markets live or demo trading account is needed in order to use this gem.
26
27
 
@@ -99,6 +100,7 @@ commands and their subcommands is:
99
100
  - `ig_markets sentiment MARKET`
100
101
  - `ig_markets sprints [list]`
101
102
  - `ig_markets sprints create ...`
103
+ - `ig_markets stream ...`
102
104
  - `ig_markets transactions --days N [...]`
103
105
  - `ig_markets watchlists [list]`
104
106
  - `ig_markets watchlists create NAME [EPIC ...]`
@@ -115,10 +117,10 @@ ig_markets account
115
117
  # Print EUR/USD transactions from the last week, excluding interest transactions
116
118
  ig_markets transactions --days 7 --instrument EUR/USD --no-interest
117
119
 
118
- # Search for EURUSD currency markets
120
+ # Search for EURUSD markets
119
121
  ig_markets search EURUSD --type currencies
120
122
 
121
- # Print details for the EURUSD currency pair and the Dow Jones Industrial Average
123
+ # Print details for the EURUSD pair and the Dow Jones Industrial Average
122
124
  ig_markets markets CS.D.EURUSD.CFD.IP IX.D.DOW.IFD.IP
123
125
 
124
126
  # Print current positions in aggregate
@@ -150,6 +152,9 @@ ig_markets prices --epic CS.D.EURUSD.CFD.IP --resolution day --number 14
150
152
  # Print account dealing performance from the last 90 days, broken down by the EPICs that were traded
151
153
  ig_markets performance --days 90
152
154
 
155
+ # Print streaming details of account balances, trading actions, and the live price of the EURUSD pair
156
+ ig_markets stream --accounts --trades --markets CS.D.EURUSD.CFD.IP
157
+
153
158
  # Log in and open a Ruby console which can be used to query the IG API, printing all REST requests
154
159
  ig_markets console --verbose
155
160
  ```
@@ -2,52 +2,169 @@ module IGMarkets
2
2
  module CLI
3
3
  # Implements the `ig_markets stream` command.
4
4
  class Main < Thor
5
- desc 'stream', 'Displays a live stream of prices for a set of markets'
5
+ desc 'stream', 'Displays live streaming updates of account balances, markets and trading activity'
6
6
 
7
- option :epics, type: :array, desc: 'The EPICs of the markets to stream live prices for'
7
+ option :accounts, type: :boolean, desc: 'Whether to stream changes to account balances'
8
+ option :markets, type: :array, desc: 'The EPICs of the markets to stream live prices for'
9
+ option :trades, type: :boolean, desc: 'Whether to stream details of any trades and position or order updates'
8
10
 
9
11
  def stream
10
12
  self.class.begin_session(options) do |dealing_platform|
11
- @data_queue = Queue.new
13
+ @dealing_platform = dealing_platform
12
14
 
13
- lightstreamer = create_lightstreamer_session dealing_platform
14
- create_lightstreamer_subscription lightstreamer
15
+ prepare_stream
15
16
 
16
- loop do
17
- puts @data_queue.pop
18
-
19
- raise lightstreamer.error if lightstreamer.error
20
- end
17
+ loop { process_new_data @queue.pop }
21
18
  end
22
19
  end
23
20
 
24
21
  private
25
22
 
26
- def create_lightstreamer_session(dealing_platform)
27
- session = dealing_platform.session
23
+ def process_new_data(data)
24
+ raise data if data.is_a? Lightstreamer::LightstreamerError
25
+
26
+ puts data
27
+ end
28
+
29
+ def prepare_stream
30
+ @queue = Queue.new
31
+
32
+ @lightstreamer = @dealing_platform.lightstreamer_session
33
+ @lightstreamer.on_error { |error| @queue.push error }
34
+ @lightstreamer.connect
35
+
36
+ subscriptions = [account_subscription, market_subscription, trade_subscription].compact
37
+ @lightstreamer.bulk_subscription_start subscriptions, snapshot: true
38
+ end
39
+
40
+ def account_subscription
41
+ return unless options[:accounts]
42
+
43
+ items = @dealing_platform.client_account_summary.accounts.map { |account| "ACCOUNT:#{account.account_id}" }
44
+ fields = [:pnl, :deposit, :available_cash, :funds, :margin, :available_to_deal, :equity]
45
+
46
+ subscription = @lightstreamer.build_subscription items: items, fields: fields, mode: :merge
47
+
48
+ subscription.on_data do |_subscription, item_name, item_data, _new_data|
49
+ @queue.push "#{item_name} - #{item_data.map { |key, value| "#{key}: #{value}" }.join ', '}"
50
+ end
51
+
52
+ subscription
53
+ end
54
+
55
+ def market_subscription
56
+ items = Array(options[:markets]).map { |market| "MARKET:#{market}" }
57
+ fields = [:bid, :offer, :high, :low, :mid_open, :strike_price, :odds]
58
+
59
+ subscription = @lightstreamer.build_subscription items: items, fields: fields, mode: :merge
60
+
61
+ subscription.on_data do |_subscription, item_name, item_data, _new_data|
62
+ @queue.push "#{item_name} - #{item_data.map { |key, value| "#{key}: #{value}" }.join ', '}"
63
+ end
64
+
65
+ subscription
66
+ end
67
+
68
+ def trade_subscription
69
+ return unless options[:trades]
28
70
 
29
- server_url = dealing_platform.client_account_summary.lightstreamer_endpoint
30
- username = dealing_platform.client_account_summary.client_id
31
- password = "CST-#{session.client_security_token}|XST-#{session.x_security_token}"
71
+ items = @dealing_platform.client_account_summary.accounts.map { |account| "TRADE:#{account.account_id}" }
72
+ fields = [:confirms, :opu, :wou]
32
73
 
33
- lightstreamer = Lightstreamer::Session.new server_url: server_url, username: username, password: password
74
+ subscription = @lightstreamer.build_subscription items: items, fields: fields, mode: :distinct
34
75
 
35
- lightstreamer.connect
36
- puts "Session ID: #{lightstreamer.session_id}"
76
+ subscription.on_data do |_subscription, item_name, item_data, _new_data|
77
+ process_confirmation item_name, item_data[:confirms]
78
+ process_position_update item_name, item_data[:opu]
79
+ process_working_order_update item_name, item_data[:wou]
80
+ end
37
81
 
38
- lightstreamer
82
+ subscription
39
83
  end
40
84
 
41
- def create_lightstreamer_subscription(lightstreamer)
42
- items = Array(options[:epics]).map { |market| "MARKET:#{market}" }
85
+ def process_confirmation(item_name, json)
86
+ return unless json
87
+
88
+ model = DealConfirmation.new ResponseParser.parse(JSON.parse(json)).merge(affected_deals: nil)
43
89
 
44
- subscription = lightstreamer.build_subscription items: items, fields: [:bid, :offer], mode: :merge
90
+ queue_item = "#{item_name} - Confirmation - id: #{model.deal_id}"
45
91
 
46
- subscription.on_data do |_subscription, item_name, item_data, _new_values|
47
- @data_queue.push "#{item_name[7..-1]} - #{item_data.map { |key, value| "#{key}: #{value}" }.join ', '}"
92
+ [:status, :deal_status, :epic, :direction, :size, :level, :stop_distance, :stop_level, :limit_distance,
93
+ :limit_level].each do |attribute|
94
+ value = model.send attribute
95
+ queue_item << ", #{attribute}: #{value}" if value
48
96
  end
49
97
 
50
- subscription.start snapshot: true
98
+ queue_item << ", profit: #{Format.currency(model.profit, model.profit_currency)}" if model.profit
99
+
100
+ @queue.push queue_item
101
+ end
102
+
103
+ def process_position_update(item_name, json)
104
+ return unless json
105
+
106
+ model = OpenPositionUpdate.new ResponseParser.parse(JSON.parse(json))
107
+
108
+ queue_item = "#{item_name} - Position update - id: #{model.deal_id}"
109
+
110
+ [:status, :deal_status, :epic, :direction, :size, :level, :stop_level, :limit_level].each do |attribute|
111
+ value = model.send attribute
112
+ queue_item << ", #{attribute}: #{value}" if value
113
+ end
114
+
115
+ @queue.push queue_item
116
+ end
117
+
118
+ def process_working_order_update(item_name, json)
119
+ return unless json
120
+
121
+ model = WorkingOrderUpdate.new ResponseParser.parse(JSON.parse(json))
122
+
123
+ queue_item = "#{item_name} - Order update - id: #{model.deal_id}"
124
+
125
+ [:status, :deal_status, :epic, :direction, :size, :level, :stop_distance, :limit_distance].each do |attribute|
126
+ value = model.send attribute
127
+ queue_item << ", #{attribute}: #{value}" if value
128
+ end
129
+
130
+ @queue.push queue_item
131
+ end
132
+
133
+ # Internal model used to parse streaming open position updates.
134
+ class OpenPositionUpdate < Model
135
+ attribute :channel
136
+ attribute :deal_id
137
+ attribute :deal_id_origin
138
+ attribute :deal_reference
139
+ attribute :deal_status, Symbol, allowed_values: [:accepted, :rejected]
140
+ attribute :direction, Symbol, allowed_values: [:buy, :sell]
141
+ attribute :epic, String, regex: Regex::EPIC
142
+ attribute :expiry, Date, nil_if: %w(- DFB), format: ['%d-%b-%y', '%b-%y']
143
+ attribute :guaranteed_stop, Boolean
144
+ attribute :level, Float
145
+ attribute :limit_level, Float
146
+ attribute :size, Float
147
+ attribute :status, Symbol, allowed_values: [:deleted, :open, :updated]
148
+ attribute :stop_level, Float
149
+ attribute :timestamp, Time, format: '%FT%T.%L'
150
+ end
151
+
152
+ # Internal model used to parse streaming working order updates.
153
+ class WorkingOrderUpdate < Model
154
+ attribute :channel
155
+ attribute :deal_id
156
+ attribute :deal_reference
157
+ attribute :deal_status, Symbol, allowed_values: [:accepted, :rejected]
158
+ attribute :direction, Symbol, allowed_values: [:buy, :sell]
159
+ attribute :epic, String, regex: Regex::EPIC
160
+ attribute :expiry, Date, nil_if: %w(- DFB), format: ['%d-%b-%y', '%b-%y']
161
+ attribute :guaranteed_stop, Boolean
162
+ attribute :level, Float
163
+ attribute :limit_distance, Fixnum
164
+ attribute :size, Float
165
+ attribute :status, Symbol, allowed_values: [:deleted, :open, :updated]
166
+ attribute :stop_distance, Fixnum
167
+ attribute :timestamp, Time, format: '%FT%T.%L'
51
168
  end
52
169
  end
53
170
  end
@@ -14,7 +14,7 @@ module IGMarkets
14
14
  attribute :deal_status, Symbol, allowed_values: [:accepted, :fund_account, :rejected]
15
15
  attribute :direction, Symbol, allowed_values: [:buy, :sell]
16
16
  attribute :epic
17
- attribute :expiry, Date, nil_if: %w(- DFB), format: '%d-%b-%y'
17
+ attribute :expiry, Date, nil_if: %w(- DFB), format: ['%d-%b-%y', '%b-%y']
18
18
  attribute :guaranteed_stop, Boolean
19
19
  attribute :level, Float
20
20
  attribute :limit_distance, Fixnum
@@ -119,6 +119,19 @@ module IGMarkets
119
119
  instantiate_models Application, session.put('operations/application/disable')
120
120
  end
121
121
 
122
+ # Creates a new Lightstreamer session instance from this dealing platform's {#session} that is ready to connect
123
+ # and start streaming account and market data.
124
+ #
125
+ # @return [Lightstreamer::Session, nil] The new Lightstreamer session instance, or `nil` if there is no active
126
+ # IG Markets session to connect through.
127
+ def lightstreamer_session
128
+ return nil unless session.alive?
129
+
130
+ Lightstreamer::Session.new server_url: client_account_summary.lightstreamer_endpoint,
131
+ username: client_account_summary.client_id,
132
+ password: "CST-#{session.client_security_token}|XST-#{session.x_security_token}"
133
+ end
134
+
122
135
  # This method is used to instantiate the various `Model` subclasses from data returned by the IG Markets API. It
123
136
  # recurses through arrays and sub-hashes present in `source`, instantiating the required models based on the types
124
137
  # of each attribute as defined on the models. All model instances returned by this method will have their
@@ -132,6 +145,8 @@ module IGMarkets
132
145
  # this method individually.
133
146
  #
134
147
  # @return [nil, `model_class`, Array<`model_class`>] The resulting instantiated model(s).
148
+ #
149
+ # @private
135
150
  def instantiate_models(model_class, source)
136
151
  return nil if source.nil?
137
152
 
@@ -166,12 +181,31 @@ module IGMarkets
166
181
  model.instance_variable_set :@dealing_platform, self
167
182
 
168
183
  attributes.each do |attribute, value|
169
- next unless model_class.valid_attribute? attribute
184
+ next unless model.class.valid_attribute? attribute
170
185
 
171
- type = model_class.attribute_type attribute
172
- value = instantiate_models(type, value) if type < Model
186
+ type = model.class.attribute_type attribute
187
+ value = instantiate_models type, value if type < Model
173
188
 
174
- model.send "#{attribute}=", value
189
+ set_model_attribute_value model, attribute, value
190
+ end
191
+ end
192
+ end
193
+
194
+ # Sets the specified attribute value on the passed model instance. If a future version of the IG Markets API adds
195
+ # new valid values for attributes that are of type `Symbol` then assigning them here would cause an `ArgumentError`
196
+ # exception due to them not being in the `allowed_values` list for the attribute. This means that an API addition
197
+ # by IG Markets could cause this library to crash, which is undesirable. Instead of crashing this method issues a
198
+ # warning about the unrecognized value, and a future version of this library can then be updated to properly support
199
+ # the new valid value(s) that were added in the API update.
200
+ def set_model_attribute_value(model, attribute, value)
201
+ value = model.class.send "sanitize_#{attribute}_value", value
202
+
203
+ if model.class.attribute_value_allowed? attribute, value
204
+ model.send "#{attribute}=", value
205
+ else
206
+ unless Array(@reported_unrecognized_values).include? [model.class, attribute, value]
207
+ warn "ig_markets: received unrecognized value for #{model.class}##{attribute}: #{value}"
208
+ (@reported_unrecognized_values ||= []) << [model.class, attribute, value]
175
209
  end
176
210
  end
177
211
  end
@@ -60,7 +60,7 @@ module IGMarkets
60
60
  attribute :country
61
61
  attribute :currencies, Currency
62
62
  attribute :epic, String, regex: Regex::EPIC
63
- attribute :expiry, Date, nil_if: %w(- DFB), format: '%d-%b-%y'
63
+ attribute :expiry, Date, nil_if: %w(- DFB), format: ['%d-%b-%y', '%b-%y']
64
64
  attribute :expiry_details, ExpiryDetails
65
65
  attribute :force_open_allowed, Boolean
66
66
  attribute :lot_size, Float
@@ -53,24 +53,26 @@ module IGMarkets
53
53
  end
54
54
 
55
55
  def typecaster_date(value, options, name)
56
- raise ArgumentError, "#{self}##{name}: invalid date format" unless options[:format].is_a? String
57
-
58
56
  if value.is_a? String
59
- begin
60
- Date.strptime value, options[:format]
61
- rescue ArgumentError
62
- raise ArgumentError, "#{self}##{name}: failed parsing date: #{value}"
63
- end
57
+ parse_formatted_date_value value, options, name
64
58
  else
65
59
  value
66
60
  end
67
61
  end
68
62
 
69
- def typecaster_time(value, options, name)
70
- format = options[:format]
63
+ def parse_formatted_date_value(value, options, name)
64
+ Array(options[:format]).each do |format|
65
+ begin
66
+ return Date.strptime value, format
67
+ rescue ArgumentError
68
+ next
69
+ end
70
+ end
71
71
 
72
- raise ArgumentError, "#{self}##{name}: invalid time format" unless [Array, String].include? format.class
72
+ raise ArgumentError, "#{self}##{name}: failed parsing date: #{value}"
73
+ end
73
74
 
75
+ def typecaster_time(value, options, name)
74
76
  if value.is_a?(String) || value.is_a?(Fixnum)
75
77
  parse_formatted_time_value value.to_s, options, name
76
78
  else
@@ -130,7 +130,19 @@ module IGMarkets
130
130
  Hash(defined_attributes).fetch(attribute_name).fetch :allowed_values
131
131
  end
132
132
 
133
- # Defines setter and getter methods for a new attribute on this class.
133
+ # Returns whether the passed value is allowed to set be set as the value of the specified attribute.
134
+ #
135
+ # @param [Symbol] attribute_name
136
+ # @param value The candidate attribute value.
137
+ #
138
+ # @return [Boolean] Whether the passed value is allowed for the attribute.
139
+ def attribute_value_allowed?(attribute_name, value)
140
+ allowed_values = Hash(defined_attributes).fetch(attribute_name, {})[:allowed_values]
141
+
142
+ value.nil? || allowed_values.nil? || allowed_values.include?(value)
143
+ end
144
+
145
+ # Defines setter and getter instance methods and a sanitizer class method for a new attribute on this class.
134
146
  #
135
147
  # @param [Symbol] name The name of the new attribute.
136
148
  # @param [Boolean, String, Date, Time, Fixnum, Float, Symbol, Model] type The attribute's type.
@@ -148,22 +160,24 @@ module IGMarkets
148
160
  # @return [$2]
149
161
  def attribute(name, type = String, options = {})
150
162
  define_attribute_reader name
151
- define_attribute_writer name, type, options
163
+ define_attribute_writer name
164
+ define_attribute_sanitizer name, type, options
152
165
 
153
- self.defined_attributes ||= {}
154
- self.defined_attributes[name] = options.merge type: type
166
+ @defined_attributes ||= {}
167
+ @defined_attributes[name] = options.merge type: type
155
168
  end
156
169
 
157
- # Defines a no-op setter method for each of the passed attribute names. This is used to silently allow deprecated
158
- # attributes to be set on the model but not have them be part of the model's structure.
170
+ # Defines no-op setter, getter and sanitize methods for each of the passed attribute names. This is used to
171
+ # silently allow deprecated attributes to be used on the model but not have them be part of the model's structure.
159
172
  #
160
173
  # @param [Array<Symbol>] names The names of the deprecated attributes.
161
174
  def deprecated_attribute(*names)
162
175
  names.each do |name|
163
- define_method "#{name}=" do |_value|
164
- end
176
+ define_method(name) {}
177
+ define_method("#{name}=") { |_value| }
178
+ define_singleton_method("sanitize_#{name}_value") { |value| value }
165
179
 
166
- (self.deprecated_attributes ||= []) << name
180
+ (@deprecated_attributes ||= []) << name
167
181
  end
168
182
  end
169
183
 
@@ -173,22 +187,27 @@ module IGMarkets
173
187
  define_method(name) { @attributes[name] }
174
188
  end
175
189
 
176
- def define_attribute_writer(name, type, options)
177
- typecaster = typecaster_for type
178
-
190
+ def define_attribute_writer(name)
179
191
  define_method "#{name}=" do |value|
180
- value = nil if Array(options.fetch(:nil_if, [])).include? value
192
+ value = self.class.send "sanitize_#{name}_value", value
181
193
 
182
- value = typecaster.call value, options, name
183
-
184
- allowed_values = options[:allowed_values]
185
- if !value.nil? && allowed_values
186
- raise ArgumentError, "#{self}##{name}: invalid value: #{value.inspect}" unless allowed_values.include? value
194
+ unless self.class.attribute_value_allowed? name, value
195
+ raise ArgumentError, "#{self}##{name}: invalid value: #{value.inspect}"
187
196
  end
188
197
 
189
198
  (@attributes ||= {})[name] = value
190
199
  end
191
200
  end
201
+
202
+ def define_attribute_sanitizer(name, type, options)
203
+ typecaster = typecaster_for type
204
+
205
+ define_singleton_method "sanitize_#{name}_value" do |value|
206
+ value = nil if Array(options[:nil_if]).include? value
207
+
208
+ typecaster.call value, options, name
209
+ end
210
+ end
192
211
  end
193
212
  end
194
213
  end
@@ -1,6 +1,6 @@
1
1
  module IGMarkets
2
- # This class contains methods for printing a REST request and its JSON response for inspection and debugging.
3
- # Request printing is enabled by setting {enabled} to `true`.
2
+ # This class contains methods for printing a REST request and its response for inspection and debugging. Request
3
+ # printing is enabled by setting {enabled} to `true`.
4
4
  #
5
5
  # @private
6
6
  class RequestPrinter
@@ -10,42 +10,58 @@ module IGMarkets
10
10
  # @return [Boolean]
11
11
  attr_accessor :enabled
12
12
 
13
- # Prints out an options hash that is ready to be passed to `RestClient::Request.execute`.
13
+ # Prints out a request options hash that is ready to be passed to `Excon`.
14
14
  #
15
- # @param [Hash] options The options hash.
16
- def print_options(options)
15
+ # @param [Hash] options The request options.
16
+ def print_request(options)
17
17
  return unless enabled
18
18
 
19
19
  puts "#{options[:method].to_s.upcase} #{options[:url]}"
20
20
 
21
- puts ' Headers:'
22
- options[:headers].each do |name, value|
23
- puts " #{name}: #{value}"
24
- end
25
-
21
+ print_request_headers options[:headers]
26
22
  print_request_body options[:body]
27
23
  end
28
24
 
29
- # Formats and prints a JSON response body.
25
+ # Formats and prints a response received form `Excon`.
30
26
  #
31
- # @param [String] body The response body.
32
- def print_response_body(body)
27
+ # @param [#headers, #body] response The response.
28
+ def print_response(response)
33
29
  return unless enabled
34
30
 
35
- print ' Response: '
31
+ puts ' Response:'
36
32
 
37
- puts JSON.pretty_generate(JSON.parse(body)).gsub "\n", "\n "
38
- rescue JSON::ParserError
39
- puts body
33
+ print_response_headers response.headers
34
+ print_response_body response.body
40
35
  end
41
36
 
42
37
  private
43
38
 
39
+ def print_request_headers(headers)
40
+ puts ' Headers:'
41
+ headers.each do |name, value|
42
+ puts " #{name}: #{value}"
43
+ end
44
+ end
45
+
44
46
  def print_request_body(body)
45
47
  return unless body
46
48
 
47
- print ' Body: '
48
- puts JSON.pretty_generate(JSON.parse(body)).gsub("\n", "\n ")
49
+ print " Body:\n "
50
+ puts JSON.pretty_generate(JSON.parse(body)).gsub("\n", "\n ")
51
+ end
52
+
53
+ def print_response_headers(headers)
54
+ puts ' Headers:'
55
+ headers.each do |name, value|
56
+ puts " #{name}: #{value}"
57
+ end
58
+ end
59
+
60
+ def print_response_body(body)
61
+ print " Body:\n "
62
+ puts JSON.pretty_generate(JSON.parse(body)).gsub "\n", "\n "
63
+ rescue JSON::ParserError
64
+ puts body
49
65
  end
50
66
  end
51
67
  end
@@ -8,9 +8,9 @@ module IGMarkets
8
8
  # Parses the specified value that was returned from a call to the IG Markets API.
9
9
  #
10
10
  # @param [Hash, Array, Object] response The response or part of a response that should be parsed. If this is of type
11
- # `Hash` then all hash keys will converted from camel case into snake case and their values each to parsed
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.
11
+ # `Hash` then all hash keys will converted from camel case into snake case and their values will each be
12
+ # parsed individually by a recursive call. If this is of type `Array` then each item will be parsed
13
+ # individually by a recursive call. All other types are passed through unchanged.
14
14
  #
15
15
  # @return [Hash, Array, Object] The parsed object, the type depends on the type of the `response` parameter.
16
16
  def parse(response)
@@ -150,11 +150,11 @@ module IGMarkets
150
150
  end
151
151
 
152
152
  def execute_request(options)
153
- RequestPrinter.print_options options
153
+ RequestPrinter.print_request options
154
154
 
155
155
  response = Excon.send options[:method], options[:url], headers: options[:headers], body: options[:body]
156
156
 
157
- RequestPrinter.print_response_body response.body
157
+ RequestPrinter.print_response response
158
158
 
159
159
  process_response response, options
160
160
  rescue Excon::Error => error
@@ -188,7 +188,7 @@ module IGMarkets
188
188
  sign_in
189
189
  true
190
190
  elsif error.is_a?(Errors::ExceededAPIKeyAllowanceError) || error.is_a?(Errors::ExceededAccountAllowanceError)
191
- sleep 5
191
+ sleep 10
192
192
  true
193
193
  end
194
194
  end
@@ -1,4 +1,4 @@
1
1
  module IGMarkets
2
2
  # The version of this gem.
3
- VERSION = '0.18'.freeze
3
+ VERSION = '0.19'.freeze
4
4
  end
data/lib/ig_markets.rb CHANGED
@@ -7,6 +7,7 @@ require 'yaml'
7
7
 
8
8
  require 'colorize'
9
9
  require 'excon'
10
+ require 'lightstreamer'
10
11
  require 'pry'
11
12
  require 'terminal-table'
12
13
  require 'thor'
@@ -66,6 +67,7 @@ require 'ig_markets/cli/commands/prices_command'
66
67
  require 'ig_markets/cli/commands/search_command'
67
68
  require 'ig_markets/cli/commands/self_test_command'
68
69
  require 'ig_markets/cli/commands/sentiment_command'
70
+ require 'ig_markets/cli/commands/stream_command'
69
71
  require 'ig_markets/cli/commands/transactions_command'
70
72
  require 'ig_markets/cli/tables/table'
71
73
  require 'ig_markets/cli/tables/accounts_table'
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.18'
4
+ version: '0.19'
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-08-02 00:00:00.000000000 Z
11
+ date: 2016-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.51'
41
+ - !ruby/object:Gem::Dependency
42
+ name: lightstreamer
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: '0.10'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: '0.10'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: pry
43
57
  requirement: !ruby/object:Gem::Requirement