Lotter (ledger-cli helper)

Artifact Content
Login

Artifact 222caad1887b6b570ca53d58d8226292c99dfd75ea66c1c4476a25ebb681009f:


// Copyright (C) 2019  David N. Cohen

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.

// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package main

import (
	"errors"
	"fmt"
	"math/big"
	"strings"
	"time"

	"src.d10.dev/command"
)

func init() {
	command.RegisterOperation(command.Operation{
		Handler:     baseMain,
		Name:        "base",
		Syntax:      "base [-b=<begin date>]",
		Description: "Convert price/cost information to base currency (using ledger-cli price data).",
	})
}

func baseMain() error {
	// define flags
	beginFlag := command.OperationFlagSet.String("b", "", "begin date")

	err := command.OperationFlagSet.Parse(command.Args()[1:])
	if err != nil {
		return err
	}

	// validate flags
	if base == "" {
		return errors.New("A base currency is required, i.e. `-base=USD`.")
	}

	var begin time.Time
	if *beginFlag != "" {
		begin, err = time.Parse("2006/01/02", *beginFlag)
		if err != nil {
			command.Check(fmt.Errorf("bad begin date (%q): %w", *beginFlag, err))
		}
	}

	for scanner.Scan() {
		txLines := scanner.Lines()

		// observe price information, if any
		// https://www.ledger-cli.org/3.0/doc/ledger3.html#Commodity-price-histories
		priceHistory := make(map[string]*big.Rat)

		for _, line := range txLines.Line {
			// we're looking for, i.e. "P 2004/06/21 02:17:58 TWCUX 27.76 USD"
			if strings.HasPrefix(line, "P ") {
				command.V(2).Info("\t", line) // debug

				field := strings.Fields(line)
				date, err := time.Parse("2006/01/02 15:04:05", strings.Join(field[1:3], " "))
				if err != nil {
					command.Check(fmt.Errorf("failed to parse historical price (%q): %w", line, err))
				}

				if field[5] != string(base) {
					command.V(1).Infof("ignoring non-base price (%q)", line)
					continue
				}

				price, ok := new(big.Rat).SetString(field[4])
				if !ok {
					command.Check(fmt.Errorf("failed to parse historical price (%q)", line))
				}

				key := historyKey(date, Asset(field[3]))
				old, ok := priceHistory[key]
				if ok {
					command.V(1).Infof("updating price history (was %f, now %f)\n\t%s", old, price, line)
				}
				priceHistory[key] = price
			}
		} // end collect price history

		payee, payeeIndex := txLines.Payee()
		if payeeIndex == PayeeNotFound {
			// not a transaction (maybe a comment)
			writeLines(append(txLines.Line, "")) // with a blank
			continue
		}
		if begin.After(txLines.Date) {
			writeLines(append(txLines.Line, "")) // with a blank
			continue
		}

		command.V(2).Info("\t", payee) // debug

		// first pass, find conversions to base
		conversion := make(map[string]Amount)
		for _, line := range txLines.Line[payeeIndex+1:] {
			split, ok := parseSplit(line)
			if !ok {
				if !strings.HasPrefix(strings.TrimLeft(line, " \t"), ";") { // check comment
					command.Check(fmt.Errorf("failed to parse transaction split: %q", line))
				}
				continue // comment is noop
			}

			if split.cost == nil && split.price == nil {
				// no price or cost to convert
				continue
			}

			cost := split.Cost()
			if cost == nil || cost.Asset == base {
				continue
			}

			// here we have a cost that must be converted into base currency

			key := historyKey(txLines.Date, cost.Asset)
			price, ok := priceHistory[key]
			if !ok {
				// strict
				command.Check(fmt.Errorf("price of %s on %s unknown", cost, txLines.Date.Format("2006/01/02")))
			}

			tmp := new(big.Rat).Mul(price, cost.Rat)
			basis := NewAmount(base, *tmp)
			command.V(1).Infof("%s @@ %s", cost, basis)
			conversion[cost.String()] = basis
		} // end first pass

		// second pass, alter
		for index, line := range txLines.Line[payeeIndex+1:] {
			split, ok := parseSplit(line)
			if !ok {
				continue // comment is noop
			}

			if split.cost != nil || split.price != nil {
				basis, ok := conversion[split.Cost().String()]
				if ok {
					// replace existing cost/price with basis
					txLines.Line[payeeIndex+1+index] = strings.Replace(line, "@", fmt.Sprintf("@@ %s ; @", basis), 1)
				}
			} else if split.delta != nil {
				deltaStr := split.delta.NegClone().String()
				basis, ok := conversion[deltaStr]
				if ok {
					// add basis where there may be no price
					txLines.Line[payeeIndex+1+index] = strings.Replace(line, deltaStr, fmt.Sprintf("%s @@ %s ; ", deltaStr, basis), 1)
				}
			}

		} // end second pass

		// write txLines (which may have been modified above)
		writeLines(txLines.Line)
		fmt.Println("") // blank line between transactions

	} // end scan loop

	return nil
}

func historyKey(date time.Time, asset Asset) string {
	return fmt.Sprintf("%s %s", date.Format("2006/01/02"), asset)
}