summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--COPYING19
-rw-r--r--Makefile20
-rw-r--r--assets.c100
-rw-r--r--assets.h36
-rw-r--r--config.lua96
-rw-r--r--json_curl.c42
-rw-r--r--json_curl.h9
-rw-r--r--output.c131
-rw-r--r--output.h12
-rw-r--r--quotes.c23
-rw-r--r--quotes.h15
-rw-r--r--stonks.c172
-rw-r--r--tda.c253
-rw-r--r--tda.h20
-rw-r--r--yahoo.c106
-rw-r--r--yahoo.h10
16 files changed, 1064 insertions, 0 deletions
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..4849533
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,19 @@
+Copyright 2021 David Vazgenovich Shakaryan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..8fea044
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+CFLAGS += -O2 -Wall -Wextra -Wpedantic
+CPPFLAGS += -MMD -MP
+LDFLAGS += -llua -lcurl -ljson-c
+
+TARGET := stonks
+
+SRC := $(wildcard *.c)
+OBJ := $(SRC:.c=.o)
+DEP := $(OBJ:.o=.d)
+
+all: $(TARGET)
+
+$(TARGET): $(OBJ)
+
+clean:
+ rm -f $(TARGET) $(OBJ) $(DEP)
+
+-include $(DEP)
+
+.PHONY: all clean
diff --git a/assets.c b/assets.c
new file mode 100644
index 0000000..a4e0043
--- /dev/null
+++ b/assets.c
@@ -0,0 +1,100 @@
+#include "quotes.h"
+#include "assets.h"
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+
+char *ASSET_TYPE_NAMES[] = {
+ FOREACH_ASSET_TYPE(GENERATE_STRING)
+};
+
+void asset_free(struct asset *a)
+{
+ free(a->symbol);
+ free(a);
+}
+
+int asset_cmp(const void *a, const void *b)
+{
+ struct asset *aa = *(struct asset **)a;
+ struct asset *bb = *(struct asset **)b;
+
+ if (aa->type > bb->type)
+ return 1;
+ else if (aa->type < bb->type)
+ return -1;
+ else
+ if (aa->usd_tot > bb->usd_tot)
+ return -1;
+ else if (aa->usd_tot < bb->usd_tot)
+ return 1;
+ else
+ return 0;
+}
+
+void add_quotes_to_assets(struct asset **assets, int n_assets,
+ struct quote **quotes, int n_quotes)
+{
+ for (int i = 0; i < n_assets; ++i) {
+ struct quote *quote = quote_find(quotes, n_quotes, assets[i]->symbol,
+ "USD");
+ if (quote) {
+ assets[i]->usd = true;
+ assets[i]->usd_pr = quote->price;
+ assets[i]->usd_ch = quote->change_percent;
+ assets[i]->usd_tot = assets[i]->amt * quote->price *
+ (quote->multiplier ? quote->multiplier : 1);
+ }
+
+ quote = quote_find(quotes, n_quotes, assets[i]->symbol, "BTC");
+ if (quote) {
+ assets[i]->btc = true;
+ assets[i]->btc_pr = quote->price;
+ assets[i]->btc_ch = quote->change_percent;
+ assets[i]->btc_tot = assets[i]->amt * quote->price *
+ (quote->multiplier ? quote->multiplier : 1);
+ } else if (strcmp(assets[i]->symbol, "BTC") == 0) {
+ assets[i]->btc = true;
+ assets[i]->btc_pr = 1;
+ assets[i]->btc_tot = assets[i]->amt;
+ }
+ }
+}
+
+int add_totals(struct asset **assets, int n_assets)
+{
+ struct asset **totals = calloc(4, sizeof(*totals));
+ totals[3] = calloc(1, sizeof(**totals));
+
+ for (int i = 0; i < n_assets; ++i) {
+ if (assets[i]->usd) {
+ totals[3]->usd = true;
+ totals[3]->usd_tot += assets[i]->usd_tot;
+ if (!totals[assets[i]->type])
+ totals[assets[i]->type] = calloc(1, sizeof(**totals));
+ totals[assets[i]->type]->usd = true;
+ totals[assets[i]->type]->usd_tot += assets[i]->usd_tot;
+ }
+ if (assets[i]->btc) {
+ totals[3]->btc = true;
+ totals[3]->btc_tot += assets[i]->btc_tot;
+ if (!totals[assets[i]->type])
+ totals[assets[i]->type] = calloc(1, sizeof(**totals));
+ totals[assets[i]->type]->btc = true;
+ totals[assets[i]->type]->btc_tot += assets[i]->btc_tot;
+ }
+ }
+
+ int c = 0;
+ for (int i = 0; i < 4; ++i) {
+ if (totals[i]) {
+ totals[i]->symbol = strdup(ASSET_TYPE_NAMES[i]);
+ totals[i]->type = TOTAL;
+ assets[n_assets + c++] = totals[i];
+ }
+ }
+
+ free(totals);
+ return c;
+}
diff --git a/assets.h b/assets.h
new file mode 100644
index 0000000..211ad66
--- /dev/null
+++ b/assets.h
@@ -0,0 +1,36 @@
+#ifndef ASSETS_H
+#define ASSETS_H
+
+#include "quotes.h"
+
+#include <stdbool.h>
+
+#define FOREACH_ASSET_TYPE(M) \
+ M(STOCK) \
+ M(OPTION) \
+ M(CRYPTO) \
+ M(TOTAL)
+#define GENERATE_ENUM(T) T,
+#define GENERATE_STRING(T) #T,
+
+enum asset_type {
+ FOREACH_ASSET_TYPE(GENERATE_ENUM)
+};
+
+// FIXME PREFIX
+extern char *ASSET_TYPE_NAMES[];
+
+struct asset {
+ char *symbol;
+ enum asset_type type;
+ bool usd, btc;
+ double amt, usd_pr, usd_ch, usd_tot, btc_pr, btc_ch, btc_tot;
+};
+
+void asset_free(struct asset *a);
+int asset_cmp(const void *a, const void *b);
+
+void add_quotes_to_assets(struct asset **, int, struct quote **, int);
+int add_totals(struct asset **, int);
+
+#endif
diff --git a/config.lua b/config.lua
new file mode 100644
index 0000000..8af2a1a
--- /dev/null
+++ b/config.lua
@@ -0,0 +1,96 @@
+stock = {
+ SPY = 100,
+ VTI = 10
+}
+crypto = {
+ BTC = 1.1,
+ ETH = 11,
+ LTC = 111
+}
+tda = {
+ account_id = "x",
+ client_id = "x",
+ token_path = "/path/to/token"
+}
+
+columns = {
+ {
+ "symbol", function(str, asset)
+ return colour_symbol(str, asset)
+ end
+ },
+ {
+ "usd_price", function(str, asset)
+ return colour_zeros(group_thousands(string.format("%.4f", str)))
+ end
+ },
+ {
+ "usd_change", function(str, asset)
+ return colour_change(string.format("%.2f%%", str))
+ end
+ },
+ {
+ "usd_total", function(str, asset)
+ return colour_zeros(group_thousands(string.format("%.2f", str)))
+ end
+ },
+ {
+ "btc_price", function(str, asset)
+ return colour_zeros(string.format("%.8f", str))
+ end
+ },
+ {
+ "btc_change", function(str, asset)
+ return colour_change(string.format("%.2f%%", str))
+ end
+ },
+ {
+ "btc_total", function(str, asset)
+ return colour_zeros(string.format("%.4f", str))
+ end
+ }
+}
+
+function group_thousands (str)
+ local n
+ repeat
+ str, n = string.gsub(str, "^(%d+)(%d%d%d)", "%1,%2")
+ until n == 0
+
+ return str
+end
+
+function colour_zeros (str)
+ local x, y = string.match(str, "(.*%.0?.-)(0*)$")
+ if y == "" then
+ return str
+ else
+ return x .. colour(y, "90")
+ end
+end
+
+function colour (str, code)
+ return string.format("\27[%sm%s\27[0m", code, str)
+end
+
+function colour_change (str)
+ if str:sub(1, 1) == "-" then
+ return colour(str, "31")
+ else
+ return colour(str, "32")
+ end
+end
+
+function colour_symbol (str, asset)
+ local t = get_asset_field(asset, "type")
+
+ if t == "STOCK" then
+ return colour(str, "1;36")
+ elseif t == "OPTION" then
+ return colour(str, "1;34")
+ elseif t == "CRYPTO" then
+ return colour(str, "1;33")
+ else
+ return colour(str, "1;35")
+ end
+end
diff --git a/json_curl.c b/json_curl.c
new file mode 100644
index 0000000..1911b0f
--- /dev/null
+++ b/json_curl.c
@@ -0,0 +1,42 @@
+#include "json_curl.h"
+
+#include <stdlib.h>
+#include <curl/curl.h>
+#include <json-c/json.h>
+
+struct json_builder {
+ json_object *obj;
+ json_tokener *tok;
+};
+
+static size_t parse_cb(char *data, size_t size, size_t nmemb, void *userdata)
+{
+ size_t rsize = size * nmemb;
+ struct json_builder *jb = userdata;
+
+ // extra data after the first valid object is ignored.
+ if (!jb->obj) {
+ json_object *tmp = json_tokener_parse_ex(jb->tok, data, rsize);
+ if (tmp)
+ jb->obj = tmp;
+ else if (json_tokener_get_error(jb->tok) != json_tokener_continue)
+ return 0;
+ }
+
+ return rsize;
+}
+
+json_object *json_curl_perform(CURL *curl, const char *uri)
+{
+ struct json_builder jb = { NULL, json_tokener_new() };
+
+ if (uri) curl_easy_setopt(curl, CURLOPT_URL, uri);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, parse_cb);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&jb);
+
+ CURLcode res = curl_easy_perform(curl);
+ json_tokener_free(jb.tok);
+ if (res != CURLE_OK) return NULL;
+
+ return jb.obj;
+}
diff --git a/json_curl.h b/json_curl.h
new file mode 100644
index 0000000..ad03f85
--- /dev/null
+++ b/json_curl.h
@@ -0,0 +1,9 @@
+#ifndef JSON_CURL_H
+#define JSON_CURL_H
+
+#include <curl/curl.h>
+#include <json-c/json.h>
+
+json_object *json_curl_perform(CURL *curl, const char *uri);
+
+#endif
diff --git a/output.c b/output.c
new file mode 100644
index 0000000..03199ab
--- /dev/null
+++ b/output.c
@@ -0,0 +1,131 @@
+#include "assets.h"
+#include "output.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <lua.h>
+
+static int push_field(lua_State *L, struct asset *a, const char *field)
+{
+ int n = 0;
+
+ if (strcmp(field, "symbol") == 0) {
+ lua_pushstring(L, a->symbol);
+ ++n;
+ } else if (strcmp(field, "type") == 0) {
+ lua_pushstring(L, ASSET_TYPE_NAMES[a->type]);
+ ++n;
+ } else if (strcmp(field, "usd_price") == 0) {
+ if (a->usd && a->type != TOTAL) {
+ lua_pushnumber(L, a->usd_pr);
+ ++n;
+ }
+ } else if (strcmp(field, "usd_change") == 0) {
+ if (a->usd && a->type != TOTAL) {
+ lua_pushnumber(L, a->usd_ch);
+ ++n;
+ }
+ } else if (strcmp(field, "usd_total") == 0) {
+ if (a->usd) {
+ lua_pushnumber(L, a->usd_tot);
+ ++n;
+ }
+ } else if (strcmp(field, "btc_price") == 0) {
+ if (a->btc && a->type != TOTAL) {
+ lua_pushnumber(L, a->btc_pr);
+ ++n;
+ }
+ } else if (strcmp(field, "btc_change") == 0) {
+ if (a->btc && a->type != TOTAL) {
+ lua_pushnumber(L, a->btc_ch);
+ ++n;
+ }
+ } else if (strcmp(field, "btc_total") == 0) {
+ if (a->btc) {
+ lua_pushnumber(L, a->btc_tot);
+ ++n;
+ }
+ }
+
+ return n;
+}
+
+static int decolour_len(const char *str)
+{
+ int len = strlen(str);
+
+ while ((str = strchr(str, '\033'))) {
+ char *ptr = strchr(str, 'm') + 1;
+ len -= ptr - str;
+ str = ptr;
+ }
+
+ return len;
+}
+
+void output(struct asset **assets, int assets_len, lua_State *L)
+{
+ lua_getglobal(L, "columns");
+ int len = lua_rawlen(L, -1);
+ lua_pop(L, 1);
+
+ int *widths = calloc(len, sizeof(int));
+ char ***out = calloc(assets_len, sizeof(char **));
+
+ for (int i = 0; i < assets_len; ++i) {
+ out[i] = calloc(len, sizeof(char *));
+
+ lua_getglobal(L, "columns");
+ lua_pushnil(L);
+ int j = 0;
+ while (lua_next(L, -2)) {
+ lua_rawgeti(L, -1, 1);
+ const char *x = lua_tostring(L, -1);
+ lua_pop(L, 1);
+
+ char *y;
+ lua_rawgeti(L, -1, 2);
+ if (push_field(L, assets[i], x) > 0) {
+ lua_pushlightuserdata(L, assets[i]);
+ lua_pcall(L, 2, 1, 0);
+ y = strdup(lua_tostring(L, -1));
+ } else {
+ y = strdup("");
+ }
+ lua_pop(L, 1);
+
+ int dlen = decolour_len(y);
+ if (dlen > widths[j])
+ widths[j] = dlen;
+
+ out[i][j++] = y;
+ lua_pop(L, 1);
+ }
+ lua_pop(L, 1);
+ }
+
+ for (int i = 0; i < assets_len; ++i) {
+ for (int j = 0; j < len; ++j) {
+ printf("%*s%s", widths[j] - decolour_len(out[i][j]) + 2, "",
+ out[i][j]);
+ }
+ printf("\n");
+ }
+
+ free(widths);
+ for (int i = 0; i < assets_len; ++i) {
+ for (int j = 0; j < len; ++j) {
+ free(out[i][j]);
+ }
+ free(out[i]);
+ }
+ free(out);
+}
+
+int push_field_lua(lua_State *L) {
+ struct asset *a = lua_touserdata(L, 1);
+ const char *field = lua_tostring(L, 2);
+ return push_field(L, a, field);
+}
diff --git a/output.h b/output.h
new file mode 100644
index 0000000..bbd727d
--- /dev/null
+++ b/output.h
@@ -0,0 +1,12 @@
+#ifndef OUTPUT_H
+#define OUTPUT_H
+
+#include "assets.h"
+
+#include <lua.h>
+
+void output(struct asset **, int, lua_State *);
+
+int push_field_lua(lua_State *);
+
+#endif
diff --git a/quotes.c b/quotes.c
new file mode 100644
index 0000000..5e269e0
--- /dev/null
+++ b/quotes.c
@@ -0,0 +1,23 @@
+#include "quotes.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+void quote_free(struct quote *q)
+{
+ free(q->symbol);
+ free(q->currency);
+ free(q);
+}
+
+struct quote *quote_find(struct quote **quotes, int n_quotes,
+ const char *symbol, const char *currency)
+{
+ for (int i = 0; i < n_quotes; ++i) {
+ if (strcmp(quotes[i]->symbol, symbol) == 0 &&
+ strcmp(quotes[i]->currency, currency) == 0)
+ return quotes[i];
+ }
+
+ return NULL;
+}
diff --git a/quotes.h b/quotes.h
new file mode 100644
index 0000000..489189a
--- /dev/null
+++ b/quotes.h
@@ -0,0 +1,15 @@
+#ifndef QUOTES_H
+#define QUOTES_H
+
+struct quote {
+ char *symbol;
+ char *currency;
+ double price, multiplier, change_percent;
+};
+
+void quote_free(struct quote *q);
+
+struct quote *quote_find(struct quote **quotes, int n_quotes,
+ const char *symbol, const char *currency);
+
+#endif
diff --git a/stonks.c b/stonks.c
new file mode 100644
index 0000000..77b7296
--- /dev/null
+++ b/stonks.c
@@ -0,0 +1,172 @@
+#include "assets.h"
+#include "json_curl.h"
+#include "output.h"
+#include "quotes.h"
+#include "tda.h"
+#include "yahoo.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <lauxlib.h>
+#include <lua.h>
+#include <lualib.h>
+
+#define ASSETS_MAX 100
+
+int load_assets_stock(struct asset **assets, lua_State *L)
+{
+ lua_getglobal(L, "stock");
+ lua_pushnil(L);
+
+ int n = 0;
+ while (lua_next(L, -2)) {
+ struct asset *asset = calloc(1, sizeof(*asset));
+ asset->symbol = strdup(lua_tostring(L, -2));
+ asset->amt = lua_tonumber(L, -1);
+ asset->type = STOCK;
+ assets[n++] = asset;
+
+ lua_pop(L, 1);
+ }
+
+ lua_pop(L, 1);
+ return n;
+}
+
+int load_assets_crypto(struct asset **assets, lua_State *L)
+{
+ lua_getglobal(L, "crypto");
+ lua_pushnil(L);
+
+ int n = 0;
+ while (lua_next(L, -2)) {
+ struct asset *asset = calloc(1, sizeof(*asset));
+ asset->symbol = strdup(lua_tostring(L, -2));
+ asset->amt = lua_tonumber(L, -1);
+ asset->type = CRYPTO;
+ assets[n++] = asset;
+
+ lua_pop(L, 1);
+ }
+
+ lua_pop(L, 1);
+ return n;
+}
+
+static int read_token(const char *path, char *buf, size_t bufsize)
+{
+ FILE *f = fopen(path, "r");
+ if (!f) return 0;
+
+ int ret = 0;
+ if (fgets(buf, bufsize, f)) {
+ ret = 1;
+ char *nl = strchr(buf, '\n');
+ if (nl) *nl = '\0';
+ }
+
+ fclose(f);
+ return ret;
+}
+
+static int write_token(const char *path, char *buf)
+{
+ FILE *f = fopen(path, "w");
+ if (!f) return 0;
+
+ int ret = 0;
+ if (fputs(buf, f) != EOF && fputc('\n', f) != EOF)
+ ret = 1;
+
+ fclose(f);
+ return ret;
+}
+
+int load_tda(struct asset **assets, lua_State *L, struct tda_session **sessptr)
+{
+ int ret = -1;
+ struct tda_session *sess = NULL;
+
+ size_t token_size = 2048;
+ char *token = malloc(token_size);
+ if (!token) goto out;
+
+ lua_getglobal(L, "tda");
+ lua_getfield(L, -1, "account_id");
+ const char *account_id = lua_tostring(L, -1);
+ lua_getfield(L, -2, "client_id");
+ const char *client_id = lua_tostring(L, -1);
+ lua_getfield(L, -3, "token_path");
+ const char *token_path = lua_tostring(L, -1);
+
+ if (read_token(token_path, token, token_size) &&
+ (sess = tda_init(client_id, account_id, token))) {
+ if (write_token(token_path, sess->refresh_token)) {
+ ret = tda_load_assets(sess, assets);
+ } else {
+ tda_cleanup(sess);
+ sess = NULL;
+ }
+ }
+
+ lua_pop(L, 4);
+ free(token);
+out:
+ *sessptr = sess;
+ return ret;
+}
+
+int main()
+{
+ lua_State *L = luaL_newstate();
+ luaL_openlibs(L);
+ luaL_dofile(L, "config.lua");
+ lua_pushcfunction(L, push_field_lua);
+ lua_setglobal(L, "get_asset_field");
+
+ if (curl_global_init(CURL_GLOBAL_DEFAULT) != 0)
+ return 1;
+
+ struct tda_session *tda;
+ struct asset **assets = calloc(ASSETS_MAX, sizeof(*assets));
+ int n_assets = load_tda(assets, L, &tda);
+ n_assets += load_assets_stock(assets + n_assets, L);
+ n_assets += load_assets_crypto(assets + n_assets, L);
+
+ struct quote **quotes;
+ char *symbols;
+ int n_quotes;
+
+ yahoo_get_quote_symbols(assets, n_assets, &symbols);
+ n_quotes = yahoo_get_quotes(symbols, &quotes);
+ free(symbols);
+ add_quotes_to_assets(assets, n_assets, quotes, n_quotes);
+
+ for (int i = 0; i < n_quotes; ++i)
+ quote_free(quotes[i]);
+ free(quotes);
+
+ tda_get_quote_symbols(assets, n_assets, &symbols);
+ n_quotes = tda_get_quotes(tda, symbols, &quotes);
+ free(symbols);
+ add_quotes_to_assets(assets, n_assets, quotes, n_quotes);
+
+ for (int i = 0; i < n_quotes; ++i)
+ quote_free(quotes[i]);
+ free(quotes);
+
+ qsort(assets, n_assets, sizeof(*assets), asset_cmp);
+ n_assets += add_totals(assets, n_assets);
+ output(assets, n_assets, L);
+
+ for (int i = 0; i < n_assets; ++i)
+ asset_free(assets[i]);
+ free(assets);
+
+ tda_cleanup(tda);
+ curl_global_cleanup();
+ lua_close(L);
+ return 0;
+}
diff --git a/tda.c b/tda.c
new file mode 100644
index 0000000..affe1d3
--- /dev/null
+++ b/tda.c
@@ -0,0 +1,253 @@
+#include "assets.h"
+#include "json_curl.h"
+#include "quotes.h"
+#include "tda.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <json-c/json.h>
+
+static json_object *tda_init_req(struct tda_session *sess,
+ const char *refresh_token)
+{
+ json_object *json = NULL;
+ CURL *curl = curl_easy_init();
+ if (!curl) return NULL;
+ char *esc = curl_easy_escape(curl, refresh_token, 0);
+ if (!esc) goto err_free_curl;
+
+ char buf[4096] = { 0 };
+ char fmt[] = "client_id=%s&grant_type=refresh_token&access_type=offline&"
+ "refresh_token=%s";
+ int c = snprintf(buf, sizeof(buf), fmt, sess->client_id, esc);
+ if (c < 0 || (unsigned)c >= sizeof(buf) ||
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, buf) != CURLE_OK)
+ goto err_free_esc;
+
+ json = json_curl_perform(curl,
+ "https://api.tdameritrade.com/v1/oauth2/token");
+
+err_free_esc:
+ curl_free(esc);
+err_free_curl:
+ curl_easy_cleanup(curl);
+ return json;
+}
+
+struct tda_session *tda_init(const char *client_id, const char *account_id,
+ const char *refresh_token)
+{
+ struct tda_session *sess = calloc(1, sizeof(*sess));
+ if (!sess) return NULL;
+
+ json_object *json;
+ if (!((sess->client_id = strdup(client_id)) &&
+ (sess->account_id = strdup(account_id)) &&
+ (json = tda_init_req(sess, refresh_token))))
+ goto err_free_sess;
+
+ json_object *tmp = json;
+ int err = !json_object_object_get_ex(json, "refresh_token", &tmp) ||
+ !(sess->refresh_token = strdup(json_object_get_string(tmp))) ||
+ !json_object_object_get_ex(json, "access_token", &tmp) ||
+ !(sess->access_token = strdup(json_object_get_string(tmp)));
+ json_object_put(json);
+ if (err) goto err_free_sess;
+
+ return sess;
+
+err_free_sess:
+ tda_cleanup(sess);
+ return NULL;
+}
+
+void tda_cleanup(struct tda_session *sess)
+{
+ free(sess->client_id);
+ free(sess->account_id);
+ free(sess->access_token);
+ free(sess->refresh_token);
+ free(sess);
+}
+
+static json_object *oauth_json_curl(struct tda_session *sess, const char *uri)
+{
+ CURL *curl = curl_easy_init();
+ if (!curl) return NULL;
+
+ json_object *json = NULL;
+ if (curl_easy_setopt(curl, CURLOPT_HTTPAUTH,
+ CURLAUTH_BEARER) == CURLE_OK &&
+ curl_easy_setopt(curl, CURLOPT_XOAUTH2_BEARER,
+ sess->access_token) == CURLE_OK)
+ json = json_curl_perform(curl, uri);
+
+ curl_easy_cleanup(curl);
+ return json;
+}
+
+static json_object *fetch_positions(struct tda_session *sess)
+{
+ char uri[2048];
+ int c = snprintf(uri, sizeof(uri),
+ "https://api.tdameritrade.com/v1/accounts/%s?fields=positions",
+ sess->account_id);
+ if (c < 0 || (unsigned)c >= sizeof(uri))
+ return NULL;
+
+ return oauth_json_curl(sess, uri);
+}
+
+static json_object *fetch_quotes(struct tda_session *sess, const char *symbols)
+{
+ char uri[2048];
+ int c = snprintf(uri, sizeof(uri),
+ "https://api.tdameritrade.com/v1/marketdata/quotes?symbol=%s",
+ symbols);
+ if (c < 0 || (unsigned)c >= sizeof(uri))
+ return NULL;
+
+ return oauth_json_curl(sess, uri);
+}
+
+static struct asset *gen_asset(json_object *json)
+{
+ json_object *tmp;
+
+ if (!json_object_object_get_ex(json, "instrument", &tmp) ||
+ !json_object_object_get_ex(tmp, "assetType", &tmp))
+ return NULL;
+
+ const char *type = json_object_get_string(tmp);
+ enum asset_type at;
+ if (strcmp(type, "EQUITY") == 0)
+ at = STOCK;
+ else if (strcmp(type, "OPTION") == 0)
+ at = OPTION;
+ else
+ return NULL;
+
+ struct asset *a = calloc(1, sizeof(*a));
+ if (!a) return NULL;
+ a->type = at;
+
+ if (!json_object_object_get_ex(json, "longQuantity", &tmp))
+ goto err;
+ a->amt = json_object_get_double(tmp);
+
+ if (!json_object_object_get_ex(json, "instrument", &tmp) ||
+ !json_object_object_get_ex(tmp, "symbol", &tmp) ||
+ !(a->symbol = strdup(json_object_get_string(tmp))))
+ goto err;
+
+ return a;
+
+err:
+ free(a);
+ return NULL;
+}
+
+int tda_load_assets(struct tda_session *sess, struct asset **assets)
+{
+ json_object *tmp, *json = fetch_positions(sess);
+ if (!json ||
+ !json_object_object_get_ex(json, "securitiesAccount", &tmp) ||
+ !json_object_object_get_ex(tmp, "positions", &tmp))
+ return -1;
+
+ int n = 0;
+
+ for (size_t i = 0; i < json_object_array_length(tmp); ++i) {
+ json_object *p_json = json_object_array_get_idx(tmp, i);
+ struct asset *a = gen_asset(p_json);
+ if (a) assets[n++] = a;
+ }
+
+ json_object_put(json);
+ return n;
+}
+
+int tda_get_quote_symbols(struct asset **assets, int n, char **bufptr)
+{
+ size_t bufsize = 1024;
+ char *buf = malloc(bufsize);
+ if (!buf) return -1;
+ int len = 0;
+
+ for (int i = 0; i < n; ++i) {
+ if (assets[i]->type != CRYPTO) {
+retry:;
+ char *tmp = memccpy(buf + len, assets[i]->symbol, '\0',
+ bufsize - len - 1); // -1 for ','
+ if (tmp) {
+ len = tmp - buf;
+ buf[len - 1] = ',';
+ } else {
+ char *tmpbuf = realloc(buf, (bufsize *= 2));
+ if (tmpbuf) {
+ buf = tmpbuf;
+ } else {
+ free(buf);
+ return -1;
+ }
+
+ goto retry;
+ }
+ }
+ }
+
+ buf[--len] = '\0';
+ *bufptr = buf;
+ return len;
+}
+
+static struct quote *gen_quote(const json_object *json)
+{
+ struct quote *q = calloc(1, sizeof(*q));
+ if (!q) return NULL;
+
+ json_object *tmp;
+ if (!json_object_object_get_ex(json, "symbol", &tmp) ||
+ !(q->symbol = strdup(json_object_get_string(tmp))) ||
+ !(q->currency = strdup("USD")))
+ goto err;
+
+ if (json_object_object_get_ex(json, "lastPrice", &tmp))
+ q->price = json_object_get_double(tmp);
+ if (json_object_object_get_ex(json, "multiplier", &tmp))
+ q->multiplier = json_object_get_double(tmp);
+ if (json_object_object_get_ex(json, "netPercentChangeInDouble", &tmp))
+ q->change_percent = json_object_get_double(tmp);
+
+ return q;
+
+err:
+ quote_free(q);
+ return NULL;
+}
+
+int tda_get_quotes(struct tda_session *sess, const char *symbols,
+ struct quote ***quotes_ptr)
+{
+ json_object *json = fetch_quotes(sess, symbols);
+ if (!json) return -1;
+
+ int n = 0;
+ struct lh_table *table = json_object_get_object(json);
+ struct quote **quotes = calloc(table->count, sizeof(*quotes));
+ if (!quotes) {
+ json_object_put(json);
+ return -1;
+ }
+
+ for (struct lh_entry *entry = table->head; entry; entry = entry->next) {
+ struct quote *q = gen_quote(entry->v);
+ if (q) quotes[n++] = q;
+ }
+
+ json_object_put(json);
+ *quotes_ptr = quotes;
+ return n;
+}
diff --git a/tda.h b/tda.h
new file mode 100644
index 0000000..8a0dcaf
--- /dev/null
+++ b/tda.h
@@ -0,0 +1,20 @@
+#ifndef TDA_H
+#define TDA_H
+
+#include "assets.h"
+#include "quotes.h"
+
+extern char *tda_refresh_token;
+
+struct tda_session {
+ char *client_id, *account_id, *access_token, *refresh_token;
+};
+
+struct tda_session *tda_init(const char *, const char *, const char *);
+void tda_cleanup(struct tda_session *);
+int tda_load_assets(struct tda_session *, struct asset **assets);
+
+int tda_get_quote_symbols(struct asset **assets, int n, char **bufptr);
+int tda_get_quotes(struct tda_session *, const char *, struct quote ***);
+
+#endif
diff --git a/yahoo.c b/yahoo.c
new file mode 100644
index 0000000..7b54922
--- /dev/null
+++ b/yahoo.c
@@ -0,0 +1,106 @@
+#include "assets.h"
+#include "json_curl.h"
+#include "quotes.h"
+#include "yahoo.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <json-c/json.h>
+
+int yahoo_get_quote_symbols(struct asset **assets, int n, char **bufptr)
+{
+ size_t bufsize = 1024;
+ char *buf = malloc(bufsize);
+ if (!buf) return -1;
+ int len = 0;
+
+ for (int i = 0; i < n; ++i) {
+ if (assets[i]->type == CRYPTO) {
+ int c;
+retry:
+ if (strcmp(assets[i]->symbol, "BTC") == 0) {
+ c = snprintf(buf + len, bufsize - len, "%s-USD,",
+ assets[i]->symbol);
+ } else {
+ c = snprintf(buf + len, bufsize - len, "%s-USD,%s-BTC,",
+ assets[i]->symbol, assets[i]->symbol);
+ }
+
+ if (c < 0)
+ continue;
+ if ((unsigned)c >= bufsize - len) {
+ char *tmpbuf = realloc(buf, (bufsize *= 2));
+ if (tmpbuf) {
+ buf = tmpbuf;
+ } else {
+ free(buf);
+ return -1;
+ }
+
+ goto retry;
+ }
+
+ len += c;
+ }
+ }
+
+ buf[--len] = '\0';
+ *bufptr = buf;
+ return len;
+}
+
+static json_object *fetch_quotes(const char *symbols)
+{
+ CURL *curl = curl_easy_init();
+ if (!curl) return NULL;
+
+ char base[] = "https://query1.finance.yahoo.com/v7/finance/quote?symbols=";
+ size_t len_symbols = strlen(symbols);
+ char uri[sizeof(base) + len_symbols];
+ memcpy(uri, base, sizeof(base) - 1);
+ memcpy(uri + sizeof(base) - 1, symbols, len_symbols + 1);
+
+ json_object *json = json_curl_perform(curl, uri);
+ curl_easy_cleanup(curl);
+ return json;
+}
+
+int yahoo_get_quotes(const char *symbols, struct quote ***quotes_ptr)
+{
+ json_object *tmp, *json = fetch_quotes(symbols);
+ json_object_object_get_ex(json, "quoteResponse", &tmp);
+ json_object_object_get_ex(tmp, "result", &tmp);
+
+ int n = json_object_array_length(tmp);
+ struct quote **quotes = calloc(n, sizeof(**quotes));
+ *quotes_ptr = quotes;
+
+ for (int i = 0; i < n; ++i) {
+ struct quote *q = calloc(1, sizeof(*q));
+ json_object *q_tmp, *q_json = json_object_array_get_idx(tmp, i);
+
+ json_object_object_get_ex(q_json, "quoteType", &q_tmp);
+ json_object_object_get_ex(q_json,
+ (strcmp(json_object_get_string(q_tmp), "CRYPTOCURRENCY") == 0) ?
+ "fromCurrency" : "symbol",
+ &q_tmp);
+ q->symbol = strdup(json_object_get_string(q_tmp));
+
+ json_object_object_get_ex(q_json, "currency", &q_tmp);
+ q->currency = strdup(json_object_get_string(q_tmp));
+
+ json_object_object_get_ex(q_json, "regularMarketPrice", &q_tmp);
+ q->price = json_object_get_double(q_tmp);
+
+ json_object_object_get_ex(q_json, "regularMarketChangePercent",
+ &q_tmp);
+ q->change_percent = json_object_get_double(q_tmp);
+
+ quotes[i] = q;
+ }
+
+ json_object_put(json);
+ return n;
+}
diff --git a/yahoo.h b/yahoo.h
new file mode 100644
index 0000000..91b880b
--- /dev/null
+++ b/yahoo.h
@@ -0,0 +1,10 @@
+#ifndef YAHOO_H
+#define YAHOO_H
+
+#include "assets.h"
+#include "quotes.h"
+
+int yahoo_get_quote_symbols(struct asset **assets, int n, char **bufptr);
+int yahoo_get_quotes(const char *symbols, struct quote ***quotes_ptr);
+
+#endif