124 lines
4.7 KiB
Plaintext
124 lines
4.7 KiB
Plaintext
|
|
#!/usr/bin/env python3
|
||
|
|
from beancount import loader
|
||
|
|
from beancount.query import query
|
||
|
|
from beancount.parser import printer
|
||
|
|
import argparse
|
||
|
|
from tabulate import tabulate
|
||
|
|
from decimal import Decimal
|
||
|
|
from beancount.core.amount import Amount, add, sub, mul
|
||
|
|
from math import floor
|
||
|
|
from datetime import datetime, timedelta
|
||
|
|
|
||
|
|
class bcolors:
|
||
|
|
HEADER = '\033[95m'
|
||
|
|
OKBLUE = '\033[94m'
|
||
|
|
OKCYAN = '\033[96m'
|
||
|
|
OKGREEN = '\033[92m'
|
||
|
|
WARNING = '\033[93m'
|
||
|
|
FAIL = '\033[91m'
|
||
|
|
ENDC = '\033[0m'
|
||
|
|
BOLD = '\033[1m'
|
||
|
|
UNDERLINE = '\033[4m'
|
||
|
|
|
||
|
|
def draw_line():
|
||
|
|
print('─' * 30)
|
||
|
|
|
||
|
|
def get_amount(inventory):
|
||
|
|
if inventory.get_only_position() == None:
|
||
|
|
return Amount(Decimal(0), "EUR").to_string()
|
||
|
|
result = inventory.get_only_position().units
|
||
|
|
return Amount(Decimal(round(result.number, 2)), result.currency).to_string()
|
||
|
|
|
||
|
|
def print_capital_gains(inventory):
|
||
|
|
if inventory.get_only_position() == None:
|
||
|
|
return Amount(Decimal(0), "EUR").to_string()
|
||
|
|
result = inventory.get_only_position().units
|
||
|
|
num = Amount(Decimal(round(result.number * -1, 2)), result.currency).to_string()
|
||
|
|
return f"{bcolors.OKGREEN if result.number <= 0 else bcolors.FAIL}{num}{bcolors.ENDC}"
|
||
|
|
|
||
|
|
def print_perc_capital_gains(inventory, init_invest):
|
||
|
|
if inventory.get_only_position() == None:
|
||
|
|
return Amount(Decimal(0), "EUR").to_string()
|
||
|
|
result = inventory.get_only_position().units
|
||
|
|
invest = init_invest.get_only_position().units
|
||
|
|
num = round((result.number * -1 / invest.number) * 100, 2)
|
||
|
|
return f"{bcolors.OKGREEN if result.number <= 0 else bcolors.FAIL}{num} %{bcolors.ENDC}"
|
||
|
|
|
||
|
|
def calc_contributions(contributions):
|
||
|
|
total = Decimal(0)
|
||
|
|
for c in contributions:
|
||
|
|
amount = c.position.units.number
|
||
|
|
if amount > 0:
|
||
|
|
total += amount
|
||
|
|
return total
|
||
|
|
|
||
|
|
def get_returns(init_invest, end_invest, contributions):
|
||
|
|
init = init_invest.get_only_position().units
|
||
|
|
end = end_invest.get_only_position().units
|
||
|
|
contr = calc_contributions(contributions)
|
||
|
|
result = sub(sub(end, init), Amount(contr, "EUR"))
|
||
|
|
return Amount(Decimal(round(result.number, 2)), result.currency).to_string()
|
||
|
|
|
||
|
|
def print_contributions(contributions):
|
||
|
|
total = calc_contributions(contributions)
|
||
|
|
return Amount(Decimal(round(total, 2)), "EUR").to_string()
|
||
|
|
|
||
|
|
def print_report(date, end_date, init_invest, end_invest, contributions, capital_gains):
|
||
|
|
print(f"{bcolors.BOLD}Investment returns (date={date}){bcolors.ENDC}")
|
||
|
|
draw_line()
|
||
|
|
print(tabulate([
|
||
|
|
[date, get_amount(init_invest)],
|
||
|
|
[end_date, get_amount(end_invest)]
|
||
|
|
], headers=['Date', 'Balance']))
|
||
|
|
|
||
|
|
print(tabulate([
|
||
|
|
["Contributions", print_contributions(contributions)],
|
||
|
|
["Returns", get_returns(init_invest, end_invest, contributions)],
|
||
|
|
["Capital gains", print_capital_gains(capital_gains)],
|
||
|
|
["Capital gains %", print_perc_capital_gains(capital_gains, init_invest)]
|
||
|
|
]))
|
||
|
|
|
||
|
|
def get_investments(entries, options, date, end_date):
|
||
|
|
initial_query = f"SELECT convert(sum(position), \"EUR\") as position FROM date <= {date} WHERE account ~ '^Assets:Invest'"
|
||
|
|
rtypes, rrows = query.run_query(
|
||
|
|
entries, options, initial_query)
|
||
|
|
end_query = f"SELECT convert(sum(position), \"EUR\") as position FROM date <= {end_date} WHERE account ~ '^Assets:Invest'"
|
||
|
|
rtypes, erows = query.run_query(
|
||
|
|
entries, options, end_query)
|
||
|
|
return rrows[0].position, erows[0].position
|
||
|
|
|
||
|
|
def get_contributions(entries, options, start_date, end_date):
|
||
|
|
contributions_query = f"SELECT position FROM date <= {end_date} WHERE account ~ '^Assets:Liquid:R4:EUR' AND date >= {start_date}"
|
||
|
|
rtypes, rrows = query.run_query(
|
||
|
|
entries, options, contributions_query)
|
||
|
|
return rrows
|
||
|
|
|
||
|
|
def get_capital_gains(entries, options, date, end_date):
|
||
|
|
q = f"SELECT convert(sum(position), \"EUR\") as position FROM date <= {end_date} WHERE account ~ '^Income:Invest:R4:CapitalGains' AND date >= {date}"
|
||
|
|
rtypes, rrows = query.run_query(
|
||
|
|
entries, options, q)
|
||
|
|
return rrows[0].position
|
||
|
|
|
||
|
|
def main():
|
||
|
|
parser = argparse.ArgumentParser(description='Generate investments report')
|
||
|
|
parser.add_argument('date', metavar='date', type=str, nargs=1,
|
||
|
|
help='Report date in ISO format (e.g. 1970-01-01)')
|
||
|
|
|
||
|
|
args = parser.parse_args()
|
||
|
|
date = args.date[0]
|
||
|
|
|
||
|
|
filename = "ledger/main.beancount"
|
||
|
|
entries, errors, options = loader.load_file(filename)
|
||
|
|
|
||
|
|
if errors:
|
||
|
|
printer.print_errors(errors)
|
||
|
|
|
||
|
|
d = date.split("-")
|
||
|
|
end_date = datetime(int(d[0]) + 1, int(d[1]), int(d[2]))
|
||
|
|
end_date = end_date.strftime("%Y-%m-%d")
|
||
|
|
init_invest, end_invest = get_investments(entries, options, date, end_date)
|
||
|
|
contributions = get_contributions(entries, options, date, end_date)
|
||
|
|
capital_gains = get_capital_gains(entries, options, date, end_date)
|
||
|
|
print_report(date, end_date, init_invest, end_invest, contributions, capital_gains)
|
||
|
|
|
||
|
|
main()
|