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,16 @@
1
+ module IGMarkets
2
+ # Contains details on client sentiment for a single market. Returned by {DealingPlatform::ClientSentimentMethods#[]}
3
+ # and {#related_sentiments}.
4
+ class ClientSentiment < Model
5
+ attribute :long_position_percentage, Float
6
+ attribute :market_id
7
+ attribute :short_position_percentage, Float
8
+
9
+ # Returns client sentiments for markets that are related to this one.
10
+ #
11
+ # @return [Array<ClientSentiment>]
12
+ def related_sentiments
13
+ @dealing_platform.gather "clientsentiment/related/#{market_id}", :client_sentiments, ClientSentiment
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,41 @@
1
+ module IGMarkets
2
+ # Contains details on a single dealing event. Returned by {DealingPlatform#deal_confirmation}.
3
+ class DealConfirmation < Model
4
+ # Contains details on a specific deal that was affected by a dealing event. Returned by {#affected_deals}.
5
+ class AffectedDeal < Model
6
+ attribute :deal_id
7
+ attribute :status, Symbol, allowed_values: [:amended, :deleted, :fully_closed, :opened, :partially_closed]
8
+ end
9
+
10
+ attribute :affected_deals, AffectedDeal
11
+ attribute :deal_id
12
+ attribute :deal_reference
13
+ attribute :deal_status, Symbol, allowed_values: [:accepted, :fund_account, :rejected]
14
+ attribute :direction, Symbol, allowed_values: [:buy, :sell]
15
+ attribute :epic
16
+ attribute :expiry, DateTime, nil_if: '-', format: '%d-%b-%y'
17
+ attribute :guaranteed_stop, Boolean
18
+ attribute :level, Float
19
+ attribute :limit_distance, Float
20
+ attribute :limit_level, Float
21
+ attribute :reason, Symbol, allowed_values: [
22
+ :account_not_enabled_to_trading, :attached_order_level_error, :attached_order_trailing_stop_error,
23
+ :cannot_change_stop_type, :cannot_remove_stop, :closing_only_trades_accepted_on_this_market, :conflicting_order,
24
+ :cr_spacing, :duplicate_order_error, :exchange_manual_override, :finance_repeat_dealing,
25
+ :force_open_on_same_market_different_currency, :general_error, :good_till_date_in_the_past, :instrument_not_found,
26
+ :insufficient_funds, :level_tolerance_error, :manual_order_timeout, :market_closed, :market_closed_with_edits,
27
+ :market_closing, :market_not_borrowable, :market_offline, :market_phone_only, :market_rolled,
28
+ :market_unavailable_to_client, :max_auto_size_exceeded, :minimum_order_size_error, :move_away_only_limit,
29
+ :move_away_only_stop, :move_away_only_trigger_level, :opposing_direction_orders_not_allowed,
30
+ :opposing_positions_not_allowed, :order_locked, :order_not_found, :over_normal_market_size,
31
+ :partially_closed_position_not_deleted, :position_not_available_to_close, :position_not_found,
32
+ :reject_spreadbet_order_on_cfd_account, :size_increment, :sprint_market_expiry_after_market_close,
33
+ :stop_or_limit_not_allowed, :stop_required_error, :strike_level_tolerance, :success, :trailing_stop_not_allowed,
34
+ :unknown, :wrong_side_of_market]
35
+ attribute :size, Fixnum
36
+ attribute :status, Symbol, allowed_values: [:amended, :closed, :deleted, :open, :partially_closed]
37
+ attribute :stop_distance, Float
38
+ attribute :stop_level, Float
39
+ attribute :trailing_stop, Boolean
40
+ end
41
+ end
@@ -0,0 +1,105 @@
1
+ module IGMarkets
2
+ # This is the primary class for interacting with the IG Markets API. After signing in using {#sign_in} most
3
+ # functionality is accessed via the following top-level methods:
4
+ #
5
+ # - {#account}
6
+ # - {#client_sentiment}
7
+ # - {#markets}
8
+ # - {#positions}
9
+ # - {#sprint_market_positions}
10
+ # - {#watchlists}
11
+ # - {#working_orders}
12
+ #
13
+ # See `README.md` for examples.
14
+ #
15
+ # If any errors occur while executing requests to the IG Markets API then {RequestFailedError} will be raised.
16
+ class DealingPlatform
17
+ # @return [Session] The session used by this dealing platform.
18
+ attr_reader :session
19
+
20
+ # @return [AccountMethods] Methods for working with the logged in account.
21
+ attr_reader :account
22
+
23
+ # @return [ClientSentimentMethods] Methods for working with client sentiment.
24
+ attr_reader :client_sentiment
25
+
26
+ # @return [MarketMethods] Methods for working with markets.
27
+ attr_reader :markets
28
+
29
+ # @return [PositionMethods] Methods for working with positions.
30
+ attr_reader :positions
31
+
32
+ # @return [SprintMarketPositionMethods] Methods for working with sprint market positions.
33
+ attr_reader :sprint_market_positions
34
+
35
+ # @return [WatchlistMethods] Methods for working with watchlists.
36
+ attr_reader :watchlists
37
+
38
+ # @return [WorkingOrderMethods] Methods for working with working orders.
39
+ attr_reader :working_orders
40
+
41
+ def initialize
42
+ @session = Session.new
43
+
44
+ @account = AccountMethods.new self
45
+ @client_sentiment = ClientSentimentMethods.new self
46
+ @markets = MarketMethods.new self
47
+ @positions = PositionMethods.new self
48
+ @sprint_market_positions = SprintMarketPositionMethods.new self
49
+ @watchlists = WatchlistMethods.new self
50
+ @working_orders = WorkingOrderMethods.new self
51
+ end
52
+
53
+ # Signs in to the IG Markets Dealing Platform, either the production platform or the demo platform.
54
+ #
55
+ # @param [String] username The account username.
56
+ # @param [String] password The account password.
57
+ # @param [String] api_key The account API key.
58
+ # @param [:production, :demo] platform The platform to use.
59
+ def sign_in(username, password, api_key, platform)
60
+ session.username = username
61
+ session.password = password
62
+ session.api_key = api_key
63
+ session.platform = platform
64
+
65
+ session.sign_in
66
+ end
67
+
68
+ # Signs out of the IG Markets Dealing Platform, ending any current session.
69
+ def sign_out
70
+ session.sign_out
71
+ end
72
+
73
+ # Returns a full deal confirmation for the specified deal reference.
74
+ #
75
+ # @return [DealConfirmation]
76
+ def deal_confirmation(deal_reference)
77
+ DealConfirmation.from session.get "confirms/#{deal_reference}", API_V1
78
+ end
79
+
80
+ # Returns details on the IG Markets applications for the accounts associated with this login.
81
+ #
82
+ # @return [Array<Application>]
83
+ def applications
84
+ Application.from session.get 'operations/application', API_V1
85
+ end
86
+
87
+ # Sends a GET request to a URL then takes a single key from the returned hash and converts its contents to an array
88
+ # of type `klass`.
89
+ #
90
+ # @param [String] url The URL to send a GET request to.
91
+ # @param [Symbol] collection The name of the top level symbol that contains the array of data to return.
92
+ # @param [Class] klass The type to return.
93
+ # @param [API_V1, API_V2, API_V3] api_version The API version to target for the request.
94
+ #
95
+ # @return [Array]
96
+ def gather(url, collection, klass, api_version = API_V1)
97
+ klass.from(session.get(url, api_version).fetch(collection)).tap do |result|
98
+ # Set @dealing_platform on all the results
99
+ result.each do |item|
100
+ item.instance_variable_set :@dealing_platform, self
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,92 @@
1
+ module IGMarkets
2
+ class DealingPlatform
3
+ # Provides methods for working with the logged in account. Returned by {DealingPlatform#account}.
4
+ class AccountMethods
5
+ # Initializes this helper class with the specified dealing platform.
6
+ #
7
+ # @param [DealingPlatform] dealing_platform The dealing platform.
8
+ def initialize(dealing_platform)
9
+ @dealing_platform = dealing_platform
10
+ end
11
+
12
+ # Returns all accounts associated with the current IG Markets login.
13
+ #
14
+ # @return [Array<Account>]
15
+ def all
16
+ @dealing_platform.gather 'accounts', :accounts, Account
17
+ end
18
+
19
+ # Returns all account activities that occurred in the specified date range.
20
+ #
21
+ # @param [Date] from_date The start date of the desired date range.
22
+ # @param [Date] to_date The end date of the desired date range.
23
+ #
24
+ # @return [Array<AccountActivity>]
25
+ def activities_in_date_range(from_date, to_date)
26
+ from_date = format_date from_date
27
+ to_date = format_date to_date
28
+
29
+ @dealing_platform.gather "history/activity/#{from_date}/#{to_date}", :activities, AccountActivity
30
+ end
31
+
32
+ # Returns all account activities that occurred in the most recent specified number of seconds.
33
+ #
34
+ # @param [Integer, Float] seconds The number of seconds to return recent activities for.
35
+ #
36
+ # @return [Array<AccountActivity>]
37
+ def recent_activities(seconds)
38
+ @dealing_platform.gather "history/activity/#{(seconds * 1000.0).to_i}", :activities, AccountActivity
39
+ end
40
+
41
+ # Returns all transactions that occurred in the specified date range.
42
+ #
43
+ # @param [Date] from_date The start date of the desired date range.
44
+ # @param [Date] to_date The end date of the desired date range.
45
+ # @param [:all, :all_deal, :deposit, :withdrawal] transaction_type The type of transactions to return.
46
+ #
47
+ # @return [Array<AccountTransaction>]
48
+ def transactions_in_date_range(from_date, to_date, transaction_type = :all)
49
+ validate_transaction_type transaction_type
50
+
51
+ from_date = format_date from_date
52
+ to_date = format_date to_date
53
+
54
+ url = "history/transactions/#{transaction_type.to_s.upcase}/#{from_date}/#{to_date}"
55
+
56
+ @dealing_platform.gather url, :transactions, AccountTransaction
57
+ end
58
+
59
+ # Returns all transactions that occurred in the last specified number of seconds.
60
+ #
61
+ # @param [Integer, Float] seconds The number of seconds to return recent transactions for.
62
+ # @param [:all, :all_deal, :deposit, :withdrawal] transaction_type The type of transactions to return.
63
+ #
64
+ # @return [Array<AccountTransaction>]
65
+ def recent_transactions(seconds, transaction_type = :all)
66
+ validate_transaction_type transaction_type
67
+
68
+ url = "history/transactions/#{transaction_type.to_s.upcase}/#{(seconds * 1000.0).to_i}"
69
+
70
+ @dealing_platform.gather url, :transactions, AccountTransaction
71
+ end
72
+
73
+ private
74
+
75
+ # Validates whether the passed argument is a valid transaction type.
76
+ #
77
+ # @param [Symbol] type The candidate transaction type to validate.
78
+ def validate_transaction_type(type)
79
+ raise ArgumentError, 'transaction type is invalid' unless [:all, :all_deal, :deposit, :withdrawal].include? type
80
+ end
81
+
82
+ # Formats the passed `Date` as a string in the manner needed for building IG Markets URLs.
83
+ #
84
+ # @param [Date] date The date to format.
85
+ #
86
+ # @return [String]
87
+ def format_date(date)
88
+ date.strftime '%d-%m-%Y'
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,26 @@
1
+ module IGMarkets
2
+ class DealingPlatform
3
+ # Provides methods for working with client sentiment. Returned by {DealingPlatform#client_sentiment}.
4
+ class ClientSentimentMethods
5
+ # Initializes this helper class with the specified dealing platform.
6
+ #
7
+ # @param [DealingPlatform] dealing_platform The dealing platform.
8
+ def initialize(dealing_platform)
9
+ @dealing_platform = dealing_platform
10
+ end
11
+
12
+ # Returns the client sentiment for a market.
13
+ #
14
+ # @param [String] market_id The ID of the market to return client sentiment for.
15
+ #
16
+ # @return [ClientSentiment]
17
+ def [](market_id)
18
+ result = @dealing_platform.session.get "clientsentiment/#{market_id}", API_V1
19
+
20
+ ClientSentiment.from(result).tap do |client_sentiment|
21
+ client_sentiment.instance_variable_set :@dealing_platform, @dealing_platform
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,59 @@
1
+ module IGMarkets
2
+ class DealingPlatform
3
+ # Provides methods for working with markets. Returned by {DealingPlatform#markets}.
4
+ class MarketMethods
5
+ # Initializes this helper class with the specified dealing platform.
6
+ #
7
+ # @param [DealingPlatform] dealing_platform The dealing platform.
8
+ def initialize(dealing_platform)
9
+ @dealing_platform = dealing_platform
10
+ end
11
+
12
+ # Returns details on the market hierarchy directly under the specified node.
13
+ #
14
+ # @param [String] node_id The ID of the node to return the market hierarchy for. If `nil` then details on the root
15
+ # node of the hierarchy will be returned.
16
+ #
17
+ # @return [MarketHierarchyResult]
18
+ def hierarchy(node_id = nil)
19
+ url = ['marketnavigation', node_id].compact.join '/'
20
+
21
+ MarketHierarchyResult.from @dealing_platform.session.get(url, API_V1)
22
+ end
23
+
24
+ # Returns details for the markets with the passed EPICs.
25
+ #
26
+ # @param [Array<String>] epics The EPICs of the markets to return details for.
27
+ #
28
+ # @return [Array<Market>]
29
+ def find(*epics)
30
+ raise ArgumentError, 'at least one EPIC must be specified' if epics.empty?
31
+
32
+ epics.each do |epic|
33
+ raise ArgumentError, "invalid EPIC: #{epic}" unless epic.to_s =~ Regex::EPIC
34
+ end
35
+
36
+ @dealing_platform.gather "markets?epics=#{epics.join(',')}", :market_details, Market, API_V2
37
+ end
38
+
39
+ # Searches markets using a search term and returns an array of results.
40
+ #
41
+ # @param [String] search_term The search term to use.
42
+ #
43
+ # @return [Array<MarketOverview>]
44
+ def search(search_term)
45
+ @dealing_platform.gather "markets?searchTerm=#{search_term}", :markets, MarketOverview
46
+ end
47
+
48
+ # Returns market details for the market with the specified EPIC, or `nil` if there is no market with that EPIC.
49
+ # Internally a call to {#find} is made.
50
+ #
51
+ # @param [String] epic The EPIC of the market to return details for.
52
+ #
53
+ # @return [Market]
54
+ def [](epic)
55
+ find(epic)[0]
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,164 @@
1
+ module IGMarkets
2
+ class DealingPlatform
3
+ # Provides methods for working with positions. Returned by {DealingPlatform#positions}.
4
+ class PositionMethods
5
+ # Initializes this helper class with the specified dealing platform.
6
+ #
7
+ # @param [DealingPlatform] dealing_platform The dealing platform.
8
+ def initialize(dealing_platform)
9
+ @dealing_platform = dealing_platform
10
+ end
11
+
12
+ # Returns all positions.
13
+ #
14
+ # @return [Array<Position>]
15
+ def all
16
+ @dealing_platform.session.get('positions', API_V2).fetch(:positions).map do |attributes|
17
+ position_from_attributes attributes
18
+ end
19
+ end
20
+
21
+ # Returns the position with the specified deal ID, or `nil` if there is no position with that ID.
22
+ #
23
+ # @param [String] deal_id The deal ID of the working order to return.
24
+ #
25
+ # @return [Position]
26
+ def [](deal_id)
27
+ attributes = @dealing_platform.session.get "positions/#{deal_id}", API_V2
28
+
29
+ position_from_attributes attributes
30
+ end
31
+
32
+ # Creates a new position.
33
+ #
34
+ # @param [Hash] attributes The attributes for the new position.
35
+ # @option attributes [String] :currency_code The 3 character currency code, must be one of the instrument's
36
+ # currencies (see {Instrument#currencies}). Required.
37
+ # @option attributes [:buy, :sell] :direction The position direction. Required.
38
+ # @option attributes [String] :epic The EPIC of the instrument to create a position for. Required.
39
+ # @option attributes [DateTime] :expiry The expiry date of the instrument, if it has one. Optional.
40
+ # @option attributes [Boolean] :force_open Whether a force open is required. Defaults to `false`.
41
+ # @option attributes [Boolean] :guaranteed_stop Whether a guaranteed stop is required. Defaults to `false`.
42
+ # @option attributes [Float] :level Required if and only if `:order_type` is `:limit` or `:quote`.
43
+ # @option attributes [Fixnum] :limit_distance The distance away in pips to place the limit. If this is set then
44
+ # `:limit_level` must be `nil`. Optional.
45
+ # @option attributes [Float] :limit_level The limit level. If this is set then `:limit_distance` must be `nil`.
46
+ # Optional.
47
+ # @option attributes [:limit, :market, :quote] :order_type The order type. `:market` indicates to fill the order
48
+ # at current market level(s). `:limit` indicates to fill at the price specified by `:level`
49
+ # (or a more favorable one). `:quote` is only permitted following agreement with IG Markets.
50
+ # Defaults to `:market`.
51
+ # @option attributes [String] :quote_id The Lightstreamer quote ID. Required when `:order_type` is `:quote`.
52
+ # @option attributes [Float] :size The size of the position to create. Required.
53
+ # @option attributes [Fixnum] :stop_distance The distance away in pips to place the stop. If this is set then
54
+ # `:stop_level` must be `nil`. Optional.
55
+ # @option attributes [Float] :stop_level The stop level. If this is set then `:stop_distance` must be `nil`.
56
+ # Optional.
57
+ # @option attributes [:execute_and_eliminate, :fill_or_kill] :time_in_force The order fill strategy.
58
+ # `:execute_and_eliminate` will fill this order as much as possible within the constraints set
59
+ # by `:order_type`, `:level` and `:quote_id`, which may result in only part of the requested
60
+ # order being filled. `:fill_or_kill` will try to fill the whole order within the constraints,
61
+ # however if this is not possible then the order will not be filled at all. If `:order_type` is
62
+ # `:market` (the default) then `:time_in_force` will be automatically set to
63
+ # `:execute_and_eliminate`.
64
+ # @option attributes [Boolean] :trailing_stop Whether to use a trailing stop. Defaults to false. Optional.
65
+ # @option attributes [Fixnum] :trailing_stop_increment The increment step in pips for the trailing stop. Required
66
+ # when `:trailing_stop` is `true`.
67
+ #
68
+ # @return [String] The resulting deal reference, use {DealingPlatform#deal_confirmation} to check the result of
69
+ # the position creation.
70
+ def create(attributes)
71
+ attributes[:force_open] = false unless attributes.key? :force_open
72
+ attributes[:guaranteed_stop] = false unless attributes.key? :guaranteed_stop
73
+ attributes[:order_type] ||= :market
74
+ attributes[:time_in_force] = :execute_and_eliminate if attributes[:order_type] == :market
75
+
76
+ model = PositionCreateAttributes.new attributes
77
+ model.validate
78
+
79
+ payload = PayloadFormatter.format model
80
+ payload[:expiry] ||= '-'
81
+
82
+ @dealing_platform.session.post('positions/otc', payload, API_V2).fetch(:deal_reference)
83
+ end
84
+
85
+ private
86
+
87
+ # Internal model used by {#create}
88
+ class PositionCreateAttributes < Model
89
+ attribute :currency_code, String, regex: Regex::CURRENCY
90
+ attribute :direction, Symbol, allowed_values: [:buy, :sell]
91
+ attribute :epic, String, regex: Regex::EPIC
92
+ attribute :expiry, DateTime, format: '%d-%b-%y'
93
+ attribute :force_open, Boolean
94
+ attribute :guaranteed_stop, Boolean
95
+ attribute :level, Float
96
+ attribute :limit_distance, Fixnum
97
+ attribute :limit_level, Float
98
+ attribute :order_type, Symbol, allowed_values: [:limit, :market, :quote]
99
+ attribute :quote_id
100
+ attribute :size, Fixnum
101
+ attribute :stop_distance, Fixnum
102
+ attribute :stop_level, Float
103
+ attribute :time_in_force, Symbol, allowed_values: [:execute_and_eliminate, :fill_or_kill]
104
+ attribute :trailing_stop, Boolean
105
+ attribute :trailing_stop_increment, Fixnum
106
+
107
+ # Runs a series of validations on this model's attributes to check whether it is ready to be sent to the IG
108
+ # Markets API.
109
+ def validate
110
+ validate_required_attributes_present
111
+ Position.validate_order_type_constraints attributes
112
+ validate_trailing_stop_constraints
113
+ validate_stop_and_limit_constraints
114
+ validate_guaranteed_stop_constraints
115
+ end
116
+
117
+ private
118
+
119
+ # Checks that all required attributes for position creation are present.
120
+ def validate_required_attributes_present
121
+ required = [:currency_code, :direction, :epic, :force_open, :guaranteed_stop, :order_type, :size,
122
+ :time_in_force]
123
+
124
+ required.each do |attribute|
125
+ raise ArgumentError, "#{attribute} attribute must be set" if attributes[attribute].nil?
126
+ end
127
+ end
128
+
129
+ # Checks that attributes associated with the trailing stops are valid.
130
+ def validate_trailing_stop_constraints
131
+ if trailing_stop
132
+ raise ArgumentError, 'do not set stop_level when trailing_stop is true' if stop_level
133
+ raise ArgumentError, 'set stop_distance when trailing_stop is true' unless stop_distance
134
+ end
135
+
136
+ if trailing_stop == trailing_stop_increment.nil?
137
+ raise ArgumentError, 'set trailing_stop_increment if and only if trailing_stop is true'
138
+ end
139
+ end
140
+
141
+ # Checks that attributes associated with the stop and limit are valid.
142
+ def validate_stop_and_limit_constraints
143
+ raise ArgumentError, 'set only one of limit_level and limit_distance' if limit_level && limit_distance
144
+ raise ArgumentError, 'set only one of stop_level and stop_distance' if stop_level && stop_distance
145
+ end
146
+
147
+ # Checks that attributes associated with the guaranteed stop are valid.
148
+ def validate_guaranteed_stop_constraints
149
+ if guaranteed_stop && !(stop_level.nil? ^ stop_distance.nil?)
150
+ raise ArgumentError, 'set exactly one of stop_level or stop_distance when guaranteed_stop is true'
151
+ end
152
+ end
153
+ end
154
+
155
+ def position_from_attributes(attributes)
156
+ Position.new(attributes.fetch(:position).merge(market: attributes.fetch(:market))).tap do |position|
157
+ position.instance_variable_set :@dealing_platform, @dealing_platform
158
+ end
159
+ end
160
+
161
+ private_constant :PositionCreateAttributes
162
+ end
163
+ end
164
+ end