Skip to content

Commit

Permalink
Handle events related to T212 Card + Withdrawals
Browse files Browse the repository at this point in the history
  • Loading branch information
gerbenjacobs committed Oct 15, 2024
1 parent 9764099 commit bb8fb77
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 54 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ The default currency is set to EUR, but you can use the dropdown to change it to

## Changelog

- v0.2.7 - 2024-10-15 - Handle events related to T212 Card + Withdrawals
- 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
Expand Down
24 changes: 16 additions & 8 deletions cmd/aggregator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"flag"
"fmt"
"math"
"os"
"strings"

Expand Down Expand Up @@ -82,14 +83,16 @@ func main() {
stocks, totals := trading212.Aggregate(cfg.Splits, cfg.Renames, events)

log.WithFields(logrus.Fields{
"deposits": totals.Deposits,
"invested": totals.Invested,
"realized": totals.Realized,
"dividends": totals.Dividends,
"fees": totals.Fees,
"taxes": totals.Taxes,
"cash": totals.Cash,
"interest": totals.Interest,
"deposits": totals.Deposits,
"invested": totals.Invested,
"realized": totals.Realized,
"realized-with-costs": ceilFloat(totals.Realized-totals.Fees-totals.Taxes, 2),
"dividends": totals.Dividends,
"fees": totals.Fees,
"taxes": totals.Taxes,
"cash": totals.Cash,
"interest": totals.Interest,
"withdrawals": totals.Withdrawals,
}).Info("Completed aggregation.")

// write output
Expand Down Expand Up @@ -208,3 +211,8 @@ func writeOutputJSON(cfg fin.Config, outputName string, output interface{}) stri

return fn
}

func ceilFloat(f float64, precision int) float64 {
d := math.Pow(10, float64(precision))
return math.Ceil(f*d) / d
}
17 changes: 9 additions & 8 deletions models.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ type Aggregate struct {
}

type Totals struct {
Deposits float64 // the money you deposited
Invested float64 // the money you have invested, minus fees
Realized float64 // gains you have realized by selling
Dividends float64 // amount of money you received from dividends
Fees float64 // fees you paid
Cash float64 // cash left in your portfolio
Taxes float64 // taxes withheld from dividends
Interest float64 // interest you received on cash
Deposits float64 // the money you deposited
Invested float64 // the money you have invested, minus fees
Realized float64 // gains you have realized by selling
Dividends float64 // amount of money you received from dividends
Fees float64 // fees you paid
Withdrawals float64 // costs that are taken away from your cash i.e. Card debit
Cash float64 // cash left in your portfolio
Taxes float64 // taxes withheld from dividends
Interest float64 // interest you received on cash, lent shares or card cashback
}
26 changes: 19 additions & 7 deletions trading212/aggregate.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,32 @@ func Aggregate(splits []fin.Splits, renames map[string]string, events []TradeEve
var stockNames []string
var totals fin.Totals
for _, e := range events {
// skip currency conversions
if e.Action == "Currency conversion" {
// skip every event we don't deal with
if e.IsSkippable() {
continue
}
// handle deposits
if e.Action == "Deposit" {
// handle deposits or additions
if e.Action == "Deposit" || e.Action == "Spending cashback" {
totals.Deposits += e.Total
totals.Withdrawals -= e.DepositFee
continue
}

// handle interest
if e.Action == "Interest on cash" || e.Action == "Lending interest" {
if e.IsInterest() {
totals.Interest += e.Total
continue
}
// handle money withdrawl
if e.IsMoneyWithdrawal() {
// we subtract the total, because it's stored as a negative number
totals.Withdrawals -= e.Total
continue
}

// if no action matches, but our symbol is empty, we continue too
if e.TickerSymbol == "" {
continue
}

// handle renamed stock symbols
symbol := e.TickerSymbol
Expand Down Expand Up @@ -114,7 +125,7 @@ func Aggregate(splits []fin.Splits, renames map[string]string, events []TradeEve

// calculate cash left over in portfolio
moneyGained := totals.Deposits + totals.Realized + totals.Dividends
moneySpent := totals.Invested + totals.Fees
moneySpent := totals.Invested + totals.Fees + totals.Withdrawals
totals.Cash = moneyGained - moneySpent

// format money values to 2 decimals
Expand All @@ -135,6 +146,7 @@ func Aggregate(splits []fin.Splits, renames map[string]string, events []TradeEve
totals.Fees = floorFloat(totals.Fees, 2)
totals.Cash = floorFloat(totals.Cash, 2)
totals.Taxes = floorFloat(totals.Taxes, 2)
totals.Withdrawals = floorFloat(totals.Withdrawals, 2)

// sort and collate aggregates
sort.Strings(stockNames)
Expand Down
40 changes: 32 additions & 8 deletions trading212/aggregate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ func TestAggregate(t *testing.T) {
{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)},
},
totals: &fin.Totals{
Deposits: 2000,
Invested: 353.2,
Realized: 2.61,
Dividends: 0.11,
Fees: 0.34,
Cash: 1649.17,
Taxes: 0.02,
Deposits: 2000,
Invested: 353.2,
Realized: 2.61,
Dividends: 0.11,
Fees: 0.34,
Cash: 1650.58,
Taxes: 0.02,
Withdrawals: -1.4,
},
},
{
Expand All @@ -61,6 +62,21 @@ func TestAggregate(t *testing.T) {
{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)},
},
},
{
name: "Test operations with T212 card",
events: []TradeEvent{
{Action: "New card cost", Time: DateTime{Time: time.Date(2021, 9, 27, 13, 19, 13, 0, time.UTC)}, Total: -4.95, TotalCurrency: "EUR", ID: "EOF1"},
{Action: "Spending cashback", Time: DateTime{Time: time.Date(2022, 9, 27, 13, 19, 13, 0, time.UTC)}, Total: 0.22, TotalCurrency: "EUR", ID: "EOF2"},
{Action: "Deposit", Time: DateTime{Time: time.Date(2022, 9, 27, 13, 19, 13, 0, time.UTC)}, Total: 100, TotalCurrency: "EUR", ID: "EOF3"},
{Action: "Card debit", Time: DateTime{Time: time.Date(2022, 9, 27, 13, 19, 13, 0, time.UTC)}, Total: -15, TotalCurrency: "EUR", ID: "EOF4"},
},
want: []fin.Aggregate{},
totals: &fin.Totals{
Deposits: 100.22,
Cash: 80.27,
Withdrawals: 19.95, // New card cost + Card debit
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -73,9 +89,17 @@ func TestAggregate(t *testing.T) {

if tt.totals != nil {
if !reflect.DeepEqual(totals, *tt.totals) {
t.Errorf("totals are a mismatch \n%#v\n%#v", totals, tt.totals)
t.Errorf("totals are a mismatch \n%#v\n%#v", totals, *tt.totals)
}
}
})
}
}

/*
trading212.TradeEvent{Action:"Deposit", Time:time.Date(2021, time.August, 9, 15, 25, 29, 0, time.UTC), ISIN:"", TickerSymbol:"", TickerName:"", ShareCount:0, SharePrice:0, ShareCurrency:"", ExchangeRate:"", ChargeAmount:1000, DepositFee:0, Result:0, ResultCurrency:"", Total:1000, TotalCurrency:"", Tax:0, TaxCurrency:"", StampDuty:0, StampDutyTax:0, Notes:"Transaction ID: xxx", ID:"d0ca160f-f407-4b9b-bb36-xxx", FXFee:0, FRFee:0, FinraFee:0, MerchantName:"", MerchantCategory:""}
trading212.TradeEvent{Action:"Deposit", Time:time.Date(2021, time.August, 9, 15, 25, 29, 0, time.UTC), ISIN:"", TickerSymbol:"", TickerName:"", ShareCount:0, SharePrice:0, ShareCurrency:"", ExchangeRate:"", ChargeAmount:0, DepositFee:0, Result:0, ResultCurrency:"", Total:1000, TotalCurrency:"", Tax:0, TaxCurrency:"", StampDuty:0, StampDutyTax:0, Notes:"Transaction ID: xxx", ID:"d0ca160f-f407-4b9b-bb36-xxx", FXFee:0, FRFee:0, FinraFee:0, MerchantName:"", MerchantCategory:""}
trading212.TradeEvent{Action:"Deposit", Time:time.Date(2021, time.September, 7, 13, 43, 10, 0, time.UTC), ISIN:"", TickerSymbol:"", TickerName:"", ShareCount:0, SharePrice:0, ShareCurrency:"", ExchangeRate:"", ChargeAmount:1001.4, DepositFee:1.4, Result:0, ResultCurrency:"", Total:1000, TotalCurrency:"", Tax:0, TaxCurrency:"", StampDuty:0, StampDutyTax:0, Notes:"Transaction ID: xxx", ID:"3e8f5274-1c62-46d6-baf4-xxx", FXFee:0, FRFee:0, FinraFee:0, MerchantName:"", MerchantCategory:""}
trading212.TradeEvent{Action:"Deposit", Time:time.Date(2021, time.September, 7, 13, 43, 10, 0, time.UTC), ISIN:"", TickerSymbol:"", TickerName:"", ShareCount:0, SharePrice:0, ShareCurrency:"", ExchangeRate:"", ChargeAmount:1000, DepositFee:0, Result:0, ResultCurrency:"", Total:1000, TotalCurrency:"", Tax:0, TaxCurrency:"", StampDuty:0, StampDutyTax:0, Notes:"Transaction ID: xxx", ID:"3e8f5274-1c62-46d6-baf4-xxx", FXFee:0, FRFee:0, FinraFee:0, MerchantName:"", MerchantCategory:""}
*/
4 changes: 2 additions & 2 deletions trading212/collect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import (
// defaultTestDataEvents is a list of TradeEvents that matches testdata/tradign212.csv
// this can be used by several tests
var defaultTestDataEvents = []TradeEvent{
{Action: "Deposit", Time: DateTime{Time: time.Date(2021, 8, 9, 15, 25, 29, 0, time.UTC)}, Total: 1000, Notes: "Transaction ID: xxx", ID: "d0ca160f-f407-4b9b-bb36-xxx"},
{Action: "Deposit", Time: DateTime{Time: time.Date(2021, 8, 9, 15, 25, 29, 0, time.UTC)}, ChargeAmount: 1000, Total: 1000, Notes: "Transaction ID: xxx", ID: "d0ca160f-f407-4b9b-bb36-xxx"},
{Action: "Market buy", Time: DateTime{Time: time.Date(2021, 8, 9, 18, 31, 41, 0, time.UTC)}, ISIN: "US30303M1027", TickerSymbol: "FB", TickerName: "Meta Platforms", ShareCount: 0.0863914000, SharePrice: 362, ShareCurrency: "USD", ExchangeRate: "1.17437", Total: 26.67, ID: "EOF1", FXFee: 0.04},
{Action: "Market buy", Time: DateTime{Time: time.Date(2021, 8, 9, 18, 31, 41, 0, time.UTC)}, ISIN: "US88160R1014", TickerSymbol: "TSLA", TickerName: "Tesla", ShareCount: 0.0766547000, SharePrice: 713.93, ShareCurrency: "USD", ExchangeRate: "1.17437", Total: 46.67, ID: "EOF2", FXFee: 0.07},
{Action: "Market buy", Time: DateTime{Time: time.Date(2021, 8, 9, 18, 31, 41, 0, time.UTC)}, ISIN: "US5949181045", TickerSymbol: "MSFT", TickerName: "Microsoft", ShareCount: 0.2709950000, SharePrice: 288.53, ShareCurrency: "USD", ExchangeRate: "1.17437", Total: 66.68, ID: "EOF3", FXFee: 0.10},
{Action: "Market sell", Time: DateTime{Time: time.Date(2021, 8, 30, 13, 30, 3, 0, time.UTC)}, ISIN: "US5949181045", TickerSymbol: "MSFT", TickerName: "Microsoft", ShareCount: 0.2709950000, SharePrice: 301.14, ShareCurrency: "USD", ExchangeRate: "1.17951", Result: 2.61, Total: 69.09, ID: "EOF4", FXFee: 0.10},
{Action: "Deposit", Time: DateTime{Time: time.Date(2021, 9, 7, 13, 43, 10, 0, time.UTC)}, Total: 1000, Notes: "Transaction ID: xxx", ID: "3e8f5274-1c62-46d6-baf4-xxx"},
{Action: "Deposit", Time: DateTime{Time: time.Date(2021, 9, 7, 13, 43, 10, 0, time.UTC)}, ChargeAmount: 1001.4, DepositFee: 1.4, Total: 1000, Notes: "Transaction ID: xxx", ID: "3e8f5274-1c62-46d6-baf4-xxx"},
{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},
Expand Down
62 changes: 41 additions & 21 deletions trading212/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,50 @@ func (e *TradeEvent) IsDividend() bool {
}

func (e *TradeEvent) Fees() float64 {
return e.FXFee + e.FRFee + e.StampDuty + e.StampDutyTax + e.FinraFee
return e.FXFee + e.FRFee + e.StampDuty + e.StampDutyTax + e.FinraFee + e.DepositFee
}

func (e *TradeEvent) IsSkippable() bool {
return e.Action == "Currency conversion"
}

// IsInterest marks if an event is considered the addition of interest
func (e *TradeEvent) IsInterest() bool {
return e.Action == "Interest on cash" ||
e.Action == "Lending interest"
}

func (e *TradeEvent) IsMoneyWithdrawal() bool {
return e.Action == "Card debit" || e.Action == "New card cost"
}

type TradeEvent struct {
Action string `csv:"Action"`
Time DateTime `csv:"Time"`
ISIN string `csv:"ISIN"`
TickerSymbol string `csv:"Ticker"`
TickerName string `csv:"Name"`
ShareCount float64 `csv:"No. of shares,omitempty"`
SharePrice float64 `csv:"Price / share,omitempty"`
ShareCurrency string `csv:"Currency (Price / share)"`
ExchangeRate string `csv:"Exchange rate,omitempty"`
Result float64 `csv:"Result,omitempty"` // gain or loss
Total float64 `csv:"Total,omitempty"` // total money gained
Tax float64 `csv:"Withholding tax,omitempty"`
TaxCurrency string `csv:"Currency (Withholding tax)"`
StampDuty float64 `csv:"Stamp duty,omitempty"`
StampDutyTax float64 `csv:"Stamp duty reserve tax,omitempty"`
Notes string `csv:"Notes"`
ID string `csv:"ID"`
FXFee float64 `csv:"Currency conversion fee,omitempty"` // foreign exchange fee
FRFee float64 `csv:"French transaction tax,omitempty"`
FinraFee float64 `csv:"Finra fee,omitempty"`
Action string `csv:"Action"`
Time DateTime `csv:"Time"`
ISIN string `csv:"ISIN"`
TickerSymbol string `csv:"Ticker"`
TickerName string `csv:"Name"`
ShareCount float64 `csv:"No. of shares,omitempty"`
SharePrice float64 `csv:"Price / share,omitempty"`
ShareCurrency string `csv:"Currency (Price / share)"`
ExchangeRate string `csv:"Exchange rate,omitempty"`
ChargeAmount float64 `csv:"Charge amount,omitempty"`
DepositFee float64 `csv:"Deposit fee,omitempty"`
Result float64 `csv:"Result,omitempty"` // gain or loss
ResultCurrency string `csv:"Currency (Result)"`
Total float64 `csv:"Total,omitempty"` // total money gained
TotalCurrency string `csv:"Currency (Total)"`
Tax float64 `csv:"Withholding tax,omitempty"`
TaxCurrency string `csv:"Currency (Withholding tax)"`
StampDuty float64 `csv:"Stamp duty,omitempty"`
StampDutyTax float64 `csv:"Stamp duty reserve tax,omitempty"`
Notes string `csv:"Notes"`
ID string `csv:"ID"`
FXFee float64 `csv:"Currency conversion fee,omitempty"` // foreign exchange fee
FRFee float64 `csv:"French transaction tax,omitempty"`
FinraFee float64 `csv:"Finra fee,omitempty"`
MerchantName string `csv:"Merchant name,omitempty"`
MerchantCategory string `csv:"Merchant category,omitempty"`
}

type DateTime struct {
Expand Down

0 comments on commit bb8fb77

Please sign in to comment.