ig_markets 0.5 → 0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -8
- data/README.md +40 -10
- data/lib/ig_markets.rb +3 -1
- data/lib/ig_markets/cli/commands/activities_command.rb +9 -7
- data/lib/ig_markets/cli/commands/orders_command.rb +8 -11
- data/lib/ig_markets/cli/commands/positions_command.rb +9 -7
- data/lib/ig_markets/cli/commands/prices_command.rb +66 -0
- data/lib/ig_markets/cli/commands/search_command.rb +19 -2
- data/lib/ig_markets/cli/commands/sprints_command.rb +2 -2
- data/lib/ig_markets/cli/config_file.rb +1 -1
- data/lib/ig_markets/cli/main.rb +25 -16
- data/lib/ig_markets/cli/tables/accounts_table.rb +10 -0
- data/lib/ig_markets/cli/tables/historical_price_result_snapshots_table.rb +25 -0
- data/lib/ig_markets/cli/tables/table.rb +2 -2
- data/lib/ig_markets/client_sentiment.rb +3 -1
- data/lib/ig_markets/dealing_platform.rb +49 -14
- data/lib/ig_markets/dealing_platform/account_methods.rb +13 -5
- data/lib/ig_markets/dealing_platform/client_sentiment_methods.rb +2 -4
- data/lib/ig_markets/dealing_platform/market_methods.rb +14 -6
- data/lib/ig_markets/dealing_platform/position_methods.rb +7 -7
- data/lib/ig_markets/dealing_platform/sprint_market_position_methods.rb +16 -2
- data/lib/ig_markets/dealing_platform/watchlist_methods.rb +4 -2
- data/lib/ig_markets/dealing_platform/working_order_methods.rb +2 -4
- data/lib/ig_markets/format.rb +10 -0
- data/lib/ig_markets/historical_price_result.rb +1 -1
- data/lib/ig_markets/instrument.rb +10 -10
- data/lib/ig_markets/market.rb +3 -3
- data/lib/ig_markets/model.rb +17 -25
- data/lib/ig_markets/model/typecasters.rb +20 -13
- data/lib/ig_markets/payload_formatter.rb +1 -1
- data/lib/ig_markets/position.rb +1 -1
- data/lib/ig_markets/session.rb +8 -8
- data/lib/ig_markets/sprint_market_position.rb +2 -2
- data/lib/ig_markets/transaction.rb +1 -1
- data/lib/ig_markets/version.rb +1 -1
- data/lib/ig_markets/watchlist.rb +6 -4
- data/lib/ig_markets/working_order.rb +2 -2
- metadata +4 -2
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 :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
|
-
|
41
|
-
exit 1
|
43
|
+
error "Request error: #{error.error}"
|
42
44
|
rescue ArgumentError => error
|
43
|
-
|
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
|
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
|
-
#
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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 "
|
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
|
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
|
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.
|
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
|
-
|
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.
|
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
|
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
|
91
|
+
instantiate_models Application, session.get('operations/application')
|
85
92
|
end
|
86
93
|
|
87
|
-
#
|
88
|
-
#
|
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 [
|
91
|
-
# @param [
|
92
|
-
#
|
93
|
-
#
|
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
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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}"
|
18
|
+
result = @dealing_platform.session.get "clientsentiment/#{market_id}"
|
19
19
|
|
20
|
-
|
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
|
-
|
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
|
-
|
32
|
+
epics = epics.flatten
|
33
|
+
|
34
|
+
return [] if epics.empty?
|
31
35
|
|
32
|
-
epics.
|
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.
|
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.
|
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)
|
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
|
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
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
165
|
-
|
166
|
-
|
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.
|
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
|
47
|
+
@dealing_platform.session.post('positions/sprintmarkets', payload).fetch :deal_reference
|
34
48
|
end
|
35
49
|
|
36
50
|
# Internal model used by {#create}
|