Compare commits

...

2 Commits

Author SHA1 Message Date
06dc25ae88 barebones pnl calculation 2025-11-10 09:40:17 +00:00
e208464cb0 initial abstraction from record reading 2025-11-07 16:41:21 +00:00

158
main.go
View File

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"log/slog" "log/slog"
"math/big"
"os" "os"
"strings" "strings"
) )
@@ -24,11 +25,11 @@ func run() error {
return fmt.Errorf("open statement: %w", err) return fmt.Errorf("open statement: %w", err)
} }
r := csv.NewReader(f) r := NewRecordReader(f)
assets := make(map[string]*list.List) assets := make(map[string]*list.List)
for { for {
record, err := r.Read() record, err := r.ReadRecord()
if err != nil { if err != nil {
if errors.Is(err, io.EOF) { if errors.Is(err, io.EOF) {
break break
@@ -36,35 +37,57 @@ func run() error {
return fmt.Errorf("read statement record: %w", err) return fmt.Errorf("read statement record: %w", err)
} }
switch strings.ToLower(record[0]) { switch record.Direction() {
case "market buy": case DirectionBuy:
lst, ok := assets[record[2]] lst, ok := assets[record.Symbol()]
if !ok { if !ok {
lst = list.New() lst = list.New()
assets[record[2]] = lst assets[record.Symbol()] = lst
} }
lst.PushBack(record[12]) lst.PushBack(record)
case "market sell": case DirectionSell:
lst, ok := assets[record[2]] lst, ok := assets[record.Symbol()]
if !ok { if !ok {
return ErrSellWithoutBuy return ErrSellWithoutBuy
} }
first := lst.Front() unmatchedQty := new(big.Float).Copy(record.Quantity())
if first == nil { zero := new(big.Float)
return ErrSellWithoutBuy
for unmatchedQty.Cmp(zero) > 0 {
front := lst.Front()
if front == nil {
return ErrSellWithoutBuy
}
next, ok := front.Value.(Record)
if !ok {
return fmt.Errorf("unexpected record type: %T", front)
}
var matchedQty *big.Float
if next.Quantity().Cmp(unmatchedQty) > 0 {
matchedQty = unmatchedQty
next.Quantity().Sub(next.Quantity(), unmatchedQty)
} else {
matchedQty = next.Quantity()
lst.Remove(front)
}
unmatchedQty.Sub(unmatchedQty, matchedQty)
sellValue := new(big.Float).Mul(matchedQty, record.Price())
buyValue := new(big.Float).Mul(matchedQty, next.Price())
realisedPnL := new(big.Float).Sub(sellValue, buyValue)
slog.Info("Realised PnL",
slog.Any("Symbol", record.Symbol()),
slog.Any("PnL", realisedPnL))
} }
slog.Info("Realised PnL", slog.Any("record", record))
case "action", "stock split open", "stock split close":
// ignored
default: default:
return fmt.Errorf("unhandled record: %s", record[0]) return fmt.Errorf("unknown direction: %s", record.Direction())
} }
} }
slog.Info("Finish processing statement", slog.Any("assets_count", len(assets))) slog.Info("Finish processing statement", slog.Any("assets_count", len(assets)))
@@ -73,3 +96,100 @@ func run() error {
} }
var ErrSellWithoutBuy = fmt.Errorf("found sell without bought volume") var ErrSellWithoutBuy = fmt.Errorf("found sell without bought volume")
type Record struct {
symbol string
direction Direction
quantity *big.Float
price *big.Float
}
func (r Record) Symbol() string {
return r.symbol
}
func (r Record) Direction() Direction {
return r.direction
}
func (r Record) Quantity() *big.Float {
return r.quantity
}
func (r Record) Price() *big.Float {
return r.price
}
type RecordReader struct {
reader *csv.Reader
}
func NewRecordReader(r io.Reader) *RecordReader {
return &RecordReader{
reader: csv.NewReader(r),
}
}
func (rr RecordReader) ReadRecord() (Record, error) {
for {
raw, err := rr.reader.Read()
if err != nil {
return Record{}, fmt.Errorf("read record: %w", err)
}
var dir Direction
switch strings.ToLower(raw[0]) {
case "market buy":
dir = DirectionBuy
case "market sell":
dir = DirectionSell
case "action", "stock split open", "stock split close":
continue
default:
return Record{}, fmt.Errorf("unhandled record: %s", raw[0])
}
qant, _, err := big.ParseFloat(raw[6], 10, 20, big.ToZero)
if err != nil {
return Record{}, fmt.Errorf("parse quantity: %w", err)
}
price, _, err := big.ParseFloat(raw[7], 10, 20, big.ToZero)
if err != nil {
return Record{}, fmt.Errorf("parse price: %w", err)
}
return Record{
symbol: raw[2],
direction: dir,
quantity: qant,
price: price,
}, nil
}
}
type Direction uint
const (
DirectionUnknown Direction = 0
DirectionBuy = 1
DirectionSell = 2
)
func (d Direction) String() string {
switch d {
case 1:
return "buy"
case 2:
return "sell"
default:
return "unknown"
}
}
func (d Direction) IsBuy() bool {
return d == DirectionBuy
}
func (d Direction) IsSell() bool {
return d == DirectionSell
}