Skip to content

Commit

Permalink
Initial commit (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
relusc authored Nov 13, 2023
1 parent 1b2a1be commit f99e0ca
Show file tree
Hide file tree
Showing 8 changed files with 388 additions and 1 deletion.
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: gomod
directory: '/'
schedule:
interval: weekly
- package-ecosystem: github-actions
directory: '/'
schedule:
interval: weekly
73 changes: 72 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,73 @@
# gonvista
Go package to communicate with the onvista.de API

Simple Go package to communicate with the non-public [onvista.de](https://onvista.de) API.

## Disclaimer

The API is non-public and not documented at all. There are only a few snippets and hints to this API that one can find when browsing forums and threads on various platforms around the internet.

There is no guarantee that all endpoints are up to date at all times. However, it's most likely guaranteed that this package does not implement all of the available endpoints by far. The package has been created out of [personal interests](#intention). Consider this package as experimental.

Also, as the API is not public it should be considered that using this package breaks the website user agreements. Treat with caution.

## Name

go + onvista = gonvista :exploding_head: :exploding_head: :exploding_head:

## Intention

This package was implemented out of personal interest. The main point was to be able to add assets to [Portfolio Performance](https://www.portfolio-performance.info/) for which the historical quotes cannot be found in Portfolio Report itself or other big financial sites (e.g. Yahoo Finance). The package can be used to find the correct API URL that can be submitted into Portfolio Performance as source for historical quotes. The quotes returned by the API can be used as JSON input when adding a new asset.

## Installation

```shell
go get -d github.com/relusc/gonvista
```

## Example usage

```go
package main

import (
"fmt"

"github.com/relusc/gonvista"
)

func main() {
c := gonvista.NewClientDefault()

// search by WKN (Apple)
i, err := c.SearchInstruments("AAPL")
if err != nil {
panic(err)
}

fmt.Println(i[0])

// Request by ISIN (IShares Core MSCI World)
msci, err := c.GetInstrumentByISIN("IE00B4L5Y983")
if err != nil {
panic(err)
}

// Get Notations of instrument
nList, err := c.GetInstrumentNotations(msci)
if err != nil {
panic(err)
}

for _, n := range nList {
fmt.Println(n)
}

// List quotes for specific instrument and exchange
q, err := c.GetInstrumentQuotesJSON(msci, nList[0].Id, "Y1", "2022-11-20")
if err != nil {
panic(err)
}

fmt.Println(string(q))
}
```
114 changes: 114 additions & 0 deletions api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Package gonvista provides a simple Go client to talk with the non-public onvista.de API
//
// This package can be considered as experimental as the API is non-public and not documented at all.
package gonvista

import (
"encoding/json"
"fmt"
"net/http"
"regexp"
)

// SearchInstruments finds all instruments based on the submitted searchKey
// Search can be done by submitting a name, ISIN or WKN
// Also parts of a name, ISIN or WKN work, but those searches of course return more results
func (c *Client) SearchInstruments(searchKey string) ([]Instrument, error) {
url := fmt.Sprintf("%s/instruments/search?searchValue=%s", apiBase, searchKey)

// do request
r, err := c.doHTTP(url, http.MethodGet)
if err != nil {
return nil, err
}

// parse response
var resp SearchInstrumentsResponse

err = json.Unmarshal(r, &resp)
if err != nil {
return nil, fmt.Errorf("error while parsing search instruments response: %s", err.Error())
}

return resp.Instruments, nil
}

// GetInstrumentByISIN finds a single instrument based on the submitted ISIN
func (c *Client) GetInstrumentByISIN(searchKey string) (Instrument, error) {
url := fmt.Sprintf("%s/instruments/search?searchValue=%s", apiBase, searchKey)

// do request
r, err := c.doHTTP(url, http.MethodGet)
if err != nil {
return Instrument{}, err
}

// parse response
var resp SearchInstrumentsResponse

err = json.Unmarshal(r, &resp)
if err != nil {
return Instrument{}, fmt.Errorf("error while parsing search instruments response: %s", err.Error())
}

if len(resp.Instruments) > 1 {
return Instrument{}, fmt.Errorf("search returned %d instruments, only one is expected; please update search string", len(resp.Instruments))
}

return resp.Instruments[0], nil
}

// GetInstrumentNotations returns the notations of a single instrument
// Notations can be found when requesting a "snapshot" from onvista.de, hence the API URL
func (c *Client) GetInstrumentNotations(i Instrument) ([]Notation, error) {
// Set type of instrument (fund, bond etc.)
instrument_type := instrument_type_map[i.EntityType]

// create API URL
url := fmt.Sprintf("%s/%s/ISIN:%s/snapshot", apiBase, instrument_type, i.ISIN)

// do request
r, err := c.doHTTP(url, http.MethodGet)
if err != nil {
return nil, err
}

// parse response
var resp SnapshotInstrumentResponse

err = json.Unmarshal(r, &resp)
if err != nil {
return nil, fmt.Errorf("error while parsing search instruments response: %s", err.Error())
}

var notation []Notation
for _, quote := range resp.QuoteList.Quotes {
notation = append(notation, quote.Notation)
}

return notation, nil
}

// Gets historical quotes for an instrument, a specific range and a specific exchange
// Uses the onvista.de API endpoint "/eod_history" (also used on website itself)
//
// The response will be returned plain (in JSON) and can be used in e.g. Portfolio Performance as input
func (c *Client) GetInstrumentQuotesJSON(i Instrument, notationID int, quoteRange, startDate string) ([]byte, error) {
// Check if provided startDate has correct format
// Expected format: yyyy-mm-dd
ok := regexp.MustCompile(`^[0-9]{4}-[0-9]{2}-[0-9]{2}$`).MatchString(startDate)
if !ok {
return nil, fmt.Errorf("provided startDate %s does not match format 'yyyy-mm-dd'", startDate)
}

// create API URL
url := fmt.Sprintf("%s/instruments/%s/%s/eod_history?idNotation=%d&range=%s&startDate=%s", apiBase, i.EntityType, i.EntityValue, notationID, quoteRange, startDate)

// do request
r, err := c.doHTTP(url, http.MethodGet)
if err != nil {
return nil, err
}

return r, nil
}
17 changes: 17 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package gonvista

import "net/http"

// NewClientDefault creates an API client using the Go default HTTP client
func NewClientDefault() *Client {
return &Client{
httpClient: http.DefaultClient,
}
}

// NewClient creates an API client using a custom created Go HTTP client
func NewClient(c *http.Client) *Client {
return &Client{
httpClient: c,
}
}
43 changes: 43 additions & 0 deletions examples/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"fmt"

"github.com/relusc/gonvista"
)

func main() {
c := gonvista.NewClientDefault()

// search by WKN (Apple)
i, err := c.SearchInstruments("AAPL")
if err != nil {
panic(err)
}

fmt.Println(i[0])

// Request by ISIN (IShares Core MSCI World)
msci, err := c.GetInstrumentByISIN("IE00B4L5Y983")
if err != nil {
panic(err)
}

// Get Notations of instrument
nList, err := c.GetInstrumentNotations(msci)
if err != nil {
panic(err)
}

for _, n := range nList {
fmt.Println(n)
}

// List quotes for specific instrument and exchange
q, err := c.GetInstrumentQuotesJSON(msci, nList[0].Id, "Y1", "2022-11-20")
if err != nil {
panic(err)
}

fmt.Println(string(q))
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/relusc/gonvista

go 1.21.3
30 changes: 30 additions & 0 deletions http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package gonvista

import (
"fmt"
"io"
"net/http"
)

// doHTTP executes HTTP requests with the submitted method and returns the response body
func (c *Client) doHTTP(url, method string) ([]byte, error) {
req, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, fmt.Errorf("error while creating HTTP request: %s", err)
}

// Execute request
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error while executing HTTP request: %s", err)
}

defer resp.Body.Close()

bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("could not read HTTP response body: %s", err)
}

return bodyBytes, err
}
99 changes: 99 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package gonvista

import "net/http"

const (
// Base URL of the onvista.de API
// There are also v2 endpoints available, but not for financial data
apiBase = "https://api.onvista.de/api/v1"

// supported ranges to get historical quotes
// NOTE I: not all of them are tested, no guarantee that all of them work for all instruments
// NOTE II: there might also be more ranges, however these are returned when requesting an instrument "snapshot"
RangeOneDay = "D1"
RangeOneWeek = "W1"
RangeOneMonth = "M1"
RangeThreeMonths = "M3"
RangeSixMonths = "M6"
RangeOneYear = "Y1"
RangeThreeYears = "Y3"
RangeFiveYears = "Y5"
RangeTenYears = "Y10"
RangeMax = "MAX"
)

var (
// mapping types of different instruments
instrument_type_map = map[string]string{
"BOND": "bonds",
"FUND": "funds",
"STOCK": "stocks",
}
)

// Client represents the client to talk to the onvista.de API
type Client struct {
httpClient *http.Client
}

// -------------------------------------------------------------------------------------------------------- //
// API "Objects"
// -------------------------------------------------------------------------------------------------------- //

// Instrument represents a single stock, fund or bond
type Instrument struct {
EntityType string `json:"entityType"`
EntityAttributes []string `json:"entityAttributes"`
EntityValue string `json:"entityValue"` // seems to be an onvista.de internal ID of the instrument
Name string `json:"name"`
URL struct {
Website string `json:"WEBSITE"`
} `json:"urls"`
InstrumentType string `json:"instrumentType"`
ISIN string `json:"isin"`
WKN string `json:"wkn"`
DisplayType string `json:"displayType"`
URLName string `json:"urlName"`
TinyName string `json:"tinyName"`
}

// Notation represents a stock exchange/market on which the instrument is listed
type Notation struct {
Id int `json:"idNotation"`
Name string `json:"name"`
Code string `json:"codeExchange"`
Country string `json:"isoCountry"`
}

// QuoteList represents the current quotes of an instrument on all exchanges it is listed on
type QuoteList struct {
Quotes []Quote `json:"list"`
}

// Quote represents a the current quote of an instrument on a specific exchange
type Quote struct {
Ask float32 `json:"ask"`
Bid float32 `json:"bid"`
Unit string `json:"unitType"`
Volume float32 `json:"volume"`
Notation Notation `json:"market"`
}

// -------------------------------------------------------------------------------------------------------- //
// API Response types
// -------------------------------------------------------------------------------------------------------- //

// SearchInstrumentsResponse represents the API response when searching for instruments
type SearchInstrumentsResponse struct {
Expires int `json:"expires"`
SearchValue string `json:"searchValue"`
Instruments []Instrument `json:"list"`
}

// SnapshotInstrumentResponse represents the API response when requesting a single instrument
// This is referenced as "snapshot" by the onvista.de API
type SnapshotInstrumentResponse struct {
Expires int `json:"expires"`
Type string `json:"type"`
QuoteList QuoteList `json:"quoteList"`
}

0 comments on commit f99e0ca

Please sign in to comment.