diff --git a/go.mod b/go.mod index b6aa580..b205b43 100644 --- a/go.mod +++ b/go.mod @@ -8,4 +8,5 @@ require ( github.com/spf13/cobra v1.1.3 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.7.0 + github.com/swhite24/cbpro-cost-basis v1.2.0 // indirect ) diff --git a/go.sum b/go.sum index 7f83779..be0f7ff 100644 --- a/go.sum +++ b/go.sum @@ -179,6 +179,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/swhite24/cbpro-cost-basis v1.2.0 h1:I6lMBUBz5Wl34ASUXOkHPmd9gV/C2stdIT1leiXZ8u8= +github.com/swhite24/cbpro-cost-basis v1.2.0/go.mod h1:OKAw72/CbMQT7Kkjh+Qm+py2THvTsM0682A4GhdB9UE= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= diff --git a/pkg/book/book.go b/pkg/book/book.go new file mode 100644 index 0000000..ccba0ab --- /dev/null +++ b/pkg/book/book.go @@ -0,0 +1,17 @@ +package book + +import ( + "strconv" + + "github.com/preichenberger/go-coinbasepro/v2" +) + +// GetPrice delivers the best ask price for a particular product +func GetPrice(client *coinbasepro.Client, product string) (float64, error) { + book, err := client.GetBook(product, 1) + if err != nil { + return 0, err + } + + return strconv.ParseFloat(book.Asks[0].Price, 64) +} diff --git a/pkg/cmd/cbprobuy.go b/pkg/cmd/cbprobuy.go index d9294ba..fc4d1de 100644 --- a/pkg/cmd/cbprobuy.go +++ b/pkg/cmd/cbprobuy.go @@ -3,10 +3,17 @@ package cmd import ( "fmt" "os" + "strconv" + "time" + "github.com/preichenberger/go-coinbasepro/v2" "github.com/spf13/cobra" + "github.com/swhite24/cbpro-buy/pkg/book" "github.com/swhite24/cbpro-buy/pkg/config" "github.com/swhite24/cbpro-buy/pkg/purchase" + + basisconfig "github.com/swhite24/cbpro-cost-basis/pkg/config" + "github.com/swhite24/cbpro-cost-basis/pkg/costbasis" ) var ( @@ -19,13 +26,52 @@ func init() { var currency, product string var useCoinbase, useSandbox, autoDeposit bool var amount float64 + var useBasis bool + var basisWindowStart, basisMultiplier float64 CBProBuyCmd = &cobra.Command{ Use: "cbpro-buy", Short: "cbpro-buy purchases crypto from coinbase pro with auto deposit", Run: func(cmd *cobra.Command, args []string) { cfg := config.InitializeConfig(cmd.Flags()) - err := purchase.InitiatePurchase(cfg) + client := initializeClient(cfg) + fmt.Println(cfg) + + // Determine basis if configured + if cfg.UseBasis { + // Calculate average cost + c := &basisconfig.Config{ + Key: cfg.Key, + Passphrase: cfg.Passphrase, + Secret: cfg.Secret, + Product: fmt.Sprintf("%s-%s", cfg.Product, cfg.Currency), + StartDate: time.Now().Add(time.Duration(cfg.BasisWindowStart*-24) * time.Hour), + EndDate: time.Now(), + } + info, err := costbasis.Calculate(client, c) + if err != nil { + fmt.Println("failed to calculate basis") + fmt.Println(err) + os.Exit(1) + } + fmt.Println(c, info, err) + + // Get current price + average, _ := strconv.ParseFloat(info.AverageCost, 64) + price, err := book.GetPrice(client, fmt.Sprintf("%s-%s", cfg.Product, cfg.Currency)) + if err != nil { + fmt.Println("failed to calculate current price") + fmt.Println(err) + os.Exit(1) + } + + // Update purchase amount if current price is less than average cost + if price < average { + cfg.Amount = cfg.Amount * cfg.BasisMultiplier + } + } + + err := purchase.InitiatePurchase(client, cfg) if err != nil { fmt.Println("failed to purchase") fmt.Println(err) @@ -46,4 +92,18 @@ func init() { CBProBuyCmd.Flags().BoolVar(&useSandbox, "sandbox", false, "Whether to use coinbase pro sandbox environment (will require different api key") CBProBuyCmd.Flags().BoolVar(&autoDeposit, "autodeposit", false, "Whether to auto deposit funds if current account is less than amount") CBProBuyCmd.Flags().Float64Var(&amount, "amount", 50, "Amount of product to purchase") + CBProBuyCmd.Flags().BoolVar(&useBasis, "use-basis", false, "Whether to adjust purchase amount if current price is below average cost over time window") + CBProBuyCmd.Flags().Float64Var(&basisWindowStart, "basis-window-start", 30, "Mumber of days in the past to for beginning of basis window") + CBProBuyCmd.Flags().Float64Var(&basisMultiplier, "basis-multiplier", 1.5, "Scale to apply to purchase amount if current price is less than average cost") +} + +func initializeClient(cfg *config.Config) *coinbasepro.Client { + client := coinbasepro.NewClient() + client.UpdateConfig(&coinbasepro.ClientConfig{ + BaseURL: cfg.BaseURL, + Key: cfg.Key, + Passphrase: cfg.Passphrase, + Secret: cfg.Secret, + }) + return client } diff --git a/pkg/config/config.go b/pkg/config/config.go index e8a1e1e..c39c6a3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,6 +1,8 @@ package config import ( + "strings" + "github.com/spf13/pflag" "github.com/spf13/viper" ) @@ -21,6 +23,11 @@ type ( UseCoinbase bool `json:"coinbase" mapstructure:"coinbase"` AutoDeposit bool `json:"autodeposit" mapstructure:"autodeposit"` Amount float64 `json:"amount"` + + // Basis adjustment configuration + UseBasis bool `json:"use_basis" mapstructure:"use-basis"` + BasisWindowStart float64 `json:"basis_window_start" mapstructure:"basis-window-start"` + BasisMultiplier float64 `json:"basis_multiplier" mapstructure:"basis-multiplier"` } ) @@ -29,6 +36,7 @@ func InitializeConfig(flags *pflag.FlagSet) *Config { viper.BindPFlags(flags) viper.SetEnvPrefix("CBPRO_BUY") + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) viper.AutomaticEnv() c := Config{BaseURL: "https://api.pro.coinbase.com"} diff --git a/pkg/purchase/purchase.go b/pkg/purchase/purchase.go index 96a3ed6..2559c68 100644 --- a/pkg/purchase/purchase.go +++ b/pkg/purchase/purchase.go @@ -10,27 +10,27 @@ import ( "github.com/swhite24/cbpro-buy/pkg/config" ) -var ( - client *coinbasepro.Client +type ( + purchaser struct { + cfg *config.Config + client *coinbasepro.Client + } ) // InitiatePurchase conducts the necessary operations to deposit funds and purchase crypto -func InitiatePurchase(cfg *config.Config) error { +func InitiatePurchase(client *coinbasepro.Client, cfg *config.Config) error { + p := &purchaser{cfg, client} + return p.initiatePurchase() +} + +func (p *purchaser) initiatePurchase() error { var err error var fundingAccount *coinbasepro.Account var initialBalance float64 - client = coinbasepro.NewClient() - client.UpdateConfig(&coinbasepro.ClientConfig{ - BaseURL: cfg.BaseURL, - Key: cfg.Key, - Passphrase: cfg.Passphrase, - Secret: cfg.Secret, - }) - // Gather details on current account balance to know if funds are available fmt.Println("Fetching current funding account status.") - if fundingAccount, err = getAccount(cfg.Currency); err != nil { + if fundingAccount, err = p.getAccount(p.cfg.Currency); err != nil { return err } if initialBalance, err = strconv.ParseFloat(fundingAccount.Balance, 64); err != nil { @@ -40,14 +40,14 @@ func InitiatePurchase(cfg *config.Config) error { fmt.Printf("Success. Available balance: %f\n", initialBalance) // Check if current balance is sufficient - if initialBalance < cfg.Amount { - fmt.Printf("Available balance is less than requested purchase: %f\n", cfg.Amount) - if !cfg.AutoDeposit { - return errors.New("Insufficient funds for purchase") + if initialBalance < p.cfg.Amount { + fmt.Printf("Available balance is less than requested purchase: %f\n", p.cfg.Amount) + if !p.cfg.AutoDeposit { + return errors.New("insufficient funds for purchase") } // initiate and wait for deposit - fmt.Printf("Initiating deposit of %f %s\n", cfg.Amount, cfg.Currency) - if err = deposit(cfg.Currency, cfg.Amount, cfg.UseCoinbase); err != nil { + fmt.Printf("Initiating deposit of %f %s\n", p.cfg.Amount, p.cfg.Currency) + if err = p.deposit(p.cfg.Currency, p.cfg.Amount, p.cfg.UseCoinbase); err != nil { return err } @@ -56,7 +56,7 @@ func InitiatePurchase(cfg *config.Config) error { go func(ch chan int, cfg *config.Config) { for { - account, err := getAccount(cfg.Currency) + account, err := p.getAccount(cfg.Currency) if err != nil { continue } @@ -73,7 +73,7 @@ func InitiatePurchase(cfg *config.Config) error { } time.Sleep(3 * time.Second) } - }(ready, cfg) + }(ready, p.cfg) fmt.Println("Waiting for deposit to be available in account.") select { @@ -85,14 +85,14 @@ func InitiatePurchase(cfg *config.Config) error { } // Make purchase - fmt.Printf("Initiating purchase of %f %s worth of %s\n", cfg.Amount, cfg.Currency, cfg.Product) - return purchase(cfg.Product, cfg.Currency, cfg.Amount) + fmt.Printf("Initiating purchase of %f %s worth of %s\n", p.cfg.Amount, p.cfg.Currency, p.cfg.Product) + return p.purchase(p.cfg.Product, p.cfg.Currency, p.cfg.Amount) } -func getAccount(typ string) (*coinbasepro.Account, error) { +func (p *purchaser) getAccount(typ string) (*coinbasepro.Account, error) { var err error var accounts []coinbasepro.Account - accounts, err = client.GetAccounts() + accounts, err = p.client.GetAccounts() if err != nil { return nil, err } @@ -103,11 +103,11 @@ func getAccount(typ string) (*coinbasepro.Account, error) { } } - return nil, fmt.Errorf("Unable to find %s account", typ) + return nil, fmt.Errorf("unable to find %s account", typ) } -func purchase(product, currency string, amount float64) error { - _, err := client.CreateOrder(&coinbasepro.Order{ +func (p *purchaser) purchase(product, currency string, amount float64) error { + _, err := p.client.CreateOrder(&coinbasepro.Order{ Side: "buy", Type: "market", ProductID: fmt.Sprintf("%s-%s", product, currency), @@ -116,18 +116,18 @@ func purchase(product, currency string, amount float64) error { return err } -func deposit(currency string, amount float64, coinbase bool) error { +func (p *purchaser) deposit(currency string, amount float64, coinbase bool) error { if coinbase { // TODO return errors.New("coinbase deposit not yet implemented") } - pm, err := getPaymentMethod(currency) + pm, err := p.getPaymentMethod(currency) if err != nil { return err } - _, err = client.CreateDeposit(&coinbasepro.Deposit{ + _, err = p.client.CreateDeposit(&coinbasepro.Deposit{ Currency: "USD", Amount: fmt.Sprintf("%.2f", amount), PaymentMethodID: pm.ID, @@ -135,9 +135,9 @@ func deposit(currency string, amount float64, coinbase bool) error { return err } -func getPaymentMethod(currency string) (*coinbasepro.PaymentMethod, error) { +func (p *purchaser) getPaymentMethod(currency string) (*coinbasepro.PaymentMethod, error) { var pm coinbasepro.PaymentMethod - pms, err := client.GetPaymentMethods() + pms, err := p.client.GetPaymentMethods() if err != nil { return nil, err } diff --git a/terraform/main.tf b/terraform/main.tf index fb70ad9..7ded78c 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -1,10 +1,10 @@ -data archive_file zip { +data "archive_file" "zip" { type = "zip" source_file = "../bin/${var.executable}" output_path = var.archive } -resource aws_iam_role iam_for_lambda { +resource "aws_iam_role" "iam_for_lambda" { name = "iam_for_lambda" assume_role_policy = <