implement reporter
also introduce gomock and tests
This commit is contained in:
2
go.mod
2
go.mod
@@ -1,3 +1,5 @@
|
|||||||
module git.naterciomoniz.net/applications/broker2anexoj
|
module git.naterciomoniz.net/applications/broker2anexoj
|
||||||
|
|
||||||
go 1.25.3
|
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)
|
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.
|
// empty returns falso on the 2nd return value, true otherwise.
|
||||||
func (rq *RecordQueue) Pop() (Record, bool) {
|
func (rq *RecordQueue) Pop() (Record, bool) {
|
||||||
if rq == nil || rq.l == nil {
|
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"
|
LimitSell = "limit sell"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rr RecordReader) ReadRecord() (Record, error) {
|
func (rr RecordReader) ReadRecord() (internal.Record, error) {
|
||||||
for {
|
for {
|
||||||
raw, err := rr.reader.Read()
|
raw, err := rr.reader.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -111,24 +111,24 @@ func TestRecordReader_ReadRecord(t *testing.T) {
|
|||||||
t.Fatalf("ReadRecord() expected an error")
|
t.Fatalf("ReadRecord() expected an error")
|
||||||
}
|
}
|
||||||
|
|
||||||
if got.symbol != tt.want.symbol {
|
if got.Symbol() != tt.want.symbol {
|
||||||
t.Fatalf("want symbol %v but got %v", tt.want.symbol, got.symbol)
|
t.Fatalf("want symbol %v but got %v", tt.want.symbol, got.Symbol())
|
||||||
}
|
}
|
||||||
|
|
||||||
if got.side != tt.want.side {
|
if got.Side() != tt.want.side {
|
||||||
t.Fatalf("want side %v but got %v", tt.want.side, got.side)
|
t.Fatalf("want side %v but got %v", tt.want.side, got.Side())
|
||||||
}
|
}
|
||||||
|
|
||||||
if got.price.Cmp(tt.want.price) != 0 {
|
if got.Price().Cmp(tt.want.price) != 0 {
|
||||||
t.Fatalf("want price %v but got %v", tt.want.price, got.price)
|
t.Fatalf("want price %v but got %v", tt.want.price, got.Price())
|
||||||
}
|
}
|
||||||
|
|
||||||
if got.quantity.Cmp(tt.want.quantity) != 0 {
|
if got.Quantity().Cmp(tt.want.quantity) != 0 {
|
||||||
t.Fatalf("want quantity %v but got %v", tt.want.quantity, got.quantity)
|
t.Fatalf("want quantity %v but got %v", tt.want.quantity, got.Quantity())
|
||||||
}
|
}
|
||||||
|
|
||||||
if !got.timestamp.Equal(tt.want.timestamp) {
|
if !got.Timestamp().Equal(tt.want.timestamp) {
|
||||||
t.Fatalf("want timestamp %v but got %v", tt.want.timestamp, got.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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/list"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"math/big"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.naterciomoniz.net/applications/broker2anexoj/internal"
|
"git.naterciomoniz.net/applications/broker2anexoj/internal"
|
||||||
@@ -26,72 +22,16 @@ func run() error {
|
|||||||
return fmt.Errorf("open statement: %w", err)
|
return fmt.Errorf("open statement: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r := trading212.NewRecordReader(f)
|
reader := trading212.NewRecordReader(f)
|
||||||
|
|
||||||
assets := make(map[string]*list.List)
|
reporter := internal.NewReporter(reader)
|
||||||
for {
|
|
||||||
record, err := r.ReadRecord()
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, io.EOF) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return fmt.Errorf("read statement record: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch record.Side() {
|
err = reporter.Run()
|
||||||
case internal.SideBuy:
|
if err != nil {
|
||||||
lst, ok := assets[record.Symbol()]
|
return err
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user