ig_markets 0.5 → 0.6

Sign up to get free protection for your applications and to get access to all the features.
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