oanda_api_v20 1.5.0 → 2.2.0

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
- SHA1:
3
- metadata.gz: b8108f8ae8240196afbf5ce81fb7cc96f9eeece1
4
- data.tar.gz: 97384a79da9a3a293529034eb5f2a483c5c9b96e
2
+ SHA256:
3
+ metadata.gz: be0ef34e9d27b283681c25c8f281c5ec264f9b36b63b39715df9bc69de0209a9
4
+ data.tar.gz: ba7a3cd6ec27e820e58565fedc2ec6b7e723f954ae1381ef79548d4ca88469af
5
5
  SHA512:
6
- metadata.gz: def31bfcd6716e880495a6396133334fd649c9fa64f42c1b957b4a165bb7b3b3ea74e13be95b3293ababb1f754ad935bd09ab24360ee9a58008e646bd61279f7
7
- data.tar.gz: 6ee7cb2047dd8e05eae5da2da3efd859a6dfad1fd1c4f989342b73a9a67e6bd43d8b6145991e1c73ab0c82e6d61a7e94a2ae7679d049af52e0a4a6855c496c1a
6
+ metadata.gz: 39f4320e897a343a542e19155c36a6b80698ab7b57bc8d5e40da608ab6ec6845ba12cf9d1a79ef29c24a28850616ddf12760929fc15f78df7b9ff7aa5ea41eb3
7
+ data.tar.gz: e791adff74088c1f6da6a23454f30d13fe226828a9683d609b9ff910819a34008722a4badb6b0d7b29fb0b59b95064f5eb44844b2474a6004877c46d1f0f8dec
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  .byebug_history
2
2
  oanda_api_v20-*.gem
3
3
  Gemfile.lock
4
+ .DS_Store
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.3.1
1
+ 2.7.2
data/.travis.yml CHANGED
@@ -1,3 +1,7 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.2
3
+ - 2.3
4
+ - 2.4
5
+ - 2.5
6
+ - 2.6
7
+ - jruby-9.2.5.0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Change Log
2
2
 
3
+ ## 2.2.0
4
+ #### 2021-08-27
5
+
6
+ * New transactions_stream endpoint. Big thanks to @plabaj.
7
+ * Upgraded development dependency version for rake to fix potential security vulnerability.
8
+
9
+ ## 2.1.1
10
+ #### 2019-10-05
11
+ * Fix to pricing_stream method. This fixes the currency pair rate being reused over and over no matter the value read in the HTTP chunk. Big thanks to @joseluis-fw & @salrepe.
12
+
13
+ ## 2.1.0
14
+ #### 2019-02-01
15
+ * New pricing_stream endpoint. Big thanks to @joseluis-fw.
16
+ * Raises an OandaApiV20::ParseError exception when a malformed response returned from Oanda API.
17
+ * The OandaApiV20::RequestError exception object now has an original_exception attribute which contains the original exception object raised.
18
+
19
+ ## 2.0.0
20
+ #### 2018-04-24
21
+ * Fixed issues when trying to reuse a Client object to make HTTP requests to Oanda.
22
+ * Moved instance variables last_action, last_arguments, instrument and account_id from the Client class to the Api class. They should not be part of a Client object.
23
+ * Removed last_transaction_id instance variable.
24
+ * RubyVM::Logger changed to ::Logger.
25
+ * Added a connection_pool_size option to allow persistent HTTP connection control on the Client object.
26
+ * Added a max_requests_per_second option to allow customized control on the Client max requests per second allowed to Oanda API.
27
+ * Added multithreading support for HTTP request governing. HTTP requests will be correctly allocated across multiple client threads.
28
+ * Raises an OandaApiV20::ApiError exception when no client object was supplied when instantiating an OandaApiV20::Api instance.
29
+
3
30
  ## 1.5.0
4
31
  #### 2017-07-07
5
32
  * Updated the orders method to allow query options to be passed along.
data/README.md CHANGED
@@ -33,10 +33,22 @@ If you would like to trade with your test account:
33
33
 
34
34
  client = OandaApiV20.new(access_token: 'my_access_token', practice: true)
35
35
 
36
+ If you would like to use the streaming endpoints:
37
+
38
+ client = OandaApiV20.new(access_token: 'my_access_token', stream: true)
39
+
36
40
  If you need your requests to go through a proxy:
37
41
 
38
42
  client = OandaApiV20.new(access_token: 'my_access_token', proxy_url: 'https://user:pass@proxy.com:80')
39
43
 
44
+ You can adjust the persistend connection pool size, the default is 2:
45
+
46
+ client = OandaApiV20.new(access_token: 'my_access_token', connection_pool_size: 10)
47
+
48
+ You can adjust the number of requests per second allowed to Oanda API, the default is 100:
49
+
50
+ client = OandaApiV20.new(access_token: 'my_access_token', max_requests_per_second: 10)
51
+
40
52
  ## Examples
41
53
 
42
54
  ### Accounts
@@ -63,10 +75,6 @@ client.account('account_id').instruments.show
63
75
  client.account('account_id').instruments('EUR_USD,EUR_CAD').show
64
76
  ```
65
77
 
66
- ```ruby
67
- client.account('account_id').changes.show
68
- ```
69
-
70
78
  ```ruby
71
79
  options = { 'sinceTransactionID' => '6358' }
72
80
 
@@ -103,6 +111,10 @@ options = { 'instrument' => 'USD_CAD' }
103
111
  client.account('account_id').orders(options).show
104
112
  ```
105
113
 
114
+ ```ruby
115
+ client.account('account_id').pending_orders.show
116
+ ```
117
+
106
118
  ```ruby
107
119
  id = client.account('account_id').orders.show['orders'][0]['id']
108
120
 
@@ -128,10 +140,12 @@ id = client.account('account_id').orders.show['orders'][0]['id']
128
140
 
129
141
  options = {
130
142
  'order' => {
143
+ 'instrument' => 'EUR_CAD',
144
+ 'price' => '1.6000',
131
145
  'timeInForce' => 'GTC',
132
- 'price' => '1.7000',
133
- 'type' => 'TAKE_PROFIT',
134
- 'tradeID' => '1'
146
+ 'type' => 'MARKET_IF_TOUCHED',
147
+ 'units' => '200',
148
+ 'positionFill' => 'DEFAULT'
135
149
  }
136
150
  }
137
151
 
@@ -182,11 +196,11 @@ id = client.account('account_id').open_trades.show['trades'][0]['id']
182
196
  options = {
183
197
  'takeProfit' => {
184
198
  'timeInForce' => 'GTC',
185
- 'price' => '0.5'
199
+ 'price' => '2.5'
186
200
  },
187
201
  'stopLoss' => {
188
202
  'timeInForce' => 'GTC',
189
- 'price' => '2.5'
203
+ 'price' => '0.5'
190
204
  }
191
205
  }
192
206
 
@@ -283,6 +297,14 @@ options = {
283
297
  client.account('account_id').transactions_since_id(options).show
284
298
  ```
285
299
 
300
+ ```ruby
301
+ client = OandaApiV20.new(access_token: 'my_access_token', stream: true)
302
+
303
+ client.account('account_id').transactions_stream.show do |json|
304
+ puts json if json['type'] != 'HEARTBEAT'
305
+ end
306
+ ```
307
+
286
308
  ### Pricing
287
309
 
288
310
  See the [Oanda Documentation](http://developer.oanda.com/rest-live-v20/pricing-ep/) for all available options on pricing.
@@ -295,10 +317,34 @@ options = {
295
317
  client.account('account_id').pricing(options).show
296
318
  ```
297
319
 
320
+ ```ruby
321
+ client = OandaApiV20.new(access_token: 'my_access_token', stream: true)
322
+
323
+ options = {
324
+ 'instruments' => 'EUR_USD,USD_CAD'
325
+ }
326
+
327
+ client.account('account_id').pricing_stream(options).show do |json|
328
+ puts json if json['type'] == 'PRICE'
329
+ end
330
+ ```
331
+
298
332
  ## Exceptions
299
333
 
334
+ A `OandaApiV20::ParseError` will be raised when a response from the Oanda API is malformed.
335
+
300
336
  A `OandaApiV20::RequestError` will be raised when a request to the Oanda API failed for any reason.
301
337
 
338
+ You can access the original exception in a `OandaApiV20::RequestError`:
339
+
340
+ ```ruby
341
+ begin
342
+ do_something
343
+ rescue OandaApiV20::RequestError => e
344
+ e.original_exception
345
+ end
346
+ ```
347
+
302
348
  ## Contributing
303
349
 
304
350
  1. Fork it
@@ -25,7 +25,7 @@ module OandaApiV20
25
25
 
26
26
  # GET /v3/accounts/:account_id/changes
27
27
  def changes(options = {})
28
- options = { 'sinceTransactionID' => last_transaction_id } unless options['sinceTransactionID']
28
+ options = { 'sinceTransactionID' => nil } unless options['sinceTransactionID']
29
29
  Client.send(http_verb, "#{base_uri}/accounts/#{account_id}/changes", headers: headers, query: options)
30
30
  end
31
31
 
@@ -8,12 +8,101 @@ module OandaApiV20
8
8
  include Transactions
9
9
  include Pricing
10
10
 
11
- attr_accessor :http_verb, :base_uri, :headers, :account_id, :instrument, :last_transaction_id
11
+ attr_accessor :client, :base_uri, :headers, :account_id, :last_action, :last_arguments
12
+ attr_writer :instrument
12
13
 
13
14
  def initialize(options = {})
14
15
  options.each do |key, value|
15
16
  self.send("#{key}=", value) if self.respond_to?("#{key}=")
16
17
  end
18
+
19
+ raise OandaApiV20::ApiError, 'No client object was supplid.' unless client
20
+ @base_uri ||= client.base_uri
21
+ @headers ||= client.headers
22
+ end
23
+
24
+ class << self
25
+ def api_methods
26
+ Accounts.instance_methods + Instruments.instance_methods + Orders.instance_methods + Trades.instance_methods + Positions.instance_methods + Transactions.instance_methods + Pricing.instance_methods
27
+ end
28
+ end
29
+
30
+ self.api_methods.each do |method_name|
31
+ original_method = instance_method(method_name)
32
+
33
+ define_method(method_name) do |*args, &block|
34
+ # Add the block below before each of the api_methods to set the last_action and last_arguments.
35
+ # Return the OandaApiV20::Api object to allow for method chaining when any of the api_methods have been called.
36
+ # Only make an HTTP request to Oanda API when an action method like show, update, cancel, close or create was called.
37
+ set_last_action_and_arguments(method_name, *args)
38
+ return self unless http_verb
39
+
40
+ original_method.bind(self).call(*args, &block)
41
+ end
42
+ end
43
+
44
+ def method_missing(name, *args, &block)
45
+ case name
46
+ when :show, :create, :update, :cancel, :close
47
+ set_http_verb(name, last_action)
48
+
49
+ if respond_to?(last_action)
50
+ api_result = {}
51
+ client.update_last_api_request_at
52
+ client.govern_api_request_rate
53
+
54
+ begin
55
+ response = Http::Exceptions.wrap_and_check do
56
+ last_arguments.nil? || last_arguments.empty? ? send(last_action, &block) : send(last_action, *last_arguments, &block)
57
+ end
58
+ rescue Http::Exceptions::HttpException => e
59
+ raise OandaApiV20::RequestError.new(e.message, response: e.response, original_exception: e.original_exception)
60
+ end
61
+
62
+ if response.body && !response.body.empty?
63
+ api_result.merge!(JSON.parse(response.body))
64
+ end
65
+ end
66
+
67
+ self.http_verb = nil
68
+ api_result
69
+ else
70
+ super
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ attr_accessor :http_verb
77
+
78
+ def set_last_action_and_arguments(action, *args)
79
+ self.last_action = action.to_sym
80
+ self.last_arguments = args
81
+ end
82
+
83
+ def set_http_verb(action, last_action)
84
+ case action
85
+ when :show
86
+ self.http_verb = :get
87
+ when :update, :cancel, :close
88
+ [:configuration].include?(last_action) ? self.http_verb = :patch : self.http_verb = :put
89
+ when :create
90
+ self.http_verb = :post
91
+ else
92
+ self.http_verb = nil
93
+ end
94
+ end
95
+
96
+ def parse(buffer, fragment, &block)
97
+ buffer.split("\n").each do |message|
98
+ cleaned_message = message.strip
99
+ next if cleaned_message.empty?
100
+ yield JSON.parse(cleaned_message)
101
+ end
102
+ rescue JSON::ParserError => e
103
+ raise OandaApiV20::ParseError, "#{e.message} in '#{fragment}'"
104
+ ensure
105
+ buffer.clear
17
106
  end
18
107
  end
19
108
  end
@@ -2,14 +2,18 @@ module OandaApiV20
2
2
  class Client
3
3
  include HTTParty
4
4
 
5
- MAX_REQUESTS_PER_SECOND_ALLOWED = 30
6
-
7
5
  BASE_URI = {
8
- live: 'https://api-fxtrade.oanda.com/v3',
9
- practice: 'https://api-fxpractice.oanda.com/v3'
10
- }
6
+ live: {
7
+ api: 'https://api-fxtrade.oanda.com/v3',
8
+ stream: 'https://stream-fxtrade.oanda.com/v3'
9
+ },
10
+ practice: {
11
+ api: 'https://api-fxpractice.oanda.com/v3',
12
+ stream: 'https://stream-fxpractice.oanda.com/v3'
13
+ }
14
+ }.freeze
11
15
 
12
- attr_accessor :access_token, :proxy_url
16
+ attr_accessor :access_token, :proxy_url, :max_requests_per_second, :connection_pool_size, :debug
13
17
  attr_reader :base_uri, :headers
14
18
 
15
19
  def initialize(options = {})
@@ -17,11 +21,15 @@ module OandaApiV20
17
21
  self.send("#{key}=", value) if self.respond_to?("#{key}=")
18
22
  end
19
23
 
20
- @debug = options[:debug] || false
21
- @last_api_request_at = Array.new(MAX_REQUESTS_PER_SECOND_ALLOWED)
22
- @base_uri = options[:practice] == true ? BASE_URI[:practice] : BASE_URI[:live]
24
+ @mutex = Mutex.new
25
+ @debug ||= false
26
+ @connection_pool_size ||= 2
27
+ @max_requests_per_second ||= 100
28
+ @last_api_request_at = Array.new(max_requests_per_second)
29
+ uris = options[:practice] == true ? BASE_URI[:practice] : BASE_URI[:live]
30
+ @base_uri = options[:stream] == true ? uris[:stream] : uris[:api]
23
31
 
24
- @headers = {}
32
+ @headers = {}
25
33
  @headers['Authorization'] = "Bearer #{access_token}"
26
34
  @headers['X-Accept-Datetime-Format'] = 'RFC3339'
27
35
  @headers['Content-Type'] = 'application/json'
@@ -35,113 +43,45 @@ module OandaApiV20
35
43
  keep_alive: 30,
36
44
  idle_timeout: 10,
37
45
  warn_timeout: 2,
38
- pool_size: 2
46
+ pool_size: connection_pool_size
39
47
  }
40
- persistent_connection_adapter_options.merge!(logger: RubyVM::Logger.new(STDOUT)) if @debug
48
+
49
+ persistent_connection_adapter_options.merge!(logger: ::Logger.new(STDOUT), debug_output: ::Logger.new(STDOUT)) if debug
41
50
  Client.persistent_connection_adapter(persistent_connection_adapter_options)
42
51
  end
43
52
 
44
53
  def method_missing(name, *args, &block)
45
54
  case name
46
- when :show, :create, :update, :cancel, :close
47
- set_http_verb(name, last_action)
48
- api = Api.new(api_attributes)
49
-
50
- if api.respond_to?(last_action)
51
- api_result = {}
52
- set_last_api_request_at
53
- govern_api_request_rate
55
+ when *Api.api_methods
56
+ api_attributes = {
57
+ client: self,
58
+ last_action: name,
59
+ last_arguments: args
60
+ }
54
61
 
55
- begin
56
- response = Http::Exceptions.wrap_and_check do
57
- last_arguments.nil? || last_arguments.empty? ? api.send(last_action, &block) : api.send(last_action, *last_arguments, &block)
58
- end
59
- rescue Http::Exceptions::HttpException => e
60
- raise OandaApiV20::RequestError, e.message
61
- end
62
+ api_attributes.merge!(account_id: args.first) if name == :account
63
+ api_attributes.merge!(instrument: args.first) if name == :instrument
62
64
 
63
- if response.body && !response.body.empty?
64
- api_result.merge!(JSON.parse(response.body))
65
- set_last_transaction_id(api_result['lastTransactionID']) if api_result['lastTransactionID']
66
- end
67
- end
68
-
69
- api_result
70
- when *api_methods
71
- set_last_action_and_arguments(name, args)
72
- set_account_id(args.first) if name == :account
73
- set_instrument(args.first) if name == :instrument
74
- self
65
+ Api.new(api_attributes)
75
66
  else
76
67
  super
77
68
  end
78
69
  end
79
70
 
80
- private
81
-
82
- attr_accessor :http_verb, :account_id, :instrument, :last_transaction_id, :last_action, :last_arguments, :last_api_request_at
83
-
84
- def api_methods
85
- Accounts.instance_methods + Instruments.instance_methods + Orders.instance_methods + Trades.instance_methods + Positions.instance_methods + Transactions.instance_methods + Pricing.instance_methods
86
- end
87
-
88
71
  def govern_api_request_rate
89
72
  return unless last_api_request_at[0]
90
- halt = 1 - (last_api_request_at[MAX_REQUESTS_PER_SECOND_ALLOWED - 1] - last_api_request_at[0])
73
+ halt = 1 - (last_api_request_at[max_requests_per_second - 1] - last_api_request_at[0])
91
74
  sleep halt if halt > 0
92
75
  end
93
76
 
94
- def set_last_api_request_at
95
- last_api_request_at.push(Time.now.utc).shift
96
- end
97
-
98
- def set_last_action_and_arguments(action, args)
99
- set_last_action(action)
100
- set_last_arguments(args)
101
- end
102
-
103
- def set_last_action(action)
104
- self.last_action = action
105
- end
106
-
107
- def set_last_arguments(args)
108
- self.last_arguments = args.nil? || args.empty? ? nil : args.flatten
109
- end
110
-
111
- def set_account_id(id)
112
- self.account_id = id
113
- end
114
-
115
- def set_instrument(instrument)
116
- self.instrument = instrument
117
- end
118
-
119
- def set_last_transaction_id(id)
120
- self.last_transaction_id = id
121
- end
122
-
123
- def set_http_verb(action, last_action)
124
- case action
125
- when :show
126
- self.http_verb = :get
127
- when :update, :cancel, :close
128
- [:configuration].include?(last_action) ? self.http_verb = :patch : self.http_verb = :put
129
- when :create
130
- self.http_verb = :post
131
- else
132
- self.http_verb = nil
77
+ def update_last_api_request_at
78
+ @mutex.synchronize do
79
+ last_api_request_at.push(Time.now.utc).shift
133
80
  end
134
81
  end
135
82
 
136
- def api_attributes
137
- {
138
- http_verb: http_verb,
139
- base_uri: base_uri,
140
- headers: headers,
141
- account_id: account_id,
142
- instrument: instrument,
143
- last_transaction_id: last_transaction_id
144
- }
145
- end
83
+ private
84
+
85
+ attr_accessor :last_api_request_at
146
86
  end
147
87
  end