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.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +15 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +2 -0
  6. data/.travis.yml +10 -0
  7. data/.yardopts +4 -0
  8. data/Gemfile +2 -0
  9. data/LICENSE.md +25 -0
  10. data/README.md +134 -0
  11. data/ig_markets.gemspec +28 -0
  12. data/lib/ig_markets.rb +42 -0
  13. data/lib/ig_markets/account.rb +23 -0
  14. data/lib/ig_markets/account_activity.rb +24 -0
  15. data/lib/ig_markets/account_transaction.rb +49 -0
  16. data/lib/ig_markets/api_versions.rb +10 -0
  17. data/lib/ig_markets/application.rb +22 -0
  18. data/lib/ig_markets/boolean.rb +5 -0
  19. data/lib/ig_markets/client_sentiment.rb +16 -0
  20. data/lib/ig_markets/deal_confirmation.rb +41 -0
  21. data/lib/ig_markets/dealing_platform.rb +105 -0
  22. data/lib/ig_markets/dealing_platform/account_methods.rb +92 -0
  23. data/lib/ig_markets/dealing_platform/client_sentiment_methods.rb +26 -0
  24. data/lib/ig_markets/dealing_platform/market_methods.rb +59 -0
  25. data/lib/ig_markets/dealing_platform/position_methods.rb +164 -0
  26. data/lib/ig_markets/dealing_platform/sprint_market_position_methods.rb +46 -0
  27. data/lib/ig_markets/dealing_platform/watchlist_methods.rb +42 -0
  28. data/lib/ig_markets/dealing_platform/working_order_methods.rb +115 -0
  29. data/lib/ig_markets/historical_price_result.rb +33 -0
  30. data/lib/ig_markets/instrument.rb +89 -0
  31. data/lib/ig_markets/market.rb +99 -0
  32. data/lib/ig_markets/market_hierarchy_result.rb +13 -0
  33. data/lib/ig_markets/market_overview.rb +24 -0
  34. data/lib/ig_markets/model.rb +185 -0
  35. data/lib/ig_markets/password_encryptor.rb +31 -0
  36. data/lib/ig_markets/payload_formatter.rb +38 -0
  37. data/lib/ig_markets/position.rb +191 -0
  38. data/lib/ig_markets/regex.rb +10 -0
  39. data/lib/ig_markets/request_failed_error.rb +21 -0
  40. data/lib/ig_markets/response_parser.rb +35 -0
  41. data/lib/ig_markets/session.rb +186 -0
  42. data/lib/ig_markets/sprint_market_position.rb +17 -0
  43. data/lib/ig_markets/version.rb +4 -0
  44. data/lib/ig_markets/watchlist.rb +37 -0
  45. data/lib/ig_markets/working_order.rb +68 -0
  46. data/spec/factories/ig_markets/account.rb +14 -0
  47. data/spec/factories/ig_markets/account_activity.rb +21 -0
  48. data/spec/factories/ig_markets/account_balance.rb +8 -0
  49. data/spec/factories/ig_markets/account_transaction.rb +15 -0
  50. data/spec/factories/ig_markets/application.rb +21 -0
  51. data/spec/factories/ig_markets/client_sentiment.rb +7 -0
  52. data/spec/factories/ig_markets/deal_confirmation.rb +20 -0
  53. data/spec/factories/ig_markets/historical_price_result.rb +7 -0
  54. data/spec/factories/ig_markets/historical_price_result_data_allowance.rb +7 -0
  55. data/spec/factories/ig_markets/historical_price_result_price.rb +7 -0
  56. data/spec/factories/ig_markets/historical_price_result_snapshot.rb +10 -0
  57. data/spec/factories/ig_markets/instrument.rb +32 -0
  58. data/spec/factories/ig_markets/instrument_currency.rb +9 -0
  59. data/spec/factories/ig_markets/instrument_expiry_details.rb +6 -0
  60. data/spec/factories/ig_markets/instrument_margin_deposit_band.rb +8 -0
  61. data/spec/factories/ig_markets/instrument_opening_hours.rb +6 -0
  62. data/spec/factories/ig_markets/instrument_rollover_details.rb +6 -0
  63. data/spec/factories/ig_markets/instrument_slippage_factor.rb +6 -0
  64. data/spec/factories/ig_markets/market.rb +7 -0
  65. data/spec/factories/ig_markets/market_dealing_rules.rb +11 -0
  66. data/spec/factories/ig_markets/market_dealing_rules_rule_details.rb +6 -0
  67. data/spec/factories/ig_markets/market_hierarchy_result.rb +6 -0
  68. data/spec/factories/ig_markets/market_hierarchy_result_hierarchy_node.rb +6 -0
  69. data/spec/factories/ig_markets/market_overview.rb +22 -0
  70. data/spec/factories/ig_markets/market_snapshot.rb +17 -0
  71. data/spec/factories/ig_markets/position.rb +19 -0
  72. data/spec/factories/ig_markets/sprint_market_position.rb +16 -0
  73. data/spec/factories/ig_markets/watchlist.rb +9 -0
  74. data/spec/factories/ig_markets/working_order.rb +21 -0
  75. data/spec/ig_markets/account_transaction_spec.rb +30 -0
  76. data/spec/ig_markets/dealing_platform/account_methods_spec.rb +58 -0
  77. data/spec/ig_markets/dealing_platform/client_sentiment_methods_spec.rb +29 -0
  78. data/spec/ig_markets/dealing_platform/market_methods_spec.rb +80 -0
  79. data/spec/ig_markets/dealing_platform/position_methods_spec.rb +137 -0
  80. data/spec/ig_markets/dealing_platform/sprint_market_position_methods_spec.rb +39 -0
  81. data/spec/ig_markets/dealing_platform/watchlist_methods_spec.rb +89 -0
  82. data/spec/ig_markets/dealing_platform/working_order_methods_spec.rb +120 -0
  83. data/spec/ig_markets/dealing_platform_spec.rb +40 -0
  84. data/spec/ig_markets/model_spec.rb +127 -0
  85. data/spec/ig_markets/password_encryptor_spec.rb +23 -0
  86. data/spec/ig_markets/payload_formatter_spec.rb +19 -0
  87. data/spec/ig_markets/position_spec.rb +37 -0
  88. data/spec/ig_markets/response_parser_spec.rb +13 -0
  89. data/spec/ig_markets/session_spec.rb +134 -0
  90. data/spec/spec_helper.rb +14 -0
  91. data/spec/support/factory_girl.rb +7 -0
  92. data/spec/support/random_test_order.rb +3 -0
  93. 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