ig_markets 0.5 → 0.6

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -8
  3. data/README.md +40 -10
  4. data/lib/ig_markets.rb +3 -1
  5. data/lib/ig_markets/cli/commands/activities_command.rb +9 -7
  6. data/lib/ig_markets/cli/commands/orders_command.rb +8 -11
  7. data/lib/ig_markets/cli/commands/positions_command.rb +9 -7
  8. data/lib/ig_markets/cli/commands/prices_command.rb +66 -0
  9. data/lib/ig_markets/cli/commands/search_command.rb +19 -2
  10. data/lib/ig_markets/cli/commands/sprints_command.rb +2 -2
  11. data/lib/ig_markets/cli/config_file.rb +1 -1
  12. data/lib/ig_markets/cli/main.rb +25 -16
  13. data/lib/ig_markets/cli/tables/accounts_table.rb +10 -0
  14. data/lib/ig_markets/cli/tables/historical_price_result_snapshots_table.rb +25 -0
  15. data/lib/ig_markets/cli/tables/table.rb +2 -2
  16. data/lib/ig_markets/client_sentiment.rb +3 -1
  17. data/lib/ig_markets/dealing_platform.rb +49 -14
  18. data/lib/ig_markets/dealing_platform/account_methods.rb +13 -5
  19. data/lib/ig_markets/dealing_platform/client_sentiment_methods.rb +2 -4
  20. data/lib/ig_markets/dealing_platform/market_methods.rb +14 -6
  21. data/lib/ig_markets/dealing_platform/position_methods.rb +7 -7
  22. data/lib/ig_markets/dealing_platform/sprint_market_position_methods.rb +16 -2
  23. data/lib/ig_markets/dealing_platform/watchlist_methods.rb +4 -2
  24. data/lib/ig_markets/dealing_platform/working_order_methods.rb +2 -4
  25. data/lib/ig_markets/format.rb +10 -0
  26. data/lib/ig_markets/historical_price_result.rb +1 -1
  27. data/lib/ig_markets/instrument.rb +10 -10
  28. data/lib/ig_markets/market.rb +3 -3
  29. data/lib/ig_markets/model.rb +17 -25
  30. data/lib/ig_markets/model/typecasters.rb +20 -13
  31. data/lib/ig_markets/payload_formatter.rb +1 -1
  32. data/lib/ig_markets/position.rb +1 -1
  33. data/lib/ig_markets/session.rb +8 -8
  34. data/lib/ig_markets/sprint_market_position.rb +2 -2
  35. data/lib/ig_markets/transaction.rb +1 -1
  36. data/lib/ig_markets/version.rb +1 -1
  37. data/lib/ig_markets/watchlist.rb +6 -4
  38. data/lib/ig_markets/working_order.rb +2 -2
  39. metadata +4 -2
@@ -13,7 +13,9 @@ module IGMarkets
13
13
  #
14
14
  # @return [Array<Watchlist>]
15
15
  def all
16
- @dealing_platform.gather 'watchlists', :watchlists, Watchlist
16
+ result = @dealing_platform.session.get('watchlists').fetch :watchlists
17
+
18
+ @dealing_platform.instantiate_models Watchlist, result
17
19
  end
18
20
 
19
21
  # Returns the watchlist that has the specified ID, or `nil` if there is no watchlist with that ID.
@@ -31,7 +33,7 @@ module IGMarkets
31
33
  #
32
34
  # @return [Watchlist] The new watchlist.
33
35
  def create(name, *epics)
34
- result = @dealing_platform.session.post 'watchlists', { name: name, epics: epics.flatten }, API_V1
36
+ result = @dealing_platform.session.post 'watchlists', name: name, epics: epics.flatten
35
37
 
36
38
  self[result.fetch(:watchlist_id)]
37
39
  end
@@ -14,11 +14,9 @@ module IGMarkets
14
14
  # @return [Array<WorkingOrder>]
15
15
  def all
16
16
  @dealing_platform.session.get('workingorders', API_V2).fetch(:working_orders).map do |attributes|
17
- attributes = attributes.fetch(:working_order_data).merge(market: attributes.fetch(:market_data))
17
+ attributes = attributes.fetch(:working_order_data).merge market: attributes.fetch(:market_data)
18
18
 
19
- WorkingOrder.new(attributes).tap do |working_order|
20
- working_order.instance_variable_set :@dealing_platform, @dealing_platform
21
- end
19
+ @dealing_platform.instantiate_models WorkingOrder, attributes
22
20
  end
23
21
  end
24
22
 
@@ -45,5 +45,15 @@ module IGMarkets
45
45
 
46
46
  result + ':' + Kernel.format('%02i', value % 60)
47
47
  end
48
+
49
+ # Formats the passed symbol into a human-readable string, replacing underscores with spaces and capitalizing the
50
+ # first letter.
51
+ #
52
+ # @param [Symbol] value The symbol to format.
53
+ #
54
+ # @return [String]
55
+ def symbol(value)
56
+ value.to_s.capitalize.tr '_', ' '
57
+ end
48
58
  end
49
59
  end
@@ -23,7 +23,7 @@ module IGMarkets
23
23
  attribute :last_traded_volume, Float
24
24
  attribute :low_price, Price
25
25
  attribute :open_price, Price
26
- attribute :snapshot_time, Time, format: '%Y/%m/%d %T', time_zone: '+1000'
26
+ attribute :snapshot_time, Time, format: '%Y/%m/%d %T', time_zone: -> { @dealing_platform.account_time_zone }
27
27
  end
28
28
 
29
29
  attribute :allowance, DataAllowance
@@ -12,7 +12,7 @@ module IGMarkets
12
12
 
13
13
  # Contains details on the expiry details of an instrument. Returned by {#expiry_details}.
14
14
  class ExpiryDetails < Model
15
- attribute :last_dealing_date, Time, format: '%FT%R', time_zone: '+1000'
15
+ attribute :last_dealing_date, Time, format: '%FT%R', time_zone: -> { @dealing_platform.account_time_zone }
16
16
  attribute :settlement_info
17
17
  end
18
18
 
@@ -30,15 +30,15 @@ module IGMarkets
30
30
  attribute :close_time
31
31
  attribute :open_time
32
32
 
33
- # (See {Model.from}).
34
- def self.from(value)
35
- # This check works around a vagary in the IG API where there is a seemingly unnecessary hash for the
36
- # :opening_hours value that only has a single :market_times key which is what holds the actual data.
37
- if value.is_a?(Hash) && value.keys == [:market_times]
38
- super value[:market_times]
39
- else
40
- super
41
- end
33
+ # This method is used by {DealingPlatform#instantiate_models} to work around a vagary in the IG API where there
34
+ # is a seemingly unnecessary hash for the :opening_hours value that contains a single :market_times key which is
35
+ # what holds the actual opening hours data.
36
+ #
37
+ # @param [Hash] attributes
38
+ #
39
+ # @return [Hash]
40
+ def self.adjusted_api_attributes(attributes)
41
+ attributes.keys == [:market_times] ? attributes[:market_times] : attributes
42
42
  end
43
43
  end
44
44
 
@@ -55,7 +55,7 @@ module IGMarkets
55
55
 
56
56
  url = "prices/#{instrument.epic}/#{resolution.to_s.upcase}/#{num_points.to_i}"
57
57
 
58
- HistoricalPriceResult.from @dealing_platform.session.get(url, API_V2)
58
+ @dealing_platform.instantiate_models HistoricalPriceResult, @dealing_platform.session.get(url, API_V2)
59
59
  end
60
60
 
61
61
  # Returns historical prices for this market at a specified resolution over a specified time period.
@@ -74,7 +74,7 @@ module IGMarkets
74
74
 
75
75
  url = "prices/#{instrument.epic}/#{resolution.to_s.upcase}/#{start_time}/#{end_time}"
76
76
 
77
- HistoricalPriceResult.from @dealing_platform.session.get(url, API_V2)
77
+ @dealing_platform.instantiate_models HistoricalPriceResult, @dealing_platform.session.get(url, API_V2)
78
78
  end
79
79
 
80
80
  private
@@ -93,7 +93,7 @@ module IGMarkets
93
93
  #
94
94
  # @param [Time] time The `Time` to format.
95
95
  def format_time(time)
96
- time.utc.strftime '%FT%T'
96
+ time.utc.strftime '%F %T'
97
97
  end
98
98
  end
99
99
  end
@@ -20,7 +20,7 @@ module IGMarkets
20
20
  (attributes.keys - defined_attribute_names).map do |attribute|
21
21
  value = attributes[attribute]
22
22
 
23
- raise ArgumentError, "Unknown attribute: #{self.class.name}##{attribute}, value: #{inspect_value value}"
23
+ raise ArgumentError, "unknown attribute: #{self.class.name}##{attribute}, value: #{inspect_value value}"
24
24
  end
25
25
  end
26
26
 
@@ -55,7 +55,7 @@ module IGMarkets
55
55
 
56
56
  private
57
57
 
58
- # Returns the #inspect string for the given value.
58
+ # Returns the {#inspect} string for the given value.
59
59
  def inspect_value(value)
60
60
  if value.is_a? Time
61
61
  value.utc.strftime '%F %T %Z'
@@ -77,19 +77,28 @@ module IGMarkets
77
77
  (defined_attributes || {}).keys
78
78
  end
79
79
 
80
+ # Returns the type of the specified attribute.
81
+ #
82
+ # @param [Symbol] attribute_name The name of the attribute to return the type for.
83
+ #
84
+ # @return The type of the specified attribute.
85
+ def attribute_type(attribute_name)
86
+ defined_attributes.fetch(attribute_name).fetch :type
87
+ end
88
+
80
89
  # Returns the array of allowed values for the specified attribute that was passed to {attribute}.
81
90
  #
82
91
  # @param [Symbol] attribute_name The name of the attribute to return the allowed values for.
83
92
  #
84
93
  # @return [Array]
85
94
  def allowed_values(attribute_name)
86
- defined_attributes.fetch(attribute_name).fetch(:allowed_values)
95
+ defined_attributes.fetch(attribute_name).fetch :allowed_values
87
96
  end
88
97
 
89
98
  # Defines setter and getter methods for a new attribute on this class.
90
99
  #
91
100
  # @param [Symbol] name The name of the new attribute.
92
- # @param [Boolean, String, Date, Time, Fixnum, Float, Symbol, #from] type The attribute's type.
101
+ # @param [Boolean, String, Date, Time, Fixnum, Float, Symbol, Model] type The attribute's type.
93
102
  # @param [Hash] options The configuration options for the new attribute.
94
103
  # @option options [Array] :allowed_values The set of values that this attribute is allowed to be set to. An
95
104
  # attempt to set this attribute to a value not in this list will raise `ArgumentError`. Optional.
@@ -98,9 +107,10 @@ module IGMarkets
98
107
  # Optional.
99
108
  # @option options [String] :format When `type` is `Date` or `Time` this specifies the format for parsing String
100
109
  # and `Fixnum` instances assigned to this attribute.
101
- # @option options [String] :time_zone When `type` is `Time` this specifies the time zone to append to
110
+ # @option options [String, Proc] :time_zone When `type` is `Time` this specifies the time zone to append to
102
111
  # `String` values assigned to this attribute prior to parsing them with `:format`. Defaults to
103
- # `+0000` (UTC) unless `:format` is `%Q`.
112
+ # `+0000` (UTC) unless `:format` is `%Q`. Can be a `Proc` that returns the time zone string to
113
+ # use.
104
114
  #
105
115
  # @macro [attach] attribute
106
116
  # The $1 attribute.
@@ -113,24 +123,6 @@ module IGMarkets
113
123
  self.defined_attributes[name] = options.merge type: type
114
124
  end
115
125
 
116
- # Creates a new Model instance from the specified source, which can take a variety of different forms.
117
- #
118
- # @param [nil, Hash, Model, Array] source The source object to create a new `Model` instance from. If `source` is
119
- # `nil` then `nil` is returned. If `source` is a hash then a new `Model` instance is returned and the
120
- # hash is passed to `Model#initialize`. If `source` is an instance of this class then `dup` is called on it
121
- # and the duplicate returned. If source is an array then it is mapped into a new array with each item
122
- # having been recursively passed through this method.
123
- #
124
- # @return [nil, Array, Model]
125
- def from(source)
126
- return nil if source.nil?
127
- return new source if source.is_a? Hash
128
- return source.dup if source.is_a? self
129
- return source.map { |item| from item } if source.is_a? Array
130
-
131
- raise ArgumentError, "Unable to make a #{self} from instance of #{source.class}"
132
- end
133
-
134
126
  private
135
127
 
136
128
  def define_attribute_reader(name)
@@ -143,7 +135,7 @@ module IGMarkets
143
135
  define_method "#{name}=" do |value|
144
136
  value = nil if Array(options.fetch(:nil_if, [])).include? value
145
137
 
146
- value = typecaster.call value, options
138
+ value = typecaster.call value, options, self, name
147
139
 
148
140
  allowed_values = options[:allowed_values]
149
141
  if !value.nil? && allowed_values
@@ -7,18 +7,24 @@ module IGMarkets
7
7
  def typecaster_for(type)
8
8
  if [Boolean, String, Fixnum, Float, Symbol, Date, Time].include? type
9
9
  method "typecaster_#{type.to_s.gsub(/\AIGMarkets::/, '').downcase}"
10
- elsif type.respond_to? :from
11
- -> (value, _options) { type.from value }
10
+ elsif type
11
+ lambda do |value, _options, model, name|
12
+ if Array(value).any? { |entry| !entry.is_a? type }
13
+ raise ArgumentError, "incorrect type set on #{model.class}##{name}: #{value.inspect}"
14
+ end
15
+
16
+ value
17
+ end
12
18
  end
13
19
  end
14
20
 
15
- def typecaster_boolean(value, _options)
21
+ def typecaster_boolean(value, _options, _model, _name)
16
22
  return value if [nil, true, false].include? value
17
23
 
18
24
  raise ArgumentError, "#{self}: invalid boolean value: #{value}"
19
25
  end
20
26
 
21
- def typecaster_string(value, options)
27
+ def typecaster_string(value, options, _model, _name)
22
28
  return nil if value.nil?
23
29
 
24
30
  if options.key? :regex
@@ -28,25 +34,25 @@ module IGMarkets
28
34
  value.to_s
29
35
  end
30
36
 
31
- def typecaster_fixnum(value, _options)
37
+ def typecaster_fixnum(value, _options, _model, _name)
32
38
  return nil if value.nil?
33
39
 
34
40
  value.to_s.to_i
35
41
  end
36
42
 
37
- def typecaster_float(value, _options)
43
+ def typecaster_float(value, _options, _model, _name)
38
44
  return nil if value.nil? || value == ''
39
45
 
40
46
  Float(value)
41
47
  end
42
48
 
43
- def typecaster_symbol(value, _options)
49
+ def typecaster_symbol(value, _options, _model, _name)
44
50
  return nil if value.nil?
45
51
 
46
52
  value.to_s.downcase.to_sym
47
53
  end
48
54
 
49
- def typecaster_date(value, options)
55
+ def typecaster_date(value, options, _model, _name)
50
56
  raise ArgumentError, "#{self}: invalid or missing date format" unless options[:format].is_a? String
51
57
 
52
58
  if value.is_a? String
@@ -60,26 +66,27 @@ module IGMarkets
60
66
  end
61
67
  end
62
68
 
63
- def typecaster_time(value, options)
69
+ def typecaster_time(value, options, model, name)
64
70
  raise ArgumentError, "#{self}: invalid or missing time format" unless options[:format].is_a? String
65
71
 
66
72
  if value.is_a?(String) || value.is_a?(Fixnum)
67
- parse_time_from_string value.to_s, options
73
+ parse_time_from_string value.to_s, options, model, name
68
74
  else
69
75
  value
70
76
  end
71
77
  end
72
78
 
73
- def parse_time_from_string(value, options)
79
+ def parse_time_from_string(value, options, model, name)
74
80
  format = options[:format]
75
- time_zone = options[:time_zone]
76
81
 
82
+ time_zone = options[:time_zone]
77
83
  time_zone ||= '+0000' unless format == '%Q'
84
+ time_zone = model.instance_exec(&time_zone) if time_zone.is_a? Proc
78
85
 
79
86
  begin
80
87
  Time.strptime "#{value}#{time_zone}", "#{format}#{'%z' if time_zone}"
81
88
  rescue ArgumentError
82
- raise ArgumentError, "#{self}: failed parsing time '#{value}' with format '#{format}'"
89
+ raise ArgumentError, "#{self}##{name}: failed parsing time '#{value}' with format '#{format}'"
83
90
  end
84
91
  end
85
92
  end
@@ -45,7 +45,7 @@ module IGMarkets
45
45
  def snake_case_to_camel_case(value)
46
46
  pieces = value.to_s.split '_'
47
47
 
48
- (pieces[0] + pieces[1..-1].map(&:capitalize).join).to_sym
48
+ (pieces.first + pieces[1..-1].map(&:capitalize).join).to_sym
49
49
  end
50
50
  end
51
51
  end
@@ -89,7 +89,7 @@ module IGMarkets
89
89
 
90
90
  payload = PayloadFormatter.format model
91
91
 
92
- @dealing_platform.session.delete('positions/otc', payload, API_V1).fetch(:deal_reference)
92
+ @dealing_platform.session.delete('positions/otc', payload).fetch :deal_reference
93
93
  end
94
94
 
95
95
  # Updates this position. No attributes are mandatory, and any attributes not specified will be kept at their
@@ -15,10 +15,10 @@ module IGMarkets
15
15
  # @return [:demo, :production] The platform variant to log into for this session.
16
16
  attr_accessor :platform
17
17
 
18
- # @return [String] The CST for the currently logged in session, or nil if there is no active session.
18
+ # @return [String] The CST for the currently logged in session, or `nil` if there is no active session.
19
19
  attr_reader :cst
20
20
 
21
- # @return [String] The security token for the currently logged in session, or nil if there is no active session.
21
+ # @return [String] The security token for the currently logged in session, or `nil` if there is no active session.
22
22
  attr_reader :x_security_token
23
23
 
24
24
  # Signs in to IG Markets using the values of {#username}, {#password}, {#api_key} and {#platform}. If an error
@@ -40,7 +40,7 @@ module IGMarkets
40
40
  # Signs out of IG Markets, ending the current session (if any). If an error occurs then {RequestFailedError} will be
41
41
  # raised.
42
42
  def sign_out
43
- delete 'session', nil, API_V1 if alive?
43
+ delete 'session' if alive?
44
44
 
45
45
  @cst = @x_security_token = nil
46
46
  end
@@ -59,7 +59,7 @@ module IGMarkets
59
59
  # @param [Fixnum] api_version The API version to target.
60
60
  #
61
61
  # @return [Hash] The response from the IG Markets API.
62
- def post(url, payload, api_version)
62
+ def post(url, payload, api_version = API_V1)
63
63
  request(method: :post, url: url, payload: payload, api_version: api_version).fetch :result
64
64
  end
65
65
 
@@ -69,7 +69,7 @@ module IGMarkets
69
69
  # @param [Fixnum] api_version The API version to target.
70
70
  #
71
71
  # @return [Hash] The response from the IG Markets API.
72
- def get(url, api_version)
72
+ def get(url, api_version = API_V1)
73
73
  request(method: :get, url: url, api_version: api_version).fetch :result
74
74
  end
75
75
 
@@ -80,7 +80,7 @@ module IGMarkets
80
80
  # @param [Fixnum] api_version The API version to target.
81
81
  #
82
82
  # @return [Hash] The response from the IG Markets API.
83
- def put(url, payload, api_version)
83
+ def put(url, payload, api_version = API_V1)
84
84
  request(method: :put, url: url, payload: payload, api_version: api_version).fetch :result
85
85
  end
86
86
 
@@ -91,7 +91,7 @@ module IGMarkets
91
91
  # @param [Fixnum] api_version The API version to target.
92
92
  #
93
93
  # @return [Hash] The response from the IG Markets API.
94
- def delete(url, payload, api_version)
94
+ def delete(url, payload = nil, api_version = API_V1)
95
95
  request(method: :delete, url: url, payload: payload, api_version: api_version).fetch :result
96
96
  end
97
97
 
@@ -115,7 +115,7 @@ module IGMarkets
115
115
  end
116
116
 
117
117
  def password_encryptor
118
- result = get 'session/encryptionKey', API_V1
118
+ result = get 'session/encryptionKey'
119
119
 
120
120
  PasswordEncryptor.new result.fetch(:encryption_key), result.fetch(:time_stamp)
121
121
  end
@@ -1,13 +1,13 @@
1
1
  module IGMarkets
2
2
  # Contains details on a sprint market position. Returned by {DealingPlatform::SprintMarketPositionMethods#all}.
3
3
  class SprintMarketPosition < Model
4
- attribute :created_date, Time, format: '%Y/%m/%d %T:%L', time_zone: '+1000'
4
+ attribute :created_date, Time, format: '%Y/%m/%d %T:%L', time_zone: -> { @dealing_platform.account_time_zone }
5
5
  attribute :currency, String, regex: Regex::CURRENCY
6
6
  attribute :deal_id
7
7
  attribute :description
8
8
  attribute :direction, Symbol, allowed_values: [:buy, :sell]
9
9
  attribute :epic, String, regex: Regex::EPIC
10
- attribute :expiry_time, Time, format: '%Y/%m/%d %T:%L', time_zone: '+1000'
10
+ attribute :expiry_time, Time, format: '%Y/%m/%d %T:%L', time_zone: -> { @dealing_platform.account_time_zone }
11
11
  attribute :instrument_name
12
12
  attribute :market_status, Symbol, allowed_values: Market::Snapshot.allowed_values(:market_status)
13
13
  attribute :payout_amount, Float
@@ -9,7 +9,7 @@ module IGMarkets
9
9
  attribute :date, Date, format: '%d/%m/%y'
10
10
  attribute :instrument_name
11
11
  attribute :open_level, String, nil_if: %w(- 0)
12
- attribute :period, Time, nil_if: '-', format: '%d/%m/%y %T', time_zone: '+10:00'
12
+ attribute :period, Time, nil_if: '-', format: '%d/%m/%y %T', time_zone: -> { @dealing_platform.account_time_zone }
13
13
  attribute :profit_and_loss
14
14
  attribute :reference
15
15
  attribute :size, String, nil_if: '-'
@@ -1,4 +1,4 @@
1
1
  module IGMarkets
2
2
  # The version of this gem.
3
- VERSION = '0.5'.freeze
3
+ VERSION = '0.6'.freeze
4
4
  end
@@ -12,26 +12,28 @@ module IGMarkets
12
12
  #
13
13
  # @return [Array<Market>]
14
14
  def markets
15
- @dealing_platform.gather "watchlists/#{id}", :markets, MarketOverview
15
+ result = @dealing_platform.session.get("watchlists/#{id}").fetch :markets
16
+
17
+ @dealing_platform.instantiate_models MarketOverview, result
16
18
  end
17
19
 
18
20
  # Deletes this watchlist.
19
21
  def delete
20
- @dealing_platform.session.delete "watchlists/#{id}", nil, API_V1
22
+ @dealing_platform.session.delete "watchlists/#{id}"
21
23
  end
22
24
 
23
25
  # Adds a market to this watchlist.
24
26
  #
25
27
  # @param [String] epic The EPIC of the market to add to this watchlist.
26
28
  def add_market(epic)
27
- @dealing_platform.session.put "watchlists/#{id}", { epic: epic }, API_V1
29
+ @dealing_platform.session.put "watchlists/#{id}", epic: epic
28
30
  end
29
31
 
30
32
  # Removes a market from this watchlist.
31
33
  #
32
34
  # @param [String] epic The EPIC of the market to remove from this watchlist.
33
35
  def remove_market(epic)
34
- @dealing_platform.session.delete "watchlists/#{id}/#{epic}", nil, API_V1
36
+ @dealing_platform.session.delete "watchlists/#{id}/#{epic}"
35
37
  end
36
38
  end
37
39
  end