Compare commits
2 Commits
8e4093b647
...
7a38ae1696
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a38ae1696 | |||
| 38113f21af |
@@ -1,6 +1,9 @@
|
||||
# broker2anexoj
|
||||
|
||||
This tool converts the statements from brokers into a format compatible with the Portuguese IRS form: [Mod_3_anexo_j](https://info.portaldasfinancas.gov.pt/pt/apoio_contribuinte/modelos_formularios/irs/Documents/Mod_3_anexo_J.pdf)
|
||||
This tool converts the statements from brokers and exchanges into a format compatible with the Portuguese IRS form: [Mod_3_anexo_j](https://info.portaldasfinancas.gov.pt/pt/apoio_contribuinte/modelos_formularios/irs/Documents/Mod_3_anexo_J.pdf)
|
||||
|
||||
> [!WARNING]
|
||||
> Although I made significant efforts to ensure the correctness of the calculations you should verify any outputs produced by this tool on your own or with a certified accountant.
|
||||
|
||||
> [!NOTE]
|
||||
> This tool is in early stages of development. Use at your own risk!
|
||||
|
||||
2
go.mod
2
go.mod
@@ -1,3 +1,5 @@
|
||||
module git.naterciomoniz.net/applications/broker2anexoj
|
||||
|
||||
go 1.25.3
|
||||
|
||||
require go.uber.org/mock v0.6.0
|
||||
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
3
internal/generate.go
Normal file
3
internal/generate.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package internal
|
||||
|
||||
//go:generate mockgen -destination=mocks/mocks_gen.go -package=mocks -typed . RecordReader,Record
|
||||
296
internal/mocks/mocks_gen.go
Normal file
296
internal/mocks/mocks_gen.go
Normal file
@@ -0,0 +1,296 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: git.naterciomoniz.net/applications/broker2anexoj/internal (interfaces: RecordReader,Record)
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -destination=mocks/mocks_gen.go -package=mocks -typed . RecordReader,Record
|
||||
//
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
|
||||
import (
|
||||
big "math/big"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
internal "git.naterciomoniz.net/applications/broker2anexoj/internal"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockRecordReader is a mock of RecordReader interface.
|
||||
type MockRecordReader struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockRecordReaderMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockRecordReaderMockRecorder is the mock recorder for MockRecordReader.
|
||||
type MockRecordReaderMockRecorder struct {
|
||||
mock *MockRecordReader
|
||||
}
|
||||
|
||||
// NewMockRecordReader creates a new mock instance.
|
||||
func NewMockRecordReader(ctrl *gomock.Controller) *MockRecordReader {
|
||||
mock := &MockRecordReader{ctrl: ctrl}
|
||||
mock.recorder = &MockRecordReaderMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockRecordReader) EXPECT() *MockRecordReaderMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ReadRecord mocks base method.
|
||||
func (m *MockRecordReader) ReadRecord() (internal.Record, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadRecord")
|
||||
ret0, _ := ret[0].(internal.Record)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadRecord indicates an expected call of ReadRecord.
|
||||
func (mr *MockRecordReaderMockRecorder) ReadRecord() *MockRecordReaderReadRecordCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadRecord", reflect.TypeOf((*MockRecordReader)(nil).ReadRecord))
|
||||
return &MockRecordReaderReadRecordCall{Call: call}
|
||||
}
|
||||
|
||||
// MockRecordReaderReadRecordCall wrap *gomock.Call
|
||||
type MockRecordReaderReadRecordCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockRecordReaderReadRecordCall) Return(arg0 internal.Record, arg1 error) *MockRecordReaderReadRecordCall {
|
||||
c.Call = c.Call.Return(arg0, arg1)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockRecordReaderReadRecordCall) Do(f func() (internal.Record, error)) *MockRecordReaderReadRecordCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockRecordReaderReadRecordCall) DoAndReturn(f func() (internal.Record, error)) *MockRecordReaderReadRecordCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// MockRecord is a mock of Record interface.
|
||||
type MockRecord struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockRecordMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockRecordMockRecorder is the mock recorder for MockRecord.
|
||||
type MockRecordMockRecorder struct {
|
||||
mock *MockRecord
|
||||
}
|
||||
|
||||
// NewMockRecord creates a new mock instance.
|
||||
func NewMockRecord(ctrl *gomock.Controller) *MockRecord {
|
||||
mock := &MockRecord{ctrl: ctrl}
|
||||
mock.recorder = &MockRecordMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockRecord) EXPECT() *MockRecordMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Price mocks base method.
|
||||
func (m *MockRecord) Price() *big.Float {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Price")
|
||||
ret0, _ := ret[0].(*big.Float)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Price indicates an expected call of Price.
|
||||
func (mr *MockRecordMockRecorder) Price() *MockRecordPriceCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Price", reflect.TypeOf((*MockRecord)(nil).Price))
|
||||
return &MockRecordPriceCall{Call: call}
|
||||
}
|
||||
|
||||
// MockRecordPriceCall wrap *gomock.Call
|
||||
type MockRecordPriceCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockRecordPriceCall) Return(arg0 *big.Float) *MockRecordPriceCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockRecordPriceCall) Do(f func() *big.Float) *MockRecordPriceCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockRecordPriceCall) DoAndReturn(f func() *big.Float) *MockRecordPriceCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// Quantity mocks base method.
|
||||
func (m *MockRecord) Quantity() *big.Float {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Quantity")
|
||||
ret0, _ := ret[0].(*big.Float)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Quantity indicates an expected call of Quantity.
|
||||
func (mr *MockRecordMockRecorder) Quantity() *MockRecordQuantityCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Quantity", reflect.TypeOf((*MockRecord)(nil).Quantity))
|
||||
return &MockRecordQuantityCall{Call: call}
|
||||
}
|
||||
|
||||
// MockRecordQuantityCall wrap *gomock.Call
|
||||
type MockRecordQuantityCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockRecordQuantityCall) Return(arg0 *big.Float) *MockRecordQuantityCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockRecordQuantityCall) Do(f func() *big.Float) *MockRecordQuantityCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockRecordQuantityCall) DoAndReturn(f func() *big.Float) *MockRecordQuantityCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// Side mocks base method.
|
||||
func (m *MockRecord) Side() internal.Side {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Side")
|
||||
ret0, _ := ret[0].(internal.Side)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Side indicates an expected call of Side.
|
||||
func (mr *MockRecordMockRecorder) Side() *MockRecordSideCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Side", reflect.TypeOf((*MockRecord)(nil).Side))
|
||||
return &MockRecordSideCall{Call: call}
|
||||
}
|
||||
|
||||
// MockRecordSideCall wrap *gomock.Call
|
||||
type MockRecordSideCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockRecordSideCall) Return(arg0 internal.Side) *MockRecordSideCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockRecordSideCall) Do(f func() internal.Side) *MockRecordSideCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockRecordSideCall) DoAndReturn(f func() internal.Side) *MockRecordSideCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// Symbol mocks base method.
|
||||
func (m *MockRecord) Symbol() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Symbol")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Symbol indicates an expected call of Symbol.
|
||||
func (mr *MockRecordMockRecorder) Symbol() *MockRecordSymbolCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Symbol", reflect.TypeOf((*MockRecord)(nil).Symbol))
|
||||
return &MockRecordSymbolCall{Call: call}
|
||||
}
|
||||
|
||||
// MockRecordSymbolCall wrap *gomock.Call
|
||||
type MockRecordSymbolCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockRecordSymbolCall) Return(arg0 string) *MockRecordSymbolCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockRecordSymbolCall) Do(f func() string) *MockRecordSymbolCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockRecordSymbolCall) DoAndReturn(f func() string) *MockRecordSymbolCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// Timestamp mocks base method.
|
||||
func (m *MockRecord) Timestamp() time.Time {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Timestamp")
|
||||
ret0, _ := ret[0].(time.Time)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Timestamp indicates an expected call of Timestamp.
|
||||
func (mr *MockRecordMockRecorder) Timestamp() *MockRecordTimestampCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Timestamp", reflect.TypeOf((*MockRecord)(nil).Timestamp))
|
||||
return &MockRecordTimestampCall{Call: call}
|
||||
}
|
||||
|
||||
// MockRecordTimestampCall wrap *gomock.Call
|
||||
type MockRecordTimestampCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockRecordTimestampCall) Return(arg0 time.Time) *MockRecordTimestampCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockRecordTimestampCall) Do(f func() time.Time) *MockRecordTimestampCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockRecordTimestampCall) DoAndReturn(f func() time.Time) *MockRecordTimestampCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func (rq *RecordQueue) Push(r Record) {
|
||||
rq.l.PushBack(r)
|
||||
}
|
||||
|
||||
// Pop removes and returns the first element of the list as the first return value. If the list is
|
||||
// Pop removes and returns the first Record of the list as the first return value. If the list is
|
||||
// empty returns falso on the 2nd return value, true otherwise.
|
||||
func (rq *RecordQueue) Pop() (Record, bool) {
|
||||
if rq == nil || rq.l == nil {
|
||||
|
||||
130
internal/reporter.go
Normal file
130
internal/reporter.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"math/big"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type RecordReader interface {
|
||||
// ReadRecord should return Records until an error is found.
|
||||
ReadRecord() (Record, error)
|
||||
}
|
||||
|
||||
// Reporter consumes each record to produce ReportItem.
|
||||
type Reporter struct {
|
||||
reader RecordReader
|
||||
}
|
||||
|
||||
func NewReporter(rr RecordReader) *Reporter {
|
||||
return &Reporter{
|
||||
reader: rr,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reporter) Run() error {
|
||||
forewarders := make(map[string]chan Record)
|
||||
|
||||
aggregator := make(chan processResult)
|
||||
defer close(aggregator)
|
||||
|
||||
go func() {
|
||||
for result := range aggregator {
|
||||
fmt.Printf("%v\n", result)
|
||||
}
|
||||
}()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
defer func() {
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
for {
|
||||
rec, err := r.reader.ReadRecord()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
router, ok := forewarders[rec.Symbol()]
|
||||
if !ok {
|
||||
router = make(chan Record, 1)
|
||||
defer close(router)
|
||||
|
||||
wg.Go(func() {
|
||||
processRecords(router, aggregator)
|
||||
})
|
||||
|
||||
forewarders[rec.Symbol()] = router
|
||||
}
|
||||
|
||||
router <- rec
|
||||
}
|
||||
}
|
||||
|
||||
func processRecords(records <-chan Record, results chan<- processResult) {
|
||||
var q RecordQueue
|
||||
|
||||
for rec := range records {
|
||||
switch rec.Side() {
|
||||
case SideBuy:
|
||||
q.Push(rec)
|
||||
|
||||
case SideSell:
|
||||
unmatchedQty := new(big.Float).Copy(rec.Quantity())
|
||||
zero := new(big.Float)
|
||||
|
||||
for unmatchedQty.Cmp(zero) > 0 {
|
||||
buy, ok := q.Pop()
|
||||
if !ok {
|
||||
results <- processResult{
|
||||
err: ErrSellWithoutBuy,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var matchedQty *big.Float
|
||||
if buy.Quantity().Cmp(unmatchedQty) > 0 {
|
||||
matchedQty = unmatchedQty
|
||||
buy.Quantity().Sub(buy.Quantity(), unmatchedQty)
|
||||
} else {
|
||||
matchedQty = buy.Quantity()
|
||||
}
|
||||
|
||||
unmatchedQty.Sub(unmatchedQty, matchedQty)
|
||||
|
||||
sellValue := new(big.Float).Mul(matchedQty, rec.Price())
|
||||
buyValue := new(big.Float).Mul(matchedQty, buy.Price())
|
||||
realisedPnL := new(big.Float).Sub(sellValue, buyValue)
|
||||
slog.Info("Realised PnL",
|
||||
slog.Any("Symbol", rec.Symbol()),
|
||||
slog.Any("PnL", realisedPnL),
|
||||
slog.Any("Timestamp", rec.Timestamp()))
|
||||
|
||||
results <- processResult{
|
||||
item: ReportItem{},
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
results <- processResult{
|
||||
err: fmt.Errorf("unknown side: %v", rec.Side()),
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type processResult struct {
|
||||
item ReportItem
|
||||
err error
|
||||
}
|
||||
|
||||
type ReportItem struct{}
|
||||
|
||||
var ErrSellWithoutBuy = fmt.Errorf("found sell without bought volume")
|
||||
42
internal/reporter_test.go
Normal file
42
internal/reporter_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package internal_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"git.naterciomoniz.net/applications/broker2anexoj/internal"
|
||||
"git.naterciomoniz.net/applications/broker2anexoj/internal/mocks"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestReporter_Run(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
rec := mocks.NewMockRecord(ctrl)
|
||||
rec.EXPECT().Price().Return(big.NewFloat(1.25)).AnyTimes()
|
||||
rec.EXPECT().Quantity().Return(big.NewFloat(10)).AnyTimes()
|
||||
rec.EXPECT().Side().Return(internal.SideBuy).AnyTimes()
|
||||
rec.EXPECT().Symbol().Return("TEST").AnyTimes()
|
||||
|
||||
reader := mocks.NewMockRecordReader(ctrl)
|
||||
records := []internal.Record{
|
||||
rec,
|
||||
rec,
|
||||
}
|
||||
reader.EXPECT().ReadRecord().DoAndReturn(func() (internal.Record, error) {
|
||||
if len(records) > 0 {
|
||||
r := records[0]
|
||||
records = records[1:]
|
||||
return r, nil
|
||||
} else {
|
||||
return nil, io.EOF
|
||||
}
|
||||
}).AnyTimes()
|
||||
|
||||
reporter := internal.NewReporter(reader)
|
||||
gotErr := reporter.Run()
|
||||
if gotErr != nil {
|
||||
t.Fatalf("got unexpected err: %v", gotErr)
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ const (
|
||||
LimitSell = "limit sell"
|
||||
)
|
||||
|
||||
func (rr RecordReader) ReadRecord() (Record, error) {
|
||||
func (rr RecordReader) ReadRecord() (internal.Record, error) {
|
||||
for {
|
||||
raw, err := rr.reader.Read()
|
||||
if err != nil {
|
||||
|
||||
@@ -111,24 +111,24 @@ func TestRecordReader_ReadRecord(t *testing.T) {
|
||||
t.Fatalf("ReadRecord() expected an error")
|
||||
}
|
||||
|
||||
if got.symbol != tt.want.symbol {
|
||||
t.Fatalf("want symbol %v but got %v", tt.want.symbol, got.symbol)
|
||||
if got.Symbol() != tt.want.symbol {
|
||||
t.Fatalf("want symbol %v but got %v", tt.want.symbol, got.Symbol())
|
||||
}
|
||||
|
||||
if got.side != tt.want.side {
|
||||
t.Fatalf("want side %v but got %v", tt.want.side, got.side)
|
||||
if got.Side() != tt.want.side {
|
||||
t.Fatalf("want side %v but got %v", tt.want.side, got.Side())
|
||||
}
|
||||
|
||||
if got.price.Cmp(tt.want.price) != 0 {
|
||||
t.Fatalf("want price %v but got %v", tt.want.price, got.price)
|
||||
if got.Price().Cmp(tt.want.price) != 0 {
|
||||
t.Fatalf("want price %v but got %v", tt.want.price, got.Price())
|
||||
}
|
||||
|
||||
if got.quantity.Cmp(tt.want.quantity) != 0 {
|
||||
t.Fatalf("want quantity %v but got %v", tt.want.quantity, got.quantity)
|
||||
if got.Quantity().Cmp(tt.want.quantity) != 0 {
|
||||
t.Fatalf("want quantity %v but got %v", tt.want.quantity, got.Quantity())
|
||||
}
|
||||
|
||||
if !got.timestamp.Equal(tt.want.timestamp) {
|
||||
t.Fatalf("want timestamp %v but got %v", tt.want.timestamp, got.timestamp)
|
||||
if !got.Timestamp().Equal(tt.want.timestamp) {
|
||||
t.Fatalf("want timestamp %v but got %v", tt.want.timestamp, got.Timestamp())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
72
main.go
72
main.go
@@ -1,12 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"math/big"
|
||||
"os"
|
||||
|
||||
"git.naterciomoniz.net/applications/broker2anexoj/internal"
|
||||
@@ -26,72 +22,16 @@ func run() error {
|
||||
return fmt.Errorf("open statement: %w", err)
|
||||
}
|
||||
|
||||
r := trading212.NewRecordReader(f)
|
||||
reader := trading212.NewRecordReader(f)
|
||||
|
||||
assets := make(map[string]*list.List)
|
||||
for {
|
||||
record, err := r.ReadRecord()
|
||||
reporter := internal.NewReporter(reader)
|
||||
|
||||
err = reporter.Run()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return fmt.Errorf("read statement record: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
switch record.Side() {
|
||||
case internal.SideBuy:
|
||||
lst, ok := assets[record.Symbol()]
|
||||
if !ok {
|
||||
lst = list.New()
|
||||
assets[record.Symbol()] = lst
|
||||
}
|
||||
lst.PushBack(record)
|
||||
|
||||
case internal.SideSell:
|
||||
lst, ok := assets[record.Symbol()]
|
||||
if !ok {
|
||||
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.(internal.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))
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown side: %s", record.Side())
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("Finish processing statement", slog.Any("assets_count", len(assets)))
|
||||
slog.Info("Finish processing statement")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user