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