Compare commits

..

2 Commits

Author SHA1 Message Date
58d4d0ba41 Add README's for providers
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-13 11:56:16 -07:00
be7e068fe6 Add Static JSON Yahoo Finance provider 2023-11-13 11:52:45 -07:00
9 changed files with 305 additions and 4 deletions

View File

@ -4,6 +4,7 @@ import (
"deadbeef.codes/steven/ynab-portfolio-monitor/bitcoin"
"deadbeef.codes/steven/ynab-portfolio-monitor/questrade"
"deadbeef.codes/steven/ynab-portfolio-monitor/staticjsonFinnhub"
"deadbeef.codes/steven/ynab-portfolio-monitor/staticjsonYahooFinance"
)
// AccountProvider is the base set of requirements to be implemented for any integration
@ -19,4 +20,5 @@ var allProviders []AccountProvider = []AccountProvider{
&questrade.Provider{},
&bitcoin.Provider{},
&staticjsonFinnhub.Provider{},
&staticjsonYahooFinance.Provider{},
}

8
bitcoin/README.md Normal file
View File

@ -0,0 +1,8 @@
# Bitcoin Provider
A provider for bitcoin, backed by blockstream.info to query address balance and coinconvert.net for fiat conversion.
### Environment Variables
* *bitcoin_address_[0...]* - A series of bitcoin addresses with a suffix of _0 and each additional address counting up.
* *bitcoin_ynab_account* - The YNAB account ID used to keep track of Bitcoin balance

8
questrade/README.md Normal file
View File

@ -0,0 +1,8 @@
# Questrade Provider
A provider for Questrade using the Questrade API.
### Environment Variables
* *questrade_account_[0...]* - A series of Questrade account numbers with a suffix of _0 and each additional account counting up.
* *questrade_ynab_account_[0...]* - The YNAB account ID for the matching questrade account. Each configured questrade account must have a matching YNAB account.

View File

@ -81,19 +81,19 @@ func (p *Provider) GetBalances() ([]int, []string, error) {
func loadInputData() (*inputData, error) {
data := &inputData{}
f, err := os.Open("data/staticjson-data.json")
f, err := os.Open("data/staticjsonFinnhub-data.json")
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("this account provider is not configured: missing data/staticjson-data.json")
return nil, fmt.Errorf("this account provider is not configured: missing data/staticjsonFinnhub-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)
return nil, fmt.Errorf("failed to read file data/staticjsonFinnhub-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 nil, fmt.Errorf("failed to unmarshal data/staticjsonFinnhub-data.json to inputData struct: %v", err)
}
return data, nil

View File

@ -0,0 +1,23 @@
# Static JSON Yahoo Finance Provider
If you just want to provide a static JSON file as input containing symbols and quantities owned, this provider will use Yahoo Finance for pricing quotes for the symbols provided.
### Example data/staticjsonYahooFinance-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
}
]
}
]
}
```

View File

@ -0,0 +1,81 @@
package staticjsonYahooFinance
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const apiBaseURL = "https://query1.finance.yahoo.com/v8/finance/"
// 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 {
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
}
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() (*client, error) {
transport := &http.Transport{
ResponseHeaderTimeout: 5 * time.Second,
}
httpClient := &http.Client{
Transport: transport,
}
// Create a new client
c := &client{
httpClient: httpClient,
transport: transport,
}
return c, nil
}

View File

@ -0,0 +1,92 @@
package staticjsonYahooFinance
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 - Yahoo Finance"
}
// 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
}
p.client, err = newClient()
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.getChart(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/staticjsonYahooFinance-data.json")
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("this account provider is not configured: missing data/staticjsonYahooFinance-data.json")
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed to read file data/staticjsonYahooFinance-data.json: %v", err)
}
err = json.Unmarshal(b, data)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal data/staticjsonYahooFinance-data.json to inputData struct: %v", err)
}
return data, nil
}

View File

@ -0,0 +1,83 @@
package staticjsonYahooFinance
import (
"fmt"
"net/url"
)
type chart struct {
Chart struct {
Result []struct {
Meta struct {
Currency string `json:"currency"`
Symbol string `json:"symbol"`
ExchangeName string `json:"exchangeName"`
InstrumentType string `json:"instrumentType"`
FirstTradeDate int `json:"firstTradeDate"`
RegularMarketTime int `json:"regularMarketTime"`
Gmtoffset int `json:"gmtoffset"`
Timezone string `json:"timezone"`
ExchangeTimezoneName string `json:"exchangeTimezoneName"`
RegularMarketPrice float64 `json:"regularMarketPrice"`
ChartPreviousClose float64 `json:"chartPreviousClose"`
PreviousClose float64 `json:"previousClose"`
Scale int `json:"scale"`
PriceHint int `json:"priceHint"`
CurrentTradingPeriod struct {
Pre struct {
Timezone string `json:"timezone"`
Start int `json:"start"`
End int `json:"end"`
Gmtoffset int `json:"gmtoffset"`
} `json:"pre"`
Regular struct {
Timezone string `json:"timezone"`
Start int `json:"start"`
End int `json:"end"`
Gmtoffset int `json:"gmtoffset"`
} `json:"regular"`
Post struct {
Timezone string `json:"timezone"`
Start int `json:"start"`
End int `json:"end"`
Gmtoffset int `json:"gmtoffset"`
} `json:"post"`
} `json:"currentTradingPeriod"`
TradingPeriods [][]struct {
Timezone string `json:"timezone"`
Start int `json:"start"`
End int `json:"end"`
Gmtoffset int `json:"gmtoffset"`
} `json:"tradingPeriods"`
DataGranularity string `json:"dataGranularity"`
Range string `json:"range"`
ValidRanges []string `json:"validRanges"`
} `json:"meta"`
Timestamp []int `json:"timestamp"`
Indicators struct {
Quote []struct {
Volume []any `json:"volume"`
High []any `json:"high"`
Close []any `json:"close"`
Open []any `json:"open"`
Low []any `json:"low"`
} `json:"quote"`
} `json:"indicators"`
} `json:"result"`
Error any `json:"error"`
} `json:"chart"`
}
func (c client) getChart(symbol string) (int, error) {
chartResponse := &chart{}
err := c.get(fmt.Sprintf("/chart/%s", symbol), chartResponse, url.Values{})
if err != nil {
return 0, fmt.Errorf("http get request error: %v", err)
}
if len(chartResponse.Chart.Result) != 1 {
return 0, fmt.Errorf("unexpected length of results - expected 1 but got %d", len(chartResponse.Chart.Result))
}
return int(chartResponse.Chart.Result[0].Meta.RegularMarketPrice * 1000), nil
}

4
ynab/README.md Normal file
View File

@ -0,0 +1,4 @@
# YNAB API
This is not considered an account provider as it does not use the AccountProvider interface, instead this package is used for interacting with YNAB, creating transactions, updating transactions, getting account balances, etc.