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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +15 -10
- data/lib/ig_markets/cli/commands/stream_command.rb +142 -25
- data/lib/ig_markets/deal_confirmation.rb +1 -1
- data/lib/ig_markets/dealing_platform.rb +38 -4
- data/lib/ig_markets/instrument.rb +1 -1
- data/lib/ig_markets/model/typecasters.rb +12 -10
- data/lib/ig_markets/model.rb +37 -18
- data/lib/ig_markets/request_printer.rb +35 -19
- data/lib/ig_markets/response_parser.rb +3 -3
- data/lib/ig_markets/session.rb +3 -3
- data/lib/ig_markets/version.rb +1 -1
- data/lib/ig_markets.rb +2 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6e6fa29281db18808e948435c5dd1dcb5da3b003
|
4
|
+
data.tar.gz: ef3c666877d55b0c85236e1982b6d6d55713ad90
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
120
|
+
# Search for EURUSD markets
|
119
121
|
ig_markets search EURUSD --type currencies
|
120
122
|
|
121
|
-
# Print details for the EURUSD
|
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
|
5
|
+
desc 'stream', 'Displays live streaming updates of account balances, markets and trading activity'
|
6
6
|
|
7
|
-
option :
|
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
|
-
@
|
13
|
+
@dealing_platform = dealing_platform
|
12
14
|
|
13
|
-
|
14
|
-
create_lightstreamer_subscription lightstreamer
|
15
|
+
prepare_stream
|
15
16
|
|
16
|
-
loop
|
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
|
27
|
-
|
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
|
-
|
30
|
-
|
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
|
-
|
74
|
+
subscription = @lightstreamer.build_subscription items: items, fields: fields, mode: :distinct
|
34
75
|
|
35
|
-
|
36
|
-
|
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
|
-
|
82
|
+
subscription
|
39
83
|
end
|
40
84
|
|
41
|
-
def
|
42
|
-
|
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
|
-
|
90
|
+
queue_item = "#{item_name} - Confirmation - id: #{model.deal_id}"
|
45
91
|
|
46
|
-
|
47
|
-
|
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
|
-
|
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
|
184
|
+
next unless model.class.valid_attribute? attribute
|
170
185
|
|
171
|
-
type =
|
172
|
-
value = instantiate_models
|
186
|
+
type = model.class.attribute_type attribute
|
187
|
+
value = instantiate_models type, value if type < Model
|
173
188
|
|
174
|
-
model
|
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
|
-
|
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
|
70
|
-
|
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}:
|
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
|
data/lib/ig_markets/model.rb
CHANGED
@@ -130,7 +130,19 @@ module IGMarkets
|
|
130
130
|
Hash(defined_attributes).fetch(attribute_name).fetch :allowed_values
|
131
131
|
end
|
132
132
|
|
133
|
-
#
|
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
|
163
|
+
define_attribute_writer name
|
164
|
+
define_attribute_sanitizer name, type, options
|
152
165
|
|
153
|
-
|
154
|
-
|
166
|
+
@defined_attributes ||= {}
|
167
|
+
@defined_attributes[name] = options.merge type: type
|
155
168
|
end
|
156
169
|
|
157
|
-
# Defines
|
158
|
-
# attributes to be
|
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
|
164
|
-
|
176
|
+
define_method(name) {}
|
177
|
+
define_method("#{name}=") { |_value| }
|
178
|
+
define_singleton_method("sanitize_#{name}_value") { |value| value }
|
165
179
|
|
166
|
-
(
|
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
|
177
|
-
typecaster = typecaster_for type
|
178
|
-
|
190
|
+
def define_attribute_writer(name)
|
179
191
|
define_method "#{name}=" do |value|
|
180
|
-
value =
|
192
|
+
value = self.class.send "sanitize_#{name}_value", value
|
181
193
|
|
182
|
-
|
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
|
3
|
-
#
|
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
|
13
|
+
# Prints out a request options hash that is ready to be passed to `Excon`.
|
14
14
|
#
|
15
|
-
# @param [Hash] options The options
|
16
|
-
def
|
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
|
-
|
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
|
25
|
+
# Formats and prints a response received form `Excon`.
|
30
26
|
#
|
31
|
-
# @param [
|
32
|
-
def
|
27
|
+
# @param [#headers, #body] response The response.
|
28
|
+
def print_response(response)
|
33
29
|
return unless enabled
|
34
30
|
|
35
|
-
|
31
|
+
puts ' Response:'
|
36
32
|
|
37
|
-
|
38
|
-
|
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
|
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
|
12
|
-
# individually by a recursive call. If this is of type `Array` then each item will be parsed
|
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)
|
data/lib/ig_markets/session.rb
CHANGED
@@ -150,11 +150,11 @@ module IGMarkets
|
|
150
150
|
end
|
151
151
|
|
152
152
|
def execute_request(options)
|
153
|
-
RequestPrinter.
|
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.
|
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
|
191
|
+
sleep 10
|
192
192
|
true
|
193
193
|
end
|
194
194
|
end
|
data/lib/ig_markets/version.rb
CHANGED
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.
|
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-
|
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
|