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