Compare commits

..

10 Commits

Author SHA1 Message Date
3c274c614b add partial share ownership support (float)... and penny rounding errors.
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2025-05-07 21:14:02 -06:00
61074bfd80 implement rudimentary caching for yahoo finance to avoid http 429 rate limiting
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-07 19:59:30 -06:00
13291da691 fix double slash in URL
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-07 12:16:55 -06:00
3fbfbab7d6 output URL in the https response error message
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-07 12:14:48 -06:00
a34dca1076 spoof user agent string for yahoo finance API
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-07 12:06:39 -06:00
0110941ac7 attempt to avoid yahoo finance rate limiting
All checks were successful
continuous-integration/drone/push Build is passing
2025-05-07 11:57:33 -06:00
43cd399c18 Fix concurrent error handling for BTC provider
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2025-01-23 23:31:25 -07:00
88552ba042 fix nil pointer dereference
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2024-11-14 09:25:39 -07:00
068004ba14 bitcoin address failure - actually check the error
All checks were successful
continuous-integration/drone/push Build is passing
2024-11-11 06:25:26 -07:00
287acc03eb change from deadbeef.codes to code.stevenpolley.net 2024-11-11 06:24:37 -07:00
8 changed files with 52 additions and 16 deletions

View File

@ -1,10 +1,10 @@
package main
import (
"deadbeef.codes/steven/ynab-portfolio-monitor/providers/bitcoin"
"deadbeef.codes/steven/ynab-portfolio-monitor/providers/questrade"
"deadbeef.codes/steven/ynab-portfolio-monitor/providers/staticjsonFinnhub"
"deadbeef.codes/steven/ynab-portfolio-monitor/providers/staticjsonYahooFinance"
"code.stevenpolley.net/steven/ynab-portfolio-monitor/providers/bitcoin"
"code.stevenpolley.net/steven/ynab-portfolio-monitor/providers/questrade"
"code.stevenpolley.net/steven/ynab-portfolio-monitor/providers/staticjsonFinnhub"
"code.stevenpolley.net/steven/ynab-portfolio-monitor/providers/staticjsonYahooFinance"
)
// AccountProvider is the base set of requirements to be implemented for any integration

2
go.mod
View File

@ -1,4 +1,4 @@
module deadbeef.codes/steven/ynab-portfolio-monitor
module code.stevenpolley.net/steven/ynab-portfolio-monitor
go 1.22

View File

@ -7,7 +7,7 @@ import (
"text/template"
"time"
"deadbeef.codes/steven/ynab-portfolio-monitor/ynab"
"code.stevenpolley.net/steven/ynab-portfolio-monitor/ynab"
)
var (

View File

@ -55,14 +55,19 @@ func (p *Provider) GetBalances() ([]int, []string, error) {
defer wg.Done()
addressResponse, err := p.client.getAddress(bitcoinAddress)
if err != nil {
err := fmt.Errorf("failed to get bitcoin address '%s': %v", bitcoinAddress, err)
goErr = &err
err := fmt.Errorf("failed to get BTC balance for bitcoin address '%s': %v", bitcoinAddress, err)
if err != nil {
goErr = &err
}
return
}
satoshiBalance += addressResponse.ChainStats.FundedTxoSum - addressResponse.ChainStats.SpentTxoSum
}(goErr)
}
wg.Wait()
if goErr != nil {
return nil, nil, *goErr
}
fiatBalance, err := p.client.convertBTCToCAD(satoshiBalance)
if err != nil {

View 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
}

View File

@ -3,10 +3,11 @@ package staticjsonYahooFinance
import (
"fmt"
"net/url"
"time"
)
// A chart response is what we get back from Yahoo Finance
type chart struct {
// A chartResponse response is what we get back from Yahoo Finance
type chartResponse struct {
Chart struct {
Result []struct {
Meta struct {
@ -69,8 +70,16 @@ type chart struct {
} `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) {
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{})
if err != nil {
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))
}
chartCache[symbol] = chartCacheEntry{Chart: *chartResponse, LastUpdated: time.Now()}
return int(chartResponse.Chart.Result[0].Meta.RegularMarketPrice * 1000), nil
}

View File

@ -9,7 +9,7 @@ import (
"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
// 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
}
// 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)
if err != nil {
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 {
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)

View File

@ -9,8 +9,8 @@ import (
)
type security struct {
Symbol string `json:"symbol"`
Quantity int `json:"quantity"`
Symbol string `json:"symbol"`
Quantity float64 `json:"quantity"`
}
type account struct {
@ -47,6 +47,8 @@ func (p *Provider) Configure() error {
return fmt.Errorf("failed to create new client: %v", err)
}
chartCache = make(map[string]chartCacheEntry)
return nil
}
@ -61,7 +63,7 @@ func (p *Provider) GetBalances() ([]int, []string, error) {
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
balance += int(float64(price) * p.data.Accounts[i].Securities[j].Quantity)
}
balances = append(balances, balance)
ynabAccountIDs = append(ynabAccountIDs, p.data.Accounts[i].YnabAccountID)