ig_markets 0.1
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 +7 -0
- data/.codeclimate.yml +15 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.rubocop.yml +2 -0
- data/.travis.yml +10 -0
- data/.yardopts +4 -0
- data/Gemfile +2 -0
- data/LICENSE.md +25 -0
- data/README.md +134 -0
- data/ig_markets.gemspec +28 -0
- data/lib/ig_markets.rb +42 -0
- data/lib/ig_markets/account.rb +23 -0
- data/lib/ig_markets/account_activity.rb +24 -0
- data/lib/ig_markets/account_transaction.rb +49 -0
- data/lib/ig_markets/api_versions.rb +10 -0
- data/lib/ig_markets/application.rb +22 -0
- data/lib/ig_markets/boolean.rb +5 -0
- data/lib/ig_markets/client_sentiment.rb +16 -0
- data/lib/ig_markets/deal_confirmation.rb +41 -0
- data/lib/ig_markets/dealing_platform.rb +105 -0
- data/lib/ig_markets/dealing_platform/account_methods.rb +92 -0
- data/lib/ig_markets/dealing_platform/client_sentiment_methods.rb +26 -0
- data/lib/ig_markets/dealing_platform/market_methods.rb +59 -0
- data/lib/ig_markets/dealing_platform/position_methods.rb +164 -0
- data/lib/ig_markets/dealing_platform/sprint_market_position_methods.rb +46 -0
- data/lib/ig_markets/dealing_platform/watchlist_methods.rb +42 -0
- data/lib/ig_markets/dealing_platform/working_order_methods.rb +115 -0
- data/lib/ig_markets/historical_price_result.rb +33 -0
- data/lib/ig_markets/instrument.rb +89 -0
- data/lib/ig_markets/market.rb +99 -0
- data/lib/ig_markets/market_hierarchy_result.rb +13 -0
- data/lib/ig_markets/market_overview.rb +24 -0
- data/lib/ig_markets/model.rb +185 -0
- data/lib/ig_markets/password_encryptor.rb +31 -0
- data/lib/ig_markets/payload_formatter.rb +38 -0
- data/lib/ig_markets/position.rb +191 -0
- data/lib/ig_markets/regex.rb +10 -0
- data/lib/ig_markets/request_failed_error.rb +21 -0
- data/lib/ig_markets/response_parser.rb +35 -0
- data/lib/ig_markets/session.rb +186 -0
- data/lib/ig_markets/sprint_market_position.rb +17 -0
- data/lib/ig_markets/version.rb +4 -0
- data/lib/ig_markets/watchlist.rb +37 -0
- data/lib/ig_markets/working_order.rb +68 -0
- data/spec/factories/ig_markets/account.rb +14 -0
- data/spec/factories/ig_markets/account_activity.rb +21 -0
- data/spec/factories/ig_markets/account_balance.rb +8 -0
- data/spec/factories/ig_markets/account_transaction.rb +15 -0
- data/spec/factories/ig_markets/application.rb +21 -0
- data/spec/factories/ig_markets/client_sentiment.rb +7 -0
- data/spec/factories/ig_markets/deal_confirmation.rb +20 -0
- data/spec/factories/ig_markets/historical_price_result.rb +7 -0
- data/spec/factories/ig_markets/historical_price_result_data_allowance.rb +7 -0
- data/spec/factories/ig_markets/historical_price_result_price.rb +7 -0
- data/spec/factories/ig_markets/historical_price_result_snapshot.rb +10 -0
- data/spec/factories/ig_markets/instrument.rb +32 -0
- data/spec/factories/ig_markets/instrument_currency.rb +9 -0
- data/spec/factories/ig_markets/instrument_expiry_details.rb +6 -0
- data/spec/factories/ig_markets/instrument_margin_deposit_band.rb +8 -0
- data/spec/factories/ig_markets/instrument_opening_hours.rb +6 -0
- data/spec/factories/ig_markets/instrument_rollover_details.rb +6 -0
- data/spec/factories/ig_markets/instrument_slippage_factor.rb +6 -0
- data/spec/factories/ig_markets/market.rb +7 -0
- data/spec/factories/ig_markets/market_dealing_rules.rb +11 -0
- data/spec/factories/ig_markets/market_dealing_rules_rule_details.rb +6 -0
- data/spec/factories/ig_markets/market_hierarchy_result.rb +6 -0
- data/spec/factories/ig_markets/market_hierarchy_result_hierarchy_node.rb +6 -0
- data/spec/factories/ig_markets/market_overview.rb +22 -0
- data/spec/factories/ig_markets/market_snapshot.rb +17 -0
- data/spec/factories/ig_markets/position.rb +19 -0
- data/spec/factories/ig_markets/sprint_market_position.rb +16 -0
- data/spec/factories/ig_markets/watchlist.rb +9 -0
- data/spec/factories/ig_markets/working_order.rb +21 -0
- data/spec/ig_markets/account_transaction_spec.rb +30 -0
- data/spec/ig_markets/dealing_platform/account_methods_spec.rb +58 -0
- data/spec/ig_markets/dealing_platform/client_sentiment_methods_spec.rb +29 -0
- data/spec/ig_markets/dealing_platform/market_methods_spec.rb +80 -0
- data/spec/ig_markets/dealing_platform/position_methods_spec.rb +137 -0
- data/spec/ig_markets/dealing_platform/sprint_market_position_methods_spec.rb +39 -0
- data/spec/ig_markets/dealing_platform/watchlist_methods_spec.rb +89 -0
- data/spec/ig_markets/dealing_platform/working_order_methods_spec.rb +120 -0
- data/spec/ig_markets/dealing_platform_spec.rb +40 -0
- data/spec/ig_markets/model_spec.rb +127 -0
- data/spec/ig_markets/password_encryptor_spec.rb +23 -0
- data/spec/ig_markets/payload_formatter_spec.rb +19 -0
- data/spec/ig_markets/position_spec.rb +37 -0
- data/spec/ig_markets/response_parser_spec.rb +13 -0
- data/spec/ig_markets/session_spec.rb +134 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/factory_girl.rb +7 -0
- data/spec/support/random_test_order.rb +3 -0
- metadata +261 -0
@@ -0,0 +1,185 @@
|
|
1
|
+
module IGMarkets
|
2
|
+
# This class is intended to be subclassed in order to create models that contain a set of attributes, where each
|
3
|
+
# attribute is defined by a call to {attribute}. Attributes have standard getter and setter methods and can also
|
4
|
+
# be subject to a variety of constraints and validations, see {attribute} for further details.
|
5
|
+
class Model
|
6
|
+
# @return [Hash] The current attribute values set on this model.
|
7
|
+
attr_reader :attributes
|
8
|
+
|
9
|
+
# Initializes this new model with the given attribute values. Attributes not known to this model will raise
|
10
|
+
# `ArgumentError`.
|
11
|
+
#
|
12
|
+
# @param [Hash] attributes The attribute values to set on this new model.
|
13
|
+
def initialize(attributes = {})
|
14
|
+
defined_attribute_names = self.class.defined_attribute_names
|
15
|
+
|
16
|
+
defined_attribute_names.each do |name|
|
17
|
+
send "#{name}=", attributes[name]
|
18
|
+
end
|
19
|
+
|
20
|
+
(attributes.keys - defined_attribute_names).map do |attribute|
|
21
|
+
value = attributes[attribute]
|
22
|
+
value = value.inspect unless value.is_a? DateTime
|
23
|
+
|
24
|
+
raise ArgumentError, "Unknown attribute: #{self.class.name}##{attribute}, value: #{value}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Compares this model to another, the attributes and class must match for them to be considered equal.
|
29
|
+
#
|
30
|
+
# @param [#class, #attributes] other The other model to compare to.
|
31
|
+
#
|
32
|
+
# @return [Boolean]
|
33
|
+
def ==(other)
|
34
|
+
self.class == other.class && attributes == other.attributes
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns a human-readable string containing this model's type and all its current attribute values.
|
38
|
+
#
|
39
|
+
# @return [String]
|
40
|
+
def inspect
|
41
|
+
formatted_attributes = self.class.defined_attribute_names.map do |attribute|
|
42
|
+
value = send attribute
|
43
|
+
|
44
|
+
"#{attribute}: #{value.is_a?(DateTime) ? value : value.inspect}"
|
45
|
+
end
|
46
|
+
|
47
|
+
"#<#{self.class.name} #{formatted_attributes.join ', '}>"
|
48
|
+
end
|
49
|
+
|
50
|
+
class << self
|
51
|
+
# @return [Hash] A hash containing details of all attributes that have been defined on this model.
|
52
|
+
attr_accessor :defined_attributes
|
53
|
+
|
54
|
+
# Returns the names of all currently defined attributes for this model.
|
55
|
+
#
|
56
|
+
# @return [Array<Symbol>]
|
57
|
+
def defined_attribute_names
|
58
|
+
(defined_attributes || {}).keys
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the array of allowed values for the specified attribute that was passed to {attribute}.
|
62
|
+
#
|
63
|
+
# @param [Symbol] attribute_name The name of the attribute to return the allowed values for.
|
64
|
+
#
|
65
|
+
# @return [Array]
|
66
|
+
def allowed_values(attribute_name)
|
67
|
+
defined_attributes.fetch(attribute_name).fetch(:allowed_values)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Defines setter and getter methods for a new attribute on this class.
|
71
|
+
#
|
72
|
+
# @param [Symbol] name The name of the new attribute.
|
73
|
+
# @param [Boolean, String, DateTime, Fixnum, Float, Symbol, #from] type The attribute's type.
|
74
|
+
# @param [Hash] options The configuration options for the new attribute.
|
75
|
+
# @option options [Array] :allowed_values The set of values that this attribute is allowed to be set to. An
|
76
|
+
# attempt to set this attribute to a value not in this list will raise `ArgumentError`. Optional.
|
77
|
+
# @option options [Array] :nil_if Values that, when set on the attribute, should be converted to `nil`.
|
78
|
+
# @option options [Regexp] :regex When `type` is `String` only values matching this regex will be allowed.
|
79
|
+
# Optional.
|
80
|
+
# @option options [String] :format When `type` is `DateTime` this specifies the format for parsing String and
|
81
|
+
# Fixnum instances assigned to this attribute. See `DateTime#strptime` for details.
|
82
|
+
#
|
83
|
+
# @macro [attach] attribute
|
84
|
+
# The $1 attribute.
|
85
|
+
# @return [$2]
|
86
|
+
def attribute(name, type = String, options = {})
|
87
|
+
define_attribute_reader name
|
88
|
+
define_attribute_writer name, type, options
|
89
|
+
|
90
|
+
(self.defined_attributes ||= {})[name] = options.merge type: type
|
91
|
+
end
|
92
|
+
|
93
|
+
# Creates a new Model instance from the specified source, which can take a variety of different forms.
|
94
|
+
#
|
95
|
+
# @param [nil, Hash, Model, Array] source The source object to create a new `Model` instance from. If `source` is
|
96
|
+
# `nil` then `nil` is returned. If `source` is a hash then a new `Model` instance is returned and the
|
97
|
+
# hash is passed to `Model#initialize`. If `source` is an instance of this class then `dup` is called on it
|
98
|
+
# and the duplicate returned. If source is an array then it is mapped into a new array with each item
|
99
|
+
# having been recursively passed through this method.
|
100
|
+
#
|
101
|
+
# @return [nil, Array, Model]
|
102
|
+
def from(source)
|
103
|
+
return nil if source.nil?
|
104
|
+
return new source if source.is_a? Hash
|
105
|
+
return source.dup if source.is_a? self
|
106
|
+
return source.map { |item| from item } if source.is_a? Array
|
107
|
+
|
108
|
+
raise ArgumentError, "Unable to make a #{self} from instance of #{source.class}"
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def define_attribute_reader(name)
|
114
|
+
define_method(name) { @attributes[name] }
|
115
|
+
end
|
116
|
+
|
117
|
+
def define_attribute_writer(name, type, options)
|
118
|
+
typecaster = typecaster_for type
|
119
|
+
|
120
|
+
define_method "#{name}=" do |value|
|
121
|
+
value = nil if Array(options.fetch(:nil_if, [])).include? value
|
122
|
+
|
123
|
+
value = typecaster.call(value, options)
|
124
|
+
|
125
|
+
allowed_values = options[:allowed_values]
|
126
|
+
if !value.nil? && allowed_values
|
127
|
+
raise ArgumentError, "#{self}##{name}: invalid value: #{value.inspect}" unless allowed_values.include? value
|
128
|
+
end
|
129
|
+
|
130
|
+
(@attributes ||= {})[name] = value
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def typecaster_for(type)
|
135
|
+
if [Boolean, String, Fixnum, Float, Symbol, DateTime].include? type
|
136
|
+
method "typecaster_#{type.to_s.gsub(/\AIGMarkets::/, '').downcase}"
|
137
|
+
elsif type.respond_to? :from
|
138
|
+
-> (value, _options) { type.from value }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def typecaster_boolean(value, _options)
|
143
|
+
return value if [nil, true, false].include? value
|
144
|
+
|
145
|
+
raise ArgumentError, "#{self}: invalid boolean value: #{value}"
|
146
|
+
end
|
147
|
+
|
148
|
+
def typecaster_string(value, options)
|
149
|
+
return nil if value.nil?
|
150
|
+
|
151
|
+
if options.key? :regex
|
152
|
+
raise ArgumentError, "#{self}: invalid string value: #{value}" unless options[:regex].match value.to_s
|
153
|
+
end
|
154
|
+
|
155
|
+
value.to_s
|
156
|
+
end
|
157
|
+
|
158
|
+
def typecaster_fixnum(value, _options)
|
159
|
+
value.nil? ? nil : value.to_s.to_i
|
160
|
+
end
|
161
|
+
|
162
|
+
def typecaster_float(value, _options)
|
163
|
+
value.nil? ? nil : Float(value)
|
164
|
+
end
|
165
|
+
|
166
|
+
def typecaster_symbol(value, _options)
|
167
|
+
value.nil? ? nil : value.to_s.downcase.to_sym
|
168
|
+
end
|
169
|
+
|
170
|
+
def typecaster_datetime(value, options)
|
171
|
+
raise ArgumentError, "#{self}: invalid or missing date time format" unless options[:format].is_a? String
|
172
|
+
|
173
|
+
if value.is_a?(String) || value.is_a?(Fixnum)
|
174
|
+
begin
|
175
|
+
DateTime.strptime(value.to_s, options[:format])
|
176
|
+
rescue ArgumentError
|
177
|
+
raise ArgumentError, "#{self}: failed parsing date '#{value}' with format '#{options[:format]}'"
|
178
|
+
end
|
179
|
+
else
|
180
|
+
value
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module IGMarkets
|
2
|
+
# Encrypts account passwords in the manner required for authentication with the IG Markets API.
|
3
|
+
class PasswordEncryptor
|
4
|
+
# @return [OpenSSL::PKey::RSA] The public key used by {#encrypt}, can also be set using {#encoded_public_key=}.
|
5
|
+
attr_accessor :public_key
|
6
|
+
|
7
|
+
# @return [String] The timestamp used by {#encrypt}.
|
8
|
+
attr_accessor :time_stamp
|
9
|
+
|
10
|
+
# Takes an encoded public key and calls {#public_key=} with the decoded key.
|
11
|
+
#
|
12
|
+
# @param [String] encoded_public_key The public key encoded in Base64.
|
13
|
+
def encoded_public_key=(encoded_public_key)
|
14
|
+
self.public_key = OpenSSL::PKey::RSA.new Base64.strict_decode64 encoded_public_key
|
15
|
+
end
|
16
|
+
|
17
|
+
# Encrypts a password using this encryptor's public key and time stamp, which must have been set prior to calling
|
18
|
+
# this method.
|
19
|
+
#
|
20
|
+
# @param [String] password The password to encrypt.
|
21
|
+
#
|
22
|
+
# @return [String] The encrypted password encoded in Base64.
|
23
|
+
def encrypt(password)
|
24
|
+
encoded_password = Base64.strict_encode64 "#{password}|#{time_stamp}"
|
25
|
+
|
26
|
+
encrypted_password = public_key.public_encrypt encoded_password
|
27
|
+
|
28
|
+
Base64.strict_encode64 encrypted_password
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module IGMarkets
|
2
|
+
# Contains methods for formatting payloads that can be passed to the IG Markets API.
|
3
|
+
module PayloadFormatter
|
4
|
+
module_function
|
5
|
+
|
6
|
+
# Takes a {Model} and returns its attributes in a hash ready to be passed as a payload to the IG Markets API.
|
7
|
+
# Attribute names will be converted to use camel case rather than snake case, `Symbol` attributes will be converted
|
8
|
+
# to strings and will be uppercased, and `DateTime` attributes will be converted to strings using their ':format'
|
9
|
+
# option.
|
10
|
+
#
|
11
|
+
# @param [Model] model The model instance to convert attributes for.
|
12
|
+
#
|
13
|
+
# @return [Hash] The resulting attributes hash.
|
14
|
+
def format(model)
|
15
|
+
model.class.defined_attributes.each_with_object({}) do |(name, options), formatted|
|
16
|
+
value = model.send name
|
17
|
+
|
18
|
+
next if value.nil?
|
19
|
+
|
20
|
+
value = value.to_s.upcase if options[:type] == Symbol
|
21
|
+
value = value.strftime(options.fetch(:format)) if options[:type] == DateTime
|
22
|
+
|
23
|
+
formatted[snake_case_to_camel_case(name)] = value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Takes a string or symbol that uses snake case and converts it to a camel case symbol.
|
28
|
+
#
|
29
|
+
# @param [String, Symbol] value The string or symbol to convert to camel case.
|
30
|
+
#
|
31
|
+
# @return [Symbol]
|
32
|
+
def snake_case_to_camel_case(value)
|
33
|
+
pieces = value.to_s.split '_'
|
34
|
+
|
35
|
+
(pieces[0] + pieces[1..-1].map(&:capitalize).join).to_sym
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
module IGMarkets
|
2
|
+
# Contains details on a trading position. Returned by {DealingPlatform::PositionMethods#all} and
|
3
|
+
# {DealingPlatform::PositionMethods#[]}.
|
4
|
+
class Position < Model
|
5
|
+
attribute :contract_size, Float
|
6
|
+
attribute :controlled_risk, Boolean
|
7
|
+
attribute :created_date, DateTime, format: '%Y/%m/%d %H:%M:%S:%L'
|
8
|
+
attribute :created_date_utc, DateTime, format: '%Y-%m-%dT%H:%M:%S'
|
9
|
+
attribute :currency, String, regex: Regex::CURRENCY
|
10
|
+
attribute :deal_id
|
11
|
+
attribute :direction, Symbol, allowed_values: [:buy, :sell]
|
12
|
+
attribute :level, Float
|
13
|
+
attribute :limit_level, Float
|
14
|
+
attribute :size, Fixnum
|
15
|
+
attribute :stop_level, Float
|
16
|
+
attribute :trailing_step, Float
|
17
|
+
attribute :trailing_stop_distance, Fixnum
|
18
|
+
|
19
|
+
attribute :market, MarketOverview
|
20
|
+
|
21
|
+
# Returns whether this position has a trailing stop.
|
22
|
+
def trailing_stop?
|
23
|
+
!trailing_step.nil? && !trailing_stop_distance.nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the favorable difference in the price between {#level} and the current market price as stored in
|
27
|
+
# {#market}. If {#direction} is `:buy` and the market has since risen then this method will return a positive value,
|
28
|
+
# but if {#direction} is `:sell` and the market has since risen then this method will return a negative value.
|
29
|
+
#
|
30
|
+
# @return [Float]
|
31
|
+
def price_delta
|
32
|
+
if direction == :buy
|
33
|
+
market.bid - level
|
34
|
+
elsif direction == :sell
|
35
|
+
level - market.offer
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns whether this position is currently profitable based on the current market state as stored in {#market}.
|
40
|
+
def profitable?
|
41
|
+
price_delta > 0.0
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns this position's current profit or loss, denominated in its {#currency}, and based on the current market
|
45
|
+
# state as stored in {#market}. {#formatted_profit_loss} can be used to get a human-readable representation of this
|
46
|
+
# value.
|
47
|
+
#
|
48
|
+
# @return [Float]
|
49
|
+
def profit_loss
|
50
|
+
price_delta * size * market.lot_size * market.scaling_factor
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns a human-readable string describing this position's current profit or loss, denominated in its {#currency},
|
54
|
+
# and based on the current market state as stored in {#market}. Some examples of the format of the return value:
|
55
|
+
#
|
56
|
+
# - `"USD -130.40"`
|
57
|
+
# - `"AUD 539.10"`
|
58
|
+
# - `"JPY 3560"`
|
59
|
+
#
|
60
|
+
# @return [String]
|
61
|
+
def formatted_profit_loss
|
62
|
+
format_string = (currency == 'JPY' ? '%.0f' : '%.2f')
|
63
|
+
"#{currency} #{format format_string, profit_loss}"
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns this position's {#size} as a string prefixed with a `+` if {#direction} is `:buy`, or a `-` if
|
67
|
+
# {#direction} is `:sell`.
|
68
|
+
#
|
69
|
+
# @return [String]
|
70
|
+
def formatted_size
|
71
|
+
"#{{ buy: '+', sell: '-' }.fetch(direction)}#{size}"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Closes this position. If called with no options then this position will be fully closed at current market prices,
|
75
|
+
# partial closes and greater control over the close conditions can be achieved with the relevant options.
|
76
|
+
#
|
77
|
+
# @param [Hash] options The options for the position close.
|
78
|
+
# @option options [Float] :level Required if and only if `:order_type` is `:limit` or `:quote`.
|
79
|
+
# @option options [:limit, :market, :quote] :order_type The order type. `:market` indicates to fill the order at
|
80
|
+
# current market level(s). `:limit` indicates to fill at the price specified by `:level` (or a more
|
81
|
+
# favorable one). `:quote` is only permitted following agreement with IG Markets. Defaults to
|
82
|
+
# `:market`.
|
83
|
+
# @option options [String] :quote_id The Lightstreamer quote ID. Required when `:order_type` is `:quote`.
|
84
|
+
# @option options [Float] :size The size of the position to close. Defaults to {#size} which will close the entire
|
85
|
+
# position, or alternatively specify a smaller value to partially close this position.
|
86
|
+
# @option options [:execute_and_eliminate, :fill_or_kill] :time_in_force The order fill strategy.
|
87
|
+
# `:execute_and_eliminate` will fill this order as much as possible within the constraints set by
|
88
|
+
# `:order_type`, `:level` and `:quote_id`. `:fill_or_kill` will try to fill this entire order within
|
89
|
+
# the constraints, however if this is not possible then the order will not be filled at all. If
|
90
|
+
# `:order_type` is `:market` (the default) then `:time_in_force` will be automatically set to
|
91
|
+
# `:execute_and_eliminate`.
|
92
|
+
#
|
93
|
+
# @return [String] The resulting deal reference, use {DealingPlatform#deal_confirmation} to check the result of
|
94
|
+
# the position close.
|
95
|
+
def close(options = {})
|
96
|
+
options[:deal_id] = deal_id
|
97
|
+
options[:direction] = { buy: :sell, sell: :buy }.fetch(direction)
|
98
|
+
options[:size] ||= size
|
99
|
+
|
100
|
+
model = PositionCloseAttributes.build options
|
101
|
+
model.validate
|
102
|
+
|
103
|
+
payload = PayloadFormatter.format model
|
104
|
+
|
105
|
+
@dealing_platform.session.delete('positions/otc', payload, API_V1).fetch(:deal_reference)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Updates this position. No attributes are mandatory, and any attributes not specified will be kept at their
|
109
|
+
# current value.
|
110
|
+
#
|
111
|
+
# @param [Hash] new_attributes The attributes of this position to update.
|
112
|
+
# @option new_attributes [Float] :limit_level The new limit level for this position.
|
113
|
+
# @option new_attributes [Float] :stop_level The new stop level for this position.
|
114
|
+
# @option new_attributes [Boolean] :trailing_stop Whether to use a trailing stop for this position.
|
115
|
+
# @option new_attributes [Fixnum] :trailing_stop_distance The distance away in pips to place the trailing stop.
|
116
|
+
# @option new_attributes [Fixnum] :trailing_stop_increment The step increment to use for the trailing stop.
|
117
|
+
#
|
118
|
+
# @return [String] The deal reference of the update operation. Use {DealingPlatform#deal_confirmation} to check
|
119
|
+
# the result of the position update.
|
120
|
+
def update(new_attributes)
|
121
|
+
new_attributes = { limit_level: limit_level, stop_level: stop_level, trailing_stop: trailing_stop?,
|
122
|
+
trailing_stop_distance: trailing_stop_distance, trailing_stop_increment: trailing_step
|
123
|
+
}.merge new_attributes
|
124
|
+
|
125
|
+
unless new_attributes[:trailing_stop]
|
126
|
+
new_attributes[:trailing_stop_distance] = new_attributes[:trailing_stop_increment] = nil
|
127
|
+
end
|
128
|
+
|
129
|
+
payload = PayloadFormatter.format PositionUpdateAttributes.new new_attributes
|
130
|
+
|
131
|
+
@dealing_platform.session.put("positions/otc/#{deal_id}", payload, API_V2).fetch(:deal_reference)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Validates the internal consistency of the `:order_type`, `:quote_id` and `:level` attributes.
|
135
|
+
#
|
136
|
+
# @param [Hash] attributes The attributes hash to validate.
|
137
|
+
def self.validate_order_type_constraints(attributes)
|
138
|
+
if (attributes[:order_type] == :quote) == attributes[:quote_id].nil?
|
139
|
+
raise ArgumentError, 'set quote_id if and only if order_type is :quote'
|
140
|
+
end
|
141
|
+
|
142
|
+
if [:limit, :quote].include?(attributes[:order_type]) == attributes[:level].nil?
|
143
|
+
raise ArgumentError, 'set level if and only if order_type is :limit or :quote'
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Internal model used by {#close}.
|
148
|
+
class PositionCloseAttributes < Model
|
149
|
+
attribute :deal_id
|
150
|
+
attribute :direction, Symbol, allowed_values: [:buy, :sell]
|
151
|
+
attribute :level, Float
|
152
|
+
attribute :order_type, Symbol, allowed_values: [:limit, :market, :quote]
|
153
|
+
attribute :quote_id
|
154
|
+
attribute :size, Fixnum
|
155
|
+
attribute :time_in_force, Symbol, allowed_values: [:execute_and_eliminate, :fill_or_kill]
|
156
|
+
|
157
|
+
# Runs a series of validations on this model's attributes to check whether it is ready to be sent to the IG
|
158
|
+
# Markets API.
|
159
|
+
def validate
|
160
|
+
[:deal_id, :direction, :order_type, :size, :time_in_force].each do |attribute|
|
161
|
+
raise ArgumentError, "#{attribute} attribute must be set" if attributes[attribute].nil?
|
162
|
+
end
|
163
|
+
|
164
|
+
Position.validate_order_type_constraints attributes
|
165
|
+
end
|
166
|
+
|
167
|
+
# Builds a new {PositionCloseAttributes} instance with the given attributes and applies relevant defaults.
|
168
|
+
#
|
169
|
+
# @param [Hash] attributes
|
170
|
+
#
|
171
|
+
# @return [PositionCloseAttributes]
|
172
|
+
def self.build(attributes)
|
173
|
+
new(attributes).tap do |model|
|
174
|
+
model.order_type ||= :market
|
175
|
+
model.time_in_force = :execute_and_eliminate if model.order_type == :market
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Internal model used by {#update}.
|
181
|
+
class PositionUpdateAttributes < Model
|
182
|
+
attribute :limit_level, Float
|
183
|
+
attribute :stop_level, Float
|
184
|
+
attribute :trailing_stop, Boolean
|
185
|
+
attribute :trailing_stop_distance, Fixnum
|
186
|
+
attribute :trailing_stop_increment, Fixnum
|
187
|
+
end
|
188
|
+
|
189
|
+
private_constant :PositionCloseAttributes, :PositionUpdateAttributes
|
190
|
+
end
|
191
|
+
end
|