From 18bde7391efb9c6bd6ea7846891ab5d16276a809 Mon Sep 17 00:00:00 2001 From: David Vazgenovich Shakaryan Date: Mon, 31 May 2021 06:11:22 -0700 Subject: initial import A bit messy and missing error handling in some places. --- COPYING | 19 +++++ Makefile | 20 +++++ assets.c | 100 ++++++++++++++++++++++++ assets.h | 36 +++++++++ config.lua | 96 +++++++++++++++++++++++ json_curl.c | 42 ++++++++++ json_curl.h | 9 +++ output.c | 131 +++++++++++++++++++++++++++++++ output.h | 12 +++ quotes.c | 23 ++++++ quotes.h | 15 ++++ stonks.c | 172 +++++++++++++++++++++++++++++++++++++++++ tda.c | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ tda.h | 20 +++++ yahoo.c | 106 +++++++++++++++++++++++++ yahoo.h | 10 +++ 16 files changed, 1064 insertions(+) create mode 100644 COPYING create mode 100644 Makefile create mode 100644 assets.c create mode 100644 assets.h create mode 100644 config.lua create mode 100644 json_curl.c create mode 100644 json_curl.h create mode 100644 output.c create mode 100644 output.h create mode 100644 quotes.c create mode 100644 quotes.h create mode 100644 stonks.c create mode 100644 tda.c create mode 100644 tda.h create mode 100644 yahoo.c create mode 100644 yahoo.h 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 +#include +#include + +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 + +#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 +#include +#include + +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 +#include + +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 +#include +#include + +#include + +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 + +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 +#include + +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 +#include +#include + +#include +#include +#include + +#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; +} 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 +#include +#include + +#include + +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 +#include +#include + +#include + +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 -- cgit v1.2.3-70-g09d2