diff options
-rw-r--r-- | .SRCINFO | 2 | ||||
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | PKGBUILD | 2 | ||||
-rw-r--r-- | README.md | 10 | ||||
-rw-r--r-- | main.c | 9 | ||||
-rw-r--r-- | portfolio.c | 340 | ||||
-rw-r--r-- | portfolio.h | 64 | ||||
-rw-r--r-- | tick.1 | 14 |
8 files changed, 240 insertions, 203 deletions
@@ -1,6 +1,6 @@ pkgbase = tick pkgdesc = Command line stock and cryptocurrency portfolio tracker. - pkgver = 1.6.4 + pkgver = 1.7.0 pkgrel = 1 url = https://github.com/aokellermann/tick arch = x86_64 @@ -1,7 +1,7 @@ CC = gcc CFLAGS = -g -Wall --std=c99 OBJECTS = main.o api.o portfolio.o -LIBS = -lcurl -ljson-c +LIBS = -lcurl -ljson-c -lm BIN = tick DESTDIR = /usr @@ -1,7 +1,7 @@ # Maintainer: Antony Kellermann <aokellermann@gmail.com> pkgname=tick -pkgver=1.6.4 +pkgver=1.7.0 pkgrel=1 pkgdesc="Command line stock and cryptocurrency portfolio tracker." arch=('x86_64') diff --git a/README.md b/README.md index d57faa476dad..31d00e80647c 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,11 @@ $ tick news [symbol] If you wish to use spaces in your input, you can either surround the phrase with quotes or replace spaces with underscores. +As of version 1.7, the portfolio file has been reconstructed and formatted +in JSON. To convert your existing portfolio, run +```bash +$ tick convert +``` Once installed, you may read the man page for more information. #### License @@ -58,7 +63,8 @@ license for more information. #### Future Ideas * Command to get info about a security * Historical support -- 7d/28d profits -* Change portfolio structure to JSON format * Different ways to sort "check all" * Look for API to replace Morningstar for MUTF/OTCMKTS data, preferably with -intraday data
\ No newline at end of file +intraday data +* Encrypt data +* Debian/RPM package
\ No newline at end of file @@ -17,9 +17,16 @@ int main(int argc, char* argv[]) { FILE* fp = fopen(portfolio_file, "a+"); if (fp == NULL) { printf("Could not open portfolio file\n"); + fclose(fp); free((void*) portfolio_file); return 0; } + if (argc == 2 && strcmp(argv[1], "convert") == 0){ + portfolio_legacy_convert(); + free((void*) portfolio_file); + fclose(fp); + return 0; + } for (int i = 0; argv[2][i] != '\0'; i++) argv[2][i] = (char) toupper(argv[2][i]); if (argc == 5) { @@ -39,7 +46,7 @@ int main(int argc, char* argv[]) { if (argc == 3 && strcmp(argv[1], "check") == 0) { if (strcmp(argv[2], "ALL") == 0) portfolio_print_all(fp); - else free(portfolio_print_stock(argv[2], fp)); + else free(portfolio_print_stock(argv[2], fp, NULL)); } else printf("Invalid arguments.\n"); } free((void*) portfolio_file); diff --git a/portfolio.c b/portfolio.c index 7c9b4b12a04c..89386ab5184a 100644 --- a/portfolio.c +++ b/portfolio.c @@ -2,208 +2,232 @@ void portfolio_file_init() { char* home = getenv("HOME"); - char* path = (char*) malloc(strlen(home) + 21); + char* path = (char*) malloc(strlen(home) + 30); memcpy(path, home, strlen(home) + 1); - memcpy(&path[strlen(home)], "/.tick_portfolio", 17); + memcpy(&path[strlen(home)], "/.tick_portfolio.json", 25); portfolio_file = path; } +char* portfolio_file_get_string(FILE* fp) { + char* portfolio_string = calloc(65536, 1); + fseek(fp, 0, SEEK_END); + size_t portfolio_size = (size_t) ftell(fp); + fseek(fp, 0, SEEK_SET); + fread(portfolio_string, portfolio_size, sizeof(char), fp); + return portfolio_string; +} + void portfolio_modify(char* ticker_name_string, double quantity_shares, double usd_spent, FILE* fp, int option) { - if (portfolio_contains(ticker_name_string, fp)) { - long position = ftell(fp); - fseek(fp, 0, SEEK_END); - size_t length = (size_t) ftell(fp); - char* beginning = (char*) calloc(length, sizeof(char)); - char* end = (char*) calloc(length, sizeof(char)); - fseek(fp, 0, SEEK_SET); - for (int i = 0; i < (int) position; i++) - beginning[i] = (char) fgetc(fp); - fseek(fp, position, SEEK_SET); - double new_quantity_shares; - double new_usd_spent; - if (option == ADD) { - new_quantity_shares = get_next_val(fp) + quantity_shares; - new_usd_spent = get_next_val(fp) + usd_spent; - } else if (option == REMOVE) { - new_quantity_shares = get_next_val(fp) - quantity_shares; - new_usd_spent = get_next_val(fp) - usd_spent; - } else { - new_quantity_shares = quantity_shares; - new_usd_spent = usd_spent; - get_next_val(fp); - get_next_val(fp); - } - char c; - for (int i = 0;; i++) { - c = (char) fgetc(fp); - if (feof(fp)) - break; - end[i] = c; + if (quantity_shares < 0 || usd_spent < 0){ + printf("You must use positive values.\n"); + return; + } + if (option != SET && quantity_shares == 0 && usd_spent == 0){ + printf("You cannot add or remove values of 0.\n"); + return; + } + char* portfolio_string = portfolio_file_get_string(fp); + Json* jobj = NULL; + if (strcmp(portfolio_string, "") == 0) { //new file + jobj = json_object_new_array(); + } else jobj = json_tokener_parse(portfolio_string); //existing file + if (jobj == NULL) + printf("NULL JOBJ\n"); + int index = portfolio_symbol_index(ticker_name_string, jobj); + if (index == -1) { //if not already in portfolio + if (option == REMOVE) { + printf("You don't have any %s to remove.\n", ticker_name_string); + free(portfolio_string); + json_object_put(jobj); + return; } - if (strlen(end) > 0 && end[strlen(end) - 1] == '\n') - end[strlen(end) - 1] = '\0'; - remove(portfolio_file); - fp = fopen(portfolio_file, "w"); - if (new_quantity_shares >= 0 && new_usd_spent >= 0) { - fprintf(fp, "%s%lf %lf", beginning, new_quantity_shares, new_usd_spent); - if (strlen(end) != 0) - fprintf(fp, "\n%s", end); - if (option == ADD) - printf("Added "); - else if (option == REMOVE) - printf("Removed "); - else { - printf("Set %s to %lf\n", ticker_name_string, quantity_shares); - return; - } - printf("%lf %s.\n", quantity_shares, ticker_name_string); - } else printf("You cannot remove more %s than you have!\n", ticker_name_string); - free(beginning); - free(end); - fclose(fp); - } else { - if (strlen(ticker_name_string) > 16 || - (api_get_current_price(ticker_name_string) == NULL && strcmp("USD$", ticker_name_string) != 0)) { + if (api_get_current_price(ticker_name_string) == NULL){ printf("Invalid symbol.\n"); + json_object_put(jobj); + free(portfolio_string); return; } - if (option == REMOVE) { - printf("You don't have any %s to remove!\n", ticker_name_string); - return; + Json* new_object = json_object_new_object(); //creates new array index and adds values to it + json_object_array_add(jobj, new_object); + json_object_object_add(new_object, "Symbol", json_object_new_string(ticker_name_string)); + json_object_object_add(new_object, "Shares", json_object_new_double(quantity_shares)); + json_object_object_add(new_object, "USD_Spent", json_object_new_double(usd_spent)); + printf("Added %lf %s bought for %lf to portfolio.\n", quantity_shares, ticker_name_string, usd_spent); + } else { + Json* current_index = json_object_array_get_idx(jobj, (size_t) index); + double current_shares = json_object_get_double(json_object_object_get(current_index, "Shares")); + double current_spent = json_object_get_double(json_object_object_get(current_index, "USD_Spent")); + json_object_object_del(current_index, "Shares"); + json_object_object_del(current_index, "USD_Spent"); + if (option == SET) { + current_shares = quantity_shares; + current_spent = usd_spent; + printf("Set amount of %s in portfolio to %lf bought for %lf.\n", ticker_name_string, quantity_shares, usd_spent); + } else if (option == ADD) { + current_shares += quantity_shares; + current_spent += usd_spent; + printf("Added %lf %s bought for %lf to portfolio.\n", quantity_shares, ticker_name_string, usd_spent); + } else { + current_shares -= quantity_shares; + current_spent -= usd_spent; + if (current_shares < 0 || current_spent < 0) { + printf("You do not have that many %s to remove.\n", ticker_name_string); + json_object_put(jobj); + free(portfolio_string); + return; + } + printf("Removed %lf %s bought for %lf to portfolio.\n", quantity_shares, ticker_name_string, usd_spent); } - fseek(fp, 0, SEEK_END); - if (ftell(fp) != 0) - fprintf(fp, "\n"); - fprintf(fp, "%s %lf %lf", ticker_name_string, quantity_shares, usd_spent); - if (option == ADD) - printf("Added "); + if (current_shares == 0 && usd_spent == 0) //deletes index from portfolio if values are 0 + json_object_array_del_idx(jobj, (size_t) index, 1); else { - printf("Set %s to %lf\n", ticker_name_string, quantity_shares); - return; + json_object_object_add(current_index, "Shares", json_object_new_double(round(current_shares * 100) / 100)); //adds computed values to index + json_object_object_add(current_index, "USD_Spent", json_object_new_double(round(current_spent * 100) / 100)); } - printf("%lf %s.\n", quantity_shares, ticker_name_string); - } -} - -double portfolio_get_quantity_shares(char* ticker_name_string, FILE* fp) { - if (portfolio_contains(ticker_name_string, fp)) - return get_next_val(fp); - return 0; -} - -double portfolio_get_usd_spent(char* ticker_name_string, FILE* fp) { - if (portfolio_contains(ticker_name_string, fp)) { - get_next_val(fp); - return get_next_val(fp); } - return 0; + fp = fopen(portfolio_file, "w"); + fprintf(fp, "%s", json_object_to_json_string(jobj)); + fclose(fp); + json_object_put(jobj); + free(portfolio_string); } void portfolio_print_all(FILE* fp) { printf(" AMOUNT SYMBOL VALUE SPENT PROFIT (%%) 24H (%%)\n"); - char* str = (char*) calloc(64, sizeof(char)); - char* ticker_name_string = (char*) calloc(32, sizeof(char)); - double* data; - double total_owned = 0, total_spent = 0, total_gain_1d = 0; - int i; - char c; - while (fgets(str, 63, fp) != NULL) { - i = 0; - while ((c = str[i]) != ' ') { - ticker_name_string[i] = c; - i++; - } - data = portfolio_print_stock(ticker_name_string, fp); + char* portfolio_string = portfolio_file_get_string(fp); + double* data, total_owned = 0, total_spent = 0, total_gain_1d = 0; + if (strcmp(portfolio_string, "") == 0) + return; + Json* jobj = json_tokener_parse(portfolio_string); + int num_symbols = (int) json_object_array_length(jobj); + for (int i = 0; i < num_symbols; i++) { + data = portfolio_print_stock(NULL, NULL, json_object_array_get_idx(jobj, (size_t) i)); if (data != NULL) { total_owned += data[0]; total_spent += data[1]; total_gain_1d += data[2]; } - memset(str, '\0', 64); - memset(ticker_name_string, '\0', 32); free(data); } printf("\n TOTALS %8.2lf %8.2lf %8.2lf (%6.2lf%%) %8.2lf (%6.2lf%%)\n", total_owned, total_spent, total_owned - total_spent, (100 * (total_owned - total_spent)) / total_spent, total_gain_1d, 100 * total_gain_1d / total_spent); - free(str); - free(ticker_name_string); + json_object_put(jobj); + free(portfolio_string); } -double* portfolio_print_stock(char* ticker_name_string, FILE* fp) { +double* portfolio_print_stock(char* ticker_name_string, FILE* fp, Json* current_index) { /** * Values in USD * a[0] -- current balance * a[1] -- amount spent * a[2] -- 1d gain */ - double* a = malloc(sizeof(double) * 3); - a[0] = portfolio_get_quantity_shares(ticker_name_string, fp); - a[1] = portfolio_get_usd_spent(ticker_name_string, fp); - a[2] = 0; - if (a[0] == 0 && a[1] == 0) { - free(a); - a = NULL; - } else { - double* ticker_data; - double ticker_current_price_usd = 1, ticker_1d_price_usd, ticker_1d_percent_change = 0; - if (strcmp(ticker_name_string, "USD$") != 0) { - ticker_data = api_get_current_price(ticker_name_string); - ticker_current_price_usd = ticker_data[0]; - ticker_1d_price_usd = ticker_data[1]; - free(ticker_data); - a[2] = ((ticker_current_price_usd - ticker_1d_price_usd) * a[0]); - ticker_1d_percent_change = 100 * (ticker_current_price_usd / ticker_1d_price_usd - 1); - a[0] *= ticker_current_price_usd; + double* data = malloc(sizeof(double) * 3); + char* portfolio_string = NULL; + if (fp == NULL) { //if being called from portfolio_print_all + ticker_name_string = (char*) strip_char( + (char*) json_object_get_string(json_object_object_get(current_index, "Symbol")), '\"'); + data[0] = json_object_get_double(json_object_object_get(current_index, "Shares")); + data[1] = json_object_get_double(json_object_object_get(current_index, "USD_Spent")); + } else { //if being called directly from main + portfolio_string = portfolio_file_get_string(fp); + Json* jobj = json_tokener_parse(portfolio_string); + int index = portfolio_symbol_index(ticker_name_string, jobj); + if (index == -1) { + printf("You do not have %s in your portfolio.\n", ticker_name_string); + return data; } - printf("%8.2lf %6s %8.2lf %8.2lf %8.2lf (%6.2lf%%) %8.2lf (%6.2lf%%)\n", - a[0] / ticker_current_price_usd, ticker_name_string, a[0], a[1], a[0] - a[1], - (100 * (a[0] - a[1])) / a[1], - a[2], ticker_1d_percent_change); + current_index = json_object_array_get_idx(jobj, (size_t) index); + } + data[0] = json_object_get_double(json_object_object_get(current_index, "Shares")); + data[1] = json_object_get_double(json_object_object_get(current_index, "USD_Spent")); + data[2] = 0; + double* ticker_data, ticker_current_price_usd = 1, ticker_1d_price_usd, ticker_1d_percent_change = 0; + if (strcmp(ticker_name_string, "USD$") != 0) { + ticker_data = api_get_current_price(ticker_name_string); + ticker_current_price_usd = ticker_data[0]; + ticker_1d_price_usd = ticker_data[1]; + free(ticker_data); + data[2] = ((ticker_current_price_usd - ticker_1d_price_usd) * data[0]); + ticker_1d_percent_change = 100 * (ticker_current_price_usd / ticker_1d_price_usd - 1); + data[0] *= ticker_current_price_usd; } - return a; + printf("%8.2lf %6s %8.2lf %8.2lf %8.2lf (%6.2lf%%) %8.2lf (%6.2lf%%)\n", + data[0] / ticker_current_price_usd, ticker_name_string, data[0], data[1], data[0] - data[1], + (100 * (data[0] - data[1])) / data[1], + data[2], ticker_1d_percent_change); + if (fp == NULL) { + free(ticker_name_string); + free(portfolio_string); + } + return data; } -int portfolio_contains(char* ticker_name_string, FILE* fp) { - fseek(fp, 0, SEEK_SET); - char* temp = (char*) calloc(16, sizeof(char)), c; - int i = 0; - while (1) { - c = (char) fgetc(fp); - if (feof(fp)) { - fseek(fp, 0, 0); - free(temp); - return 0; - } - if (c == ' ') { - if (strcmp(ticker_name_string, temp) == 0) { - free(temp); - return 1; - } else { - while (fgetc(fp) != '\n') { - if (feof(fp)) - break; - } - memset(temp, '\0', 16); - i = 0; - } - } else { - temp[i] = c; - i++; +int portfolio_symbol_index(char* ticker_name_string, Json* jarray) { + int num_symbols = (int) json_object_array_length(jarray); + char* str; + for (int i = 0; i < num_symbols; i++) { + str = (char*) json_object_to_json_string( + json_object_object_get(json_object_array_get_idx(jarray, (size_t) i), "Symbol")); + str = (char*) strip_char(str, '\"'); + if (strcmp(str, ticker_name_string) == 0) { + free(str); + return i; } + free(str); } + return -1; } -double get_next_val(FILE* fp) { - char* temp = (char*) calloc(32, sizeof(char)), c; - int i = 0; - while ((c = (char) fgetc(fp)) != ' ' && c != '\n') { - if (feof(fp)) +char* portfolio_legacy_get_next_val(FILE* fp) { + char* val = calloc(16, 1); + char c; + for (int i = 0; i < 16; i++) { + c = (char) fgetc(fp); + if (c == ' ' || c == '\n'|| feof(fp)) break; - temp[i] = c; - i++; + val[i] = c; } - double val = strtod(temp, NULL); - free(temp); return val; +} + +void portfolio_legacy_convert() { + printf("Warning: this will overwrite your JSON formatted portfolio, which is stored at: \"$HOME/.tick_portfolio\". Continue? y/n\n"); + char c = 0; + while (c != 'y' && c != 'n') + scanf("%c", &c); + if (c == 'n'){ + printf("Aborted.\n"); + return; + } + FILE* fp = fopen(portfolio_file, "a+"); + char* pf = portfolio_file_get_string(fp); + if (strcmp(pf, "") != 0) + remove(portfolio_file); + free(pf); + char* legacy_path = malloc(64); + strcpy(legacy_path, portfolio_file); + legacy_path[strlen(legacy_path) - 5] = '\0'; + FILE* fp_legacy = fopen(legacy_path, "r"); + free(legacy_path); + if (!fp) { + printf("Could not open legacy portfolio.\n"); + return; + } + char* symbol, * amount, * spent; + while (1) { + if (feof(fp_legacy)) + break; + symbol = portfolio_legacy_get_next_val(fp_legacy); + amount = portfolio_legacy_get_next_val(fp_legacy); + spent = portfolio_legacy_get_next_val(fp_legacy); + portfolio_modify(symbol, strtod(amount, NULL), strtod(spent, NULL), fp, ADD); + free(symbol); + free(amount); + free(spent); + } + printf("Successfully converted portfolio!\n"); + fclose(fp_legacy); + fclose(fp); }
\ No newline at end of file diff --git a/portfolio.h b/portfolio.h index 0f140e52f538..a59584ad12b8 100644 --- a/portfolio.h +++ b/portfolio.h @@ -4,6 +4,7 @@ #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <math.h> #include "api.h" #define REMOVE 0 @@ -12,11 +13,15 @@ const char* portfolio_file; +/** + * Sets portfolio_file to $HOME/.tick_portfolio.json + */ void portfolio_file_init(); +char* portfolio_file_get_string(FILE* fp); + /** - * Adds quantity_shares of given currency at - * given price to portfolio + * Adds quantity_shares of given symbol at given price to portfolio * @param id name of currency * @param quantity_shares quantity_shares of currency * @param purchase_price quantity_shares of money spent @@ -25,25 +30,7 @@ void portfolio_file_init(); void portfolio_modify(char* ticker_name_string, double quantity_shares, double usd_spent, FILE* fp, int option); /** - * Taken from the portfolio file, returns a double - * of the quantity_shares of crypto - * @param id the crypto - * @param fp portfolio file - * @return the quantity_shares - */ -double portfolio_get_quantity_shares(char* ticker_name_string, FILE* fp); - -/** - * Taken from the portfolio file, returns a double - * of the quantity_shares of money spent of a crypto - * @param id the crypto - * @param fp portfolio file - * @return the quantity_shares spent - */ -double portfolio_get_usd_spent(char* ticker_name_string, FILE* fp); - -/** - * Prints current holdings of all currencies + * Prints current holdings of all symbols * @param api_data STRING to hold data in * @param crypto_data JSON to hold STRING data in * @param fp portfolio file @@ -51,29 +38,32 @@ double portfolio_get_usd_spent(char* ticker_name_string, FILE* fp); void portfolio_print_all(FILE* fp); /** - * Prints current holdings of a currency - * @param id name of currency - * @param fp portfolio file + * Prints current holdings of a symbol + * @param id name of currency, NULL if printing all + * @param fp portfolio file, NULL if printing all + * @param current_index portfolio file index, NULL if printing one */ -double* portfolio_print_stock(char* ticker_name_string, FILE* fp); +double* portfolio_print_stock(char* ticker_name_string, FILE* fp, Json* current_index); /** - * Sets stream pointer to first digit - * of currency quantity_shares if exists, otherwise - * sets stream pointer to beginning of stream - * @param id name of currency - * @param fp portfolio file - * @return 1 if contains, 0 if not + * Returns the index in the Json array of the given symbol + * @param ticker_name_string the symbol + * @param jarray the array + * @return -1 if not found, the index otherwise */ -int portfolio_contains(char* ticker_name_string, FILE* fp); +int portfolio_symbol_index(char* ticker_name_string, Json* jarray); /** - * Given a file stream pointing to - * the first digit of a number, - * return the number as a double + * Returns a string from the current stream pointer to the next space, newline, or EOF * @param fp the file - * @return the value + * @return the string + */ +char* portfolio_legacy_get_next_val(FILE* fp); + +/** + * Adds contents of legacy portfolio to JSON formatted portfolio + * @param fp JSON formatted portfolio */ -double get_next_val(FILE* fp); +void portfolio_legacy_convert(); #endif
\ No newline at end of file @@ -1,4 +1,4 @@ -.TH TICK "1" "January 2018" "Tick 1.6.4" "User Commands" +.TH TICK "1" "January 2018" "Tick 1.7.0" "User Commands" .SH NAME Tick - Command line stock and cryptocurrency portfolio tracker. @@ -28,6 +28,11 @@ are not reinvested into a specific security, you should update your portfolio wi Prints information about your current portfolio holdings. Either a symbol or the keyword 'all' can be used. The keyword "all" will print information about all your current holdings, as well as a grand total. +.TP +[convert] +If it exists, removes $HOME/.tick_portfolio.json and adds your current holding from the legacy portfolio, located at +$HOME/.tick_portfolio. + .SS News: @@ -39,10 +44,15 @@ your phrase in double quotes. Technically, the input may be something completely in the future. .SH FILES -.I ~/.tick_portfolio +.I ~/.tick_portfolio.json .RS Portfolio file. +.RE +.I ~/.tick_portfolio +.RS +Portfolio file (legacy). + .SH GITHUB Please report any bugs using the GitHub issue tracker: https://github.com/aokellermann/tick/issues |