ig_markets 0.11 → 0.12

Sign up to get free protection for your applications and to get access to all the features.
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