schwab-api-ruby 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b9659741b90ac05d0426da9e7ff4c6d3a03a205deeb2901d3ec97b9a8dd39165
4
+ data.tar.gz: 5d27d65ef5d09158b61d41d621db97869cd137d11912d339fa0ff3cbea71258e
5
+ SHA512:
6
+ metadata.gz: 3f5c84e4f946111c19adc93b4d73a2d3c4c45edfd397b9f6ec3ee05e1632f18695d0c311e6fd4d07442f2bd0d7604fda875056c09a55c496b721a4bcfddc0919
7
+ data.tar.gz: a9ae1f55fd29ff52d032463b8e0d28ab50f51576957e78d98aefa3c7c9a15ee9e6a020dac4fec5d59d24183c24d448665a5019a6920206a45ddfd2e8321aef4a
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .idea
7
+ .DS_STORE
8
+ spec/test_data/sample_stream_rspec_test.binary
9
+ Gemfile.lock
10
+ InstalledFiles
11
+ _yardoc
12
+ coverage
13
+ doc/
14
+ lib/bundler/man
15
+ pkg
16
+ rdoc
17
+ spec/reports
18
+ spec/test_data/sample_stream_archives
19
+ test/tmp
20
+ test/version_tmp
21
+ tmp
22
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ Version 2.0.0
2
+ - This is version 2.0.0 becuase it's a continuation of the TDAmeritradeAPI gem.
3
+
4
+ Version 1.3.0.20210215
5
+ - Added tracking of token expiration dates to instance vars
6
+ - API request results are now returned as a Hashie::Mash to make 'quotes' or :symbol reference of values indifferent
7
+ - Corrected incorrect date submission in create watchlist operation
8
+
9
+ Verion 1.2.0.20190915
10
+ - (Breaking change) Make get_price_history return datetime stamp as Ruby Time vs milliseconds since epoch
11
+
12
+ Verion 1.1.1.20190915
13
+ - Fix incorrect formatting when using startdate and enddate in get_price_history
14
+
15
+ Version 1.1.0 8/28/19
16
+ - Enhance error messages for rate limit and invalid token
17
+
18
+ Version 1.0 8/27/19
19
+ - Change structure internally to use dependency injection of oprations
20
+ - Price history feature
21
+ - Real time quotes feature
22
+ - Added RSpec test coverage
23
+
24
+ Version 0.2.alpha
25
+ - Basic Watchlist retrieval and modification
26
+
27
+ Version 0.1.alpha - 11/29/2018
28
+ - Initial, very basic release
29
+ - Authentication: refresh access token by refresh_token
30
+ - Instruments: get fundamentals
31
+
32
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in schwab_api.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 wkotzan
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # TD Ameritrade API gem for Ruby
2
+
3
+ This is a gem for connecting to the OAuth/JSON-based Schwab API released in 2018. Go to
4
+ https://beta-developer.schwab.com/ to create your OAuth application login and view the official documentation.
5
+
6
+ For a gem that allows you to connect to the older version of the TDAmeritrade API, go to
7
+ https://github.com/wakproductions/tdameritrade_api
8
+
9
+ ## Installation
10
+
11
+ In your Gemfile
12
+
13
+ `gem 'schwab-api-ruby', git: 'https://github.com/wakproductions/schwab-api-ruby.git'`
14
+
15
+ ## Authenticating
16
+
17
+ Currently this gem is designed for local app authorization of the "Trader API - Individual". It is based on the
18
+ assumption that you will be using https://127.0.0.1 as the OAuth redirect_uri, as that is the only value that's
19
+ been tested and verified to work
20
+
21
+ ## Basic Usage
22
+
23
+ ```
24
+ client = SchwabAPI::Client.new(
25
+ client_id: <client_id>,
26
+ redirect_uri: 'https://127.0.0.1',
27
+ refresh_token: '<refresh_token>'
28
+ )
29
+
30
+ client.get_instrument_fundamentals('MSFT')
31
+ #=> {"MSFT"=>
32
+ {"fundamental"=>
33
+ {"symbol"=>"MSFT",
34
+ "high52"=>425.24,
35
+ "low52"=>340.33,
36
+ ...
37
+ ```
38
+
39
+ # Current State of Functionality
40
+
41
+ The official API is documented [here](https://developer.tdameritrade.com/apis). This gem currently implements the
42
+ following functionality. If you would like to expand its functionality, then please submit a pull request.
43
+
44
+ - [x] Authentication
45
+ - [ ] Accounts and Trading
46
+ - [ ] Instruments
47
+ - [x] Price History
48
+ - [x] Real-time Quotes
49
+
50
+ ## Contributions
51
+
52
+ If you would like to make a contribution, please submit a pull request to the original branch. Feel free to email me Winston Kotzan
53
+ at wak@wakproductions.com with any feature requests, bug reports, or feedback.
54
+
55
+ #### Wish List
56
+
57
+ * Test Coverage in RSpec
58
+
59
+ ## Support
60
+
61
+ Please open an issue on Github if you have any problems or questions.
62
+
63
+ ## Release Notes
64
+
65
+ See CHANGELOG.md
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'schwab-api-ruby'
3
+
4
+ # This is just a placeholder
5
+ # task :taskname do
6
+ # # perform logic
7
+ # end
8
+
@@ -0,0 +1,87 @@
1
+ require 'schwab/util'
2
+ require 'httparty'
3
+
4
+ module Schwab
5
+ module Authentication
6
+ include Util
7
+
8
+ attr_reader :client_id, :secret, :redirect_uri, :authorization_code, :access_token, :refresh_token,
9
+ :access_token_expires_at, :refresh_token_expires_at
10
+
11
+ def authorization_header_token
12
+ client_id_and_secret = "#{client_id}:#{secret}"
13
+ Base64.strict_encode64(client_id_and_secret)
14
+ end
15
+
16
+ def default_headers
17
+ {
18
+ 'Content-Type': 'application/x-www-form-urlencoded',
19
+ 'Authorization': "Basic #{authorization_header_token}"
20
+ }
21
+ end
22
+
23
+ # Return value looks like:
24
+ # {"expires_in"=>1800,
25
+ # "token_type"=>"Bearer",
26
+ # "scope"=>"api",
27
+ # "refresh_token"=>"LGGun...",
28
+ # "access_token"=>"I0.b2F...@",
29
+ # "id_token"=>"eyJ0eXA..."}
30
+ def request_access_token(authorization_grant_code)
31
+ # headers = { 'Content-Type': 'application/x-www-form-urlencoded' } # turns out didn't need this
32
+ options = {
33
+ body: {
34
+ 'grant_type': 'authorization_code',
35
+ 'code': authorization_grant_code,
36
+ 'redirect_uri': redirect_uri
37
+ },
38
+ headers: default_headers
39
+ }
40
+
41
+ response = HTTParty.post(
42
+ 'https://api.schwabapi.com/v1/oauth/token',
43
+ options
44
+ )
45
+
46
+ unless response_success?(response)
47
+ raise Schwab::Error::SchwabAPIError.new(
48
+ "Unable to retrieve access tokens from API - #{response.code} - #{response.body}"
49
+ )
50
+ end
51
+
52
+ update_tokens(response)
53
+ response
54
+ end
55
+
56
+ def refresh_access_token
57
+ options = {
58
+ body: {
59
+ grant_type: 'refresh_token',
60
+ refresh_token: refresh_token,
61
+ },
62
+ headers: default_headers
63
+ }
64
+
65
+ response = HTTParty.post(
66
+ 'https://api.schwabapi.com/v1/oauth/token',
67
+ options
68
+ )
69
+
70
+ update_tokens(response)
71
+ response
72
+ end
73
+
74
+ private
75
+
76
+ def update_tokens(args={})
77
+ raise_error("#{args['error']} - #{args['error_description']}") if args.has_key?('error')
78
+
79
+ @access_token = args['access_token']
80
+ @refresh_token = args['refresh_token']
81
+ @access_token_expires_at = Time.now + args['expires_in'].to_i
82
+ @refresh_token_expires_at = 7.days.from_now
83
+
84
+ args
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,37 @@
1
+ require 'schwab/authentication'
2
+ require 'schwab/client'
3
+ require 'schwab/error'
4
+ require 'schwab/version'
5
+ # require 'schwab/operations/get_instrument_fundamentals'
6
+ require 'schwab/operations/get_price_history'
7
+ require 'schwab/operations/get_quotes'
8
+
9
+ module Schwab
10
+ class Client
11
+ include Schwab::Authentication
12
+ include Schwab::Error
13
+
14
+ def initialize(**args)
15
+ @access_token = args[:access_token]
16
+ @refresh_token = args[:refresh_token]
17
+ @access_token_expires_at = args[:access_token_expires_at]
18
+ @refresh_token_expires_at = args[:refresh_token_expires_at]
19
+ @client_id = args[:client_id] || raise_error('client_id is required!')
20
+ @secret = args[:secret] || raise_error('client_id is required!')
21
+ @redirect_uri = args[:redirect_uri] || raise_error('redirect_uri is required!')
22
+ end
23
+
24
+ # def get_instrument_fundamentals(symbol)
25
+ # Operations::GetInstrumentFundamentals.new(self).call(symbol)
26
+ # end
27
+
28
+ def get_price_history(symbol, **options)
29
+ Operations::GetPriceHistory.new(self).call(symbol, **options)
30
+ end
31
+
32
+ def get_quotes(symbols, **options)
33
+ Operations::GetQuotes.new(self).call(symbols:, **options)
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ module Schwab
2
+ class APIError < StandardError
3
+ end
4
+
5
+ class RateLimitError < StandardError
6
+ end
7
+
8
+ class NotAuthorizedError < StandardError
9
+ end
10
+
11
+ module Error
12
+ module_function
13
+
14
+ def raise_error(message, klass=Schwab::APIError)
15
+ error = klass.new(message)
16
+ error.set_backtrace(caller)
17
+ raise error
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,67 @@
1
+ require 'schwab/error'
2
+ require 'schwab/util'
3
+
4
+ module Schwab
5
+ module Operations
6
+ class BaseOperation
7
+ include Schwab::Error
8
+ include Util
9
+
10
+ HTTP_DEBUG_OUTPUT=ENV['DEBUG_OUTPUT'] # to make live testing easier
11
+
12
+ attr_reader :client
13
+
14
+ def initialize(client)
15
+ @client = client # inject dependency of client credentials
16
+ end
17
+
18
+ private
19
+
20
+ def debug_output?
21
+ HTTP_DEBUG_OUTPUT.to_s == 'true'
22
+ end
23
+
24
+ def handle_api_error(response)
25
+ # "Individual App's transactions per seconds restriction, please update to commercial apps for unrestricted tps"
26
+ if response.code == 429
27
+ raise Schwab::RateLimitError.new(response.body)
28
+ elsif response.code == 401
29
+ raise Schwab::NotAuthorizedError.new(response.body)
30
+ end
31
+
32
+ error_message = response['errors'].to_s
33
+ raise Schwab::APIError.new("#{response.code}: #{error_message}")
34
+ rescue JSON::ParserError
35
+ raise Schwab::APIError.new(
36
+ "Unable to parse error response from Schwab API: #{response.code} - #{response.body} | #{response}"
37
+ )
38
+ end
39
+
40
+ def parse_response(response)
41
+ raise ArgumentError unless response.is_a?(HTTParty::Response)
42
+ handle_api_error(response) unless response_success?(response)
43
+
44
+ response.to_h
45
+ end
46
+
47
+ def perform_api_get_request(url: , query: nil)
48
+ options = {
49
+ headers: {
50
+ 'Authorization': "Bearer #{client.access_token}",
51
+ 'Accept': "application/json"
52
+ }
53
+ }
54
+ options.merge!(query: query) if query
55
+ options.merge!(debug_output: $stdout) if debug_output?
56
+
57
+ response = HTTParty.get(
58
+ url,
59
+ options
60
+ )
61
+
62
+ parse_response(response)
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,23 @@
1
+ # require 'schwab/operations/base_operation'
2
+ #
3
+ # module Schwab
4
+ # ; module Operations
5
+ # class GetInstrumentFundamentals < BaseOperation
6
+ #
7
+ # def call(symbol)
8
+ # params = {
9
+ # apikey: client.client_id,
10
+ # symbol: symbol,
11
+ # projection: 'fundamental'
12
+ # }
13
+ #
14
+ # response = perform_api_get_request(
15
+ # url: 'https://api.tdameritrade.com/v1/instruments',
16
+ # query: params,
17
+ # )
18
+ #
19
+ # parse_api_response(response)
20
+ # end
21
+ #
22
+ # end
23
+ # end; end
@@ -0,0 +1,54 @@
1
+ require 'schwab/operations/base_operation'
2
+
3
+ module Schwab; module Operations
4
+ class GetPriceHistory < BaseOperation
5
+
6
+ # Not used right now, but can be used later on for validation
7
+ FREQUENCY_TYPE=[:minute, :daily, :weekly, :monthly]
8
+ PERIOD_TYPE=[:day, :month, :year, :ytd]
9
+
10
+ def call(
11
+ symbol,
12
+ period_type: nil,
13
+ period: nil,
14
+ frequency_type: nil,
15
+ frequency: nil,
16
+ end_date: nil, # should be a Date
17
+ start_date: nil, # should be a Date
18
+ need_extended_hours_data: false
19
+ )
20
+ raise(ArgumentError, "Can't use period if using start_date/end_date") if start_date.present? && period.present?
21
+
22
+ params = {
23
+ needExtendedHoursData: need_extended_hours_data,
24
+ symbol: symbol
25
+ }
26
+
27
+ params.merge!(frequencyType: frequency_type) if frequency_type
28
+ params.merge!(frequency: frequency) if frequency
29
+
30
+ params.merge!(periodType: period_type) if period_type
31
+ params.merge!(period: period) if period
32
+ params.merge!(startDate: "#{start_date.strftime('%s')}000") if start_date
33
+ params.merge!(endDate: "#{end_date.strftime('%s')}000") if end_date
34
+
35
+ response = perform_api_get_request(
36
+ url: 'https://api.schwabapi.com/marketdata/v1/pricehistory',
37
+ query: params,
38
+ )
39
+
40
+ if response["candles"]
41
+ response["candles"].map do |candle|
42
+ if candle["datetime"].is_a? Numeric
43
+ candle["datetime"] = Time.at(candle["datetime"] / 1000)
44
+ end
45
+ end
46
+ end
47
+
48
+ response
49
+ end
50
+
51
+ private
52
+
53
+ end
54
+ end; end
@@ -0,0 +1,23 @@
1
+ require 'schwab/operations/base_operation'
2
+
3
+ module Schwab; module Operations
4
+ class GetQuotes < BaseOperation
5
+
6
+ FIELDS = %i[quote fundamental]
7
+
8
+ def call(symbols: [], fields: nil)
9
+ raise(ArgumentError, 'fields must be :quote or :fundamental') if fields.present? && FIELDS.exclude?(fields)
10
+
11
+ params = {
12
+ symbols: symbols.join(','),
13
+ }
14
+ params.merge!(fields:) if fields.present?
15
+
16
+ perform_api_get_request(
17
+ url: "https://api.schwabapi.com/marketdata/v1/quotes",
18
+ query: params,
19
+ )
20
+ end
21
+
22
+ end
23
+ end; end
@@ -0,0 +1,22 @@
1
+ module Schwab
2
+ ; module Operations; module Support
3
+ module BuildWatchlistItems
4
+
5
+ # This gem only supports EQUITY type, even though there is a lot more you can do with the API
6
+ def build_watchlist_items(symbol_list)
7
+ symbol_list.map do |symbol|
8
+ {
9
+ "quantity": 0,
10
+ "averagePrice": 0,
11
+ "commission": 0,
12
+ "purchasedDate": (Date.today).strftime('%Y-%m-%d'),
13
+ "instrument": {
14
+ "symbol": symbol,
15
+ "assetType": "EQUITY"
16
+ }
17
+ }
18
+ end
19
+ end
20
+
21
+ end
22
+ end; end; end
@@ -0,0 +1,12 @@
1
+ require 'schwab/error'
2
+
3
+ module Schwab
4
+ module Util
5
+ module_function
6
+
7
+ def response_success?(response)
8
+ response.code.to_s =~ /^2\d\d/
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Schwab
2
+ VERSION = '2.0.0'
3
+ end
@@ -0,0 +1 @@
1
+ require 'schwab'
data/lib/schwab.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'httparty'
2
+
3
+ require 'schwab/client'
4
+ require 'schwab/version'
5
+
6
+ module Schwab
7
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'schwab/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "schwab-api-ruby"
8
+ spec.version = Schwab::VERSION
9
+ spec.authors = ["Winston Kotzan"]
10
+ spec.email = ["wak@wakproductions.com"]
11
+ spec.summary = %q{This is a simple gem for connecting to the Schwab OAuth API}
12
+ spec.description = "This is a gem for connecting to the OAuth/JSON-based Schwab API. See "
13
+ "https://beta-developer.schwab.com/ for the official documentation and to create your developer account."
14
+ spec.homepage = "https://github.com/wakproductions/schwab-api-ruby"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = [`git ls-files`.split($/)] + Dir["lib/**/*"]
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "bundler", ">= 2.0"
23
+ spec.add_dependency "rake"
24
+ spec.add_dependency "httparty", ">= 0.20"
25
+
26
+ spec.add_development_dependency "rspec", ">= 3.2"
27
+ spec.add_development_dependency "pry"
28
+ spec.add_development_dependency "webmock"
29
+ end