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,112 @@
1
+ module OandaAPI
2
+ module Client
3
+ # A client proxy and method-chaining enabler.
4
+ #
5
+ # @example Example usage
6
+ # client = OandaAPI::Client::TokenClient.new :practice, token
7
+ # account = client.account(1234) # => OandaAPI::Client::NamespaceProxy
8
+ # account.get # => OandaAPI::Resource::Account
9
+ # account.orders.get # => OandaAPI::Resource::ResourceCollection
10
+ #
11
+ # @!attribute [rw] conditions
12
+ # @return [Hash] a collection of parameters that typically specifies
13
+ # conditions and filters for a resource request.
14
+ #
15
+ # @!attribute [rw] namespace_segments
16
+ # @return [Array<String>] an ordered list of namespaces, when joined,
17
+ # creates a path to a resource URI.
18
+ class NamespaceProxy
19
+ attr_accessor :conditions, :namespace_segments
20
+
21
+ # @param [OandaAPI::Client] client
22
+ #
23
+ # @param [String] namespace_segment a _segment_ in a resource's URI. An
24
+ # ordered list of segments, joined, creates a path to a resource URI.
25
+ #
26
+ # @param [Hash] conditions an optional list of parameters that typically
27
+ # specifies conditions and filters for a resource request. A a _"key"_
28
+ # or _"id"_ is a condition that identifies a particular resource. If a
29
+ # key condition is included, it is extracted and added as a namespace
30
+ # segment. See {#extract_key_and_conditions}.
31
+ def initialize(client, namespace_segment, conditions)
32
+ fail ArgumentError, "expecting an OandaAPI::Client instance" unless client && client.is_a?(OandaAPI::Client)
33
+ fail ArgumentError, "expecting a namespace value" if namespace_segment.to_s.empty?
34
+
35
+ @client = client
36
+ @conditions = {}
37
+ @namespace_segments = [Utils.pluralize(namespace_segment)]
38
+ extract_key_and_conditions conditions
39
+ end
40
+
41
+ # Returns a deep clone of +self+.
42
+ # @return [NamespaceProxy]
43
+ def clone
44
+ ns = self.dup
45
+ ns.conditions = conditions.dup
46
+ ns.namespace_segments = namespace_segments.dup
47
+ ns
48
+ end
49
+
50
+ # Returns the namespace (URI path to a resource).
51
+ # @return [String]
52
+ def namespace
53
+ "/" + @namespace_segments.join("/")
54
+ end
55
+
56
+ # Extracts a _key_ parameter from the arguments.
57
+ # If a key is found, it's appended to the list namespace segments. Non-key
58
+ # parameters are merged into the {#conditions} collection. A parameter is a
59
+ # key if it's named ":id", or if there is only a single scalar argument.
60
+ #
61
+ # @example "key" parameters
62
+ # client = OandaAPI::Client::TokenClient.new :practice, token
63
+ # account = client.account(1234) # 1234 is a _key_ (accountId)
64
+ # account.namespace # => /accounts/1234
65
+ #
66
+ # order = account.order(instrument: "USD_JPY",
67
+ # type: "market",
68
+ # units: 10_000,
69
+ # side: "buy").create # No key parameters here
70
+ #
71
+ # position = account.position("USD_JPY").get # USD_JPY is a key
72
+ #
73
+ # @param conditions either a hash of parameter values, single scalar value, or nil.
74
+ #
75
+ # @return [void]
76
+ def extract_key_and_conditions(conditions)
77
+ key =
78
+ case
79
+ when conditions && conditions.is_a?(Hash)
80
+ @conditions.merge! Utils.rubyize_keys(conditions)
81
+ @conditions.delete :id
82
+ when conditions
83
+ conditions
84
+ end
85
+ @namespace_segments << key if key
86
+ end
87
+
88
+ # Executes an API request and returns a resource object, or returns a
89
+ # clone of +self+ for method chaining.
90
+ #
91
+ # @return [OandaAPI::Client::NamespaceProxy] if the method is used
92
+ # for chaining.
93
+ #
94
+ # @return [OandaAPI::ResourceBase] if the method is one of the supported
95
+ # _terminating_ methods (+:create+, +:close+, +:delete+, +:get+, +:update+).
96
+ #
97
+ # @return [OandaAPI::ResourceCollection] if the method is +:get+ and the
98
+ # API returns a collection of resources.
99
+ def method_missing(sym, *args)
100
+ # Check for terminating method
101
+ if [:create, :close, :delete, :get, :update].include?(sym)
102
+ @client.execute_request sym, namespace, conditions
103
+ else
104
+ ns = self.clone
105
+ ns.namespace_segments << Utils.pluralize(sym)
106
+ ns.extract_key_and_conditions args.first
107
+ ns
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,52 @@
1
+ module OandaAPI
2
+ module Client
3
+ # @private
4
+ # Metadata about a resource request.
5
+ #
6
+ # @!attribute [r] collection_name
7
+ # @return [Symbol] method name that returns a collection of the resource
8
+ # from the API response.
9
+ #
10
+ # @!attribute [r] path
11
+ # @return [String] path of the resource URI.
12
+ #
13
+ # @!attribute [r] resource_klass
14
+ # @return [Symbol] class of the resource.
15
+ class ResourceDescriptor
16
+ attr_reader :collection_name, :path, :resource_klass
17
+
18
+ # Analyzes the resource request and determines the type of resource
19
+ # expected from the API.
20
+ #
21
+ # @param [String] path a path to a resource.
22
+ #
23
+ # @param [Symbol] method an http verb (see {OandaAPI::Client.map_method_to_http_verb}).
24
+ def initialize(path, method)
25
+ @path = path
26
+ path.match(/\/(?<resource_name>[a-z]*)\/?(?<resource_id>\w*?)$/) do |names|
27
+ resource_name, resource_id = [Utils.singularize(names[:resource_name]), names[:resource_id]]
28
+ self.resource_klass = resource_name
29
+ @is_collection = method == :get && resource_id.empty?
30
+ @collection_name = Utils.pluralize(resource_name).to_sym if is_collection?
31
+ end
32
+ end
33
+
34
+ # True if the request returns a collection.
35
+ # @return [Boolean]
36
+ def is_collection?
37
+ @is_collection
38
+ end
39
+
40
+ private
41
+
42
+ # The resource type
43
+ # @param [String] resource_name
44
+ # @return [void]
45
+ def resource_klass=(resource_name)
46
+ klass_symbol = resource_name.capitalize.to_sym
47
+ fail ArgumentError, "Invalid resource" unless OandaAPI::Resource.constants.include?(klass_symbol)
48
+ @resource_klass = OandaAPI::Resource.const_get klass_symbol
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,69 @@
1
+ module OandaAPI
2
+ module Client
3
+ # Makes requests to the API.
4
+ # Instances access Oanda's _practice_ or _live_ environments.
5
+ # Most API methods require an account API token to perform requests in the
6
+ # associated environment.
7
+ # See the Oanda Development Guide for information about
8
+ # {http://developer.oanda.com/rest-live/authentication/ obtaining a personal access token from Oanda}.
9
+ #
10
+ # @example Example usage
11
+ # token = ENV["oanda_practice_account_token"]
12
+ # client = OandaAPI::Client::TokenClient.new :practice, token
13
+ #
14
+ # # Get information for an account.
15
+ # # See http://developer.oanda.com/rest-live/accounts/
16
+ # account = client.accounts.get.first # => OandaAPI::Resource::Account
17
+ #
18
+ # # Get a list of open positions.
19
+ # # See http://developer.oanda.com/rest-live/positions/
20
+ # positions = client.account(account.id)
21
+ # .positions.get # => OandaAPI::ResourceCollection
22
+ #
23
+ #
24
+ # @!attribute [r] auth_token
25
+ # @return [String] Oanda personal access token.
26
+ #
27
+ # @!attribute [rw] domain
28
+ # @return [Symbol] identifies the Oanda subdomain (+:practice+ or +:live+)
29
+ # accessed by the client.
30
+ #
31
+ # @!attribute [rw] default_params
32
+ # @return [Hash] parameters that are included with every API
33
+ # request as either query or url_form encoded parameters.
34
+ #
35
+ # @!attribute [rw] headers
36
+ # @return [Hash] parameters that are included with every API request
37
+ # as HTTP headers.
38
+ class TokenClient
39
+ include Client
40
+
41
+ attr_reader :auth_token
42
+ attr_accessor :domain, :default_params, :headers
43
+
44
+ # @param [Symbol] domain see {#domain}
45
+ # @param [String] auth_token see {#auth_token}
46
+ def initialize(domain, auth_token)
47
+ super()
48
+ @auth_token = auth_token
49
+ @default_params = {}
50
+ self.domain = domain
51
+ @headers = auth
52
+ end
53
+
54
+ # Parameters used for authentication.
55
+ # @return [Hash]
56
+ def auth
57
+ { "Authorization" => "Bearer #{auth_token}" }
58
+ end
59
+
60
+ # @private
61
+ # Sets the domain the client can access. (Testing convenience only).
62
+ # @return [void]
63
+ def domain=(value)
64
+ fail ArgumentError, "Invalid domain" unless OandaAPI::DOMAINS.include? value
65
+ @domain = value
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,53 @@
1
+ module OandaAPI
2
+ module Client
3
+ # Makes requests to the API.
4
+ # Instances access the Oanda _sandbox_ environment.
5
+ # Most client requests require a valid Oanda sandbox account username.
6
+ # See the Oanda Development Guide for information about
7
+ # {http://developer.oanda.com/rest-live/accounts/#createTestAccount creating a test account}.
8
+ #
9
+ # @example Example usage (creates a new test account).
10
+ # client = OandaAPI::Client::UsernameClient.new "_" # Note: A new test account can be created without having an
11
+ # # existing account, which is why we create a client in this
12
+ # # example with a bogus username ("_").
13
+ # new_account = client.account.create # => OandaAPI::Resource::Account
14
+ # new_account.username # => "<username>"
15
+ #
16
+ #
17
+ # @!attribute [r] domain
18
+ # @return [Symbol] identifies the Oanda subdomain (+:sandbox+) which the
19
+ # client accesses.
20
+ #
21
+ # @!attribute [r] username
22
+ # @return [String] the username used for authentication.
23
+ #
24
+ # @!attribute [rw] default_params
25
+ # @return [Hash] parameters that are included with every API request as
26
+ # either query or url_form encoded parameters.
27
+ #
28
+ # @!attribute [rw] headers
29
+ # @return [Hash] parameters that are included with every API request as
30
+ # HTTP headers.
31
+ class UsernameClient
32
+ include Client
33
+
34
+ attr_reader :domain, :username
35
+ attr_accessor :default_params, :headers
36
+
37
+ # @param [String] username used for authentication.
38
+ def initialize(username)
39
+ super()
40
+ @domain = :sandbox
41
+ @username = username
42
+ @default_params = auth
43
+ @headers = {}
44
+ end
45
+
46
+ # Parameters used for authentication.
47
+ # @return [Hash]
48
+ def auth
49
+ { "username" => @username }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,167 @@
1
+ module OandaAPI
2
+ DATETIME_FORMATS = [:rfc3339, :unix]
3
+
4
+ # Configures client API settings.
5
+ class Configuration
6
+ DATETIME_FORMAT = :rfc3339
7
+ MAX_REQUESTS_PER_SECOND = 15
8
+ OPEN_TIMEOUT = 10
9
+ READ_TIMEOUT = 10
10
+ REST_API_VERSION = "v1"
11
+ USE_COMPRESSION = false
12
+ USE_REQUEST_THROTTLING = false
13
+
14
+ # The format in which dates will be returned by the API (+:rfc3339+ or +:unix+).
15
+ # See the Oanda Development Guide for more details about {http://developer.oanda.com/rest-live/development-guide/#date_Time_Format DateTime formats}.
16
+ # @return [Symbol]
17
+ def datetime_format
18
+ @datetime_format ||= DATETIME_FORMAT
19
+ end
20
+
21
+ # See {#datetime_format}.
22
+ # @param [Symbol] value
23
+ # @return [void]
24
+ def datetime_format=(value)
25
+ fail ArgumentError, "Invalid datetime format" unless OandaAPI::DATETIME_FORMATS.include? value
26
+ @datetime_format = value
27
+ end
28
+
29
+ # The maximum number of requests per second allowed to be made through the
30
+ # API. Only enforced if {#use_request_throttling?} is +true+.
31
+ #
32
+ # @return [Numeric]
33
+ def max_requests_per_second
34
+ @max_requests_per_second ||= MAX_REQUESTS_PER_SECOND
35
+ end
36
+
37
+ # See {#max_requests_per_second}.
38
+ # @param [Numeric] value
39
+ # @return [void]
40
+ def max_requests_per_second=(value)
41
+ fail ArgumentError, "must be a number > 0" unless value.is_a?(Numeric) && value > 0
42
+ @min_request_interval = nil
43
+ @max_requests_per_second = value
44
+ end
45
+
46
+ # The minimum amount of time in seconds that must elapse between consecutive requests to the API.
47
+ # Determined by {#max_requests_per_second}. Only enforced if {#use_request_throttling?} is +true+.
48
+ # @return [Float]
49
+ def min_request_interval
50
+ @min_request_interval ||= (1.0 / max_requests_per_second)
51
+ end
52
+
53
+ # The number of seconds the client waits for a new HTTP connection to be established before
54
+ # raising a timeout exception.
55
+ # @return [Numeric]
56
+ def open_timeout
57
+ @open_timeout ||= OPEN_TIMEOUT
58
+ end
59
+
60
+ # See {#open_timeout}.
61
+ # @param [Numeric] value
62
+ # @return [void]
63
+ def open_timeout=(value)
64
+ fail ArgumentError, "must be an integer or float" unless value && (value.is_a?(Integer) || value.is_a?(Float))
65
+ @open_timeout = value
66
+ end
67
+
68
+ # The number of seconds the client waits for a response from the API before
69
+ # raising a timeout exception.
70
+ # @return [Numeric]
71
+ def read_timeout
72
+ @read_timeout ||= READ_TIMEOUT
73
+ end
74
+
75
+ # See {#read_timeout}.
76
+ # @param [Numeric] value
77
+ # @return [void]
78
+ def read_timeout=(value)
79
+ fail ArgumentError, "must be an integer or float" unless value && (value.is_a?(Integer) || value.is_a?(Float))
80
+ @read_timeout = value
81
+ end
82
+
83
+ # The Oanda REST API version used by the client.
84
+ # @return [String]
85
+ def rest_api_version
86
+ @rest_api_version ||= REST_API_VERSION
87
+ end
88
+
89
+ # See {#rest_api_version}.
90
+ # @param [String] value
91
+ # @return [void]
92
+ def rest_api_version=(value)
93
+ @rest_api_version = value
94
+ end
95
+
96
+ # Specifies whether the API uses compressed responses. See the Oanda Development Guide
97
+ # for more information about {http://developer.oanda.com/rest-live/best-practices/#compression compression}.
98
+ #
99
+ # @return [Boolean]
100
+ def use_compression
101
+ @use_compression = USE_COMPRESSION if @use_compression.nil?
102
+ @use_compression
103
+ end
104
+
105
+ alias_method :use_compression?, :use_compression
106
+
107
+ # See {#use_compression}.
108
+ # @param [Boolean] value
109
+ # @return [void]
110
+ def use_compression=(value)
111
+ @use_compression = !!value
112
+ end
113
+
114
+ # Throttles the rate of requests made to the API. See the Oanda Developers
115
+ # Guide for information about
116
+ # {http://developer.oanda.com/rest-live/best-practices/ connection limits}.
117
+ # If enabled, requests will not exceed {#max_requests_per_second}. If the
118
+ # rate of requests received by the client exceeds this limit, the client
119
+ # delays the rate-exceeding request for the minimum amount of time needed
120
+ # to satisfy the rate limit.
121
+ #
122
+ # @return [Boolean]
123
+ def use_request_throttling
124
+ @use_request_throttling = USE_REQUEST_THROTTLING if @use_request_throttling.nil?
125
+ @use_request_throttling
126
+ end
127
+
128
+ alias_method :use_request_throttling?, :use_request_throttling
129
+
130
+ # See {#use_request_throttling}.
131
+ # @param [Boolean] value
132
+ # @return [void]
133
+ def use_request_throttling=(value)
134
+ @use_request_throttling = !!value
135
+ end
136
+
137
+ # @private
138
+ # @return [Hash] headers that are set on every request as a result of
139
+ # configuration settings.
140
+ def headers
141
+ h = {}
142
+ h["X-Accept-Datetime-Format"] = datetime_format.to_s.upcase
143
+ h["Accept-Encoding"] = "deflate, gzip" if use_compression?
144
+ h
145
+ end
146
+ end
147
+
148
+ # Use to configure application-wide settings.
149
+ #
150
+ # @example Example Usage
151
+ # OandaAPI.configure |config|
152
+ # config.use_compression = true
153
+ # config.use_request_throttling = true
154
+ # end
155
+ #
156
+ # @yield [Configuration]
157
+ # @return [void]
158
+ def self.configure
159
+ yield configuration
160
+ end
161
+
162
+ # @private
163
+ # @return [Configuration]
164
+ def self.configuration
165
+ @configuration ||= Configuration.new
166
+ end
167
+ end