print a pretty table with correct country coder
This commit is contained in:
@@ -106,6 +106,82 @@ func (m *MockRecord) EXPECT() *MockRecordMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// AssetCountry mocks base method.
|
||||
func (m *MockRecord) AssetCountry() int64 {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "AssetCountry")
|
||||
ret0, _ := ret[0].(int64)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// AssetCountry indicates an expected call of AssetCountry.
|
||||
func (mr *MockRecordMockRecorder) AssetCountry() *MockRecordAssetCountryCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssetCountry", reflect.TypeOf((*MockRecord)(nil).AssetCountry))
|
||||
return &MockRecordAssetCountryCall{Call: call}
|
||||
}
|
||||
|
||||
// MockRecordAssetCountryCall wrap *gomock.Call
|
||||
type MockRecordAssetCountryCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockRecordAssetCountryCall) Return(arg0 int64) *MockRecordAssetCountryCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockRecordAssetCountryCall) Do(f func() int64) *MockRecordAssetCountryCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockRecordAssetCountryCall) DoAndReturn(f func() int64) *MockRecordAssetCountryCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// BrokerCountry mocks base method.
|
||||
func (m *MockRecord) BrokerCountry() int64 {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "BrokerCountry")
|
||||
ret0, _ := ret[0].(int64)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// BrokerCountry indicates an expected call of BrokerCountry.
|
||||
func (mr *MockRecordMockRecorder) BrokerCountry() *MockRecordBrokerCountryCall {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BrokerCountry", reflect.TypeOf((*MockRecord)(nil).BrokerCountry))
|
||||
return &MockRecordBrokerCountryCall{Call: call}
|
||||
}
|
||||
|
||||
// MockRecordBrokerCountryCall wrap *gomock.Call
|
||||
type MockRecordBrokerCountryCall struct {
|
||||
*gomock.Call
|
||||
}
|
||||
|
||||
// Return rewrite *gomock.Call.Return
|
||||
func (c *MockRecordBrokerCountryCall) Return(arg0 int64) *MockRecordBrokerCountryCall {
|
||||
c.Call = c.Call.Return(arg0)
|
||||
return c
|
||||
}
|
||||
|
||||
// Do rewrite *gomock.Call.Do
|
||||
func (c *MockRecordBrokerCountryCall) Do(f func() int64) *MockRecordBrokerCountryCall {
|
||||
c.Call = c.Call.Do(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
||||
func (c *MockRecordBrokerCountryCall) DoAndReturn(f func() int64) *MockRecordBrokerCountryCall {
|
||||
c.Call = c.Call.DoAndReturn(f)
|
||||
return c
|
||||
}
|
||||
|
||||
// Fees mocks base method.
|
||||
func (m *MockRecord) Fees() decimal.Decimal {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type Record interface {
|
||||
Symbol() string
|
||||
Side() Side
|
||||
Price() decimal.Decimal
|
||||
Quantity() decimal.Decimal
|
||||
Timestamp() time.Time
|
||||
Fees() decimal.Decimal
|
||||
Taxes() decimal.Decimal
|
||||
}
|
||||
@@ -10,11 +10,39 @@ import (
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type Record interface {
|
||||
Symbol() string
|
||||
BrokerCountry() int64
|
||||
AssetCountry() int64
|
||||
Side() Side
|
||||
Price() decimal.Decimal
|
||||
Quantity() decimal.Decimal
|
||||
Timestamp() time.Time
|
||||
Fees() decimal.Decimal
|
||||
Taxes() decimal.Decimal
|
||||
}
|
||||
|
||||
type RecordReader interface {
|
||||
// ReadRecord should return Records until an error is found.
|
||||
ReadRecord(context.Context) (Record, error)
|
||||
}
|
||||
|
||||
type ReportItem struct {
|
||||
Symbol string
|
||||
BrokerCountry int64
|
||||
AssetCountry int64
|
||||
BuyValue decimal.Decimal
|
||||
BuyTimestamp time.Time
|
||||
SellValue decimal.Decimal
|
||||
SellTimestamp time.Time
|
||||
Fees decimal.Decimal
|
||||
Taxes decimal.Decimal
|
||||
}
|
||||
|
||||
func (ri ReportItem) RealisedPnL() decimal.Decimal {
|
||||
return ri.SellValue.Sub(ri.BuyValue)
|
||||
}
|
||||
|
||||
type ReportWriter interface {
|
||||
// ReportWriter writes report items
|
||||
Write(context.Context, ReportItem) error
|
||||
@@ -79,6 +107,9 @@ func processRecord(ctx context.Context, q *FillerQueue, rec Record, writer Repor
|
||||
sellValue := matchedQty.Mul(rec.Price())
|
||||
|
||||
err := writer.Write(ctx, ReportItem{
|
||||
Symbol: rec.Symbol(),
|
||||
BrokerCountry: rec.BrokerCountry(),
|
||||
AssetCountry: rec.AssetCountry(),
|
||||
BuyValue: buyValue,
|
||||
BuyTimestamp: buy.Timestamp(),
|
||||
SellValue: sellValue,
|
||||
@@ -97,16 +128,3 @@ func processRecord(ctx context.Context, q *FillerQueue, rec Record, writer Repor
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReportItem struct {
|
||||
BuyValue decimal.Decimal
|
||||
BuyTimestamp time.Time
|
||||
SellValue decimal.Decimal
|
||||
SellTimestamp time.Time
|
||||
Fees decimal.Decimal
|
||||
Taxes decimal.Decimal
|
||||
}
|
||||
|
||||
func (ri ReportItem) RealisedPnL() decimal.Decimal {
|
||||
return ri.SellValue.Sub(ri.BuyValue)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/biter777/countries"
|
||||
"github.com/nmoniz/any2anexoj/internal"
|
||||
"github.com/nmoniz/any2anexoj/internal/mocks"
|
||||
"github.com/shopspring/decimal"
|
||||
@@ -50,10 +51,12 @@ func TestBuildReport(t *testing.T) {
|
||||
|
||||
func mockRecord(ctrl *gomock.Controller, price, quantity float64, side internal.Side, ts time.Time) *mocks.MockRecord {
|
||||
rec := mocks.NewMockRecord(ctrl)
|
||||
rec.EXPECT().Symbol().Return("TEST").AnyTimes()
|
||||
rec.EXPECT().BrokerCountry().Return(int64(countries.PT)).AnyTimes()
|
||||
rec.EXPECT().AssetCountry().Return(int64(countries.USA)).AnyTimes()
|
||||
rec.EXPECT().Price().Return(decimal.NewFromFloat(price)).AnyTimes()
|
||||
rec.EXPECT().Quantity().Return(decimal.NewFromFloat(quantity)).AnyTimes()
|
||||
rec.EXPECT().Side().Return(side).AnyTimes()
|
||||
rec.EXPECT().Symbol().Return("TEST").AnyTimes()
|
||||
rec.EXPECT().Timestamp().Return(ts).AnyTimes()
|
||||
rec.EXPECT().Fees().Return(decimal.Decimal{}).AnyTimes()
|
||||
rec.EXPECT().Taxes().Return(decimal.Decimal{}).AnyTimes()
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ReportLogger writes a simple, human readable, line to the provided io.Writer for each
|
||||
// ReportItem received.
|
||||
type ReportLogger struct {
|
||||
counter int
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func NewStdOutLogger() *ReportLogger {
|
||||
return &ReportLogger{
|
||||
writer: os.Stdout,
|
||||
}
|
||||
}
|
||||
|
||||
func NewReportLogger(w io.Writer) *ReportLogger {
|
||||
return &ReportLogger{
|
||||
writer: w,
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *ReportLogger) Write(_ context.Context, ri ReportItem) error {
|
||||
rl.counter++
|
||||
_, err := fmt.Fprintf(rl.writer, "%6d: realised %s on %s\n", rl.counter, ri.RealisedPnL().String(), ri.SellTimestamp.Format(time.RFC3339))
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package internal_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nmoniz/any2anexoj/internal"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func TestReportLogger_Write(t *testing.T) {
|
||||
tNow := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
items []internal.ReportItem
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "single item positive",
|
||||
items: []internal.ReportItem{
|
||||
{
|
||||
BuyValue: decimal.NewFromFloat(100.0),
|
||||
SellValue: decimal.NewFromFloat(200.0),
|
||||
SellTimestamp: tNow,
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
fmt.Sprintf("%6d: realised 100 on %s\n", 1, tNow.Format(time.RFC3339)),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single item negative",
|
||||
items: []internal.ReportItem{
|
||||
{
|
||||
BuyValue: decimal.NewFromFloat(200.0),
|
||||
SellValue: decimal.NewFromFloat(150.0),
|
||||
SellTimestamp: tNow,
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
fmt.Sprintf("%6d: realised -50 on %s\n", 1, tNow.Format(time.RFC3339)),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple items",
|
||||
items: []internal.ReportItem{
|
||||
{
|
||||
BuyValue: decimal.NewFromFloat(100.0),
|
||||
SellValue: decimal.NewFromFloat(200.0),
|
||||
SellTimestamp: tNow,
|
||||
},
|
||||
{
|
||||
BuyValue: decimal.NewFromFloat(200.0),
|
||||
SellValue: decimal.NewFromFloat(150.0),
|
||||
SellTimestamp: tNow.Add(1),
|
||||
},
|
||||
},
|
||||
want: []string{
|
||||
fmt.Sprintf("%6d: realised 100 on %s\n", 1, tNow.Format(time.RFC3339)),
|
||||
fmt.Sprintf("%6d: realised -50 on %s\n", 2, tNow.Add(1).Format(time.RFC3339)),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
rw := internal.NewReportLogger(buf)
|
||||
|
||||
for _, item := range tt.items {
|
||||
err := rw.Write(t.Context(), item)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error on write: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, wantLine := range tt.want {
|
||||
gotLine, err := buf.ReadString(byte('\n'))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error on buffer read: %v", err)
|
||||
}
|
||||
if wantLine != gotLine {
|
||||
t.Fatalf("want line %q but got %q", wantLine, gotLine)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
53
internal/table_writer.go
Normal file
53
internal/table_writer.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// TableWriter writes a simple, human readable, table row to the provided io.Writer for each
|
||||
// ReportItem received.
|
||||
type TableWriter struct {
|
||||
table table.Writer
|
||||
output io.Writer
|
||||
|
||||
totalEarned decimal.Decimal
|
||||
totalSpent decimal.Decimal
|
||||
totalFees decimal.Decimal
|
||||
totalTaxes decimal.Decimal
|
||||
}
|
||||
|
||||
func NewTableWriter(w io.Writer) *TableWriter {
|
||||
t := table.NewWriter()
|
||||
t.SetOutputMirror(w)
|
||||
t.SetAutoIndex(true)
|
||||
t.SetStyle(table.StyleLight)
|
||||
|
||||
t.AppendHeader(table.Row{"", "", "Realisation", "Realisation", "Realisation", "Realisation", "Aquisition", "Aquisition", "Aquisition", "Aquisition", "", "", ""}, table.RowConfig{AutoMerge: true})
|
||||
t.AppendHeader(table.Row{"Source Country", "Code", "Year", "Month", "Day", "Value", "Year", "Month", "Day", "Value", "Expenses", "Payed Taxes", "Counter Country"})
|
||||
|
||||
return &TableWriter{
|
||||
table: t,
|
||||
output: w,
|
||||
}
|
||||
}
|
||||
|
||||
func (tw *TableWriter) Write(_ context.Context, ri ReportItem) error {
|
||||
tw.totalEarned = tw.totalEarned.Add(ri.SellValue)
|
||||
tw.totalSpent = tw.totalSpent.Add(ri.BuyValue)
|
||||
tw.totalFees = tw.totalFees.Add(ri.Fees)
|
||||
tw.totalTaxes = tw.totalTaxes.Add(ri.Taxes)
|
||||
|
||||
tw.table.AppendRow(table.Row{ri.AssetCountry, ri.Symbol, ri.SellTimestamp.Year(), int(ri.SellTimestamp.Month()), ri.SellTimestamp.Day(), ri.SellValue, ri.BuyTimestamp.Year(), ri.BuyTimestamp.Month(), ri.BuyTimestamp.Day(), ri.BuyValue, ri.Fees, ri.Taxes, ri.BrokerCountry})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tw *TableWriter) Render() {
|
||||
tw.table.AppendFooter(table.Row{"SUM", "SUM", "SUM", "SUM", "SUM", tw.totalEarned, "", "", "", tw.totalSpent, tw.totalFees, tw.totalTaxes}, table.RowConfig{AutoMerge: true, AutoMergeAlign: text.AlignRight})
|
||||
tw.table.Render()
|
||||
}
|
||||
116
internal/table_writer_test.go
Normal file
116
internal/table_writer_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func TestTableWriter_Write(t *testing.T) {
|
||||
tNow := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
items []ReportItem
|
||||
wantTotalSpent decimal.Decimal
|
||||
wantTotalEarned decimal.Decimal
|
||||
wantTotalTaxes decimal.Decimal
|
||||
wantTotalFees decimal.Decimal
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "single item positive",
|
||||
items: []ReportItem{
|
||||
{
|
||||
BuyValue: decimal.NewFromFloat(100.0),
|
||||
SellValue: decimal.NewFromFloat(200.0),
|
||||
SellTimestamp: tNow,
|
||||
Taxes: decimal.NewFromFloat(2.5),
|
||||
Fees: decimal.NewFromFloat(2.5),
|
||||
},
|
||||
},
|
||||
wantTotalSpent: decimal.NewFromFloat(100.0),
|
||||
wantTotalEarned: decimal.NewFromFloat(200.0),
|
||||
wantTotalTaxes: decimal.NewFromFloat(2.5),
|
||||
wantTotalFees: decimal.NewFromFloat(2.5),
|
||||
},
|
||||
{
|
||||
name: "single item negative",
|
||||
items: []ReportItem{
|
||||
{
|
||||
BuyValue: decimal.NewFromFloat(200.0),
|
||||
SellValue: decimal.NewFromFloat(150.0),
|
||||
SellTimestamp: tNow,
|
||||
Taxes: decimal.NewFromFloat(2.5),
|
||||
Fees: decimal.NewFromFloat(2.5),
|
||||
},
|
||||
},
|
||||
wantTotalSpent: decimal.NewFromFloat(200.0),
|
||||
wantTotalEarned: decimal.NewFromFloat(150.0),
|
||||
wantTotalTaxes: decimal.NewFromFloat(2.5),
|
||||
wantTotalFees: decimal.NewFromFloat(2.5),
|
||||
},
|
||||
{
|
||||
name: "multiple items",
|
||||
items: []ReportItem{
|
||||
{
|
||||
Symbol: "US1912161007",
|
||||
BuyValue: decimal.NewFromFloat(100.0),
|
||||
SellValue: decimal.NewFromFloat(200.0),
|
||||
SellTimestamp: tNow,
|
||||
Taxes: decimal.NewFromFloat(2.5),
|
||||
Fees: decimal.NewFromFloat(2.5),
|
||||
},
|
||||
{
|
||||
Symbol: "US1912161007",
|
||||
BuyValue: decimal.NewFromFloat(200.0),
|
||||
SellValue: decimal.NewFromFloat(150.0),
|
||||
SellTimestamp: tNow.Add(1),
|
||||
Taxes: decimal.NewFromFloat(2.5),
|
||||
Fees: decimal.NewFromFloat(2.5),
|
||||
},
|
||||
},
|
||||
wantTotalSpent: decimal.NewFromFloat(300.0),
|
||||
wantTotalEarned: decimal.NewFromFloat(350.0),
|
||||
wantTotalTaxes: decimal.NewFromFloat(5.0),
|
||||
wantTotalFees: decimal.NewFromFloat(5.0),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
tw := NewTableWriter(buf)
|
||||
|
||||
for _, item := range tt.items {
|
||||
err := tw.Write(t.Context(), item)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error on write: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if tw.table.Length() != len(tt.items) {
|
||||
t.Fatalf("want %d items in table but got %d", len(tt.items), tw.table.Length())
|
||||
}
|
||||
|
||||
if !tw.totalSpent.Equal(tt.wantTotalSpent) {
|
||||
t.Errorf("want totalSpent to be %v but got %v", tt.wantTotalSpent, tw.totalSpent)
|
||||
}
|
||||
|
||||
if !tw.totalEarned.Equal(tt.wantTotalEarned) {
|
||||
t.Errorf("want totalEarned to be %v but got %v", tt.wantTotalEarned, tw.totalEarned)
|
||||
}
|
||||
|
||||
if !tw.totalTaxes.Equal(tt.wantTotalTaxes) {
|
||||
t.Errorf("want totalTaxes to be %v but got %v", tt.wantTotalTaxes, tw.totalTaxes)
|
||||
}
|
||||
|
||||
if !tw.totalFees.Equal(tt.wantTotalFees) {
|
||||
t.Errorf("want totalFees to be %v but got %v", tt.wantTotalFees, tw.totalFees)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
7
internal/trading212/constants.go
Normal file
7
internal/trading212/constants.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package trading212
|
||||
|
||||
import (
|
||||
"github.com/biter777/countries"
|
||||
)
|
||||
|
||||
const Country = countries.Cyprus
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/biter777/countries"
|
||||
"github.com/nmoniz/any2anexoj/internal"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
@@ -26,6 +27,14 @@ func (r Record) Symbol() string {
|
||||
return r.symbol
|
||||
}
|
||||
|
||||
func (r Record) BrokerCountry() int64 {
|
||||
return int64(Country)
|
||||
}
|
||||
|
||||
func (r Record) AssetCountry() int64 {
|
||||
return int64(countries.ByName(r.Symbol()[:2]).Info().Code)
|
||||
}
|
||||
|
||||
func (r Record) Side() internal.Side {
|
||||
return r.side
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user