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 |