Giter VIP home page Giter VIP logo

sats-stacker's Introduction

Sats Stacker

A simple tool to stack, create orders to try and buy dips, withdraw sats from exchanges. At the moment only the Kraken plugin is implemented.

sats-stacker is intented to be run through a scheduler like Systemd Timer or Crontab and is is provided as Docker Images and pre-compiled binaries.

Use this at your own risk and decide for yourself whether or not you want to run this tool - access to your exchange account , and so your funds, is required so you are expected to validate the Code before running it. This software is provided as it is.

Supported Exchanges

Kraken

You will need to get your Kraken API Key and Secret Key from the Api Settings page.

Required permissions are:

  • Query Funds
  • Modify Orders
  • Withdraw Funds ( Only if you plan to automate the withdrawal )

Note sats-stacker, on Kraken exchange, can only withdraw to a pre-configured withdrawal address referenced by name. You will need to create it using the UI before you can automate withdrawals. This is to avoid allowing this tool to create new withdrawal addresses on kraken and potentially loose you funds.

Supported Notifiers

sats-stacker support sending notifications for each stack or withdraw event so you always know how your stacking is going.

The infrastructure for notifications is pluggable but somewhat limited at the moment to STDOUT and SIMPLEPUSH.IO

STDOUT

The simplest of notifications. Just redirect stdout to whatever/wherever you like

SIMPLEPUSH

Send notification using SIMPLEPUSH.IO notification infrastructure with optional support for encrypting your message.

Help

Global Options

GLOBAL OPTIONS:
   --debug, -d            debug logging (default: false) [$STACKER_DEBUG]
   --dry-run, --validate  dry-run (default: true) [$STACKER_VALIDATE, $STACKER_DRY_RUN]
   --exchange value       Exchange ['kraken', 'binance'] (default: "kraken") [$STACKER_EXCHANGE]
   --api-key value        Exchange Api Key [$STACKER_API_KEY]
   --secret-key value     Exchange Api Secret [$STACKER_SECRET_KEY, $STACKER_API_SECRET]
   --notifier value       What notifier to use ['stdout','simplepush'] (default: "stdout") [$STACKER_NOTIFIER]
   --sp-encrypt           Simplepush: If set, the message will be sent end-to-end encrypted with the provided Password/Salt. If false, the message is sent unencrypted. (default: true) [$STACKER_SP_ENCRYPT]
   --sp-key value         Simplepush: Your simplepush.io Key [$STACKER_SP_KEY]
   --sp-event value       Simplepush: The event the message should be associated with [$STACKER_SP_EVENT]
   --sp-password value    Simplepush: Encryption Password [$STACKER_SP_PASSWORD]
   --sp-salt value        Simplepush: The salt for the encrypted message [$STACKER_SP_SALT]
   --help, -h             show help (default: false)
   --version, -v          print the version (default: false)

Stack command options

the stack command will buy the specified amount of fiat in btc using a market value order

This is the default Dollar Cost Averaging mode and will buy you some btc every time you run it.

OPTIONS:
   --amount value                    Amount of fiat to exchange (default: 0) [$STACKER_STACK_AMOUNT]
   --fiat value                      Fiat to exchange [$STACKER_STACK_FIAT]
   --order-type value, --type value  Order type (default: "limit") [$STACKER_STACK_ORDER_TYPE]
   --help, -h                        show help (default: false)

BTD command options

the btd command will place a number of orders at progressively more discounted prices and progressively higher amount of fiat, trying to catch a DIP in price.

this mode will first delete any previous btd order, will then place the new orders based on the budget it is allowed to spend.
The frequency at which you run this command will define the window of time in which you will try and catch a DIP.

The first order will be placed at a discount of dip-percentage from the current ASK price for a given amount of fiat. Every successive order will apply a bigger discount, based on dip-increments for an amount of fiat higher the previous one.

OPTIONS:
   --budget value                     Budget to allocate for the DIPs, set to 0 to allocate all of the available budget (default: 0) [$STACKER_BTD_BUDGET]
   --dip-percentage value             Initial percentage of the firt dip, the other values will be calculated (default: 10) [$STACKER_BTD_DIP_PERCENTAGE]
   --dip-increments value             Increment of dip percentage for each order (default: 5) [$STACKER_BTD_DIP_INCREMENTS_PERCENTAGE]
   --n-orders value                   Number of DIPS orders to place (default: 5) [$STACKER_BTD_DIP_N_ORDERS]
   --high-price-gap-percentage value  Gap between current price and high price to trigger modifier (default: 5) [$STACKER_BTD_HIGH_PRICE_GAP_PERCENTAGE]
   --fiat value                       Fiat to exchange [$STACKER_BTD_FIAT]
   --help, -h                         show help (default: false)
Example BTD orders
  • Budget: 500
  • Fiat: Eur
  • Dip-Percentage: 5%
  • Dip-Ingrements: 5%
  • N of Orders: 4
  • Current Ask Price: 10000EUR

Given this conditions the following orders would be placed:

  • 1st Order: FIAT: 50EUR - PRICE: 10000 - ( 5% of 10000) = 9500 - Volume: 0.00526315789 BTC
  • 2nd Order: FIAT: 100EUR - PRICE: 10000 - ( 10% of 10000) = 9000 - Volume: 0.01111111111 BTC
  • 3rd Order: FIAT: 150EUR - PRICE: 10000 - ( 15% of 10000) = 8500 - Volume: 0.01764705882 BTC
  • 4th Order: FIAT: 200EUR - PRICE: 10000 - ( 20% of 10000) = 8000 - Volume: 0.025 BTC
Gap from High price discount

A modifier to the discount will be applied based on the gap between the current Ask Price and the Highest price in the last week. ( This is currently not configurable)
The bigger the Gap between the current Ask price and the highest price in the last week the bigger the modifier is up to 40% of the actualy dip-discount specified.

This cannot be disabled right now but by using the --high-price-gap-percentage flag you can trick sats-stacker to never initiate this modifier by setting it to 100 since it will only apply if the gap
between current Ask Price and Highest price in the last week is > than that value.

Frequency of run

In order for the tool to try and catch the Dips you need to run the tool on a regular basis, the frequency at which you run it will determine the window of time in which you will try to catch a dip. At every run the old orders will be deleted and new ones, based on the current Ask Price, will be created

I run the tool every 2 hour with an initial dip value of 5% , so i try to catch any dip of 5% in 2 hours of time.

The right values are entirely up to you depending on how big and how fast of a dip you are trying to catch

Withdraw command options

OPTIONS:
   --max-fee value  Max fee in percentage, only withdraw if the relative fee does not exceed this limit (default: 0) [$STACKER_WITHDRAW_MAX_FEE]
   --address value  Address to withdraw to, the actual value will depend on the exchange selected [$STACKER_WITHDRAW_ADDRESS]
   --help, -h       show help (default: false)

Configuration

sats-stacker support being configured either via Environment variables or cli arguments.

Running sats-stacker using systemd timers

Example systemd units and timers , and environment configuration files, can be found in the contrib/systemd directory

Example Run of sats-stacker

export STACKER_DEBUG=true
export STACKER_DRY_RUN=false
export STACKER_EXCHANGE=kraken
export STACKER_NOTIFIER=stdout
export STACKER_API_KEY=YOUR_KRAKEN_KEY
export STACKER_API_SECRET=YOUR_KRAKEN_SECRET_KEY

stack some sats

# Stack with a too small amount of fiat

./sats-stacker stack --amount 10 --fiat eur
INFO[2020-12-30T10:46:06+01:00] Stacking some sats on KRAKEN                  action=stack exchange=kraken
DEBU[2020-12-30T10:46:07+01:00] Balance before placing the Order              action=stack crypto=XBT cryptoBalance=0.0040813 exchange=kraken fiat=EUR fiatBalance=291.1935
Minimum volume for BTC Order on Kraken is 0.001 got 0.00044237. Consider increasing the amount of Fiat
# Increase amount of fiat so that we can stack
./sats-stacker stack --amount 25 --fiat eur 2>/dev/null

Kraken - Stack Sats

๐Ÿ™Œ Limit order successful

๐Ÿ’ฐ Balance Before Order
   Crypto  XBT: 0.004081
   Fiat EUR: 291.193500

๐Ÿ“ˆ Ask Price: 22608.00000

๐Ÿ’ธ: buy 0.00110580 XBTEUR @ limit 22608.0
๐Ÿ“Ž Transatcion: DryRun
# Stack at market price rather than ask price ( In this case the ASK Price is printed for reference only )
./sats-stacker stack --amount 25 --fiat eur 2>/dev/null

Kraken - Stack Sats

๐Ÿ™Œ Market order successful

๐Ÿ’ฐ Balance Before Order
   Crypto  XBT: 0.004081
   Fiat EUR: 291.193500

๐Ÿ“ˆ Ask Price: 22652.20000

๐Ÿ’ธ: buy 0.00110365 XBTEUR @ market
๐Ÿ“Ž Transatcion: DryRun

withdraw sats to pre-existing kraken address

# Set relative fee to 0.5%  - Won't withdraw since the relative fee of withdrawal ( Kraken fee / XBT Amount ) = 3.55% > 0.5%

./sats-stacker withdraw --address "address" --max-fee 0.5 2>/dev/null

Kraken - Withdraw Sats

๐Ÿ’ก Relative fee of withdrawal: 3.55%
โŒ Fees are too high for withdrawal

๐Ÿ‘› Kraken Address: address
๐Ÿ’ฐ Withdraw Amount XBT: 0.01408130
๐Ÿฆ Kraken Fees: 0.00050000

๐Ÿ“Ž Transatcion: DRY-RUN

Example Run of BTD Command

export STACKER_DEBUG=true
export STACKER_DRY_RUN=false
export STACKER_EXCHANGE=kraken
export STACKER_API_KEY=YOUR_KRAKEN_KEY
export STACKER_API_SECRET=YOUR_KRAKEN_SECRET_KEY

Create some Orders

./sats-stacker btd --amount 500 --fiat eur --dip-percentage 5 --dip-increments 4 --n-orders 4

Feb 27 18:16:07 DietPi docker[18792]: time="2021-02-27T18:16:07Z" level=info msg="4 Open Orders Canceled" action=btd exchange=KRAKEN userref=300

Feb 27 18:16:07 DietPi docker[18792]: time="2021-02-27T18:16:07Z" level=info msg="Order Placed 1" action=btd askPrice=39074.00000 dryrun= exchange=KRAKEN order="buy 0.00132620 XBTEUR @ limit 37701.6" order-number=1 orderFlags=fciq orderId=AAAAAA-AAAAA-AAAAAA orderType=limit price=37701.6 userref=300 volume=0.00132620
Feb 27 18:16:07 DietPi docker[18792]: time="2021-02-27T18:16:07Z" level=info msg="Order Placed 2" action=btd askPrice=39074.00000 dryrun= exchange=KRAKEN order="buy 0.00273197 XBTEUR @ limit 36603.7" order-number=2 orderFlags=fciq orderId=BBBBBB-BBBBB-BBBBBB orderType=limit price=36603.7 userref=300 volume=0.00273197
Feb 27 18:16:08 DietPi docker[18792]: time="2021-02-27T18:16:08Z" level=info msg="Order Placed 3" action=btd askPrice=39074.00000 dryrun= exchange=KRAKEN order="buy 0.00422467 XBTEUR @ limit 35505.7" order-number=3 orderFlags=fciq orderId=CCCCCC-CCCCC-CCCCCC orderType=limit price=35505.7 userref=300 volume=0.00422467
Feb 27 18:16:08 DietPi docker[18792]: time="2021-02-27T18:16:08Z" level=info msg="Order Placed 4" action=btd askPrice=39074.00000 dryrun= exchange=KRAKEN order="buy 0.00581263 XBTEUR @ limit 34407.8" order-number=4 orderFlags=fciq orderId=DDDDDD-DDDDD-DDDDDD orderType=limit price=34407.8 userref=300 volume=0.00581263

sats-stacker's People

Contributors

fciocchetti avatar primeroz avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar

sats-stacker's Issues

Add modifier to customize drift from high price

Add modifier to customize drift from high price

// TODO Add modifier to customize drift from high price

	"github.com/sirupsen/logrus"
	"github.com/urfave/cli/v2"
	"math/big"
	"reflect"
	"strconv"
	"strings"
	//"time"
)

type Kraken struct {
	Name          string
	Action        string
	ApiKey        string
	SecretKey     string
	Crypto        string
	Fiat          string
	Api           *krakenapi.KrakenApi
	Pair          string
	BalanceCrypto float64
	BalanceFiat   float64
	Ticker        krakenapi.PairTickerInfo
	Ask           string
	AskFloat      float64
	UserRef       int32
}

const MIN_BTC_AMOUNT = 0.0002

//func getTimeInfo() (time.Time, time.Time, time.Duration) {
//	// Always use the local timezone
//	loc, _ := time.LoadLocation("Local")
//
//	now := time.Now().In(loc)
//	year, month, day := now.Date()
//
//	// Start is TODAY at 00:00
//	start := time.Date(year, month, day, 0, 0, 0, 0, now.Location())
//
//	// END is now
//	end := now
//
//	return start, end, end.Sub(start)
//}

func (k *Kraken) Config(c *cli.Context) error {
	k.Name = strings.ToTitle("kraken")
	k.ApiKey = c.String("api-key")
	k.SecretKey = c.String("secret-key")
	k.Crypto = "XBT"

	return nil
}

func (k *Kraken) Init(c *cli.Context) error {
	k.Fiat = strings.ToUpper(c.String("fiat"))
	k.Pair = "X" + k.Crypto + "Z" + k.Fiat

	k.Api = krakenapi.New(k.ApiKey, k.SecretKey)

	k.Action = c.Command.FullName()

	if k.Action != "withdraw" {
		// Initialize the current Balance
		balance, err := k.Api.Balance()
		if err != nil {
			return errors.New("Failed to get Balance. Check API and SECRET Keys")
		}

		// Extract Values from Kraken Responses
		refBalance := reflect.ValueOf(balance)
		k.BalanceCrypto = reflect.Indirect(refBalance).FieldByName("X" + k.Crypto).Interface().(float64)
		k.BalanceFiat = reflect.Indirect(refBalance).FieldByName("Z" + k.Fiat).Interface().(float64)

		// Get the current ticker for the given PAIR
		ticker, err := k.Api.Ticker(k.Pair)
		if err != nil {
			return fmt.Errorf("Failed to get ticker for pair %s: %s", k.Pair, err)
		}

		k.Ticker = ticker.GetPairTickerInfo(k.Pair)
		k.Ask = ticker.GetPairTickerInfo(k.Pair).Ask[0]
		k.AskFloat, err = strconv.ParseFloat(k.Ask, 64)
		if err != nil {
			return fmt.Errorf("Failed to get Ask price for pair %s: %s", k.Pair, err)
		}
	}

	return nil
}

//func (k *Kraken) closedOrderTodayForUserRef(orders *krakenapi.ClosedOrdersResponse, c *cli.Context) (string, krakenapi.Order, error) {
//
//	for id, v := range orders.Closed {
//		if v.Status == "closed" {
//			return id, v, nil
//		}
//	}
//	return "", krakenapi.Order{}, nil
//}

func (k *Kraken) priceModifierBasedOnGapFromHighPrice(c *cli.Context) (float64, error) {

	// 15 interval will give a week worth of data
	ohlcs, err := k.Api.OHLCWithInterval(k.Pair, "15")
	if err != nil {
		return 0.0, fmt.Errorf("Failed to get OHLC Data for pair %s: %s", k.Pair, err)
	}

	// Find highest price in the range of OHLC
	var highest float64
	for _, o := range ohlcs.OHLC {
		if o.High > highest {
			highest = o.High
		}
	}

	// max modifier is 40% ( applied to the discount price ) when the gap is >= 25%
	maxDiscountModifier := 40.0
	var discountModifier float64
	// Is the highest price from the last week more than GAP PERCENTAGE over the current ask price ?
	gapPrice := highest - k.AskFloat
	if gapPrice > 0 {
		gapPercentage := gapPrice / highest * 100
		if gapPercentage > c.Float64("high-price-gap-percentage") {

			// calculate modifier
			discountModifier = gapPercentage / 25.0 * maxDiscountModifier
			if discountModifier > maxDiscountModifier {
				discountModifier = maxDiscountModifier
			}

			log.WithFields(logrus.Fields{
				"action":             k.Action,
				"pair":               k.Pair,
				"arg-gap-interval":   "7d",
				"arg-gap-percentage": c.Float64("high-price-gap-percentage"),
				"highest":            highest,
				"ask":                k.AskFloat,
				"gap":                gapPrice,
				"gap-percentage":     gapPercentage,
				"discount-modifier":  discountModifier,
			}).Debug("Price Gap calculator")
		}
	}

	return discountModifier, nil
}

func (k *Kraken) createOrderArgs(c *cli.Context, volume float64, price string, longshot bool) (map[string]string, error) {

	args := make(map[string]string)

	// Simple DCA
	if k.Action == "stack" {
		args["orderType"] = "market"
	} else if k.Action == "btd" {
		args["orderType"] = "limit"
	} else {
		return args, fmt.Errorf("Unknown Action: %s", k.Action)
	}

	validate := "false"
	if c.Bool("dry-run") {
		validate = "true"
		args["validate"] = "true"
	}

	args["userref"] = fmt.Sprintf("%d", k.UserRef)
	args["volume"] = strconv.FormatFloat(volume, 'f', 8, 64)
	args["price"] = price
	args["oflags"] = "fciq" // "buy" button will actually sell the quote currency in exchange for the base currency, pay fee in the the quote currenty ( fiat )

	// If volume < MIN_BTC_AMOUNT then error - this is the minimum kraken order volume
	if volume < MIN_BTC_AMOUNT {
		return args, fmt.Errorf("Minimum volume for BTC Order on Kraken is %f got %s. Consider increasing the amount of Fiat", MIN_BTC_AMOUNT, args["volume"])
	}

	log.WithFields(logrus.Fields{
		"action":     k.Action,
		"pair":       k.Pair,
		"type":       "buy",
		"orderType":  args["orderType"],
		"volume":     args["volume"],
		"price":      args["price"],
		"dryrun":     validate,
		"orderFlags": args["oflags"],
		"userref":    args["userref"],
	}).Debug("Order to execute")

	return args, nil
}

func (k *Kraken) BuyTheDips(c *cli.Context) (result string, e error) {
	// TODO Handle cancel only mode from Kraken
	// TODO Add modifier to customize drift from high price
	k.UserRef = 300

	log.WithFields(logrus.Fields{
		"action":  "btd",
		"userRef": k.UserRef,
	}).Info("Buying the DIPs on " + k.Name)

	log.WithFields(logrus.Fields{
		"action":        "btd",
		"crypto":        k.Crypto,
		"cryptoBalance": k.BalanceCrypto,
		"fiat":          k.Fiat,
		"fiatBalance":   k.BalanceFiat,
		"ask":           k.Ask,
		"budget":        c.Float64("budget"),
		"n-orders":      c.Int64("n-orders"),
	}).Debug("Balance before any action is taken")

	// Calculate order values from budget
	// Each _Unit_ will have the double the value of the unit before
	var totalOrderUnits int64
	var fiatValueUnit float64

	totalOrders := c.Int64("n-orders")
	for totalOrders != 0 {
		totalOrderUnits += totalOrders
		totalOrders -= 1
	}

	fiatValueUnit = c.Float64("budget") / float64(totalOrderUnits)

	//var dipOrders []map[string]string

	log.WithFields(logrus.Fields{
		"action":          "btd",
		"budget":          c.Float64("budget"),
		"total-sats":      fmt.Sprintf("%.8f", c.Float64("budget")/k.AskFloat),
		"dip-percentage":  c.Int64("dip-percentage"),
		"dip-increments":  c.Int64("dip-increments"),
		"n-orders":        c.Int64("n-orders"),
		"total-units":     totalOrderUnits,
		"fiat-value-unit": fiatValueUnit,
	}).Debug("Calculating orders")

	var dipOrders []map[string]string

	var orderNumber int64
	//Calculate DIP Discount for this order
	modifier, err := k.priceModifierBasedOnGapFromHighPrice(c)
	if err != nil {
		modifier = 0.0
	}

	for orderNumber != c.Int64("n-orders") {
		// Discount based on order number
		discount := float64(c.Int64("dip-percentage") + (orderNumber * c.Int64("dip-increments")))
		// Calculate modifier to apply to discount based on the gap from the Highest Weekly price
		discountModifier := (float64(discount) * modifier) / float64(100.0)
		dipDiscountedPrice := (k.AskFloat / float64(100)) * (float64(100.0) - discount + discountModifier)
		dipVolume := (fiatValueUnit * float64(orderNumber+1)) / dipDiscountedPrice

		log.WithFields(logrus.Fields{
			"action":       "btd",
			"order-number": orderNumber + 1,
			"ask-price":    k.Ask,
			"dip-discount": discount - discountModifier,
			"dip-price":    dipDiscountedPrice,
			"dip-volume":   dipVolume,
		}).Debug(fmt.Sprintf("Creating discounted order %d", orderNumber+1))

		// Create Order and add to list
		dipOrderArgs, _ := k.createOrderArgs(c, dipVolume, fmt.Sprintf("%.1f", dipDiscountedPrice), false)

		// If volume < MIN_BTC_AMOUNT then do not add to the list, skip to next iteration
		if dipVolume < MIN_BTC_AMOUNT {
			orderNumber += 1
			continue
		}

		dipOrders = append(dipOrders, dipOrderArgs)
		log.WithFields(logrus.Fields{
			"action":       "btd",
			"order-number": orderNumber + 1,
		}).Debug("Added Order to list")

		orderNumber += 1
	}

	if len(dipOrders) == 0 {
		return "", fmt.Errorf("No Orders were added to the list")
	}

	//Cancel any open order with our UserRef
	if !c.Bool("dry-run") {
		openordersArgs := make(map[string]string)
		//openordersArgs["trades"] = "true"
		openordersArgs["userref"] = fmt.Sprintf("%d", k.UserRef)

		resp, _ := k.Api.OpenOrders(openordersArgs)
		if len(resp.Open) > 0 {

			_, err := k.Api.CancelOrder(fmt.Sprintf("%d", k.UserRef))
			if err != nil {
				return "", fmt.Errorf("Failed to Cancel Orders for UserRef: %d - %s", k.UserRef, err)
			}

			log.WithFields(logrus.Fields{
				"action":  "btd",
				"userref": k.UserRef,
			}).Info(fmt.Sprintf("%d Open Orders Canceled", len(resp.Open)))
		}
	}

	//Place Orders
	orderNumber = 0
	for orderNumber != int64(len(dipOrders)) {
		thisOrder := dipOrders[orderNumber]
		order, err := k.Api.AddOrder(k.Pair, "buy", thisOrder["orderType"], thisOrder["volume"], thisOrder)
		if err != nil {
			log.WithFields(logrus.Fields{
				"action":       "btd",
				"userref":      thisOrder["userref"],
				"order-number": orderNumber + 1,
			}).Error(fmt.Sprintf("Error Creating orderNumber %d: %s", orderNumber+1, err))

			orderNumber += 1
			continue
		}

		var orderId string
		if c.Bool("dry-run") {
			orderId = "DRY-RUN"
		} else {
			orderId = strings.Join(order.TransactionIds, ",")
		}

		log.WithFields(logrus.Fields{
			"action":       "btd",
			"order":        order.Description.Order,
			"orderId":      orderId,
			"order-number": orderNumber + 1,
			"dryrun":       thisOrder["validate"],
			"orderType":    thisOrder["orderType"],
			"volume":       thisOrder["volume"],
			"askPrice":     k.Ask,
			"price":        thisOrder["price"],
			"orderFlags":   thisOrder["oflags"],
			"userref":      thisOrder["userref"],
		}).Info(fmt.Sprintf("Order Placed %d", orderNumber+1))

		orderNumber += 1
	}

	return "", nil
}

func (k *Kraken) Stack(c *cli.Context) (result string, e error) {

	k.UserRef = 100

	log.WithFields(logrus.Fields{
		"action":  "stack",
		"userRef": k.UserRef,
	}).Info("Stacking some sats on " + k.Name)

	log.WithFields(logrus.Fields{
		"action":        "stack",
		"crypto":        k.Crypto,
		"cryptoBalance": k.BalanceCrypto,
		"fiat":          k.Fiat,
		"fiatBalance":   k.BalanceFiat,
		"ask":           k.Ask,
	}).Debug("Balance before placing the Order")

	volume := (c.Float64("amount") / k.AskFloat)
	orderArgs, err := k.createOrderArgs(c, volume, k.Ask, false)
	if err != nil {
		return "", fmt.Errorf("Failed to create args to place order: %s", err)
	}

	// Place the Order
	order, err := k.Api.AddOrder(k.Pair, "buy", orderArgs["orderType"], orderArgs["volume"], orderArgs)
	if err != nil {
		log.WithFields(logrus.Fields{
			"action":  "btd",
			"userref": k.UserRef,
		}).Error(fmt.Sprintf("Error Creating order: %s", err))
		return "", fmt.Errorf("Failed to place order: %s", err)
	}

	var orderId string
	if c.Bool("dry-run") {
		orderId = "DRY-RUN"
	} else {
		orderId = strings.Join(order.TransactionIds, ",")

accc025e52fed7977012a2c16b07315bae3851cc

Handle cancel only mode from Kraken

Handle cancel only mode from Kraken

// TODO Handle cancel only mode from Kraken

	"github.com/beldur/kraken-go-api-client"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli/v2"
	"math/big"
	"reflect"
	"strconv"
	"strings"
	//"time"
)

type Kraken struct {
	Name          string
	Action        string
	ApiKey        string
	SecretKey     string
	Crypto        string
	Fiat          string
	Api           *krakenapi.KrakenApi
	Pair          string
	BalanceCrypto float64
	BalanceFiat   float64
	Ticker        krakenapi.PairTickerInfo
	Ask           string
	AskFloat      float64
	UserRef       int32
}

const MIN_BTC_AMOUNT = 0.0002

//func getTimeInfo() (time.Time, time.Time, time.Duration) {
//	// Always use the local timezone
//	loc, _ := time.LoadLocation("Local")
//
//	now := time.Now().In(loc)
//	year, month, day := now.Date()
//
//	// Start is TODAY at 00:00
//	start := time.Date(year, month, day, 0, 0, 0, 0, now.Location())
//
//	// END is now
//	end := now
//
//	return start, end, end.Sub(start)
//}

func (k *Kraken) Config(c *cli.Context) error {
	k.Name = strings.ToTitle("kraken")
	k.ApiKey = c.String("api-key")
	k.SecretKey = c.String("secret-key")
	k.Crypto = "XBT"

	return nil
}

func (k *Kraken) Init(c *cli.Context) error {
	k.Fiat = strings.ToUpper(c.String("fiat"))
	k.Pair = "X" + k.Crypto + "Z" + k.Fiat

	k.Api = krakenapi.New(k.ApiKey, k.SecretKey)

	k.Action = c.Command.FullName()

	if k.Action != "withdraw" {
		// Initialize the current Balance
		balance, err := k.Api.Balance()
		if err != nil {
			return errors.New("Failed to get Balance. Check API and SECRET Keys")
		}

		// Extract Values from Kraken Responses
		refBalance := reflect.ValueOf(balance)
		k.BalanceCrypto = reflect.Indirect(refBalance).FieldByName("X" + k.Crypto).Interface().(float64)
		k.BalanceFiat = reflect.Indirect(refBalance).FieldByName("Z" + k.Fiat).Interface().(float64)

		// Get the current ticker for the given PAIR
		ticker, err := k.Api.Ticker(k.Pair)
		if err != nil {
			return fmt.Errorf("Failed to get ticker for pair %s: %s", k.Pair, err)
		}

		k.Ticker = ticker.GetPairTickerInfo(k.Pair)
		k.Ask = ticker.GetPairTickerInfo(k.Pair).Ask[0]
		k.AskFloat, err = strconv.ParseFloat(k.Ask, 64)
		if err != nil {
			return fmt.Errorf("Failed to get Ask price for pair %s: %s", k.Pair, err)
		}
	}

	return nil
}

//func (k *Kraken) closedOrderTodayForUserRef(orders *krakenapi.ClosedOrdersResponse, c *cli.Context) (string, krakenapi.Order, error) {
//
//	for id, v := range orders.Closed {
//		if v.Status == "closed" {
//			return id, v, nil
//		}
//	}
//	return "", krakenapi.Order{}, nil
//}

func (k *Kraken) priceModifierBasedOnGapFromHighPrice(c *cli.Context) (float64, error) {

	// 15 interval will give a week worth of data
	ohlcs, err := k.Api.OHLCWithInterval(k.Pair, "15")
	if err != nil {
		return 0.0, fmt.Errorf("Failed to get OHLC Data for pair %s: %s", k.Pair, err)
	}

	// Find highest price in the range of OHLC
	var highest float64
	for _, o := range ohlcs.OHLC {
		if o.High > highest {
			highest = o.High
		}
	}

	// max modifier is 40% ( applied to the discount price ) when the gap is >= 25%
	maxDiscountModifier := 40.0
	var discountModifier float64
	// Is the highest price from the last week more than GAP PERCENTAGE over the current ask price ?
	gapPrice := highest - k.AskFloat
	if gapPrice > 0 {
		gapPercentage := gapPrice / highest * 100
		if gapPercentage > c.Float64("high-price-gap-percentage") {

			// calculate modifier
			discountModifier = gapPercentage / 25.0 * maxDiscountModifier
			if discountModifier > maxDiscountModifier {
				discountModifier = maxDiscountModifier
			}

			log.WithFields(logrus.Fields{
				"action":             k.Action,
				"pair":               k.Pair,
				"arg-gap-interval":   "7d",
				"arg-gap-percentage": c.Float64("high-price-gap-percentage"),
				"highest":            highest,
				"ask":                k.AskFloat,
				"gap":                gapPrice,
				"gap-percentage":     gapPercentage,
				"discount-modifier":  discountModifier,
			}).Debug("Price Gap calculator")
		}
	}

	return discountModifier, nil
}

func (k *Kraken) createOrderArgs(c *cli.Context, volume float64, price string, longshot bool) (map[string]string, error) {

	args := make(map[string]string)

	// Simple DCA
	if k.Action == "stack" {
		args["orderType"] = "market"
	} else if k.Action == "btd" {
		args["orderType"] = "limit"
	} else {
		return args, fmt.Errorf("Unknown Action: %s", k.Action)
	}

	validate := "false"
	if c.Bool("dry-run") {
		validate = "true"
		args["validate"] = "true"
	}

	args["userref"] = fmt.Sprintf("%d", k.UserRef)
	args["volume"] = strconv.FormatFloat(volume, 'f', 8, 64)
	args["price"] = price
	args["oflags"] = "fciq" // "buy" button will actually sell the quote currency in exchange for the base currency, pay fee in the the quote currenty ( fiat )

	// If volume < MIN_BTC_AMOUNT then error - this is the minimum kraken order volume
	if volume < MIN_BTC_AMOUNT {
		return args, fmt.Errorf("Minimum volume for BTC Order on Kraken is %f got %s. Consider increasing the amount of Fiat", MIN_BTC_AMOUNT, args["volume"])
	}

	log.WithFields(logrus.Fields{
		"action":     k.Action,
		"pair":       k.Pair,
		"type":       "buy",
		"orderType":  args["orderType"],
		"volume":     args["volume"],
		"price":      args["price"],
		"dryrun":     validate,
		"orderFlags": args["oflags"],
		"userref":    args["userref"],
	}).Debug("Order to execute")

	return args, nil
}

func (k *Kraken) BuyTheDips(c *cli.Context) (result string, e error) {
	// TODO Handle cancel only mode from Kraken
	// TODO Add modifier to customize drift from high price
	k.UserRef = 300

	log.WithFields(logrus.Fields{
		"action":  "btd",
		"userRef": k.UserRef,
	}).Info("Buying the DIPs on " + k.Name)

	log.WithFields(logrus.Fields{
		"action":        "btd",
		"crypto":        k.Crypto,
		"cryptoBalance": k.BalanceCrypto,
		"fiat":          k.Fiat,
		"fiatBalance":   k.BalanceFiat,
		"ask":           k.Ask,
		"budget":        c.Float64("budget"),
		"n-orders":      c.Int64("n-orders"),
	}).Debug("Balance before any action is taken")

	// Calculate order values from budget
	// Each _Unit_ will have the double the value of the unit before
	var totalOrderUnits int64
	var fiatValueUnit float64

	totalOrders := c.Int64("n-orders")
	for totalOrders != 0 {
		totalOrderUnits += totalOrders
		totalOrders -= 1
	}

	fiatValueUnit = c.Float64("budget") / float64(totalOrderUnits)

	//var dipOrders []map[string]string

	log.WithFields(logrus.Fields{
		"action":          "btd",
		"budget":          c.Float64("budget"),
		"total-sats":      fmt.Sprintf("%.8f", c.Float64("budget")/k.AskFloat),
		"dip-percentage":  c.Int64("dip-percentage"),
		"dip-increments":  c.Int64("dip-increments"),
		"n-orders":        c.Int64("n-orders"),
		"total-units":     totalOrderUnits,
		"fiat-value-unit": fiatValueUnit,
	}).Debug("Calculating orders")

	var dipOrders []map[string]string

	var orderNumber int64
	//Calculate DIP Discount for this order
	modifier, err := k.priceModifierBasedOnGapFromHighPrice(c)
	if err != nil {
		modifier = 0.0
	}

	for orderNumber != c.Int64("n-orders") {
		// Discount based on order number
		discount := float64(c.Int64("dip-percentage") + (orderNumber * c.Int64("dip-increments")))
		// Calculate modifier to apply to discount based on the gap from the Highest Weekly price
		discountModifier := (float64(discount) * modifier) / float64(100.0)
		dipDiscountedPrice := (k.AskFloat / float64(100)) * (float64(100.0) - discount + discountModifier)
		dipVolume := (fiatValueUnit * float64(orderNumber+1)) / dipDiscountedPrice

		log.WithFields(logrus.Fields{
			"action":       "btd",
			"order-number": orderNumber + 1,
			"ask-price":    k.Ask,
			"dip-discount": discount - discountModifier,
			"dip-price":    dipDiscountedPrice,
			"dip-volume":   dipVolume,
		}).Debug(fmt.Sprintf("Creating discounted order %d", orderNumber+1))

		// Create Order and add to list
		dipOrderArgs, _ := k.createOrderArgs(c, dipVolume, fmt.Sprintf("%.1f", dipDiscountedPrice), false)

		// If volume < MIN_BTC_AMOUNT then do not add to the list, skip to next iteration
		if dipVolume < MIN_BTC_AMOUNT {
			orderNumber += 1
			continue
		}

		dipOrders = append(dipOrders, dipOrderArgs)
		log.WithFields(logrus.Fields{
			"action":       "btd",
			"order-number": orderNumber + 1,
		}).Debug("Added Order to list")

		orderNumber += 1
	}

	if len(dipOrders) == 0 {
		return "", fmt.Errorf("No Orders were added to the list")
	}

	//Cancel any open order with our UserRef
	if !c.Bool("dry-run") {
		openordersArgs := make(map[string]string)
		//openordersArgs["trades"] = "true"
		openordersArgs["userref"] = fmt.Sprintf("%d", k.UserRef)

		resp, _ := k.Api.OpenOrders(openordersArgs)
		if len(resp.Open) > 0 {

			_, err := k.Api.CancelOrder(fmt.Sprintf("%d", k.UserRef))
			if err != nil {
				return "", fmt.Errorf("Failed to Cancel Orders for UserRef: %d - %s", k.UserRef, err)
			}

			log.WithFields(logrus.Fields{
				"action":  "btd",
				"userref": k.UserRef,
			}).Info(fmt.Sprintf("%d Open Orders Canceled", len(resp.Open)))
		}
	}

	//Place Orders
	orderNumber = 0
	for orderNumber != int64(len(dipOrders)) {
		thisOrder := dipOrders[orderNumber]
		order, err := k.Api.AddOrder(k.Pair, "buy", thisOrder["orderType"], thisOrder["volume"], thisOrder)
		if err != nil {
			log.WithFields(logrus.Fields{
				"action":       "btd",
				"userref":      thisOrder["userref"],
				"order-number": orderNumber + 1,
			}).Error(fmt.Sprintf("Error Creating orderNumber %d: %s", orderNumber+1, err))

			orderNumber += 1
			continue
		}

		var orderId string
		if c.Bool("dry-run") {
			orderId = "DRY-RUN"
		} else {
			orderId = strings.Join(order.TransactionIds, ",")
		}

		log.WithFields(logrus.Fields{
			"action":       "btd",
			"order":        order.Description.Order,
			"orderId":      orderId,
			"order-number": orderNumber + 1,
			"dryrun":       thisOrder["validate"],
			"orderType":    thisOrder["orderType"],
			"volume":       thisOrder["volume"],
			"askPrice":     k.Ask,
			"price":        thisOrder["price"],
			"orderFlags":   thisOrder["oflags"],
			"userref":      thisOrder["userref"],
		}).Info(fmt.Sprintf("Order Placed %d", orderNumber+1))

		orderNumber += 1
	}

	return "", nil
}

func (k *Kraken) Stack(c *cli.Context) (result string, e error) {

	k.UserRef = 100

	log.WithFields(logrus.Fields{
		"action":  "stack",
		"userRef": k.UserRef,
	}).Info("Stacking some sats on " + k.Name)

	log.WithFields(logrus.Fields{
		"action":        "stack",
		"crypto":        k.Crypto,
		"cryptoBalance": k.BalanceCrypto,
		"fiat":          k.Fiat,
		"fiatBalance":   k.BalanceFiat,
		"ask":           k.Ask,
	}).Debug("Balance before placing the Order")

	volume := (c.Float64("amount") / k.AskFloat)
	orderArgs, err := k.createOrderArgs(c, volume, k.Ask, false)
	if err != nil {
		return "", fmt.Errorf("Failed to create args to place order: %s", err)
	}

	// Place the Order
	order, err := k.Api.AddOrder(k.Pair, "buy", orderArgs["orderType"], orderArgs["volume"], orderArgs)
	if err != nil {
		log.WithFields(logrus.Fields{
			"action":  "btd",
			"userref": k.UserRef,
		}).Error(fmt.Sprintf("Error Creating order: %s", err))
		return "", fmt.Errorf("Failed to place order: %s", err)
	}

	var orderId string
	if c.Bool("dry-run") {
		orderId = "DRY-RUN"
	} else {
		orderId = strings.Join(order.TransactionIds, ",")

3e5067aa38cbcca3b42ceed5aa391634bfc3a212

Use a struct for result and set action inside of it rather than use 2 string var...

Use a struct for result and set action inside of it rather than use 2 string variables

// TODO Use a struct for result and set action inside of it rather than use 2 string variables

)

// Global Variables
var log = logrus.New()
var ex exchange.Exchange
var nf notifier.Notifier

// TODO Use a struct for result and set action inside of it rather than use 2 string variables
var result string
var action string

dd464c832098e4e50cde67f8a324d86f038cd102

Add support for notification on errors

Add support for notification on errors

// TODO Add support for notification on errors

		},
		After: func(c *cli.Context) error {
			// Handle notification at the end of the CLI app run
			title := fmt.Sprintf("%s - %s Sats",
				strings.Title(c.String("exchange")),
				strings.Title(action),
			)

			// Do not notify if result is not set ( for example if the required args where not specified )
			// TODO Add support for notification on errors
			if result != "" {
				err := nf.Notify(title, result)

9bad4f45c7757d929b14ac5cd8b39a914320f76b

Add support for more OHLC Intervals

Add support for more OHLC Intervals

// TODO Add support for more OHLC Intervals

}

// Calculate a price modifier based on the Gap between current price and Highest price in a given time
func (k *Kraken) priceModifierBasedOnGapFromHighPrice(c *cli.Context) (float64, error) {

	var OHLCInterval string
	switch c.Int64("high-price-days-modifier") {
	// TODO Add support for more OHLC Intervals
	case 7:
		// 15 interval will give a week worth of data
		OHLCInterval = "15"
	default:
		// 15 interval will give a week worth of data
		OHLCInterval = "15"
	}

	ohlcs, err := k.Api.OHLCWithInterval(k.Pair, OHLCInterval)
	if err != nil {
		return 0.0, fmt.Errorf("Failed to get OHLC Data for pair %s: %s", k.Pair, err)
	}

a0ef830e8417a5f4b4eb9f394c698f76d101704a

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.