Skip to content

Commit

Permalink
Merge pull request #3 from swhite24/feature/weighted-buys
Browse files Browse the repository at this point in the history
Feature/weighted buys
  • Loading branch information
swhite24 authored Apr 16, 2021
2 parents abdf381 + 89502ba commit 8e0b30c
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 53 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
17 changes: 17 additions & 0 deletions pkg/book/book.go
Original file line number Diff line number Diff line change
@@ -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)
}
62 changes: 61 additions & 1 deletion pkg/cmd/cbprobuy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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)
Expand All @@ -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
}
8 changes: 8 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package config

import (
"strings"

"github.com/spf13/pflag"
"github.com/spf13/viper"
)
Expand All @@ -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"`
}
)

Expand All @@ -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"}
Expand Down
64 changes: 32 additions & 32 deletions pkg/purchase/purchase.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}

Expand All @@ -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
}
Expand All @@ -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 {
Expand All @@ -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
}
Expand All @@ -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),
Expand All @@ -116,28 +116,28 @@ 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,
})
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
}
Expand Down
17 changes: 9 additions & 8 deletions terraform/main.tf
Original file line number Diff line number Diff line change
@@ -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 = <<EOF
Expand All @@ -24,7 +24,7 @@ resource aws_iam_role iam_for_lambda {
EOF
}

resource aws_lambda_function cbpro_buy {
resource "aws_lambda_function" "cbpro_buy" {
filename = var.archive
function_name = var.function_name
role = aws_iam_role.iam_for_lambda.arn
Expand All @@ -42,11 +42,12 @@ resource aws_lambda_function cbpro_buy {
CBPRO_BUY_PRODUCT = var.product
CBPRO_BUY_AMOUNT = var.amount
CBPRO_BUY_AUTODEPOSIT = var.auto_deposit
CBPRO_BUY_USE_BASIS = var.use_basis
}
}
}

resource aws_iam_policy lambda_logging {
resource "aws_iam_policy" "lambda_logging" {
name = "lambda_logging"
path = "/"
description = "IAM policy for logging from a lambda"
Expand All @@ -69,21 +70,21 @@ resource aws_iam_policy lambda_logging {
EOF
}

resource aws_iam_role_policy_attachment lambda_logs {
resource "aws_iam_role_policy_attachment" "lambda_logs" {
role = aws_iam_role.iam_for_lambda.name
policy_arn = aws_iam_policy.lambda_logging.arn
}

resource aws_cloudwatch_event_rule event_rule {
resource "aws_cloudwatch_event_rule" "event_rule" {
schedule_expression = var.lambda_schedule_expression
}

resource aws_cloudwatch_event_target event_target {
resource "aws_cloudwatch_event_target" "event_target" {
rule = aws_cloudwatch_event_rule.event_rule.name
arn = aws_lambda_function.cbpro_buy.arn
}

resource aws_lambda_permission cloudwatch_permission {
resource "aws_lambda_permission" "cloudwatch_permission" {
statement_id = "AllowExecutionFromCloudWatch"
action = "lambda:InvokeFunction"
function_name = var.function_name
Expand Down
25 changes: 13 additions & 12 deletions terraform/var.tf
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
variable region { default = "us-east-1" }
variable function_name { default = "cbpro-buy-weekly" }
variable lambda_schedule_expression { default = "cron(0 5 * * ? *)" }
variable executable { default = "cbpro-buy-lambda" }
variable archive { default = "cbpro-buy-lambda.zip" }
variable "region" { default = "us-east-1" }
variable "function_name" { default = "cbpro-buy-weekly" }
variable "lambda_schedule_expression" { default = "cron(0 5 * * ? *)" }
variable "executable" { default = "cbpro-buy-lambda" }
variable "archive" { default = "cbpro-buy-lambda.zip" }
# set to 1 or null
variable auto_deposit { default = 1 }
variable amount { default = 50 }
variable currency { default = "USD" }
variable product { default = "BTC" }
variable cbpro_key {
variable "auto_deposit" { default = 1 }
variable "amount" { default = 50 }
variable "currency" { default = "USD" }
variable "product" { default = "BTC" }
variable "use_basis" { default = true }
variable "cbpro_key" {
type = string
sensitive = true
}
variable cbpro_passphrase {
variable "cbpro_passphrase" {
type = string
sensitive = true
}
variable cbpro_secret {
variable "cbpro_secret" {
type = string
sensitive = true
}

0 comments on commit 8e0b30c

Please sign in to comment.