ynab-portfolio-monitor/main.go
Steven Polley bb7d0a29ea
All checks were successful
continuous-integration/drone/push Build is passing
add support for bitcoin
2023-11-12 16:50:46 -07:00

173 lines
4.8 KiB
Go

package main
import (
"fmt"
"log"
"os"
"strconv"
"time"
"deadbeef.codes/steven/ynab-portfolio-monitor/bitcoin"
"deadbeef.codes/steven/ynab-portfolio-monitor/questrade"
"deadbeef.codes/steven/ynab-portfolio-monitor/ynab"
)
var (
persistentData *PersistentData
questradeClient *questrade.Client
ynabClient *ynab.Client
bitcoinClient *bitcoin.Client
questradeAccountIDs []int
questradeYnabAccountIDs []string
bitcoinAddresses []string
bitcoinYnabAccountID string
)
func init() {
log.Printf("ynab-portfolio-monitor init")
// Load application configuration from environment variables
envVars := make(map[string]string)
envVars["questrade_refresh_token"] = os.Getenv("questrade_refresh_token")
envVars["ynab_secret"] = os.Getenv("ynab_secret")
envVars["ynab_budget_id"] = os.Getenv("ynab_budget_id")
envVars["bitcoin_ynab_account"] = os.Getenv("bitcoin_ynab_account")
// Validate that all required environment variables are set
for key, value := range envVars {
if value == "" {
log.Fatalf("shell environment variable %s is not set", key)
}
}
// questrade
questradeAccountIDs = make([]int, 0)
questradeYnabAccountIDs = make([]string, 0)
for i := 0; true; i++ {
questradeAccountIDString := os.Getenv(fmt.Sprintf("questrade_account_%d", i))
ynabAccountID := os.Getenv(fmt.Sprintf("questrade_ynab_account_%d", i))
if questradeAccountIDString == "" || ynabAccountID == "" {
break
}
questradeAccountID, err := strconv.Atoi(questradeAccountIDString)
if err != nil {
log.Fatalf("failed to convert environment variable questrade_account_%d with value of '%s' to integer: %v", i, questradeAccountIDString, err)
}
questradeAccountIDs = append(questradeAccountIDs, questradeAccountID)
questradeYnabAccountIDs = append(questradeYnabAccountIDs, ynabAccountID)
}
// bitcoin
bitcoinAddresses = make([]string, 0)
for i := 0; true; i++ {
bitcoinAddress := os.Getenv(fmt.Sprintf("bitcoin_address_%d", i))
if bitcoinAddress == "" {
break
}
bitcoinAddresses = append(bitcoinAddresses, bitcoinAddress)
}
bitcoinYnabAccountID = envVars["bitcoin_ynab_account"]
// Load persistent data
var err error
persistentData, err = loadPersistentData()
if err != nil {
log.Fatalf("failed to load persistent data: %v", err)
}
// ynab client is static and has no persistent data so is initialized here and not in main program loop
ynabClient, err = ynab.NewClient(envVars["ynab_budget_id"], envVars["ynab_secret"])
if err != nil {
log.Fatalf("failed to create ynab client: %v", err)
}
bitcoinClient, err = bitcoin.NewClient()
if err != nil {
log.Fatalf("failed to create bitcoin client: %v", err)
}
}
func main() {
for {
var err error
// Questrade authentication needs to be refreshed and persistentData written to disk in case app restarts
questradeClient, err = questrade.NewClient(persistentData.QuestradeRefreshToken)
if err != nil {
log.Printf("failed to create questrade client: %v", err)
time.Sleep(time.Minute * 5) // prevent multiple fast login failures
continue
}
persistentData.QuestradeRefreshToken = questradeClient.Credentials.RefreshToken
err = savePersistentData(*persistentData)
if err != nil {
log.Fatalf("failed to save persistent data: %v", err)
}
// Update Questrade accounts
err = syncQuestrade()
if err != nil {
log.Fatalf("failed to sync questrade to ynab: %v", err)
}
// Update Bitcoin account
err = syncBitoin()
if err != nil {
log.Fatalf("failed to sync bitcoin to ynab: %v", err)
}
// TBD: Update ComputerShare account
log.Print("Sleeping for 6 hours...")
time.Sleep(time.Hour * 6)
}
}
func syncQuestrade() error {
for i, questradeAccountID := range questradeAccountIDs {
questradeBalance, err := questradeClient.GetQuestradeAccountBalance(questradeAccountID)
if err != nil {
return fmt.Errorf("failed to get questrade account balance for account ID '%d': %v", questradeAccountID, err)
}
err = ynabClient.SetAccountBalance(questradeYnabAccountIDs[i], questradeBalance)
if err != nil {
return fmt.Errorf("failed to set YNAB account balance: %v", err)
}
}
return nil
}
func syncBitoin() error {
var satoshiBalance int
for _, bitcoinAddress := range bitcoinAddresses {
addressResponse, err := bitcoinClient.GetAddress(bitcoinAddress)
if err != nil {
return fmt.Errorf("failed to get bitcoin address '%s': %v", bitcoinAddress, err)
}
satoshiBalance += addressResponse.ChainStats.FundedTxoSum - addressResponse.ChainStats.SpentTxoSum
}
fiatBalance, err := bitcoinClient.ConvertBTCToCAD(satoshiBalance)
if err != nil {
return fmt.Errorf("failed to convert satoshi balance to fiat balance: %v", err)
}
err = ynabClient.SetAccountBalance(bitcoinYnabAccountID, fiatBalance)
if err != nil {
return fmt.Errorf("failed to set YNAB account balance: %v", err)
}
return nil
}