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 +5 -5
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +5 -1
- data/CHANGELOG.md +27 -0
- data/README.md +55 -9
- data/lib/oanda_api_v20/accounts.rb +1 -1
- data/lib/oanda_api_v20/api.rb +90 -1
- data/lib/oanda_api_v20/client.rb +37 -97
- data/lib/oanda_api_v20/exceptions.rb +12 -1
- data/lib/oanda_api_v20/instruments.rb +1 -1
- data/lib/oanda_api_v20/pricing.rb +12 -0
- data/lib/oanda_api_v20/transactions.rb +12 -0
- data/lib/oanda_api_v20/version.rb +1 -1
- data/oanda_api_v20.gemspec +1 -1
- data/spec/oanda_api_v20/api_spec.rb +482 -251
- data/spec/oanda_api_v20/client_spec.rb +105 -114
- data/spec/oanda_api_v20/exceptions_spec.rb +24 -0
- data/spec/oanda_api_v20/oanda_api_v20_spec.rb +2 -2
- metadata +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: be0ef34e9d27b283681c25c8f281c5ec264f9b36b63b39715df9bc69de0209a9
|
4
|
+
data.tar.gz: ba7a3cd6ec27e820e58565fedc2ec6b7e723f954ae1381ef79548d4ca88469af
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39f4320e897a343a542e19155c36a6b80698ab7b57bc8d5e40da608ab6ec6845ba12cf9d1a79ef29c24a28850616ddf12760929fc15f78df7b9ff7aa5ea41eb3
|
7
|
+
data.tar.gz: e791adff74088c1f6da6a23454f30d13fe226828a9683d609b9ff910819a34008722a4badb6b0d7b29fb0b59b95064f5eb44844b2474a6004877c46d1f0f8dec
|
data/.gitignore
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.7.2
|
data/.travis.yml
CHANGED
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
|
-
'
|
133
|
-
'
|
134
|
-
'
|
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' => '
|
199
|
+
'price' => '2.5'
|
186
200
|
},
|
187
201
|
'stopLoss' => {
|
188
202
|
'timeInForce' => 'GTC',
|
189
|
-
'price' => '
|
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' =>
|
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
|
|
data/lib/oanda_api_v20/api.rb
CHANGED
@@ -8,12 +8,101 @@ module OandaApiV20
|
|
8
8
|
include Transactions
|
9
9
|
include Pricing
|
10
10
|
|
11
|
-
attr_accessor :
|
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
|
data/lib/oanda_api_v20/client.rb
CHANGED
@@ -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:
|
9
|
-
|
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
|
-
@
|
21
|
-
@
|
22
|
-
@
|
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:
|
46
|
+
pool_size: connection_pool_size
|
39
47
|
}
|
40
|
-
|
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
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
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[
|
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
|
95
|
-
|
96
|
-
|
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
|
-
|
137
|
-
|
138
|
-
|
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
|