ig_markets 0.18 → 0.19

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