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,38 @@
1
+ require 'spec_helper'
2
+ require 'support/client_helper'
3
+
4
+ describe "OandaAPI::Resource::Position" do
5
+ let(:account) { ClientHelper.account }
6
+
7
+ it "gets all open positions", :vcr do
8
+ VCR.use_cassette("account(id).positions.get") do
9
+ ClientHelper.create_trade
10
+ positions = account.positions.get
11
+ expect(positions.first).to be_an OandaAPI::Resource::Position
12
+ end
13
+ end
14
+
15
+ it "gets the position for an instrument", :vcr do
16
+ VCR.use_cassette("account(id).positions(instrument).get") do
17
+ ClientHelper.create_trade(instrument: "USD_JPY")
18
+ position = account.positions("USD_JPY").get
19
+ expect(position).to be_an OandaAPI::Resource::Position
20
+ end
21
+ end
22
+
23
+ it "closes an existing position", :vcr do
24
+ VCR.use_cassette("account(id).positions(instrument).close") do
25
+ ClientHelper.create_trade(instrument: "USD_JPY")
26
+
27
+ # Verify we have an open position.
28
+ position = account.position("USD_JPY").get
29
+ expect(position).to be_an OandaAPI::Resource::Position
30
+
31
+ account.position("USD_JPY").close
32
+
33
+ # Verify that the position has been closed.
34
+ positions = account.positions.get
35
+ expect(positions.map(&:instrument)).to_not include "USD_JPY"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+ require 'support/client_helper'
3
+
4
+ describe "OandaAPI::Resource" do
5
+ let(:client) { ClientHelper.client }
6
+
7
+ describe "Instrument" do
8
+ it "gets all instruments", :vcr do
9
+ VCR.use_cassette("instruments.get") do
10
+ instruments = client.instruments.get
11
+ expect(instruments.first).to be_an OandaAPI::Resource::Instrument
12
+ end
13
+ end
14
+
15
+ it "gets a filtered list of instruments", :vcr do
16
+ VCR.use_cassette("instruments(options).get") do
17
+ instruments = client.instruments(instruments: %w(EUR_USD EUR_CAD),
18
+ fields: %w(pip precision))
19
+ .get
20
+ expect(instruments.first).to be_an OandaAPI::Resource::Instrument
21
+ end
22
+ end
23
+ end
24
+
25
+ describe "Price" do
26
+ it "gets a filtered list of prices", :vcr do
27
+ VCR.use_cassette("prices(options).get") do
28
+ prices = client.prices(instruments: %w(EUR_USD EUR_CAD)).get
29
+ expect(prices.first).to be_an OandaAPI::Resource::Price
30
+ end
31
+ end
32
+ end
33
+
34
+ describe "Candle" do
35
+ it "gets candles for an instrument", :vcr do
36
+ VCR.use_cassette("candles(options).get") do
37
+ candles = client.candles(instrument: "EUR_USD",
38
+ granularity: "M1",
39
+ count: 1,
40
+ candle_format: "midpoint")
41
+ .get
42
+ expect(candles.first).to be_an OandaAPI::Resource::Candle
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+ require 'support/client_helper'
3
+
4
+ describe "OandaAPI::Resource::Trade" do
5
+ let(:account) { ClientHelper.account }
6
+
7
+ it "gets a list of trades", :vcr do
8
+ VCR.use_cassette("account(id).trades.get") do
9
+ ClientHelper.create_trade
10
+ trades = account.trades.get
11
+ expect(trades.first).to be_an OandaAPI::Resource::Trade
12
+ end
13
+ end
14
+
15
+ it "gets a filtered list of trades", :vcr do
16
+ VCR.use_cassette("account(id).trades(filter).get") do
17
+ ClientHelper.create_trade(instrument: "USD_JPY")
18
+ trades = account.trades(instrument: "USD_JPY").get
19
+ expect(trades.first).to be_an OandaAPI::Resource::Trade
20
+ end
21
+ end
22
+
23
+ it "gets information on a specific trade", :vcr do
24
+ VCR.use_cassette("account(id).trade(id).get") do
25
+ trade_id = ClientHelper.create_trade.trade_opened.id
26
+ trade = account.trade(trade_id).get
27
+ expect(trade).to be_an OandaAPI::Resource::Trade
28
+ end
29
+ end
30
+
31
+ it "modifies an open trade " do
32
+ VCR.use_cassette("account(id).trade(options).modify") do
33
+ order = ClientHelper.create_trade
34
+ modified_order =
35
+ account.trade(id: order.trade_opened.id, take_profit: order.price + 2.0)
36
+ .update
37
+ expect(modified_order).to be_an OandaAPI::Resource::Trade
38
+ end
39
+ end
40
+
41
+ it "closes an open trade" do
42
+ VCR.use_cassette("account(id).trade(id).close") do
43
+ ClientHelper.create_trade(instrument: "USD_JPY").trade_opened.id
44
+
45
+ # Get the oldest open trade for USD_JPY, so we can close that trade.
46
+ oldest_trade =
47
+ account.trades(instrument: "USD_JPY")
48
+ .get
49
+ .min_by(&:id)
50
+
51
+ closed_trade = account.trade(oldest_trade.id).close
52
+ expect(closed_trade).to be_an OandaAPI::Resource::Trade
53
+
54
+ open_trades = account.trades.get
55
+ expect(open_trades.map(&:id)).to_not include oldest_trade.id
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+ require 'support/client_helper'
3
+
4
+ describe "OandaAPI::Transaction" do
5
+ let(:account) { ClientHelper.account }
6
+
7
+ it "gets transaction history" do
8
+ VCR.use_cassette("account(id).transactions(options).get") do
9
+ ClientHelper.create_trade(instrument: "USD_JPY")
10
+ transactions = account.transactions(instrument: "USD_JPY").get
11
+ expect(transactions.first).to be_an OandaAPI::Resource::Transaction
12
+ end
13
+ end
14
+
15
+ it "gets information for a specific transaction" do
16
+ VCR.use_cassette("account(id).transaction(id).get") do
17
+ ClientHelper.create_trade(instrument: "USD_JPY")
18
+ id = account.transactions(instrument: "USD_JPY").get.first.id
19
+ transaction = account.transaction(id).get
20
+ expect(transaction).to be_an OandaAPI::Resource::Transaction
21
+ expect(transaction.trade_opened).to be_a OandaAPI::Resource::Transaction::TradeOpened
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ describe "OandaAPI::ResourceCollection" do
4
+ let(:resource_descriptor) { OandaAPI::Client::ResourceDescriptor.new "/candles", :get }
5
+
6
+ describe "#initialize" do
7
+ context "with a collection attribute matching the one expected by the resource descriptor" do
8
+ let(:resource_collection) { OandaAPI::ResourceCollection.new({ candles: [{ volume: 10 }] }, resource_descriptor) }
9
+
10
+ it "returns a ResourceCollection" do
11
+ expect(resource_collection).to be_an(OandaAPI::ResourceCollection)
12
+ end
13
+
14
+ it "is Enumerable" do
15
+ expect(resource_collection).to be_an Enumerable
16
+ end
17
+
18
+ it "contains elements of the expected resource" do
19
+ expect(resource_collection.first).to be_an OandaAPI::Resource::Candle
20
+ end
21
+
22
+ it "contains initialized resource elements" do
23
+ expect(resource_collection.first.volume).to eq 10
24
+ end
25
+
26
+ describe "when the collection element is empty" do
27
+ let(:resource_collection) { OandaAPI::ResourceCollection.new({ candles: [] }, resource_descriptor) }
28
+ it "returns a ResourceCollection" do
29
+ expect(resource_collection).to be_an(OandaAPI::ResourceCollection)
30
+ end
31
+ it "is an empty collection" do
32
+ expect(resource_collection.empty?).to be true
33
+ end
34
+ end
35
+
36
+ describe "with extra attributes" do
37
+ let(:resource_collection) { OandaAPI::ResourceCollection.new({ candles: [], extra: "I'm special" }, resource_descriptor) }
38
+
39
+ it "returns a ResourceCollection" do
40
+ expect(resource_collection).to be_an(OandaAPI::ResourceCollection)
41
+ end
42
+
43
+ it "has an accessor for the extra attributes" do
44
+ expect(resource_collection.extra).to eq "I'm special"
45
+ end
46
+ end
47
+ end
48
+
49
+ context "without the collection attribute expected by the resource descriptor " do
50
+ let(:resource_collection) { OandaAPI::ResourceCollection.new({}, resource_descriptor) }
51
+
52
+ it "returns a ResourceCollection" do
53
+ expect(resource_collection).to be_an(OandaAPI::ResourceCollection)
54
+ end
55
+
56
+ it "is Enumerable" do
57
+ expect(resource_collection).to be_an Enumerable
58
+ end
59
+
60
+ it "is an empty collection" do
61
+ expect(resource_collection.empty?).to be true
62
+ end
63
+
64
+ describe "with extra attributes" do
65
+ let(:resource_collection) { OandaAPI::ResourceCollection.new({ extra: "I'm special" }, resource_descriptor) }
66
+
67
+ it "returns a ResourceCollection" do
68
+ expect(resource_collection).to be_an(OandaAPI::ResourceCollection)
69
+ end
70
+
71
+ it "has an accessor for the extra attribute" do
72
+ expect(resource_collection.extra).to eq "I'm special"
73
+ end
74
+ end
75
+ end
76
+
77
+ context "with nil" do
78
+ let(:resource_collection) { OandaAPI::ResourceCollection.new(nil, resource_descriptor) }
79
+
80
+ it "returns a ResourceCollection" do
81
+ expect(resource_collection).to be_an(OandaAPI::ResourceCollection)
82
+ end
83
+
84
+ it "is Enumerable" do
85
+ expect(resource_collection).to be_an Enumerable
86
+ end
87
+
88
+ it "is an empty collection" do
89
+ expect(resource_collection.empty?).to be true
90
+ end
91
+ end
92
+ end
93
+
94
+ describe "#respond_to?" do
95
+ let(:resource_collection) { OandaAPI::ResourceCollection.new({ candles: [], extra: "I'm special" }, resource_descriptor) }
96
+
97
+ it "responds to collection methods" do
98
+ expect(resource_collection.respond_to?(:shuffle)).to be true
99
+ end
100
+
101
+ it "responds to dynamic accessors for extra attributes" do
102
+ expect(resource_collection.respond_to?(:extra)).to be true
103
+ end
104
+
105
+ it "responds to concrete accessors" do
106
+ expect(resource_collection.respond_to?(:location)).to be true
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ describe "OandaAPI::Utils" do
4
+ describe ".camelize" do
5
+ it "puts humps on a string starting out low" do
6
+ expect(OandaAPI::Utils.camelize("ruby_is_awesome")).to eq("rubyIsAwesome")
7
+ end
8
+
9
+ it "works with already camelized" do
10
+ expect(OandaAPI::Utils.camelize("imACamel")).to eq("imACamel")
11
+ end
12
+
13
+ it "works with numbers" do
14
+ expect(OandaAPI::Utils.camelize("a_number_1_concern")).to eq("aNumber1Concern")
15
+ end
16
+
17
+ it "works with symbols" do
18
+ expect(OandaAPI::Utils.camelize(:ugly_symbol)).to eq("uglySymbol")
19
+ end
20
+
21
+ it "turns nil to empty" do
22
+ expect(OandaAPI::Utils.camelize(nil)).to eq("")
23
+ end
24
+
25
+ it "ignores empty" do
26
+ expect(OandaAPI::Utils.camelize("")).to eq("")
27
+ end
28
+ end
29
+
30
+ describe ".pluralize" do
31
+ it "naively adds an s to the end of strings" do
32
+ expect(OandaAPI::Utils.pluralize("thing")).to eq("things")
33
+ end
34
+
35
+ it "doesn't add an s if it's already there" do
36
+ expect(OandaAPI::Utils.pluralize("things")).to eq("things")
37
+ end
38
+
39
+ it "turns nil to empty" do
40
+ expect(OandaAPI::Utils.pluralize(nil)).to eq("")
41
+ end
42
+
43
+ it "ignores empty" do
44
+ expect(OandaAPI::Utils.pluralize("")).to eq("")
45
+ end
46
+ end
47
+
48
+ describe ".singularize" do
49
+ it "naively removes trailing s" do
50
+ expect(OandaAPI::Utils.singularize("things")).to eq("thing")
51
+ end
52
+
53
+ it "turns nil to empty" do
54
+ expect(OandaAPI::Utils.singularize(nil)).to eq("")
55
+ end
56
+
57
+ it "ignores empty" do
58
+ expect(OandaAPI::Utils.singularize("")).to eq("")
59
+ end
60
+ end
61
+
62
+ describe ".rubyize_keys" do
63
+ it "converts all keys in a hash into snake_cased_symbols, " do
64
+ hash = { "a" => "a", "b" => [{ "a" => "a" }], :c => "c", "camelCase" => "" }
65
+ symbolized_hash = { :a => "a", :b => [{ :a => "a" }], :c => "c", :camel_case => "" }
66
+ expect(OandaAPI::Utils.rubyize_keys(hash)).to eq(symbolized_hash)
67
+ end
68
+ end
69
+
70
+ describe ".underscore" do
71
+ it "converts a string to snake_case" do
72
+ expect(OandaAPI::Utils.underscore("camelCase-dashedTLA")).to eq("camel_case_dashed_tla")
73
+ end
74
+
75
+ it "turns nil to empty" do
76
+ expect(OandaAPI::Utils.underscore(nil)).to eq("")
77
+ end
78
+
79
+ it "ignores empty" do
80
+ expect(OandaAPI::Utils.underscore("")).to eq("")
81
+ end
82
+ end
83
+
84
+ describe ".transform_hash_keys" do
85
+ it "yields every key in a nested hash" do
86
+ hash = { "a" => "a", :b => [{ :c => "c" }], :d => "d" }
87
+ expect { |b| OandaAPI::Utils.transform_hash_keys(hash, &b) }.to yield_successive_args("a", :b, :c, :d)
88
+ end
89
+
90
+ it "replaces hash keys with the yielded return values" do
91
+ hash = { "a" => "a", "b" => [{ "c" => "c" }], "d" => "d" }
92
+ transformed_hash = { :a => "a", :b => [{ :c => "c" }], :d => "d" }
93
+ expect(OandaAPI::Utils.transform_hash_keys(hash) { |key| key.to_sym }).to eq(transformed_hash)
94
+ end
95
+ end
96
+
97
+ describe ".transform_hash_values" do
98
+ it "yields every scalar key,value pair in a nested hash" do
99
+ hash = { "a" => "a", :b => [{ :c => "c" }], :d => "d" }
100
+ expect { |b| OandaAPI::Utils.transform_hash_values(hash, &b) }.to yield_successive_args(["a", "a"], [:c, "c"], [:d, "d"])
101
+ end
102
+
103
+ it "replaces scalar hash element values with the yielded return values" do
104
+ hash = { :a => "a", :b => [{ :c => "c" }], :d => "d" }
105
+ transformed_hash = { :a => :a, :b => [{ :c => :c }], :d => :d }
106
+ expect(OandaAPI::Utils.transform_hash_values(hash) { |_key, value| value.to_sym }).to eq(transformed_hash)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,10 @@
1
+ lib = File.expand_path("../../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'oanda_api'
4
+
5
+ RSpec.configure do |c|
6
+ c.before(:suite) do
7
+ # Disable request throttling because all examples are stubbed with VCR and webmock.
8
+ OandaAPI.configure { |config| config.use_request_throttling = false }
9
+ end
10
+ end
@@ -0,0 +1,60 @@
1
+ require 'support/vcr'
2
+ module ClientHelper
3
+ # Returns a sandbox client
4
+ def self.client
5
+ VCR.use_cassette("sandbox_client") do
6
+ client = OandaAPI::Client::UsernameClient.new "_"
7
+ new_account = client.account.create
8
+ OandaAPI::Client::UsernameClient.new new_account.username
9
+ end
10
+ end
11
+
12
+ # Returns the account namespace for the client
13
+ def self.account
14
+ VCR.use_cassette("sandbox_client_account") do
15
+ account_id = ClientHelper.client.accounts.get.first.account_id
16
+ ClientHelper.client.account(account_id)
17
+ end
18
+ end
19
+
20
+ # Creates a trade
21
+ def self.create_trade(instrument: "USD_JPY",
22
+ type: "market",
23
+ side: "buy",
24
+ units: 10_000,
25
+ price: nil,
26
+ expiry: (Time.now + 3600).utc.to_datetime.rfc3339)
27
+ opts =
28
+ case type
29
+ when "market"
30
+ { instrument: instrument,
31
+ type: type,
32
+ side: side,
33
+ units: units }
34
+ when "limit"
35
+ { instrument: instrument,
36
+ type: type,
37
+ side: side,
38
+ units: units,
39
+ price: price,
40
+ expiry: expiry }
41
+ else
42
+ fail ArgumentError, "invalid order type: #{type}"
43
+ end
44
+ account.order(opts).create
45
+ end
46
+
47
+ singleton_class.send(:alias_method, :create_order, :create_trade)
48
+
49
+ # Returns a lightweight instrument object
50
+ def self.instrument(instrument)
51
+ VCR.use_cassette("sandbox_instrument_#{instrument}") do
52
+ price = ClientHelper.client.prices(instruments: instrument).get.first
53
+ o = Struct.new(:instrument, :bid, :ask).new
54
+ o.instrument = instrument
55
+ o.bid = price.bid
56
+ o.ask = price.ask
57
+ o
58
+ end
59
+ end
60
+ end