From 976409912a98cc5bc6f2c4691292c2bf8f3368e0 Mon Sep 17 00:00:00 2001 From: Gerben Jacobs Date: Sat, 5 Oct 2024 13:28:59 +0200 Subject: [PATCH] adds "Renames" config for renamed/delisted stocks --- README.md | 9 ++++++++- cmd/aggregator/main.go | 3 ++- config.go | 1 + config.yaml | 6 ++++++ testdata/gbp/trading212.csv | 2 +- testdata/multiple/3-wrong-order.csv | 2 +- testdata/trading212.csv | 2 +- trading212/aggregate.go | 23 +++++++++++++++-------- trading212/aggregate_test.go | 25 +++++++++++++++++++------ trading212/collect_test.go | 2 +- 10 files changed, 55 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 898260e..24fe91c 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,12 @@ symbols: RIO: RIO.L SAN: SAN.PA +# Renames deal with stocks that have changed their symbol +# for example by becoming a new company or by being delisted +renames: + GPS: GAP + TUP: TUPBQ + # Pies allows you split your aggregation into multiple CSVs # uncomment to use #pies: @@ -95,7 +101,8 @@ The default currency is set to EUR, but you can use the dropdown to change it to ## Changelog -- v0.2.5 - 2024-02-98 - Added "Lending interest" field +- v0.2.6 - 2024-10-05 - Added "Renames" config for renamed/delisted stocks +- v0.2.5 - 2024-02-08 - Added "Lending interest" field - v0.2.4 - 2023-08-01 - Fix stock splits for stocks that are untouched + Update dependencies - v0.2.3 - 2023-07-31 - Skip "Currency conversion" action + Deposits has changed fields - v0.2.2 - 2023-07-24 - Currency name not in headers anymore diff --git a/cmd/aggregator/main.go b/cmd/aggregator/main.go index 2baed1f..adf6025 100644 --- a/cmd/aggregator/main.go +++ b/cmd/aggregator/main.go @@ -72,13 +72,14 @@ func main() { "pies": len(cfg.Pies), "splits": len(cfg.Splits), "symbols": len(cfg.Symbols), + "renames": len(cfg.Renames), }).Info("Starting process.") // loop through directory and find csv files events := trading212.Collect(cfg.Input) // aggregate events via Trading212 algorithm - stocks, totals := trading212.Aggregate(cfg.Splits, events) + stocks, totals := trading212.Aggregate(cfg.Splits, cfg.Renames, events) log.WithFields(logrus.Fields{ "deposits": totals.Deposits, diff --git a/config.go b/config.go index bb0730e..20230fb 100644 --- a/config.go +++ b/config.go @@ -17,6 +17,7 @@ type Config struct { PieOnly string `yaml:"pie-only"` Splits []Splits `yaml:"splits"` Symbols map[string]string `yaml:"symbols"` + Renames map[string]string `yaml:"renames"` Pies []struct { Name string `yaml:"name"` Symbols []string `yaml:"symbols"` diff --git a/config.yaml b/config.yaml index cdc9f44..bd461e9 100644 --- a/config.yaml +++ b/config.yaml @@ -21,6 +21,12 @@ symbols: RIO: RIO.L SAN: SAN.PA +# Renames deal with stocks that have changed their symbol +# for example by becoming a new company or by being delisted +renames: + GPS: GAP + TUP: TUPBQ + # Pies allows you split your aggregation into multiple CSVs # uncomment to use #pies: diff --git a/testdata/gbp/trading212.csv b/testdata/gbp/trading212.csv index fc7c8af..5461749 100644 --- a/testdata/gbp/trading212.csv +++ b/testdata/gbp/trading212.csv @@ -8,5 +8,5 @@ Deposit,2021-09-07 13:43:10,,,,,,,,,1000.00,,,1001.40,1.40,,"Transaction ID: xxx Market buy,2021-09-27 13:19:13,US02079K1079,ABEC,"Google",0.0041253700,2424.00,EUR,1.00000,,10.00,,,,,,,EOF5,, Dividend (Ordinary),2021-09-30 11:15:32,US5949181045,MSFT,"Microsoft",0.2709950000,0.48,USD,Not available,,0.11,0.02,USD,,,,,,, Market buy,2022-03-07 16:10:26,FR0000120578,SAN,"Sanofi",0.1117960000,89.18,EUR,1.00000,,10.00,,,,,,,EOF6,,0.03 -Market buy,2022-07-29 14:28:17,US02079K1079,ABEC,"Alphabet (Class C)",2.2887315000,113.60,EUR,1.00000,,260.00,,,,,,,EOF7,, +Market buy,2022-07-29 14:28:17,US02079K1079,GOOG,"Alphabet (Class C)",2.2887315000,113.60,EUR,1.00000,,260.00,,,,,,,EOF7,, diff --git a/testdata/multiple/3-wrong-order.csv b/testdata/multiple/3-wrong-order.csv index b80397f..8094ad0 100644 --- a/testdata/multiple/3-wrong-order.csv +++ b/testdata/multiple/3-wrong-order.csv @@ -1,4 +1,4 @@ Action,Time,ISIN,Ticker,Name,No. of shares,Price / share,Currency (Price / share),Exchange rate,Result,Total,Withholding tax,Currency (Withholding tax),Charge amount,Deposit fee,Stamp duty reserve tax,Notes,ID,Currency conversion fee,French transaction tax Market buy,2022-03-07 16:10:26,FR0000120578,SAN,"Sanofi",0.1117960000,89.18,EUR,1.00000,,10.00,,,,,,,EOF6,,0.03 -Market buy,2022-07-29 14:28:17,US02079K1079,ABEC,"Alphabet (Class C)",2.2887315000,113.60,EUR,1.00000,,260.00,,,,,,,EOF7,, +Market buy,2022-07-29 14:28:17,US02079K1079,GOOG,"Alphabet (Class C)",2.2887315000,113.60,EUR,1.00000,,260.00,,,,,,,EOF7,, diff --git a/testdata/trading212.csv b/testdata/trading212.csv index fc7c8af..5461749 100644 --- a/testdata/trading212.csv +++ b/testdata/trading212.csv @@ -8,5 +8,5 @@ Deposit,2021-09-07 13:43:10,,,,,,,,,1000.00,,,1001.40,1.40,,"Transaction ID: xxx Market buy,2021-09-27 13:19:13,US02079K1079,ABEC,"Google",0.0041253700,2424.00,EUR,1.00000,,10.00,,,,,,,EOF5,, Dividend (Ordinary),2021-09-30 11:15:32,US5949181045,MSFT,"Microsoft",0.2709950000,0.48,USD,Not available,,0.11,0.02,USD,,,,,,, Market buy,2022-03-07 16:10:26,FR0000120578,SAN,"Sanofi",0.1117960000,89.18,EUR,1.00000,,10.00,,,,,,,EOF6,,0.03 -Market buy,2022-07-29 14:28:17,US02079K1079,ABEC,"Alphabet (Class C)",2.2887315000,113.60,EUR,1.00000,,260.00,,,,,,,EOF7,, +Market buy,2022-07-29 14:28:17,US02079K1079,GOOG,"Alphabet (Class C)",2.2887315000,113.60,EUR,1.00000,,260.00,,,,,,,EOF7,, diff --git a/trading212/aggregate.go b/trading212/aggregate.go index 82faf66..550f585 100644 --- a/trading212/aggregate.go +++ b/trading212/aggregate.go @@ -9,7 +9,7 @@ import ( // Aggregate takes a map of events and aggregates them into a map of stocks and totals, // based on the Trading212 algorithm, along with stock splits. -func Aggregate(splits []fin.Splits, events []TradeEvent) ([]fin.Aggregate, fin.Totals) { +func Aggregate(splits []fin.Splits, renames map[string]string, events []TradeEvent) ([]fin.Aggregate, fin.Totals) { var stocks = make(map[string]fin.Aggregate) var stockNames []string var totals fin.Totals @@ -30,20 +30,26 @@ func Aggregate(splits []fin.Splits, events []TradeEvent) ([]fin.Aggregate, fin.T continue } + // handle renamed stock symbols + symbol := e.TickerSymbol + if rn, ok := renames[symbol]; ok { + symbol = rn + } + // create entry if it doesn't exist - if _, ok := stocks[e.TickerSymbol]; !ok { - stocks[e.TickerSymbol] = fin.Aggregate{ - Symbol: e.TickerSymbol, + if _, ok := stocks[symbol]; !ok { + stocks[symbol] = fin.Aggregate{ + Symbol: symbol, } - stockNames = append(stockNames, e.TickerSymbol) + stockNames = append(stockNames, symbol) } // calculate changes - a := stocks[e.TickerSymbol] + a := stocks[symbol] // did a stock split happen today for _, split := range splits { - if split.Symbol == e.TickerSymbol && + if split.Symbol == symbol && split.Date > a.LastUpdate.Format("2006-01-02") && split.Date <= e.Time.Format("2006-01-02") { a.ShareCount = a.ShareCount * split.Ratio } @@ -74,7 +80,8 @@ func Aggregate(splits []fin.Splits, events []TradeEvent) ([]fin.Aggregate, fin.T totals.Taxes += e.Tax // update totals - if a.ShareCount > 0 { + if floorFloat(a.ShareCount, 4) > 0 { + // if it's practically zero, reset it (float comparison issues) a.AvgPrice = a.ShareCost / a.ShareCount } else { // during this event everything was sold diff --git a/trading212/aggregate_test.go b/trading212/aggregate_test.go index 6d07dc7..43ed46c 100644 --- a/trading212/aggregate_test.go +++ b/trading212/aggregate_test.go @@ -10,7 +10,10 @@ import ( func TestAggregate(t *testing.T) { splits := []fin.Splits{ - {Symbol: "ABEC", Date: "2022-07-16", Ratio: 20}, + {Symbol: "GOOG", Date: "2022-07-16", Ratio: 20}, + } + renames := map[string]string{ + "ABEC": "GOOG", } tests := []struct { name string @@ -22,8 +25,8 @@ func TestAggregate(t *testing.T) { name: "Regular test like our testdata", events: defaultTestDataEvents, want: []fin.Aggregate{ - {Symbol: "ABEC", Name: "Alphabet (Class C)", ShareCount: 2.371231, AvgPrice: 113.86, PriceCurrency: "EUR", ShareCost: 270, ShareCostLocal: 270, ShareResult: 0, TotalDividend: 0, Fees: 0, Final: 0, LastUpdate: time.Date(2022, 7, 29, 14, 28, 17, 0, time.UTC)}, {Symbol: "FB", Name: "Meta Platforms", ShareCount: 0.086391, AvgPrice: 362, PriceCurrency: "USD", ShareCost: 31.27, ShareCostLocal: 26.67, ShareResult: 0, TotalDividend: 0, Fees: 0.04, Final: -0.04, LastUpdate: time.Date(2021, 8, 9, 18, 31, 41, 0, time.UTC)}, + {Symbol: "GOOG", Name: "Alphabet (Class C)", ShareCount: 2.371231, AvgPrice: 113.86, PriceCurrency: "EUR", ShareCost: 270, ShareCostLocal: 270, ShareResult: 0, TotalDividend: 0, Fees: 0, Final: 0, LastUpdate: time.Date(2022, 7, 29, 14, 28, 17, 0, time.UTC)}, {Symbol: "MSFT", Name: "Microsoft", ShareCount: 0, AvgPrice: 0, PriceCurrency: "USD", ShareCost: 0, ShareCostLocal: 0, ShareResult: 2.61, TotalDividend: 0.11, Fees: 0.2, Final: 2.51, LastUpdate: time.Date(2021, 9, 30, 11, 15, 32, 0, time.UTC)}, {Symbol: "SAN", Name: "Sanofi", ShareCount: 0.111796, AvgPrice: 89.18, PriceCurrency: "EUR", ShareCost: 9.97, ShareCostLocal: 10, ShareResult: 0, TotalDividend: 0, Fees: 0.03, Final: -0.03, LastUpdate: time.Date(2022, 3, 7, 16, 10, 26, 0, time.UTC)}, {Symbol: "TSLA", Name: "Tesla", ShareCount: 0.076654, AvgPrice: 713.94, PriceCurrency: "USD", ShareCost: 54.72, ShareCostLocal: 46.67, ShareResult: 0, TotalDividend: 0, Fees: 0.07, Final: -0.08, LastUpdate: time.Date(2021, 8, 9, 18, 31, 41, 0, time.UTC)}, @@ -41,17 +44,27 @@ func TestAggregate(t *testing.T) { { name: "Test with a split", events: []TradeEvent{ - {Action: "Market buy", Time: DateTime{Time: time.Date(2021, 9, 27, 13, 19, 13, 0, time.UTC)}, TickerSymbol: "ABEC", ShareCount: 0.005, SharePrice: 2000.00, Total: 10.00, ID: "EOF1"}, - {Action: "Market buy", Time: DateTime{Time: time.Date(2022, 9, 27, 13, 19, 13, 0, time.UTC)}, TickerSymbol: "ABEC", ShareCount: 0.125, SharePrice: 80.00, Total: 10.00, ID: "EOF2"}, + {Action: "Market buy", Time: DateTime{Time: time.Date(2021, 9, 27, 13, 19, 13, 0, time.UTC)}, TickerSymbol: "GOOG", ShareCount: 0.005, SharePrice: 2000.00, Total: 10.00, ID: "EOF1"}, + {Action: "Market buy", Time: DateTime{Time: time.Date(2022, 9, 27, 13, 19, 13, 0, time.UTC)}, TickerSymbol: "GOOG", ShareCount: 0.125, SharePrice: 80.00, Total: 10.00, ID: "EOF2"}, + }, + want: []fin.Aggregate{ + {Symbol: "GOOG", ShareCount: 0.225, AvgPrice: 88.88, ShareCost: 20, ShareCostLocal: 20, ShareResult: 0, TotalDividend: 0, Fees: 0, Final: 0, LastUpdate: time.Date(2022, 9, 27, 13, 19, 13, 0, time.UTC)}, + }, + }, + { + name: "Test float precision when selling", + events: []TradeEvent{ + {Action: "Market buy", Time: DateTime{Time: time.Date(2021, 9, 27, 13, 19, 13, 0, time.UTC)}, TickerSymbol: "FB", ShareCount: 1.2345678, SharePrice: 2000.00, Total: 10.00, ID: "EOF1"}, + {Action: "Market sell", Time: DateTime{Time: time.Date(2022, 9, 27, 13, 19, 13, 0, time.UTC)}, TickerSymbol: "FB", ShareCount: 1.2345, SharePrice: 2100.00, Result: 100, Total: 10.00, ID: "EOF2"}, }, want: []fin.Aggregate{ - {Symbol: "ABEC", ShareCount: 0.225, AvgPrice: 88.88, ShareCost: 20, ShareCostLocal: 20, ShareResult: 0, TotalDividend: 0, Fees: 0, Final: 0, LastUpdate: time.Date(2022, 9, 27, 13, 19, 13, 0, time.UTC)}, + {Symbol: "FB", ShareCount: 0, AvgPrice: 0, ShareCost: 0, ShareCostLocal: 0, ShareResult: 100, TotalDividend: 0, Fees: 0, Final: 100, LastUpdate: time.Date(2022, 9, 27, 13, 19, 13, 0, time.UTC)}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - aggregates, totals := Aggregate(splits, tt.events) + aggregates, totals := Aggregate(splits, renames, tt.events) for idx, agg := range aggregates { if !reflect.DeepEqual(agg, tt.want[idx]) { t.Errorf("aggregate for %s is a mismatch \n%#v\n%#v", agg.Symbol, agg, tt.want[idx]) diff --git a/trading212/collect_test.go b/trading212/collect_test.go index 0d82916..fff2873 100644 --- a/trading212/collect_test.go +++ b/trading212/collect_test.go @@ -18,7 +18,7 @@ var defaultTestDataEvents = []TradeEvent{ {Action: "Market buy", Time: DateTime{Time: time.Date(2021, 9, 27, 13, 19, 13, 0, time.UTC)}, ISIN: "US02079K1079", TickerSymbol: "ABEC", TickerName: "Google", ShareCount: 0.0041253700, SharePrice: 2424.00, ShareCurrency: "EUR", ExchangeRate: "1.00000", Total: 10.00, ID: "EOF5"}, {Action: "Dividend (Ordinary)", Time: DateTime{Time: time.Date(2021, 9, 30, 11, 15, 32, 0, time.UTC)}, ISIN: "US5949181045", TickerSymbol: "MSFT", TickerName: "Microsoft", ShareCount: 0.2709950000, SharePrice: 0.48, ShareCurrency: "USD", ExchangeRate: "Not available", Total: 0.11, Tax: 0.02, TaxCurrency: "USD"}, {Action: "Market buy", Time: DateTime{Time: time.Date(2022, 3, 7, 16, 10, 26, 0, time.UTC)}, ISIN: "FR0000120578", TickerSymbol: "SAN", TickerName: "Sanofi", ShareCount: 0.1117960000, SharePrice: 89.18, ShareCurrency: "EUR", ExchangeRate: "1.00000", Total: 10.00, ID: "EOF6", FRFee: 0.03}, - {Action: "Market buy", Time: DateTime{Time: time.Date(2022, 7, 29, 14, 28, 17, 0, time.UTC)}, ISIN: "US02079K1079", TickerSymbol: "ABEC", TickerName: "Alphabet (Class C)", ShareCount: 2.2887315000, SharePrice: 113.60, ShareCurrency: "EUR", ExchangeRate: "1.00000", Total: 260.00, ID: "EOF7"}, + {Action: "Market buy", Time: DateTime{Time: time.Date(2022, 7, 29, 14, 28, 17, 0, time.UTC)}, ISIN: "US02079K1079", TickerSymbol: "GOOG", TickerName: "Alphabet (Class C)", ShareCount: 2.2887315000, SharePrice: 113.60, ShareCurrency: "EUR", ExchangeRate: "1.00000", Total: 260.00, ID: "EOF7"}, } func TestCollect(t *testing.T) {