oanda_api_v20 1.5.0 → 2.2.0

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
- 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