questrade_api 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/.yardopts +9 -0
- data/Gemfile +15 -0
- data/Guardfile +6 -0
- data/{LICENSE.txt → LICENSE} +0 -0
- data/README.md +31 -45
- data/Rakefile +4 -0
- data/lib/questrade_api/authorization.rb +94 -0
- data/lib/questrade_api/client.rb +87 -0
- data/lib/questrade_api/modules/util.rb +22 -0
- data/lib/questrade_api/rest/account.rb +101 -0
- data/lib/questrade_api/rest/activity.rb +58 -0
- data/lib/questrade_api/rest/balance.rb +81 -0
- data/lib/questrade_api/rest/base.rb +88 -0
- data/lib/questrade_api/rest/execution.rb +59 -0
- data/lib/questrade_api/rest/market.rb +42 -0
- data/lib/questrade_api/rest/order.rb +58 -0
- data/lib/questrade_api/rest/position.rb +48 -0
- data/lib/questrade_api/rest/time.rb +26 -0
- data/lib/questrade_api/version.rb +1 -1
- data/questrade_api.gemspec +2 -3
- data/spec/fixtures/json/accounts.json +21 -0
- data/spec/fixtures/json/activities.json +36 -0
- data/spec/fixtures/json/balances.json +55 -0
- data/spec/fixtures/json/executions.json +46 -0
- data/spec/fixtures/json/markets.json +34 -0
- data/spec/fixtures/json/orders.json +49 -0
- data/spec/fixtures/json/positions.json +17 -0
- data/spec/fixtures/json/time.json +3 -0
- data/spec/questrade_api/authorization_spec.rb +74 -0
- data/spec/questrade_api/client_spec.rb +38 -0
- data/spec/questrade_api/rest/account_spec.rb +88 -0
- data/spec/questrade_api/rest/activity_spec.rb +70 -0
- data/spec/questrade_api/rest/balance_spec.rb +34 -0
- data/spec/questrade_api/rest/execution_spec.rb +80 -0
- data/spec/questrade_api/rest/market_spec.rb +63 -0
- data/spec/questrade_api/rest/order_spec.rb +95 -0
- data/spec/questrade_api/rest/position_spec.rb +47 -0
- data/spec/questrade_api/rest/time_spec.rb +34 -0
- data/spec/spec_helper.rb +107 -0
- data/spec/support/json_fixtures.rb +21 -0
- metadata +61 -47
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 49d2e66e9476e189911591ed7fac0b63134b2fce
|
4
|
+
data.tar.gz: c941e8f74073691030eaed6bd8066c1af859b88b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7895d0c76f3515bad23d095557f010f0d1d8528a660557335a7b05e2c3aeec99baa586f4f6685ee59f93c8dc2987fa56248ab29bc2c9246fc8066ae29302dd61
|
7
|
+
data.tar.gz: edf1116607c99e92f40e7618809eb5ee7d4995e2d5c651001ce3649f017892bcf04de3baf59ad467d358befcb27cdd1fd338e62d008dd8723557dd86123bc922
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/.yardopts
ADDED
data/Gemfile
CHANGED
@@ -1,4 +1,19 @@
|
|
1
1
|
source 'http://rubygems.org'
|
2
2
|
|
3
|
+
group :docs do
|
4
|
+
gem 'yard'
|
5
|
+
gem "redcarpet"
|
6
|
+
gem 'github-markup'
|
7
|
+
end
|
8
|
+
|
9
|
+
group :test do
|
10
|
+
gem 'guard'
|
11
|
+
gem 'guard-rspec'
|
12
|
+
gem 'bundler', '~> 1.14'
|
13
|
+
end
|
14
|
+
|
15
|
+
gem 'rake'
|
16
|
+
|
3
17
|
# Specify your gem's dependencies in questrade_api.gemspec
|
4
18
|
gemspec
|
19
|
+
|
data/Guardfile
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
guard :rspec, cmd: 'bundle exec rspec' do
|
2
|
+
watch(%r{^spec/.+_spec\.rb$})
|
3
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
4
|
+
watch(%r{^lib/questrade_api/rest/(.+)\.rb$}) { |m| "spec/questrade_api/rest/#{m[1]}_spec.rb" }
|
5
|
+
watch('spec/spec_helper.rb') { "spec" }
|
6
|
+
end
|
data/{LICENSE.txt → LICENSE}
RENAMED
File without changes
|
data/README.md
CHANGED
@@ -1,37 +1,32 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/brunomeira/questrade_api.svg?branch=master)](https://travis-ci.org/brunomeira/questrade_api)
|
2
|
+
[![Code Climate](https://codeclimate.com/github/brunomeira/questrade_api/badges/gpa.svg)](https://codeclimate.com/github/brunomeira/questrade_api)
|
3
|
+
|
1
4
|
# QuestradeApi
|
2
5
|
|
3
6
|
A Ruby interface to use the [Questrade API](http://www.questrade.com/api).
|
4
7
|
|
5
8
|
## Quick Start
|
6
9
|
|
7
|
-
|
8
|
-
|
9
|
-
2. Add the following line to your Gemfile
|
10
|
+
- Create a Questrade Demo Account on: <http://www.questrade.com/api/free-practice-account>
|
11
|
+
- Add the following line to your Gemfile
|
10
12
|
|
11
13
|
```
|
12
14
|
gem 'questrade_api'
|
13
|
-
|
14
15
|
```
|
15
|
-
|
16
|
-
3. Run Bundle to install gem
|
16
|
+
- Run Bundle to install gem
|
17
17
|
|
18
18
|
```
|
19
19
|
$ bundle
|
20
|
-
|
21
20
|
```
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
5. Copy the snippet of code below to your application, and replace the 'XXXX' token with the token generated on step 4.
|
21
|
+
- Follow the tutorial on <http://www.questrade.com/api/documentation/getting-started> to generate a refresh token
|
22
|
+
- Copy the snippet of code below to your application, and replace the 'XXXX' token with the token generated in the previous step.
|
26
23
|
|
27
24
|
```ruby
|
28
25
|
# By default this API calls the practice(demo) endpoint.
|
29
26
|
# Check our documentation to learn how to call the live endpoint.
|
30
27
|
client = QuestradeApi::Client.new(refresh_token: 'XXXX')
|
31
|
-
|
32
28
|
```
|
33
|
-
|
34
|
-
6. That's all you need to access the API. A few examples of what you can do with it:
|
29
|
+
- That's all you need to access the API. A few examples of what you can do with it:
|
35
30
|
|
36
31
|
```ruby
|
37
32
|
# Get Questrade's current server time
|
@@ -53,57 +48,48 @@ client.activities('account_id', startTime: DateTime.yesterday.to_s, endTime: Dat
|
|
53
48
|
# authorization can be any object that responds to url and access_token
|
54
49
|
authorization = QuestradeApi::Authorization.new(access_token: 'access_token', api_server: 'url')
|
55
50
|
accounts = QuestradeApi::REST::Account.all(accounts)
|
56
|
-
|
57
51
|
```
|
58
|
-
|
59
|
-
For more advanced options, check out our [documentation](http://www.google.com).
|
52
|
+
For more advanced options, check out our [documentation](http://www.rubydoc.info/gems/questrade_api).
|
60
53
|
|
61
54
|
## Current Status
|
62
55
|
|
63
56
|
This Project is under development and some endpoints are still not accessible through the gem.
|
64
57
|
Check the tables below for more details.
|
65
58
|
|
66
|
-
<dl>
|
67
|
-
<dt>LEGEND</dt>
|
68
|
-
|
69
|
-
<dd>:green_heart: = Available</dd>
|
70
|
-
<dd>:heart: = Not Available</dd>
|
71
|
-
</dl>
|
72
|
-
|
73
59
|
### Account Calls
|
74
60
|
|
75
61
|
| Endpoint | Development | Documentation |
|
76
62
|
| --- | --- | --- |
|
77
|
-
| /time |
|
78
|
-
| /accounts |
|
79
|
-
| /accounts/:id/positions |
|
80
|
-
| /accounts/:id/balances |
|
81
|
-
| /accounts/:id/executions |
|
82
|
-
| /accounts/:id/orders |
|
83
|
-
| /accounts/:id/activities |
|
63
|
+
| /time |DONE | DONE |
|
64
|
+
| /accounts |DONE | DONE |
|
65
|
+
| /accounts/:id/positions |DONE | DONE |
|
66
|
+
| /accounts/:id/balances |DONE | DONE |
|
67
|
+
| /accounts/:id/executions |DONE | DONE |
|
68
|
+
| /accounts/:id/orders |DONE | DONE |
|
69
|
+
| /accounts/:id/activities |DONE | DONE |
|
84
70
|
|
85
71
|
### Market Calls
|
86
72
|
|
87
73
|
| Endpoint | Development | Documentation |
|
88
74
|
| --- | --- | --- |
|
89
|
-
| /symbols/:id |
|
90
|
-
| /symbols/:search |
|
91
|
-
| /symbols/:id/options |
|
92
|
-
| /markets |
|
93
|
-
| /markets/quotes/:id |
|
94
|
-
| /markets/quotes/options |
|
95
|
-
| /markets/quotes/strategies |
|
96
|
-
| /markets/candles/:id |
|
75
|
+
| /symbols/:id | | |
|
76
|
+
| /symbols/:search | | |
|
77
|
+
| /symbols/:id/options | | |
|
78
|
+
| /markets |DONE| |
|
79
|
+
| /markets/quotes/:id | | |
|
80
|
+
| /markets/quotes/options | | |
|
81
|
+
| /markets/quotes/strategies | | |
|
82
|
+
| /markets/candles/:id | | |
|
97
83
|
|
98
84
|
### Order Calls
|
99
85
|
|
100
86
|
| Endpoint | Development | Documentation |
|
101
87
|
| --- | --- | --- |
|
102
|
-
| POST accounts/:id/orders |
|
103
|
-
| POST accounts/:id/orders/impact |
|
104
|
-
| DELETE accounts/:id/orders |
|
105
|
-
| POST accounts/:id/orders/brackets |
|
106
|
-
| POST accounts/:id/orders/strategy |
|
88
|
+
| POST accounts/:id/orders | | |
|
89
|
+
| POST accounts/:id/orders/impact | | |
|
90
|
+
| DELETE accounts/:id/orders | | |
|
91
|
+
| POST accounts/:id/orders/brackets | | |
|
92
|
+
| POST accounts/:id/orders/strategy | | |
|
107
93
|
|
108
94
|
## Contributing
|
109
95
|
|
@@ -123,4 +109,4 @@ We are not responsible for any damages, capital losses, or any claim caused by t
|
|
123
109
|
|
124
110
|
USE IT AT YOUR OWN RISK.
|
125
111
|
|
126
|
-
[LICENSE]: LICENSE
|
112
|
+
[LICENSE]: https://github.com/brunomeira/questrade_api/blob/master/LICENSE
|
data/Rakefile
CHANGED
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require 'questrade_api/modules/util'
|
5
|
+
|
6
|
+
module QuestradeApi
|
7
|
+
# @author Bruno Meira <goesmeira@gmail.com>
|
8
|
+
class Authorization
|
9
|
+
include QuestradeApi::Util
|
10
|
+
|
11
|
+
MODE = %i(live practice).freeze
|
12
|
+
attr_reader :mode, :data, :connection
|
13
|
+
|
14
|
+
# Initialize an instance of QuestradeApi::Client.
|
15
|
+
#
|
16
|
+
# @note Only access_token, api_server, and refresh_token are needed for this gem.
|
17
|
+
#
|
18
|
+
# @param params [Hash] for Questrade authorization.
|
19
|
+
# @option params [String] :access_token Access token used to access API.
|
20
|
+
# @option params [String] :api_server Endpoint used to access API. Example: 'https://apiXX.iq.questrade.com/', where X is a number.
|
21
|
+
# @option params [Integer] :expires_in How much time the access token is valid.
|
22
|
+
# @option params [String] :refresh_token Token used to get a new access token.
|
23
|
+
# @option params [String] :token_type Token type.
|
24
|
+
#
|
25
|
+
# @param mode [Symbol] accessed on Questrade, `:live` or `:practice`
|
26
|
+
def initialize(params, mode = :practice)
|
27
|
+
raise 'Mode must be :live or :practice' unless MODE.include?(mode)
|
28
|
+
|
29
|
+
@mode = mode
|
30
|
+
@connection = build_connection
|
31
|
+
|
32
|
+
build_data(params)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Uses refresh_token to fetch a new valid access token.
|
36
|
+
#
|
37
|
+
# @note #data will be populated accordingly, if call is successful.
|
38
|
+
#
|
39
|
+
# @return The result of the call.
|
40
|
+
def refresh_token
|
41
|
+
response = connection.get do |req|
|
42
|
+
req.params[:grant_type] = 'refresh_token'
|
43
|
+
req.params[:refresh_token] = data.refresh_token
|
44
|
+
end
|
45
|
+
|
46
|
+
if response.status == 200
|
47
|
+
raw_body = JSON.parse(response.body)
|
48
|
+
build_data(raw_body)
|
49
|
+
end
|
50
|
+
|
51
|
+
response
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the authorized access token.
|
55
|
+
def access_token
|
56
|
+
data.access_token
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the server associated with the authorized access token.
|
60
|
+
def url
|
61
|
+
data.api_server
|
62
|
+
end
|
63
|
+
|
64
|
+
# Checks if selected mode is live.
|
65
|
+
#
|
66
|
+
# @return [Boolean]
|
67
|
+
def live?
|
68
|
+
mode == :live
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def login_url
|
74
|
+
if live?
|
75
|
+
'https://login.questrade.com/oauth2/token'
|
76
|
+
else
|
77
|
+
'https://practicelogin.questrade.com/oauth2/token'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_data(data)
|
82
|
+
hash = hash_to_snakecase(data)
|
83
|
+
@data = OpenStruct.new(hash)
|
84
|
+
end
|
85
|
+
|
86
|
+
def build_connection
|
87
|
+
Faraday.new(login_url) do |faraday|
|
88
|
+
faraday.response :logger
|
89
|
+
faraday.adapter Faraday.default_adapter
|
90
|
+
faraday.headers['Content-Type'] = 'application/json'
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'questrade_api/authorization'
|
2
|
+
require 'questrade_api/rest/time'
|
3
|
+
require 'questrade_api/rest/account'
|
4
|
+
require 'questrade_api/rest/balance'
|
5
|
+
require 'questrade_api/rest/position'
|
6
|
+
require 'questrade_api/rest/execution'
|
7
|
+
require 'questrade_api/rest/activity'
|
8
|
+
require 'questrade_api/rest/order'
|
9
|
+
|
10
|
+
require 'questrade_api/rest/market'
|
11
|
+
|
12
|
+
module QuestradeApi
|
13
|
+
# @author Bruno Meira <goesmeira@gmail.com>
|
14
|
+
class Client
|
15
|
+
attr_reader :authorization
|
16
|
+
|
17
|
+
# @see QuestradeApi::Client#initialize for more details
|
18
|
+
def initialize(params = {}, mode = :practice)
|
19
|
+
@authorization = QuestradeApi::Authorization.new(params, mode)
|
20
|
+
refresh_token if refresh_token?
|
21
|
+
end
|
22
|
+
|
23
|
+
# Fetches a new access_token. (see QuestradeApi::Authorization#refresh_token)
|
24
|
+
def refresh_token
|
25
|
+
@authorization.refresh_token
|
26
|
+
end
|
27
|
+
|
28
|
+
# Fetch current server time.
|
29
|
+
#
|
30
|
+
# @return [DateTime] if no issues to call /time endpoint occurs.
|
31
|
+
# @return [nil] if current server time cannot be fetched.
|
32
|
+
def time
|
33
|
+
time = QuestradeApi::REST::Time.new(@authorization)
|
34
|
+
time.get
|
35
|
+
|
36
|
+
time.data && time.data.time
|
37
|
+
end
|
38
|
+
|
39
|
+
# Fetch all accounts associated with user.
|
40
|
+
#
|
41
|
+
# @return [Array<QuestradeApi::REST::Account>]
|
42
|
+
def accounts
|
43
|
+
QuestradeApi::REST::Account.all(@authorization).accounts
|
44
|
+
end
|
45
|
+
|
46
|
+
# Fetch all positions associated with account.
|
47
|
+
#
|
48
|
+
# @param account_id [String] to which positions will be fetched.
|
49
|
+
#
|
50
|
+
# @return [Array<QuestradeApi::REST::Position>]
|
51
|
+
def positions(account_id)
|
52
|
+
QuestradeApi::REST::Position.all(@authorization, account_id).positions
|
53
|
+
end
|
54
|
+
|
55
|
+
# Fetch all balances associated with account.
|
56
|
+
#
|
57
|
+
# @param account_id [String] to which balances will be fetched.
|
58
|
+
#
|
59
|
+
# @return [OpenStruct(per_currency_balances)]
|
60
|
+
def balances(account_id)
|
61
|
+
QuestradeApi::REST::Balance.all(@authorization, account_id)
|
62
|
+
end
|
63
|
+
|
64
|
+
def executions(account_id, params = {})
|
65
|
+
QuestradeApi::REST::Execution.all(@authorization, account_id, params)
|
66
|
+
end
|
67
|
+
|
68
|
+
def activities(account_id, params = {})
|
69
|
+
QuestradeApi::REST::Activity.all(@authorization, account_id, params)
|
70
|
+
end
|
71
|
+
|
72
|
+
def orders(account_id, params = {})
|
73
|
+
QuestradeApi::REST::Activity.all(@authorization, account_id, params)
|
74
|
+
end
|
75
|
+
|
76
|
+
def markets
|
77
|
+
QuestradeApi::REST::Market.all(@authorization)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def refresh_token?
|
83
|
+
data = @authorization.data
|
84
|
+
data.refresh_token && !data.api_server && !data.access_token
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module QuestradeApi
|
2
|
+
module Util
|
3
|
+
private
|
4
|
+
|
5
|
+
def hash_to_snakecase(hash)
|
6
|
+
values = hash.map do |k, v|
|
7
|
+
[underscore(k.to_s).to_sym, v]
|
8
|
+
end
|
9
|
+
|
10
|
+
Hash[values]
|
11
|
+
end
|
12
|
+
|
13
|
+
def underscore(camel_cased_word)
|
14
|
+
camel_cased_word
|
15
|
+
.gsub(/::/, '/')
|
16
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
17
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
18
|
+
.tr('-', '_')
|
19
|
+
.downcase
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'questrade_api/modules/util'
|
2
|
+
|
3
|
+
require 'questrade_api/rest/base'
|
4
|
+
require 'questrade_api/rest/activity'
|
5
|
+
require 'questrade_api/rest/position'
|
6
|
+
require 'questrade_api/rest/balance'
|
7
|
+
require 'questrade_api/rest/execution'
|
8
|
+
|
9
|
+
module QuestradeApi
|
10
|
+
module REST
|
11
|
+
# @author Bruno Meira <goesmeira@gmail.com>
|
12
|
+
class Account < QuestradeApi::REST::Base
|
13
|
+
attr_accessor :id, :user_id
|
14
|
+
|
15
|
+
def initialize(authorization, params)
|
16
|
+
super(authorization)
|
17
|
+
|
18
|
+
@id = params[:id]
|
19
|
+
@user_id = params[:user_id]
|
20
|
+
|
21
|
+
@raw_body = params[:data]
|
22
|
+
build_data(params[:data]) if @raw_body
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Fetch activities associated with account.
|
27
|
+
#
|
28
|
+
# @param see QuestradeApi::REST::Activity.all
|
29
|
+
#
|
30
|
+
# @return [OpenStruct(executions: [QuestradeApi::REST::Activity)]
|
31
|
+
def activities(params)
|
32
|
+
QuestradeApi::REST::Activity.all(authorization, id, params)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Fetch executions associated with account.
|
36
|
+
#
|
37
|
+
# @param see QuestradeApi::REST::Execution.all
|
38
|
+
#
|
39
|
+
# @return [OpenStruct(executions: [QuestradeApi::REST::Execution)]
|
40
|
+
def executions(params)
|
41
|
+
QuestradeApi::REST::Execution.all(authorization, id, params)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Fetch balances associated with account.
|
45
|
+
#
|
46
|
+
# @return [OpenStruct(per_currency_balances)]
|
47
|
+
def balances
|
48
|
+
QuestradeApi::REST::Balance.all(authorization, id)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Fetch positions associated with account.
|
52
|
+
#
|
53
|
+
# @return [OpenStruct(positions: [QuestradeApi::REST::Position])]
|
54
|
+
def positions
|
55
|
+
QuestradeApi::REST::Position.all(authorization, id)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Fetch accounts for specific authorized user.
|
59
|
+
#
|
60
|
+
# @param authorization is any object that
|
61
|
+
# responds to #access_token and #url
|
62
|
+
#
|
63
|
+
# @return [OpenStruct(accounts: [QuestradeApi::REST::Account])] if call to server is successful
|
64
|
+
# @return [Faraday::Response] if call to server is not successful
|
65
|
+
def self.all(authorization)
|
66
|
+
response = super(access_token: authorization.access_token,
|
67
|
+
endpoint: endpoint,
|
68
|
+
url: authorization.url)
|
69
|
+
|
70
|
+
result = OpenStruct.new(accounts: [])
|
71
|
+
|
72
|
+
if response.status == 200
|
73
|
+
result.accounts = parse_accounts(authorization, response.body)
|
74
|
+
response = result
|
75
|
+
end
|
76
|
+
|
77
|
+
response
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.endpoint
|
81
|
+
"#{BASE_ENDPOINT}/accounts"
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.parse_accounts(authorization, body)
|
85
|
+
raw = JSON.parse(body)
|
86
|
+
clients = []
|
87
|
+
|
88
|
+
raw['accounts'].each do |client|
|
89
|
+
clients << new(authorization,
|
90
|
+
id: client['number'],
|
91
|
+
user_id: raw['userId'],
|
92
|
+
data: client)
|
93
|
+
end
|
94
|
+
|
95
|
+
clients
|
96
|
+
end
|
97
|
+
|
98
|
+
private_class_method :parse_accounts
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|