Compare commits
No commits in common. "67fcfeb1771a9ba9aa956fe3bbb6caabfb5427ee" and "72845455711e7e5eb4f3fc11fcb3e33c0d508409" have entirely different histories.
67fcfeb177
...
7284545571
@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"deadbeef.codes/steven/ynab-portfolio-monitor/bitcoin"
|
"deadbeef.codes/steven/ynab-portfolio-monitor/bitcoin"
|
||||||
"deadbeef.codes/steven/ynab-portfolio-monitor/questrade"
|
"deadbeef.codes/steven/ynab-portfolio-monitor/questrade"
|
||||||
"deadbeef.codes/steven/ynab-portfolio-monitor/staticjsonFinnhub"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AccountProvider is the base set of requirements to be implemented for any integration
|
// AccountProvider is the base set of requirements to be implemented for any integration
|
||||||
@ -18,5 +17,4 @@ type AccountProvider interface {
|
|||||||
var allProviders []AccountProvider = []AccountProvider{
|
var allProviders []AccountProvider = []AccountProvider{
|
||||||
&questrade.Provider{},
|
&questrade.Provider{},
|
||||||
&bitcoin.Provider{},
|
&bitcoin.Provider{},
|
||||||
&staticjsonFinnhub.Provider{},
|
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ func (l LoginCredentials) authHeader() string {
|
|||||||
|
|
||||||
// Send an HTTP GET request, and return the processed response
|
// Send an HTTP GET request, and return the processed response
|
||||||
func (c *client) get(endpoint string, out interface{}, query url.Values) error {
|
func (c *client) get(endpoint string, out interface{}, query url.Values) error {
|
||||||
req, err := http.NewRequest("GET", c.Credentials.ApiServer+endpoint+"?"+query.Encode(), nil)
|
req, err := http.NewRequest("GET", c.Credentials.ApiServer+endpoint+query.Encode(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,7 @@ func loadPersistentData() (*persistentData, error) {
|
|||||||
defer f.Close()
|
defer f.Close()
|
||||||
b, err := io.ReadAll(f)
|
b, err := io.ReadAll(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read file data/questrade-data.json: %v", err)
|
return nil, fmt.Errorf("failed to read file data/questrade-data.jsonn: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(b, data)
|
err = json.Unmarshal(b, data)
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
# Static JSON Finnhub Provider
|
|
||||||
|
|
||||||
If you just want to provide a static JSON file as input containing symbols and quantities owned, this provider will use finnhub for pricing quotes for the symbols provided.
|
|
||||||
|
|
||||||
### Example data/staticjsonFinnhub-data.json
|
|
||||||
|
|
||||||
You can define many to many relationships, multiple YNAB accounts containing multiple types of securities using json arrays.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"accounts":[
|
|
||||||
{
|
|
||||||
"ynabAccountId":"d54da05a-cs20-4654-bcff-9ce36f43225d",
|
|
||||||
"securities":[
|
|
||||||
{
|
|
||||||
"symbol":"SPY",
|
|
||||||
"quantity":420
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
@ -1,84 +0,0 @@
|
|||||||
package staticjsonFinnhub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const apiBaseURL = "https://finnhub.io/api/v1/"
|
|
||||||
|
|
||||||
// A client is the structure that will be used to consume the API
|
|
||||||
// endpoints. It holds the login credentials, http client/transport,
|
|
||||||
// rate limit information, and the login session timer.
|
|
||||||
type client struct {
|
|
||||||
apiToken string
|
|
||||||
httpClient *http.Client
|
|
||||||
transport *http.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send an HTTP GET request, and return the processed response
|
|
||||||
func (c *client) get(endpoint string, out interface{}, query url.Values) error {
|
|
||||||
req, err := http.NewRequest("GET", apiBaseURL+endpoint+"?"+query.Encode(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Add("X-Finnhub-Token", c.apiToken)
|
|
||||||
|
|
||||||
res, err := c.httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("http get request failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.processResponse(res, out)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processResponse takes the body of an HTTP response, and either returns
|
|
||||||
// the error code, or unmarshalls the JSON response, extracts
|
|
||||||
// rate limit info, and places it into the object
|
|
||||||
// output parameter. This function closes the response body after reading it.
|
|
||||||
func (c *client) processResponse(res *http.Response, out interface{}) error {
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
res.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
return fmt.Errorf("got http response status '%d' but expected 200", res.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, out)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newClient is the factory function for clients - takes an API token
|
|
||||||
func newClient(apiToken string) (*client, error) {
|
|
||||||
transport := &http.Transport{
|
|
||||||
ResponseHeaderTimeout: 5 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new client
|
|
||||||
c := &client{
|
|
||||||
apiToken: apiToken,
|
|
||||||
httpClient: httpClient,
|
|
||||||
transport: transport,
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
package staticjsonFinnhub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type security struct {
|
|
||||||
Symbol string `json:"symbol"`
|
|
||||||
Quantity int `json:"quantity"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type account struct {
|
|
||||||
YnabAccountID string `json:"ynabAccountId"`
|
|
||||||
Securities []security `json:"securities"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type inputData struct {
|
|
||||||
Accounts []account `json:"accounts"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Provider struct {
|
|
||||||
data *inputData // Data stored on disk and loaded when program starts
|
|
||||||
client *client // HTTP client for interacting with Finnhub API
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Provider) Name() string {
|
|
||||||
return "Static JSON - Finnhub"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configures the provider for usage via environment variables and inputData
|
|
||||||
// If an error is returned, the provider will not be used
|
|
||||||
func (p *Provider) Configure() error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Load input data from disk
|
|
||||||
p.data, err = loadInputData()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
b, _ := json.Marshal(p.data)
|
|
||||||
fmt.Println(string(b))
|
|
||||||
|
|
||||||
apiKey := os.Getenv("staticjson_finnhub_key")
|
|
||||||
if apiKey == "" {
|
|
||||||
return fmt.Errorf("this account provider is not configured: missing staticjson_finnhub_key environment variable")
|
|
||||||
}
|
|
||||||
|
|
||||||
p.client, err = newClient(apiKey)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create new client: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns slices of account balances and mapped YNAB account IDs, along with an error
|
|
||||||
func (p *Provider) GetBalances() ([]int, []string, error) {
|
|
||||||
balances := make([]int, 0)
|
|
||||||
ynabAccountIDs := make([]string, 0)
|
|
||||||
for i := range p.data.Accounts {
|
|
||||||
balance := 0
|
|
||||||
for j := range p.data.Accounts[i].Securities {
|
|
||||||
price, err := p.client.getQuote(p.data.Accounts[i].Securities[j].Symbol)
|
|
||||||
if err != nil {
|
|
||||||
return balances, ynabAccountIDs, fmt.Errorf("failed to get quote for security with symbol '%s': %v", p.data.Accounts[i].Securities[j].Symbol, err)
|
|
||||||
}
|
|
||||||
balance += price * p.data.Accounts[i].Securities[j].Quantity
|
|
||||||
}
|
|
||||||
balances = append(balances, balance)
|
|
||||||
ynabAccountIDs = append(ynabAccountIDs, p.data.Accounts[i].YnabAccountID)
|
|
||||||
}
|
|
||||||
return balances, ynabAccountIDs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load input data from disk
|
|
||||||
func loadInputData() (*inputData, error) {
|
|
||||||
data := &inputData{}
|
|
||||||
|
|
||||||
f, err := os.Open("data/staticjson-data.json")
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
return nil, fmt.Errorf("this account provider is not configured: missing data/staticjson-data.json")
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
b, err := io.ReadAll(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read file data/staticjson-data.json: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(b, data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal data/staticjson-data.json to inputData struct: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package staticjsonFinnhub
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
type quote struct {
|
|
||||||
C float64 `json:"c"` // Current price
|
|
||||||
H float64 `json:"h"` // High price of the day
|
|
||||||
L float64 `json:"l"` // Low price of the day
|
|
||||||
O float64 `json:"O"` // Open price of the day
|
|
||||||
Pc float64 `json:"pc"` // Previous close price
|
|
||||||
T int `json:"t"` // ?
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c client) getQuote(symbol string) (int, error) {
|
|
||||||
quoteResponse := "e{}
|
|
||||||
query := url.Values{}
|
|
||||||
query.Add("symbol", symbol)
|
|
||||||
err := c.get("/quote", quoteResponse, query)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("http get request error: %v", err)
|
|
||||||
}
|
|
||||||
return int(quoteResponse.C * 1000), nil
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user