Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto import trade ID and transaction notes #31

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
transactions.csv
open-trades.json

# Binaries for programs and plugins
*.exe
*.exe~
Expand Down
9 changes: 8 additions & 1 deletion cmd/transaction_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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 {
Expand Down
131 changes: 124 additions & 7 deletions parse/journal.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package parse

import (
"encoding/csv"
"encoding/json"
"fmt"
"io"
"log"
Expand All @@ -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
Expand Down Expand Up @@ -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{}
}
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Loading