From f9f6d074f40436f5617a628d6b2bcb7124b71a9d Mon Sep 17 00:00:00 2001 From: thinktwice13 Date: Fri, 10 Jun 2022 14:05:48 +0200 Subject: [PATCH] Summarize asset imports by year --- assetsummary.go | 170 +++++++++++++++++++++++++++++++++++++++++++++- import_results.go | 18 +++++ main.go | 28 ++++++-- 3 files changed, 208 insertions(+), 8 deletions(-) diff --git a/assetsummary.go b/assetsummary.go index 1cbbb0b..7b4bfe5 100644 --- a/assetsummary.go +++ b/assetsummary.go @@ -1,14 +1,178 @@ package main +import ( + "math" + "sort" + "time" +) + type AssetYear struct { Pl, Taxable, Fees, Dividends, WithholdingTax float64 } type Asset struct { Instrument - Summary map[int]*AssetYear + Holdings []Trade + Summary map[int]*AssetYear +} + +type AssetSummary map[int]*AssetYear + +func (s AssetSummary) year(y int) *AssetYear { + _, ok := s[y] + if !ok { + s[y] = new(AssetYear) + } + return s[y] +} + +type Rater interface { + Rate(string, int) float64 +} + +func summarizeAssets(imports []AssetImport, r Rater) []Asset { + assets := make([]Asset, len(imports)) + + for i, ai := range imports { + sort.Slice(ai.Trades, func(i, j int) bool { + return ai.Trades[i].Time.Before(ai.Trades[j].Time) + }) + + // sales, fees, active holdings + sales, fees, holdings := tradeAsset(ai.Trades) + fromYear := ai.Trades[0].Time.Year() + toYear := time.Now().Year() + if len(holdings) == 0 { + toYear = ai.Trades[len(ai.Trades)-1].Time.Year() + } + sum := make(AssetSummary, toYear-fromYear+1) + + // profits + for _, s := range sales { + y := sum.year(s.Time.Year()) + for _, c := range s.Basis { + proceeds := s.Price * c.Quantity * r.Rate(s.Currency, s.Time.Year()) + cost := c.Price * c.Quantity * r.Rate(c.Currency, s.Time.Year()) + y.Pl += proceeds - cost + if s.Time.After(TaxableDeadline(c.Time)) { + continue + } + y.Taxable += proceeds - cost + } + } + + // fees + for _, f := range fees { + amt := f.Amount * r.Rate(f.Currency, f.Year) + sum.year(f.Year).Fees += amt + } + + // dividends + for _, d := range ai.Dividends { + amt := d.Amount * r.Rate(d.Currency, d.Year) + sum.year(d.Year).Dividends += amt + } + // withholding tax + for _, t := range ai.WithholdingTax { + amt := t.Amount * r.Rate(t.Currency, t.Year) + sum.year(t.Year).WithholdingTax += amt + } + + assets[i] = Asset{ + Instrument: ai.Instrument, + Holdings: holdings, + Summary: sum, + } + } + + return assets +} + +type fifo struct { + data []Trade +} + +type Cost struct { + Time time.Time + Currency string + Price, Quantity float64 +} + +type Sale struct { + Time time.Time + Currency string + Price float64 + Basis []Cost +} + +func tradeAsset(ts []Trade) ([]Sale, []Transaction, []Trade) { + fifo := new(fifo) + fees := make([]Transaction, 0, len(ts)) + + var sales []Sale + for i, t := range ts { + fees = append(fees, Transaction{ + Currency: t.Currency, + Amount: t.Fee, + Year: t.Time.Year(), + }) + + if t.Quantity > 0 { + // Purchase + fifo.data = append(fifo.data, ts[i]) + continue + } + + // Sale + s := Sale{ + Time: t.Time, + Currency: t.Currency, + Price: t.Price, + } + // Calculate costs + for { + if t.Quantity == 0 { + sales = append(sales, s) + break + } + + purchase := &fifo.data[0] + cost := Cost{ + Time: purchase.Time, + Currency: purchase.Currency, + Price: purchase.Price, + Quantity: math.Min(purchase.Quantity, math.Abs(t.Quantity)), + } + purchase.Quantity -= cost.Quantity + t.Quantity += cost.Quantity + s.Basis = append(s.Basis, cost) + if purchase.Quantity == 0 { + // TODO Handle error when this is the last purchase, but there are more trade (import error) + fifo.data = fifo.data[1:] + } + } + } + + return sales, fees, fifo.data } -func NewAssetSummary(size int) map[int]*AssetYear { - return make(map[int]*AssetYear, size) +func TaxableDeadline(since time.Time) time.Time { + return since.AddDate(2, 0, 0) +} + +func filterFromTo(l []int, min, max int) []int { + cap := max - min + 1 + if len(l) < cap { + cap = len(l) + } + filtered := make([]int, 0, cap) + for _, i := range l { + if i < min || i > max { + continue + } + + filtered = append(filtered, i) + } + + return filtered } diff --git a/import_results.go b/import_results.go index 2efa20f..75270dd 100644 --- a/import_results.go +++ b/import_results.go @@ -21,6 +21,23 @@ type Instrument struct { Category string } +func (i *Instrument) Domicile() string { + var isin string + for _, symbol := range i.Symbols { + if len(symbol) != 11 { + continue + } + isin = symbol + break + } + + if isin == "" { + return i.Symbols[0] + } + + return isin[:2] +} + type AssetImport struct { Instrument Trades []Trade @@ -186,5 +203,6 @@ func (a assets) list() []AssetImport { list = append(list, *a) listed[a] = true } + return list } diff --git a/main.go b/main.go index a3f1f20..38d1e26 100644 --- a/main.go +++ b/main.go @@ -9,18 +9,36 @@ import ( func main() { fmt.Println("Hello World") - assets, _, years, currencies := readDir() + assets, fees, years, currencies := readDir() rates, err := fx.New(currencies, years) if err != nil { log.Fatalln(err) } - PrettyPrint(rates) - // var ss []Asset - for _, assetimport := range assets { - PrettyPrint(assetimport) + summaries := summarizeAssets(assets, rates) + print(len(summaries)) + PrettyPrint(summaries) + convFees := convertFees(fees, rates) + tr := buildTaxReport(summaries, convFees, len(years)) + PrettyPrint(tr) +} + +type YearAmount struct { + Amount float64 + Year int +} + +func convertFees(fees []Transaction, r Rater) []YearAmount { + converted := make([]YearAmount, len(fees)) + for i := range fees { + f := &fees[i] + converted[i] = YearAmount{ + Amount: f.Amount * r.Rate(f.Currency, f.Year), + Year: f.Year, + } } + return converted }