schwab-api-ruby 2.0.0

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