Compare commits
	
		
			2 Commits
		
	
	
		
			7284545571
			...
			67fcfeb177
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 67fcfeb177 | |||
| 7cdf013b1e | 
| @@ -3,6 +3,7 @@ package main | ||||
| import ( | ||||
| 	"deadbeef.codes/steven/ynab-portfolio-monitor/bitcoin" | ||||
| 	"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 | ||||
| @@ -17,4 +18,5 @@ type AccountProvider interface { | ||||
| var allProviders []AccountProvider = []AccountProvider{ | ||||
| 	&questrade.Provider{}, | ||||
| 	&bitcoin.Provider{}, | ||||
| 	&staticjsonFinnhub.Provider{}, | ||||
| } | ||||
|   | ||||
| @@ -40,7 +40,7 @@ func (l LoginCredentials) authHeader() string { | ||||
|  | ||||
| // 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", c.Credentials.ApiServer+endpoint+query.Encode(), nil) | ||||
| 	req, err := http.NewRequest("GET", c.Credentials.ApiServer+endpoint+"?"+query.Encode(), nil) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|   | ||||
| @@ -123,7 +123,7 @@ func loadPersistentData() (*persistentData, error) { | ||||
| 	defer f.Close() | ||||
| 	b, err := io.ReadAll(f) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to read file data/questrade-data.jsonn: %v", err) | ||||
| 		return nil, fmt.Errorf("failed to read file data/questrade-data.json: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	err = json.Unmarshal(b, data) | ||||
|   | ||||
							
								
								
									
										23
									
								
								staticjsonFinnhub/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								staticjsonFinnhub/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| # 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 | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     ] | ||||
| } | ||||
| ``` | ||||
							
								
								
									
										84
									
								
								staticjsonFinnhub/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								staticjsonFinnhub/client.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| 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 | ||||
| } | ||||
							
								
								
									
										100
									
								
								staticjsonFinnhub/providerImpl.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								staticjsonFinnhub/providerImpl.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| 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 | ||||
| } | ||||
							
								
								
									
										26
									
								
								staticjsonFinnhub/quote.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								staticjsonFinnhub/quote.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| 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 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user