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
@@ -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}