tdameritrade-api-ruby 1.3.0.20210215

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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +2 -0
  4. data/CHANGELOG.md +29 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +84 -0
  8. data/Rakefile +8 -0
  9. data/lib/tdameritrade-api-ruby.rb +1 -0
  10. data/lib/tdameritrade.rb +9 -0
  11. data/lib/tdameritrade/authentication.rb +65 -0
  12. data/lib/tdameritrade/client.rb +56 -0
  13. data/lib/tdameritrade/error.rb +20 -0
  14. data/lib/tdameritrade/operations/base_operation.rb +36 -0
  15. data/lib/tdameritrade/operations/create_watchlist.rb +30 -0
  16. data/lib/tdameritrade/operations/get_instrument_fundamentals.rb +22 -0
  17. data/lib/tdameritrade/operations/get_price_history.rb +55 -0
  18. data/lib/tdameritrade/operations/get_quotes.rb +21 -0
  19. data/lib/tdameritrade/operations/get_watchlists.rb +12 -0
  20. data/lib/tdameritrade/operations/replace_watchlist.rb +33 -0
  21. data/lib/tdameritrade/operations/support/build_watchlist_items.rb +21 -0
  22. data/lib/tdameritrade/operations/update_watchlist.rb +33 -0
  23. data/lib/tdameritrade/util.rb +50 -0
  24. data/lib/tdameritrade/version.rb +3 -0
  25. data/spec/spec_helper.rb +110 -0
  26. data/spec/support/authenticated_client.rb +12 -0
  27. data/spec/support/spec/mocks/mock_get_instrument_fundamentals.rb +17 -0
  28. data/spec/support/spec/mocks/mock_get_price_history.rb +17 -0
  29. data/spec/support/spec/mocks/mock_get_quotes.rb +17 -0
  30. data/spec/support/spec/mocks/mock_watchlists.rb +43 -0
  31. data/spec/support/spec/mocks/tdameritrade_api_mock_base.rb +21 -0
  32. data/spec/support/webmock_off.rb +7 -0
  33. data/spec/tdameritrade/operations/create_watchlist_spec.rb +42 -0
  34. data/spec/tdameritrade/operations/error_spec.rb +75 -0
  35. data/spec/tdameritrade/operations/get_instrument_fundamentals_spec.rb +182 -0
  36. data/spec/tdameritrade/operations/get_price_history_spec.rb +160 -0
  37. data/spec/tdameritrade/operations/get_quotes_spec.rb +351 -0
  38. data/spec/tdameritrade/operations/get_watchlists_spec.rb +80 -0
  39. data/spec/tdameritrade/operations/replace_watchlist_spec.rb +45 -0
  40. data/spec/tdameritrade/operations/update_watchlist_spec.rb +45 -0
  41. data/tdameritrade-api-ruby.gemspec +33 -0
  42. metadata +253 -0
@@ -0,0 +1,21 @@
1
+ module TDAmeritrade; module Spec; module Mocks
2
+ class TDAmeritradeMockBase
3
+
4
+ API_BASE_URL = 'https://api.tdameritrade.com/v1'
5
+
6
+ def self.build_url_params(request)
7
+ # Need to do this to get around a WebMock encoding bug. It's looking for 'APIKEY@AMER.OAUTHAP' when it
8
+ # should be looking for 'APIKEY%40AMER.OAUTHAP'
9
+ if request[:query]
10
+ "?" + request.delete(:query).map { |k,v| "#{k}=#{CGI::escape(v)}" }.join('&')
11
+ else
12
+ ''
13
+ end
14
+ end
15
+
16
+ def self.webmock_off?
17
+ WebMock.net_connect_allowed?
18
+ end
19
+
20
+ end
21
+ end; end; end
@@ -0,0 +1,7 @@
1
+ # Include this context if you want to do a live test. Be sure to enter actual connection details in
2
+ # authenticated_client.rb so that it authenticates.
3
+ shared_context 'webmock off' do
4
+ before do
5
+ WebMock.allow_net_connect!
6
+ end
7
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+ require 'support/authenticated_client'
3
+ require 'tdameritrade'
4
+
5
+ describe TDAmeritrade::Operations::CreateWatchlist do
6
+ include_context 'authenticated client'
7
+ # include_context 'webmock off'
8
+
9
+ context 'for single account' do
10
+ subject do
11
+ client.create_watchlist(account_id, 'My New Watchlist', %w(AAPL MSFT VZ))
12
+ end
13
+
14
+ let(:account_id) { '123456789' }
15
+
16
+ let!(:expected_request) do
17
+ TDAmeritrade::Spec::Mocks::MockWatchlists.mock_create(
18
+ account_id,
19
+ request: {
20
+ headers: { 'Authorization': "Bearer #{client.access_token}" },
21
+ body: expected_request_body
22
+ }
23
+ )
24
+ end
25
+
26
+ let(:expected_request_body) do
27
+ {
28
+ :name=>"My New Watchlist",
29
+ :watchlistItems=>
30
+ [
31
+ {:quantity=>0, :averagePrice=>0, :commission=>0, :purchasedDate=>Date.today.strftime('%Y-%m-%d'), :instrument=>{:symbol=>"AAPL", :assetType=>"EQUITY"}},
32
+ {:quantity=>0, :averagePrice=>0, :commission=>0, :purchasedDate=>Date.today.strftime('%Y-%m-%d'), :instrument=>{:symbol=>"MSFT", :assetType=>"EQUITY"}},
33
+ {:quantity=>0, :averagePrice=>0, :commission=>0, :purchasedDate=>Date.today.strftime('%Y-%m-%d'), :instrument=>{:symbol=>"VZ", :assetType=>"EQUITY"}}
34
+ ]
35
+ }.to_json
36
+ end
37
+
38
+ it { is_expected.to be_truthy }
39
+ end
40
+
41
+
42
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+ require 'support/authenticated_client'
3
+ require 'tdameritrade'
4
+
5
+ describe TDAmeritrade::Error do
6
+ include_context 'authenticated client'
7
+ # include_context 'webmock off'
8
+
9
+ context 'rate limit error' do
10
+ subject do
11
+ client.get_quotes(symbols)
12
+ end
13
+
14
+ let(:symbols) { %w(PG MSFT CVX) }
15
+
16
+ let!(:expected_request) do
17
+ TDAmeritrade::Spec::Mocks::MockGetQuotes.mock_find(
18
+ request: {
19
+ headers: { 'Authorization': "Bearer #{client.access_token}" },
20
+ query: {
21
+ apikey: client.client_id,
22
+ symbol: symbols.join(','),
23
+ }.map {|k,v| [k, v.to_s] }.to_h,
24
+ },
25
+ response: {
26
+ status: 429,
27
+ body: <<~RESPONSE
28
+ {"error":"Rate limit error"}
29
+ RESPONSE
30
+ }
31
+ )
32
+ end
33
+
34
+ let(:expected_result) do
35
+ end
36
+
37
+ it 'raises a RateLimitError' do
38
+ expect { subject }.to raise_error(TDAmeritrade::Error::RateLimitError)
39
+ end
40
+ end
41
+
42
+ context 'access token is invalid or expired' do
43
+ subject do
44
+ client.get_quotes(symbols)
45
+ end
46
+
47
+ let(:symbols) { %w(PG MSFT CVX) }
48
+
49
+ let!(:expected_request) do
50
+ TDAmeritrade::Spec::Mocks::MockGetQuotes.mock_find(
51
+ request: {
52
+ headers: { 'Authorization': "Bearer #{client.access_token}" },
53
+ query: {
54
+ apikey: client.client_id,
55
+ symbol: symbols.join(','),
56
+ }.map {|k,v| [k, v.to_s] }.to_h,
57
+ },
58
+ response: {
59
+ status: 401,
60
+ body: <<~RESPONSE
61
+ {"error":"Not Authorized."}
62
+ RESPONSE
63
+ }
64
+ )
65
+ end
66
+
67
+ let(:expected_result) do
68
+ end
69
+
70
+ it 'raises a NotAuthorizedError' do
71
+ expect { subject }.to raise_error(TDAmeritrade::Error::NotAuthorizedError)
72
+ end
73
+ end
74
+
75
+ end
@@ -0,0 +1,182 @@
1
+ require 'spec_helper'
2
+ require 'support/authenticated_client'
3
+ require 'tdameritrade'
4
+
5
+ describe TDAmeritrade::Operations::GetInstrumentFundamentals do
6
+ include_context 'authenticated client'
7
+ # include_context 'webmock off'
8
+
9
+ context 'valid symbol' do
10
+ subject do
11
+ client.get_instrument_fundamentals(symbol)
12
+ end
13
+
14
+ let(:symbol) { 'PG' }
15
+
16
+ let!(:expected_request) do
17
+ TDAmeritrade::Spec::Mocks::MockGetInstrumentFundamentals.mock_find(
18
+ request: {
19
+ headers: { 'Authorization': "Bearer #{client.access_token}" },
20
+ query: {
21
+ apikey: client.client_id,
22
+ symbol: symbol,
23
+ projection: 'fundamental'
24
+ }.map {|k,v| [k, v.to_s] }.to_h,
25
+ },
26
+ response: {
27
+ body: <<~RESPONSE
28
+ {
29
+ "PG": {
30
+ "fundamental": {
31
+ "symbol": "PG",
32
+ "high52": 121.76,
33
+ "low52": 78.49,
34
+ "dividendAmount": 2.9836,
35
+ "dividendYield": 2.54,
36
+ "dividendDate": "2019-07-18 00:00:00.000",
37
+ "peRatio": 79.14674,
38
+ "pegRatio": 0.0,
39
+ "pbRatio": 6.35135,
40
+ "prRatio": 4.33729,
41
+ "pcfRatio": 44.97703,
42
+ "grossMarginTTM": 49.84339,
43
+ "grossMarginMRQ": 50.80145,
44
+ "netProfitMarginTTM": 5.85958,
45
+ "netProfitMarginMRQ": 0.0,
46
+ "operatingMarginTTM": 8.61208,
47
+ "operatingMarginMRQ": 0.0,
48
+ "returnOnEquity": 7.44733,
49
+ "returnOnAssets": 3.39838,
50
+ "returnOnInvestment": 4.55386,
51
+ "quickRatio": 0.58165,
52
+ "currentRatio": 0.74883,
53
+ "interestCoverage": 467.625,
54
+ "totalDebtToCapital": 38.7429,
55
+ "ltDebtToEquity": 43.21524,
56
+ "totalDebtToEquity": 63.76234,
57
+ "epsTTM": 1.48231,
58
+ "epsChangePercentTTM": 0.0,
59
+ "epsChangeYear": 0.0,
60
+ "epsChange": 0.0,
61
+ "revChangeYear": 0.0,
62
+ "revChangeTTM": 1.27484,
63
+ "revChangeIn": 3.83914,
64
+ "sharesOutstanding": 2502.26,
65
+ "marketCapFloat": 2499.515,
66
+ "marketCap": 293565.1,
67
+ "bookValuePerShare": 0.0,
68
+ "shortIntToFloat": 0.0,
69
+ "shortIntDayToCover": 0.0,
70
+ "divGrowthRate3Year": 0.0,
71
+ "dividendPayAmount": 0.7459,
72
+ "dividendPayDate": "2019-11-15 00:00:00.000",
73
+ "beta": 0.40382,
74
+ "vol1DayAvg": 6650680.0,
75
+ "vol10DayAvg": 6548680.0,
76
+ "vol3MonthAvg": 161420290.0
77
+ },
78
+ "cusip": "742718109",
79
+ "symbol": "PG",
80
+ "description": "Procter & Gamble Company (The) Common Stock",
81
+ "exchange": "NYSE",
82
+ "assetType": "EQUITY"
83
+ }
84
+ }
85
+ RESPONSE
86
+ }
87
+ )
88
+ end
89
+
90
+ let(:expected_result) do
91
+ {"PG"=>
92
+ {"fundamental"=>
93
+ {"symbol"=>"PG",
94
+ "high52"=>121.76,
95
+ "low52"=>78.49,
96
+ "dividendAmount"=>2.9836,
97
+ "dividendYield"=>2.54,
98
+ "dividendDate"=>"2019-07-18 00:00:00.000",
99
+ "peRatio"=>79.14674,
100
+ "pegRatio"=>0.0,
101
+ "pbRatio"=>6.35135,
102
+ "prRatio"=>4.33729,
103
+ "pcfRatio"=>44.97703,
104
+ "grossMarginTTM"=>49.84339,
105
+ "grossMarginMRQ"=>50.80145,
106
+ "netProfitMarginTTM"=>5.85958,
107
+ "netProfitMarginMRQ"=>0.0,
108
+ "operatingMarginTTM"=>8.61208,
109
+ "operatingMarginMRQ"=>0.0,
110
+ "returnOnEquity"=>7.44733,
111
+ "returnOnAssets"=>3.39838,
112
+ "returnOnInvestment"=>4.55386,
113
+ "quickRatio"=>0.58165,
114
+ "currentRatio"=>0.74883,
115
+ "interestCoverage"=>467.625,
116
+ "totalDebtToCapital"=>38.7429,
117
+ "ltDebtToEquity"=>43.21524,
118
+ "totalDebtToEquity"=>63.76234,
119
+ "epsTTM"=>1.48231,
120
+ "epsChangePercentTTM"=>0.0,
121
+ "epsChangeYear"=>0.0,
122
+ "epsChange"=>0.0,
123
+ "revChangeYear"=>0.0,
124
+ "revChangeTTM"=>1.27484,
125
+ "revChangeIn"=>3.83914,
126
+ "sharesOutstanding"=>2502.26,
127
+ "marketCapFloat"=>2499.515,
128
+ "marketCap"=>293565.1,
129
+ "bookValuePerShare"=>0.0,
130
+ "shortIntToFloat"=>0.0,
131
+ "shortIntDayToCover"=>0.0,
132
+ "divGrowthRate3Year"=>0.0,
133
+ "dividendPayAmount"=>0.7459,
134
+ "dividendPayDate"=>"2019-11-15 00:00:00.000",
135
+ "beta"=>0.40382,
136
+ "vol1DayAvg"=>6650680.0,
137
+ "vol10DayAvg"=>6548680.0,
138
+ "vol3MonthAvg"=>161420290.0},
139
+ "cusip"=>"742718109",
140
+ "symbol"=>"PG",
141
+ "description"=>"Procter & Gamble Company (The) Common Stock",
142
+ "exchange"=>"NYSE",
143
+ "assetType"=>"EQUITY"}
144
+ }
145
+ end
146
+
147
+ it { is_expected.to eql(expected_result) }
148
+ end
149
+
150
+ context 'invalid symbol' do
151
+ subject do
152
+ client.get_instrument_fundamentals(invalid_symbol)
153
+ end
154
+
155
+ let(:invalid_symbol) { 'XXX' }
156
+
157
+ let!(:expected_request) do
158
+ TDAmeritrade::Spec::Mocks::MockGetInstrumentFundamentals.mock_find(
159
+ request: {
160
+ headers: { 'Authorization': "Bearer #{client.access_token}" },
161
+ query: {
162
+ apikey: client.client_id,
163
+ symbol: invalid_symbol,
164
+ projection: 'fundamental'
165
+ }.map {|k,v| [k, v.to_s] }.to_h,
166
+ },
167
+ response: {
168
+ body: <<~RESPONSE
169
+ {}
170
+ RESPONSE
171
+ }
172
+ )
173
+ end
174
+
175
+ let(:expected_result) do
176
+ {}
177
+ end
178
+
179
+ it { is_expected.to eql(expected_result) }
180
+ end
181
+
182
+ end
@@ -0,0 +1,160 @@
1
+ require 'spec_helper'
2
+ require 'support/authenticated_client'
3
+ require 'tdameritrade'
4
+
5
+ describe TDAmeritrade::Operations::GetPriceHistory do
6
+ include_context 'authenticated client'
7
+ # include_context 'webmock off'
8
+
9
+ let(:single_ticker) { 'PG' }
10
+ let(:multiple_tickers) { ['PG', 'MSFT', 'BA'] }
11
+
12
+ context '5 days of 5-min candles' do
13
+ context 'single ticker' do
14
+ subject do
15
+ client.get_price_history(single_ticker, period_type: :day, period: 10, frequency: 5, frequency_type: :minute)
16
+ end
17
+
18
+ let!(:expected_request) do
19
+ TDAmeritrade::Spec::Mocks::MockGetPriceHistory.mock_find(
20
+ symbol: single_ticker,
21
+ request: {
22
+ headers: { 'Authorization': "Bearer #{client.access_token}" },
23
+ query: {
24
+ apikey: client.client_id,
25
+ periodType: 'day',
26
+ period: 10,
27
+ frequencyType: 'minute',
28
+ frequency: 5,
29
+ needExtendedHoursData: 'false'
30
+ }.map {|k,v| [k, v.to_s] }.to_h,
31
+ },
32
+ response: {
33
+ body: <<~RESPONSE
34
+ {
35
+ "candles": [
36
+ {
37
+ "open": 119.44,
38
+ "high": 119.54,
39
+ "low": 119.44,
40
+ "close": 119.54,
41
+ "volume": 208,
42
+ "datetime": 1566558000000
43
+ },
44
+ {
45
+ "open": 119.54,
46
+ "high": 119.54,
47
+ "low": 119.54,
48
+ "close": 119.54,
49
+ "volume": 104,
50
+ "datetime": 1566559800000
51
+ }
52
+ ],
53
+ "symbol": "PG",
54
+ "empty": false
55
+ }
56
+ RESPONSE
57
+ }
58
+ )
59
+ end
60
+
61
+ let(:expected_result) do
62
+ {
63
+ "candles"=>
64
+ [{"open"=>119.44, "high"=>119.54, "low"=>119.44, "close"=>119.54, "volume"=>208, "datetime"=>Time.at(1566558000000 / 1000)},
65
+ {"open"=>119.54, "high"=>119.54, "low"=>119.54, "close"=>119.54, "volume"=>104, "datetime"=>Time.at(1566559800000 / 1000)}],
66
+ "symbol"=>"PG",
67
+ "empty"=>false
68
+ }
69
+ end
70
+
71
+ it { is_expected.to eql(expected_result) }
72
+ end
73
+
74
+ context 'multiple tickers' do
75
+ pending 'does not appear to be supported yet by the API'
76
+ end
77
+
78
+ context 'invalid ticker or one with no data' do
79
+ subject do
80
+ client.get_price_history(invalid_ticker, period: 1, period_type: :month, frequency: 5, frequency_type: :minute)
81
+ end
82
+ let(:invalid_ticker) { 'XXX' }
83
+
84
+ let!(:expected_request) do
85
+ TDAmeritrade::Spec::Mocks::MockGetPriceHistory.mock_find(
86
+ symbol: invalid_ticker,
87
+ request: {
88
+ headers: { 'Authorization': "Bearer #{client.access_token}" },
89
+ query: {
90
+ apikey: client.client_id,
91
+ periodType: 'month',
92
+ period: 1,
93
+ frequencyType: 'minute',
94
+ frequency: 5,
95
+ needExtendedHoursData: 'false'
96
+ }.map {|k,v| [k, v.to_s] }.to_h,
97
+ },
98
+ response: {
99
+ body: <<~RESPONSE
100
+ {"candles":[],"symbol":"XXX","empty":true}
101
+ RESPONSE
102
+ }
103
+ )
104
+ end
105
+
106
+ let(:expected_result) do
107
+ {
108
+ "candles"=> [],
109
+ "symbol"=>"XXX",
110
+ "empty"=>true
111
+ }
112
+ end
113
+
114
+ it { is_expected.to eql(expected_result) }
115
+ end
116
+
117
+ context 'invalid request due to mismatched frequency type and interval' do
118
+ subject do
119
+ # Can't use minute with the monthly period type
120
+ client.get_price_history(
121
+ single_ticker,
122
+ period_type: :month,
123
+ period: 10,
124
+ frequency: 5,
125
+ frequency_type: :minute,
126
+ need_extended_hours_data: true
127
+ )
128
+ end
129
+
130
+ let!(:expected_request) do
131
+ TDAmeritrade::Spec::Mocks::MockGetPriceHistory.mock_find(
132
+ symbol: single_ticker,
133
+ request: {
134
+ headers: { 'Authorization': "Bearer #{client.access_token}" },
135
+ query: {
136
+ apikey: client.client_id,
137
+ periodType: 'month',
138
+ period: 10,
139
+ frequencyType: 'minute',
140
+ frequency: 5,
141
+ needExtendedHoursData: 'true'
142
+ }.map {|k,v| [k, v.to_s] }.to_h,
143
+ },
144
+ response: {
145
+ status: 400,
146
+ body: <<~RESPONSE
147
+ {"error":"Bad request."}
148
+ RESPONSE
149
+ }
150
+ )
151
+ end
152
+
153
+ it 'raises an error' do
154
+ expect { subject }.to raise_error(TDAmeritrade::Error::TDAmeritradeError, "400: Bad request.")
155
+ end
156
+ end
157
+
158
+ end
159
+
160
+ end