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