ig_markets 0.11 → 0.12

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3ffcc67e15b995a577e2a546e9c3167194422eeb
4
- data.tar.gz: 6f2bbe2ba36c50719b1cb17de93a5bd3f3dfe037
3
+ metadata.gz: 7e50f2a597b16e6d091f0eff038350ec67847f60
4
+ data.tar.gz: 769b2056a2a1db136b05947fccbe42127ff54809
5
5
  SHA512:
6
- metadata.gz: a11539879eeb53bb9027be01f626e276ceb522cbbaf51f45686155070612914ab218f8adb5d395087095c77af61fcb0e43916252562bda8c0803d2330072732d
7
- data.tar.gz: ff1647ed07d0c563fae8956ec4b8a4c0591e7ecc3f45817ca732bb38cf58ca4c3d3df31e36908c14cfaa11ff33a9c8a77907be438d7afd1b575cdf3645870864
6
+ metadata.gz: 091827f393098b7b5b19df4686832ce96dd630038aee32ab15af550bfbdae5c4236bb238993198c9caae96273d02b83d8f34dd8d287dc4456b068e974969a3f0
7
+ data.tar.gz: 4ee120e6a912fdeef9ee98752c15559800ee3b406f168d1bb68524ee213b42a92afb7638dcf1675747bab6e6ed634f2a7030a41cb009b2235ff9ef5f090d17ad
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # IG Markets Changelog
2
2
 
3
+ ### 0.12 - June 21, 2016
4
+
5
+ - Unrecognized attributes returned by the IG Markets API now trigger a warning rather than causing an error, this makes
6
+ the library able to handle new return values being added to existing APIs
7
+ - Upgraded to version 3 of the activities API, which significantly changes the structure of `IGMarkets::Activity`
8
+ - Accept "`DFB`" (daily funded bet) on instrument periods, it is silently converted to `nil`
9
+ - Correctly handle instrument periods that are formatted as `MMM-YY`
10
+ - Stops and limits can now be specified as levels rather than distances when creating and updating working orders
11
+ - Report profit/loss information when printing deal confirmations
12
+ - Renamed `--print-requests` option to `--verbose`
13
+ - The `--currency-code` option to `ig_markets positions create` and `ig_markets orders create` no longer defaults to
14
+ `USD` and must be specified explicitly
15
+
3
16
  ### 0.11 — May 23, 2016
4
17
 
5
18
  - `IGMarkets::DealingPlatform::AccountMethods#activities` and `IGMarkets::DealingPlatform::AccountMethods#transactions`
@@ -7,7 +20,7 @@
7
20
  of results returned by a single request at 500
8
21
  - `IGMarkets::DealingPlatform::AccountMethods#activities` and `IGMarkets::DealingPlatform::AccountMethods#transactions`
9
22
  no longer take a `:days` option, and their `:to` option defaults to today's date if it is omitted
10
- - Accept "`DFB`" (Daily Funded Bet) on instrument expiries, it is silently converted to `nil`
23
+ - Accept "`DFB`" (daily funded bet) on instrument expiries, it is silently converted to `nil`
11
24
  - Instrument high and low is now shown in the `ig_markets positions list` command
12
25
 
13
26
  ### 0.10 — May 10, 2016
data/README.md CHANGED
@@ -108,7 +108,7 @@ ig_markets search EURUSD --type currencies
108
108
  ig_markets positions --aggregate
109
109
 
110
110
  # Create a EURUSD long position of size 2
111
- ig_markets positions create --direction buy --epic CS.D.EURUSD.CFD.IP --size 2
111
+ ig_markets positions create --direction buy --epic CS.D.EURUSD.CFD.IP --size 2 --currency-code USD
112
112
 
113
113
  # Change the limit and stop levels for an existing position
114
114
  ig_markets positions update DEAL-ID --limit-level 1.15 --stop-level 1.10
@@ -120,16 +120,18 @@ ig_markets positions close DEAL-ID
120
120
  ig_markets positions close DEAL-ID --size 1
121
121
 
122
122
  # Create a EURUSD sprint market short position of size 100 that expires in 20 minutes
123
- ig_markets sprints create --direction sell --epic FM.D.EURUSD24.EURUSD24.IP --expiry-period 20 --size 100
123
+ ig_markets sprints create --direction sell --epic FM.D.EURUSD24.EURUSD24.IP --expiry-period 20
124
+ --size 100
124
125
 
125
126
  # Create a working order to buy 1 unit of EURUSD at the level 1.1
126
127
  ig_markets orders create --direction buy --epic CS.D.EURUSD.CFD.IP --level 1.1 --size 1 --type limit
128
+ --currency-code USD
127
129
 
128
130
  # Print daily prices for EURUSD from the last two weeks
129
131
  ig_markets prices --epic CS.D.EURUSD.CFD.IP --resolution day --number 14
130
132
 
131
- # Log in and then open a Ruby console which can be used to query the IG API, all REST requests will be shown
132
- ig_markets console --print-requests
133
+ # Log in and open a Ruby console which can be used to query the IG API, printing all REST requests
134
+ ig_markets console --verbose
133
135
  ```
134
136
 
135
137
  ## Usage — Library
@@ -176,8 +178,8 @@ ig.sprint_market_positions.create direction: :buy, epic: 'FM.D.EURUSD24.EURUSD24
176
178
 
177
179
  # Working orders
178
180
  ig.working_orders.all
179
- ig.working_orders.create currency_code: 'USD', direction: :buy, epic: 'CS.D.EURUSD.CFD.IP', level: 0.99,
180
- size: 1, type: :limit
181
+ ig.working_orders.create currency_code: 'USD', direction: :buy, epic: 'CS.D.EURUSD.CFD.IP',
182
+ level: 0.99, size: 1, type: :limit
181
183
  ig.working_orders['deal_id']
182
184
  ig.working_orders['deal_id'].update level: 1.25, limit_distance: 50, stop_distance: 50
183
185
  ig.working_orders['deal_id'].delete
@@ -187,7 +189,8 @@ ig.markets.hierarchy
187
189
  ig.markets.search 'EURUSD'
188
190
  ig.markets['CS.D.EURUSD.CFD.IP']
189
191
  ig.markets['CS.D.EURUSD.CFD.IP'].historical_prices resolution: :hour, number: 48
190
- ig.markets['CS.D.EURUSD.CFD.IP'].historical_prices resolution: :second, from: Time.now - 120, to: Time.now - 60
192
+ ig.markets['CS.D.EURUSD.CFD.IP'].historical_prices resolution: :second, from: Time.now - 120,
193
+ to: Time.now - 60
191
194
 
192
195
  # Watchlists
193
196
  ig.watchlists.all
@@ -2,21 +2,45 @@ module IGMarkets
2
2
  # Contains details on a single activity that occurred on an IG Markets account. Returned by
3
3
  # {DealingPlatform::AccountMethods#activities}.
4
4
  class Activity < Model
5
- attribute :action_status, Symbol, allowed_values: [:accept, :reject, :manual, :not_set]
6
- attribute :activity
7
- attribute :activity_history_id
8
- attribute :channel
9
- attribute :currency
5
+ # Contains additional details on an activity. Returned by {#details}.
6
+ class Details < Model
7
+ # Contains details on the actions that were performed by an activity. Returned by {#actions}.
8
+ class Action < Model
9
+ attribute :action_type, Symbol, allowed_values: [:limit_order_amended, :limit_order_deleted,
10
+ :limit_order_filled, :limit_order_opened, :limit_order_rolled,
11
+ :position_closed, :position_deleted, :position_opended,
12
+ :position_partially_closed, :position_rolled,
13
+ :stop_limit_amended, :stop_order_amended, :stop_order_deleted,
14
+ :stop_order_filled, :stop_order_opened, :stop_order_rolled,
15
+ :unknown, :working_order_deleted]
16
+ attribute :affected_deal_id
17
+ end
18
+
19
+ attribute :actions, Action
20
+ attribute :currency
21
+ attribute :deal_reference
22
+ attribute :direction, Symbol, allowed_values: [:buy, :sell]
23
+ attribute :good_till_date
24
+ attribute :guaranteed_stop, Boolean
25
+ attribute :level, Float, nil_if: 0
26
+ attribute :limit_distance, Fixnum
27
+ attribute :limit_level, Float, nil_if: 0
28
+ attribute :market_name
29
+ attribute :size
30
+ attribute :stop_distance, Fixnum
31
+ attribute :stop_level, Float, nil_if: 0
32
+ attribute :trailing_step, Float, nil_if: 0
33
+ attribute :trailing_stop_distance, Fixnum
34
+ end
35
+
36
+ attribute :channel, Symbol, allowed_values: [:dealer, :mobile, :public_fix_api, :public_web_api, :system, :web]
10
37
  attribute :date, Time, format: '%FT%T'
11
38
  attribute :deal_id
39
+ attribute :description
40
+ attribute :details, Details
12
41
  attribute :epic, String, regex: Regex::EPIC
13
- attribute :level, Float
14
- attribute :limit, Float, nil_if: '-'
15
- attribute :market_name
16
- attribute :period, String, nil_if: '-'
17
- attribute :result
18
- attribute :size
19
- attribute :stop, String, nil_if: '-'
20
- attribute :stop_type, String, nil_if: '-', allowed_values: %w(G N T(50))
42
+ attribute :period, Time, nil_if: %w(- DFB), format: ['%FT%T', '%b-%y']
43
+ attribute :status, Symbol, allowed_values: [:accepted, :rejected, :unknown]
44
+ attribute :type, Symbol, allowed_values: [:edit_stop_and_limit, :position, :system, :working_order]
21
45
  end
22
46
  end
@@ -24,7 +24,7 @@ module IGMarkets
24
24
  private
25
25
 
26
26
  def gather_activities(dealing_platform)
27
- result = gather_account_history(:activities, dealing_platform).select do |activity|
27
+ result = dealing_platform.account.activities(history_options).select do |activity|
28
28
  activity_filter activity
29
29
  end
30
30
 
@@ -45,19 +45,6 @@ module IGMarkets
45
45
  'type' => :transaction_type
46
46
  }.fetch options[:sort_by]
47
47
  end
48
-
49
- def gather_account_history(method_name, dealing_platform)
50
- history_options = if options[:from]
51
- from = Date.strptime options[:from], '%F'
52
- to = from + options[:days]
53
-
54
- { from: from, to: to }
55
- else
56
- { from: Date.today - options[:days] }
57
- end
58
-
59
- dealing_platform.account.send method_name, history_options
60
- end
61
48
  end
62
49
  end
63
50
  end
@@ -18,7 +18,7 @@ module IGMarkets
18
18
 
19
19
  desc 'create', 'Creates a new working order'
20
20
 
21
- option :currency_code, default: 'USD', desc: 'The 3 character currency code, must be one of the instrument\'s ' \
21
+ option :currency_code, required: true, desc: 'The 3 character currency code, must be one of the instrument\'s ' \
22
22
  'currencies'
23
23
  option :direction, enum: %w(buy sell), required: true, desc: 'The trade direction'
24
24
  option :epic, required: true, desc: 'The EPIC of the market to trade'
@@ -28,8 +28,10 @@ module IGMarkets
28
28
  option :guaranteed_stop, type: :boolean, default: false, desc: 'Whether a guaranteed stop is required'
29
29
  option :level, type: :numeric, required: true, desc: 'The level at which the order will be triggered'
30
30
  option :limit_distance, type: :numeric, desc: 'The distance away in pips to place the limit'
31
+ option :limit_level, type: :numeric, desc: 'The level at which to place the limit'
31
32
  option :size, type: :numeric, required: true, desc: 'The size of the order'
32
33
  option :stop_distance, type: :numeric, desc: 'The distance away in pips to place the stop'
34
+ option :stop_level, type: :numeric, desc: 'The level at which to place the stop'
33
35
  option :type, enum: %w(limit stop), required: true, desc: 'The order type'
34
36
 
35
37
  def create
@@ -45,7 +47,9 @@ module IGMarkets
45
47
  option :good_till_date, desc: 'The date that the order will live till, format: yyyy-mm-ddThh:mm(+|-)zz:zz'
46
48
  option :level, type: :numeric, desc: 'The level at which the order will be triggered'
47
49
  option :limit_distance, desc: 'The distance away in pips to place the limit'
50
+ option :limit_level, type: :numeric, desc: 'The level at which to place the limit'
48
51
  option :stop_distance, desc: 'The distance away in pips to place the stop'
52
+ option :stop_level, type: :numeric, desc: 'The level at which to place the stop'
49
53
  option :type, enum: %w(limit stop), desc: 'The order type'
50
54
 
51
55
  def update(deal_id)
@@ -73,7 +77,7 @@ module IGMarkets
73
77
  private
74
78
 
75
79
  ATTRIBUTES = [:currency_code, :direction, :epic, :expiry, :force_open, :good_till_date, :guaranteed_stop, :level,
76
- :limit_distance, :size, :stop_distance, :type].freeze
80
+ :limit_distance, :limit_level, :size, :stop_distance, :stop_level, :type].freeze
77
81
 
78
82
  def working_order_attributes
79
83
  attributes = Main.filter_options options, ATTRIBUTES
@@ -24,7 +24,7 @@ module IGMarkets
24
24
 
25
25
  desc 'create', 'Creates a new position'
26
26
 
27
- option :currency_code, default: 'USD', desc: 'The 3 character currency code, must be one of the instrument\'s ' \
27
+ option :currency_code, required: true, desc: 'The 3 character currency code, must be one of the instrument\'s ' \
28
28
  'currencies'
29
29
  option :direction, required: true, enum: %w(buy sell), desc: 'The trade direction'
30
30
  option :epic, required: true, desc: 'The EPIC of the market to trade'
@@ -31,7 +31,7 @@ module IGMarkets
31
31
  private
32
32
 
33
33
  def gather_transactions(dealing_platform)
34
- result = gather_account_history(:transactions, dealing_platform).select do |transaction|
34
+ result = dealing_platform.account.transactions(history_options).select do |transaction|
35
35
  transaction_filter transaction
36
36
  end
37
37
 
@@ -7,7 +7,7 @@ module IGMarkets
7
7
  class_option :password, required: true, desc: 'The password for the session'
8
8
  class_option :api_key, required: true, desc: 'The API key for the session'
9
9
  class_option :demo, type: :boolean, desc: 'Use the demo platform (default is production)'
10
- class_option :print_requests, type: :boolean, desc: 'Whether to print the raw REST API requests and responses'
10
+ class_option :verbose, type: :boolean, desc: 'Whether to print the raw REST API requests and responses'
11
11
 
12
12
  desc 'orders [SUBCOMAND=list ...]', 'Command for working with orders'
13
13
  subcommand 'orders', Orders
@@ -21,6 +21,23 @@ module IGMarkets
21
21
  desc 'watchlists [SUBCOMAND=list ...]', 'Command for working with watchlists'
22
22
  subcommand 'watchlists', Watchlists
23
23
 
24
+ private
25
+
26
+ # Turns the `:days` and `:from` options into a hash with `:from` and `:to` keys that can be passed to
27
+ # {AccountMethods#activities} and {AccountMethods#transactions}.
28
+ #
29
+ # @return [Hash]
30
+ def history_options
31
+ if options[:from]
32
+ from = Date.strptime options[:from], '%F'
33
+ to = from + options[:days]
34
+
35
+ { from: from, to: to }
36
+ else
37
+ { from: Date.today - options[:days] }
38
+ end
39
+ end
40
+
24
41
  class << self
25
42
  # This is the initial entry point for the execution of the command-line client. It is responsible for reading
26
43
  # any config files, implementing the --version/-v options, and then invoking the main application.
@@ -44,7 +61,7 @@ module IGMarkets
44
61
  def begin_session(options)
45
62
  platform = options[:demo] ? :demo : :production
46
63
 
47
- RequestPrinter.enabled = true if options[:print_requests]
64
+ RequestPrinter.enabled = true if options[:verbose]
48
65
 
49
66
  @dealing_platform ||= DealingPlatform.new
50
67
  @dealing_platform.sign_in options[:username], options[:password], options[:api_key], platform
@@ -143,8 +160,21 @@ Status: #{Format.symbol deal_confirmation.deal_status}
143
160
  Result: #{Format.symbol deal_confirmation.status}
144
161
  END
145
162
 
163
+ print_deal_confirmation_profit_loss deal_confirmation
164
+
146
165
  puts "Reason: #{Format.symbol deal_confirmation.reason}" unless deal_confirmation.deal_status == :accepted
147
166
  end
167
+
168
+ # Prints out the profit/loss for the passed deal confirmation if applicable.
169
+ #
170
+ # @param [DealConfirmation] deal_confirmation The deal confirmation to print out the profit/loss for.
171
+ def print_deal_confirmation_profit_loss(deal_confirmation)
172
+ return unless deal_confirmation.profit
173
+
174
+ profit = Format.currency deal_confirmation.profit, deal_confirmation.profit_currency
175
+
176
+ puts "Profit/loss: #{profit.colorize(deal_confirmation.profit < 0 ? :red : :green)}"
177
+ end
148
178
  end
149
179
  end
150
180
  end
@@ -17,12 +17,20 @@ module IGMarkets
17
17
  end
18
18
 
19
19
  def row(activity)
20
- [activity.date, activity.channel, activity.activity, activity_status(activity), activity.epic,
21
- activity.market_name, activity.size, activity.level, activity.limit, activity.stop, activity.result]
20
+ details = activity.details
21
+
22
+ [activity.date, activity.channel, activity.type, activity.status, activity.epic, details.market_name,
23
+ details.size, details.level, details.limit_level, details.stop_level, action_types(details)]
22
24
  end
23
25
 
24
- def activity_status(activity)
25
- { accept: 'Accepted', reject: 'Rejected', manual: 'Manual', not_set: '' }.fetch activity.action_status
26
+ def action_types(details)
27
+ types = details.actions.map(&:action_type).uniq
28
+ types.delete :unknown
29
+
30
+ # Fix a typo in one of the values
31
+ types.map! { |v| v == :position_opended ? :position_opened : v }
32
+
33
+ types.map { |v| format_cell_value v }.join ', '
26
34
  end
27
35
  end
28
36
  end
@@ -39,8 +39,8 @@ module IGMarkets
39
39
 
40
40
  def market_status(market_overview)
41
41
  { closed: 'Closed', edits_only: 'Edits only', offline: 'Offline', on_auction: 'On auction',
42
- on_auction_no_edits: 'On auction no edits', suspended: 'Suspended', tradeable: 'Tradeable'
43
- }.fetch market_overview.market_status
42
+ on_auction_no_edits: 'On auction no edits', suspended: 'Suspended', tradeable: 'Tradeable' }
43
+ .fetch market_overview.market_status
44
44
  end
45
45
  end
46
46
  end
@@ -84,7 +84,13 @@ module IGMarkets
84
84
  end
85
85
 
86
86
  def combine_position_deal_ids(positions)
87
- positions.map(&:deal_id).join ', '
87
+ lines = []
88
+
89
+ positions.map(&:deal_id).each_slice(2) do |deal_ids|
90
+ lines << deal_ids.join(', ')
91
+ end
92
+
93
+ lines.join "\n"
88
94
  end
89
95
  end
90
96
  end
@@ -20,9 +20,10 @@ module IGMarkets
20
20
  attribute :account_info, Account::Balance
21
21
  attribute :account_type, Symbol, allowed_values: [:cfd, :physical, :spreadbet]
22
22
  attribute :accounts, AccountDetails
23
- attribute :authentication_status, Symbol, allowed_values: [
24
- :authenticated, :authenticated_missing_credentials, :change_environment, :disabled_preferred_account,
25
- :missing_preferred_account, :rejected_invalid_client_version]
23
+ attribute :authentication_status, Symbol, allowed_values: [:authenticated, :authenticated_missing_credentials,
24
+ :change_environment, :disabled_preferred_account,
25
+ :missing_preferred_account,
26
+ :rejected_invalid_client_version]
26
27
  attribute :client_id
27
28
  attribute :currency_iso_code, String, regex: Regex::CURRENCY
28
29
  attribute :currency_symbol
@@ -18,20 +18,28 @@ module IGMarkets
18
18
  attribute :level, Float
19
19
  attribute :limit_distance, Fixnum
20
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]
21
+ attribute :profit, Float
22
+ attribute :profit_currency, String, regex: Regex::CURRENCY
23
+ attribute :reason, Symbol, allowed_values: [:account_not_enabled_to_trading, :attached_order_level_error,
24
+ :attached_order_trailing_stop_error, :cannot_change_stop_type,
25
+ :cannot_remove_stop, :closing_only_trades_accepted_on_this_market,
26
+ :conflicting_order, :cr_spacing, :duplicate_order_error,
27
+ :exchange_manual_override, :finance_repeat_dealing,
28
+ :force_open_on_same_market_different_currency, :general_error,
29
+ :good_till_date_in_the_past, :instrument_not_found, :insufficient_funds,
30
+ :level_tolerance_error, :manual_order_timeout, :market_closed,
31
+ :market_closed_with_edits, :market_closing, :market_not_borrowable,
32
+ :market_offline, :market_phone_only, :market_rolled,
33
+ :market_unavailable_to_client, :max_auto_size_exceeded,
34
+ :minimum_order_size_error, :move_away_only_limit, :move_away_only_stop,
35
+ :move_away_only_trigger_level, :opposing_direction_orders_not_allowed,
36
+ :opposing_positions_not_allowed, :order_locked, :order_not_found,
37
+ :over_normal_market_size, :partially_closed_position_not_deleted,
38
+ :position_not_available_to_close, :position_not_found,
39
+ :reject_spreadbet_order_on_cfd_account, :size_increment,
40
+ :sprint_market_expiry_after_market_close, :stop_or_limit_not_allowed,
41
+ :stop_required_error, :strike_level_tolerance, :success,
42
+ :trailing_stop_not_allowed, :unknown, :wrong_side_of_market]
35
43
  attribute :size, Float
36
44
  attribute :status, Symbol, allowed_values: [:amended, :closed, :deleted, :open, :partially_closed]
37
45
  attribute :stop_distance, Fixnum
@@ -26,7 +26,10 @@ module IGMarkets
26
26
  #
27
27
  # @return [Array<Activity>]
28
28
  def activities(options)
29
- history_request_complete url: 'history/activity', url_parameters: prepare_history_options(options),
29
+ url_parameters = history_url_parameters options
30
+ url_parameters[:detailed] = true
31
+
32
+ history_request_complete url: 'history/activity', url_parameters: url_parameters, api_version: API_V3,
30
33
  collection_name: :activities, model_class: Activity, date_attribute: :date
31
34
  end
32
35
 
@@ -42,8 +45,9 @@ module IGMarkets
42
45
  def transactions(options)
43
46
  options[:type] ||= :all
44
47
 
45
- history_request_complete url: 'history/transactions', url_parameters: prepare_history_options(options),
46
- collection_name: :transactions, model_class: Transaction, date_attribute: :date_utc
48
+ history_request_complete url: 'history/transactions', url_parameters: history_url_parameters(options),
49
+ api_version: API_V2, collection_name: :transactions, model_class: Transaction,
50
+ date_attribute: :date_utc
47
51
  end
48
52
 
49
53
  private
@@ -53,11 +57,11 @@ module IGMarkets
53
57
 
54
58
  # Retrieves historical data for this account (either activities or transactions) in the specified date range. This
55
59
  # methods sends a single GET request with the passed URL parameters and returns the response. The maximum number
56
- # of items this method can return is capped at 500 {MAXIMUM_PAGE_SIZE}.
60
+ # of items this method can return is capped at 500 ({MAXIMUM_PAGE_SIZE}).
57
61
  def history_request(options)
58
62
  url = "#{options[:url]}?#{options[:url_parameters].map { |key, value| "#{key}=#{value.to_s.upcase}" }.join '&'}"
59
63
 
60
- get_result = @dealing_platform.session.get url, API_V2
64
+ get_result = @dealing_platform.session.get url, options.fetch(:api_version)
61
65
 
62
66
  @dealing_platform.instantiate_models options[:model_class], get_result.fetch(options[:collection_name])
63
67
  end
@@ -80,12 +84,12 @@ module IGMarkets
80
84
  models.uniq
81
85
  end
82
86
 
83
- # Parses and formats the history options shared by {#activities} and {#transactions}.
84
- def prepare_history_options(options)
87
+ # Parses and formats options shared by {#activities} and {#transactions} into a set of URL parameters.
88
+ def history_url_parameters(options)
85
89
  options[:to] ||= Date.today + 1
86
90
 
87
- options[:from] = options[:from].strftime('%F')
88
- options[:to] = options[:to].strftime('%F')
91
+ options[:from] = options.fetch(:from).strftime('%F')
92
+ options[:to] = options.fetch(:to).strftime('%F')
89
93
 
90
94
  options[:pageSize] = MAXIMUM_PAGE_SIZE
91
95
 
@@ -51,8 +51,8 @@ module IGMarkets
51
51
  class SprintMarketPositionCreateAttributes < Model
52
52
  attribute :direction, Symbol, allowed_values: [:buy, :sell]
53
53
  attribute :epic, String, regex: Regex::EPIC
54
- attribute :expiry_period, Symbol, allowed_values: [
55
- :one_minute, :two_minutes, :five_minutes, :twenty_minutes, :sixty_minutes]
54
+ attribute :expiry_period, Symbol, allowed_values: [:one_minute, :two_minutes, :five_minutes, :twenty_minutes,
55
+ :sixty_minutes]
56
56
  attribute :size, Float
57
57
  end
58
58
 
@@ -44,9 +44,15 @@ module IGMarkets
44
44
  # the working order will remain until it is cancelled.
45
45
  # @option attributes [Boolean] :guaranteed_stop Whether a guaranteed stop is required. Defaults to `false`.
46
46
  # @option attributes [Float] :level The level at which the order will be triggered. Required.
47
- # @option attributes [Fixnum] :limit_distance The distance away in pips to place the limit. Optional.
47
+ # @option attributes [Fixnum] :limit_distance The distance away in pips to place the limit. If this is set then
48
+ # the `:limit_level` option must be omitted. Optional.
49
+ # @option attributes [Float] :limit_level The level at which to place the limit. If this is set then the
50
+ # `:limit_distance` option must be omitted. Optional.
48
51
  # @option attributes [Float] :size The size of the working order. Required.
49
- # @option attributes [Fixnum] :stop_distance The distance away in pips to place the stop. Optional.
52
+ # @option attributes [Fixnum] :stop_distance The distance away in pips to place the stop. If this is set then the
53
+ # `:stop_level` option must be omitted. Optional.
54
+ # @option attributes [Float] :stop_level The level at which to place the stop. If this is set then the
55
+ # `:stop_distance` option must be omitted. Optional.
50
56
  # @option attributes [:limit, :stop] :type `:limit` means the working order is intended to buy at a price lower
51
57
  # than at present, or to sell at a price higher than at present, i.e. there is an expectation
52
58
  # of a market reversal at the specified `:level`. `:stop` means the working order is intended
@@ -56,30 +62,14 @@ module IGMarkets
56
62
  # @return [String] The resulting deal reference, use {DealingPlatform#deal_confirmation} to check the result of
57
63
  # the working order creation.
58
64
  def create(attributes)
59
- attributes[:force_open] ||= false
60
- attributes[:guaranteed_stop] ||= false
61
- attributes[:time_in_force] = attributes[:good_till_date] ? :good_till_date : :good_till_cancelled
62
-
63
- model = build_working_order_model attributes
65
+ model = WorkingOrderCreateAttributes.new attributes
66
+ model.validate
64
67
 
65
68
  payload = PayloadFormatter.format model
66
69
 
67
70
  payload[:expiry] ||= '-'
68
71
 
69
- @dealing_platform.session.post('workingorders/otc', payload, API_V2).fetch(:deal_reference)
70
- end
71
-
72
- private
73
-
74
- def build_working_order_model(attributes)
75
- model = WorkingOrderCreateAttributes.new attributes
76
-
77
- required = [:currency_code, :direction, :epic, :guaranteed_stop, :level, :size, :time_in_force, :type]
78
- required.each do |attribute|
79
- raise ArgumentError, "#{attribute} attribute must be set" if attributes[attribute].nil?
80
- end
81
-
82
- model
72
+ @dealing_platform.session.post('workingorders/otc', payload, API_V2).fetch :deal_reference
83
73
  end
84
74
 
85
75
  # Internal model used by {#create}.
@@ -93,10 +83,41 @@ module IGMarkets
93
83
  attribute :guaranteed_stop, Boolean
94
84
  attribute :level, Float
95
85
  attribute :limit_distance, Fixnum
86
+ attribute :limit_level, Float
96
87
  attribute :size, Float
97
88
  attribute :stop_distance, Fixnum
89
+ attribute :stop_level, Float
98
90
  attribute :time_in_force, Symbol, allowed_values: [:good_till_cancelled, :good_till_date]
99
91
  attribute :type, Symbol, allowed_values: [:limit, :stop]
92
+
93
+ def initialize(attributes)
94
+ super
95
+
96
+ set_defaults
97
+ end
98
+
99
+ # Runs a series of validations on this model's attributes to check whether it is ready to be sent to the IG
100
+ # Markets API.
101
+ def validate
102
+ required = [:currency_code, :direction, :epic, :guaranteed_stop, :level, :size, :time_in_force, :type]
103
+ required.each do |attribute|
104
+ raise ArgumentError, "#{attribute} attribute must be set" if send(attribute).nil?
105
+ end
106
+
107
+ if limit_distance && limit_level
108
+ raise ArgumentError, 'Do not specify both limit_distance and limit_level options'
109
+ end
110
+
111
+ raise ArgumentError, 'Do not specify both stop_distance and stop_level options' if stop_distance && stop_level
112
+ end
113
+
114
+ private
115
+
116
+ def set_defaults
117
+ self.force_open = false if force_open.nil?
118
+ self.guaranteed_stop = false if guaranteed_stop.nil?
119
+ self.time_in_force = good_till_date ? :good_till_date : :good_till_cancelled
120
+ end
100
121
  end
101
122
 
102
123
  private_constant :WorkingOrderCreateAttributes
@@ -135,6 +135,8 @@ module IGMarkets
135
135
  model.instance_variable_set :@dealing_platform, self
136
136
 
137
137
  attributes.each do |attribute, value|
138
+ next unless model_class.valid_attribute? attribute
139
+
138
140
  type = model_class.attribute_type attribute
139
141
  value = instantiate_models(type, value) if type < Model
140
142
 
@@ -79,10 +79,10 @@ module IGMarkets
79
79
  attribute :sprint_markets_minimum_expiry_time, Float
80
80
  attribute :stops_limits_allowed, Boolean
81
81
  attribute :streaming_prices_available, Boolean
82
- attribute :type, Symbol, allowed_values: [
83
- :binary, :bungee_capped, :bungee_commodities, :bungee_currencies, :bungee_indices, :commodities,
84
- :currencies, :indices, :opt_commodities, :opt_currencies, :opt_indices, :opt_rates, :opt_shares,
85
- :rates, :sectors, :shares, :sprint_market, :test_market, :unknown]
82
+ attribute :type, Symbol, allowed_values: [:binary, :bungee_capped, :bungee_commodities, :bungee_currencies,
83
+ :bungee_indices, :commodities, :currencies, :indices, :opt_commodities,
84
+ :opt_currencies, :opt_indices, :opt_rates, :opt_shares, :rates, :sectors,
85
+ :shares, :sprint_market, :test_market, :unknown]
86
86
  attribute :unit, Symbol, allowed_values: [:amount, :contracts, :shares]
87
87
  attribute :value_of_one_pip
88
88
  end
@@ -11,8 +11,8 @@ module IGMarkets
11
11
  attribute :value, Float
12
12
  end
13
13
 
14
- attribute :market_order_preference, Symbol, allowed_values: [
15
- :available_default_off, :available_default_on, :not_available]
14
+ attribute :market_order_preference, Symbol, allowed_values: [:available_default_off, :available_default_on,
15
+ :not_available]
16
16
  attribute :max_stop_or_limit_distance, RuleDetails
17
17
  attribute :min_controlled_risk_stop_distance, RuleDetails
18
18
  attribute :min_deal_size, RuleDetails
@@ -30,8 +30,8 @@ module IGMarkets
30
30
  attribute :delay_time, Float
31
31
  attribute :high, Float
32
32
  attribute :low, Float
33
- attribute :market_status, Symbol, allowed_values: [
34
- :closed, :edits_only, :offline, :on_auction, :on_auction_no_edits, :suspended, :tradeable]
33
+ attribute :market_status, Symbol, allowed_values: [:closed, :edits_only, :offline, :on_auction,
34
+ :on_auction_no_edits, :suspended, :tradeable]
35
35
  attribute :net_change, Float
36
36
  attribute :offer, Float
37
37
  attribute :percentage_change, Float
@@ -53,7 +53,7 @@ module IGMarkets
53
53
  end
54
54
 
55
55
  def typecaster_date(value, options, name)
56
- raise ArgumentError, "#{self}##{name}: invalid or missing date format" unless options[:format].is_a? String
56
+ raise ArgumentError, "#{self}##{name}: invalid date format" unless options[:format].is_a? String
57
57
 
58
58
  if value.is_a? String
59
59
  begin
@@ -67,28 +67,36 @@ module IGMarkets
67
67
  end
68
68
 
69
69
  def typecaster_time(value, options, name)
70
- raise ArgumentError, "#{self}##{name}: invalid or missing time format" unless options[:format].is_a? String
70
+ format = options[:format]
71
+
72
+ raise ArgumentError, "#{self}##{name}: invalid time format" unless [Array, String].include? format.class
71
73
 
72
74
  if value.is_a?(String) || value.is_a?(Fixnum)
73
- parse_time_from_string value.to_s, options, name
75
+ parse_formatted_time_value value.to_s, options, name
74
76
  else
75
77
  value
76
78
  end
77
79
  end
78
80
 
79
- def parse_time_from_string(value, options, name)
80
- format = options[:format]
81
+ def parse_formatted_time_value(value, options, name)
82
+ Array(options[:format]).each do |format|
83
+ begin
84
+ return parse_time_using_format value, format
85
+ rescue ArgumentError
86
+ next
87
+ end
88
+ end
81
89
 
90
+ raise ArgumentError, "#{self}##{name}: failed parsing time: #{value}"
91
+ end
92
+
93
+ def parse_time_using_format(value, format)
82
94
  unless format == '%Q'
83
95
  format += '%z'
84
96
  value += '+0000'
85
97
  end
86
98
 
87
- begin
88
- Time.strptime value, format
89
- rescue ArgumentError
90
- raise ArgumentError, "#{self}##{name}: failed parsing time: #{value}"
91
- end
99
+ Time.strptime value, format
92
100
  end
93
101
  end
94
102
  end
@@ -68,7 +68,7 @@ module IGMarkets
68
68
  # @return [Hash] A hash containing details of all attributes that have been defined on this model.
69
69
  attr_accessor :defined_attributes
70
70
 
71
- # @return [Hash] The names of the deprecated attributes on this model.
71
+ # @return [Array] The names of the deprecated attributes on this model.
72
72
  attr_accessor :deprecated_attributes
73
73
 
74
74
  # Returns the names of all currently defined attributes for this model.
@@ -78,6 +78,23 @@ module IGMarkets
78
78
  Hash(defined_attributes).keys
79
79
  end
80
80
 
81
+ # Returns whether the passed value is a valid attribute name. If the passed attribute name is not recognized then
82
+ # an error will be printed to `stderr`. Only one warning will be printed for each unrecognized attribute.
83
+ #
84
+ # @param [Symbol] name The candidate attribute name.
85
+ #
86
+ # @return [Boolean]
87
+ def valid_attribute?(name)
88
+ return true if defined_attribute_names.include?(name) || Array(deprecated_attributes).include?(name)
89
+
90
+ unless Array(@reported_invalid_attributes).include? name
91
+ warn "ig_markets: unrecognized attribute #{self}##{name}"
92
+ (@reported_invalid_attributes ||= []) << name
93
+ end
94
+
95
+ false
96
+ end
97
+
81
98
  # Returns the type of the specified attribute.
82
99
  #
83
100
  # @param [Symbol] attribute_name The name of the attribute to return the type for.
@@ -108,8 +125,8 @@ module IGMarkets
108
125
  # @option options [Array] :nil_if Values that, when set on the attribute, should be converted to `nil`.
109
126
  # @option options [Regexp] :regex When `type` is `String` only values matching this regex will be allowed.
110
127
  # Optional.
111
- # @option options [String] :format When `type` is `Date` or `Time` this specifies the format for parsing String
112
- # and `Fixnum` instances assigned to this attribute.
128
+ # @option options [String] :format When `type` is `Date` or `Time` this specifies the format or formats for
129
+ # parsing String and `Fixnum` instances assigned to this attribute.
113
130
  #
114
131
  # @macro [attach] attribute
115
132
  # The $1 attribute.
@@ -4,9 +4,9 @@ module IGMarkets
4
4
  module_function
5
5
 
6
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 both `Date` and `Time` attributes will be converted to strings using their
9
- # ':format' option.
7
+ # Attribute names will be converted from snake case to camel case, `Symbol` attributes will be converted to strings
8
+ # and will be uppercased, and both `Date` and `Time` attributes will be converted to strings using their first
9
+ # available `:format` option.
10
10
  #
11
11
  # @param [Model] model The model instance to convert attributes for.
12
12
  #
@@ -32,7 +32,7 @@ module IGMarkets
32
32
 
33
33
  value = value.utc if options[:type] == Time
34
34
 
35
- return value.strftime(options.fetch(:format)) if [Date, Time].include? options[:type]
35
+ return value.strftime(Array(options.fetch(:format)).first) if [Date, Time].include? options[:type]
36
36
 
37
37
  value
38
38
  end
@@ -107,8 +107,8 @@ module IGMarkets
107
107
  # the result of the position update.
108
108
  def update(new_attributes)
109
109
  new_attributes = { limit_level: limit_level, stop_level: stop_level, trailing_stop: trailing_stop?,
110
- trailing_stop_distance: trailing_stop_distance, trailing_stop_increment: trailing_step
111
- }.merge new_attributes
110
+ trailing_stop_distance: trailing_stop_distance, trailing_stop_increment: trailing_step }
111
+ .merge new_attributes
112
112
 
113
113
  unless new_attributes[:trailing_stop]
114
114
  new_attributes[:trailing_stop_distance] = new_attributes[:trailing_stop_increment] = nil
@@ -8,7 +8,7 @@ module IGMarkets
8
8
  attribute :date_utc, Time, format: '%FT%T'
9
9
  attribute :instrument_name
10
10
  attribute :open_level, String, nil_if: %w(- 0)
11
- attribute :period, Time, nil_if: '-', format: '%FT%T'
11
+ attribute :period, Time, nil_if: %w(- DFB), format: ['%FT%T', '%b-%y']
12
12
  attribute :profit_and_loss
13
13
  attribute :reference
14
14
  attribute :size, String, nil_if: '-'
@@ -1,4 +1,4 @@
1
1
  module IGMarkets
2
2
  # The version of this gem.
3
- VERSION = '0.11'.freeze
3
+ VERSION = '0.12'.freeze
4
4
  end
@@ -37,21 +37,23 @@ module IGMarkets
37
37
  # @option new_attributes [Time] :good_till_date
38
38
  # @option new_attributes [Float] :level
39
39
  # @option new_attributes [Fixnum] :limit_distance
40
+ # @option new_attributes [Float] :limit_level
40
41
  # @option new_attributes [Fixnum] :stop_distance
42
+ # @option new_attributes [Float] :stop_level
41
43
  # @option new_attributes [:limit, :stop] :type
42
44
  #
43
45
  # @return [String] The deal reference of the update operation. Use {DealingPlatform#deal_confirmation} to check
44
46
  # the result of the working order update.
45
47
  def update(new_attributes)
46
- new_attributes = { good_till_date: good_till_date, level: order_level, limit_distance: limit_distance,
47
- stop_distance: stop_distance, time_in_force: time_in_force, type: order_type
48
- }.merge new_attributes
48
+ existing_attributes = { good_till_date: good_till_date, level: order_level, limit_distance: limit_distance,
49
+ stop_distance: stop_distance, time_in_force: time_in_force, type: order_type }
49
50
 
50
- new_attributes[:time_in_force] = new_attributes[:good_till_date] ? :good_till_date : :good_till_cancelled
51
+ model = WorkingOrderUpdateAttributes.new existing_attributes, new_attributes
52
+ model.validate
51
53
 
52
- payload = PayloadFormatter.format WorkingOrderUpdateAttributes.new new_attributes
54
+ payload = PayloadFormatter.format model
53
55
 
54
- @dealing_platform.session.put("workingorders/otc/#{deal_id}", payload).fetch(:deal_reference)
56
+ @dealing_platform.session.put("workingorders/otc/#{deal_id}", payload, API_V2).fetch(:deal_reference)
55
57
  end
56
58
 
57
59
  # Internal model used by {#update}.
@@ -59,9 +61,30 @@ module IGMarkets
59
61
  attribute :good_till_date, Time, format: '%Y/%m/%d %R:%S'
60
62
  attribute :level, Float
61
63
  attribute :limit_distance, Fixnum
64
+ attribute :limit_level, Float
62
65
  attribute :stop_distance, Fixnum
66
+ attribute :stop_level, Float
63
67
  attribute :time_in_force, Symbol, allowed_values: [:good_till_cancelled, :good_till_date]
64
68
  attribute :type, Symbol, allowed_values: [:limit, :stop]
69
+
70
+ def initialize(existing_attributes, new_attributes)
71
+ existing_attributes.delete :limit_distance if new_attributes.key? :limit_level
72
+ existing_attributes.delete :stop_distance if new_attributes.key? :stop_level
73
+
74
+ super existing_attributes.merge(new_attributes)
75
+
76
+ self.time_in_force = good_till_date ? :good_till_date : :good_till_cancelled
77
+ end
78
+
79
+ # Runs a series of validations on this model's attributes to check whether it is ready to be sent to the IG
80
+ # Markets API.
81
+ def validate
82
+ if limit_distance && limit_level
83
+ raise ArgumentError, 'Do not specify both limit_distance and limit_level options'
84
+ end
85
+
86
+ raise ArgumentError, 'Do not specify both stop_distance and stop_level options' if stop_distance && stop_level
87
+ end
65
88
  end
66
89
 
67
90
  private_constant :WorkingOrderUpdateAttributes
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ig_markets
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.11'
4
+ version: '0.12'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Viney
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-23 00:00:00.000000000 Z
11
+ date: 2016-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -86,14 +86,14 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '0.4'
89
+ version: '0.5'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '0.4'
96
+ version: '0.5'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: factory_girl
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -170,14 +170,14 @@ dependencies:
170
170
  requirements:
171
171
  - - "~>"
172
172
  - !ruby/object:Gem::Version
173
- version: '0.39'
173
+ version: '0.40'
174
174
  type: :development
175
175
  prerelease: false
176
176
  version_requirements: !ruby/object:Gem::Requirement
177
177
  requirements:
178
178
  - - "~>"
179
179
  - !ruby/object:Gem::Version
180
- version: '0.39'
180
+ version: '0.40'
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: yard
183
183
  requirement: !ruby/object:Gem::Requirement