diff options
-rw-r--r-- | README | 228 | ||||
-rw-r--r-- | ledger.cc | 17 | ||||
-rw-r--r-- | ledger.h | 5 | ||||
-rw-r--r-- | reports.cc | 199 |
4 files changed, 274 insertions, 175 deletions
@@ -128,7 +128,7 @@ $ ledger -f ledger.dat register checking $ ledger -f ledger.dat register bell </example> -* Building the program +** Building the program Ledger is written in ANSI C++, and should compile on any platform. It depends only on the GNU multiprecision integer library (libgmp), and @@ -364,6 +364,63 @@ Euro=MD 0.75 This is a roundabout way of reporting AAPL shares in their Deutsch Mark equivalent. +*** Commodity price histories + +Whenever a commodity is purchased using a different commodity (such as +a share of common stock using dollars), it establishes a price for +that commodity on that day. It is also possible, by recording price +details in a ledger file, to specify other prices for commodities at +any given time. Such price entries might look like those below: + +<example> +P 2004/06/21 02:17:58 TWCUX $27.76 +P 2004/06/21 02:17:59 AGTHX $25.41 +P 2004/06/21 02:18:00 OPTFX $39.31 +P 2004/06/21 02:18:01 FEQTX $22.49 +P 2004/06/21 02:18:02 AAPL $32.91 +</example> + +By default, ledger will not consider commodity prices when generating +its various reports. It will always report balances in terms of the +commodity total, rather than the current value of those commodities. +To enable pricing reports, three options are possible: + +**-B** :: + Report commodities in terms of their "basis cost", or what they cost + at the time of purchase. Totals in the register and balance report + reflect the total amount spent. + +**-L MINS** :: + When using the =-P= or =-Q= flags, the Internet is consulted only + if current pricing data is older than MINS minutes. + +**-P** :: + Report commodities in terms of their market price. The Internet is + consulted for current prices, by calling an external script named + =getquote= (a sample Perl script is provided, but the interface is + kept simple so replacements may be made). Register reports always + give the total market value for the date of the entry -- which means + they may vary greatly from the sum of the values of the individual + entries. + +**-Q FILE** :: + Works like the =-P= flag, except it reads and saves downloaded price + information in =FILE=, accumulating a history of prices for each + commodity. This is the same as using =-P= with the environment + variable =PRICE_HIST= set to =FILE=. + +**-T** :: + Report only commodity totals, not the market value or basis cost. + +**-V** :: + Report the market value for commodities, but without consulting the + Internet for current prices. This uses only the pricing data saved + in the ledger file, or in the history file referenced by the + environment variable =PRICE_HIST=. + +Note that the =-B=, =-T=, =-V=, and =-P= and =-Q= flags are all +mutually exclusive. Whichever option appears last is used. + ** Accounts and Inventories Since Ledger's accounts and commodity system is so flexible, you can @@ -490,7 +547,8 @@ output all the earlier entries to a file called =ledger-old.dat=. 2002 is mentioned in the following command): <example> -$ ledger -f ledger.dat -b 2000/1/1 -e 2002/1/1 print > ledger-old.dat +$ ledger -f ledger.dat -b 2000/1/1 -e 2002/1/1 print \ + > ledger-old.dat </example> To delete older data from the current ledger file, use "print" again, @@ -830,12 +888,9 @@ and how it should be parsed. The possibilities are: NUMBER :: A line starting with a number denotes a regular ledger entry. It may be followed by any number of lines that beginning whitespace, to - denote account transactions. The format of an entry is: + denote account transactions. The format of the header line is: <example> DATE [*] [(CODE)] DESC - ACCOUNT AMOUNT - ACCOUNT AMOUNT - ... </example> + :: @@ -843,26 +898,17 @@ DATE [*] [(CODE)] DESC will always be considered, as if it had been specified by the user at the end of their command-line. -- :: +**-** :: If a line begins with minus, it denotes an exclusion regexp that will always be considered, as if it had been specified by the user at the end of their command-line. -<literal>=</literal> :: +**<verbatim>=</verbatim>** :: If a line begins with equals, it denotes an automated transaction. The next item on the line must be a regular expression. Any number of such lines may appear, with no intervening whitespace. Following this block of lines can be a list of account transactions - preceded by whitespace. The format is: -<example> -= REGEXP -= REGEXP -= REGEXP -... - ACCOUNT AMOUNT - ACCOUNT AMOUNT - ... -</example> + preceded by whitespace. !WORD :: A line beginning with an exclamation mark denotes a command @@ -964,33 +1010,6 @@ Equity transactions are used to establish the starting value of an account. You might think of equity as the "ether" from which initial balances appear. -The "equity" command makes it easy to archiving past years, and then -remove them without changing any current balances. For example, if -it's now 2004 and we want to archive all of 2003's transactions to -another file, write: - -<example> -export LEDGER=ledger.dat -ledger -e 2004/1/1 print > ledger-2003.dat -ledger -e 2004/1/1 equity > /tmp/balances -ledger -b 2004/1/1 print > /tmp/current -cat /tmp/balances /tmp/current > ledger.dat -rm /tmp/balances /tmp/current -</example> - -After these commands, **ledger-2003.dat** will contain all the -transactions up to year 2004, with **ledger.dat** containing only those -since 2004. However, the balances reported from **ledger.dat** will still -be the same. - -Sometimes you will not want to carry forward certain balances, such as -those for Expense and Income. To do this, change the second command -above to: - -<example> -ledger -e 2004/1/1 equity -^Income -^Expenses > /tmp/balances -</example> - *** price This commands displays the last known current price for a given @@ -1046,93 +1065,87 @@ launches =vi= to let you confirm that the entry looks appropriate. ** Option summary --B :: +**-B** :: When printing accounts containing commodities, display the base price for the commodity, rather than the quantity of that commodity (the default) or its current price (if =-P= or =-Q= is used). This option causes only the price at time(s) of purchase to be considered, not the current or historical price afterwards. --b DATE :: +**-b DATE** :: Only consider entries occuring on or after the given date. --e DATE :: - Only consider entries occuring before the given date. The date is - not inclusive, so any entries occurring on that date will not be - used. - --c :: - Only consider entries occurring on or before the current date. - --C :: +**-C** :: Only consider entries whose cleared flag has been set. The default is to consider both. --d DATE :: +**-c** :: + Only consider entries occurring on or before the current date. + +**-d DATE** :: Only consider entries fitting the given date mask. DATE in this case may be the name of a month, or a year, or a year and month, such as "2004/05". It's a shorthand for having to specify -b and -e together. --E :: +**-E** :: Also show empty accounts in the balance totals report. --f FILE[=ACCOUNT] :: - Read ledger entries from FILE. This takes precedence over the - environment variable LEDGER. If "=ACCOUNT" is appended to the - filename, then all of the entries are seen as if the transactions - accounts were prefixed by "ACCOUNT:". There may be multiple - occurrences of the -f option. +**-e DATE** :: + Only consider entries occuring before the given date. The date is + not inclusive, so any entries occurring on that date will not be + used. --F :: +**-F** :: Print full account names in all cases, such as "Assets:Checking" instead of just "Checking". Only used current by the "balance" command. --h :: +**-f FILE[<verbatim>=</verbatim>ACCOUNT]** :: + Read ledger entries from FILE. This takes precedence over the + environment variable LEDGER. If "<verbatim>=</verbatim>ACCOUNT" is + appended to the filename, then all of the entries are seen as if the + transactions accounts were prefixed by "ACCOUNT:". There may be + multiple occurrences of the =-f= option. + +**-G** :: + Modifies the output generated by -M to be friendly to programs like + Gnuplot. It strips away the commodity label, and outputs only two + columns: the date and the amount. + +**-h** :: Print out quick help on the various options and commands. --i FILE :: +**-i FILE** :: Read in the list of patterns to include/exclude from FILE. Ordinarily, these are specified as arguments after the command. --L MINS :: +**-L MINS** :: Specifies the number of minutes old that pricing data can be, before the =-Q= and =-P= options will download a new quote from the Internet. =-P= only downloads the information, while =-Q= maintains the information in a history file. The default value for this option is one day, or 1440 minutes. --M :: +**-l AMT** :: + Limit balance reports to those which are greater than AMT. + +**-M** :: When used with the "register" command, causes only monthly subtotals to appear. This can be useful for looking at spending patterns. TODO: Accept an argument which specifies the period to use. --G :: - Modifies the output generated by -M to be friendly to programs like - Gnuplot. It strips away the commodity label, and outputs only two - columns: the date and the amount. - --n :: - Do not show subtotals in the balance report, or split transactions - in the register report. - --N REGEXP :: +**-N REGEXP** :: If an account matches REGEXP, only display it in the balance report if its total is negative. Useful to avoid seeing credit in accounts where one cannot spend that credit, and it will soon become negative anyway (such as credit cards). --p ARG :: - If a string, such as "COMM=$1.20", the commodity COMM will be - reported only in terms of the conversion factor, which supersedes - all other pricing histories for that commodity. This can be used to - perform arbitrary value substitutions. For example, to report the - value of your dollars in terms of the ounces of gold they would buy, - use: -p "$=0.00280112 AU" (or whatever the current exchange rate - is). +**-n** :: + Do not show subtotals in the balance report, or split transactions + in the register report. --P :: +**-P** :: Download current prices for all commodities by calling the script "getquote". There is a "getquote" script included with ledger, although any similar program could be used. It must take a single @@ -1141,7 +1154,16 @@ launches =vi= to let you confirm that the entry looks appropriate. commodity has no price, nothing should be output and the exit code should be set to a non-zero value. --Q FILE :: +**-p ARG** :: + If a string, such as "COMM=$1.20", the commodity COMM will be + reported only in terms of the conversion factor, which supersedes + all other pricing histories for that commodity. This can be used to + perform arbitrary value substitutions. For example, to report the + value of your dollars in terms of the ounces of gold they would buy, + use: -p "$=0.00280112 AU" (or whatever the current exchange rate + is). + +**-Q FILE** :: This option, like =-P=, downloads commodities prices from the Internet as needed, by calling the script "getquote" (see above). However, this option takes a string argument: the file to write the @@ -1154,39 +1176,45 @@ launches =vi= to let you confirm that the entry looks appropriate. command-line. Also, it is recommended that the =-Q= option always appear after all uses of =-f=. --R :: +**-R** :: Ignore all virtual transactions, and report only the real balance for each account. --s :: - If an account has children, show them in the balance report. - --S :: +**-S** :: Sort the ledger after reading it. This may affect "register" and "print" output. --T :: +**-s** :: + If an account has children, show them in the balance report. + +**-T** :: Show only commodities totals, do not convert to the basis cost or the current market value. This disables the effect of =-B=, =-P= and =-Q=. --U :: +**-U** :: Show only uncleared transactions. The default is to consider both. --v :: +**-V** :: + Report the market value for commodities, but without consulting the + Internet for current prices. This uses only the pricing data saved + in the ledger file, or in the history file referenced by the + environment variable =PRICE_HIST=. + +**-v** :: Display the version of ledger being used. ** Environment variables -LEDGER :: +=LEDGER= :: A colon-separated list of files to be parsed whenever ledger is run. Easier than typing =-f= all the time. -<verbatim>PRICE_HIST</verbatim> :: +=PRICE_HIST= :: The ledger file used to hold pricing data. =~/.pricedb= would be a good choice. -<verbatim>PRICE_EXP</verbatim> :: +=PRICE_EXP= :: The number of minutes before pricing data becomes out-of-date. The default is one day. Use =-L= to temporarily decrease or increase the value. @@ -448,6 +448,23 @@ void totals::print(std::ostream& out, int width) const } } +void totals::print_street(std::ostream& out, int width, std::time_t * when, + bool use_history, bool download) const +{ + totals street_balance; + + for (const_iterator i = amounts.begin(); i != amounts.end(); i++) { + if ((*i).second->is_zero()) + continue; + + amount * street = (*i).second->street(when, use_history, download); + street_balance.credit(street); + delete street; + } + + street_balance.print(out, width); +} + account::~account() { for (accounts_map_iterator i = children.begin(); @@ -237,6 +237,10 @@ class totals bool is_negative() const; void print(std::ostream& out, int width) const; + void print_street(std::ostream& out, int width, + std::time_t * when = NULL, + bool use_history = false, + bool download = false) const; }; @@ -347,7 +351,6 @@ extern void read_regexps(const std::string& path, regexps_list& regexps); extern bool matches(const regexps_list& regexps, const std::string& str, bool * by_exclusion = NULL); -extern void read_prices(const std::string& path); extern void parse_price_setting(const std::string& setting); } // namespace ledger @@ -9,7 +9,6 @@ namespace ledger { static bool cleared_only = false; static bool uncleared_only = false; -static bool cost_basis = false; static bool show_virtual = true; static bool show_children = false; static bool show_sorted = false; @@ -19,7 +18,10 @@ static bool full_names = false; static bool print_monthly = false; static bool gnuplot_safe = false; -static bool get_quotes = false; +static bool cost_basis = false; +static bool use_history = false; +static bool read_prices = false; +static bool get_quotes = false; long pricing_leeway = 24 * 3600; std::string price_db; @@ -71,6 +73,58 @@ static bool matches_date_range(entry * ent) return true; } +static amount * resolve_amount(amount * amt, + std::time_t * when = NULL, + totals * balance = NULL, + bool add_base_value = false, + bool free_memory = false) +{ + amount * value; + bool alloced = true; + + if (! use_history) { + value = amt; + alloced = false; + } + else if (cost_basis) { + value = amt->value(); + } + else { + value = amt->street(when ? when : (have_ending ? &end_date : NULL), + use_history, get_quotes); + } + + if (balance) { + if (add_base_value) + balance->credit(cost_basis ? value : amt); + else + balance->credit(value); + } + + if (free_memory && alloced) { + delete value; + value = NULL; + } + else if (! free_memory && ! alloced) { + value = value->copy(); + } + + return value; +} + +static inline void print_resolved_balance(std::ostream& out, + std::time_t * when, + totals& balance, + bool added_base_value = false) +{ + if (! added_base_value || ! use_history || cost_basis) + balance.print(out, 12); + else + balance.print_street(out, 12, + when ? when : (have_ending ? &end_date : NULL), + use_history, get_quotes); +} + ////////////////////////////////////////////////////////////////////// // // Balance reporting code @@ -274,16 +328,7 @@ void report_balances(std::ostream& out, regexps_list& regexps) } if (acct->checked == 1) { - amount * street = (*x)->cost->street(have_ending ? &end_date : NULL, - cost_basis || get_quotes, - get_quotes); - if (cost_basis && - street->commdty() == (*x)->cost->commdty() && - (*x)->cost->has_price()) { - street = (*x)->cost->value(); - } - acct->balance.credit(street); - delete street; + resolve_amount((*x)->cost, NULL, &acct->balance, false, true); } else if (show_subtotals) { if (! regexps.empty() && ! match) { @@ -364,9 +409,7 @@ void print_register_transaction(std::ostream& out, entry *ent, // Always display the street value, if prices have been // specified - amount * street = xact->cost->street(&ent->date, cost_basis || get_quotes, - get_quotes); - balance.credit(street); + amount * street = resolve_amount(xact->cost, &ent->date, &balance, true); // If there are two transactions, use the one which does not // refer to this account. If there are more than two, print @@ -395,7 +438,7 @@ void print_register_transaction(std::ostream& out, entry *ent, out << std::right << street->as_str(true); delete street; - balance.print(out, 12); + print_resolved_balance(out, &ent->date, balance, true); out << std::endl; @@ -412,17 +455,16 @@ void print_register_transaction(std::ostream& out, entry *ent, out.width(22); out << std::left << truncated((*y)->acct_as_str(), 22) << " "; - out.width(12); - street = (*y)->cost->street(&ent->date, cost_basis || get_quotes, - get_quotes); + + street = resolve_amount((*y)->cost, &ent->date); out << std::right << street->as_str(true) << std::endl; delete street; } } void print_register_period(std::ostream& out, std::time_t date, - account *acct, amount& sum, totals& balance) + account * acct, amount& sum, totals& balance) { char buf[32]; std::strftime(buf, 31, "%Y/%m/%d ", std::localtime(&date)); @@ -448,7 +490,7 @@ void print_register_period(std::ostream& out, std::time_t date, out << std::right << sum.as_str(); if (! gnuplot_safe) - balance.print(out, 12); + print_resolved_balance(out, &date, balance, true); out << std::endl; } @@ -496,11 +538,8 @@ void print_register(std::ostream& out, const std::string& acct_name, if (period == PERIOD_NONE) { print_register_transaction(out, *i, *x, balance); } else { - amount * street = (*x)->cost->street(&(*i)->date, - cost_basis || get_quotes, - get_quotes); - balance.credit(street); - + amount * street = resolve_amount((*x)->cost, &(*i)->date, + &balance, true); if (period_sum) { period_sum->credit(street); delete street; @@ -550,16 +589,12 @@ static void equity_entry(account * acct, regexps_list& regexps, transaction * xact = new transaction(); xact->acct = const_cast<account *>(acct); - xact->cost = (*i).second->street(have_ending ? &end_date : NULL, - cost_basis || get_quotes, - get_quotes); + xact->cost = (*i).second->copy(); opening.xacts.push_back(xact); xact = new transaction(); xact->acct = main_ledger->find_account("Equity:Opening Balances"); - xact->cost = (*i).second->street(have_ending ? &end_date : NULL, - cost_basis || get_quotes, - get_quotes); + xact->cost = (*i).second->copy(); xact->cost->negate(); opening.xacts.push_back(xact); } @@ -605,8 +640,7 @@ void price_report(std::ostream& out, regexps_list& regexps) i++) if (regexps.empty() || matches(regexps, (*i).first)) { amount * price = (*i).second->price(have_ending ? &end_date : NULL, - cost_basis || get_quotes, - get_quotes); + use_history, get_quotes); if (price && ! price->is_zero()) { out.width(20); out << std::right << price->as_str() << " " << (*i).first @@ -787,28 +821,33 @@ static void show_help(std::ostream& out) << "usage: ledger [options] COMMAND [options] [REGEXPS]" << std::endl << std::endl << "ledger options:" << std::endl + << " -B report commodities in terms of their basis cost" << std::endl << " -b DATE specify a beginning date" << std::endl - << " -e DATE specify an ending date" << std::endl - << " -c do not show future entries (same as -e TODAY)" << std::endl << " -C show only cleared transactions and balances" << std::endl + << " -c do not show future entries (same as -e TODAY)" << std::endl << " -d DATE specify a date mask ('-d mon', for all mondays)" << std::endl << " -E also show accounts with zero totals" << std::endl - << " -f FILE specify pathname of ledger data file" << std::endl + << " -e DATE specify an ending date" << std::endl << " -F print each account's full name" << std::endl + << " -f FILE specify pathname of ledger data file" << std::endl + << " -G use with -M to produce gnuplot-friendly output" << std::endl << " -h display this help text" << std::endl << " -i FILE read the list of inclusion regexps from FILE" << std::endl + << " -L MINS fetch price quotes if info older than MINS" << std::endl << " -l AMT don't print balance totals whose abs value is <AMT" << std::endl << " -M print register using monthly sub-totals" << std::endl - << " -G use with -M to produce gnuplot-friendly output" << std::endl - << " -n do not calculate parent account totals" << std::endl << " -N REGEX accounts matching REGEXP only display if negative" << std::endl - << " -p ARG set a price, or read prices from a file" << std::endl + << " -n do not calculate parent account totals" << std::endl << " -P download price quotes from the Internet" << std::endl << " (works by running the command \"getquote SYMBOL\")" << std::endl + << " -p ARG set a direct price conversion: COMM=PRICE" << std::endl + << " -Q FILE keep price histories in FILE (implies -P)" << std::endl << " -R do not factor in virtual transactions" << std::endl - << " -s show sub-accounts in balance totals" << std::endl << " -S sort the output of \"print\" by date" << std::endl + << " -s show sub-accounts in balance totals" << std::endl + << " -T report only commodities totals, not their value" << std::endl << " -U show only uncleared transactions and balances" << std::endl + << " -V report commodity values, but don't download quotes" << std::endl << " -v display version information" << std::endl << std::endl << "commands:" << std::endl << " balance show balance totals" << std::endl @@ -830,17 +869,24 @@ int main(int argc, char * argv[]) std::string prices; std::string limit; regexps_list regexps; - bool no_history = false; std::vector<std::string> files; main_ledger = new book; + // Initialize some variables based on environment variable settings + + if (char * p = std::getenv("PRICE_HIST")) + price_db = p; + + if (char * p = std::getenv("PRICE_EXP")) + pricing_leeway = std::atol(p) * 60; + // Parse the command-line options int c; while (-1 != (c = getopt(argc, argv, - "+b:e:d:cCUhBRV:f:i:p:PL:Q:TvsSEnFMGl:N:"))) { + "+Bb:Ccd:Ee:Ff:Ghi:L:l:MN:nPp:Q:RSsTUVv"))) { switch (char(c)) { case 'b': have_beginning = true; @@ -900,24 +946,43 @@ int main(int argc, char * argv[]) break; case 'P': - get_quotes = true; + cost_basis = false; + use_history = true; + get_quotes = true; + read_prices = true; break; - case 'L': - pricing_leeway = std::atol(optarg) * 60; + case 'Q': + cost_basis = false; + use_history = true; + get_quotes = true; + read_prices = true; + price_db = optarg; break; - case 'Q': - get_quotes = true; - price_db = optarg; + case 'V': + cost_basis = false; + use_history = true; + get_quotes = false; + read_prices = true; break; case 'B': - cost_basis = true; - // fall through... + cost_basis = true; + use_history = true; + get_quotes = false; + read_prices = false; + break; + case 'T': - no_history = true; - get_quotes = false; + cost_basis = false; + use_history = false; + get_quotes = false; + read_prices = false; + break; + + case 'L': + pricing_leeway = std::atol(optarg) * 60; break; case 'l': @@ -966,20 +1031,6 @@ int main(int argc, char * argv[]) for (; index < argc; index++) regexps.push_back(mask(argv[index])); - // If a price history file is specified with the environment - // variable PRICE_HIST, add it to the list of ledger files to read. - - if (! no_history) { - if (price_db.empty()) - if (char * p = std::getenv("PRICE_HIST")) { - get_quotes = true; - price_db = p; - } - - if (char * p = std::getenv("PRICE_EXP")) - pricing_leeway = std::atol(p) * 60; - } - // A ledger data file must be specified int entry_count = 0; @@ -989,8 +1040,8 @@ int main(int argc, char * argv[]) for (p = std::strtok(p, ":"); p; p = std::strtok(NULL, ":")) { char * sep = std::strrchr(p, '='); if (sep) *sep++ = '\0'; - entry_count += parse_ledger_file(main_ledger, std::string(p), - regexps, command == "equity", sep); + entry_count += parse_ledger_file(main_ledger, std::string(p), regexps, + command == "equity", sep); } } } else { @@ -1001,14 +1052,14 @@ int main(int argc, char * argv[]) std::strcpy(p, (*i).c_str()); char * sep = std::strrchr(p, '='); if (sep) *sep++ = '\0'; - entry_count += parse_ledger_file(main_ledger, std::string(p), - regexps, command == "equity", sep); + entry_count += parse_ledger_file(main_ledger, std::string(p), regexps, + command == "equity", sep); } } - if (! no_history && ! price_db.empty()) - entry_count += parse_ledger_file(main_ledger, price_db, - regexps, command == "equity"); + if (read_prices && ! price_db.empty()) + entry_count += parse_ledger_file(main_ledger, price_db, regexps, + command == "equity"); if (entry_count == 0) { std::cerr << ("Please specify ledger file(s) using -f option " |