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
@@ -17,7 +17,7 @@ module IGMarkets
17
17
  .split(' ')
18
18
  end
19
19
 
20
- # Returns the config file to use, or nil if there is no config file.
20
+ # Returns the config file to use, or `nil` if there is no config file.
21
21
  #
22
22
  # @return [CLI::ConfigFile]
23
23
  def self.find
@@ -7,6 +7,7 @@ module IGMarkets
7
7
  class_option :password, required: true, desc: 'The password for the session'
8
8
  class_option :api_key, required: true, desc: 'The API key for the session'
9
9
  class_option :demo, type: :boolean, desc: 'Use the demo platform (default is production)'
10
+ class_option :account_time_zone, default: '+0000', desc: 'The time zone of the account'
10
11
  class_option :print_requests, type: :boolean, desc: 'Whether to print the raw REST API requests and responses'
11
12
 
12
13
  desc 'orders [SUBCOMAND=list ...]', 'Command for working with orders'
@@ -33,23 +34,31 @@ module IGMarkets
33
34
 
34
35
  RequestPrinter.enabled = true if options[:print_requests]
35
36
 
37
+ dealing_platform.account_time_zone = options[:account_time_zone]
38
+
36
39
  dealing_platform.sign_in options[:username], options[:password], options[:api_key], platform
37
40
 
38
41
  yield dealing_platform
39
42
  rescue IGMarkets::RequestFailedError => error
40
- warn "Request error: #{error.error}"
41
- exit 1
43
+ error "Request error: #{error.error}"
42
44
  rescue ArgumentError => error
43
- warn "Argument error: #{error}"
45
+ error "Argument error: #{error}"
46
+ end
47
+
48
+ # Writes the passed message to `stderr` and then exits the application.
49
+ #
50
+ # @param [String] message The error message.
51
+ def error(message)
52
+ warn message
44
53
  exit 1
45
54
  end
46
55
 
47
- # The dealing platform instance used by {begin_session}.
56
+ # The {DealingPlatform} instance used by {begin_session}.
48
57
  def dealing_platform
49
58
  @dealing_platform ||= DealingPlatform.new
50
59
  end
51
60
 
52
- # Takes a deal reference and prints out its full deal confirmation.
61
+ # Requests and displays the deal confirmation for the passed deal reference.
53
62
  #
54
63
  # @param [String] deal_reference The deal reference.
55
64
  #
@@ -59,24 +68,24 @@ module IGMarkets
59
68
 
60
69
  deal_confirmation = dealing_platform.deal_confirmation deal_reference
61
70
 
62
- print "Deal confirmation: #{deal_confirmation.deal_id}, #{deal_confirmation.deal_status}, "
63
-
64
- unless deal_confirmation.deal_status == :accepted
65
- print "reason: #{deal_confirmation.reason}, "
66
- end
71
+ puts <<-END
72
+ Deal ID: #{deal_confirmation.deal_id}
73
+ Status: #{Format.symbol deal_confirmation.deal_status}
74
+ Result: #{Format.symbol deal_confirmation.status}
75
+ END
67
76
 
68
- puts "epic: #{deal_confirmation.epic}"
77
+ puts "Reason: #{Format.symbol deal_confirmation.reason}" unless deal_confirmation.deal_status == :accepted
69
78
  end
70
79
 
71
- # Parses and validates a Date or Time option received as a command-line argument. Raises `ArgumentError` if it
72
- # is been specified in an invalid format.
80
+ # Parses and validates a `Date` or `Time` option received as a command-line argument. Raises `ArgumentError` if
81
+ # it is been specified in an invalid format.
73
82
  #
74
83
  # @param [Hash] attributes The attributes hash.
75
84
  # @param [Symbol] attribute The name of the date or time attribute to parse and validate.
76
85
  # @param [Date, Time] klass The class to validate with.
77
86
  # @param [String] format The `strptime` format string for the attribute.
78
- # @param [String] display_format The human-readable version of `format` to put into an exception if there is
79
- # a problem parsing the attribute.
87
+ # @param [String] display_format The human-readable version of `format` to put into the raised exception if
88
+ # there is a problem parsing the attribute.
80
89
  #
81
90
  # @return [void]
82
91
  def parse_date_time(attributes, attribute, klass, format, display_format)
@@ -95,7 +104,7 @@ module IGMarkets
95
104
 
96
105
  # Takes a Thor options hash and filters out its keys in the specified whitelist. Thor has an unusual behavior
97
106
  # when an option is specified without a value: its value is set to the option's name. This method resets any
98
- # such occurrences to nil.
107
+ # such occurrences to `nil`.
99
108
  #
100
109
  # @param [Thor::CoreExt::HashWithIndifferentAccess] options The Thor options.
101
110
  # @param [Array<Symbol>] whitelist The list of options allowed in the returned `Hash`.
@@ -25,6 +25,16 @@ module IGMarkets
25
25
  Format.currency account.balance.send(attribute), account.currency
26
26
  end
27
27
  end
28
+
29
+ def cell_color(value, _model, _row_index, column_index)
30
+ return unless headings[column_index] == 'Profit/loss'
31
+
32
+ if value =~ /-/
33
+ :red
34
+ else
35
+ :green
36
+ end
37
+ end
28
38
  end
29
39
  end
30
40
  end
@@ -0,0 +1,25 @@
1
+ module IGMarkets
2
+ module CLI
3
+ # Helper class that prints out an array of {IGMarkets::HistoricalPriceResult::Snapshot} instances in a table.
4
+ class HistoricalPriceResultSnapshotsTable < Table
5
+ private
6
+
7
+ def headings
8
+ %w(Date Open Close Low High)
9
+ end
10
+
11
+ def right_aligned_columns
12
+ [1, 2, 3, 4]
13
+ end
14
+
15
+ def row(snapshot)
16
+ [snapshot.snapshot_time, format_price(snapshot.open_price), format_price(snapshot.close_price),
17
+ format_price(snapshot.low_price), format_price(snapshot.high_price)]
18
+ end
19
+
20
+ def format_price(price)
21
+ (price.ask + price.bid) / 2.0
22
+ end
23
+ end
24
+ end
25
+ end
@@ -80,7 +80,7 @@ module IGMarkets
80
80
  end
81
81
 
82
82
  def format_time(value)
83
- value.utc.strftime '%F %T %Z'
83
+ value.localtime.strftime '%F %T %Z'
84
84
  end
85
85
 
86
86
  def format_date(value)
@@ -89,7 +89,7 @@ module IGMarkets
89
89
 
90
90
  def format_string(value)
91
91
  value = if value.is_a? Symbol
92
- value.to_s.tr '_', ' '
92
+ Format.symbol value
93
93
  else
94
94
  value.to_s
95
95
  end
@@ -10,7 +10,9 @@ module IGMarkets
10
10
  #
11
11
  # @return [Array<ClientSentiment>]
12
12
  def related_sentiments
13
- @dealing_platform.gather "clientsentiment/related/#{market_id}", :client_sentiments, ClientSentiment
13
+ result = @dealing_platform.session.get("clientsentiment/related/#{market_id}").fetch :client_sentiments
14
+
15
+ @dealing_platform.instantiate_models ClientSentiment, result
14
16
  end
15
17
  end
16
18
  end
@@ -38,6 +38,11 @@ module IGMarkets
38
38
  # @return [WorkingOrderMethods] Methods for working with working orders.
39
39
  attr_reader :working_orders
40
40
 
41
+ # @return [String] The time zone of the account, e.g. `'+1000'` or `'-0800'`. This is required in order for certain
42
+ # dates and times reported by this library to be correct, due to the fact that the IG Markets API does not reliably
43
+ # report time zone details in all attributes. Defaults to `'+0000'`.
44
+ attr_accessor :account_time_zone
45
+
41
46
  def initialize
42
47
  @session = Session.new
43
48
 
@@ -48,6 +53,8 @@ module IGMarkets
48
53
  @sprint_market_positions = SprintMarketPositionMethods.new self
49
54
  @watchlists = WatchlistMethods.new self
50
55
  @working_orders = WorkingOrderMethods.new self
56
+
57
+ @account_time_zone = '+0000'
51
58
  end
52
59
 
53
60
  # Signs in to the IG Markets Dealing Platform, either the production platform or the demo platform.
@@ -74,30 +81,58 @@ module IGMarkets
74
81
  #
75
82
  # @return [DealConfirmation]
76
83
  def deal_confirmation(deal_reference)
77
- DealConfirmation.from session.get "confirms/#{deal_reference}", API_V1
84
+ instantiate_models DealConfirmation, session.get("confirms/#{deal_reference}")
78
85
  end
79
86
 
80
87
  # Returns details on the IG Markets applications for the accounts associated with this login.
81
88
  #
82
89
  # @return [Array<Application>]
83
90
  def applications
84
- Application.from session.get 'operations/application', API_V1
91
+ instantiate_models Application, session.get('operations/application')
85
92
  end
86
93
 
87
- # Sends a GET request to a URL then takes a single key from the returned hash and converts its contents to an array
88
- # of type `klass`.
94
+ # This method is used to instantiate the various `Model` subclasses from data returned by the IG Markets API. It
95
+ # recurses through arrays and sub-hashes present in `source`, instantiating the required models based on the types
96
+ # of each attribute as defined on the models. All model instances returned by this method will have their
97
+ # `@dealing_platform` instance variable set.
89
98
  #
90
- # @param [String] url The URL to send a GET request to.
91
- # @param [Symbol] collection The name of the top level symbol that contains the array of data to return.
92
- # @param [Class] klass The type to return.
93
- # @param [API_V1, API_V2, API_V3] api_version The API version to target for the request.
99
+ # @param [Class] model_class The type of model to create from `source`.
100
+ # @param [nil, Hash, Array, Model] source The source object to construct the model(s) from. If `nil` then `nil` is
101
+ # returned. If an instance of `model_class` subclass then a deep copy of it is
102
+ # returned. If a `Hash` then it will be interprted as the attributes for a new
103
+ # instance of `model_class. If an `Array` then each entry will be passed through
104
+ # this method individually.
94
105
  #
95
- # @return [Array]
96
- def gather(url, collection, klass, api_version = API_V1)
97
- klass.from(session.get(url, api_version).fetch(collection)).tap do |result|
98
- # Set @dealing_platform on all the results
99
- result.each do |item|
100
- item.instance_variable_set :@dealing_platform, self
106
+ # @return [nil, `model_class`, Array<`model_class`>] The resulting instantiated model(s).
107
+ def instantiate_models(model_class, source)
108
+ return nil if source.nil?
109
+
110
+ source = source.attributes if source.is_a? model_class
111
+
112
+ if source.is_a? Array
113
+ source.map { |entry| instantiate_models model_class, entry }
114
+ elsif source.is_a? Hash
115
+ source = model_class.adjusted_api_attributes source if model_class.respond_to? :adjusted_api_attributes
116
+
117
+ instantiate_model_from_attributes_hash model_class, source
118
+ else
119
+ raise ArgumentError, "#{model_class}: can't instantiate from a source of type #{source.class}"
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ # This method is a companion to {#instantiate_models} and creates a single instance of `model_class` from the passed
126
+ # attributes hash, setting the `@dealing_platform` instance variable on the new model instance.
127
+ def instantiate_model_from_attributes_hash(model_class, attributes)
128
+ model_class.new.tap do |model|
129
+ model.instance_variable_set :@dealing_platform, self
130
+
131
+ attributes.each do |attribute, value|
132
+ type = model_class.attribute_type attribute
133
+ value = instantiate_models(type, value) if type < Model
134
+
135
+ model.send "#{attribute}=", value
101
136
  end
102
137
  end
103
138
  end
@@ -13,7 +13,9 @@ module IGMarkets
13
13
  #
14
14
  # @return [Array<Account>]
15
15
  def all
16
- @dealing_platform.gather 'accounts', :accounts, Account
16
+ result = @dealing_platform.session.get('accounts').fetch :accounts
17
+
18
+ @dealing_platform.instantiate_models Account, result
17
19
  end
18
20
 
19
21
  # Returns all account activities that occurred in the specified date range.
@@ -26,7 +28,9 @@ module IGMarkets
26
28
  from_date = format_date from_date
27
29
  to_date = format_date to_date
28
30
 
29
- @dealing_platform.gather "history/activity/#{from_date}/#{to_date}", :activities, Activity
31
+ result = @dealing_platform.session.get("history/activity/#{from_date}/#{to_date}").fetch :activities
32
+
33
+ @dealing_platform.instantiate_models Activity, result
30
34
  end
31
35
 
32
36
  # Returns all account activities that occurred in the most recent specified number of days.
@@ -35,7 +39,9 @@ module IGMarkets
35
39
  #
36
40
  # @return [Array<Activity>]
37
41
  def recent_activities(days)
38
- @dealing_platform.gather "history/activity/#{milliseconds(days)}", :activities, Activity
42
+ result = @dealing_platform.session.get("history/activity/#{milliseconds(days)}").fetch :activities
43
+
44
+ @dealing_platform.instantiate_models Activity, result
39
45
  end
40
46
 
41
47
  # Returns all transactions that occurred in the specified date range.
@@ -52,8 +58,9 @@ module IGMarkets
52
58
  to_date = format_date to_date
53
59
 
54
60
  url = "history/transactions/#{transaction_type.to_s.upcase}/#{from_date}/#{to_date}"
61
+ result = @dealing_platform.session.get(url).fetch :transactions
55
62
 
56
- @dealing_platform.gather url, :transactions, Transaction
63
+ @dealing_platform.instantiate_models Transaction, result
57
64
  end
58
65
 
59
66
  # Returns all transactions that occurred in the last specified number of days.
@@ -66,8 +73,9 @@ module IGMarkets
66
73
  validate_transaction_type transaction_type
67
74
 
68
75
  url = "history/transactions/#{transaction_type.to_s.upcase}/#{milliseconds(days)}"
76
+ result = @dealing_platform.session.get(url).fetch :transactions
69
77
 
70
- @dealing_platform.gather url, :transactions, Transaction
78
+ @dealing_platform.instantiate_models Transaction, result
71
79
  end
72
80
 
73
81
  private
@@ -15,14 +15,12 @@ module IGMarkets
15
15
  #
16
16
  # @return [ClientSentiment]
17
17
  def [](market_id)
18
- result = @dealing_platform.session.get "clientsentiment/#{market_id}", API_V1
18
+ result = @dealing_platform.session.get "clientsentiment/#{market_id}"
19
19
 
20
- ClientSentiment.from(result).tap do |client_sentiment|
20
+ @dealing_platform.instantiate_models(ClientSentiment, result).tap do |client_sentiment|
21
21
  if client_sentiment.long_position_percentage == 0.0 && client_sentiment.short_position_percentage == 0.0
22
22
  raise ArgumentError, "unknown market '#{market_id}'"
23
23
  end
24
-
25
- client_sentiment.instance_variable_set :@dealing_platform, @dealing_platform
26
24
  end
27
25
  end
28
26
  end
@@ -18,7 +18,9 @@ module IGMarkets
18
18
  def hierarchy(node_id = nil)
19
19
  url = ['marketnavigation', node_id].compact.join '/'
20
20
 
21
- MarketHierarchyResult.from @dealing_platform.session.get(url, API_V1)
21
+ result = @dealing_platform.session.get url
22
+
23
+ @dealing_platform.instantiate_models MarketHierarchyResult, result
22
24
  end
23
25
 
24
26
  # Returns details for the markets with the passed EPICs.
@@ -27,13 +29,17 @@ module IGMarkets
27
29
  #
28
30
  # @return [Array<Market>]
29
31
  def find(*epics)
30
- raise ArgumentError, 'at least one EPIC must be specified' if epics.empty?
32
+ epics = epics.flatten
33
+
34
+ return [] if epics.empty?
31
35
 
32
- epics.flatten.each do |epic|
36
+ epics.each do |epic|
33
37
  raise ArgumentError, "invalid EPIC: #{epic}" unless epic.to_s =~ Regex::EPIC
34
38
  end
35
39
 
36
- @dealing_platform.gather "markets?epics=#{epics.join(',')}", :market_details, Market, API_V2
40
+ result = @dealing_platform.session.get("markets?epics=#{epics.join(',')}", API_V2).fetch :market_details
41
+
42
+ @dealing_platform.instantiate_models Market, result
37
43
  end
38
44
 
39
45
  # Searches markets using a search term and returns an array of results.
@@ -42,7 +48,9 @@ module IGMarkets
42
48
  #
43
49
  # @return [Array<MarketOverview>]
44
50
  def search(search_term)
45
- @dealing_platform.gather "markets?searchTerm=#{search_term}", :markets, MarketOverview
51
+ result = @dealing_platform.session.get("markets?searchTerm=#{search_term}").fetch :markets
52
+
53
+ @dealing_platform.instantiate_models MarketOverview, result
46
54
  end
47
55
 
48
56
  # Returns market details for the market with the specified EPIC, or `nil` if there is no market with that EPIC.
@@ -52,7 +60,7 @@ module IGMarkets
52
60
  #
53
61
  # @return [Market]
54
62
  def [](epic)
55
- find(epic)[0]
63
+ find(epic).first
56
64
  end
57
65
  end
58
66
  end
@@ -20,13 +20,13 @@ module IGMarkets
20
20
 
21
21
  # Returns the position with the specified deal ID, or `nil` if there is no position with that ID.
22
22
  #
23
- # @param [String] deal_id The deal ID of the working order to return.
23
+ # @param [String] deal_id The deal ID of the position to return.
24
24
  #
25
25
  # @return [Position]
26
26
  def [](deal_id)
27
- attributes = @dealing_platform.session.get "positions/#{deal_id}", API_V2
28
-
29
- position_from_attributes attributes
27
+ all.detect do |position|
28
+ position.deal_id == deal_id
29
+ end
30
30
  end
31
31
 
32
32
  # Creates a new position.
@@ -161,9 +161,9 @@ module IGMarkets
161
161
  end
162
162
 
163
163
  def position_from_attributes(attributes)
164
- Position.new(attributes.fetch(:position).merge(market: attributes.fetch(:market))).tap do |position|
165
- position.instance_variable_set :@dealing_platform, @dealing_platform
166
- end
164
+ attributes = attributes.fetch(:position).merge market: attributes.fetch(:market)
165
+
166
+ @dealing_platform.instantiate_models Position, attributes
167
167
  end
168
168
 
169
169
  private_constant :PositionCreateAttributes
@@ -13,7 +13,21 @@ module IGMarkets
13
13
  #
14
14
  # @return [Array<SprintMarketPosition>]
15
15
  def all
16
- @dealing_platform.gather 'positions/sprintmarkets', :sprint_market_positions, SprintMarketPosition
16
+ result = @dealing_platform.session.get('positions/sprintmarkets').fetch :sprint_market_positions
17
+
18
+ @dealing_platform.instantiate_models SprintMarketPosition, result
19
+ end
20
+
21
+ # Returns the sprint market position with the specified deal ID, or `nil` if there is no sprint market position
22
+ # with that ID.
23
+ #
24
+ # @param [String] deal_id The deal ID of the sprint market position to return.
25
+ #
26
+ # @return [SprintMarketPosition]
27
+ def [](deal_id)
28
+ all.detect do |sprint_market_position|
29
+ sprint_market_position.deal_id == deal_id
30
+ end
17
31
  end
18
32
 
19
33
  # Creates a new sprint market position.
@@ -30,7 +44,7 @@ module IGMarkets
30
44
  def create(attributes)
31
45
  payload = PayloadFormatter.format SprintMarketPositionCreateAttributes.new attributes
32
46
 
33
- @dealing_platform.session.post('positions/sprintmarkets', payload, API_V1).fetch(:deal_reference)
47
+ @dealing_platform.session.post('positions/sprintmarkets', payload).fetch :deal_reference
34
48
  end
35
49
 
36
50
  # Internal model used by {#create}