diff --git a/.gitignore b/.gitignore index d1baad6..e508b9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +transactions.csv +open-trades.json + # Binaries for programs and plugins *.exe *.exe~ diff --git a/cmd/transaction_reader.go b/cmd/transaction_reader.go index c101824..fe21af4 100644 --- a/cmd/transaction_reader.go +++ b/cmd/transaction_reader.go @@ -7,9 +7,10 @@ import ( "os" ) -// usage: go run cmd/transaction_reader.go --data "./testdata/input/1-dmc.csv" +// usage: go run cmd/transaction_reader.go --data "./testdata/input/1-dmc.csv" --open "./testdata/input/open-trades.json" func main() { dataFlag := flag.String("data", "", "Path to CSV data.") + openFlag := flag.String("open", "", "Path to open trades JSON file.") flag.Parse() @@ -18,8 +19,14 @@ func main() { os.Exit(1) } + if *openFlag == "" { + flag.PrintDefaults() + os.Exit(1) + } + journal := parse.NewJournal() transactions := journal.ReadTransactions(*dataFlag) + transactions = journal.AddOpenTradesData(*openFlag, transactions) journal.ToCsv(transactions) for i, transaction := range transactions { diff --git a/parse/journal.go b/parse/journal.go index 2a98fa1..cb34f1d 100644 --- a/parse/journal.go +++ b/parse/journal.go @@ -2,6 +2,7 @@ package parse import ( "encoding/csv" + "encoding/json" "fmt" "io" "log" @@ -11,7 +12,11 @@ import ( "strings" ) +const CALLED_AWAY_NOTE = "called away for profit" +const HIT_GTC_TARGET_NOTE = "hit GTC target" + type Transaction struct { + id string // trade id will be imported from a separate JSON file ticker string account string date string @@ -45,6 +50,16 @@ type Journal struct { trades map[string][]Transaction } +// OpenTrade represents the data associated with each open trade that we want to add to an exported transaction. +// This will eliminate the need to copy / paste properties of open trades such as the trade ID and notes +type OpenTrade struct { + ID string `json:"ID"` + Ticker string `json:"ticker"` + Account string `json:"account"` + Notes string `json:"notes"` + Matched bool // keeps track if this entry was previously matched so we don't duplicate Notes property for subsequent entries. +} + func NewJournal() Journal { return Journal{} } @@ -254,7 +269,7 @@ func (j *Journal) ReadTransactions(csvPath string) []Transaction { // short call option assignments (i.e. short calls called away) will have a price of 0 case "C": singleTransaction.actionModified = "Trade - Option - Assignment" - singleTransaction.notes = "called away for profit" + singleTransaction.notes = CALLED_AWAY_NOTE // long put option exercises will have a price of 0 case "P": @@ -292,8 +307,8 @@ func (j *Journal) ReadTransactions(csvPath string) []Transaction { costBasisPerShare := costBasisTotal / shares // always round to 8 decimal places - Go sometimes is slightly off in decimal calculations singleTransaction.costBasisShare = fmt.Sprintf("%.8f", costBasisPerShare) - singleTransaction.notes = "hit GTC target" - transaction.notes = "hit GTC target" + singleTransaction.notes = HIT_GTC_TARGET_NOTE + transaction.notes = HIT_GTC_TARGET_NOTE j.updateSingleTransaction(transaction.ticker, *singleTransaction) } @@ -411,6 +426,25 @@ func (j *Journal) findSingleTransaction(ticker string, action string) *Transacti return &(matchedTransactions)[0] } +func (j *Journal) loadTickerMap() map[string]*OpenTrade { + data, err := os.ReadFile("tickers.json") + if err != nil { + log.Fatal("Error reading file:", err) + } + + // Create a map to store openTickers by their ticker symbols + tickerMap := make(map[string]*OpenTrade) + + // Unmarshal the JSON data into a slice of Ticker structs + var openTickers []OpenTrade + err = json.Unmarshal(data, &openTickers) + if err != nil { + log.Fatal("Error unmarshaling JSON:", err) + } + + return tickerMap +} + // update updateSingleTransaction to take another parameter of the Action to match so that can have multiple transactions for the same ticker func (j *Journal) updateSingleTransaction(ticker string, transaction Transaction) { if j.trades == nil { @@ -421,9 +455,6 @@ func (j *Journal) updateSingleTransaction(ticker string, transaction Transaction if transactions == nil { panic(fmt.Sprintf("no transactions for ticker %s", ticker)) } - //if len(transactions) > 1 { - // panic(fmt.Sprintf("expected only 1 transaction for ticker %s but have %d", ticker, len(transactions))) - //} // loop over transactions and find the one with the action matchedTransactions := make([]Transaction, 0) @@ -462,7 +493,7 @@ func (j *Journal) ToCsv(txs []Transaction) { row = append(row, "") row = append(row, "") row = append(row, tx.ticker) - row = append(row, "") + row = append(row, tx.id) row = append(row, "") row = append(row, tx.optionContract) row = append(row, tx.buySell) @@ -504,3 +535,89 @@ func (j *Journal) ToCsv(txs []Transaction) { writer := csv.NewWriter(f) writer.WriteAll(txsStr) } + +// AddOpenTradesData appends to the list of transactions that will be imported by adding more data. This is to eliminate +// the need for copying and pasting data in the spreadsheet, so it's already there when transactions are imported. +// For example, the trade ID and notes are properties that aren't present in the original exported CSV from Interactive Brokers, +// but they need to be added to the general ledger of trades. +func (j *Journal) AddOpenTradesData(openTradesPath string, transactions []Transaction) []Transaction { + // load JSON data of open trades into a map + jsonData, err := os.ReadFile(openTradesPath) + if err != nil { + fmt.Println("Error reading file:", err) + return nil + } + + // parse the JSON data + var openTrades []OpenTrade + if err := json.Unmarshal(jsonData, &openTrades); err != nil { + fmt.Println("Error parsing JSON:", err) + return nil + } + + // initialize an empty map to store the openTrades. + /* + Map structure: + - ticker 1, account 1 + - ticker + - ID + - account + - notes + + - ticker 1, account 2 + - ticker + - ID + - account + - notes + + - ticker 2, account 2 + - ticker + - ID + - account + - notes + + - ticker 3, account 3 + - ticker + - ID + - account + - notes + */ + openTradesMap := make(map[[2]interface{}]OpenTrade) + + // create a unique key for each openTrade (ticker + account) + for _, openTrade := range openTrades { + key := [2]interface{}{openTrade.Ticker, openTrade.Account} + openTradesMap[key] = openTrade + } + + // loop over all L1 transactions and update the transactions that have matches in openTradesMap + for i, transaction := range transactions { + // for each L1 transaction, lookup that { ticker, account } in openTradesMap + lookupKey := [2]interface{}{transaction.ticker, transaction.account} + + if foundEntry, exists := openTradesMap[lookupKey]; exists { + // update transaction with additional properties - always update the ID property + transaction.id = foundEntry.ID + + // only update notes property for the first {ticker / account } match - don't want notes duplicated on subsequent matches + if !foundEntry.Matched { + // don't overwrite existing notes (e.g. dividend payment) + if transaction.notes == "" { + transaction.notes = foundEntry.Notes + } else if transaction.notes == CALLED_AWAY_NOTE { + // append to the note if called away + transaction.notes = foundEntry.Notes + "\n" + CALLED_AWAY_NOTE + } else if transaction.notes == HIT_GTC_TARGET_NOTE { + // append to the note if hit GTC target + transaction.notes = foundEntry.Notes + "\n" + HIT_GTC_TARGET_NOTE + } + foundEntry.Matched = true + // save updated entry in map, so won't update the notes on subsequent matches + openTradesMap[lookupKey] = foundEntry + } + // update transactions slice with new transaction values + transactions[i] = transaction + } + } + return transactions +} diff --git a/parse/journal_test.go b/parse/journal_test.go index d19bf88..9371143 100644 --- a/parse/journal_test.go +++ b/parse/journal_test.go @@ -6,364 +6,127 @@ import ( "github.com/stretchr/testify/require" ) -type TestData struct { - expectedTransactions []Transaction - filePath string +type TestDataL1 struct { + csvExportPath string // input + expectedL1Transactions []Transaction // expected output } -func TestReadTransactions(t *testing.T) { - expectedTransactions1 := []Transaction{ - { - date: "2022-11-25", - account: "TFSA", - action: "Trade", - ticker: "PR", - buySell: "Buy", - shares: "600", - price: "10.588333333", - proceeds: "-6353.00", - costBasisBuyOrOption: "-6355.999999799999", - costBasisTotal: "-6355.999999799999", - commission: "-3", - }, - { - date: "2022-11-25", - account: "TFSA", - action: "Trade - Option", - ticker: "PR", - optionContract: "20JAN23 9 C", - buySell: "Sell", - optionContracts: "-6", - price: "1.971666667", - proceeds: "1183.00", - costBasisShare: "0", - costBasisBuyOrOption: "1179.9809295", - commission: "-3.0190707", - }, - { - date: "2022-11-25", - account: "TFSA", - action: "Trade - Option", - ticker: "PR", - optionContract: "20JAN23 5 P", - buySell: "Buy", - optionContracts: "6", - price: "0.053333333", - proceeds: "-32.00", - costBasisShare: "0", - costBasisBuyOrOption: "-32.9788998", - commission: "-0.9789", - }, - } - - expectedTransactions2 := []Transaction{ - { - date: "2023-06-08", - account: "RRSP", - action: "Dividend", - ticker: "MSFT", - dividend: "136", - notes: "MSFT(US5949181045) Cash Dividend USD 0.68 per Share (Ordinary Dividend)", - }, - } - - expectedTransactions3 := []Transaction{ - { - date: "2023-06-05", - account: "Margin", - action: "Forex", - commission: "-2", - forexUSDBuy: "4,838.82", - forexUSDCAD: "1.3433", - forexCADSell: "-6499.986906", - notes: "converted all CAD to USD", - }, - { - date: "2023-06-05", - account: "Margin", - action: "Trade", - ticker: "TECK", - buySell: "Buy", - shares: "100", - price: "42.09", - proceeds: "-4209.00", - costBasisBuyOrOption: "-4209.370257250001", - costBasisTotal: "-4209.370257250001", - commission: "-0.37025725", - }, - { - date: "2023-06-05", - account: "Margin", - action: "Trade - Option", - ticker: "TECK", - optionContract: "21JUL23 38 C", - buySell: "Sell", - optionContracts: "-1", - price: "5.07", - proceeds: "507.00", - costBasisShare: "0", - costBasisBuyOrOption: "505.944454", - commission: "-1.055546", - }, - } +type TestDataL2 struct { + openTradesPath string // input + l1Transactions []Transaction // input - based on output from L1 process + expectedL2Transactions []Transaction // expected output +} - expectedTransactions4 := []Transaction{ - { - date: "2023-06-09", - account: "Margin", - action: "Dividend", - ticker: "SMG", - dividend: "66", - fee: "-9.9", - notes: "SMG(US8101861065) Payment in Lieu of Dividend (Ordinary Dividend)\n15% tax withdrawn", +func TestReadTransactions(t *testing.T) { + testDataMap := map[string]TestDataL1{ + "stock, short call, long put": { + expectedL1Transactions: expectedL1Transactions1, + csvExportPath: "../testdata/input/1-dmc.csv", }, - } - - expectedTransactions5 := []Transaction{ - { - date: "2023-06-08", - account: "RRSP", - action: "Trade - Option - Assignment", - actionModified: "Trade - Option - Assignment", - ticker: "FDX", - optionContract: "16JUN23 155 C", - buySell: "Sell", - shares: "-100", - price: "155", - proceeds: "15500.00", - costBasisShare: "-172.67370257", - costBasisTotal: "17267.370257", // import IBKR value and multiply by -1 - realizedPL: "3873.744617", // imports IBKR value - commission: "-0.1385", - notes: "called away for profit", + "dividend": { + expectedL1Transactions: expectedL1Transactions2, + csvExportPath: "../testdata/input/2-dividend.csv", }, - } - - expectedTransactions6 := []Transaction{ - { - date: "2023-06-08", - account: "TFSA", - action: "Trade - Close", - actionModified: "Trade - Close", - ticker: "BBWI", - buySell: "Sell", - shares: "-100", - price: "41.44", - proceeds: "4144.00", - costBasisShare: "-38.17000000", - costBasisBuyOrOption: "", - costBasisTotal: "3817", // imports IBKR value and multiply by -1 - realizedPL: "326.482091", // imports IBKR value - commission: "-0.51790925", - notes: "hit GTC target", + "forex": { + expectedL1Transactions: expectedL1Transactions3, + csvExportPath: "../testdata/input/3-forex.csv", }, - { - date: "2023-06-08", - account: "TFSA", - action: "Trade - Option", - ticker: "BBWI", - optionContract: "16JUN23 35 C", - buySell: "Buy", - optionContracts: "1", - price: "6.53", - proceeds: "-653.00", - costBasisShare: "0", - costBasisBuyOrOption: "-654.05155", - commission: "-1.05155", - notes: "hit GTC target", + "dividend - withholding tax": { + expectedL1Transactions: expectedL1Transactions4, + csvExportPath: "../testdata/input/4-dividend-withholding-tax.csv", }, - } - - expectedTransactions7 := []Transaction{ - // HPQ roll out - { - date: "2023-06-12", - account: "RRSP", - action: "Trade - Option", - ticker: "HPQ", - optionContract: "16JUN23 27 C", - buySell: "Buy", - optionContracts: "2", - price: "3.32", - proceeds: "-664.00", - costBasisShare: "0", - costBasisBuyOrOption: "-664.6581", - commission: "-0.6581", + "call assignment": { + expectedL1Transactions: expectedL1Transactions5, + csvExportPath: "../testdata/input/5-call-assignment.csv", }, - { - date: "2023-06-12", - account: "RRSP", - action: "Trade - Option", - ticker: "HPQ", - optionContract: "18AUG23 27 C", - buySell: "Sell", - optionContracts: "-2", - price: "3.62", - proceeds: "724.00", - costBasisShare: "0", - costBasisBuyOrOption: "723.331228", - commission: "-0.668772", + "hit target": { + expectedL1Transactions: expectedL1Transactions6, + csvExportPath: "../testdata/input/6-hit-target.csv", }, - - // STNG roll down - { - date: "2023-06-12", - account: "RRSP", - action: "Trade - Option", - ticker: "STNG", - optionContract: "21JUL23 46 C", - buySell: "Buy", - optionContracts: "1", - price: "1.97", - proceeds: "-197.00", - costBasisShare: "0", - costBasisBuyOrOption: "-197.64905", - commission: "-0.64905", + "roll out call, roll down call": { + expectedL1Transactions: expectedL1Transactions7, + csvExportPath: "../testdata/input/7-call-roll-out-roll-down.csv", }, - { - date: "2023-06-12", - account: "RRSP", - action: "Trade - Option", - ticker: "STNG", - optionContract: "21JUL23 44 C", - buySell: "Sell", - optionContracts: "-1", - price: "2.79", - proceeds: "279.00", - costBasisShare: "0", - costBasisBuyOrOption: "278.346278", - commission: "-0.653722", + "dividend - withholding tax + other transactions same ticker": { + expectedL1Transactions: expectedL1Transactions8, + csvExportPath: "../testdata/input/8-dividend-withholding-tax-other-tx.csv", }, - } - - expectedTransactions8 := []Transaction{ - { - date: "2023-06-15", - account: "TFSA", - action: "Trade - Option", - ticker: "MOS", - optionContract: "16JUN23 32.5 C", - buySell: "Buy", - optionContracts: "2", - price: "3.125", - proceeds: "-625.00", - costBasisShare: "0", - costBasisBuyOrOption: "-625.6581", - commission: "-0.6581", + "expired OTM put": { + expectedL1Transactions: expectedL1EmptyTransactions, + csvExportPath: "../testdata/input/9-lapsed-put.csv", }, - - { - date: "2023-06-15", - account: "TFSA", - action: "Trade - Option", - ticker: "MOS", - optionContract: "21JUL23 32.5 C", - buySell: "Sell", - optionContracts: "-2", - price: "3.825", - proceeds: "765.00", - costBasisShare: "0", - costBasisBuyOrOption: "764.3309", - commission: "-0.6691", + "expired OTM call, OTM puts": { + expectedL1Transactions: expectedL1EmptyTransactions, + csvExportPath: "../testdata/input/10-lapsed-call-puts.csv", }, - { - date: "2023-06-15", - account: "TFSA", - action: "Dividend", - ticker: "MOS", - dividend: "40", - fee: "-6", - notes: "MOS(US61945C1036) Cash Dividend USD 0.20 per Share (Ordinary Dividend)\n15% tax withdrawn", + "exercise put, lapsed call for same ticker": { + expectedL1Transactions: expectedL1Transactions9, + csvExportPath: "../testdata/input/11-exercise-put.csv", }, } - expectedTransactions9 := []Transaction{ - { - date: "2023-07-21", - account: "RRSP", - action: "Trade - Option - Exercise", - actionModified: "Trade - Option - Exercise", - ticker: "STNG", - optionContract: "21JUL23 50 P", - buySell: "Sell", - shares: "-100", - price: "50", - proceeds: "5000.00", - costBasisShare: "-62.61370257", - costBasisBuyOrOption: "", - costBasisTotal: "6261.370257", // imports IBKR value and multiply by -1 - realizedPL: "-1791.673807", // imports IBKR value - commission: "-0.0545", - notes: "exercised long put", - }, - { - date: "2023-07-21", - account: "RRSP", - action: "Trade - Option - Exercise", - actionModified: "Trade - Option - Exercise", - ticker: "TGT", - optionContract: "21JUL23 140 P", - buySell: "Sell", - shares: "-100", - price: "140", - proceeds: "14000.00", - costBasisShare: "-163.60370257", - costBasisBuyOrOption: "", - costBasisTotal: "16360.370257", // imports IBKR value and multiply by -1 - realizedPL: "-3011.535807", // imports IBKR value - commission: "-0.1265", - notes: "exercised long put", - }, - } + for k, testData := range testDataMap { + t.Run(k, func(t *testing.T) { + // read original csv file trade data + journal := NewJournal() + actualTransactions := journal.ReadTransactions(testData.csvExportPath) - expectedEmptyTransactions := []Transaction{ - // should be an empty array because the put option expired out of the money + require.ElementsMatch(t, testData.expectedL1Transactions, actualTransactions) + }) } +} - testDataMap := map[string]TestData{ +func TestAddOpenTickers(t *testing.T) { + testDataMap := map[string]TestDataL2{ "stock, short call, long put": { - expectedTransactions: expectedTransactions1, - filePath: "../testdata/input/1-dmc.csv", + openTradesPath: "../testdata/input/open-trades.json", + l1Transactions: expectedL1Transactions1, + expectedL2Transactions: expectedL2Transactions1, }, "dividend": { - expectedTransactions: expectedTransactions2, - filePath: "../testdata/input/2-dividend.csv", + openTradesPath: "../testdata/input/open-trades.json", + l1Transactions: expectedL1Transactions2, + expectedL2Transactions: expectedL2Transactions2, }, "forex": { - expectedTransactions: expectedTransactions3, - filePath: "../testdata/input/3-forex.csv", + openTradesPath: "../testdata/input/open-trades.json", + l1Transactions: expectedL1Transactions3, + expectedL2Transactions: expectedL2Transactions3, }, "dividend - withholding tax": { - expectedTransactions: expectedTransactions4, - filePath: "../testdata/input/4-dividend-withholding-tax.csv", + openTradesPath: "../testdata/input/open-trades.json", + l1Transactions: expectedL1Transactions4, + expectedL2Transactions: expectedL2Transactions4, }, "call assignment": { - expectedTransactions: expectedTransactions5, - filePath: "../testdata/input/5-call-assignment.csv", + openTradesPath: "../testdata/input/open-trades.json", + l1Transactions: expectedL1Transactions5, + expectedL2Transactions: expectedL2Transactions5, }, "hit target": { - expectedTransactions: expectedTransactions6, - filePath: "../testdata/input/6-hit-target.csv", + openTradesPath: "../testdata/input/open-trades.json", + l1Transactions: expectedL1Transactions6, + expectedL2Transactions: expectedL2Transactions6, }, "roll out call, roll down call": { - expectedTransactions: expectedTransactions7, - filePath: "../testdata/input/7-call-roll-out-roll-down.csv", + openTradesPath: "../testdata/input/open-trades.json", + l1Transactions: expectedL1Transactions7, + expectedL2Transactions: expectedL2Transactions7, }, "dividend - withholding tax + other transactions same ticker": { - expectedTransactions: expectedTransactions8, - filePath: "../testdata/input/8-dividend-withholding-tax-other-tx.csv", + openTradesPath: "../testdata/input/open-trades.json", + l1Transactions: expectedL1Transactions8, + expectedL2Transactions: expectedL2Transactions8, }, - "expired OTM put": { - expectedTransactions: expectedEmptyTransactions, - filePath: "../testdata/input/9-lapsed-put.csv", + "exercise put, lapsed call for same ticker": { + openTradesPath: "../testdata/input/open-trades.json", + l1Transactions: expectedL1Transactions9, + expectedL2Transactions: expectedL2Transactions9, }, "expired OTM call, OTM puts": { - expectedTransactions: expectedEmptyTransactions, - filePath: "../testdata/input/10-lapsed-call-puts.csv", - }, - "exercise put, lapsed call for same ticker": { - expectedTransactions: expectedTransactions9, - filePath: "../testdata/input/11-exercise-put.csv", + openTradesPath: "../testdata/input/open-trades.json", + l1Transactions: expectedL1EmptyTransactions, + expectedL2Transactions: expectedL1EmptyTransactions, }, } @@ -371,9 +134,10 @@ func TestReadTransactions(t *testing.T) { t.Run(k, func(t *testing.T) { // read original csv file trade data journal := NewJournal() - actualTransactions := journal.ReadTransactions(testData.filePath) + actualTransactions := journal.AddOpenTradesData(testData.openTradesPath, testData.l1Transactions) - require.ElementsMatch(t, testData.expectedTransactions, actualTransactions) + require.ElementsMatch(t, testData.expectedL2Transactions, actualTransactions) }) } + } diff --git a/parse/test_data.go b/parse/test_data.go new file mode 100644 index 0000000..3b8c269 --- /dev/null +++ b/parse/test_data.go @@ -0,0 +1,639 @@ +package parse + +var expectedL1Transactions1 = []Transaction{ + { + date: "2022-11-25", + account: "TFSA", + action: "Trade", + ticker: "PR", + buySell: "Buy", + shares: "600", + price: "10.588333333", + proceeds: "-6353.00", + costBasisBuyOrOption: "-6355.999999799999", + costBasisTotal: "-6355.999999799999", + commission: "-3", + }, + { + date: "2022-11-25", + account: "TFSA", + action: "Trade - Option", + ticker: "PR", + optionContract: "20JAN23 9 C", + buySell: "Sell", + optionContracts: "-6", + price: "1.971666667", + proceeds: "1183.00", + costBasisShare: "0", + costBasisBuyOrOption: "1179.9809295", + commission: "-3.0190707", + }, + { + date: "2022-11-25", + account: "TFSA", + action: "Trade - Option", + ticker: "PR", + optionContract: "20JAN23 5 P", + buySell: "Buy", + optionContracts: "6", + price: "0.053333333", + proceeds: "-32.00", + costBasisShare: "0", + costBasisBuyOrOption: "-32.9788998", + commission: "-0.9789", + }, +} + +var expectedL2Transactions1 = []Transaction{ + { + id: "MCDA278", + date: "2022-11-25", + account: "TFSA", + action: "Trade", + ticker: "PR", + buySell: "Buy", + shares: "600", + price: "10.588333333", + proceeds: "-6353.00", + costBasisBuyOrOption: "-6355.999999799999", + costBasisTotal: "-6355.999999799999", + commission: "-3", + notes: "\"stock screen: Div Upcoming (15D)\ncc screen: OTM-60 Day 0.1 4+%\nex-div 3/22 0.20 earn 5/1\n3/25 48.69 50GTCp5c\"", + }, + { + id: "MCDA278", + date: "2022-11-25", + account: "TFSA", + action: "Trade - Option", + ticker: "PR", + optionContract: "20JAN23 9 C", + buySell: "Sell", + optionContracts: "-6", + price: "1.971666667", + proceeds: "1183.00", + costBasisShare: "0", + costBasisBuyOrOption: "1179.9809295", + commission: "-3.0190707", + notes: "", + }, + { + id: "MCDA278", + date: "2022-11-25", + account: "TFSA", + action: "Trade - Option", + ticker: "PR", + optionContract: "20JAN23 5 P", + buySell: "Buy", + optionContracts: "6", + price: "0.053333333", + proceeds: "-32.00", + costBasisShare: "0", + costBasisBuyOrOption: "-32.9788998", + commission: "-0.9789", + notes: "", + }, +} + +var expectedL1Transactions2 = []Transaction{ + { + date: "2023-06-08", + account: "RRSP", + ticker: "MSFT", + action: "Dividend", + dividend: "136", + notes: "MSFT(US5949181045) Cash Dividend USD 0.68 per Share (Ordinary Dividend)", + }, +} + +var expectedL2Transactions2 = []Transaction{ + { + id: "MCDA279", + date: "2023-06-08", + account: "RRSP", + ticker: "MSFT", + action: "Dividend", + dividend: "136", + notes: "MSFT(US5949181045) Cash Dividend USD 0.68 per Share (Ordinary Dividend)", + }, +} + +var expectedL1Transactions3 = []Transaction{ + { + date: "2023-06-05", + account: "Margin", + action: "Forex", + commission: "-2", + forexUSDBuy: "4,838.82", + forexUSDCAD: "1.3433", + forexCADSell: "-6499.986906", + notes: "converted all CAD to USD", + }, + { + date: "2023-06-05", + account: "Margin", + action: "Trade", + ticker: "TECK", + buySell: "Buy", + shares: "100", + price: "42.09", + proceeds: "-4209.00", + costBasisBuyOrOption: "-4209.370257250001", + costBasisTotal: "-4209.370257250001", + commission: "-0.37025725", + }, + { + date: "2023-06-05", + account: "Margin", + action: "Trade - Option", + ticker: "TECK", + optionContract: "21JUL23 38 C", + buySell: "Sell", + optionContracts: "-1", + price: "5.07", + proceeds: "507.00", + costBasisShare: "0", + costBasisBuyOrOption: "505.944454", + commission: "-1.055546", + }, +} + +var expectedL2Transactions3 = []Transaction{ + { + date: "2023-06-05", + account: "Margin", + action: "Forex", + commission: "-2", + forexUSDBuy: "4,838.82", + forexUSDCAD: "1.3433", + forexCADSell: "-6499.986906", + notes: "converted all CAD to USD", + }, + { + id: "ABC288", + date: "2023-06-05", + account: "Margin", + action: "Trade", + ticker: "TECK", + buySell: "Buy", + shares: "100", + price: "42.09", + proceeds: "-4209.00", + costBasisBuyOrOption: "-4209.370257250001", + costBasisTotal: "-4209.370257250001", + commission: "-0.37025725", + notes: "\"stock screen: Div Upcoming (15D)\ncc screen: OTM-60 Day 0.1 4+%\nex-div 3/27 0.19 earn 5/2\n3/27 50GTCp5c 10.80\"", + }, + { + id: "ABC288", + date: "2023-06-05", + account: "Margin", + action: "Trade - Option", + ticker: "TECK", + optionContract: "21JUL23 38 C", + buySell: "Sell", + optionContracts: "-1", + price: "5.07", + proceeds: "507.00", + costBasisShare: "0", + costBasisBuyOrOption: "505.944454", + commission: "-1.055546", + notes: "", + }, +} + +var expectedL1Transactions4 = []Transaction{ + { + date: "2023-06-09", + account: "Margin", + action: "Dividend", + ticker: "SMG", + dividend: "66", + fee: "-9.9", + notes: "SMG(US8101861065) Payment in Lieu of Dividend (Ordinary Dividend)\n15% tax withdrawn", + }, +} + +var expectedL2Transactions4 = []Transaction{ + { + id: "DEF985", + date: "2023-06-09", + account: "Margin", + action: "Dividend", + ticker: "SMG", + dividend: "66", + fee: "-9.9", + notes: "SMG(US8101861065) Payment in Lieu of Dividend (Ordinary Dividend)\n15% tax withdrawn", + }, +} + +var expectedL1Transactions5 = []Transaction{ + { + date: "2023-06-08", + account: "RRSP", + action: "Trade - Option - Assignment", + actionModified: "Trade - Option - Assignment", + ticker: "FDX", + optionContract: "16JUN23 155 C", + buySell: "Sell", + shares: "-100", + price: "155", + proceeds: "15500.00", + costBasisShare: "-172.67370257", + costBasisTotal: "17267.370257", // import IBKR value and multiply by -1 + realizedPL: "3873.744617", // imports IBKR value + commission: "-0.1385", + notes: "called away for profit", + }, +} + +var expectedL2Transactions5 = []Transaction{ + { + id: "ZXYK999", + date: "2023-06-08", + account: "RRSP", + action: "Trade - Option - Assignment", + actionModified: "Trade - Option - Assignment", + ticker: "FDX", + optionContract: "16JUN23 155 C", + buySell: "Sell", + shares: "-100", + price: "155", + proceeds: "15500.00", + costBasisShare: "-172.67370257", + costBasisTotal: "17267.370257", // import IBKR value and multiply by -1 + realizedPL: "3873.744617", // imports IBKR value + commission: "-0.1385", + notes: "195.00 ex-div tom, 1/31 187.90 - price jumped sharply up yesterday, created GTC roll up to 19Apr 145C 2.50DB to squeeze out 250 more (currently 147 profit if called away, CA)\n3/26 195.00 ex-div tom, 0 TP; ok to get called away for small profit (147)\ncalled away for profit", + }, +} + +var expectedL1Transactions6 = []Transaction{ + { + date: "2023-06-08", + account: "TFSA", + action: "Trade - Close", + actionModified: "Trade - Close", + ticker: "BBWI", + buySell: "Sell", + shares: "-100", + price: "41.44", + proceeds: "4144.00", + costBasisShare: "-38.17000000", + costBasisBuyOrOption: "", + costBasisTotal: "3817", // imports IBKR value and multiply by -1 + realizedPL: "326.482091", // imports IBKR value + commission: "-0.51790925", + notes: "hit GTC target", + }, + { + date: "2023-06-08", + account: "TFSA", + action: "Trade - Option", + ticker: "BBWI", + optionContract: "16JUN23 35 C", + buySell: "Buy", + optionContracts: "1", + price: "6.53", + proceeds: "-653.00", + costBasisShare: "0", + costBasisBuyOrOption: "-654.05155", + commission: "-1.05155", + notes: "hit GTC target", + }, +} + +var expectedL2Transactions6 = []Transaction{ + { + id: "1234", + date: "2023-06-08", + account: "TFSA", + action: "Trade - Close", + actionModified: "Trade - Close", + ticker: "BBWI", + buySell: "Sell", + shares: "-100", + price: "41.44", + proceeds: "4144.00", + costBasisShare: "-38.17000000", + costBasisBuyOrOption: "", + costBasisTotal: "3817", // imports IBKR value and multiply by -1 + realizedPL: "326.482091", // imports IBKR value + commission: "-0.51790925", + notes: "3/25 50GTCp5c 86.87\nhit GTC target", + }, + { + id: "1234", + date: "2023-06-08", + account: "TFSA", + action: "Trade - Option", + ticker: "BBWI", + optionContract: "16JUN23 35 C", + buySell: "Buy", + optionContracts: "1", + price: "6.53", + proceeds: "-653.00", + costBasisShare: "0", + costBasisBuyOrOption: "-654.05155", + commission: "-1.05155", + notes: "hit GTC target", + }, +} + +var expectedL1Transactions7 = []Transaction{ + // HPQ roll out + { + date: "2023-06-12", + account: "RRSP", + action: "Trade - Option", + ticker: "HPQ", + optionContract: "16JUN23 27 C", + buySell: "Buy", + optionContracts: "2", + price: "3.32", + proceeds: "-664.00", + costBasisShare: "0", + costBasisBuyOrOption: "-664.6581", + commission: "-0.6581", + }, + { + date: "2023-06-12", + account: "RRSP", + action: "Trade - Option", + ticker: "HPQ", + optionContract: "18AUG23 27 C", + buySell: "Sell", + optionContracts: "-2", + price: "3.62", + proceeds: "724.00", + costBasisShare: "0", + costBasisBuyOrOption: "723.331228", + commission: "-0.668772", + }, + + // STNG roll down + { + date: "2023-06-12", + account: "RRSP", + action: "Trade - Option", + ticker: "STNG", + optionContract: "21JUL23 46 C", + buySell: "Buy", + optionContracts: "1", + price: "1.97", + proceeds: "-197.00", + costBasisShare: "0", + costBasisBuyOrOption: "-197.64905", + commission: "-0.64905", + }, + { + date: "2023-06-12", + account: "RRSP", + action: "Trade - Option", + ticker: "STNG", + optionContract: "21JUL23 44 C", + buySell: "Sell", + optionContracts: "-1", + price: "2.79", + proceeds: "279.00", + costBasisShare: "0", + costBasisBuyOrOption: "278.346278", + commission: "-0.653722", + }, +} + +var expectedL2Transactions7 = []Transaction{ + // HPQ roll out + { + id: "MR06", + date: "2023-06-12", + account: "RRSP", + action: "Trade - Option", + ticker: "HPQ", + optionContract: "16JUN23 27 C", + buySell: "Buy", + optionContracts: "2", + price: "3.32", + proceeds: "-664.00", + costBasisShare: "0", + costBasisBuyOrOption: "-664.6581", + commission: "-0.6581", + notes: "Rolled out to 18Aug23 for 0.30CR", + }, + { + id: "MR06", + date: "2023-06-12", + account: "RRSP", + action: "Trade - Option", + ticker: "HPQ", + optionContract: "18AUG23 27 C", + buySell: "Sell", + optionContracts: "-2", + price: "3.62", + proceeds: "724.00", + costBasisShare: "0", + costBasisBuyOrOption: "723.331228", + commission: "-0.668772", + }, + + // STNG roll down + { + id: "MCDO653", + date: "2023-06-12", + account: "RRSP", + action: "Trade - Option", + ticker: "STNG", + optionContract: "21JUL23 46 C", + buySell: "Buy", + optionContracts: "1", + price: "1.97", + proceeds: "-197.00", + costBasisShare: "0", + costBasisBuyOrOption: "-197.64905", + commission: "-0.64905", + notes: "6/12 40.59 rolled down to 21Jul23 44C for 0.82CR", + }, + { + id: "MCDO653", + date: "2023-06-12", + account: "RRSP", + action: "Trade - Option", + ticker: "STNG", + optionContract: "21JUL23 44 C", + buySell: "Sell", + optionContracts: "-1", + price: "2.79", + proceeds: "279.00", + costBasisShare: "0", + costBasisBuyOrOption: "278.346278", + commission: "-0.653722", + }, +} + +var expectedL1Transactions8 = []Transaction{ + { + date: "2023-06-15", + account: "TFSA", + action: "Trade - Option", + ticker: "MOS", + optionContract: "16JUN23 32.5 C", + buySell: "Buy", + optionContracts: "2", + price: "3.125", + proceeds: "-625.00", + costBasisShare: "0", + costBasisBuyOrOption: "-625.6581", + commission: "-0.6581", + }, + + { + date: "2023-06-15", + account: "TFSA", + action: "Trade - Option", + ticker: "MOS", + optionContract: "21JUL23 32.5 C", + buySell: "Sell", + optionContracts: "-2", + price: "3.825", + proceeds: "765.00", + costBasisShare: "0", + costBasisBuyOrOption: "764.3309", + commission: "-0.6691", + }, + { + date: "2023-06-15", + account: "TFSA", + action: "Dividend", + ticker: "MOS", + dividend: "40", + fee: "-6", + notes: "MOS(US61945C1036) Cash Dividend USD 0.20 per Share (Ordinary Dividend)\n15% tax withdrawn", + }, +} + +var expectedL2Transactions8 = []Transaction{ + { + id: "MCDA462", + date: "2023-06-15", + account: "TFSA", + action: "Trade - Option", + ticker: "MOS", + optionContract: "16JUN23 32.5 C", + buySell: "Buy", + optionContracts: "2", + price: "3.125", + proceeds: "-625.00", + costBasisShare: "0", + costBasisBuyOrOption: "-625.6581", + commission: "-0.6581", + notes: "6/15 33.92, -180 if CA, rolled out to 21Jul 32.5C 2x0.70CR, no 31.5C strike so rolled up", + }, + { + id: "MCDA462", + date: "2023-06-15", + account: "TFSA", + action: "Trade - Option", + ticker: "MOS", + optionContract: "21JUL23 32.5 C", + buySell: "Sell", + optionContracts: "-2", + price: "3.825", + proceeds: "765.00", + costBasisShare: "0", + costBasisBuyOrOption: "764.3309", + commission: "-0.6691", + }, + { + id: "MCDA462", + date: "2023-06-15", + account: "TFSA", + action: "Dividend", + ticker: "MOS", + dividend: "40", + fee: "-6", + notes: "MOS(US61945C1036) Cash Dividend USD 0.20 per Share (Ordinary Dividend)\n15% tax withdrawn", + }, +} + +var expectedL1Transactions9 = []Transaction{ + { + date: "2023-07-21", + account: "RRSP", + action: "Trade - Option - Exercise", + actionModified: "Trade - Option - Exercise", + ticker: "STNG", + optionContract: "21JUL23 50 P", + buySell: "Sell", + shares: "-100", + price: "50", + proceeds: "5000.00", + costBasisShare: "-62.61370257", + costBasisBuyOrOption: "", + costBasisTotal: "6261.370257", // imports IBKR value and multiply by -1 + realizedPL: "-1791.673807", // imports IBKR value + commission: "-0.0545", + notes: "exercised long put", + }, + { + date: "2023-07-21", + account: "RRSP", + action: "Trade - Option - Exercise", + actionModified: "Trade - Option - Exercise", + ticker: "TGT", + optionContract: "21JUL23 140 P", + buySell: "Sell", + shares: "-100", + price: "140", + proceeds: "14000.00", + costBasisShare: "-163.60370257", + costBasisBuyOrOption: "", + costBasisTotal: "16360.370257", // imports IBKR value and multiply by -1 + realizedPL: "-3011.535807", // imports IBKR value + commission: "-0.1265", + notes: "exercised long put", + }, +} + +var expectedL2Transactions9 = []Transaction{ + { + id: "MCDO653", + date: "2023-07-21", + account: "RRSP", + action: "Trade - Option - Exercise", + actionModified: "Trade - Option - Exercise", + ticker: "STNG", + optionContract: "21JUL23 50 P", + buySell: "Sell", + shares: "-100", + price: "50", + proceeds: "5000.00", + costBasisShare: "-62.61370257", + costBasisBuyOrOption: "", + costBasisTotal: "6261.370257", // imports IBKR value and multiply by -1 + realizedPL: "-1791.673807", // imports IBKR value + commission: "-0.0545", + notes: "exercised long put", + }, + { + id: "MCDA419", + date: "2023-07-21", + account: "RRSP", + action: "Trade - Option - Exercise", + actionModified: "Trade - Option - Exercise", + ticker: "TGT", + optionContract: "21JUL23 140 P", + buySell: "Sell", + shares: "-100", + price: "140", + proceeds: "14000.00", + costBasisShare: "-163.60370257", + costBasisBuyOrOption: "", + costBasisTotal: "16360.370257", // imports IBKR value and multiply by -1 + realizedPL: "-3011.535807", // imports IBKR value + commission: "-0.1265", + notes: "exercised long put", + }, +} + +var expectedL1EmptyTransactions = []Transaction{ + // should be an empty array because the put option expired out of the money +} diff --git a/testdata/input/open-trades.json b/testdata/input/open-trades.json new file mode 100644 index 0000000..28ba7a0 --- /dev/null +++ b/testdata/input/open-trades.json @@ -0,0 +1,62 @@ +[ + { + "id": "MCDA278", + "ticker": "PR", + "account": "TFSA", + "notes": "\"stock screen: Div Upcoming (15D)\ncc screen: OTM-60 Day 0.1 4+%\nex-div 3/22 0.20 earn 5/1\n3/25 48.69 50GTCp5c\"" + }, + { + "id": "MCDA279", + "ticker": "MSFT", + "account": "RRSP", + "notes": "Microsoft Corporation" + }, + { + "id": "ABC288", + "ticker": "TECK", + "account": "Margin", + "notes": "\"stock screen: Div Upcoming (15D)\ncc screen: OTM-60 Day 0.1 4+%\nex-div 3/27 0.19 earn 5/2\n3/27 50GTCp5c 10.80\"" + }, + { + "id": "DEF985", + "ticker": "SMG", + "account": "Margin", + "notes": "foobar - should not replace notes" + }, + { + "id": "ZXYK999", + "ticker": "FDX", + "account": "RRSP", + "notes": "195.00 ex-div tom, 1/31 187.90 - price jumped sharply up yesterday, created GTC roll up to 19Apr 145C 2.50DB to squeeze out 250 more (currently 147 profit if called away, CA)\n3/26 195.00 ex-div tom, 0 TP; ok to get called away for small profit (147)" + }, + { + "id": "1234", + "ticker": "BBWI", + "account": "TFSA", + "notes": "3/25 50GTCp5c 86.87" + }, + { + "id": "MR06", + "ticker": "HPQ", + "account": "RRSP", + "notes": "Rolled out to 18Aug23 for 0.30CR" + }, + { + "id": "MCDO653", + "ticker": "STNG", + "account": "RRSP", + "notes": "6/12 40.59 rolled down to 21Jul23 44C for 0.82CR" + }, + { + "id": "MCDA462", + "ticker": "MOS", + "account": "TFSA", + "notes": "6/15 33.92, -180 if CA, rolled out to 21Jul 32.5C 2x0.70CR, no 31.5C strike so rolled up" + }, + { + "id": "MCDA419", + "ticker": "TGT", + "account": "RRSP", + "notes": "6/15 33.92, -180 if CA, rolled out to 21Jul 32.5C 2x0.70CR, no 31.5C strike so rolled up" + } +]