· 6 years ago · Dec 31, 2019, 06:20 AM
1var W3CWebSocket = require('websocket').w3cwebsocket;
2const axios = require('axios');
3const fs = require('fs');
4
5
6// Some helpers functions for url encoding (TD API is weird... but whatevs...)
7function jsonToQueryString(json) {
8 return Object.keys(json).map(function (key) {
9 return encodeURIComponent(key) + '=' +
10 encodeURIComponent(json[key]);
11 }).join('&');
12}
13
14// End Helpers
15
16
17// We need to store the access_token data separately. Each time we connect, if > 30 minutes,
18// we'll need to resend in the old access_token with the refresh_token
19// The refresh_token is what expires in 90 days, but the access_token I believe in 30 minutes. It is stupid.
20// Here are the simplified steps to get everything going:
21// 1) Create an app here: https://developer.tdameritrade.com/user/me/apps with setting uri-redirect to https://localhost:8443
22// 2) From the app, look under "KEYS" and copy the "Consumer Key", which henceforth will be known as {CLIENT_ID} in this writeup
23// 3) <THIS IS JUST A MUST-READ STEP> How the authentication works is you're given an access_token for your app and a refresh token. The
24// documentation is confusing but essentially the access_token expires in like 30 minutes and you use the refresh_token (90 days)
25// to update the access_token. The access_token is what ultimately lets you connect to the streamer. Thus, we need to preload
26// this output into a file manually, until I can get a better workaround going. Then, each time the app starts, we'll re-fresh automatically
27// the access_token, of course until the refresh_token is invalidated
28// 4) Open a web browser and go to the following url. BE SURE TO INSERT YOUR {CLIENT_ID} FROM STEP 2 ABOVE INTO THE BELOW URL
29// https://auth.tdameritrade.com/auth?response_type=code&redirect_uri=https%3A%2F%2Flocalhost%3A8443&client_id={CLIENT_ID}%40AMER.OAUTHAP
30// It will ask you to login, and then will take you to a 404 page. No worries - select the url from the address bar, you'll see a code='...'
31// parameter and copy everything AFTER the 'code='
32// 5) Paste the output of step 4 into https://www.urldecoder.org which basically DECODES the url parameter
33// 6) Go to https://developer.tdameritrade.com/authentication/apis/post/token-0 and do the following:
34// - grant_type: authorization_code
35// - access_type: offline
36// - code: {OUTPUT FROM STEP 5}
37// - client_id: {CLIENT_ID}@AMER.OAUTHAP /// NOTICE THE @ symbol, make sure yours doesn't have %40 (url encoded)
38// - redirect_uri: https://localhost:8443
39// 7) PASTE THE JSON OUTPUT FROM #6 into "accessCredentials.json"
40//
41// YOU SHOULD ONLY HAVE TO DO STEPS 4-7 once every 90 days.
42//
43
44// THEN TO CUSTOMIZE THE DATA YOU GET BACK, GO HERE: https://developer.tdameritrade.com/content/streaming-data#_Toc504640625
45// TO MODIFY THE REQUESTED DATA, UPDATE THE VARIBLE esRequest
46//
47// TO THEN HANDLE THE DATA RETURNING, MODIFY >>>>>> mySock.onmessage = function (...) <<<<<< BELOW
48// JUST PUT A console.log() under the main onmessage function if you're not sure what its returning and go from there
49
50
51const accessCredentialsPath = "accessCredentials.json" // Where to store the credentials from
52const CLIENT_ID = "{CLIENT_ID}@AMER.OAUTHAPP" // CLIENT_ID FROM CREATING APP. MUST END IN @AMER.OAUTHAP
53
54
55function GoCollectData(userPrincipalsResponse) {
56
57 var tokenTimeStampAsDateObj = new Date(userPrincipalsResponse.streamerInfo.tokenTimestamp);
58 var tokenTimeStampAsMs = tokenTimeStampAsDateObj.getTime();
59
60 var credentials = {
61 "userid": userPrincipalsResponse.accounts[0].accountId,
62 "token": userPrincipalsResponse.streamerInfo.token,
63 "company": userPrincipalsResponse.accounts[0].company,
64 "segment": userPrincipalsResponse.accounts[0].segment,
65 "cddomain": userPrincipalsResponse.accounts[0].accountCdDomainId,
66 "usergroup": userPrincipalsResponse.streamerInfo.userGroup,
67 "accesslevel": userPrincipalsResponse.streamerInfo.accessLevel,
68 "authorized": "Y",
69 "timestamp": tokenTimeStampAsMs,
70 "appid": userPrincipalsResponse.streamerInfo.appId,
71 "acl": userPrincipalsResponse.streamerInfo.acl
72 }
73
74 var loginRequest = {
75 "requests": [
76 {
77 "service": "ADMIN",
78 "command": "LOGIN",
79 "requestid": 0,
80 "account": userPrincipalsResponse.accounts[0].accountId,
81 "source": userPrincipalsResponse.streamerInfo.appId,
82 "parameters": {
83 "credential": jsonToQueryString(credentials),
84 "token": userPrincipalsResponse.streamerInfo.token,
85 "version": "1.0"
86 }
87 }
88 ]
89 }
90
91 var esRequest = {
92 "requests": [
93 // {
94 // "service": "CHART_HISTORY_FUTURES",
95 // "requestid": "2",
96 // "command": "GET",
97 // "account": userPrincipalsResponse.accounts[0].accountId,
98 // "source": userPrincipalsResponse.streamerInfo.appId,
99 // "parameters": { "symbol": "/ES", "frequency": "m1", "period": "d5" }
100 // }
101 // {
102 // "service": "LEVELONE_FUTURES",
103 // "requestid": "4",
104 // "command": "SUBS",
105 // "account": userPrincipalsResponse.accounts[0].accountId,
106 // "source": userPrincipalsResponse.streamerInfo.appId,
107 // "parameters": {
108 // "keys": "/ES",
109 // "fields": "0,1,2,3,4"
110 // }
111 // }
112 {
113 "service": "TIMESALE_FUTURES",
114 "requestid": "3",
115 "command": "SUBS",
116 "account": userPrincipalsResponse.accounts[0].accountId,
117 "source": userPrincipalsResponse.streamerInfo.appId,
118 "parameters": {
119 "keys": "/ES,/NQ,/CL,/RTY",
120 "fields": "0,1,2,3,4"
121 }
122 }
123 ]
124 }
125
126 // Little hack to make sure we have unique request ids... otherwise they'll just overwrite each other
127 var requestIdCounter = 2;
128 for (item of esRequest['requests']) {
129 item.requestid = requestIdCounter++;
130 }
131
132 var mySock = new W3CWebSocket("wss://" + userPrincipalsResponse.streamerInfo.streamerSocketUrl + "/ws");
133
134 mySock.onmessage = function (evt) {
135
136 var evtAsObject = JSON.parse(evt.data);
137
138 // Wait for us to receive the login response
139 if (evtAsObject.response) { // generic response
140 if (evtAsObject.response[0]["service"] == "ADMIN") {
141 // Sent the actual request now
142 console.log("Sending initialization request...");
143 mySock.send(JSON.stringify(esRequest));
144 } else {
145 console.log(evtAsObject.response);
146 }
147 } else if (evtAsObject.snapshot) { // this returns information, i.e. futures query for 1 minute charts
148 console.log(evt.data);
149 } else if (evtAsObject.notify) { // notify, ex: heartbeat
150
151 } else if (evtAsObject.data) {
152 // COMPLETE ME... WHAT DO YOU WANT TO DO WITH THE DATA
153 // Lets dump this to file
154 console.log(JSON.stringify(evtAsObject.data));
155 }
156 };
157
158 mySock.onclose = function () { console.log("CLOSED"); };
159
160 mySock.onerror = function () {
161 console.log('Connection Error');
162 };
163 mySock.onopen = function () {
164 console.log('Websocket client connected');
165 mySock.send(JSON.stringify(loginRequest));
166 }
167
168}
169
170var accessCredentials = JSON.parse(fs.readFileSync(accessCredentialsPath));
171
172var RefreshAccessCredentials = axios({
173 method: 'post',
174 url: 'https://api.tdameritrade.com/v1/oauth2/token',
175 data: jsonToQueryString({
176 grant_type: "refresh_token",
177 access_type: "offline",
178 refresh_token: accessCredentials["refresh_token"],
179 client_id: CLIENT_ID
180 })
181}).then(function (resp) {
182 // Write data out to file. This is so we can pull it in and grab another one
183 console.log(resp.data);
184 fs.writeFileSync(accessCredentialsPath, JSON.stringify(resp.data));
185 accessCredentials = resp.data;
186 return resp.data;
187}).catch(function (error) {
188 console.error(error);
189}).then(function (refresh_token) {
190 return axios({
191 method: 'get',
192 headers: {
193 "Authorization": "Bearer " + refresh_token['access_token']
194 },
195 params: {
196 "fields": "streamerSubscriptionKeys,streamerConnectionInfo"
197 },
198 url: 'https://api.tdameritrade.com/v1/userprincipals',
199 })
200}).then(function (response) {
201 GoCollectData(response.data);
202});