Compare commits
2 Commits
c109e68a7a
...
06dc25ae88
| Author | SHA1 | Date | |
|---|---|---|---|
| 06dc25ae88 | |||
| e208464cb0 |
158
main.go
158
main.go
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"math/big"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
@@ -24,11 +25,11 @@ func run() error {
|
||||
return fmt.Errorf("open statement: %w", err)
|
||||
}
|
||||
|
||||
r := csv.NewReader(f)
|
||||
r := NewRecordReader(f)
|
||||
|
||||
assets := make(map[string]*list.List)
|
||||
for {
|
||||
record, err := r.Read()
|
||||
record, err := r.ReadRecord()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
@@ -36,35 +37,57 @@ func run() error {
|
||||
return fmt.Errorf("read statement record: %w", err)
|
||||
}
|
||||
|
||||
switch strings.ToLower(record[0]) {
|
||||
case "market buy":
|
||||
lst, ok := assets[record[2]]
|
||||
switch record.Direction() {
|
||||
case DirectionBuy:
|
||||
lst, ok := assets[record.Symbol()]
|
||||
if !ok {
|
||||
lst = list.New()
|
||||
assets[record[2]] = lst
|
||||
assets[record.Symbol()] = lst
|
||||
}
|
||||
lst.PushBack(record[12])
|
||||
lst.PushBack(record)
|
||||
|
||||
case "market sell":
|
||||
lst, ok := assets[record[2]]
|
||||
case DirectionSell:
|
||||
lst, ok := assets[record.Symbol()]
|
||||
if !ok {
|
||||
return ErrSellWithoutBuy
|
||||
}
|
||||
|
||||
first := lst.Front()
|
||||
if first == nil {
|
||||
return ErrSellWithoutBuy
|
||||
unmatchedQty := new(big.Float).Copy(record.Quantity())
|
||||
zero := new(big.Float)
|
||||
|
||||
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:
|
||||
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)))
|
||||
@@ -73,3 +96,100 @@ func run() error {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user