oanda_api 0.8.1 → 0.8.3

Sign up to get free protection for your applications and to get access to all the features.
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