ig_markets 0.4 → 0.5

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -4
  3. data/README.md +70 -17
  4. data/lib/ig_markets.rb +27 -15
  5. data/lib/ig_markets/{account_activity.rb → activity.rb} +1 -1
  6. data/lib/ig_markets/cli/{account_command.rb → commands/account_command.rb} +5 -4
  7. data/lib/ig_markets/cli/commands/activities_command.rb +37 -0
  8. data/lib/ig_markets/cli/commands/confirmation_command.rb +14 -0
  9. data/lib/ig_markets/cli/{orders_command.rb → commands/orders_command.rb} +16 -21
  10. data/lib/ig_markets/cli/{positions_command.rb → commands/positions_command.rb} +31 -19
  11. data/lib/ig_markets/cli/commands/search_command.rb +18 -0
  12. data/lib/ig_markets/cli/commands/sentiment_command.rb +19 -0
  13. data/lib/ig_markets/cli/{sprints_command.rb → commands/sprints_command.rb} +10 -8
  14. data/lib/ig_markets/cli/commands/transactions_command.rb +61 -0
  15. data/lib/ig_markets/cli/{watchlists_command.rb → commands/watchlists_command.rb} +16 -12
  16. data/lib/ig_markets/cli/main.rb +65 -15
  17. data/lib/ig_markets/cli/tables/accounts_table.rb +30 -0
  18. data/lib/ig_markets/cli/tables/activities_table.rb +29 -0
  19. data/lib/ig_markets/cli/tables/client_sentiments_table.rb +31 -0
  20. data/lib/ig_markets/cli/tables/market_overviews_table.rb +47 -0
  21. data/lib/ig_markets/cli/tables/positions_table.rb +83 -0
  22. data/lib/ig_markets/cli/tables/sprint_market_positions_table.rb +55 -0
  23. data/lib/ig_markets/cli/tables/table.rb +103 -0
  24. data/lib/ig_markets/cli/tables/transactions_table.rb +41 -0
  25. data/lib/ig_markets/cli/tables/working_orders_table.rb +27 -0
  26. data/lib/ig_markets/dealing_platform/account_methods.rb +8 -8
  27. data/lib/ig_markets/dealing_platform/client_sentiment_methods.rb +4 -0
  28. data/lib/ig_markets/dealing_platform/market_methods.rb +1 -1
  29. data/lib/ig_markets/format.rb +20 -7
  30. data/lib/ig_markets/market_overview.rb +1 -1
  31. data/lib/ig_markets/model.rb +10 -1
  32. data/lib/ig_markets/model/typecasters.rb +1 -1
  33. data/lib/ig_markets/position.rb +12 -11
  34. data/lib/ig_markets/request_printer.rb +56 -0
  35. data/lib/ig_markets/response_parser.rb +11 -0
  36. data/lib/ig_markets/session.rb +7 -8
  37. data/lib/ig_markets/{account_transaction.rb → transaction.rb} +4 -17
  38. data/lib/ig_markets/version.rb +1 -1
  39. metadata +54 -16
  40. data/lib/ig_markets/cli/activities_command.rb +0 -31
  41. data/lib/ig_markets/cli/confirmation_command.rb +0 -16
  42. data/lib/ig_markets/cli/output.rb +0 -115
  43. data/lib/ig_markets/cli/search_command.rb +0 -16
  44. data/lib/ig_markets/cli/sentiment_command.rb +0 -24
  45. data/lib/ig_markets/cli/transactions_command.rb +0 -57
@@ -0,0 +1,18 @@
1
+ module IGMarkets
2
+ module CLI
3
+ # Implements the `ig_markets search` command.
4
+ class Main < Thor
5
+ desc 'search QUERY', 'Searches markets based on the specified query string'
6
+
7
+ def search(query)
8
+ self.class.begin_session(options) do |dealing_platform|
9
+ market_overviews = dealing_platform.markets.search query
10
+
11
+ table = MarketOverviewsTable.new market_overviews
12
+
13
+ puts table
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module IGMarkets
2
+ module CLI
3
+ # Implements the `ig_markets sentiment` command.
4
+ class Main
5
+ desc 'sentiment MARKET', 'Prints sentiment and related sentiments for the specified market'
6
+
7
+ def sentiment(market)
8
+ self.class.begin_session(options) do |dealing_platform|
9
+ client_sentiment = dealing_platform.client_sentiment[market]
10
+ client_sentiments = [client_sentiment, :separator, client_sentiment.related_sentiments]
11
+
12
+ table = ClientSentimentsTable.new client_sentiments, title: "Client sentiment for '#{market}'"
13
+
14
+ puts table
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -6,9 +6,13 @@ module IGMarkets
6
6
 
7
7
  def list
8
8
  Main.begin_session(options) do |dealing_platform|
9
- dealing_platform.sprint_market_positions.all.each do |sprint_market_position|
10
- Output.print_sprint_market_position sprint_market_position
11
- end
9
+ sprints = dealing_platform.sprint_market_positions.all
10
+
11
+ markets = dealing_platform.markets.find sprints.map(&:epic).uniq
12
+
13
+ table = SprintMarketPositionsTable.new sprints, markets: markets
14
+
15
+ puts table
12
16
  end
13
17
  end
14
18
 
@@ -23,17 +27,15 @@ module IGMarkets
23
27
 
24
28
  def create
25
29
  Main.begin_session(options) do |dealing_platform|
26
- deal_reference = dealing_platform.sprint_market_positions.create new_sprint_market_position_attributes
27
-
28
- puts "Deal reference: #{deal_reference}"
30
+ deal_reference = dealing_platform.sprint_market_positions.create sprint_market_position_attributes
29
31
 
30
- Output.print_deal_confirmation dealing_platform.deal_confirmation deal_reference
32
+ Main.report_deal_confirmation deal_reference
31
33
  end
32
34
  end
33
35
 
34
36
  private
35
37
 
36
- def new_sprint_market_position_attributes
38
+ def sprint_market_position_attributes
37
39
  {
38
40
  direction: options[:direction],
39
41
  epic: options[:epic],
@@ -0,0 +1,61 @@
1
+ module IGMarkets
2
+ module CLI
3
+ # Implements the `ig_markets transactions` command.
4
+ class Main
5
+ desc 'transactions', 'Prints account transactions'
6
+
7
+ option :days, type: :numeric, required: true, desc: 'The number of days to print account transactions for'
8
+ option :start_date, desc: 'The start date to print account transactions from, format: yyyy-mm-dd'
9
+ option :instrument, desc: 'Regex for filtering transactions based on their instrument'
10
+ option :interest, type: :boolean, default: true, desc: 'Whether to show interest deposits and withdrawals'
11
+
12
+ def transactions
13
+ self.class.begin_session(options) do |_dealing_platform|
14
+ transactions = gather_transactions
15
+
16
+ table = TransactionsTable.new transactions
17
+
18
+ puts table
19
+
20
+ if transactions.any?
21
+ puts ''
22
+ print_transaction_totals transactions
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def gather_transactions
30
+ regex = Regexp.new options.fetch('instrument', '')
31
+
32
+ gather_account_history(:transactions).sort_by(&:date).select do |transaction|
33
+ regex.match(transaction.instrument_name) && (options[:interest] || !transaction.interest?)
34
+ end
35
+ end
36
+
37
+ def transaction_totals(transactions)
38
+ transactions.each_with_object({}) do |transaction, hash|
39
+ profit_loss = transaction.profit_and_loss_amount
40
+
41
+ currency = (hash[transaction.currency] ||= Hash.new(0))
42
+
43
+ currency[:delta] += profit_loss
44
+ currency[:interest] += profit_loss if transaction.interest?
45
+ end
46
+ end
47
+
48
+ def print_transaction_totals(transactions)
49
+ totals = transaction_totals transactions
50
+
51
+ return if totals.empty?
52
+
53
+ if options[:interest]
54
+ puts "Interest: #{totals.map { |currency, value| Format.currency value[:interest], currency }.join ', '}"
55
+ end
56
+
57
+ puts "Profit/loss: #{totals.map { |currency, value| Format.currency value[:delta], currency }.join ', '}"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -6,22 +6,18 @@ module IGMarkets
6
6
 
7
7
  def list
8
8
  Main.begin_session(options) do |dealing_platform|
9
- dealing_platform.watchlists.all.each do |watchlist|
10
- Output.print_watchlist watchlist
9
+ dealing_platform.watchlists.all.each_with_index do |watchlist, index|
10
+ table = MarketOverviewsTable.new watchlist.markets, title: table_title(watchlist)
11
11
 
12
- watchlist.markets.each do |market_overview|
13
- print ' - '
14
- Output.print_market_overview market_overview
15
- end
16
-
17
- puts ''
12
+ puts '' if index > 0
13
+ puts table
18
14
  end
19
15
  end
20
16
  end
21
17
 
22
18
  default_task :list
23
19
 
24
- desc 'create <NAME> [<EPIC> <EPIC> ...]', 'Creates a new watchlist with the specified name and EPICs'
20
+ desc 'create NAME [EPIC EPIC ...]', 'Creates a new watchlist with the specified name and EPICs'
25
21
 
26
22
  def create(name, *epics)
27
23
  Main.begin_session(options) do |dealing_platform|
@@ -31,7 +27,7 @@ module IGMarkets
31
27
  end
32
28
  end
33
29
 
34
- desc 'add-markets <WATCHLIST-ID> <EPIC> [<EPIC> ...]', 'Adds the specified markets to a watchlist'
30
+ desc 'add-markets WATCHLIST-ID EPIC [EPIC ...]', 'Adds the specified markets to a watchlist'
35
31
 
36
32
  def add_markets(watchlist_id, *epics)
37
33
  with_watchlist(watchlist_id) do |watchlist|
@@ -41,7 +37,7 @@ module IGMarkets
41
37
  end
42
38
  end
43
39
 
44
- desc 'remove-markets <WATCHLIST-ID> <EPIC> [<EPIC> ...]', 'Removes the specified markets from a watchlist'
40
+ desc 'remove-markets WATCHLIST-ID EPIC [EPIC ...]', 'Removes the specified markets from a watchlist'
45
41
 
46
42
  def remove_markets(watchlist_id, *epics)
47
43
  with_watchlist(watchlist_id) do |watchlist|
@@ -51,7 +47,7 @@ module IGMarkets
51
47
  end
52
48
  end
53
49
 
54
- desc 'delete <WATCHLIST-ID>', 'Deletes the watchlist with the specified ID'
50
+ desc 'delete WATCHLIST-ID', 'Deletes the watchlist with the specified ID'
55
51
 
56
52
  def delete(watchlist_id)
57
53
  with_watchlist(watchlist_id, &:delete)
@@ -68,6 +64,14 @@ module IGMarkets
68
64
  yield watchlist
69
65
  end
70
66
  end
67
+
68
+ def table_title(watchlist)
69
+ title = "#{watchlist.name} (id: #{watchlist.id}"
70
+ title << ', editable' if watchlist.editable
71
+ title << ', deleteable' if watchlist.deleteable
72
+ title << ', default' if watchlist.default_system_watchlist
73
+ title << ')'
74
+ end
71
75
  end
72
76
  end
73
77
  end
@@ -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 :print_requests, type: :boolean, desc: 'Whether to print the raw REST API requests and responses'
10
11
 
11
12
  desc 'orders [SUBCOMAND=list ...]', 'Command for working with orders'
12
13
  subcommand 'orders', Orders
@@ -21,8 +22,8 @@ module IGMarkets
21
22
  subcommand 'watchlists', Watchlists
22
23
 
23
24
  class << self
24
- # Signs in to IG Markets and yields back an {DealingPlatform} instance, with common error handling if exceptions
25
- # occur. This method is used by all of the CLI commands to authenticate.
25
+ # Signs in to IG Markets and yields back a {DealingPlatform} instance, with common error handling if exceptions
26
+ # occur. This method is used by all of the commands in order to authenticate.
26
27
  #
27
28
  # @param [Thor::CoreExt::HashWithIndifferentAccess] options The Thor options hash.
28
29
  #
@@ -30,14 +31,16 @@ module IGMarkets
30
31
  def begin_session(options)
31
32
  platform = options[:demo] ? :demo : :production
32
33
 
34
+ RequestPrinter.enabled = true if options[:print_requests]
35
+
33
36
  dealing_platform.sign_in options[:username], options[:password], options[:api_key], platform
34
37
 
35
38
  yield dealing_platform
36
39
  rescue IGMarkets::RequestFailedError => error
37
- warn "Request failed: #{error.error}"
40
+ warn "Request error: #{error.error}"
38
41
  exit 1
39
- rescue StandardError => error
40
- warn "Error: #{error}"
42
+ rescue ArgumentError => error
43
+ warn "Argument error: #{error}"
41
44
  exit 1
42
45
  end
43
46
 
@@ -46,13 +49,32 @@ module IGMarkets
46
49
  @dealing_platform ||= DealingPlatform.new
47
50
  end
48
51
 
49
- # Parses and validates a Date or Time option received on the command line. Raises `ArgumentError` if the
50
- # attribute has been specified in an invalid format.
52
+ # Takes a deal reference and prints out its full deal confirmation.
53
+ #
54
+ # @param [String] deal_reference The deal reference.
55
+ #
56
+ # @return [void]
57
+ def report_deal_confirmation(deal_reference)
58
+ puts "Deal reference: #{deal_reference}"
59
+
60
+ deal_confirmation = dealing_platform.deal_confirmation deal_reference
61
+
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
67
+
68
+ puts "epic: #{deal_confirmation.epic}"
69
+ end
70
+
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.
51
73
  #
52
74
  # @param [Hash] attributes The attributes hash.
53
75
  # @param [Symbol] attribute The name of the date or time attribute to parse and validate.
54
76
  # @param [Date, Time] klass The class to validate with.
55
- # @param [String] format The `strptime` format string to parse the attribute with.
77
+ # @param [String] format The `strptime` format string for the attribute.
56
78
  # @param [String] display_format The human-readable version of `format` to put into an exception if there is
57
79
  # a problem parsing the attribute.
58
80
  #
@@ -64,13 +86,29 @@ module IGMarkets
64
86
  begin
65
87
  attributes[attribute] = klass.strptime attributes[attribute], format
66
88
  rescue ArgumentError
67
- raise "invalid #{attribute}, use format \"#{display_format}\""
89
+ raise ArgumentError, "invalid #{attribute}, use format \"#{display_format}\""
68
90
  end
69
91
  else
70
92
  attributes[attribute] = nil
71
93
  end
72
94
  end
73
95
 
96
+ # Takes a Thor options hash and filters out its keys in the specified whitelist. Thor has an unusual behavior
97
+ # 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.
99
+ #
100
+ # @param [Thor::CoreExt::HashWithIndifferentAccess] options The Thor options.
101
+ # @param [Array<Symbol>] whitelist The list of options allowed in the returned `Hash`.
102
+ #
103
+ # @return [Hash]
104
+ def filter_options(options, whitelist)
105
+ options.each_with_object({}) do |(key, value), new_hash|
106
+ next unless whitelist.include? key.to_sym
107
+
108
+ new_hash[key.to_sym] = (value == key) ? nil : value
109
+ end
110
+ end
111
+
74
112
  # This is the initial entry point for the execution of the command-line client. It is responsible for reading
75
113
  # any config files, implementing the --version/-v options, and then invoking the main application.
76
114
  #
@@ -78,19 +116,31 @@ module IGMarkets
78
116
  #
79
117
  # @return [void]
80
118
  def bootstrap(argv)
119
+ prepend_config_file_arguments argv
120
+
81
121
  if argv.index('--version') || argv.index('-v')
82
122
  puts VERSION
83
123
  exit
84
124
  end
85
125
 
86
- # Use arguments from a config file if one exists
126
+ start argv
127
+ end
128
+
129
+ # Searches for a config file and if found inserts its arguments into the passed arguments array.
130
+ #
131
+ # @param [Array<String>] argv The array of command-line arguments.
132
+ #
133
+ # @return [void]
134
+ def prepend_config_file_arguments(argv)
87
135
  config_file = ConfigFile.find
88
- if config_file
89
- insert_index = argv.index { |argument| argument[0] == '-' } || -1
90
- argv.insert insert_index, *config_file.arguments
91
- end
92
136
 
93
- start argv
137
+ return unless config_file
138
+
139
+ insert_index = argv.index do |argument|
140
+ argument[0] == '-'
141
+ end || -1
142
+
143
+ argv.insert insert_index, *config_file.arguments
94
144
  end
95
145
  end
96
146
  end
@@ -0,0 +1,30 @@
1
+ module IGMarkets
2
+ module CLI
3
+ # Helper class that prints out an array of {IGMarkets::Account} instances in a table.
4
+ class AccountsTable < Table
5
+ private
6
+
7
+ def default_title
8
+ 'Accounts'
9
+ end
10
+
11
+ def headings
12
+ ['Name', 'ID', 'Type', 'Currency', 'Status', 'Preferred', 'Available', 'Balance', 'Margin', 'Profit/loss']
13
+ end
14
+
15
+ def right_aligned_columns
16
+ [6, 7, 8, 9]
17
+ end
18
+
19
+ def row(account)
20
+ type = { cfd: 'CFD', physical: 'Physical', spreadbet: 'Spreadbet' }.fetch account.account_type
21
+ status = { disabled: 'Disabled', enabled: 'Enabled', suspended_from_dealing: 'Suspended' }.fetch account.status
22
+
23
+ [account.account_name, account.account_id, type, account.currency, status, account.preferred] +
24
+ [:available, :balance, :deposit, :profit_loss].map do |attribute|
25
+ Format.currency account.balance.send(attribute), account.currency
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ module IGMarkets
2
+ module CLI
3
+ # Helper class that prints out an array of {IGMarkets::Activity} instances in a table.
4
+ class ActivitiesTable < Table
5
+ private
6
+
7
+ def default_title
8
+ 'Activities'
9
+ end
10
+
11
+ def headings
12
+ %w(Date Time Channel Type Status EPIC Market Size Level Limit Stop Result)
13
+ end
14
+
15
+ def right_aligned_columns
16
+ [7, 8, 9, 10]
17
+ end
18
+
19
+ def row(activity)
20
+ [activity.date, activity.time, activity.channel, activity.activity, activity_status(activity), activity.epic,
21
+ activity.market_name, activity.size, activity.level, activity.limit, activity.stop, activity.result]
22
+ end
23
+
24
+ def activity_status(activity)
25
+ { accept: 'Accepted', reject: 'Rejected', manual: 'Manual', not_set: '' }.fetch activity.action_status
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ module IGMarkets
2
+ module CLI
3
+ # Helper class that prints out an array of {IGMarkets::ClientSentiment} instances in a table.
4
+ class ClientSentimentsTable < Table
5
+ private
6
+
7
+ def headings
8
+ ['Market', 'Long %', 'Short %']
9
+ end
10
+
11
+ def right_aligned_columns
12
+ [1, 2]
13
+ end
14
+
15
+ def row(client_sentiment)
16
+ [client_sentiment.market_id, client_sentiment.long_position_percentage,
17
+ client_sentiment.short_position_percentage]
18
+ end
19
+
20
+ def cell_color(_value, client_sentiment, _row_index, _column_index)
21
+ distance_from_center = (client_sentiment.long_position_percentage - 50.0).abs
22
+
23
+ if distance_from_center > 35
24
+ :red
25
+ elsif distance_from_center > 20
26
+ :yellow
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end