-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
388 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/relusc/gonvista | ||
|
||
go 1.21.3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |