Coverage for hledger_lots/info.py: 67%
87 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-05 00:04 -0300
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-05 00:04 -0300
1import csv
2import re
3import subprocess
4from dataclasses import dataclass
5from datetime import date, datetime
6from io import StringIO
7from typing import List, Optional, Tuple, TypedDict
9from tabulate import tabulate
11from .hl import hledger2txn
12from .lib import adjust_commodity, get_files_comm, get_xirr
15class LotsInfo(TypedDict):
16 comm: str
17 cur: str
18 qtty: str
19 amount: str
20 avg_cost: str
21 mkt_price: Optional[str]
22 mkt_amount: Optional[str]
23 mkt_profit: Optional[str]
24 mkt_date: Optional[str]
25 xirr: Optional[str]
28@dataclass
29class Price:
30 date: date
31 comm: str
32 price: float
33 cur: str
36def get_last_price(files_comm: List[str], commodity: str):
37 prices_comm = [
38 "hledger",
39 *files_comm,
40 "prices",
41 f"cur:{commodity}",
42 "--infer-reverse-prices",
43 ]
44 prices_proc = subprocess.run(prices_comm, capture_output=True)
45 prices_str = prices_proc.stdout.decode("utf8")
47 if prices_str == "":
48 return (None, None)
50 prices_list = [row.split(" ", 3) for row in prices_str.split("\n") if row != ""]
52 date_list = [
53 (row[1], re.sub(r"[^0-9.]", "", row[3])) for row in prices_list if len(row) > 0
54 ]
56 if len(date_list) == 0:
57 return (None, None)
59 last_date_str = date_list[-1][0]
60 last_date = datetime.strptime(last_date_str, "%Y-%m-%d").date()
61 last_price = float(date_list[-1][1])
62 return (last_date, last_price)
65def get_commodities(journals: Tuple[str, ...]):
66 files_comm = get_files_comm(journals)
67 comm = ["hledger", *files_comm, "commodities"]
68 commodities_proc = subprocess.run(comm, capture_output=True)
69 commodities_str = commodities_proc.stdout.decode("utf8")
71 commodities_list = [com for com in commodities_str.split("\n") if com != ""]
72 return commodities_list
75class Info:
76 def __init__(
77 self, journals: Tuple[str, ...], commodity: str, no_desc: Optional[str] = None
78 ) -> None:
79 self.journals = journals
80 self.files_comm = get_files_comm(journals)
81 self.commodity = commodity.upper()
82 self.txns = hledger2txn(journals, commodity, no_desc)
84 self.has_txn = len(self.txns) > 0
85 self.last_price = get_last_price(self.files_comm, commodity)
87 self.market_date, self.market_price = self.last_price
89 def get_lots_xirr(self, last_buy_date: date):
90 if self.market_date and self.market_price and self.market_date >= last_buy_date:
91 xirr = get_xirr(self.market_price, self.market_date, self.txns)
92 return xirr
94 def get_info_txt(self, info: LotsInfo):
95 info_txt = f"""
96Info
97----
98Commodity: {info['comm']}
99Quantity: {info['qtty']}
100Amount: {info['amount']}
101Average Cost: {info['avg_cost']}
102"""
104 if self.market_date or self.market_price:
105 info_txt += f"""
106Market Price: {info['mkt_price']}
107Market Amount: {info['mkt_amount']}
108Market Profit: {info['mkt_profit']}
109Market Date: {info['mkt_date']}
110Xirr: {info['xirr']} (APR 30/360US)
111"""
112 else:
113 info_txt += "\nMarket Data not available"
115 return info_txt
118class AllInfo:
119 def __init__(self, journals: Tuple[str, ...], no_desc: str) -> None:
120 self.journals = journals
121 self.no_desc = no_desc
122 self.commodities = get_commodities(journals)
124 def get_infos_table(self, infos: List[LotsInfo], output_format: str):
125 infos_list = [info for info in infos]
126 infos_sorted = sorted(
127 infos_list, key=lambda info: info["xirr"] or "", reverse=True
128 )
129 table = tabulate(
130 infos_sorted,
131 headers="keys",
132 numalign="decimal",
133 floatfmt=",.4f",
134 tablefmt=output_format,
135 )
136 return table
138 def get_infos_csv(self, infos: List[LotsInfo]):
139 infos_list = [info for info in infos]
140 infos_sorted = sorted(
141 infos_list, key=lambda info: info["xirr"] or "", reverse=True
142 )
144 fieldnames = infos_sorted[0].keys()
145 infos_io = StringIO()
146 writer = csv.DictWriter(infos_io, fieldnames=fieldnames)
147 writer.writeheader()
148 writer.writerows(infos_sorted)
149 infos_io.seek(0)
150 return infos_io