Skip to content

Commit

Permalink
Implemented simple quote provider for ING
Browse files Browse the repository at this point in the history
Can be useful for derivates.

Fixes #165
  • Loading branch information
oxisto committed Oct 7, 2023
1 parent cb342c5 commit 3c4a463
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 4 deletions.
8 changes: 7 additions & 1 deletion import/csv/csv_importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,13 @@ func readLine(cr *csv.Reader, pname string) (tx *portfoliov1.PortfolioEvent, sec
Currency: lsCurrency(record[3], record[5]),
},
}
sec.QuoteProvider = moneygopher.Ref(securities.QuoteProviderYF)

// Default to YF, but only if we have a ticker symbol, otherwise, let's try ING
if len(sec.ListedOn) >= 0 && len(sec.ListedOn[0].Ticker) > 0 {
sec.QuoteProvider = moneygopher.Ref(securities.QuoteProviderYF)
} else {
sec.QuoteProvider = moneygopher.Ref(securities.QuoteProviderING)

Check warning on line 157 in import/csv/csv_importer.go

View check run for this annotation

Codecov / codecov/patch

import/csv/csv_importer.go#L157

Added line #L157 was not covered by tests
}

tx.PortfolioName = pname
tx.SecurityName = sec.Name
Expand Down
7 changes: 4 additions & 3 deletions import/csv/csv_importer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/oxisto/assert"
moneygopher "github.com/oxisto/money-gopher"
portfoliov1 "github.com/oxisto/money-gopher/gen"
"github.com/oxisto/money-gopher/service/securities"
"google.golang.org/protobuf/types/known/timestamppb"
)

Expand Down Expand Up @@ -111,7 +112,7 @@ func Test_readLine(t *testing.T) {
wantSec: &portfoliov1.Security{
Name: "US0378331005",
DisplayName: "Apple Inc.",
QuoteProvider: moneygopher.Ref("yf"),
QuoteProvider: moneygopher.Ref(securities.QuoteProviderYF),
ListedOn: []*portfoliov1.ListedSecurity{
{
SecurityName: "US0378331005",
Expand Down Expand Up @@ -141,7 +142,7 @@ func Test_readLine(t *testing.T) {
wantSec: &portfoliov1.Security{
Name: "US00827B1061",
DisplayName: "Affirm Holdings Inc.",
QuoteProvider: moneygopher.Ref("yf"),
QuoteProvider: moneygopher.Ref(securities.QuoteProviderYF),
ListedOn: []*portfoliov1.ListedSecurity{
{
SecurityName: "US00827B1061",
Expand Down Expand Up @@ -173,7 +174,7 @@ func Test_readLine(t *testing.T) {
wantSec: &portfoliov1.Security{
Name: "DE0005557508",
DisplayName: "Deutsche Telekom AG",
QuoteProvider: moneygopher.Ref("yf"),
QuoteProvider: moneygopher.Ref(securities.QuoteProviderYF),
ListedOn: []*portfoliov1.ListedSecurity{
{
SecurityName: "DE0005557508",
Expand Down
1 change: 1 addition & 0 deletions service/securities/quote_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var providers map[string]QuoteProvider = make(map[string]QuoteProvider)

func init() {
RegisterQuoteProvider(QuoteProviderYF, &yf{})
RegisterQuoteProvider(QuoteProviderING, &ing{})
}

// AddCommand adds a command using the specific symbol.
Expand Down
69 changes: 69 additions & 0 deletions service/securities/quote_provider_ing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2023 Christian Banse
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// This file is part of The Money Gopher.

package securities

import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"

portfoliov1 "github.com/oxisto/money-gopher/gen"
)

const QuoteProviderING = "ing"

type ing struct {
http.Client
}

type header struct {
Ask float32 `json:"ask"`
AskDate time.Time `json:"askDate"`
Bid float32 `json:"bid"`
BidDate time.Time `json:"bidDate"`
Currency string `json:"currency"`
ISIN string `json:"isin"`
HasBidAsk bool `json:"hasBidAsk"`
Price float32 `json:"price"`
PriceChangedDate time.Time `json:"priceChangeDate"`
WKN string `json:"wkn"`
}

func (ing *ing) LatestQuote(ctx context.Context, ls *portfoliov1.ListedSecurity) (quote float32, t time.Time, err error) {
var (
res *http.Response
h header
)

res, err = ing.Get(fmt.Sprintf("https://component-api.wertpapiere.ing.de/api/v1/components/instrumentheader/%s", ls.SecurityName))
if err != nil {
return 0, t, fmt.Errorf("could not fetch quote: %w", err)
}

err = json.NewDecoder(res.Body).Decode(&h)
if err != nil {
return 0, t, fmt.Errorf("could not decode JSON: %w", err)
}

if h.HasBidAsk {
return h.Bid, h.BidDate, nil
} else {
return h.Price, h.PriceChangedDate, nil

Check warning on line 67 in service/securities/quote_provider_ing.go

View check run for this annotation

Codecov / codecov/patch

service/securities/quote_provider_ing.go#L67

Added line #L67 was not covered by tests
}
}
119 changes: 119 additions & 0 deletions service/securities/quote_provider_ing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright 2023 Christian Banse
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// This file is part of The Money Gopher.

package securities

import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"

"github.com/oxisto/assert"
portfoliov1 "github.com/oxisto/money-gopher/gen"
)

func Test_ing_LatestQuote(t *testing.T) {
type fields struct {
Client http.Client
}
type args struct {
ctx context.Context
ls *portfoliov1.ListedSecurity
}
tests := []struct {
name string
fields fields
args args
wantQuote float32
wantTime time.Time
wantErr assert.Want[error]
}{
{
name: "http response error",
fields: fields{
Client: newMockClient(func(req *http.Request) (res *http.Response, err error) {
return nil, http.ErrNotSupported
}),
},
args: args{
ctx: context.TODO(),
ls: &portfoliov1.ListedSecurity{
SecurityName: "My Security",
Ticker: "TICK",
Currency: "USD",
},
},
wantErr: func(t *testing.T, err error) bool {
return errors.Is(err, http.ErrNotSupported)
},
},
{
name: "invalid JSON",
fields: fields{
Client: newMockClient(func(req *http.Request) (res *http.Response, err error) {
r := httptest.NewRecorder()
r.WriteString(`{]`)
return r.Result(), nil
}),
},
args: args{
ls: &portfoliov1.ListedSecurity{
SecurityName: "My Security",
Ticker: "TICK",
Currency: "USD",
},
},
wantErr: func(t *testing.T, err error) bool {
return strings.Contains(err.Error(), "invalid")
},
},
{
name: "happy path",
fields: fields{
Client: newMockClient(func(req *http.Request) (res *http.Response, err error) {
r := httptest.NewRecorder()
r.WriteString(`{"id":1,"name":"Call on My Security","bid": 100.0, "bidDate":"2023-05-04T20:00:00+00:00","hasBidAsk":true}`)
return r.Result(), nil
}),
},
args: args{
ls: &portfoliov1.ListedSecurity{
SecurityName: "DE0000000001",
Ticker: "",
Currency: "EUR",
},
},
wantQuote: float32(100.0),
wantTime: time.Date(2023, 05, 04, 20, 0, 0, 0, time.UTC),
wantErr: func(t *testing.T, err error) bool { return true },
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ing := &ing{
Client: tt.fields.Client,
}
gotQuote, gotTime, err := ing.LatestQuote(tt.args.ctx, tt.args.ls)
assert.Equals(t, true, tt.wantErr(t, err))
assert.Equals(t, tt.wantQuote, gotQuote)
assert.Equals(t, tt.wantTime.UTC(), gotTime.UTC())
})
}
}

0 comments on commit 3c4a463

Please sign in to comment.