Skip to content

Instantly share code, notes, and snippets.

@guo14
Last active June 27, 2020 08:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save guo14/b6dcbc20c0fa15cddd25e4383d089a85 to your computer and use it in GitHub Desktop.
Save guo14/b6dcbc20c0fa15cddd25e4383d089a85 to your computer and use it in GitHub Desktop.
ESP32 Quotes TDA
/*
* This package does the following:
* - connects to home WiFi,
* - set clock,
* - setup a websocket connection with TD Ameritrade backend,
* - login and subscribe to quotes of some tickers
*/
#include <heltec.h>
#include <ArduinoJson.h>
#include <ArduinoWebsockets.h>
#include <EasyButton.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
// Arduino pin where the button is connected to.
#define BUTTON_PIN 0
using namespace websockets;
const uint8_t* small_font_size = ArialMT_Plain_10;
const uint8_t* medium_font_size = ArialMT_Plain_16;
const uint8_t* large_font_size = ArialMT_Plain_24;
const char* websockets_server = "wss://streamer-ws.tdameritrade.com/ws";
WebsocketsClient ws_client;
EasyButton button(BUTTON_PIN);
typedef struct {
const char* wifi_ssid;
const char* wifi_password;
const char* login_req; // e.g. "{\"requests\":[{\"service\":\"ADMIN\",\"command\":\"LOGIN\",\"requestid\":0,\"account\":\"YOUR_ACCOUNT_NUMBER\",\"source\":\"DP\",\"parameters\":{\"credential\":\"YOUR_CREDENTIAL\",\"qoslevel\":0,\"token\":\"YOUR_TOKEN\",\"version\":\"1.0\"}}]}";
const char* quote_req; // e.g. "{\"requests\":[{\"service\":\"LEVELONE_FUTURES\",\"requestid\":1,\"command\":\"SUBS\",\"account\":\"YOUR_ACCOUNT_NUMBER\",\"source\":\"DP\",\"parameters\":{\"keys\":\"/ES,/ZN,/GC\",\"fields\":\"0,3,19,20\"}}]}";
} Config;
Config config;
class Quote {
String ticker;
float last_price;
float net_change;
float change_pct;
public:
Quote (String ticker);
void Update(float last_price, float net_change, float change_pct) {
this->last_price = last_price;
this->net_change = net_change;
this->change_pct = change_pct;
}
String RenderPriceStr() {
return ticker + " " + String(last_price, 2);
}
String RenderNetChangeStr() {
return (net_change > 0 ? "+" : " ") + String(net_change, 2);
}
String RenderChangePctStr() {
return (change_pct > 0 ? "+" : " ") + String(change_pct * 100.0, 2) + '%';
}
};
Quote::Quote(String ticker) {
this->ticker = ticker;
}
Quote es ("ES");
Quote zn = Quote("ZN");
Quote gc = Quote("GC");
void InitDisplay() {
// Initialize the Heltec ESP32 object
Heltec.begin(true /*DisplayEnable Enable*/, false /*LoRa Disable*/, true /*Serial Enable*/, true /*PABOOST Enable*/, 470E6 /**/);
Heltec.display -> clear();
}
//void InitConfig() {
// config.wifi_ssid = "";
// config.wifi_password = "";
// // See https://developer.tdameritrade.com/content/streaming-data for details.
// config.login_req = \
// "{\"requests\":[{\"service\":\"ADMIN\",\"command\":\"LOGIN\",\"requestid\":0,\"account\":\"YOUR_ACCOUNT_NUMBER\",\"source\":\"DP\",\"parameters\":{\"credential\":\"YOUR_CREDENTIAL\",\"qoslevel\":0,\"token\":\"YOUR_TOKEN\",\"version\":\"1.0\"}}]}";
// config.quote_req = \
// "{\"requests\":[{\"service\":\"LEVELONE_FUTURES\",\"requestid\":1,\"command\":\"SUBS\",\"account\":\"YOUR_ACCOUNT_NUMBER\",\"source\":\"DP\",\"parameters\":{\"keys\":\"/ES,/ZN,/GC\",\"fields\":\"0,3,19,20\"}}]}";
//}
void InitWiFi() {
WiFi.mode(WIFI_STA);
WiFi.setAutoConnect(true);
WiFi.begin(config.wifi_ssid, config.wifi_password);
while(WiFi.status() != WL_CONNECTED) {
Serial.println("Connecting WiFi...");
delay(500);
}
Heltec.display -> clear();
}
void ParseData(JsonArray& json_array) {
for (int i = 0; i < json_array.size(); i++) {
Quote* quote_to_update;
const char* key = json_array[i]["key"];
String ticker = String(key);
// find ticker to update
if (ticker == "/ES") {
quote_to_update = &es;
} else if (ticker == "/ZN") {
quote_to_update = &zn;
} else if (ticker == "/GC") {
quote_to_update = &gc;
} else {
Serial.print("Unknown ticker: ");
Serial.println(ticker);
continue;
}
// For other fields see https://developer.tdameritrade.com/content/streaming-data#_Toc504640604
quote_to_update->Update(json_array[i]["3"], json_array[i]["19"], json_array[i]["20"]);
}
}
void OnMessageCallback(WebsocketsMessage message) {
Serial.print("Got Message: ");
Serial.println(message.data());
StaticJsonDocument<2000> doc;
DeserializationError error = deserializeJson(doc, message.data());
// Test if parsing succeeds.
if (error) {
Serial.print(F("deserializeJson() failed: "));
Serial.println(error.c_str());
return;
}
if (!doc["data"].isNull()) { // Incoming Data
JsonObject data_0 = doc["data"][0];
JsonArray json_array = data_0["content"]; // complier need some type hints to disambiguate overloads
ParseData(json_array);
PrintScreen();
} else if (!doc["notify"].isNull()) { // Incoming Heartbeat, ignore
Serial.println("Received heartbeat.");
} else if (!doc["response"].isNull()) { // Incoming Command Response
const char* cmd = doc["response"][0]["command"];
long status_code = doc["response"][0]["content"]["code"];
if (status_code != 0) {
Serial.println("Error in status_code, abort.");
}
if (strcmp(cmd, "LOGIN") == 0) {
Serial.println("Logged in TD Ameritrade.");
ws_client.send(config.quote_req);
} else if (strcmp(cmd, "SUBS") == 0) {
Serial.println("Subscribed quote stream.");
} else {
Serial.println("Unknown response.");
}
} else {
Serial.println("Unknown incoming doc.");
}
}
void OnEventsCallback(WebsocketsEvent event, String data) {
if(event == WebsocketsEvent::ConnectionOpened) {
Serial.println("Connnection Opened");
ws_client.send(config.login_req);
} else if(event == WebsocketsEvent::ConnectionClosed) {
Serial.println("Connnection Closed");
// TODO: print error, delay, then retry
} else if(event == WebsocketsEvent::GotPing) {
Serial.println("Got a Ping!");
ws_client.pong();
} else if(event == WebsocketsEvent::GotPong) {
Serial.println("Got a Pong!");
}
}
void InitWebSocket() {
// Setup Callbacks
ws_client.onMessage(OnMessageCallback);
ws_client.onEvent(OnEventsCallback);
// Connect to server
ws_client.connect(websockets_server);
}
void PrintScreen() {
Heltec.display -> clear();
Heltec.display -> setFont(large_font_size);
Heltec.display -> drawString(0, 0, es.RenderPriceStr());
Heltec.display -> setFont(medium_font_size);
Heltec.display -> drawString(0, 24, es.RenderNetChangeStr() + " " + es.RenderChangePctStr());
Heltec.display -> drawHorizontalLine(0, 40, 128);
Heltec.display -> setFont(small_font_size);
Heltec.display -> drawString(0, 41, String(zn.RenderPriceStr() + " " + zn.RenderNetChangeStr() + " " + zn.RenderChangePctStr()));
Heltec.display -> drawHorizontalLine(0, 52, 128);
Heltec.display -> drawString(0, 52, String(gc.RenderPriceStr() + " " + gc.RenderNetChangeStr() + " " + gc.RenderChangePctStr()));
Heltec.display -> display();
}
uint8_t brightness = -1;
// Callback function to be called when the button is pressed.
void OnPressed() {
brightness -= 63;
Serial.print("Brightness changed to: ");
Serial.println(brightness);
Heltec.display -> setBrightness(brightness);
}
// the setup routine runs once when starts up
void setup() {
Serial.begin(115200);
InitConfig();
InitDisplay();
InitWiFi();
InitWebSocket();
// Initialize the button.
button.begin();
// Add the callback function to be called when the button is pressed.
button.onPressed(OnPressed);
}
// the loop routine runs over and over again forever
void loop() {
ws_client.poll();
button.read();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment