ig_markets 0.4 → 0.5

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