diff options
| -rw-r--r-- | COPYING | 19 | ||||
| -rw-r--r-- | Makefile | 20 | ||||
| -rw-r--r-- | assets.c | 100 | ||||
| -rw-r--r-- | assets.h | 36 | ||||
| -rw-r--r-- | config.lua | 96 | ||||
| -rw-r--r-- | json_curl.c | 42 | ||||
| -rw-r--r-- | json_curl.h | 9 | ||||
| -rw-r--r-- | output.c | 131 | ||||
| -rw-r--r-- | output.h | 12 | ||||
| -rw-r--r-- | quotes.c | 23 | ||||
| -rw-r--r-- | quotes.h | 15 | ||||
| -rw-r--r-- | stonks.c | 172 | ||||
| -rw-r--r-- | tda.c | 253 | ||||
| -rw-r--r-- | tda.h | 20 | ||||
| -rw-r--r-- | yahoo.c | 106 | ||||
| -rw-r--r-- | yahoo.h | 10 | 
16 files changed, 1064 insertions, 0 deletions
| @@ -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, "es); +	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, "es); +	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; +} @@ -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; +} @@ -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 @@ -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; +} @@ -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 | 
