oanda_api 0.8.1 → 0.8.3

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.
Files changed (73) hide show
  1. data/.gitignore +38 -0
  2. data/.rspec_non_jruby +2 -0
  3. data/.yardopts +6 -0
  4. data/Gemfile +13 -0
  5. data/Guardfile +7 -0
  6. data/LICENSE +22 -0
  7. data/README.md +218 -0
  8. data/Rakefile +23 -0
  9. data/lib/oanda_api.rb +25 -0
  10. data/lib/oanda_api/client/client.rb +175 -0
  11. data/lib/oanda_api/client/namespace_proxy.rb +112 -0
  12. data/lib/oanda_api/client/resource_descriptor.rb +52 -0
  13. data/lib/oanda_api/client/token_client.rb +69 -0
  14. data/lib/oanda_api/client/username_client.rb +53 -0
  15. data/lib/oanda_api/configuration.rb +167 -0
  16. data/lib/oanda_api/errors.rb +4 -0
  17. data/lib/oanda_api/resource/account.rb +37 -0
  18. data/lib/oanda_api/resource/candle.rb +29 -0
  19. data/lib/oanda_api/resource/instrument.rb +21 -0
  20. data/lib/oanda_api/resource/order.rb +74 -0
  21. data/lib/oanda_api/resource/position.rb +18 -0
  22. data/lib/oanda_api/resource/price.rb +16 -0
  23. data/lib/oanda_api/resource/trade.rb +23 -0
  24. data/lib/oanda_api/resource/transaction.rb +67 -0
  25. data/lib/oanda_api/resource_base.rb +35 -0
  26. data/lib/oanda_api/resource_collection.rb +77 -0
  27. data/lib/oanda_api/utils/utils.rb +101 -0
  28. data/lib/oanda_api/version.rb +3 -0
  29. data/oanda_api.gemspec +32 -0
  30. data/spec/fixtures/vcr_cassettes/account_id_order_id_close.yml +264 -0
  31. data/spec/fixtures/vcr_cassettes/account_id_order_id_get.yml +114 -0
  32. data/spec/fixtures/vcr_cassettes/account_id_order_options_create.yml +74 -0
  33. data/spec/fixtures/vcr_cassettes/account_id_order_options_update.yml +112 -0
  34. data/spec/fixtures/vcr_cassettes/account_id_orders_get.yml +118 -0
  35. data/spec/fixtures/vcr_cassettes/account_id_orders_options_get.yml +123 -0
  36. data/spec/fixtures/vcr_cassettes/account_id_positions_get.yml +112 -0
  37. data/spec/fixtures/vcr_cassettes/account_id_positions_instrument_close.yml +214 -0
  38. data/spec/fixtures/vcr_cassettes/account_id_positions_instrument_get.yml +110 -0
  39. data/spec/fixtures/vcr_cassettes/account_id_trade_id_close.yml +252 -0
  40. data/spec/fixtures/vcr_cassettes/account_id_trade_id_get.yml +112 -0
  41. data/spec/fixtures/vcr_cassettes/account_id_trade_options_modify.yml +110 -0
  42. data/spec/fixtures/vcr_cassettes/account_id_trades_filter_get.yml +118 -0
  43. data/spec/fixtures/vcr_cassettes/account_id_trades_get.yml +118 -0
  44. data/spec/fixtures/vcr_cassettes/account_id_transaction_id_get.yml +283 -0
  45. data/spec/fixtures/vcr_cassettes/account_id_transactions_options_get.yml +205 -0
  46. data/spec/fixtures/vcr_cassettes/accounts_create.yml +75 -0
  47. data/spec/fixtures/vcr_cassettes/accounts_get.yml +111 -0
  48. data/spec/fixtures/vcr_cassettes/accounts_id_get.yml +187 -0
  49. data/spec/fixtures/vcr_cassettes/candles_options_get.yml +79 -0
  50. data/spec/fixtures/vcr_cassettes/instruments_get.yml +501 -0
  51. data/spec/fixtures/vcr_cassettes/instruments_options_get.yml +81 -0
  52. data/spec/fixtures/vcr_cassettes/prices_options_get.yml +81 -0
  53. data/spec/fixtures/vcr_cassettes/sandbox_client.yml +116 -0
  54. data/spec/fixtures/vcr_cassettes/sandbox_client_account.yml +111 -0
  55. data/spec/fixtures/vcr_cassettes/sandbox_instrument_EUR_USD.yml +77 -0
  56. data/spec/oanda_api/client/client_spec.rb +107 -0
  57. data/spec/oanda_api/client/namespace_proxy_spec.rb +16 -0
  58. data/spec/oanda_api/client/resource_descriptor_spec.rb +39 -0
  59. data/spec/oanda_api/client/token_client_spec.rb +60 -0
  60. data/spec/oanda_api/client/username_client_spec.rb +31 -0
  61. data/spec/oanda_api/configuration_spec.rb +138 -0
  62. data/spec/oanda_api/examples/accounts_spec.rb +28 -0
  63. data/spec/oanda_api/examples/orders_spec.rb +68 -0
  64. data/spec/oanda_api/examples/positions_spec.rb +38 -0
  65. data/spec/oanda_api/examples/rates_spec.rb +46 -0
  66. data/spec/oanda_api/examples/trades_spec.rb +58 -0
  67. data/spec/oanda_api/examples/transactions_spec.rb +24 -0
  68. data/spec/oanda_api/resource_collection_spec.rb +109 -0
  69. data/spec/oanda_api/utils/utils_spec.rb +109 -0
  70. data/spec/spec_helper.rb +10 -0
  71. data/spec/support/client_helper.rb +60 -0
  72. data/spec/support/vcr.rb +7 -0
  73. metadata +124 -9
@@ -0,0 +1,107 @@
1
+ require 'spec_helper'
2
+ require 'uri'
3
+ require 'webmock/rspec'
4
+
5
+ describe "OandaAPI::Client" do
6
+ describe ".query_string_normalizer" do
7
+ let(:query_string_normalizer) { OandaAPI::Client.default_options[:query_string_normalizer] }
8
+
9
+ it "responds to :call" do
10
+ expect(query_string_normalizer.respond_to? :call).to be true
11
+ end
12
+
13
+ it "converts a hash to a query params string with camelized keys" do
14
+ hash = { ruby_key1: 1 }
15
+ expect(query_string_normalizer.call(hash)).to eq("rubyKey1=1")
16
+ end
17
+
18
+ it "sorts query parameters alphabetically" do
19
+ hash = { ruby_key: 1, a_b_c: 1 }
20
+ expect(query_string_normalizer.call(hash)).to eq("aBC=1&rubyKey=1")
21
+ end
22
+
23
+ it "URI encodes the parameter values" do
24
+ hash = { key: "a,b,c" }
25
+ expect(query_string_normalizer.call(hash)).to eq("key=a%2Cb%2Cc")
26
+ end
27
+
28
+ it "converts array values into comma-delimited strings" do
29
+ hash = { key: %w(a b c) }
30
+ expect(query_string_normalizer.call(hash)).to eq("key=a%2Cb%2Cc")
31
+ end
32
+
33
+ it "works with nil values" do
34
+ hash = { camel_key: nil }
35
+ expect(query_string_normalizer.call(hash)).to eq("camelKey")
36
+ end
37
+ end
38
+
39
+ describe ".throttle_request_rate" do
40
+ before(:all) do
41
+ @throttle_setting = OandaAPI.configuration.use_request_throttling
42
+ end
43
+ after(:all) do
44
+ OandaAPI.configuration.use_request_throttling = @throttle_setting
45
+ end
46
+
47
+ context "without request throttling" do
48
+ it "does not throttle" do
49
+ OandaAPI.configuration.use_request_throttling = false
50
+ last_throttled_at = OandaAPI::Client.last_throttled_at
51
+
52
+ (OandaAPI.configuration.max_requests_per_second + 2).times do
53
+ OandaAPI::Client.throttle_request_rate
54
+ end
55
+ expect(OandaAPI::Client.last_throttled_at).to eq last_throttled_at
56
+ end
57
+ end
58
+
59
+ context "with request throttling" do
60
+ it "limits the number of requests per second" do
61
+ OandaAPI.configuration.use_request_throttling = true
62
+ before_throttled_at = Time.now
63
+
64
+ (OandaAPI.configuration.max_requests_per_second + 2).times do
65
+ OandaAPI::Client.throttle_request_rate
66
+ end
67
+ expect(OandaAPI::Client.last_throttled_at).to be > before_throttled_at
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "#api_uri" do
73
+ let(:client) { Struct.new(:domain) { include OandaAPI::Client }.new }
74
+ it "is domain specific" do
75
+ uris = {}
76
+ OandaAPI::DOMAINS.each do |domain|
77
+ client.domain = domain
78
+ uris[client.api_uri("/path")] = domain
79
+ end
80
+ expect(uris.size).to eq(3)
81
+ end
82
+
83
+ it "is an absolute URI" do
84
+ OandaAPI::DOMAINS.each do |domain|
85
+ client.domain = domain
86
+ uri = URI.parse client.api_uri("/path")
87
+ expect(uri.absolute?).to be true
88
+ end
89
+ end
90
+ end
91
+
92
+ describe "#execute_request" do
93
+ let(:client) { OandaAPI::Client::UsernameClient.new "spongebob" }
94
+
95
+ it "returns a response for a successful request" do
96
+ stub_request(:get, "http://api-sandbox.oanda.com/v1/accounts?username=spongebob")
97
+ .to_return(status: 200, body: "{\"accounts\" : []}", headers: { "content-type" => "application/json" })
98
+ expect(client.execute_request(:get, "/accounts")).to be_an OandaAPI::ResourceCollection
99
+ end
100
+
101
+ it "raises OandaAPI::RequestError for an invalid request" do
102
+ stub_request(:get, "http://api-sandbox.oanda.com/v1/accounts/a_bad_request?username=spongebob")
103
+ .to_return(status: [404, "Something bad happened (but we still love you)."])
104
+ expect { client.execute_request :get, "/accounts/a_bad_request" }.to raise_error(OandaAPI::RequestError)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe "OandaAPI::Client::NamespaceProxy" do
4
+ let(:client) { OandaAPI::Client::UsernameClient.new("spongebob") }
5
+ let(:namespace_proxy) { OandaAPI::Client::NamespaceProxy.new client, "account" }
6
+
7
+ describe "#initialize" do
8
+ it "raises ArgumentError unless an OandaAPI::Client is passed" do
9
+ expect { OandaAPI::Client::NamespaceProxy.new nil, "account" }.to raise_error(ArgumentError)
10
+ end
11
+
12
+ it "raises ArgumentError unless a non-empty namespace string is passed" do
13
+ expect { OandaAPI::Client::NamespaceProxy.new client, "" }.to raise_error(ArgumentError)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe "OandaAPI::Client::ResourceDescriptor" do
4
+ describe "#initialize" do
5
+ let(:resource_descriptor) { OandaAPI::Client::ResourceDescriptor.new "/accounts", :get }
6
+ it "sets path" do
7
+ expect(resource_descriptor.path).to eq("/accounts")
8
+ end
9
+ end
10
+
11
+ context "when the path ends with a known resource" do
12
+ let(:resource_descriptor) { OandaAPI::Client::ResourceDescriptor.new "/account/123/orders", :get }
13
+
14
+ describe "#resource_klass" do
15
+ it "is the requested resource class" do
16
+ expect(resource_descriptor.resource_klass).to be(OandaAPI::Resource::Order)
17
+ end
18
+ end
19
+ end
20
+
21
+ describe "#is_collection?" do
22
+ it "is true for GET requests without a resource id" do
23
+ resource_descriptor = OandaAPI::Client::ResourceDescriptor.new "/account/123/orders", :get
24
+ expect(resource_descriptor.is_collection?).to be true
25
+ end
26
+
27
+ it "is false for GET requests with a resource id" do
28
+ resource_descriptor = OandaAPI::Client::ResourceDescriptor.new "/account/123", :get
29
+ expect(resource_descriptor.is_collection?).to be false
30
+ end
31
+
32
+ it "is false for non-GET requests" do
33
+ [:delete, :patch, :post].each do |verb|
34
+ resource_descriptor = OandaAPI::Client::ResourceDescriptor.new "/account", verb
35
+ expect(resource_descriptor.is_collection?).to be false
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe "OandaAPI::Client::TokenClient" do
4
+ let(:client) { OandaAPI::Client::TokenClient.new(:practice, "token") }
5
+
6
+ describe "#initialize" do
7
+ it "creates a TokenClient instance" do
8
+ expect(client).to be_a(OandaAPI::Client::TokenClient)
9
+ end
10
+
11
+ it "sets the authorization token" do
12
+ expect(client.auth_token).to eq("token")
13
+ end
14
+
15
+ it "sets the domain" do
16
+ expect(client.domain).to eq(:practice)
17
+ end
18
+
19
+ it "sets authorization headers" do
20
+ expect(client.headers).to eq(client.auth)
21
+ end
22
+
23
+ it "sets default_params to {}" do
24
+ expect(client.default_params).to eq({})
25
+ end
26
+ end
27
+
28
+ describe "#auth" do
29
+ it "returns a hash with an Authorization key" do
30
+ expect(client.auth["Authorization"]).to eq("Bearer token")
31
+ end
32
+ end
33
+
34
+ describe "#domain=" do
35
+ OandaAPI::DOMAINS.each do |domain|
36
+ it "allows domain :#{domain} " do
37
+ client.domain = domain
38
+ expect(client.domain).to be(domain)
39
+ end
40
+ end
41
+
42
+ it "doesn't allow invalid domains" do
43
+ expect { client.domain = :bogus }.to raise_error(ArgumentError)
44
+ end
45
+ end
46
+
47
+ describe "default_params" do
48
+ it "has a reader and writer" do
49
+ client.default_params = { key: "value" }
50
+ expect(client.default_params).to eq(key: "value")
51
+ end
52
+ end
53
+
54
+ describe "headers" do
55
+ it "has a reader and writer" do
56
+ client.headers = { key: "value" }
57
+ expect(client.headers).to eq(key: "value")
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe "OandaAPI::Client::UsernameClient" do
4
+ let(:client) { OandaAPI::Client::UsernameClient.new("spongebob") }
5
+
6
+ describe "#initialize" do
7
+ it "sets the username" do
8
+ expect(client.username).to eq("spongebob")
9
+ end
10
+
11
+ it "sets domain to sandbox" do
12
+ expect(client.domain).to eq(:sandbox)
13
+ end
14
+
15
+ it "sets default_params to include username" do
16
+ expect(client.default_params).to include("username" => "spongebob")
17
+ end
18
+ end
19
+
20
+ describe "#auth" do
21
+ it "returns a hash with username" do
22
+ expect(client.auth["username"]).to eq("spongebob")
23
+ end
24
+ end
25
+
26
+ describe "#default_params" do
27
+ it "initialize as #auth" do
28
+ expect(client.default_params).to eq(client.auth)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,138 @@
1
+ require 'spec_helper'
2
+
3
+ describe "OandaAPI::Configuration" do
4
+ let(:config) { OandaAPI::Configuration.new }
5
+
6
+ describe "#datetime_format" do
7
+ it "returns the default datetime format" do
8
+ expect(config.datetime_format).to eq OandaAPI::Configuration::DATETIME_FORMAT
9
+ end
10
+ end
11
+
12
+ describe "#datetime_format=" do
13
+ it "overrides the default datetime format" do
14
+ config.datetime_format = :unix
15
+ expect(config.datetime_format).to eq :unix
16
+
17
+ config.datetime_format = :rfc3339
18
+ expect(config.datetime_format).to eq :rfc3339
19
+ end
20
+
21
+ it "must be a valid datetime format" do
22
+ expect { config.datetime_format = "X" }.to raise_error(ArgumentError)
23
+ end
24
+ end
25
+
26
+ describe "#rest_api_version" do
27
+ it "returns the default rest api version" do
28
+ expect(config.rest_api_version).to eq OandaAPI::Configuration::REST_API_VERSION
29
+ end
30
+ end
31
+
32
+ describe "#rest_api_version=" do
33
+ it "overrides the default rest api version" do
34
+ config.rest_api_version = "X"
35
+ expect(config.rest_api_version).to eq "X"
36
+ end
37
+ end
38
+
39
+ describe "#max_requests_per_second" do
40
+ it "returns the default max requests per second" do
41
+ expect(config.max_requests_per_second).to eq OandaAPI::Configuration::MAX_REQUESTS_PER_SECOND
42
+ end
43
+ end
44
+
45
+ describe "#max_requests_per_second=" do
46
+ it "overrides the default max requests per second" do
47
+ config.max_requests_per_second = 50
48
+ expect(config.max_requests_per_second).to eq 50
49
+ end
50
+
51
+ it "must be numeric" do
52
+ expect { config.max_requests_per_second = "X" }.to raise_error(ArgumentError)
53
+ end
54
+
55
+ it "must be > 0" do
56
+ [-10, 0].each do |val|
57
+ expect { config.max_requests_per_second = val }.to raise_error(ArgumentError)
58
+ end
59
+ end
60
+ end
61
+
62
+ describe "#min_request_interval" do
63
+ it "is the inverse of #max_requests_per_second" do
64
+ config.max_requests_per_second = 2
65
+ expect(config.min_request_interval).to eq(0.5)
66
+ end
67
+ end
68
+
69
+ describe "#open_timeout" do
70
+ it "returns the default open timeout" do
71
+ expect(config.open_timeout).to eq OandaAPI::Configuration::OPEN_TIMEOUT
72
+ end
73
+ end
74
+
75
+ describe "#open_timeout=" do
76
+ it "overrides the default open timeout" do
77
+ config.open_timeout = 20
78
+ expect(config.open_timeout).to eq 20
79
+ end
80
+
81
+ it "must be numeric" do
82
+ expect { config.open_timeout = "X" }.to raise_error(ArgumentError)
83
+ end
84
+ end
85
+
86
+ describe "#read_timeout" do
87
+ it "returns the default read timeout" do
88
+ expect(config.read_timeout).to eq OandaAPI::Configuration::READ_TIMEOUT
89
+ end
90
+ end
91
+
92
+ describe "#read_timeout=" do
93
+ it "overrides the default read timeout" do
94
+ config.read_timeout = 20
95
+ expect(config.read_timeout).to eq 20
96
+ end
97
+
98
+ it "must be numeric" do
99
+ expect { config.read_timeout = "X" }.to raise_error(ArgumentError)
100
+ end
101
+ end
102
+
103
+ describe "#use_compression=" do
104
+ it "enables compression" do
105
+ config.use_compression = true
106
+ expect(config.use_compression?).to be true
107
+ expect(config.headers).to include("Accept-Encoding" => "deflate, gzip")
108
+ end
109
+
110
+ it "disables compression" do
111
+ config.use_compression = false
112
+ expect(config.use_compression?).to be false
113
+ expect(config.headers).to_not include("Accept-Encoding" => "deflate, gzip")
114
+ end
115
+ end
116
+
117
+ describe "#use_request_throttling=" do
118
+ it "enables throttling" do
119
+ config.use_request_throttling = true
120
+ expect(config.use_request_throttling?).to be true
121
+ end
122
+
123
+ it "disables throttling" do
124
+ config.use_request_throttling = false
125
+ expect(config.use_request_throttling?).to be false
126
+ end
127
+ end
128
+
129
+ describe "#headers" do
130
+ it "is a hash" do
131
+ expect(config.headers).to be_a Hash
132
+ end
133
+
134
+ it "includes a date format header" do
135
+ expect(config.headers).to include("X-Accept-Datetime-Format")
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+ require 'support/client_helper'
3
+
4
+ describe "OandaAPI::Resource::Account" do
5
+ let(:client) { ClientHelper.client }
6
+
7
+ it "creates a new account" do
8
+ VCR.use_cassette("accounts.create") do
9
+ account = client.accounts.create
10
+ expect(account).to be_an OandaAPI::Resource::Account
11
+ end
12
+ end
13
+
14
+ it "gets all accounts", :vcr do
15
+ VCR.use_cassette("accounts.get") do
16
+ accounts = client.accounts.get
17
+ expect(accounts.first).to be_an OandaAPI::Resource::Account
18
+ end
19
+ end
20
+
21
+ it "gets a specific account", :vcr do
22
+ VCR.use_cassette("accounts(id).get") do
23
+ account_id = client.accounts.get.first.account_id
24
+ account = client.accounts(account_id).get
25
+ expect(account).to be_an OandaAPI::Resource::Account
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+ require 'support/client_helper'
3
+
4
+ describe "OandaAPI::Resource::Order" do
5
+ let(:account) { ClientHelper.account }
6
+ let(:price) { ClientHelper.instrument("EUR_USD").bid - 0.1 }
7
+
8
+ it "creates an order", :vcr do
9
+ VCR.use_cassette("account(id).order(options).create") do
10
+ order = account.order(instrument: "EUR_USD",
11
+ type: "market",
12
+ side: "buy",
13
+ units: 10_000)
14
+ .create
15
+ expect(order).to be_an OandaAPI::Resource::Order
16
+ end
17
+ end
18
+
19
+ it "gets all open orders", :vcr do
20
+ VCR.use_cassette("account(id).orders.get") do
21
+ ClientHelper.create_order(type: "limit", price: price)
22
+ orders = account.orders.get
23
+ expect(orders.first).to be_an OandaAPI::Resource::Order
24
+ end
25
+ end
26
+
27
+ it "gets a filtered list of open orders", :vcr do
28
+ VCR.use_cassette("account(id).orders(options).get") do
29
+ ClientHelper.create_order(instrument: "USD_JPY", type: "limit", price: price)
30
+
31
+ orders = account.orders(instrument: "USD_JPY").get
32
+ expect(orders.first).to be_an OandaAPI::Resource::Order
33
+ end
34
+ end
35
+
36
+ it "gets a specific open order", :vcr do
37
+ VCR.use_cassette("account(id).order(id).get") do
38
+ order_id = ClientHelper.create_order(type: "limit", price: price)
39
+ .order_opened
40
+ .id
41
+ expect(account.order(order_id).get).to be_an OandaAPI::Resource::Order
42
+ end
43
+ end
44
+
45
+ it "modifies an open order", :vcr do
46
+ VCR.use_cassette("account(id).order(options).update") do
47
+ order_id = ClientHelper.create_order(type: "limit", price: price)
48
+ .order_opened
49
+ .id
50
+ updated_order = account.order(id: order_id, units: 9_000).update
51
+ expect(updated_order.units).to eq 9_000
52
+ end
53
+ end
54
+
55
+ it "closes an open order", :vcr do
56
+ VCR.use_cassette("account(id).order(id).close") do
57
+ order_id = ClientHelper.create_order(type: "limit", price: price)
58
+ .order_opened
59
+ .id
60
+ before_order_ids = account.orders.get.map(&:id)
61
+ account.order(order_id).close
62
+ after_order_ids = account.orders.get.map(&:id)
63
+
64
+ expect(before_order_ids).to include order_id
65
+ expect(after_order_ids).not_to include order_id
66
+ end
67
+ end
68
+ end