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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -4
- data/README.md +70 -17
- data/lib/ig_markets.rb +27 -15
- data/lib/ig_markets/{account_activity.rb → activity.rb} +1 -1
- data/lib/ig_markets/cli/{account_command.rb → commands/account_command.rb} +5 -4
- data/lib/ig_markets/cli/commands/activities_command.rb +37 -0
- data/lib/ig_markets/cli/commands/confirmation_command.rb +14 -0
- data/lib/ig_markets/cli/{orders_command.rb → commands/orders_command.rb} +16 -21
- data/lib/ig_markets/cli/{positions_command.rb → commands/positions_command.rb} +31 -19
- data/lib/ig_markets/cli/commands/search_command.rb +18 -0
- data/lib/ig_markets/cli/commands/sentiment_command.rb +19 -0
- data/lib/ig_markets/cli/{sprints_command.rb → commands/sprints_command.rb} +10 -8
- data/lib/ig_markets/cli/commands/transactions_command.rb +61 -0
- data/lib/ig_markets/cli/{watchlists_command.rb → commands/watchlists_command.rb} +16 -12
- data/lib/ig_markets/cli/main.rb +65 -15
- data/lib/ig_markets/cli/tables/accounts_table.rb +30 -0
- data/lib/ig_markets/cli/tables/activities_table.rb +29 -0
- data/lib/ig_markets/cli/tables/client_sentiments_table.rb +31 -0
- data/lib/ig_markets/cli/tables/market_overviews_table.rb +47 -0
- data/lib/ig_markets/cli/tables/positions_table.rb +83 -0
- data/lib/ig_markets/cli/tables/sprint_market_positions_table.rb +55 -0
- data/lib/ig_markets/cli/tables/table.rb +103 -0
- data/lib/ig_markets/cli/tables/transactions_table.rb +41 -0
- data/lib/ig_markets/cli/tables/working_orders_table.rb +27 -0
- data/lib/ig_markets/dealing_platform/account_methods.rb +8 -8
- data/lib/ig_markets/dealing_platform/client_sentiment_methods.rb +4 -0
- data/lib/ig_markets/dealing_platform/market_methods.rb +1 -1
- data/lib/ig_markets/format.rb +20 -7
- data/lib/ig_markets/market_overview.rb +1 -1
- data/lib/ig_markets/model.rb +10 -1
- data/lib/ig_markets/model/typecasters.rb +1 -1
- data/lib/ig_markets/position.rb +12 -11
- data/lib/ig_markets/request_printer.rb +56 -0
- data/lib/ig_markets/response_parser.rb +11 -0
- data/lib/ig_markets/session.rb +7 -8
- data/lib/ig_markets/{account_transaction.rb → transaction.rb} +4 -17
- data/lib/ig_markets/version.rb +1 -1
- metadata +54 -16
- data/lib/ig_markets/cli/activities_command.rb +0 -31
- data/lib/ig_markets/cli/confirmation_command.rb +0 -16
- data/lib/ig_markets/cli/output.rb +0 -115
- data/lib/ig_markets/cli/search_command.rb +0 -16
- data/lib/ig_markets/cli/sentiment_command.rb +0 -24
- 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
|
10
|
-
|
11
|
-
|
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
|
27
|
-
|
28
|
-
puts "Deal reference: #{deal_reference}"
|
30
|
+
deal_reference = dealing_platform.sprint_market_positions.create sprint_market_position_attributes
|
29
31
|
|
30
|
-
|
32
|
+
Main.report_deal_confirmation deal_reference
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
34
36
|
private
|
35
37
|
|
36
|
-
def
|
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.
|
10
|
-
|
9
|
+
dealing_platform.watchlists.all.each_with_index do |watchlist, index|
|
10
|
+
table = MarketOverviewsTable.new watchlist.markets, title: table_title(watchlist)
|
11
11
|
|
12
|
-
|
13
|
-
|
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
|
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
|
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
|
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
|
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
|
data/lib/ig_markets/cli/main.rb
CHANGED
@@ -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
|
25
|
-
# occur. This method is used by all of the
|
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
|
40
|
+
warn "Request error: #{error.error}"
|
38
41
|
exit 1
|
39
|
-
rescue
|
40
|
-
warn "
|
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
|
-
#
|
50
|
-
#
|
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
|
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
|
-
|
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
|
-
|
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
|