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