Compare commits
10 Commits
c4a79b0f4c
...
main
Author | SHA1 | Date | |
---|---|---|---|
3c274c614b | |||
61074bfd80 | |||
13291da691 | |||
3fbfbab7d6 | |||
a34dca1076 | |||
0110941ac7 | |||
43cd399c18 | |||
88552ba042 | |||
068004ba14 | |||
287acc03eb |
@ -1,10 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"deadbeef.codes/steven/ynab-portfolio-monitor/providers/bitcoin"
|
"code.stevenpolley.net/steven/ynab-portfolio-monitor/providers/bitcoin"
|
||||||
"deadbeef.codes/steven/ynab-portfolio-monitor/providers/questrade"
|
"code.stevenpolley.net/steven/ynab-portfolio-monitor/providers/questrade"
|
||||||
"deadbeef.codes/steven/ynab-portfolio-monitor/providers/staticjsonFinnhub"
|
"code.stevenpolley.net/steven/ynab-portfolio-monitor/providers/staticjsonFinnhub"
|
||||||
"deadbeef.codes/steven/ynab-portfolio-monitor/providers/staticjsonYahooFinance"
|
"code.stevenpolley.net/steven/ynab-portfolio-monitor/providers/staticjsonYahooFinance"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
|
2
go.mod
2
go.mod
@ -1,4 +1,4 @@
|
|||||||
module deadbeef.codes/steven/ynab-portfolio-monitor
|
module code.stevenpolley.net/steven/ynab-portfolio-monitor
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
|
2
main.go
2
main.go
@ -7,7 +7,7 @@ import (
|
|||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"deadbeef.codes/steven/ynab-portfolio-monitor/ynab"
|
"code.stevenpolley.net/steven/ynab-portfolio-monitor/ynab"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -55,14 +55,19 @@ func (p *Provider) GetBalances() ([]int, []string, error) {
|
|||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
addressResponse, err := p.client.getAddress(bitcoinAddress)
|
addressResponse, err := p.client.getAddress(bitcoinAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("failed to get bitcoin address '%s': %v", bitcoinAddress, err)
|
err := fmt.Errorf("failed to get BTC balance for bitcoin address '%s': %v", bitcoinAddress, err)
|
||||||
goErr = &err
|
if err != nil {
|
||||||
|
goErr = &err
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
satoshiBalance += addressResponse.ChainStats.FundedTxoSum - addressResponse.ChainStats.SpentTxoSum
|
satoshiBalance += addressResponse.ChainStats.FundedTxoSum - addressResponse.ChainStats.SpentTxoSum
|
||||||
}(goErr)
|
}(goErr)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
if goErr != nil {
|
||||||
|
return nil, nil, *goErr
|
||||||
|
}
|
||||||
|
|
||||||
fiatBalance, err := p.client.convertBTCToCAD(satoshiBalance)
|
fiatBalance, err := p.client.convertBTCToCAD(satoshiBalance)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
15
providers/staticjsonYahooFinance/cache.go
Normal file
15
providers/staticjsonYahooFinance/cache.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package staticjsonYahooFinance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const cacheAgeSeconds = 900
|
||||||
|
|
||||||
|
// intialized in providerImpl.go's Configure
|
||||||
|
var chartCache map[string]chartCacheEntry
|
||||||
|
|
||||||
|
type chartCacheEntry struct {
|
||||||
|
Chart chartResponse
|
||||||
|
LastUpdated time.Time
|
||||||
|
}
|
@ -3,10 +3,11 @@ package staticjsonYahooFinance
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A chart response is what we get back from Yahoo Finance
|
// A chartResponse response is what we get back from Yahoo Finance
|
||||||
type chart struct {
|
type chartResponse struct {
|
||||||
Chart struct {
|
Chart struct {
|
||||||
Result []struct {
|
Result []struct {
|
||||||
Meta struct {
|
Meta struct {
|
||||||
@ -69,8 +70,16 @@ type chart struct {
|
|||||||
} `json:"chart"`
|
} `json:"chart"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getChart first checks if the symbol chartCacheEntry is valid,
|
||||||
|
// and if not then it downloads fresh chart data
|
||||||
func (c client) getChart(symbol string) (int, error) {
|
func (c client) getChart(symbol string) (int, error) {
|
||||||
chartResponse := &chart{}
|
if cacheItem, ok := chartCache[symbol]; ok {
|
||||||
|
// if the cacheEntry is still valid, use it
|
||||||
|
if time.Now().Before(cacheItem.LastUpdated.Add(cacheAgeSeconds)) {
|
||||||
|
return int(cacheItem.Chart.Chart.Result[0].Meta.RegularMarketPrice * 1000), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chartResponse := &chartResponse{}
|
||||||
err := c.get(fmt.Sprintf("/chart/%s", symbol), chartResponse, url.Values{})
|
err := c.get(fmt.Sprintf("/chart/%s", symbol), chartResponse, url.Values{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("http get request error: %v", err)
|
return 0, fmt.Errorf("http get request error: %v", err)
|
||||||
@ -80,5 +89,7 @@ func (c client) getChart(symbol string) (int, error) {
|
|||||||
return 0, fmt.Errorf("unexpected length of results - expected 1 but got %d", len(chartResponse.Chart.Result))
|
return 0, fmt.Errorf("unexpected length of results - expected 1 but got %d", len(chartResponse.Chart.Result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chartCache[symbol] = chartCacheEntry{Chart: *chartResponse, LastUpdated: time.Now()}
|
||||||
|
|
||||||
return int(chartResponse.Chart.Result[0].Meta.RegularMarketPrice * 1000), nil
|
return int(chartResponse.Chart.Result[0].Meta.RegularMarketPrice * 1000), nil
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const apiBaseURL = "https://query1.finance.yahoo.com/v8/finance/"
|
const apiBaseURL = "https://query1.finance.yahoo.com/v8/finance"
|
||||||
|
|
||||||
// A client is the structure that will be used to consume the API
|
// A client is the structure that will be used to consume the API
|
||||||
// endpoints. It holds the login credentials, http client/transport,
|
// endpoints. It holds the login credentials, http client/transport,
|
||||||
@ -26,6 +26,9 @@ func (c *client) get(endpoint string, out interface{}, query url.Values) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Yahoo finance requires user agent string now?
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0")
|
||||||
|
|
||||||
res, err := c.httpClient.Do(req)
|
res, err := c.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("http get request failed: %v", err)
|
return fmt.Errorf("http get request failed: %v", err)
|
||||||
@ -50,7 +53,7 @@ func (c *client) processResponse(res *http.Response, out interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
if res.StatusCode != 200 {
|
||||||
return fmt.Errorf("got http response status '%d' but expected 200", res.StatusCode)
|
return fmt.Errorf("got http response status '%d' but expected 200 for request at URL '%s'", res.StatusCode, res.Request.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(body, out)
|
err = json.Unmarshal(body, out)
|
||||||
|
@ -9,8 +9,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type security struct {
|
type security struct {
|
||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
Quantity int `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type account struct {
|
type account struct {
|
||||||
@ -47,6 +47,8 @@ func (p *Provider) Configure() error {
|
|||||||
return fmt.Errorf("failed to create new client: %v", err)
|
return fmt.Errorf("failed to create new client: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chartCache = make(map[string]chartCacheEntry)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +63,7 @@ func (p *Provider) GetBalances() ([]int, []string, error) {
|
|||||||
if err != nil {
|
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)
|
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
|
balance += int(float64(price) * p.data.Accounts[i].Securities[j].Quantity)
|
||||||
}
|
}
|
||||||
balances = append(balances, balance)
|
balances = append(balances, balance)
|
||||||
ynabAccountIDs = append(ynabAccountIDs, p.data.Accounts[i].YnabAccountID)
|
ynabAccountIDs = append(ynabAccountIDs, p.data.Accounts[i].YnabAccountID)
|
||||||
|
Reference in New Issue
Block a user