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