From c9562d66822ba8bee39a220b2847b0053d115334 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Wed, 12 Sep 2018 22:50:33 -0700 Subject: [PATCH 01/31] cap3: 1 - implement liabilities for XLM --- plugins/sdex.go | 60 +++++++++++++++++++++++++++++++----------------- trader/trader.go | 4 ++-- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/plugins/sdex.go b/plugins/sdex.go index b17001284..f701fb3ae 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -1,6 +1,7 @@ package plugins import ( + "fmt" "log" "strconv" @@ -25,9 +26,16 @@ type SDEX struct { simMode bool // uninitialized - seqNum uint64 - reloadSeqNum bool - cachedXlmExposure *float64 + seqNum uint64 + reloadSeqNum bool + // explicitly calculate liabilities here for now, we can switch over to using the values from Horizon once the protocol change has taken effect + cachedLiabilities map[horizon.Asset]Liabilities +} + +// Liabilities represents the "committed" units of an asset on both the buy and sell sides +type Liabilities struct { + Buying float64 // affects how much more can be bought + Selling float64 // affects how much more can be sold } // MakeSDEX is a factory method for SDEX @@ -180,16 +188,17 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As return nil } - xlmExposure, err := sdex.xlmExposure() + xlmLiabilities, err := sdex.liabilities(selling) if err != nil { log.Println(err) return nil } + xlmSellingLiabilities := xlmLiabilities.Selling additionalExposure := incrementalXlmAmount >= 0 - possibleTerminalExposure := ((xlmExposure + incrementalXlmAmount) / float64(sdex.FractionalReserveMultiplier)) > (bal - minAccountBal - sdex.operationalBuffer) + possibleTerminalExposure := ((xlmSellingLiabilities + incrementalXlmAmount) / float64(sdex.FractionalReserveMultiplier)) > (bal - minAccountBal - sdex.operationalBuffer) if additionalExposure && possibleTerminalExposure { - log.Println("not placing offer because we run the risk of running out of lumens | xlmExposure:", xlmExposure, + log.Println("not placing offer because we run the risk of running out of lumens | xlmSellingLiabilities:", xlmSellingLiabilities, "| incrementalXlmAmount:", incrementalXlmAmount, "| bal:", bal, "| minAccountBal:", minAccountBal, "| operationalBuffer:", sdex.operationalBuffer, "| fractionalReserveMultiplier:", sdex.FractionalReserveMultiplier) return nil @@ -294,35 +303,44 @@ func (sdex *SDEX) submit(txeB64 string) { log.Printf("(async) tx confirmation hash: %s\n", resp.Hash) } -// ResetCachedXlmExposure resets the cache -func (sdex *SDEX) ResetCachedXlmExposure() { - sdex.cachedXlmExposure = nil +// ResetCachedLiabilities resets the cache +func (sdex *SDEX) ResetCachedLiabilities() { + sdex.cachedLiabilities = map[horizon.Asset]Liabilities{} } -func (sdex *SDEX) xlmExposure() (float64, error) { - if sdex.cachedXlmExposure != nil { - return *sdex.cachedXlmExposure, nil +func (sdex *SDEX) liabilities(asset horizon.Asset) (*Liabilities, error) { + if v, ok := sdex.cachedLiabilities[asset]; ok { + return &v, nil } // uses all offers for this trading account to accommodate sharing by other bots offers, err := utils.LoadAllOffers(sdex.TradingAccount, sdex.API) if err != nil { - log.Printf("error computing XLM exposure: %s\n", err) - return -1, err + assetString := utils.Native + if asset.Type != utils.Native { + assetString = fmt.Sprintf("%s:%s", asset.Code, asset.Issuer) + } + log.Printf("error: cannot load offers to compute liabilities for asset (%s): %s\n", assetString, err) + return nil, err } - var sum float64 + liabilities := Liabilities{} for _, offer := range offers { - // only need to compute sum of selling because that's the max XLM we can give up if all our offers are taken - if offer.Selling.Type == utils.Native { + if offer.Selling == asset { + offerAmt, err := sdex.ParseOfferAmount(offer.Amount) + if err != nil { + return nil, err + } + liabilities.Selling += offerAmt + } else if offer.Buying == asset { offerAmt, err := sdex.ParseOfferAmount(offer.Amount) if err != nil { - return -1, err + return nil, err } - sum += offerAmt + liabilities.Buying += offerAmt } } - sdex.cachedXlmExposure = &sum - return sum, nil + sdex.cachedLiabilities[asset] = liabilities + return &liabilities, nil } diff --git a/trader/trader.go b/trader/trader.go index 8506c4f5d..4ad66a7b3 100644 --- a/trader/trader.go +++ b/trader/trader.go @@ -114,9 +114,9 @@ func (t *Trader) update() { } } - // reset cached xlm exposure here so we only compute it once per update + // reset cached liabilities here so we only compute it once per update // TODO 2 - calculate this here and pass it in - t.sdex.ResetCachedXlmExposure() + t.sdex.ResetCachedLiabilities() ops, e := t.strat.UpdateWithOps(t.buyingAOffers, t.sellingAOffers) if e != nil { log.Println(e) From 89fe7bf5684c8c064db429e519fdcf2663f65f0f Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 13 Sep 2018 00:38:42 -0700 Subject: [PATCH 02/31] cap3: 2 - integrate liabilities into willOversell and willOverbuy methods --- plugins/sdex.go | 164 +++++++++++++++++++++++++++---------- support/utils/functions.go | 9 ++ 2 files changed, 129 insertions(+), 44 deletions(-) diff --git a/plugins/sdex.go b/plugins/sdex.go index f701fb3ae..0b5aae7ab 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -145,62 +145,56 @@ func (sdex *SDEX) minReserve(subentries int32) float64 { return float64(2+subentries) * baseReserve } -func (sdex *SDEX) lumenBalance() (float64, float64, error) { +// assetBalance returns asset balance, asset trust limit (zero for XLM), reserve balance (zero for non-XLM), error +func (sdex *SDEX) assetBalance(asset horizon.Asset) (float64, float64, float64, error) { account, err := sdex.API.LoadAccount(sdex.TradingAccount) if err != nil { - log.Printf("error loading account to fetch lumen balance: %s\n", err) - return -1, -1, nil + return -1, -1, -1, fmt.Errorf("error: unable to load account to fetch balance: %s", err) } for _, balance := range account.Balances { - if balance.Asset.Type == utils.Native { + if utils.AssetsEqual(balance.Asset, asset) { b, e := strconv.ParseFloat(balance.Balance, 64) if e != nil { - log.Printf("error parsing native balance: %s\n", e) + return -1, -1, -1, fmt.Errorf("error: cannot parse balance: %s", e) } - return b, sdex.minReserve(account.SubentryCount), e + if balance.Asset.Type == utils.Native { + return b, 0, sdex.minReserve(account.SubentryCount) + sdex.operationalBuffer, e + } + + t, e := strconv.ParseFloat(balance.Limit, 64) + if e != nil { + return -1, -1, -1, fmt.Errorf("error: cannot parse trust limit: %s", e) + } + return b, t, 0, e } } - return -1, -1, errors.New("could not find a native lumen balance") + return -1, -1, -1, errors.New("could not find a balance for the asset passed in") } // createModifySellOffer is the main method that handles the logic of creating or modifying an offer, note that all offers are treated as sell offers in Stellar func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.Asset, buying horizon.Asset, price float64, amount float64) *build.ManageOfferBuilder { - if selling.Type == utils.Native { - var incrementalXlmAmount float64 - if offer != nil { - offerAmt, err := sdex.ParseOfferAmount(offer.Amount) - if err != nil { - log.Println(err) - return nil - } - // modifying an offer will not increase the min reserve but will affect the xlm exposure - incrementalXlmAmount = amount - offerAmt - } else { - // creating a new offer will incrase the min reserve on the account so add baseReserve - incrementalXlmAmount = amount + baseReserve - } - - // check if incrementalXlmAmount is within budget - bal, minAccountBal, err := sdex.lumenBalance() - if err != nil { - log.Println(err) - return nil - } - - xlmLiabilities, err := sdex.liabilities(selling) - if err != nil { - log.Println(err) + // check liability limits on the asset being sold + willOversell, e := sdex.willOversell(offer, selling, amount) + if e != nil { + log.Println(e) + return nil + } + if willOversell { + assetString := utils.Asset2String(selling) + log.Printf("not placing offer because we run the risk of overselling the asset '%s'\n", assetString) + return nil + } + // check trust limits on asset being bought (doesn't apply to native XLM) + if buying.Type != utils.Native { + willOverbuy, e := sdex.willOverbuy(offer, buying, price*amount) + if e != nil { + log.Println(e) return nil } - xlmSellingLiabilities := xlmLiabilities.Selling - - additionalExposure := incrementalXlmAmount >= 0 - possibleTerminalExposure := ((xlmSellingLiabilities + incrementalXlmAmount) / float64(sdex.FractionalReserveMultiplier)) > (bal - minAccountBal - sdex.operationalBuffer) - if additionalExposure && possibleTerminalExposure { - log.Println("not placing offer because we run the risk of running out of lumens | xlmSellingLiabilities:", xlmSellingLiabilities, - "| incrementalXlmAmount:", incrementalXlmAmount, "| bal:", bal, "| minAccountBal:", minAccountBal, - "| operationalBuffer:", sdex.operationalBuffer, "| fractionalReserveMultiplier:", sdex.FractionalReserveMultiplier) + if willOverbuy { + assetString := utils.Asset2String(selling) + log.Printf("not placing offer because we run the risk of overbuying the asset '%s'\n", assetString) return nil } } @@ -226,6 +220,91 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As return &result } +func (sdex *SDEX) willOversell(offer *horizon.Offer, asset horizon.Asset, amountSelling float64) (bool, error) { + var incrementalAmount float64 + if offer != nil { + if offer.Selling != asset { + return false, fmt.Errorf("error: offer.Selling (%s) does not match asset being sold (%s)", utils.Asset2String(offer.Selling), asset) + } + + // modifying an offer will only affect the exposure of the asset (positive or negative) + offerAmt, err := sdex.ParseOfferAmount(offer.Amount) + if err != nil { + return false, err + } + incrementalAmount = amountSelling - offerAmt + + // if we are reducing our selling amount of an asset then it cannot exceed the threshold for that asset + if incrementalAmount < 0 { + return false, nil + } + } else { + // TODO need to add an additional check for increase in XLM liabilities vs. XLM limits for new non-XLM offers caused by base reserve increases + incrementalAmount = amountSelling + // creating a new offer will increase the min reserve on the account + if asset.Type == utils.Native { + incrementalAmount += baseReserve + } + } + + bal, _, minAccountBal, err := sdex.assetBalance(asset) + if err != nil { + return false, err + } + liabilities, err := sdex.liabilities(asset) + if err != nil { + return false, err + } + + result := incrementalAmount > (bal - minAccountBal - liabilities.Selling) + return result, nil +} + +func (sdex *SDEX) willOverbuy(offer *horizon.Offer, asset horizon.Asset, amountBuying float64) (bool, error) { + if asset.Type == utils.Native { + return false, fmt.Errorf("error: should not check willOverbuy for the native asset") + } + + var incrementalAmount float64 + if offer != nil { + if offer.Buying != asset { + return false, fmt.Errorf("error: offer.Buying (%s) does not match asset being bought (%s)", utils.Asset2String(offer.Buying), asset) + } + + // modifying an offer will only affect the exposure of the asset (positive or negative) + offerAmt, err := sdex.ParseOfferAmount(offer.Amount) + if err != nil { + return false, err + } + offerPrice, err := sdex.ParseOfferAmount(offer.Price) + if err != nil { + return false, fmt.Errorf("error parsing offer price: %s", err) + } + offerBuyingAmount := offerAmt * offerPrice + incrementalAmount = amountBuying - offerBuyingAmount + + // if we are reducing our buying amount of an asset then it cannot exceed the threshold for that asset + if incrementalAmount < 0 { + return false, nil + } + } else { + // TODO need to add an additional check for increase in XLM liabilities vs. XLM limits for new non-XLM offers caused by base reserve increases + incrementalAmount = amountBuying + } + + _, trust, _, err := sdex.assetBalance(asset) + if err != nil { + return false, err + } + liabilities, err := sdex.liabilities(asset) + if err != nil { + return false, err + } + + result := incrementalAmount > (trust - liabilities.Buying) + return result, nil +} + // SubmitOps submits the passed in operations to the network asynchronously in a single transaction func (sdex *SDEX) SubmitOps(ops []build.TransactionMutator) error { sdex.incrementSeqNum() @@ -316,10 +395,7 @@ func (sdex *SDEX) liabilities(asset horizon.Asset) (*Liabilities, error) { // uses all offers for this trading account to accommodate sharing by other bots offers, err := utils.LoadAllOffers(sdex.TradingAccount, sdex.API) if err != nil { - assetString := utils.Native - if asset.Type != utils.Native { - assetString = fmt.Sprintf("%s:%s", asset.Code, asset.Issuer) - } + assetString := utils.Asset2String(asset) log.Printf("error: cannot load offers to compute liabilities for asset (%s): %s\n", assetString, err) return nil, err } diff --git a/support/utils/functions.go b/support/utils/functions.go index 1c6babcad..edcf7b344 100644 --- a/support/utils/functions.go +++ b/support/utils/functions.go @@ -2,6 +2,7 @@ package utils import ( "encoding/json" + "fmt" "log" "math" "math/big" @@ -104,6 +105,14 @@ func Asset2Asset2(Asset build.Asset) horizon.Asset { return a } +// Asset2String converts a horizon.Asset to a string representation, using "native" for the native XLM +func Asset2String(asset horizon.Asset) string { + if asset.Type == Native { + return Native + } + return fmt.Sprintf("%s:%s", asset.Code, asset.Issuer) +} + // String2Asset converts a code:issuer to a horizon.Asset func String2Asset(code string, issuer string) horizon.Asset { if code == "XLM" { From 19630f66462e4f11a83660fae1d93c9e1bf10dee Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 13 Sep 2018 01:19:31 -0700 Subject: [PATCH 03/31] cap3: 3 - remove fractionalReserveMultiplier cli arg --- cmd/terminate.go | 1 - cmd/trade.go | 3 --- plugins/sdex.go | 35 ++++++++++++++++------------------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/cmd/terminate.go b/cmd/terminate.go index a055b94b2..4ea9dc60b 100644 --- a/cmd/terminate.go +++ b/cmd/terminate.go @@ -46,7 +46,6 @@ func init() { *configFile.TradingAccount, utils.ParseNetwork(configFile.HORIZON_URL), -1, // not needed here - -1, // not needed here false, ) terminator := terminator.MakeTerminator(client, sdex, *configFile.TradingAccount, configFile.TICK_INTERVAL_SECONDS, configFile.ALLOW_INACTIVE_MINUTES) diff --git a/cmd/trade.go b/cmd/trade.go index f055e3a4d..4d12c337a 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -43,13 +43,11 @@ func init() { strategy := tradeCmd.Flags().StringP("strategy", "s", "", "(required) type of strategy to run") stratConfigPath := tradeCmd.Flags().StringP("stratConf", "f", "", "strategy config file path") // long-only flags - fractionalReserveMultiplier := tradeCmd.Flags().Int8("fractionalReserveMultiplier", 1, "fractional multiplier for XLM reserves") operationalBuffer := tradeCmd.Flags().Float64("operationalBuffer", 20, "buffer of native XLM to maintain beyond minimum account balance requirement") simMode := tradeCmd.Flags().Bool("sim", false, "simulate the bot's actions without placing any trades") requiredFlag("botConf") requiredFlag("strategy") - hiddenFlag("fractionalReserveMultiplier") hiddenFlag("operationalBuffer") tradeCmd.Flags().SortFlags = false @@ -84,7 +82,6 @@ func init() { botConfig.SourceAccount(), botConfig.TradingAccount(), utils.ParseNetwork(botConfig.HORIZON_URL), - *fractionalReserveMultiplier, *operationalBuffer, *simMode, ) diff --git a/plugins/sdex.go b/plugins/sdex.go index 0b5aae7ab..3d5487d92 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -15,15 +15,14 @@ const baseReserve = 0.5 // SDEX helps with building and submitting transactions to the Stellar network type SDEX struct { - API *horizon.Client - SourceAccount string - TradingAccount string - SourceSeed string - TradingSeed string - Network build.Network - FractionalReserveMultiplier int8 - operationalBuffer float64 - simMode bool + API *horizon.Client + SourceAccount string + TradingAccount string + SourceSeed string + TradingSeed string + Network build.Network + operationalBuffer float64 + simMode bool // uninitialized seqNum uint64 @@ -46,20 +45,18 @@ func MakeSDEX( sourceAccount string, tradingAccount string, network build.Network, - fractionalReserveMultiplier int8, operationalBuffer float64, simMode bool, ) *SDEX { sdex := &SDEX{ - API: api, - SourceSeed: sourceSeed, - TradingSeed: tradingSeed, - SourceAccount: sourceAccount, - TradingAccount: tradingAccount, - Network: network, - FractionalReserveMultiplier: fractionalReserveMultiplier, - operationalBuffer: operationalBuffer, - simMode: simMode, + API: api, + SourceSeed: sourceSeed, + TradingSeed: tradingSeed, + SourceAccount: sourceAccount, + TradingAccount: tradingAccount, + Network: network, + operationalBuffer: operationalBuffer, + simMode: simMode, } log.Printf("Using network passphrase: %s\n", sdex.Network.Passphrase) From 54dd02422d57367fe5fb73adb22e9348522079e6 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 13 Sep 2018 01:38:04 -0700 Subject: [PATCH 04/31] cap3: 4 - offers in the same tx will contribute to liabilities, incorporate into cachedLiabilities --- plugins/sdex.go | 51 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/plugins/sdex.go b/plugins/sdex.go index 3d5487d92..82d80844d 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -172,7 +172,7 @@ func (sdex *SDEX) assetBalance(asset horizon.Asset) (float64, float64, float64, // createModifySellOffer is the main method that handles the logic of creating or modifying an offer, note that all offers are treated as sell offers in Stellar func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.Asset, buying horizon.Asset, price float64, amount float64) *build.ManageOfferBuilder { // check liability limits on the asset being sold - willOversell, e := sdex.willOversell(offer, selling, amount) + willOversell, incrementalSell, e := sdex.willOversell(offer, selling, amount) if e != nil { log.Println(e) return nil @@ -183,8 +183,11 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As return nil } // check trust limits on asset being bought (doesn't apply to native XLM) + var incrementalBuy float64 if buying.Type != utils.Native { - willOverbuy, e := sdex.willOverbuy(offer, buying, price*amount) + var willOverbuy bool + var e error + willOverbuy, incrementalBuy, e = sdex.willOverbuy(offer, buying, price*amount) if e != nil { log.Println(e) return nil @@ -195,6 +198,7 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As return nil } } + // TODO handle case of setting incrementalBuy for native asset stringPrice := strconv.FormatFloat(price, 'f', int(utils.SdexPrecision), 64) rate := build.Rate{ @@ -214,26 +218,36 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As mutators = append(mutators, build.SourceAccount{AddressOrSeed: sdex.TradingAccount}) } result := build.ManageOffer(false, mutators...) + // update the cached liabilities + sdex.cachedLiabilities[selling] = Liabilities{ + Selling: sdex.cachedLiabilities[selling].Selling + incrementalSell, + Buying: sdex.cachedLiabilities[selling].Buying, + } + sdex.cachedLiabilities[buying] = Liabilities{ + Selling: sdex.cachedLiabilities[buying].Selling, + Buying: sdex.cachedLiabilities[buying].Buying + incrementalBuy, + } return &result } -func (sdex *SDEX) willOversell(offer *horizon.Offer, asset horizon.Asset, amountSelling float64) (bool, error) { +// willOversell returns willOversell, incrementalAmount, error +func (sdex *SDEX) willOversell(offer *horizon.Offer, asset horizon.Asset, amountSelling float64) (bool, float64, error) { var incrementalAmount float64 if offer != nil { if offer.Selling != asset { - return false, fmt.Errorf("error: offer.Selling (%s) does not match asset being sold (%s)", utils.Asset2String(offer.Selling), asset) + return false, 0, fmt.Errorf("error: offer.Selling (%s) does not match asset being sold (%s)", utils.Asset2String(offer.Selling), asset) } // modifying an offer will only affect the exposure of the asset (positive or negative) offerAmt, err := sdex.ParseOfferAmount(offer.Amount) if err != nil { - return false, err + return false, 0, err } incrementalAmount = amountSelling - offerAmt // if we are reducing our selling amount of an asset then it cannot exceed the threshold for that asset if incrementalAmount < 0 { - return false, nil + return false, incrementalAmount, nil } } else { // TODO need to add an additional check for increase in XLM liabilities vs. XLM limits for new non-XLM offers caused by base reserve increases @@ -246,43 +260,44 @@ func (sdex *SDEX) willOversell(offer *horizon.Offer, asset horizon.Asset, amount bal, _, minAccountBal, err := sdex.assetBalance(asset) if err != nil { - return false, err + return false, 0, err } liabilities, err := sdex.liabilities(asset) if err != nil { - return false, err + return false, 0, err } result := incrementalAmount > (bal - minAccountBal - liabilities.Selling) - return result, nil + return result, incrementalAmount, nil } -func (sdex *SDEX) willOverbuy(offer *horizon.Offer, asset horizon.Asset, amountBuying float64) (bool, error) { +// willOverbuy returns willOverbuy, incrementalAmount, error +func (sdex *SDEX) willOverbuy(offer *horizon.Offer, asset horizon.Asset, amountBuying float64) (bool, float64, error) { if asset.Type == utils.Native { - return false, fmt.Errorf("error: should not check willOverbuy for the native asset") + return false, 0, fmt.Errorf("error: should not check willOverbuy for the native asset") } var incrementalAmount float64 if offer != nil { if offer.Buying != asset { - return false, fmt.Errorf("error: offer.Buying (%s) does not match asset being bought (%s)", utils.Asset2String(offer.Buying), asset) + return false, 0, fmt.Errorf("error: offer.Buying (%s) does not match asset being bought (%s)", utils.Asset2String(offer.Buying), asset) } // modifying an offer will only affect the exposure of the asset (positive or negative) offerAmt, err := sdex.ParseOfferAmount(offer.Amount) if err != nil { - return false, err + return false, 0, err } offerPrice, err := sdex.ParseOfferAmount(offer.Price) if err != nil { - return false, fmt.Errorf("error parsing offer price: %s", err) + return false, 0, fmt.Errorf("error parsing offer price: %s", err) } offerBuyingAmount := offerAmt * offerPrice incrementalAmount = amountBuying - offerBuyingAmount // if we are reducing our buying amount of an asset then it cannot exceed the threshold for that asset if incrementalAmount < 0 { - return false, nil + return false, incrementalAmount, nil } } else { // TODO need to add an additional check for increase in XLM liabilities vs. XLM limits for new non-XLM offers caused by base reserve increases @@ -291,15 +306,15 @@ func (sdex *SDEX) willOverbuy(offer *horizon.Offer, asset horizon.Asset, amountB _, trust, _, err := sdex.assetBalance(asset) if err != nil { - return false, err + return false, 0, err } liabilities, err := sdex.liabilities(asset) if err != nil { - return false, err + return false, 0, err } result := incrementalAmount > (trust - liabilities.Buying) - return result, nil + return result, incrementalAmount, nil } // SubmitOps submits the passed in operations to the network asynchronously in a single transaction From 028e8a42fb9cc9926a30006de77b2e4f889e931c Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 13 Sep 2018 01:47:58 -0700 Subject: [PATCH 05/31] cap3: 5 - handle case of setting incrementalBuy for native asset --- plugins/sdex.go | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/plugins/sdex.go b/plugins/sdex.go index 82d80844d..d0c9af215 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -183,22 +183,16 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As return nil } // check trust limits on asset being bought (doesn't apply to native XLM) - var incrementalBuy float64 - if buying.Type != utils.Native { - var willOverbuy bool - var e error - willOverbuy, incrementalBuy, e = sdex.willOverbuy(offer, buying, price*amount) - if e != nil { - log.Println(e) - return nil - } - if willOverbuy { - assetString := utils.Asset2String(selling) - log.Printf("not placing offer because we run the risk of overbuying the asset '%s'\n", assetString) - return nil - } + willOverbuy, incrementalBuy, e := sdex.willOverbuy(offer, buying, price*amount) + if e != nil { + log.Println(e) + return nil + } + if willOverbuy { + assetString := utils.Asset2String(selling) + log.Printf("not placing offer because we run the risk of overbuying the asset '%s'\n", assetString) + return nil } - // TODO handle case of setting incrementalBuy for native asset stringPrice := strconv.FormatFloat(price, 'f', int(utils.SdexPrecision), 64) rate := build.Rate{ @@ -273,10 +267,6 @@ func (sdex *SDEX) willOversell(offer *horizon.Offer, asset horizon.Asset, amount // willOverbuy returns willOverbuy, incrementalAmount, error func (sdex *SDEX) willOverbuy(offer *horizon.Offer, asset horizon.Asset, amountBuying float64) (bool, float64, error) { - if asset.Type == utils.Native { - return false, 0, fmt.Errorf("error: should not check willOverbuy for the native asset") - } - var incrementalAmount float64 if offer != nil { if offer.Buying != asset { @@ -302,6 +292,11 @@ func (sdex *SDEX) willOverbuy(offer *horizon.Offer, asset horizon.Asset, amountB } else { // TODO need to add an additional check for increase in XLM liabilities vs. XLM limits for new non-XLM offers caused by base reserve increases incrementalAmount = amountBuying + // TODO include base reserve on sell side for all assets against XLM (i.e. opposite polarity of incrementalAmount) + } + if asset.Type == utils.Native { + // you can never overbuy the native asset + return false, incrementalAmount, nil } _, trust, _, err := sdex.assetBalance(asset) From 62ba9166303019afec688fefe941a1770b798f11 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 20 Sep 2018 16:52:56 -0700 Subject: [PATCH 06/31] cap3: 6 - refactored willOversell and willOverbuy to extract common offer logic --- plugins/sdex.go | 143 ++++++++++++++++++++++++++--------------------- trader/trader.go | 3 +- 2 files changed, 80 insertions(+), 66 deletions(-) diff --git a/plugins/sdex.go b/plugins/sdex.go index d0c9af215..65cb4f0fc 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -3,6 +3,7 @@ package plugins import ( "fmt" "log" + "math" "strconv" "github.com/lightyeario/kelp/support/utils" @@ -12,6 +13,7 @@ import ( ) const baseReserve = 0.5 +const maxLumenTrust = math.MaxFloat64 // SDEX helps with building and submitting transactions to the Stellar network type SDEX struct { @@ -142,7 +144,7 @@ func (sdex *SDEX) minReserve(subentries int32) float64 { return float64(2+subentries) * baseReserve } -// assetBalance returns asset balance, asset trust limit (zero for XLM), reserve balance (zero for non-XLM), error +// assetBalance returns asset balance, asset trust limit, reserve balance (zero for non-XLM), error func (sdex *SDEX) assetBalance(asset horizon.Asset) (float64, float64, float64, error) { account, err := sdex.API.LoadAccount(sdex.TradingAccount) if err != nil { @@ -156,7 +158,7 @@ func (sdex *SDEX) assetBalance(asset horizon.Asset) (float64, float64, float64, return -1, -1, -1, fmt.Errorf("error: cannot parse balance: %s", e) } if balance.Asset.Type == utils.Native { - return b, 0, sdex.minReserve(account.SubentryCount) + sdex.operationalBuffer, e + return b, maxLumenTrust, sdex.minReserve(account.SubentryCount) + sdex.operationalBuffer, e } t, e := strconv.ParseFloat(balance.Limit, 64) @@ -226,90 +228,101 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As // willOversell returns willOversell, incrementalAmount, error func (sdex *SDEX) willOversell(offer *horizon.Offer, asset horizon.Asset, amountSelling float64) (bool, float64, error) { - var incrementalAmount float64 - if offer != nil { - if offer.Selling != asset { - return false, 0, fmt.Errorf("error: offer.Selling (%s) does not match asset being sold (%s)", utils.Asset2String(offer.Selling), asset) - } + offerSellingAmount, e := sdex.getSignedAmount(offer, asset) + if e != nil { + return false, 0, e + } + if offerSellingAmount > 0 { + return false, 0, fmt.Errorf("error: offer.Selling (%s) does not match asset being sold (%s)", utils.Asset2String(offer.Selling), asset) + } - // modifying an offer will only affect the exposure of the asset (positive or negative) - offerAmt, err := sdex.ParseOfferAmount(offer.Amount) - if err != nil { - return false, 0, err - } - incrementalAmount = amountSelling - offerAmt + // TODO need to add an additional check for increase in XLM liabilities vs. XLM limits for new non-XLM offers caused by base reserve increases + incrementalAmount := amountSelling - offerSellingAmount + // creating a new offer will increase the min reserve on the account + if asset.Type == utils.Native && offerSellingAmount != 0 { + incrementalAmount += baseReserve + } - // if we are reducing our selling amount of an asset then it cannot exceed the threshold for that asset - if incrementalAmount < 0 { - return false, incrementalAmount, nil - } - } else { - // TODO need to add an additional check for increase in XLM liabilities vs. XLM limits for new non-XLM offers caused by base reserve increases - incrementalAmount = amountSelling - // creating a new offer will increase the min reserve on the account - if asset.Type == utils.Native { - incrementalAmount += baseReserve - } + // if we are reducing our selling amount of an asset then it cannot exceed the threshold for that asset + if incrementalAmount < 0 { + return false, incrementalAmount, nil } - bal, _, minAccountBal, err := sdex.assetBalance(asset) - if err != nil { - return false, 0, err + bal, _, minAccountBal, e := sdex.assetBalance(asset) + if e != nil { + return false, 0, e } - liabilities, err := sdex.liabilities(asset) - if err != nil { - return false, 0, err + liabilities, e := sdex.liabilities(asset) + if e != nil { + return false, 0, e } - result := incrementalAmount > (bal - minAccountBal - liabilities.Selling) - return result, incrementalAmount, nil + willOversell := incrementalAmount > (bal - minAccountBal - liabilities.Selling) + return willOversell, incrementalAmount, nil } // willOverbuy returns willOverbuy, incrementalAmount, error func (sdex *SDEX) willOverbuy(offer *horizon.Offer, asset horizon.Asset, amountBuying float64) (bool, float64, error) { - var incrementalAmount float64 - if offer != nil { - if offer.Buying != asset { - return false, 0, fmt.Errorf("error: offer.Buying (%s) does not match asset being bought (%s)", utils.Asset2String(offer.Buying), asset) - } - - // modifying an offer will only affect the exposure of the asset (positive or negative) - offerAmt, err := sdex.ParseOfferAmount(offer.Amount) - if err != nil { - return false, 0, err - } - offerPrice, err := sdex.ParseOfferAmount(offer.Price) - if err != nil { - return false, 0, fmt.Errorf("error parsing offer price: %s", err) - } - offerBuyingAmount := offerAmt * offerPrice - incrementalAmount = amountBuying - offerBuyingAmount + offerBuyingAmount, e := sdex.getSignedAmount(offer, asset) + if e != nil { + return false, 0, e + } + if offerBuyingAmount < 0 { + return false, 0, fmt.Errorf("error: offer.Buying (%s) does not match asset being bought (%s)", utils.Asset2String(offer.Buying), asset) + } - // if we are reducing our buying amount of an asset then it cannot exceed the threshold for that asset - if incrementalAmount < 0 { - return false, incrementalAmount, nil - } - } else { - // TODO need to add an additional check for increase in XLM liabilities vs. XLM limits for new non-XLM offers caused by base reserve increases - incrementalAmount = amountBuying - // TODO include base reserve on sell side for all assets against XLM (i.e. opposite polarity of incrementalAmount) + // TODO need to add an additional check for increase in XLM liabilities vs. XLM limits for new non-XLM offers caused by base reserve increases + // TODO include base reserve on sell side for all assets against XLM (i.e. opposite polarity of incrementalAmount) + incrementalAmount := amountBuying - offerBuyingAmount + // if we are reducing our buying amount of an asset then it cannot exceed the threshold for that asset + if incrementalAmount < 0 { + return false, incrementalAmount, nil } + if asset.Type == utils.Native { // you can never overbuy the native asset return false, incrementalAmount, nil } - _, trust, _, err := sdex.assetBalance(asset) - if err != nil { - return false, 0, err + _, trust, _, e := sdex.assetBalance(asset) + if e != nil { + return false, 0, e } - liabilities, err := sdex.liabilities(asset) - if err != nil { - return false, 0, err + liabilities, e := sdex.liabilities(asset) + if e != nil { + return false, 0, e } - result := incrementalAmount > (trust - liabilities.Buying) - return result, incrementalAmount, nil + willOverbuy := incrementalAmount > (trust - liabilities.Buying) + return willOverbuy, incrementalAmount, nil +} + +// getSignedAmount gets the amount in the unit of the asset passed in, + for buying, - for selling. 0 if asset is not in the offer +func (sdex *SDEX) getSignedAmount(offer *horizon.Offer, asset horizon.Asset) (float64, error) { + if offer == nil { + return 0, nil + } + + if offer.Buying != asset && offer.Selling != asset { + return 0, nil + } + + offerAmt, e := sdex.ParseOfferAmount(offer.Amount) + if e != nil { + return 0, e + } + + if offer.Selling == asset { + // negative sign because offer is being sold + return -offerAmt, nil + } + + offerPrice, e := sdex.ParseOfferAmount(offer.Price) + if e != nil { + return 0, e + } + // positive sign because offer is being bought; multiply with offerPrice to keep the units in the asset being bought + return offerAmt * offerPrice, nil } // SubmitOps submits the passed in operations to the network asynchronously in a single transaction diff --git a/trader/trader.go b/trader/trader.go index 4ad66a7b3..8b99860f6 100644 --- a/trader/trader.go +++ b/trader/trader.go @@ -2,6 +2,7 @@ package trader import ( "log" + "math" "sort" "time" @@ -13,7 +14,7 @@ import ( "github.com/stellar/go/clients/horizon" ) -const maxLumenTrust float64 = 100000000000 +const maxLumenTrust float64 = math.MaxFloat64 // Trader represents a market making bot, which is composed of various parts include the strategy and various APIs. type Trader struct { From 4754e11d5176a6383eda438f4cae4a02eafa4bf4 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 20 Sep 2018 19:36:18 -0700 Subject: [PATCH 07/31] cap3: 7 - added support for checking XLM fee and min reserves --- plugins/sdex.go | 64 +++++++++++++++++++++++++++++--------- support/utils/functions.go | 3 ++ 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/plugins/sdex.go b/plugins/sdex.go index 65cb4f0fc..5ab33ed93 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -13,6 +13,7 @@ import ( ) const baseReserve = 0.5 +const baseFee = 0.0000100 const maxLumenTrust = math.MaxFloat64 // SDEX helps with building and submitting transactions to the Stellar network @@ -180,19 +181,41 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As return nil } if willOversell { - assetString := utils.Asset2String(selling) - log.Printf("not placing offer because we run the risk of overselling the asset '%s'\n", assetString) + log.Printf("not placing offer because we run the risk of overselling the asset '%s'\n", utils.Asset2String(selling)) return nil } - // check trust limits on asset being bought (doesn't apply to native XLM) + + // check trust limits on asset being bought willOverbuy, incrementalBuy, e := sdex.willOverbuy(offer, buying, price*amount) if e != nil { log.Println(e) return nil } if willOverbuy { - assetString := utils.Asset2String(selling) - log.Printf("not placing offer because we run the risk of overbuying the asset '%s'\n", assetString) + log.Printf("not placing offer because we run the risk of overbuying the asset '%s'\n", utils.Asset2String(buying)) + return nil + } + + // explicitly check that we will not oversell XLM because of fee and min reserves + // at the minimum it will cost us a unit of base fee for this operation + incrementalNativeAmountRaw := baseFee + if offer == nil { + // new offers will increase the min reserve + incrementalNativeAmountRaw += baseReserve + } + incrementalNativeAmountTotal := incrementalNativeAmountRaw + if selling.Type == utils.Native { + incrementalNativeAmountTotal += incrementalSell + } else if buying.Type == utils.Native { + incrementalNativeAmountTotal -= incrementalBuy + } + willOversellNative, e := sdex.willOversellNative(incrementalNativeAmountTotal) + if e != nil { + log.Println(e) + return nil + } + if willOversellNative { + log.Println("not placing offer because we run the risk of overselling the native asset after including fee and min reserves") return nil } @@ -214,6 +237,7 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As mutators = append(mutators, build.SourceAccount{AddressOrSeed: sdex.TradingAccount}) } result := build.ManageOffer(false, mutators...) + // update the cached liabilities sdex.cachedLiabilities[selling] = Liabilities{ Selling: sdex.cachedLiabilities[selling].Selling + incrementalSell, @@ -223,9 +247,28 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As Selling: sdex.cachedLiabilities[buying].Selling, Buying: sdex.cachedLiabilities[buying].Buying + incrementalBuy, } + sdex.cachedLiabilities[utils.NativeAsset] = Liabilities{ + Selling: sdex.cachedLiabilities[utils.NativeAsset].Selling + incrementalNativeAmountRaw, + Buying: sdex.cachedLiabilities[utils.NativeAsset].Buying, + } return &result } +// willOversellNative returns willOversellNative, error +func (sdex *SDEX) willOversellNative(incrementalNativeAmount float64) (bool, error) { + nativeBal, _, minAccountBal, e := sdex.assetBalance(utils.NativeAsset) + if e != nil { + return false, e + } + nativeLiabilities, e := sdex.liabilities(utils.NativeAsset) + if e != nil { + return false, e + } + + willOversellNative := incrementalNativeAmount > (nativeBal - minAccountBal - nativeLiabilities.Selling) + return willOversellNative, nil +} + // willOversell returns willOversell, incrementalAmount, error func (sdex *SDEX) willOversell(offer *horizon.Offer, asset horizon.Asset, amountSelling float64) (bool, float64, error) { offerSellingAmount, e := sdex.getSignedAmount(offer, asset) @@ -236,14 +279,8 @@ func (sdex *SDEX) willOversell(offer *horizon.Offer, asset horizon.Asset, amount return false, 0, fmt.Errorf("error: offer.Selling (%s) does not match asset being sold (%s)", utils.Asset2String(offer.Selling), asset) } - // TODO need to add an additional check for increase in XLM liabilities vs. XLM limits for new non-XLM offers caused by base reserve increases incrementalAmount := amountSelling - offerSellingAmount - // creating a new offer will increase the min reserve on the account - if asset.Type == utils.Native && offerSellingAmount != 0 { - incrementalAmount += baseReserve - } - - // if we are reducing our selling amount of an asset then it cannot exceed the threshold for that asset + // reducing our selling amount can never exceed the threshold for that asset if incrementalAmount < 0 { return false, incrementalAmount, nil } @@ -271,14 +308,11 @@ func (sdex *SDEX) willOverbuy(offer *horizon.Offer, asset horizon.Asset, amountB return false, 0, fmt.Errorf("error: offer.Buying (%s) does not match asset being bought (%s)", utils.Asset2String(offer.Buying), asset) } - // TODO need to add an additional check for increase in XLM liabilities vs. XLM limits for new non-XLM offers caused by base reserve increases - // TODO include base reserve on sell side for all assets against XLM (i.e. opposite polarity of incrementalAmount) incrementalAmount := amountBuying - offerBuyingAmount // if we are reducing our buying amount of an asset then it cannot exceed the threshold for that asset if incrementalAmount < 0 { return false, incrementalAmount, nil } - if asset.Type == utils.Native { // you can never overbuy the native asset return false, incrementalAmount, nil diff --git a/support/utils/functions.go b/support/utils/functions.go index edcf7b344..e7d2b559c 100644 --- a/support/utils/functions.go +++ b/support/utils/functions.go @@ -21,6 +21,9 @@ import ( // Native is the string representing the type for the native lumen asset const Native = "native" +// NativeAsset represents the native asset +var NativeAsset = horizon.Asset{Type: Native} + // SdexPrecision defines the number of decimals used in SDEX const SdexPrecision int8 = 7 From cc2ffbfafe5d37d2dc39824bf8c641b19a9d9bc3 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Fri, 21 Sep 2018 10:23:54 -0700 Subject: [PATCH 08/31] =?UTF-8?q?cap3:=208=20-=20update=20ordering=20of=20?= =?UTF-8?q?operations=20in=20strategies=20(sellSideStrategy,=20mirrorStrat?= =?UTF-8?q?egy)=20when=20not=20all=20offers=20can=20be=20placed=20?= =?UTF-8?q?=E2=80=94=20always=20place=20inside=20orders=20first?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/mirrorStrategy.go | 14 ++++++-------- plugins/sellSideStrategy.go | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/plugins/mirrorStrategy.go b/plugins/mirrorStrategy.go index d9f489886..9ef63a6ca 100644 --- a/plugins/mirrorStrategy.go +++ b/plugins/mirrorStrategy.go @@ -116,13 +116,12 @@ func (s *mirrorStrategy) updateLevels( ) []build.TransactionMutator { ops := []build.TransactionMutator{} if len(newOrders) >= len(oldOffers) { - offset := len(newOrders) - len(oldOffers) - for i := len(newOrders) - 1; (i - offset) >= 0; i-- { - ops = doModifyOffer(oldOffers[i-offset], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, ops, hackPriceInvertForBuyOrderChangeCheck) + for i := 0; i < len(oldOffers); i++ { + ops = doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, ops, hackPriceInvertForBuyOrderChangeCheck) } // create offers for remaining new bids - for i := offset - 1; i >= 0; i-- { + for i := len(oldOffers); i < len(newOrders); i++ { price := model.NumberFromFloat(newOrders[i].Price.AsFloat()*priceMultiplier, utils.SdexPrecision).AsFloat() vol := model.NumberFromFloat(newOrders[i].Volume.AsFloat()/s.config.VOLUME_DIVIDE_BY, utils.SdexPrecision).AsFloat() mo := createOffer(*s.baseAsset, *s.quoteAsset, price, vol) @@ -131,13 +130,12 @@ func (s *mirrorStrategy) updateLevels( } } } else { - offset := len(oldOffers) - len(newOrders) - for i := len(oldOffers) - 1; (i - offset) >= 0; i-- { - ops = doModifyOffer(oldOffers[i], newOrders[i-offset], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, ops, hackPriceInvertForBuyOrderChangeCheck) + for i := 0; i < len(newOrders); i++ { + ops = doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, ops, hackPriceInvertForBuyOrderChangeCheck) } // delete remaining prior offers - for i := offset - 1; i >= 0; i-- { + for i := len(newOrders); i < len(oldOffers); i++ { op := s.sdex.DeleteOffer(oldOffers[i]) ops = append(ops, op) } diff --git a/plugins/sellSideStrategy.go b/plugins/sellSideStrategy.go index 7e6822366..ec2901b2b 100644 --- a/plugins/sellSideStrategy.go +++ b/plugins/sellSideStrategy.go @@ -91,7 +91,7 @@ func (s *sellSideStrategy) PreUpdate(maxAssetBase float64, maxAssetQuote float64 // UpdateWithOps impl func (s *sellSideStrategy) UpdateWithOps(offers []horizon.Offer) (ops []build.TransactionMutator, newTopOffer *model.Number, e error) { newTopOffer = nil - for i := len(s.currentLevels) - 1; i >= 0; i-- { + for i := 0; i < len(s.currentLevels); i++ { op := s.updateSellLevel(offers, i) if op != nil { offer, e := model.NumberFromString(op.MO.Price.String(), 7) From 5894e15045619df3e5e7556e3409a59ca30492c8 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Fri, 21 Sep 2018 11:57:59 -0700 Subject: [PATCH 09/31] cap3: 9 - native fee inclusion checks source/trader account usage --- plugins/sdex.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/sdex.go b/plugins/sdex.go index 5ab33ed93..9883c8a91 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -197,8 +197,11 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As } // explicitly check that we will not oversell XLM because of fee and min reserves - // at the minimum it will cost us a unit of base fee for this operation - incrementalNativeAmountRaw := baseFee + incrementalNativeAmountRaw := 0.0 + if sdex.TradingAccount == sdex.SourceAccount { + // at the minimum it will cost us a unit of base fee for this operation + incrementalNativeAmountRaw += baseFee + } if offer == nil { // new offers will increase the min reserve incrementalNativeAmountRaw += baseReserve From 8a58266aa9764ae02bf2cf70413b548bdebbbfef Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Fri, 21 Sep 2018 13:00:52 -0700 Subject: [PATCH 10/31] cap3: 10 - delete offer if nothing was created and we were planning to modify an existing offer --- plugins/sdex.go | 160 +++++++++++++++++------------------- plugins/sellSideStrategy.go | 17 ++-- trader/trader.go | 7 +- 3 files changed, 94 insertions(+), 90 deletions(-) diff --git a/plugins/sdex.go b/plugins/sdex.go index 9883c8a91..e6c3bf12e 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -175,7 +175,8 @@ func (sdex *SDEX) assetBalance(asset horizon.Asset) (float64, float64, float64, // createModifySellOffer is the main method that handles the logic of creating or modifying an offer, note that all offers are treated as sell offers in Stellar func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.Asset, buying horizon.Asset, price float64, amount float64) *build.ManageOfferBuilder { // check liability limits on the asset being sold - willOversell, incrementalSell, e := sdex.willOversell(offer, selling, amount) + incrementalSell := amount + willOversell, e := sdex.willOversell(selling, amount) if e != nil { log.Println(e) return nil @@ -186,7 +187,8 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As } // check trust limits on asset being bought - willOverbuy, incrementalBuy, e := sdex.willOverbuy(offer, buying, price*amount) + incrementalBuy := price * amount + willOverbuy, e := sdex.willOverbuy(buying, incrementalBuy) if e != nil { log.Println(e) return nil @@ -209,8 +211,6 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As incrementalNativeAmountTotal := incrementalNativeAmountRaw if selling.Type == utils.Native { incrementalNativeAmountTotal += incrementalSell - } else if buying.Type == utils.Native { - incrementalNativeAmountTotal -= incrementalBuy } willOversellNative, e := sdex.willOversellNative(incrementalNativeAmountTotal) if e != nil { @@ -263,7 +263,7 @@ func (sdex *SDEX) willOversellNative(incrementalNativeAmount float64) (bool, err if e != nil { return false, e } - nativeLiabilities, e := sdex.liabilities(utils.NativeAsset) + nativeLiabilities, e := sdex.assetLiabilities(utils.NativeAsset) if e != nil { return false, e } @@ -272,94 +272,39 @@ func (sdex *SDEX) willOversellNative(incrementalNativeAmount float64) (bool, err return willOversellNative, nil } -// willOversell returns willOversell, incrementalAmount, error -func (sdex *SDEX) willOversell(offer *horizon.Offer, asset horizon.Asset, amountSelling float64) (bool, float64, error) { - offerSellingAmount, e := sdex.getSignedAmount(offer, asset) - if e != nil { - return false, 0, e - } - if offerSellingAmount > 0 { - return false, 0, fmt.Errorf("error: offer.Selling (%s) does not match asset being sold (%s)", utils.Asset2String(offer.Selling), asset) - } - - incrementalAmount := amountSelling - offerSellingAmount - // reducing our selling amount can never exceed the threshold for that asset - if incrementalAmount < 0 { - return false, incrementalAmount, nil - } - +// willOversell returns willOversell, error +func (sdex *SDEX) willOversell(asset horizon.Asset, amountSelling float64) (bool, error) { bal, _, minAccountBal, e := sdex.assetBalance(asset) if e != nil { - return false, 0, e + return false, e } - liabilities, e := sdex.liabilities(asset) + liabilities, e := sdex.assetLiabilities(asset) if e != nil { - return false, 0, e + return false, e } - willOversell := incrementalAmount > (bal - minAccountBal - liabilities.Selling) - return willOversell, incrementalAmount, nil + willOversell := amountSelling > (bal - minAccountBal - liabilities.Selling) + return willOversell, nil } -// willOverbuy returns willOverbuy, incrementalAmount, error -func (sdex *SDEX) willOverbuy(offer *horizon.Offer, asset horizon.Asset, amountBuying float64) (bool, float64, error) { - offerBuyingAmount, e := sdex.getSignedAmount(offer, asset) - if e != nil { - return false, 0, e - } - if offerBuyingAmount < 0 { - return false, 0, fmt.Errorf("error: offer.Buying (%s) does not match asset being bought (%s)", utils.Asset2String(offer.Buying), asset) - } - - incrementalAmount := amountBuying - offerBuyingAmount - // if we are reducing our buying amount of an asset then it cannot exceed the threshold for that asset - if incrementalAmount < 0 { - return false, incrementalAmount, nil - } +// willOverbuy returns willOverbuy, error +func (sdex *SDEX) willOverbuy(asset horizon.Asset, amountBuying float64) (bool, error) { if asset.Type == utils.Native { // you can never overbuy the native asset - return false, incrementalAmount, nil + return false, nil } _, trust, _, e := sdex.assetBalance(asset) if e != nil { - return false, 0, e - } - liabilities, e := sdex.liabilities(asset) - if e != nil { - return false, 0, e - } - - willOverbuy := incrementalAmount > (trust - liabilities.Buying) - return willOverbuy, incrementalAmount, nil -} - -// getSignedAmount gets the amount in the unit of the asset passed in, + for buying, - for selling. 0 if asset is not in the offer -func (sdex *SDEX) getSignedAmount(offer *horizon.Offer, asset horizon.Asset) (float64, error) { - if offer == nil { - return 0, nil - } - - if offer.Buying != asset && offer.Selling != asset { - return 0, nil + return false, e } - - offerAmt, e := sdex.ParseOfferAmount(offer.Amount) + liabilities, e := sdex.assetLiabilities(asset) if e != nil { - return 0, e - } - - if offer.Selling == asset { - // negative sign because offer is being sold - return -offerAmt, nil + return false, e } - offerPrice, e := sdex.ParseOfferAmount(offer.Price) - if e != nil { - return 0, e - } - // positive sign because offer is being bought; multiply with offerPrice to keep the units in the asset being bought - return offerAmt * offerPrice, nil + willOverbuy := amountBuying > (trust - liabilities.Buying) + return willOverbuy, nil } // SubmitOps submits the passed in operations to the network asynchronously in a single transaction @@ -439,41 +384,90 @@ func (sdex *SDEX) submit(txeB64 string) { log.Printf("(async) tx confirmation hash: %s\n", resp.Hash) } -// ResetCachedLiabilities resets the cache -func (sdex *SDEX) ResetCachedLiabilities() { +// ResetCachedLiabilities resets the cache to include only the two assets passed in +func (sdex *SDEX) ResetCachedLiabilities(assetBase horizon.Asset, assetQuote horizon.Asset) error { + // re-compute the liabilities sdex.cachedLiabilities = map[horizon.Asset]Liabilities{} + baseLiabilities, basePairLiabilities, e := sdex.pairLiabilities(assetBase, assetQuote) + if e != nil { + return e + } + quoteLiabilities, quotePairLiabilities, e := sdex.pairLiabilities(assetQuote, assetBase) + if e != nil { + return e + } + + // delete liability amounts related to all offers (filter on only those offers involving **both** assets in case the account is used by multiple bots) + sdex.cachedLiabilities[assetBase] = Liabilities{ + Buying: baseLiabilities.Buying - basePairLiabilities.Buying, + Selling: baseLiabilities.Selling - basePairLiabilities.Selling, + } + sdex.cachedLiabilities[assetQuote] = Liabilities{ + Buying: quoteLiabilities.Buying - quotePairLiabilities.Buying, + Selling: quoteLiabilities.Selling - quotePairLiabilities.Selling, + } + return nil } -func (sdex *SDEX) liabilities(asset horizon.Asset) (*Liabilities, error) { +// assetLiabilities returns the liabilities for the asset +func (sdex *SDEX) assetLiabilities(asset horizon.Asset) (*Liabilities, error) { if v, ok := sdex.cachedLiabilities[asset]; ok { return &v, nil } + assetLiabilities, _, e := sdex._liabilities(asset, asset) // pass in the same asset, we ignore the returned object anyway + return assetLiabilities, e +} + +// pairLiabilities returns the liabilities for the asset along with the pairLiabilities +func (sdex *SDEX) pairLiabilities(asset horizon.Asset, otherAsset horizon.Asset) (*Liabilities, *Liabilities, error) { + assetLiabilities, pairLiabilities, e := sdex._liabilities(asset, otherAsset) + return assetLiabilities, pairLiabilities, e +} + +// liabilities returns the asset liabilities and pairLiabilities (non-nil only if the other asset is specified) +func (sdex *SDEX) _liabilities(asset horizon.Asset, otherAsset horizon.Asset) (*Liabilities, *Liabilities, error) { // uses all offers for this trading account to accommodate sharing by other bots offers, err := utils.LoadAllOffers(sdex.TradingAccount, sdex.API) if err != nil { assetString := utils.Asset2String(asset) log.Printf("error: cannot load offers to compute liabilities for asset (%s): %s\n", assetString, err) - return nil, err + return nil, nil, err } + // liabilities for the asset liabilities := Liabilities{} + // liabilities for the asset w.r.t. the trading pair + pairLiabilities := Liabilities{} for _, offer := range offers { if offer.Selling == asset { offerAmt, err := sdex.ParseOfferAmount(offer.Amount) if err != nil { - return nil, err + return nil, nil, err } liabilities.Selling += offerAmt + + if offer.Buying == otherAsset { + pairLiabilities.Selling += offerAmt + } } else if offer.Buying == asset { offerAmt, err := sdex.ParseOfferAmount(offer.Amount) if err != nil { - return nil, err + return nil, nil, err + } + offerPrice, err := sdex.ParseOfferAmount(offer.Price) + if err != nil { + return nil, nil, err + } + buyingAmount := offerAmt * offerPrice + liabilities.Buying += buyingAmount + + if offer.Selling == otherAsset { + pairLiabilities.Buying += buyingAmount } - liabilities.Buying += offerAmt } } sdex.cachedLiabilities[asset] = liabilities - return &liabilities, nil + return &liabilities, &pairLiabilities, nil } diff --git a/plugins/sellSideStrategy.go b/plugins/sellSideStrategy.go index ec2901b2b..a0cd1e693 100644 --- a/plugins/sellSideStrategy.go +++ b/plugins/sellSideStrategy.go @@ -92,7 +92,7 @@ func (s *sellSideStrategy) PreUpdate(maxAssetBase float64, maxAssetQuote float64 func (s *sellSideStrategy) UpdateWithOps(offers []horizon.Offer) (ops []build.TransactionMutator, newTopOffer *model.Number, e error) { newTopOffer = nil for i := 0; i < len(s.currentLevels); i++ { - op := s.updateSellLevel(offers, i) + deleteIfNilOp, op := s.updateSellLevel(offers, i) if op != nil { offer, e := model.NumberFromString(op.MO.Price.String(), 7) if e != nil { @@ -104,6 +104,11 @@ func (s *sellSideStrategy) UpdateWithOps(offers []horizon.Offer) (ops []build.Tr newTopOffer = offer } + ops = append(ops, op) + } else if op == nil && deleteIfNilOp { + // delete offer if nothing was created and we had an extra offer + op := s.sdex.DeleteOffer(offers[i]) + log.Printf("deleting offer because it is extra and we could not modify it, offerId=%d\n", offers[i].ID) ops = append(ops, op) } } @@ -115,8 +120,8 @@ func (s *sellSideStrategy) PostUpdate() error { return nil } -// Selling Base -func (s *sellSideStrategy) updateSellLevel(offers []horizon.Offer, index int) *build.ManageOfferBuilder { +// updateSellLevel returns true if we should delete the offer at this level when the manageOfferBuilder is nil +func (s *sellSideStrategy) updateSellLevel(offers []horizon.Offer, index int) (bool, *build.ManageOfferBuilder) { targetPrice := s.currentLevels[index].Price targetAmount := s.currentLevels[index].Amount if s.divideAmountByPrice { @@ -133,7 +138,7 @@ func (s *sellSideStrategy) updateSellLevel(offers []horizon.Offer, index int) *b } // no existing offer at this index log.Printf("sell,create,p=%.7f,a=%.7f\n", targetPrice.AsFloat(), targetAmount.AsFloat()) - return s.sdex.CreateSellOffer(*s.assetBase, *s.assetQuote, targetPrice.AsFloat(), targetAmount.AsFloat()) + return false, s.sdex.CreateSellOffer(*s.assetBase, *s.assetQuote, targetPrice.AsFloat(), targetAmount.AsFloat()) } highestPrice := targetPrice.AsFloat() + targetPrice.AsFloat()*s.priceTolerance @@ -157,7 +162,7 @@ func (s *sellSideStrategy) updateSellLevel(offers []horizon.Offer, index int) *b } log.Printf("sell,modify,tp=%.7f,ta=%.7f,curPrice=%.7f,highPrice=%.7f,lowPrice=%.7f,curAmt=%.7f,minAmt=%.7f,maxAmt=%.7f\n", targetPrice.AsFloat(), targetAmount.AsFloat(), curPrice, highestPrice, lowestPrice, curAmount, minAmount, maxAmount) - return s.sdex.ModifySellOffer(offers[index], targetPrice.AsFloat(), targetAmount.AsFloat()) + return true, s.sdex.ModifySellOffer(offers[index], targetPrice.AsFloat(), targetAmount.AsFloat()) } - return nil + return false, nil } diff --git a/trader/trader.go b/trader/trader.go index 8b99860f6..20584c0f9 100644 --- a/trader/trader.go +++ b/trader/trader.go @@ -117,7 +117,12 @@ func (t *Trader) update() { // reset cached liabilities here so we only compute it once per update // TODO 2 - calculate this here and pass it in - t.sdex.ResetCachedLiabilities() + e = t.sdex.ResetCachedLiabilities(t.assetBase, t.assetQuote) + if e != nil { + log.Println(e) + t.deleteAllOffers() + return + } ops, e := t.strat.UpdateWithOps(t.buyingAOffers, t.sellingAOffers) if e != nil { log.Println(e) From c443fe427fdb745506ea425ea362a3d5aa1644b9 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 24 Sep 2018 14:17:33 -0700 Subject: [PATCH 11/31] cap3: 11 - prepend deleteOps so we "free" up our liabilities capacity to place the new/modified offers --- plugins/sellSideStrategy.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/sellSideStrategy.go b/plugins/sellSideStrategy.go index a0cd1e693..1a606e129 100644 --- a/plugins/sellSideStrategy.go +++ b/plugins/sellSideStrategy.go @@ -90,6 +90,7 @@ func (s *sellSideStrategy) PreUpdate(maxAssetBase float64, maxAssetQuote float64 // UpdateWithOps impl func (s *sellSideStrategy) UpdateWithOps(offers []horizon.Offer) (ops []build.TransactionMutator, newTopOffer *model.Number, e error) { + deleteOps := []build.TransactionMutator{} newTopOffer = nil for i := 0; i < len(s.currentLevels); i++ { deleteIfNilOp, op := s.updateSellLevel(offers, i) @@ -107,11 +108,15 @@ func (s *sellSideStrategy) UpdateWithOps(offers []horizon.Offer) (ops []build.Tr ops = append(ops, op) } else if op == nil && deleteIfNilOp { // delete offer if nothing was created and we had an extra offer - op := s.sdex.DeleteOffer(offers[i]) + delOp := s.sdex.DeleteOffer(offers[i]) log.Printf("deleting offer because it is extra and we could not modify it, offerId=%d\n", offers[i].ID) - ops = append(ops, op) + deleteOps = append(deleteOps, delOp) } } + + // prepend deleteOps because we want to delete offers first so we "free" up our liabilities capacity to place the new/modified offers + ops = append(deleteOps, ops...) + return ops, newTopOffer, nil } From 57640bea08b3e32deab34d18eba692e0a9c3211e Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 24 Sep 2018 14:47:45 -0700 Subject: [PATCH 12/31] cap3: 12 - better error propagation from sdex.createModifySellOffer --- plugins/mirrorStrategy.go | 47 ++++++++++++++++++++++++++----------- plugins/sdex.go | 41 ++++++++++++++++---------------- plugins/sellSideStrategy.go | 15 ++++++++---- 3 files changed, 64 insertions(+), 39 deletions(-) diff --git a/plugins/mirrorStrategy.go b/plugins/mirrorStrategy.go index 9ef63a6ca..09235a116 100644 --- a/plugins/mirrorStrategy.go +++ b/plugins/mirrorStrategy.go @@ -75,7 +75,7 @@ func (s mirrorStrategy) UpdateWithOps( return nil, e } - buyOps := s.updateLevels( + buyOps, e := s.updateLevels( buyingAOffers, ob.Bids(), s.sdex.ModifyBuyOffer, @@ -83,8 +83,11 @@ func (s mirrorStrategy) UpdateWithOps( (1 - s.config.PER_LEVEL_SPREAD), true, ) + if e != nil { + return nil, e + } log.Printf("num. buyOps in this update: %d\n", len(buyOps)) - sellOps := s.updateLevels( + sellOps, e := s.updateLevels( sellingAOffers, ob.Asks(), s.sdex.ModifySellOffer, @@ -92,6 +95,9 @@ func (s mirrorStrategy) UpdateWithOps( (1 + s.config.PER_LEVEL_SPREAD), false, ) + if e != nil { + return nil, e + } log.Printf("num. sellOps in this update: %d\n", len(sellOps)) ops := []build.TransactionMutator{} @@ -109,29 +115,39 @@ func (s mirrorStrategy) UpdateWithOps( func (s *mirrorStrategy) updateLevels( oldOffers []horizon.Offer, newOrders []model.Order, - modifyOffer func(offer horizon.Offer, price float64, amount float64) *build.ManageOfferBuilder, - createOffer func(baseAsset horizon.Asset, quoteAsset horizon.Asset, price float64, amount float64) *build.ManageOfferBuilder, + modifyOffer func(offer horizon.Offer, price float64, amount float64) (*build.ManageOfferBuilder, error), + createOffer func(baseAsset horizon.Asset, quoteAsset horizon.Asset, price float64, amount float64) (*build.ManageOfferBuilder, error), priceMultiplier float64, hackPriceInvertForBuyOrderChangeCheck bool, // needed because createBuy and modBuy inverts price so we need this for price comparison in doModifyOffer -) []build.TransactionMutator { +) ([]build.TransactionMutator, error) { ops := []build.TransactionMutator{} + var e error if len(newOrders) >= len(oldOffers) { for i := 0; i < len(oldOffers); i++ { - ops = doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, ops, hackPriceInvertForBuyOrderChangeCheck) + ops, e = doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, ops, hackPriceInvertForBuyOrderChangeCheck) + if e != nil { + return nil, e + } } // create offers for remaining new bids for i := len(oldOffers); i < len(newOrders); i++ { price := model.NumberFromFloat(newOrders[i].Price.AsFloat()*priceMultiplier, utils.SdexPrecision).AsFloat() vol := model.NumberFromFloat(newOrders[i].Volume.AsFloat()/s.config.VOLUME_DIVIDE_BY, utils.SdexPrecision).AsFloat() - mo := createOffer(*s.baseAsset, *s.quoteAsset, price, vol) + mo, e := createOffer(*s.baseAsset, *s.quoteAsset, price, vol) + if e != nil { + return nil, e + } if mo != nil { ops = append(ops, *mo) } } } else { for i := 0; i < len(newOrders); i++ { - ops = doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, ops, hackPriceInvertForBuyOrderChangeCheck) + ops, e = doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, ops, hackPriceInvertForBuyOrderChangeCheck) + if e != nil { + return nil, e + } } // delete remaining prior offers @@ -140,7 +156,7 @@ func (s *mirrorStrategy) updateLevels( ops = append(ops, op) } } - return ops + return ops, nil } func doModifyOffer( @@ -148,10 +164,10 @@ func doModifyOffer( newOrder model.Order, priceMultiplier float64, volumeDivideBy float64, - modifyOffer func(offer horizon.Offer, price float64, amount float64) *build.ManageOfferBuilder, + modifyOffer func(offer horizon.Offer, price float64, amount float64) (*build.ManageOfferBuilder, error), ops []build.TransactionMutator, hackPriceInvertForBuyOrderChangeCheck bool, // needed because createBuy and modBuy inverts price so we need this for price comparison in doModifyOffer -) []build.TransactionMutator { +) ([]build.TransactionMutator, error) { price := newOrder.Price.AsFloat() * priceMultiplier vol := newOrder.Volume.AsFloat() / volumeDivideBy @@ -167,18 +183,21 @@ func doModifyOffer( epsilon := 0.0001 sameOrderParams := utils.FloatEquals(oldPrice.AsFloat(), newPrice.AsFloat(), epsilon) && utils.FloatEquals(oldVol.AsFloat(), newVol.AsFloat(), epsilon) if sameOrderParams { - return ops + return ops, nil } - mo := modifyOffer( + mo, e := modifyOffer( oldOffer, model.NumberFromFloat(price, utils.SdexPrecision).AsFloat(), model.NumberFromFloat(vol, utils.SdexPrecision).AsFloat(), ) + if e != nil { + return nil, e + } if mo != nil { ops = append(ops, *mo) } - return ops + return ops, nil } // PostUpdate changes the strategy's state after the update has taken place diff --git a/plugins/sdex.go b/plugins/sdex.go index e6c3bf12e..1fdb6d6b1 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -113,22 +113,21 @@ func (sdex *SDEX) DeleteOffer(offer horizon.Offer) build.ManageOfferBuilder { } // ModifyBuyOffer modifies a buy offer -func (sdex *SDEX) ModifyBuyOffer(offer horizon.Offer, price float64, amount float64) *build.ManageOfferBuilder { +func (sdex *SDEX) ModifyBuyOffer(offer horizon.Offer, price float64, amount float64) (*build.ManageOfferBuilder, error) { return sdex.ModifySellOffer(offer, 1/price, amount*price) } // ModifySellOffer modifies a sell offer -func (sdex *SDEX) ModifySellOffer(offer horizon.Offer, price float64, amount float64) *build.ManageOfferBuilder { +func (sdex *SDEX) ModifySellOffer(offer horizon.Offer, price float64, amount float64) (*build.ManageOfferBuilder, error) { return sdex.createModifySellOffer(&offer, offer.Selling, offer.Buying, price, amount) } // CreateSellOffer creates a sell offer -func (sdex *SDEX) CreateSellOffer(base horizon.Asset, counter horizon.Asset, price float64, amount float64) *build.ManageOfferBuilder { +func (sdex *SDEX) CreateSellOffer(base horizon.Asset, counter horizon.Asset, price float64, amount float64) (*build.ManageOfferBuilder, error) { if amount > 0 { return sdex.createModifySellOffer(nil, base, counter, price, amount) } - log.Println("error: cannot place sell order, zero amount") - return nil + return nil, fmt.Errorf("error: cannot place sell order, zero amount") } // ParseOfferAmount is a convenience method to parse an offer amount @@ -173,29 +172,25 @@ func (sdex *SDEX) assetBalance(asset horizon.Asset) (float64, float64, float64, } // createModifySellOffer is the main method that handles the logic of creating or modifying an offer, note that all offers are treated as sell offers in Stellar -func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.Asset, buying horizon.Asset, price float64, amount float64) *build.ManageOfferBuilder { +func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.Asset, buying horizon.Asset, price float64, amount float64) (*build.ManageOfferBuilder, error) { // check liability limits on the asset being sold incrementalSell := amount willOversell, e := sdex.willOversell(selling, amount) if e != nil { - log.Println(e) - return nil + return nil, e } if willOversell { - log.Printf("not placing offer because we run the risk of overselling the asset '%s'\n", utils.Asset2String(selling)) - return nil + return nil, nil } // check trust limits on asset being bought incrementalBuy := price * amount willOverbuy, e := sdex.willOverbuy(buying, incrementalBuy) if e != nil { - log.Println(e) - return nil + return nil, e } if willOverbuy { - log.Printf("not placing offer because we run the risk of overbuying the asset '%s'\n", utils.Asset2String(buying)) - return nil + return nil, nil } // explicitly check that we will not oversell XLM because of fee and min reserves @@ -214,12 +209,10 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As } willOversellNative, e := sdex.willOversellNative(incrementalNativeAmountTotal) if e != nil { - log.Println(e) - return nil + return nil, e } if willOversellNative { - log.Println("not placing offer because we run the risk of overselling the native asset after including fee and min reserves") - return nil + return nil, nil } stringPrice := strconv.FormatFloat(price, 'f', int(utils.SdexPrecision), 64) @@ -254,7 +247,7 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As Selling: sdex.cachedLiabilities[utils.NativeAsset].Selling + incrementalNativeAmountRaw, Buying: sdex.cachedLiabilities[utils.NativeAsset].Buying, } - return &result + return &result, nil } // willOversellNative returns willOversellNative, error @@ -269,6 +262,10 @@ func (sdex *SDEX) willOversellNative(incrementalNativeAmount float64) (bool, err } willOversellNative := incrementalNativeAmount > (nativeBal - minAccountBal - nativeLiabilities.Selling) + if willOversellNative { + log.Printf("we will oversell the native asset after considering fee and min reserves, incrementalNativeAmount = %.7f, nativeBal = %.7f, minAccountBal = %.7f, nativeLiabilities.Selling = %.7f\n", + incrementalNativeAmount, nativeBal, minAccountBal, nativeLiabilities.Selling) + } return willOversellNative, nil } @@ -284,6 +281,10 @@ func (sdex *SDEX) willOversell(asset horizon.Asset, amountSelling float64) (bool } willOversell := amountSelling > (bal - minAccountBal - liabilities.Selling) + if willOversell { + log.Printf("we will oversell the asset '%s', amountSelling = %.7f, bal = %.7f, minAccountBal = %.7f, liabilities.Selling = %.7f\n", + utils.Asset2String(asset), amountSelling, bal, minAccountBal, liabilities.Selling) + } return willOversell, nil } @@ -340,7 +341,7 @@ func (sdex *SDEX) SubmitOps(ops []build.TransactionMutator) error { } // CreateBuyOffer creates a buy offer -func (sdex *SDEX) CreateBuyOffer(base horizon.Asset, counter horizon.Asset, price float64, amount float64) *build.ManageOfferBuilder { +func (sdex *SDEX) CreateBuyOffer(base horizon.Asset, counter horizon.Asset, price float64, amount float64) (*build.ManageOfferBuilder, error) { return sdex.CreateSellOffer(counter, base, 1/price, amount*price) } diff --git a/plugins/sellSideStrategy.go b/plugins/sellSideStrategy.go index 1a606e129..9da395f0c 100644 --- a/plugins/sellSideStrategy.go +++ b/plugins/sellSideStrategy.go @@ -93,7 +93,10 @@ func (s *sellSideStrategy) UpdateWithOps(offers []horizon.Offer) (ops []build.Tr deleteOps := []build.TransactionMutator{} newTopOffer = nil for i := 0; i < len(s.currentLevels); i++ { - deleteIfNilOp, op := s.updateSellLevel(offers, i) + deleteIfNilOp, op, e := s.updateSellLevel(offers, i) + if e != nil { + return nil, nil, e + } if op != nil { offer, e := model.NumberFromString(op.MO.Price.String(), 7) if e != nil { @@ -126,7 +129,7 @@ func (s *sellSideStrategy) PostUpdate() error { } // updateSellLevel returns true if we should delete the offer at this level when the manageOfferBuilder is nil -func (s *sellSideStrategy) updateSellLevel(offers []horizon.Offer, index int) (bool, *build.ManageOfferBuilder) { +func (s *sellSideStrategy) updateSellLevel(offers []horizon.Offer, index int) (bool, *build.ManageOfferBuilder, error) { targetPrice := s.currentLevels[index].Price targetAmount := s.currentLevels[index].Amount if s.divideAmountByPrice { @@ -143,7 +146,8 @@ func (s *sellSideStrategy) updateSellLevel(offers []horizon.Offer, index int) (b } // no existing offer at this index log.Printf("sell,create,p=%.7f,a=%.7f\n", targetPrice.AsFloat(), targetAmount.AsFloat()) - return false, s.sdex.CreateSellOffer(*s.assetBase, *s.assetQuote, targetPrice.AsFloat(), targetAmount.AsFloat()) + op, e := s.sdex.CreateSellOffer(*s.assetBase, *s.assetQuote, targetPrice.AsFloat(), targetAmount.AsFloat()) + return false, op, e } highestPrice := targetPrice.AsFloat() + targetPrice.AsFloat()*s.priceTolerance @@ -167,7 +171,8 @@ func (s *sellSideStrategy) updateSellLevel(offers []horizon.Offer, index int) (b } log.Printf("sell,modify,tp=%.7f,ta=%.7f,curPrice=%.7f,highPrice=%.7f,lowPrice=%.7f,curAmt=%.7f,minAmt=%.7f,maxAmt=%.7f\n", targetPrice.AsFloat(), targetAmount.AsFloat(), curPrice, highestPrice, lowestPrice, curAmount, minAmount, maxAmount) - return true, s.sdex.ModifySellOffer(offers[index], targetPrice.AsFloat(), targetAmount.AsFloat()) + op, e := s.sdex.ModifySellOffer(offers[index], targetPrice.AsFloat(), targetAmount.AsFloat()) + return true, op, e } - return false, nil + return false, nil, nil } From 6f49cf664751ada92de2005d67d4ceec4ae36cbe Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 24 Sep 2018 15:36:29 -0700 Subject: [PATCH 13/31] cap3: 13 - extracted liability updates after placing/modifying offers to callers in sellSideStrategy and mirrorStrategy --- plugins/mirrorStrategy.go | 28 +++++++++++++++------- plugins/sdex.go | 47 +++++++++++++++++++++---------------- plugins/sellSideStrategy.go | 16 +++++++++++-- 3 files changed, 60 insertions(+), 31 deletions(-) diff --git a/plugins/mirrorStrategy.go b/plugins/mirrorStrategy.go index 09235a116..520e87074 100644 --- a/plugins/mirrorStrategy.go +++ b/plugins/mirrorStrategy.go @@ -115,8 +115,8 @@ func (s mirrorStrategy) UpdateWithOps( func (s *mirrorStrategy) updateLevels( oldOffers []horizon.Offer, newOrders []model.Order, - modifyOffer func(offer horizon.Offer, price float64, amount float64) (*build.ManageOfferBuilder, error), - createOffer func(baseAsset horizon.Asset, quoteAsset horizon.Asset, price float64, amount float64) (*build.ManageOfferBuilder, error), + modifyOffer func(offer horizon.Offer, price float64, amount float64, incrementalNativeAmountRaw float64) (*build.ManageOfferBuilder, error), + createOffer func(baseAsset horizon.Asset, quoteAsset horizon.Asset, price float64, amount float64, incrementalNativeAmountRaw float64) (*build.ManageOfferBuilder, error), priceMultiplier float64, hackPriceInvertForBuyOrderChangeCheck bool, // needed because createBuy and modBuy inverts price so we need this for price comparison in doModifyOffer ) ([]build.TransactionMutator, error) { @@ -124,7 +124,7 @@ func (s *mirrorStrategy) updateLevels( var e error if len(newOrders) >= len(oldOffers) { for i := 0; i < len(oldOffers); i++ { - ops, e = doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, ops, hackPriceInvertForBuyOrderChangeCheck) + ops, e = s.doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, ops, hackPriceInvertForBuyOrderChangeCheck) if e != nil { return nil, e } @@ -134,17 +134,21 @@ func (s *mirrorStrategy) updateLevels( for i := len(oldOffers); i < len(newOrders); i++ { price := model.NumberFromFloat(newOrders[i].Price.AsFloat()*priceMultiplier, utils.SdexPrecision).AsFloat() vol := model.NumberFromFloat(newOrders[i].Volume.AsFloat()/s.config.VOLUME_DIVIDE_BY, utils.SdexPrecision).AsFloat() - mo, e := createOffer(*s.baseAsset, *s.quoteAsset, price, vol) + incrementalNativeAmountRaw := s.sdex.ComputeIncrementalNativeAmountRaw(true) + var mo *build.ManageOfferBuilder + mo, e = createOffer(*s.baseAsset, *s.quoteAsset, price, vol, incrementalNativeAmountRaw) if e != nil { return nil, e } if mo != nil { ops = append(ops, *mo) + // update the cached liabilities if we create a valid operation to create an offer + s.sdex.AddLiabilities(*s.baseAsset, *s.quoteAsset, vol, vol*price, incrementalNativeAmountRaw) } } } else { for i := 0; i < len(newOrders); i++ { - ops, e = doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, ops, hackPriceInvertForBuyOrderChangeCheck) + ops, e = s.doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, ops, hackPriceInvertForBuyOrderChangeCheck) if e != nil { return nil, e } @@ -159,12 +163,12 @@ func (s *mirrorStrategy) updateLevels( return ops, nil } -func doModifyOffer( +func (s *mirrorStrategy) doModifyOffer( oldOffer horizon.Offer, newOrder model.Order, priceMultiplier float64, volumeDivideBy float64, - modifyOffer func(offer horizon.Offer, price float64, amount float64) (*build.ManageOfferBuilder, error), + modifyOffer func(offer horizon.Offer, price float64, amount float64, incrementalNativeAmountRaw float64) (*build.ManageOfferBuilder, error), ops []build.TransactionMutator, hackPriceInvertForBuyOrderChangeCheck bool, // needed because createBuy and modBuy inverts price so we need this for price comparison in doModifyOffer ) ([]build.TransactionMutator, error) { @@ -186,16 +190,22 @@ func doModifyOffer( return ops, nil } + incrementalNativeAmountRaw := s.sdex.ComputeIncrementalNativeAmountRaw(false) + offerPrice := model.NumberFromFloat(price, utils.SdexPrecision).AsFloat() + offerAmount := model.NumberFromFloat(vol, utils.SdexPrecision).AsFloat() mo, e := modifyOffer( oldOffer, - model.NumberFromFloat(price, utils.SdexPrecision).AsFloat(), - model.NumberFromFloat(vol, utils.SdexPrecision).AsFloat(), + offerPrice, + offerAmount, + incrementalNativeAmountRaw, ) if e != nil { return nil, e } if mo != nil { ops = append(ops, *mo) + // update the cached liabilities if we create a valid operation to modify the offer + s.sdex.AddLiabilities(oldOffer.Selling, oldOffer.Buying, offerAmount, offerAmount*offerPrice, incrementalNativeAmountRaw) } return ops, nil } diff --git a/plugins/sdex.go b/plugins/sdex.go index 1fdb6d6b1..39e61cf90 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -113,19 +113,19 @@ func (sdex *SDEX) DeleteOffer(offer horizon.Offer) build.ManageOfferBuilder { } // ModifyBuyOffer modifies a buy offer -func (sdex *SDEX) ModifyBuyOffer(offer horizon.Offer, price float64, amount float64) (*build.ManageOfferBuilder, error) { - return sdex.ModifySellOffer(offer, 1/price, amount*price) +func (sdex *SDEX) ModifyBuyOffer(offer horizon.Offer, price float64, amount float64, incrementalNativeAmountRaw float64) (*build.ManageOfferBuilder, error) { + return sdex.ModifySellOffer(offer, 1/price, amount*price, incrementalNativeAmountRaw) } // ModifySellOffer modifies a sell offer -func (sdex *SDEX) ModifySellOffer(offer horizon.Offer, price float64, amount float64) (*build.ManageOfferBuilder, error) { - return sdex.createModifySellOffer(&offer, offer.Selling, offer.Buying, price, amount) +func (sdex *SDEX) ModifySellOffer(offer horizon.Offer, price float64, amount float64, incrementalNativeAmountRaw float64) (*build.ManageOfferBuilder, error) { + return sdex.createModifySellOffer(&offer, offer.Selling, offer.Buying, price, amount, incrementalNativeAmountRaw) } // CreateSellOffer creates a sell offer -func (sdex *SDEX) CreateSellOffer(base horizon.Asset, counter horizon.Asset, price float64, amount float64) (*build.ManageOfferBuilder, error) { +func (sdex *SDEX) CreateSellOffer(base horizon.Asset, counter horizon.Asset, price float64, amount float64, incrementalNativeAmountRaw float64) (*build.ManageOfferBuilder, error) { if amount > 0 { - return sdex.createModifySellOffer(nil, base, counter, price, amount) + return sdex.createModifySellOffer(nil, base, counter, price, amount, incrementalNativeAmountRaw) } return nil, fmt.Errorf("error: cannot place sell order, zero amount") } @@ -171,8 +171,22 @@ func (sdex *SDEX) assetBalance(asset horizon.Asset) (float64, float64, float64, return -1, -1, -1, errors.New("could not find a balance for the asset passed in") } +// ComputeIncrementalNativeAmountRaw returns the native amount that will be added to liabilities because of fee and min-reserve additions +func (sdex *SDEX) ComputeIncrementalNativeAmountRaw(isNewOffer bool) float64 { + incrementalNativeAmountRaw := 0.0 + if sdex.TradingAccount == sdex.SourceAccount { + // at the minimum it will cost us a unit of base fee for this operation + incrementalNativeAmountRaw += baseFee + } + if isNewOffer { + // new offers will increase the min reserve + incrementalNativeAmountRaw += baseReserve + } + return incrementalNativeAmountRaw +} + // createModifySellOffer is the main method that handles the logic of creating or modifying an offer, note that all offers are treated as sell offers in Stellar -func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.Asset, buying horizon.Asset, price float64, amount float64) (*build.ManageOfferBuilder, error) { +func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.Asset, buying horizon.Asset, price float64, amount float64, incrementalNativeAmountRaw float64) (*build.ManageOfferBuilder, error) { // check liability limits on the asset being sold incrementalSell := amount willOversell, e := sdex.willOversell(selling, amount) @@ -194,15 +208,6 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As } // explicitly check that we will not oversell XLM because of fee and min reserves - incrementalNativeAmountRaw := 0.0 - if sdex.TradingAccount == sdex.SourceAccount { - // at the minimum it will cost us a unit of base fee for this operation - incrementalNativeAmountRaw += baseFee - } - if offer == nil { - // new offers will increase the min reserve - incrementalNativeAmountRaw += baseReserve - } incrementalNativeAmountTotal := incrementalNativeAmountRaw if selling.Type == utils.Native { incrementalNativeAmountTotal += incrementalSell @@ -233,8 +238,11 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As mutators = append(mutators, build.SourceAccount{AddressOrSeed: sdex.TradingAccount}) } result := build.ManageOffer(false, mutators...) + return &result, nil +} - // update the cached liabilities +// addLiabilities updates the cached liabilities, units are in their respective assets +func (sdex *SDEX) AddLiabilities(selling horizon.Asset, buying horizon.Asset, incrementalSell float64, incrementalBuy float64, incrementalNativeAmountRaw float64) { sdex.cachedLiabilities[selling] = Liabilities{ Selling: sdex.cachedLiabilities[selling].Selling + incrementalSell, Buying: sdex.cachedLiabilities[selling].Buying, @@ -247,7 +255,6 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As Selling: sdex.cachedLiabilities[utils.NativeAsset].Selling + incrementalNativeAmountRaw, Buying: sdex.cachedLiabilities[utils.NativeAsset].Buying, } - return &result, nil } // willOversellNative returns willOversellNative, error @@ -341,8 +348,8 @@ func (sdex *SDEX) SubmitOps(ops []build.TransactionMutator) error { } // CreateBuyOffer creates a buy offer -func (sdex *SDEX) CreateBuyOffer(base horizon.Asset, counter horizon.Asset, price float64, amount float64) (*build.ManageOfferBuilder, error) { - return sdex.CreateSellOffer(counter, base, 1/price, amount*price) +func (sdex *SDEX) CreateBuyOffer(base horizon.Asset, counter horizon.Asset, price float64, amount float64, incrementalNativeAmountRaw float64) (*build.ManageOfferBuilder, error) { + return sdex.CreateSellOffer(counter, base, 1/price, amount*price, incrementalNativeAmountRaw) } func (sdex *SDEX) sign(tx *build.TransactionBuilder) (string, error) { diff --git a/plugins/sellSideStrategy.go b/plugins/sellSideStrategy.go index 9da395f0c..4e91bb48d 100644 --- a/plugins/sellSideStrategy.go +++ b/plugins/sellSideStrategy.go @@ -146,7 +146,12 @@ func (s *sellSideStrategy) updateSellLevel(offers []horizon.Offer, index int) (b } // no existing offer at this index log.Printf("sell,create,p=%.7f,a=%.7f\n", targetPrice.AsFloat(), targetAmount.AsFloat()) - op, e := s.sdex.CreateSellOffer(*s.assetBase, *s.assetQuote, targetPrice.AsFloat(), targetAmount.AsFloat()) + incrementalNativeAmountRaw := s.sdex.ComputeIncrementalNativeAmountRaw(true) + op, e := s.sdex.CreateSellOffer(*s.assetBase, *s.assetQuote, targetPrice.AsFloat(), targetAmount.AsFloat(), incrementalNativeAmountRaw) + if e == nil && op != nil { + // update the cached liabilities if we create a valid operation to create an offer + s.sdex.AddLiabilities(*s.assetBase, *s.assetQuote, targetAmount.AsFloat(), targetAmount.AsFloat()*targetPrice.AsFloat(), incrementalNativeAmountRaw) + } return false, op, e } @@ -162,6 +167,7 @@ func (s *sellSideStrategy) updateSellLevel(offers []horizon.Offer, index int) (b // existing offer not within tolerances priceTrigger := (curPrice > highestPrice) || (curPrice < lowestPrice) amountTrigger := (curAmount < minAmount) || (curAmount > maxAmount) + incrementalNativeAmountRaw := s.sdex.ComputeIncrementalNativeAmountRaw(false) if priceTrigger || amountTrigger { if targetPrice.Precision() > utils.SdexPrecision { targetPrice = *model.NumberFromFloat(targetPrice.AsFloat(), utils.SdexPrecision) @@ -171,8 +177,14 @@ func (s *sellSideStrategy) updateSellLevel(offers []horizon.Offer, index int) (b } log.Printf("sell,modify,tp=%.7f,ta=%.7f,curPrice=%.7f,highPrice=%.7f,lowPrice=%.7f,curAmt=%.7f,minAmt=%.7f,maxAmt=%.7f\n", targetPrice.AsFloat(), targetAmount.AsFloat(), curPrice, highestPrice, lowestPrice, curAmount, minAmount, maxAmount) - op, e := s.sdex.ModifySellOffer(offers[index], targetPrice.AsFloat(), targetAmount.AsFloat()) + op, e := s.sdex.ModifySellOffer(offers[index], targetPrice.AsFloat(), targetAmount.AsFloat(), incrementalNativeAmountRaw) + if e == nil && op != nil { + // update the cached liabilities if we create a valid operation to modify the offer + s.sdex.AddLiabilities(offers[index].Selling, offers[index].Buying, targetAmount.AsFloat(), targetAmount.AsFloat()*targetPrice.AsFloat(), incrementalNativeAmountRaw) + } return true, op, e } + // always add back the current offer in the cached liabilities when we don't modify it + s.sdex.AddLiabilities(offers[index].Selling, offers[index].Buying, targetAmount.AsFloat(), targetAmount.AsFloat()*targetPrice.AsFloat(), incrementalNativeAmountRaw) return false, nil, nil } From 242556624024adf6f59c25156a3513985750b8e0 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 24 Sep 2018 16:23:31 -0700 Subject: [PATCH 14/31] cap3: 14 - prepend deleteOps for mirror strategy too --- plugins/mirrorStrategy.go | 48 +++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/plugins/mirrorStrategy.go b/plugins/mirrorStrategy.go index 520e87074..ea65f9c85 100644 --- a/plugins/mirrorStrategy.go +++ b/plugins/mirrorStrategy.go @@ -121,13 +121,19 @@ func (s *mirrorStrategy) updateLevels( hackPriceInvertForBuyOrderChangeCheck bool, // needed because createBuy and modBuy inverts price so we need this for price comparison in doModifyOffer ) ([]build.TransactionMutator, error) { ops := []build.TransactionMutator{} - var e error + deleteOps := []build.TransactionMutator{} if len(newOrders) >= len(oldOffers) { for i := 0; i < len(oldOffers); i++ { - ops, e = s.doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, ops, hackPriceInvertForBuyOrderChangeCheck) + modifyOp, deleteOp, e := s.doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, hackPriceInvertForBuyOrderChangeCheck) if e != nil { return nil, e } + if modifyOp != nil { + ops = append(ops, modifyOp) + } + if deleteOp != nil { + deleteOps = append(deleteOps, deleteOp) + } } // create offers for remaining new bids @@ -135,8 +141,7 @@ func (s *mirrorStrategy) updateLevels( price := model.NumberFromFloat(newOrders[i].Price.AsFloat()*priceMultiplier, utils.SdexPrecision).AsFloat() vol := model.NumberFromFloat(newOrders[i].Volume.AsFloat()/s.config.VOLUME_DIVIDE_BY, utils.SdexPrecision).AsFloat() incrementalNativeAmountRaw := s.sdex.ComputeIncrementalNativeAmountRaw(true) - var mo *build.ManageOfferBuilder - mo, e = createOffer(*s.baseAsset, *s.quoteAsset, price, vol, incrementalNativeAmountRaw) + mo, e := createOffer(*s.baseAsset, *s.quoteAsset, price, vol, incrementalNativeAmountRaw) if e != nil { return nil, e } @@ -148,33 +153,43 @@ func (s *mirrorStrategy) updateLevels( } } else { for i := 0; i < len(newOrders); i++ { - ops, e = s.doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, ops, hackPriceInvertForBuyOrderChangeCheck) + modifyOp, deleteOp, e := s.doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, s.config.VOLUME_DIVIDE_BY, modifyOffer, hackPriceInvertForBuyOrderChangeCheck) if e != nil { return nil, e } + if modifyOp != nil { + ops = append(ops, modifyOp) + } + if deleteOp != nil { + deleteOps = append(deleteOps, deleteOp) + } } // delete remaining prior offers for i := len(newOrders); i < len(oldOffers); i++ { - op := s.sdex.DeleteOffer(oldOffers[i]) - ops = append(ops, op) + deleteOp := s.sdex.DeleteOffer(oldOffers[i]) + deleteOps = append(deleteOps, deleteOp) } } - return ops, nil + + // prepend deleteOps because we want to delete offers first so we "free" up our liabilities capacity to place the new/modified offers + allOps := append(deleteOps, ops...) + log.Printf("prepended %d deleteOps\n", len(deleteOps)) + + return allOps, nil } +// doModifyOffer returns a new modifyOp, deleteOp, error func (s *mirrorStrategy) doModifyOffer( oldOffer horizon.Offer, newOrder model.Order, priceMultiplier float64, volumeDivideBy float64, modifyOffer func(offer horizon.Offer, price float64, amount float64, incrementalNativeAmountRaw float64) (*build.ManageOfferBuilder, error), - ops []build.TransactionMutator, hackPriceInvertForBuyOrderChangeCheck bool, // needed because createBuy and modBuy inverts price so we need this for price comparison in doModifyOffer -) ([]build.TransactionMutator, error) { +) (build.TransactionMutator, build.TransactionMutator, error) { price := newOrder.Price.AsFloat() * priceMultiplier vol := newOrder.Volume.AsFloat() / volumeDivideBy - oldPrice := model.MustNumberFromString(oldOffer.Price, 6) oldVol := model.MustNumberFromString(oldOffer.Amount, 6) if hackPriceInvertForBuyOrderChangeCheck { @@ -187,7 +202,7 @@ func (s *mirrorStrategy) doModifyOffer( epsilon := 0.0001 sameOrderParams := utils.FloatEquals(oldPrice.AsFloat(), newPrice.AsFloat(), epsilon) && utils.FloatEquals(oldVol.AsFloat(), newVol.AsFloat(), epsilon) if sameOrderParams { - return ops, nil + return nil, nil, nil } incrementalNativeAmountRaw := s.sdex.ComputeIncrementalNativeAmountRaw(false) @@ -200,14 +215,17 @@ func (s *mirrorStrategy) doModifyOffer( incrementalNativeAmountRaw, ) if e != nil { - return nil, e + return nil, nil, e } if mo != nil { - ops = append(ops, *mo) // update the cached liabilities if we create a valid operation to modify the offer s.sdex.AddLiabilities(oldOffer.Selling, oldOffer.Buying, offerAmount, offerAmount*offerPrice, incrementalNativeAmountRaw) + return *mo, nil, nil } - return ops, nil + + // since mo is nil we want to delete this offer + deleteOp := s.sdex.DeleteOffer(oldOffer) + return nil, deleteOp, nil } // PostUpdate changes the strategy's state after the update has taken place From 4d5f622e42a5b65d5ac240fc64ccde660b29c25a Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Tue, 25 Sep 2018 07:25:06 -0700 Subject: [PATCH 15/31] cap3: 15 - log liabilities in trader.go after resetting and after updating ops --- plugins/sdex.go | 18 ++++++++++++++++++ trader/trader.go | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/plugins/sdex.go b/plugins/sdex.go index 39e61cf90..082eb9f6b 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -392,6 +392,24 @@ func (sdex *SDEX) submit(txeB64 string) { log.Printf("(async) tx confirmation hash: %s\n", resp.Hash) } +func (sdex *SDEX) logLiabilities(asset horizon.Asset) { + l, e := sdex.assetLiabilities(asset) + log.Printf("asset = %s, buyingLiabilities=%.7f, sellingLiabilities=%.7f\n", asset, l.Buying, l.Selling) + if e != nil { + log.Printf("could not fetch liability for asset '%s', error = %s\n", asset, e) + } +} + +// LogAllLiabilities logs the liabilities for the two assets along with the native asset +func (sdex *SDEX) LogAllLiabilities(assetBase horizon.Asset, assetQuote horizon.Asset) { + sdex.logLiabilities(assetBase) + sdex.logLiabilities(assetQuote) + + if assetBase != utils.NativeAsset && assetQuote != utils.NativeAsset { + sdex.logLiabilities(utils.NativeAsset) + } +} + // ResetCachedLiabilities resets the cache to include only the two assets passed in func (sdex *SDEX) ResetCachedLiabilities(assetBase horizon.Asset, assetQuote horizon.Asset) error { // re-compute the liabilities diff --git a/trader/trader.go b/trader/trader.go index 20584c0f9..a2e883046 100644 --- a/trader/trader.go +++ b/trader/trader.go @@ -118,12 +118,16 @@ func (t *Trader) update() { // reset cached liabilities here so we only compute it once per update // TODO 2 - calculate this here and pass it in e = t.sdex.ResetCachedLiabilities(t.assetBase, t.assetQuote) + log.Printf("liabilities after resetting\n") + t.sdex.LogAllLiabilities(t.assetBase, t.assetQuote) if e != nil { log.Println(e) t.deleteAllOffers() return } ops, e := t.strat.UpdateWithOps(t.buyingAOffers, t.sellingAOffers) + log.Printf("liabilities at the end of a call to UpdateWithOps\n") + t.sdex.LogAllLiabilities(t.assetBase, t.assetQuote) if e != nil { log.Println(e) t.deleteAllOffers() From f64535eaed0f29c9855c2529549b03e9871974bc Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 24 Sep 2018 17:28:07 -0700 Subject: [PATCH 16/31] cap3: 16 - add liabilities in mirror strat --- plugins/mirrorStrategy.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/plugins/mirrorStrategy.go b/plugins/mirrorStrategy.go index ea65f9c85..db1ab618b 100644 --- a/plugins/mirrorStrategy.go +++ b/plugins/mirrorStrategy.go @@ -148,7 +148,11 @@ func (s *mirrorStrategy) updateLevels( if mo != nil { ops = append(ops, *mo) // update the cached liabilities if we create a valid operation to create an offer - s.sdex.AddLiabilities(*s.baseAsset, *s.quoteAsset, vol, vol*price, incrementalNativeAmountRaw) + if hackPriceInvertForBuyOrderChangeCheck { + s.sdex.AddLiabilities(*s.quoteAsset, *s.baseAsset, vol*price, vol, incrementalNativeAmountRaw) + } else { + s.sdex.AddLiabilities(*s.baseAsset, *s.quoteAsset, vol, vol*price, incrementalNativeAmountRaw) + } } } } else { @@ -200,12 +204,18 @@ func (s *mirrorStrategy) doModifyOffer( newPrice := model.NumberFromFloat(price, 6) newVol := model.NumberFromFloat(vol, 6) epsilon := 0.0001 + incrementalNativeAmountRaw := s.sdex.ComputeIncrementalNativeAmountRaw(false) sameOrderParams := utils.FloatEquals(oldPrice.AsFloat(), newPrice.AsFloat(), epsilon) && utils.FloatEquals(oldVol.AsFloat(), newVol.AsFloat(), epsilon) if sameOrderParams { + // update the cached liabilities if we keep the existing offer + if hackPriceInvertForBuyOrderChangeCheck { + s.sdex.AddLiabilities(oldOffer.Selling, oldOffer.Buying, price*vol, vol, incrementalNativeAmountRaw) + } else { + s.sdex.AddLiabilities(oldOffer.Selling, oldOffer.Buying, vol, price*vol, incrementalNativeAmountRaw) + } return nil, nil, nil } - incrementalNativeAmountRaw := s.sdex.ComputeIncrementalNativeAmountRaw(false) offerPrice := model.NumberFromFloat(price, utils.SdexPrecision).AsFloat() offerAmount := model.NumberFromFloat(vol, utils.SdexPrecision).AsFloat() mo, e := modifyOffer( @@ -219,7 +229,11 @@ func (s *mirrorStrategy) doModifyOffer( } if mo != nil { // update the cached liabilities if we create a valid operation to modify the offer - s.sdex.AddLiabilities(oldOffer.Selling, oldOffer.Buying, offerAmount, offerAmount*offerPrice, incrementalNativeAmountRaw) + if hackPriceInvertForBuyOrderChangeCheck { + s.sdex.AddLiabilities(oldOffer.Selling, oldOffer.Buying, offerAmount*offerPrice, offerAmount, incrementalNativeAmountRaw) + } else { + s.sdex.AddLiabilities(oldOffer.Selling, oldOffer.Buying, offerAmount, offerAmount*offerPrice, incrementalNativeAmountRaw) + } return *mo, nil, nil } From 8dc16d8819bd182ae5b2d293313b92f6d505698d Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 27 Sep 2018 17:32:24 -0700 Subject: [PATCH 17/31] cap3: 17 - added support for partial offers in sellSideStrategy, refactored updateSellLevel --- model/number.go | 8 ++ plugins/sdex.go | 36 +++++- plugins/sellSideStrategy.go | 220 ++++++++++++++++++++++++++---------- trader/trader.go | 3 + 4 files changed, 206 insertions(+), 61 deletions(-) diff --git a/model/number.go b/model/number.go index 6c1871887..bd8728932 100644 --- a/model/number.go +++ b/model/number.go @@ -66,6 +66,14 @@ func InvertNumber(n *Number) *Number { return NumberFromFloat(1/n.AsFloat(), n.Precision()) } +// NumberByCappingPrecision returns a number with a precision that is at max the passed in precision +func NumberByCappingPrecision(n *Number, precision int8) *Number { + if n.Precision() > precision { + return NumberFromFloat(n.AsFloat(), precision) + } + return n +} + func round(num float64) int { return int(num + math.Copysign(0.5, num)) } diff --git a/plugins/sdex.go b/plugins/sdex.go index 082eb9f6b..c401b62c3 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -124,10 +124,7 @@ func (sdex *SDEX) ModifySellOffer(offer horizon.Offer, price float64, amount flo // CreateSellOffer creates a sell offer func (sdex *SDEX) CreateSellOffer(base horizon.Asset, counter horizon.Asset, price float64, amount float64, incrementalNativeAmountRaw float64) (*build.ManageOfferBuilder, error) { - if amount > 0 { - return sdex.createModifySellOffer(nil, base, counter, price, amount, incrementalNativeAmountRaw) - } - return nil, fmt.Errorf("error: cannot place sell order, zero amount") + return sdex.createModifySellOffer(nil, base, counter, price, amount, incrementalNativeAmountRaw) } // ParseOfferAmount is a convenience method to parse an offer amount @@ -187,6 +184,13 @@ func (sdex *SDEX) ComputeIncrementalNativeAmountRaw(isNewOffer bool) float64 { // createModifySellOffer is the main method that handles the logic of creating or modifying an offer, note that all offers are treated as sell offers in Stellar func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.Asset, buying horizon.Asset, price float64, amount float64, incrementalNativeAmountRaw float64) (*build.ManageOfferBuilder, error) { + if price <= 0 { + return nil, fmt.Errorf("error: cannot create or modify offer, invalid price: %.7f", price) + } + if amount <= 0 { + return nil, fmt.Errorf("error: cannot create or modify offer, invalid amount: %.7f", amount) + } + // check liability limits on the asset being sold incrementalSell := amount willOversell, e := sdex.willOversell(selling, amount) @@ -410,6 +414,12 @@ func (sdex *SDEX) LogAllLiabilities(assetBase horizon.Asset, assetQuote horizon. } } +// RecomputeAndLogCachedLiabilities clears the cached liabilities and recomputes from the network before logging +func (sdex *SDEX) RecomputeAndLogCachedLiabilities(assetBase horizon.Asset, assetQuote horizon.Asset) { + sdex.cachedLiabilities = map[horizon.Asset]Liabilities{} + sdex.LogAllLiabilities(assetBase, assetQuote) +} + // ResetCachedLiabilities resets the cache to include only the two assets passed in func (sdex *SDEX) ResetCachedLiabilities(assetBase horizon.Asset, assetQuote horizon.Asset) error { // re-compute the liabilities @@ -435,6 +445,24 @@ func (sdex *SDEX) ResetCachedLiabilities(assetBase horizon.Asset, assetQuote hor return nil } +// AvailableCapacity returns the buying and selling amounts available for a given asset +func (sdex *SDEX) AvailableCapacity(asset horizon.Asset) (*Liabilities, error) { + l, e := sdex.assetLiabilities(asset) + if e != nil { + return nil, e + } + + bal, trust, minAccountBal, e := sdex.assetBalance(asset) + if e != nil { + return nil, e + } + + return &Liabilities{ + Buying: trust - l.Buying, + Selling: bal - minAccountBal - l.Selling, + }, nil +} + // assetLiabilities returns the liabilities for the asset func (sdex *SDEX) assetLiabilities(asset horizon.Asset) (*Liabilities, error) { if v, ok := sdex.cachedLiabilities[asset]; ok { diff --git a/plugins/sellSideStrategy.go b/plugins/sellSideStrategy.go index 4e91bb48d..9a62f7186 100644 --- a/plugins/sellSideStrategy.go +++ b/plugins/sellSideStrategy.go @@ -1,6 +1,7 @@ package plugins import ( + "fmt" "log" "math" @@ -92,28 +93,48 @@ func (s *sellSideStrategy) PreUpdate(maxAssetBase float64, maxAssetQuote float64 func (s *sellSideStrategy) UpdateWithOps(offers []horizon.Offer) (ops []build.TransactionMutator, newTopOffer *model.Number, e error) { deleteOps := []build.TransactionMutator{} newTopOffer = nil + hitCapacityLimit := false for i := 0; i < len(s.currentLevels); i++ { - deleteIfNilOp, op, e := s.updateSellLevel(offers, i) + isModify := i < len(offers) + // we only want to delete offers after we hit the capacity limit which is why we perform this check in the beginning + if hitCapacityLimit { + if isModify { + delOp := s.sdex.DeleteOffer(offers[i]) + log.Printf("deleting offer because we previously hit the capacity limit, offerId=%d\n", offers[i].ID) + deleteOps = append(deleteOps, delOp) + continue + } else { + // we can break because we would never see a modify operation happen after a non-modify operation + break + } + } + + // hitCapacityLimit can be updated below + targetPrice := s.currentLevels[i].Price + targetAmount := s.currentLevels[i].Amount + if s.divideAmountByPrice { + targetAmount = *model.NumberFromFloat(targetAmount.AsFloat()/targetPrice.AsFloat(), targetAmount.Precision()) + } + targetAmount = *model.NumberFromFloat(math.Min(targetAmount.AsFloat(), s.maxAssetBase), targetAmount.Precision()) + + var offerPrice *model.Number + var op *build.ManageOfferBuilder + var e error + if isModify { + offerPrice, hitCapacityLimit, op, e = s.modifySellLevel(offers, i, targetPrice, targetAmount) + } else { + offerPrice, hitCapacityLimit, op, e = s.createSellLevel(targetPrice, targetAmount) + } if e != nil { return nil, nil, e } if op != nil { - offer, e := model.NumberFromString(op.MO.Price.String(), 7) - if e != nil { - return nil, nil, e - } - - // newTopOffer is minOffer because this is a sell strategy, and the lowest price is the best (top) price on the orderbook - if newTopOffer == nil || offer.AsFloat() < newTopOffer.AsFloat() { - newTopOffer = offer - } - ops = append(ops, op) - } else if op == nil && deleteIfNilOp { - // delete offer if nothing was created and we had an extra offer - delOp := s.sdex.DeleteOffer(offers[i]) - log.Printf("deleting offer because it is extra and we could not modify it, offerId=%d\n", offers[i].ID) - deleteOps = append(deleteOps, delOp) + } + + // update top offer, newTopOffer is minOffer because this is a sell strategy, and the lowest price is the best (top) price on the orderbook + if newTopOffer == nil || offerPrice.AsFloat() < newTopOffer.AsFloat() { + newTopOffer = offerPrice } } @@ -128,33 +149,70 @@ func (s *sellSideStrategy) PostUpdate() error { return nil } -// updateSellLevel returns true if we should delete the offer at this level when the manageOfferBuilder is nil -func (s *sellSideStrategy) updateSellLevel(offers []horizon.Offer, index int) (bool, *build.ManageOfferBuilder, error) { - targetPrice := s.currentLevels[index].Price - targetAmount := s.currentLevels[index].Amount - if s.divideAmountByPrice { - targetAmount = *model.NumberFromFloat(targetAmount.AsFloat()/targetPrice.AsFloat(), targetAmount.Precision()) +// computeRemainderAmount returns sellingAmount, buyingAmount, error +func (s *sellSideStrategy) computeRemainderAmount(incrementalSellAmount float64, incrementalBuyAmount float64, price float64, incrementalNativeAmountRaw float64) (float64, float64, error) { + availableSellingCapacity, e := s.sdex.AvailableCapacity(*s.assetBase) + if e != nil { + return 0, 0, e + } + availableBuyingCapacity, e := s.sdex.AvailableCapacity(*s.assetQuote) + if e != nil { + return 0, 0, e } - targetAmount = *model.NumberFromFloat(math.Min(targetAmount.AsFloat(), s.maxAssetBase), targetAmount.Precision()) - if len(offers) <= index { - if targetPrice.Precision() > utils.SdexPrecision { - targetPrice = *model.NumberFromFloat(targetPrice.AsFloat(), utils.SdexPrecision) - } - if targetAmount.Precision() > utils.SdexPrecision { - targetAmount = *model.NumberFromFloat(targetAmount.AsFloat(), utils.SdexPrecision) - } - // no existing offer at this index - log.Printf("sell,create,p=%.7f,a=%.7f\n", targetPrice.AsFloat(), targetAmount.AsFloat()) - incrementalNativeAmountRaw := s.sdex.ComputeIncrementalNativeAmountRaw(true) - op, e := s.sdex.CreateSellOffer(*s.assetBase, *s.assetQuote, targetPrice.AsFloat(), targetAmount.AsFloat(), incrementalNativeAmountRaw) - if e == nil && op != nil { - // update the cached liabilities if we create a valid operation to create an offer - s.sdex.AddLiabilities(*s.assetBase, *s.assetQuote, targetAmount.AsFloat(), targetAmount.AsFloat()*targetPrice.AsFloat(), incrementalNativeAmountRaw) - } - return false, op, e + // factor in cost of increase in minReserve and fee when calculating selling capacity of native asset + if *s.assetBase == utils.NativeAsset { + availableSellingCapacity.Selling -= incrementalNativeAmountRaw + } + + if availableSellingCapacity.Selling >= incrementalSellAmount && availableBuyingCapacity.Buying >= incrementalBuyAmount { + return 0, 0, fmt.Errorf("error: (programmer?) unable to create offer but available capacities were more than the attempted offer amounts, sellingCapacity=%.7f, incrementalSellAmount=%.7f, buyingCapacity=%.7f, incrementalBuyAmount=%.7f", + availableSellingCapacity.Selling, incrementalSellAmount, availableBuyingCapacity.Buying, incrementalBuyAmount) + } + + if availableSellingCapacity.Selling <= 0 || availableBuyingCapacity.Buying <= 0 { + log.Printf("computed remainder amount, no capacity available: availableSellingCapacity=%.7f, availableBuyingCapacity=%.7f\n", availableSellingCapacity.Selling, availableBuyingCapacity.Buying) + return 0, 0, nil + } + + // return the smaller amount between the buying and selling capacities that will max out either one + if availableSellingCapacity.Selling*price < availableBuyingCapacity.Buying { + sellingAmount := availableSellingCapacity.Selling + buyingAmount := availableSellingCapacity.Selling * price + log.Printf("computed remainder amount, constrained by selling capacity, returning sellingAmount=%.7f, buyingAmount=%.7f\n", sellingAmount, buyingAmount) + return sellingAmount, buyingAmount, nil + } else if availableBuyingCapacity.Buying/price < availableBuyingCapacity.Selling { + sellingAmount := availableBuyingCapacity.Buying / price + buyingAmount := availableBuyingCapacity.Buying + log.Printf("computed remainder amount, constrained by buying capacity, returning sellingAmount=%.7f, buyingAmount=%.7f\n", sellingAmount, buyingAmount) + return sellingAmount, buyingAmount, nil } + return 0, 0, fmt.Errorf("error: (programmer?) unable to constrain by either buying capacity or selling capacity, sellingCapacity=%.7f, buyingCapacity=%.7f, price=%.7f", + availableSellingCapacity.Selling, availableBuyingCapacity.Buying, price) +} + +// createSellLevel returns offerPrice, hitCapacityLimit, op, error. +func (s *sellSideStrategy) createSellLevel(targetPrice model.Number, targetAmount model.Number) (*model.Number, bool, *build.ManageOfferBuilder, error) { + incrementalNativeAmountRaw := s.sdex.ComputeIncrementalNativeAmountRaw(true) + targetPrice = *model.NumberByCappingPrecision(&targetPrice, utils.SdexPrecision) + targetAmount = *model.NumberByCappingPrecision(&targetAmount, utils.SdexPrecision) + hitCapacityLimit, op, e := s.placeOrderWithRetry( + targetPrice.AsFloat(), + targetAmount.AsFloat(), + incrementalNativeAmountRaw, + func(price float64, amount float64, incrementalNativeAmountRaw float64) (*build.ManageOfferBuilder, error) { + log.Printf("sell,create,p=%.7f,a=%.7f\n", price, amount) + return s.sdex.CreateSellOffer(*s.assetBase, *s.assetQuote, price, amount, incrementalNativeAmountRaw) + }, + *s.assetBase, + *s.assetQuote, + ) + return &targetPrice, hitCapacityLimit, op, e +} + +// modifySellLevel returns offerPrice, hitCapacityLimit, op, error. +func (s *sellSideStrategy) modifySellLevel(offers []horizon.Offer, index int, targetPrice model.Number, targetAmount model.Number) (*model.Number, bool, *build.ManageOfferBuilder, error) { highestPrice := targetPrice.AsFloat() + targetPrice.AsFloat()*s.priceTolerance lowestPrice := targetPrice.AsFloat() - targetPrice.AsFloat()*s.priceTolerance minAmount := targetAmount.AsFloat() - targetAmount.AsFloat()*s.amountTolerance @@ -168,23 +226,71 @@ func (s *sellSideStrategy) updateSellLevel(offers []horizon.Offer, index int) (b priceTrigger := (curPrice > highestPrice) || (curPrice < lowestPrice) amountTrigger := (curAmount < minAmount) || (curAmount > maxAmount) incrementalNativeAmountRaw := s.sdex.ComputeIncrementalNativeAmountRaw(false) - if priceTrigger || amountTrigger { - if targetPrice.Precision() > utils.SdexPrecision { - targetPrice = *model.NumberFromFloat(targetPrice.AsFloat(), utils.SdexPrecision) - } - if targetAmount.Precision() > utils.SdexPrecision { - targetAmount = *model.NumberFromFloat(targetAmount.AsFloat(), utils.SdexPrecision) - } - log.Printf("sell,modify,tp=%.7f,ta=%.7f,curPrice=%.7f,highPrice=%.7f,lowPrice=%.7f,curAmt=%.7f,minAmt=%.7f,maxAmt=%.7f\n", - targetPrice.AsFloat(), targetAmount.AsFloat(), curPrice, highestPrice, lowestPrice, curAmount, minAmount, maxAmount) - op, e := s.sdex.ModifySellOffer(offers[index], targetPrice.AsFloat(), targetAmount.AsFloat(), incrementalNativeAmountRaw) - if e == nil && op != nil { - // update the cached liabilities if we create a valid operation to modify the offer - s.sdex.AddLiabilities(offers[index].Selling, offers[index].Buying, targetAmount.AsFloat(), targetAmount.AsFloat()*targetPrice.AsFloat(), incrementalNativeAmountRaw) - } - return true, op, e + if !priceTrigger && !amountTrigger { + // always add back the current offer in the cached liabilities when we don't modify it + s.sdex.AddLiabilities(offers[index].Selling, offers[index].Buying, targetAmount.AsFloat(), targetAmount.AsFloat()*targetPrice.AsFloat(), incrementalNativeAmountRaw) + offerPrice := model.NumberFromFloat(curPrice, utils.SdexPrecision) + return offerPrice, false, nil, nil + } + + targetPrice = *model.NumberByCappingPrecision(&targetPrice, utils.SdexPrecision) + targetAmount = *model.NumberByCappingPrecision(&targetAmount, utils.SdexPrecision) + hitCapacityLimit, op, e := s.placeOrderWithRetry( + targetPrice.AsFloat(), + targetAmount.AsFloat(), + incrementalNativeAmountRaw, + func(price float64, amount float64, incrementalNativeAmountRaw float64) (*build.ManageOfferBuilder, error) { + log.Printf("sell,modify,tp=%.7f,ta=%.7f,curPrice=%.7f,highPrice=%.7f,lowPrice=%.7f,curAmt=%.7f,minAmt=%.7f,maxAmt=%.7f\n", + price, amount, curPrice, highestPrice, lowestPrice, curAmount, minAmount, maxAmount) + return s.sdex.ModifySellOffer(offers[index], price, amount, incrementalNativeAmountRaw) + }, + offers[index].Selling, + offers[index].Buying, + ) + return &targetPrice, hitCapacityLimit, op, e +} + +// placeOrderWithRetry returns hitCapacityLimit, op, error +func (s *sellSideStrategy) placeOrderWithRetry( + targetPrice float64, + targetAmount float64, + incrementalNativeAmountRaw float64, + placeOffer func(price float64, amount float64, incrementalNativeAmountRaw float64) (*build.ManageOfferBuilder, error), + assetBase horizon.Asset, + assetQuote horizon.Asset, +) (bool, *build.ManageOfferBuilder, error) { + op, e := placeOffer(targetPrice, targetAmount, incrementalNativeAmountRaw) + if e != nil { + return false, nil, e + } + incrementalSellAmount := targetAmount + incrementalBuyAmount := targetAmount * targetPrice + // op is nil only when we hit capacity limits + if op != nil { + // update the cached liabilities if we create a valid operation to create an offer + s.sdex.AddLiabilities(assetBase, assetQuote, incrementalSellAmount, incrementalBuyAmount, incrementalNativeAmountRaw) + return false, op, nil + } + + // place an order for the remainder between our intended amount and our remaining capacity + newSellingAmount, newBuyingAmount, e := s.computeRemainderAmount(incrementalSellAmount, incrementalBuyAmount, targetPrice, incrementalNativeAmountRaw) + if e != nil { + return true, nil, e + } + if newSellingAmount == 0 || newBuyingAmount == 0 { + return true, nil, nil + } + + op, e = placeOffer(targetPrice, newSellingAmount, incrementalNativeAmountRaw) + if e != nil { + return true, nil, e + } + + if op != nil { + // update the cached liabilities if we create a valid operation to create an offer + s.sdex.AddLiabilities(assetBase, assetQuote, newSellingAmount, newBuyingAmount, incrementalNativeAmountRaw) + return true, op, nil } - // always add back the current offer in the cached liabilities when we don't modify it - s.sdex.AddLiabilities(offers[index].Selling, offers[index].Buying, targetAmount.AsFloat(), targetAmount.AsFloat()*targetPrice.AsFloat(), incrementalNativeAmountRaw) - return false, nil, nil + return true, nil, fmt.Errorf("error: (programmer?) unable to place offer with the new (reduced) selling and buying amounts, oldSellingAmount=%.7f, newSellingAmount=%.7f, oldBuyingAmount=%.7f, newBuyingAmount=%.7f", + incrementalSellAmount, newSellingAmount, incrementalBuyAmount, newBuyingAmount) } diff --git a/trader/trader.go b/trader/trader.go index a2e883046..937cab1ad 100644 --- a/trader/trader.go +++ b/trader/trader.go @@ -71,6 +71,7 @@ func (t *Trader) Start() { // deletes all offers for the bot (not all offers on the account) func (t *Trader) deleteAllOffers() { + log.Printf("deleting all offers\n") dOps := []build.TransactionMutator{} dOps = append(dOps, t.sdex.DeleteAllOffers(t.sellingAOffers)...) @@ -130,6 +131,8 @@ func (t *Trader) update() { t.sdex.LogAllLiabilities(t.assetBase, t.assetQuote) if e != nil { log.Println(e) + log.Printf("liabilities (force recomputed) after encountering an error after a call to UpdateWithOps\n") + t.sdex.RecomputeAndLogCachedLiabilities(t.assetBase, t.assetQuote) t.deleteAllOffers() return } From 351d8bc7c78df471c886d696f84403f41b7f1a9d Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 27 Sep 2018 17:36:28 -0700 Subject: [PATCH 18/31] cap3: 18 - updated comment when resetting cached liabilities --- trader/trader.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/trader/trader.go b/trader/trader.go index 937cab1ad..13b63983d 100644 --- a/trader/trader.go +++ b/trader/trader.go @@ -116,8 +116,7 @@ func (t *Trader) update() { } } - // reset cached liabilities here so we only compute it once per update - // TODO 2 - calculate this here and pass it in + // reset and recompute cached liabilities for this update cycle e = t.sdex.ResetCachedLiabilities(t.assetBase, t.assetQuote) log.Printf("liabilities after resetting\n") t.sdex.LogAllLiabilities(t.assetBase, t.assetQuote) From 835f2562e64ec6969a405164f27b250312f699e0 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Fri, 28 Sep 2018 09:39:19 -0700 Subject: [PATCH 19/31] cap3: 19 - add current offer amounts to liabilities when not modifying an offer --- plugins/mirrorStrategy.go | 4 ++-- plugins/sdex.go | 10 ++++++++-- plugins/sellSideStrategy.go | 11 +++-------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/plugins/mirrorStrategy.go b/plugins/mirrorStrategy.go index db1ab618b..bc21e10d6 100644 --- a/plugins/mirrorStrategy.go +++ b/plugins/mirrorStrategy.go @@ -209,9 +209,9 @@ func (s *mirrorStrategy) doModifyOffer( if sameOrderParams { // update the cached liabilities if we keep the existing offer if hackPriceInvertForBuyOrderChangeCheck { - s.sdex.AddLiabilities(oldOffer.Selling, oldOffer.Buying, price*vol, vol, incrementalNativeAmountRaw) + s.sdex.AddLiabilities(oldOffer.Selling, oldOffer.Buying, oldVol.AsFloat()*oldPrice.AsFloat(), oldVol.AsFloat(), incrementalNativeAmountRaw) } else { - s.sdex.AddLiabilities(oldOffer.Selling, oldOffer.Buying, vol, price*vol, incrementalNativeAmountRaw) + s.sdex.AddLiabilities(oldOffer.Selling, oldOffer.Buying, oldVol.AsFloat(), oldVol.AsFloat()*oldPrice.AsFloat(), incrementalNativeAmountRaw) } return nil, nil, nil } diff --git a/plugins/sdex.go b/plugins/sdex.go index c401b62c3..be96c1703 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -446,7 +446,7 @@ func (sdex *SDEX) ResetCachedLiabilities(assetBase horizon.Asset, assetQuote hor } // AvailableCapacity returns the buying and selling amounts available for a given asset -func (sdex *SDEX) AvailableCapacity(asset horizon.Asset) (*Liabilities, error) { +func (sdex *SDEX) AvailableCapacity(asset horizon.Asset, incrementalNativeAmountRaw float64) (*Liabilities, error) { l, e := sdex.assetLiabilities(asset) if e != nil { return nil, e @@ -457,9 +457,15 @@ func (sdex *SDEX) AvailableCapacity(asset horizon.Asset) (*Liabilities, error) { return nil, e } + // factor in cost of increase in minReserve and fee when calculating selling capacity of native asset + incrementalSellingLiability := 0.0 + if asset == utils.NativeAsset { + incrementalSellingLiability = incrementalNativeAmountRaw + } + return &Liabilities{ Buying: trust - l.Buying, - Selling: bal - minAccountBal - l.Selling, + Selling: bal - minAccountBal - l.Selling - incrementalSellingLiability, }, nil } diff --git a/plugins/sellSideStrategy.go b/plugins/sellSideStrategy.go index 9a62f7186..e64ac1962 100644 --- a/plugins/sellSideStrategy.go +++ b/plugins/sellSideStrategy.go @@ -151,20 +151,15 @@ func (s *sellSideStrategy) PostUpdate() error { // computeRemainderAmount returns sellingAmount, buyingAmount, error func (s *sellSideStrategy) computeRemainderAmount(incrementalSellAmount float64, incrementalBuyAmount float64, price float64, incrementalNativeAmountRaw float64) (float64, float64, error) { - availableSellingCapacity, e := s.sdex.AvailableCapacity(*s.assetBase) + availableSellingCapacity, e := s.sdex.AvailableCapacity(*s.assetBase, incrementalNativeAmountRaw) if e != nil { return 0, 0, e } - availableBuyingCapacity, e := s.sdex.AvailableCapacity(*s.assetQuote) + availableBuyingCapacity, e := s.sdex.AvailableCapacity(*s.assetQuote, incrementalNativeAmountRaw) if e != nil { return 0, 0, e } - // factor in cost of increase in minReserve and fee when calculating selling capacity of native asset - if *s.assetBase == utils.NativeAsset { - availableSellingCapacity.Selling -= incrementalNativeAmountRaw - } - if availableSellingCapacity.Selling >= incrementalSellAmount && availableBuyingCapacity.Buying >= incrementalBuyAmount { return 0, 0, fmt.Errorf("error: (programmer?) unable to create offer but available capacities were more than the attempted offer amounts, sellingCapacity=%.7f, incrementalSellAmount=%.7f, buyingCapacity=%.7f, incrementalBuyAmount=%.7f", availableSellingCapacity.Selling, incrementalSellAmount, availableBuyingCapacity.Buying, incrementalBuyAmount) @@ -228,7 +223,7 @@ func (s *sellSideStrategy) modifySellLevel(offers []horizon.Offer, index int, ta incrementalNativeAmountRaw := s.sdex.ComputeIncrementalNativeAmountRaw(false) if !priceTrigger && !amountTrigger { // always add back the current offer in the cached liabilities when we don't modify it - s.sdex.AddLiabilities(offers[index].Selling, offers[index].Buying, targetAmount.AsFloat(), targetAmount.AsFloat()*targetPrice.AsFloat(), incrementalNativeAmountRaw) + s.sdex.AddLiabilities(offers[index].Selling, offers[index].Buying, curAmount, curAmount*curPrice, incrementalNativeAmountRaw) offerPrice := model.NumberFromFloat(curPrice, utils.SdexPrecision) return offerPrice, false, nil, nil } From 83d77ae3f1f321d53e08529c35a83cd60aa083e0 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Fri, 28 Sep 2018 08:23:46 -0700 Subject: [PATCH 20/31] cap3: 20 - add a caching layer for asset balances to reduce requests --- plugins/sdex.go | 40 +++++++++++++++++++++++++++++++++++++--- trader/trader.go | 4 ++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/plugins/sdex.go b/plugins/sdex.go index be96c1703..5ec84412d 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -32,6 +32,10 @@ type SDEX struct { reloadSeqNum bool // explicitly calculate liabilities here for now, we can switch over to using the values from Horizon once the protocol change has taken effect cachedLiabilities map[horizon.Asset]Liabilities + + // TODO 2 streamline requests instead of caching + // cache balances to avoid redundant requests + cachedBalances map[horizon.Asset]Balance } // Liabilities represents the "committed" units of an asset on both the buy and sell sides @@ -40,6 +44,13 @@ type Liabilities struct { Selling float64 // affects how much more can be sold } +// Balance repesents an asset's balance response from the assetBalance method below +type Balance struct { + Balance float64 + Trust float64 + Reserve float64 +} + // MakeSDEX is a factory method for SDEX func MakeSDEX( api *horizon.Client, @@ -141,8 +152,31 @@ func (sdex *SDEX) minReserve(subentries int32) float64 { return float64(2+subentries) * baseReserve } -// assetBalance returns asset balance, asset trust limit, reserve balance (zero for non-XLM), error +// ResetCachedBalances resets the cached balances map +func (sdex *SDEX) ResetCachedBalances() { + sdex.cachedBalances = map[horizon.Asset]Balance{} +} + +// assetBalance is a memoized version of _assetBalance func (sdex *SDEX) assetBalance(asset horizon.Asset) (float64, float64, float64, error) { + if v, ok := sdex.cachedBalances[asset]; ok { + return v.Balance, v.Trust, v.Reserve, nil + } + + b, t, r, e := sdex._assetBalance(asset) + if e == nil { + sdex.cachedBalances[asset] = Balance{ + Balance: b, + Trust: t, + Reserve: r, + } + } + + return b, t, r, e +} + +// assetBalance returns asset balance, asset trust limit, reserve balance (zero for non-XLM), error +func (sdex *SDEX) _assetBalance(asset horizon.Asset) (float64, float64, float64, error) { account, err := sdex.API.LoadAccount(sdex.TradingAccount) if err != nil { return -1, -1, -1, fmt.Errorf("error: unable to load account to fetch balance: %s", err) @@ -162,7 +196,7 @@ func (sdex *SDEX) assetBalance(asset horizon.Asset) (float64, float64, float64, if e != nil { return -1, -1, -1, fmt.Errorf("error: cannot parse trust limit: %s", e) } - return b, t, 0, e + return b, t, 0, nil } } return -1, -1, -1, errors.New("could not find a balance for the asset passed in") @@ -245,7 +279,7 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As return &result, nil } -// addLiabilities updates the cached liabilities, units are in their respective assets +// AddLiabilities updates the cached liabilities, units are in their respective assets func (sdex *SDEX) AddLiabilities(selling horizon.Asset, buying horizon.Asset, incrementalSell float64, incrementalBuy float64, incrementalNativeAmountRaw float64) { sdex.cachedLiabilities[selling] = Liabilities{ Selling: sdex.cachedLiabilities[selling].Selling + incrementalSell, diff --git a/trader/trader.go b/trader/trader.go index 13b63983d..f95f1ee44 100644 --- a/trader/trader.go +++ b/trader/trader.go @@ -95,6 +95,10 @@ func (t *Trader) update() { t.load() t.loadExistingOffers() + // TODO 2 streamline the request data instead of caching + // reset cache of balances for this update cycle to reduce redundant requests to calculate asset balances + t.sdex.ResetCachedBalances() + // strategy has a chance to set any state it needs e = t.strat.PreUpdate(t.maxAssetA, t.maxAssetB, t.trustAssetA, t.trustAssetB) if e != nil { From e793362790dd9dbc05d1ab45ddbcbdef1e047017 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Fri, 28 Sep 2018 08:06:26 -0700 Subject: [PATCH 21/31] cap3: 21 - cleaner logging of asset balance and trust amounts --- trader/trader.go | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/trader/trader.go b/trader/trader.go index f95f1ee44..58e63fd70 100644 --- a/trader/trader.go +++ b/trader/trader.go @@ -1,6 +1,7 @@ package trader import ( + "fmt" "log" "math" "sort" @@ -170,29 +171,33 @@ func (t *Trader) load() { var maxB float64 var trustA float64 var trustB float64 + var trustAString string + var trustBString string for _, balance := range account.Balances { + trust := maxLumenTrust + trustString := "math.MaxFloat64" + if balance.Asset.Type != utils.Native { + trust = utils.AmountStringAsFloat(balance.Limit) + trustString = fmt.Sprintf("%.7f", trust) + } + if utils.AssetsEqual(balance.Asset, t.assetBase) { maxA = utils.AmountStringAsFloat(balance.Balance) - if balance.Asset.Type == utils.Native { - trustA = maxLumenTrust - } else { - trustA = utils.AmountStringAsFloat(balance.Limit) - } - log.Printf("maxA=%.7f,trustA=%.7f\n", maxA, trustA) + trustA = trust + trustAString = trustString } else if utils.AssetsEqual(balance.Asset, t.assetQuote) { maxB = utils.AmountStringAsFloat(balance.Balance) - if balance.Asset.Type == utils.Native { - trustB = maxLumenTrust - } else { - trustB = utils.AmountStringAsFloat(balance.Limit) - } - log.Printf("maxB=%.7f,trustB=%.7f\n", maxB, trustB) + trustB = trust + trustBString = trustString } } t.maxAssetA = maxA t.maxAssetB = maxB t.trustAssetA = trustA t.trustAssetB = trustB + + log.Printf(" (base) assetA=%s, maxA=%.7f, trustA=%s\n", utils.Asset2String(t.assetBase), maxA, trustAString) + log.Printf("(quote) assetB=%s, maxB=%.7f, trustB=%s\n", utils.Asset2String(t.assetQuote), maxB, trustBString) } func (t *Trader) loadExistingOffers() { From d3d7a09a9f541007bdee071bfefc68b31e2cf707 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Fri, 28 Sep 2018 09:55:04 -0700 Subject: [PATCH 22/31] cap3: 22 - let new capacity constraint system handle max limits for sellSideStrategy --- plugins/sellSideStrategy.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/sellSideStrategy.go b/plugins/sellSideStrategy.go index e64ac1962..fd4c7521c 100644 --- a/plugins/sellSideStrategy.go +++ b/plugins/sellSideStrategy.go @@ -3,7 +3,6 @@ package plugins import ( "fmt" "log" - "math" "github.com/lightyeario/kelp/api" "github.com/lightyeario/kelp/model" @@ -115,7 +114,6 @@ func (s *sellSideStrategy) UpdateWithOps(offers []horizon.Offer) (ops []build.Tr if s.divideAmountByPrice { targetAmount = *model.NumberFromFloat(targetAmount.AsFloat()/targetPrice.AsFloat(), targetAmount.Precision()) } - targetAmount = *model.NumberFromFloat(math.Min(targetAmount.AsFloat(), s.maxAssetBase), targetAmount.Precision()) var offerPrice *model.Number var op *build.ManageOfferBuilder From 68c82f150e8ccfd8119420ce86b3a4b3977bfbe2 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Fri, 28 Sep 2018 10:58:59 -0700 Subject: [PATCH 23/31] cap3: 23 - reset caches before pruning and updating operations --- trader/trader.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/trader/trader.go b/trader/trader.go index 58e63fd70..e1ab1036d 100644 --- a/trader/trader.go +++ b/trader/trader.go @@ -99,6 +99,15 @@ func (t *Trader) update() { // TODO 2 streamline the request data instead of caching // reset cache of balances for this update cycle to reduce redundant requests to calculate asset balances t.sdex.ResetCachedBalances() + // reset and recompute cached liabilities for this update cycle + e = t.sdex.ResetCachedLiabilities(t.assetBase, t.assetQuote) + log.Printf("liabilities after resetting\n") + t.sdex.LogAllLiabilities(t.assetBase, t.assetQuote) + if e != nil { + log.Println(e) + t.deleteAllOffers() + return + } // strategy has a chance to set any state it needs e = t.strat.PreUpdate(t.maxAssetA, t.maxAssetB, t.trustAssetA, t.trustAssetB) @@ -121,6 +130,9 @@ func (t *Trader) update() { } } + // TODO 2 streamline the request data instead of caching + // reset cache of balances for this update cycle to reduce redundant requests to calculate asset balances + t.sdex.ResetCachedBalances() // reset and recompute cached liabilities for this update cycle e = t.sdex.ResetCachedLiabilities(t.assetBase, t.assetQuote) log.Printf("liabilities after resetting\n") @@ -130,6 +142,7 @@ func (t *Trader) update() { t.deleteAllOffers() return } + ops, e := t.strat.UpdateWithOps(t.buyingAOffers, t.sellingAOffers) log.Printf("liabilities at the end of a call to UpdateWithOps\n") t.sdex.LogAllLiabilities(t.assetBase, t.assetQuote) From d208c474bff248721ba98c7df5837755ee641148 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Fri, 28 Sep 2018 11:06:08 -0700 Subject: [PATCH 24/31] cap3: 24 - update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6860368b0..4dcbcfb84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - This CHANGELOG file +- support for [CAP-0003](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0003.md) introduced in stellar-core protocol v10 ([issue #2](https://github.com/lightyeario/kelp/issues/2)) ### Changed - Updated dependency `github.com/stellar/go` to latest version `5bbd27814a3ffca9aeffcbd75a09a6164959776a`, run `glide up` to update this dependency From 217534d4c3ca91f6caf2d8c8abd5808fcfc77255 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Fri, 28 Sep 2018 11:45:11 -0700 Subject: [PATCH 25/31] new release: v1.0.0-rc2 --- CHANGELOG.md | 21 ++++++++++++++++----- README.md | 20 ++++++++++---------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dcbcfb84..8ed7ab237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,25 +4,35 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + ## [Unreleased] ### Added -- This CHANGELOG file -- support for [CAP-0003](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0003.md) introduced in stellar-core protocol v10 ([issue #2](https://github.com/lightyeario/kelp/issues/2)) ### Changed -- Updated dependency `github.com/stellar/go` to latest version `5bbd27814a3ffca9aeffcbd75a09a6164959776a`, run `glide up` to update this dependency ### Deprecated ### Removed ### Fixed -- If `SOURCE_SECRET_SEED` is missing or empty then the bot will not crash now. ### Security +## [v1.0.0-rc2] - 2018-09-28 + +### Added +- This CHANGELOG file + +### Changed +- Updated dependency `github.com/stellar/go` to latest version `5bbd27814a3ffca9aeffcbd75a09a6164959776a`, run `glide up` to update this dependency + +### Fixed +- If `SOURCE_SECRET_SEED` is missing or empty then the bot will not crash now. +- support for [CAP-0003](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0003.md) introduced in stellar-core protocol v10 ([issue #2](https://github.com/lightyeario/kelp/issues/2)) + + ## v1.0.0-rc1 - 2018-08-13 ### Added @@ -32,4 +42,5 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Configuration file based approach to setting up a bot - Documentation on existing capabilities -[Unreleased]: https://github.com/lightyeario/kelp/compare/v1.0.0-rc1...HEAD +[Unreleased]: https://github.com/lightyeario/kelp/compare/v1.0.0-rc2...HEAD +[v1.0.0-rc2]: https://github.com/lightyeario/kelp/compare/v1.0.0-rc1...v1.0.0-rc2 diff --git a/README.md b/README.md index c8a533207..672433210 100644 --- a/README.md +++ b/README.md @@ -54,24 +54,24 @@ There is **one** binary associated with this project: `kelp`. Once the binary is You can find the pre-compiled binary for your platform from the [Github Releases Page][github-releases]. -Here is a list of binaries for the most recent release **v1.0.0-rc1**: +Here is a list of binaries for the most recent release **v1.0.0-rc2**: | Platform | Architecture | Binary File Name | | -------------- | ------------ | ---------------- | -| MacOS (Darwin) | 64-bit | [kelp-v1.0.0-rc1-darwin-amd64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc1/kelp-v1.0.0-rc1-darwin-amd64.tar) | -| Windows | 64-bit | [kelp-v1.0.0-rc1-windows-amd64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc1/kelp-v1.0.0-rc1-windows-amd64.tar) | -| Linux | 64-bit | [kelp-v1.0.0-rc1-linux-amd64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc1/kelp-v1.0.0-rc1-linux-amd64.tar) | -| Linux | 64-bit arm | [kelp-v1.0.0-rc1-linux-arm64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc1/kelp-v1.0.0-rc1-linux-arm64.tar) | -| Linux | 32-bit arm5 | [kelp-v1.0.0-rc1-linux-arm5.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc1/kelp-v1.0.0-rc1-linux-arm5.tar) | -| Linux | 32-bit arm6 | [kelp-v1.0.0-rc1-linux-arm6.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc1/kelp-v1.0.0-rc1-linux-arm6.tar) | -| Linux | 32-bit arm7 | [kelp-v1.0.0-rc1-linux-arm7.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc1/kelp-v1.0.0-rc1-linux-arm7.tar) | +| MacOS (Darwin) | 64-bit | [kelp-v1.0.0-rc2-darwin-amd64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc2/kelp-v1.0.0-rc2-darwin-amd64.tar) | +| Windows | 64-bit | [kelp-v1.0.0-rc2-windows-amd64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc2/kelp-v1.0.0-rc2-windows-amd64.tar) | +| Linux | 64-bit | [kelp-v1.0.0-rc2-linux-amd64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc2/kelp-v1.0.0-rc2-linux-amd64.tar) | +| Linux | 64-bit arm | [kelp-v1.0.0-rc2-linux-arm64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc2/kelp-v1.0.0-rc2-linux-arm64.tar) | +| Linux | 32-bit arm5 | [kelp-v1.0.0-rc2-linux-arm5.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc2/kelp-v1.0.0-rc2-linux-arm5.tar) | +| Linux | 32-bit arm6 | [kelp-v1.0.0-rc2-linux-arm6.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc2/kelp-v1.0.0-rc2-linux-arm6.tar) | +| Linux | 32-bit arm7 | [kelp-v1.0.0-rc2-linux-arm7.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc2/kelp-v1.0.0-rc2-linux-arm7.tar) | -After you _untar_ the downloaded file, change to the generated directory (`kelp-v1.0.0-rc1`) and invoke the `kelp` binary. +After you _untar_ the downloaded file, change to the generated directory (`kelp-v1.0.0-rc2`) and invoke the `kelp` binary. Here's an example to get you started (replace `filename` with the name of the file that you download): tar xvf filename - cd kelp-v1.0.0-rc1 + cd kelp-v1.0.0-rc2 ./kelp To run the bot in simulation mode, try this command: From 2767aa44b982d7a049cb30bc737dcb5fbfc436ff Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Sat, 29 Sep 2018 08:54:09 -0700 Subject: [PATCH 26/31] expand assets allowed to use with the Kraken exchange, fixes #13 --- CHANGELOG.md | 1 + model/assets.go | 105 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 88 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ed7ab237..b667de1f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added +- support for all currencies available on Kraken ### Changed diff --git a/model/assets.go b/model/assets.go index e60815ec1..9050fe9d3 100644 --- a/model/assets.go +++ b/model/assets.go @@ -11,12 +11,35 @@ type Asset string // this is the list of assets understood by the bot. // This string can be converted by the specific exchange adapter as is needed by the exchange's API const ( - XLM Asset = "XLM" - BTC Asset = "BTC" - USD Asset = "USD" - ETH Asset = "ETH" - LTC Asset = "LTC" - REP Asset = "REP" + XLM Asset = "XLM" + BTC Asset = "BTC" + USD Asset = "USD" + ETH Asset = "ETH" + LTC Asset = "LTC" + REP Asset = "REP" + ADA Asset = "ADA" + BCH Asset = "BCH" + DASH Asset = "DASH" + EOS Asset = "EOS" + GNO Asset = "GNO" + FEE Asset = "FEE" + QTUM Asset = "QTUM" + USDT Asset = "USDT" + DAO Asset = "DAO" + ETC Asset = "ETC" + ICN Asset = "ICN" + MLN Asset = "MLN" + NMC Asset = "NMC" + XDG Asset = "XDG" + XMR Asset = "XMR" + XRP Asset = "XRP" + XVN Asset = "XVN" + ZEC Asset = "ZEC" + CAD Asset = "CAD" + EUR Asset = "EUR" + GBP Asset = "GBP" + JPY Asset = "JPY" + KRW Asset = "KRW" ) // AssetConverter converts to and from the asset type, it is specific to an exchange @@ -67,20 +90,66 @@ func (c AssetConverter) MustFromString(s string) Asset { // Display is a basic converter for display purposes var Display = makeAssetConverter(map[Asset]string{ - XLM: string(XLM), - BTC: string(BTC), - USD: string(USD), - ETH: string(ETH), - LTC: string(LTC), - REP: string(REP), + XLM: string(XLM), + BTC: string(BTC), + USD: string(USD), + ETH: string(ETH), + LTC: string(LTC), + REP: string(REP), + ADA: string(ADA), + BCH: string(BCH), + DASH: string(DASH), + EOS: string(EOS), + GNO: string(GNO), + FEE: string(FEE), + QTUM: string(QTUM), + USDT: string(USDT), + DAO: string(DAO), + ETC: string(ETC), + ICN: string(ICN), + MLN: string(MLN), + NMC: string(NMC), + XDG: string(XDG), + XMR: string(XMR), + XRP: string(XRP), + XVN: string(XVN), + ZEC: string(ZEC), + CAD: string(CAD), + EUR: string(EUR), + GBP: string(GBP), + JPY: string(JPY), + KRW: string(KRW), }) // KrakenAssetConverter is the asset converter for the Kraken exchange var KrakenAssetConverter = makeAssetConverter(map[Asset]string{ - XLM: "XXLM", - BTC: "XXBT", - USD: "ZUSD", - ETH: "XETH", - LTC: "XLTC", - REP: "XREP", + XLM: "XXLM", + BTC: "XXBT", + USD: "ZUSD", + ETH: "XETH", + LTC: "XLTC", + REP: "XREP", + ADA: "ADA", + BCH: "BCH", + DASH: "DASH", + EOS: "EOS", + GNO: "GNO", + FEE: "KFEE", + QTUM: "QTUM", + USDT: "USDT", + DAO: "XDAO", + ETC: "XETC", + ICN: "XICN", + MLN: "XMLN", + NMC: "XNMC", + XDG: "XXDG", + XMR: "XXMR", + XRP: "XXRP", + XVN: "XXVN", + ZEC: "XZEC", + CAD: "ZCAD", + EUR: "ZEUR", + GBP: "ZGBP", + JPY: "ZJPY", + KRW: "ZKRW", }) From c8363e74c80cc3854dfbf19110f10b8ad21fe3d4 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Sat, 29 Sep 2018 08:56:58 -0700 Subject: [PATCH 27/31] new release: v1.0.0-rc3 --- CHANGELOG.md | 8 ++++++-- README.md | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b667de1f4..5876a5559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added -- support for all currencies available on Kraken ### Changed @@ -20,6 +19,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Security +## [v1.0.0-rc3] - 2018-09-29 + +### Added +- support for all currencies available on Kraken ## [v1.0.0-rc2] - 2018-09-28 @@ -43,5 +46,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Configuration file based approach to setting up a bot - Documentation on existing capabilities -[Unreleased]: https://github.com/lightyeario/kelp/compare/v1.0.0-rc2...HEAD +[Unreleased]: https://github.com/lightyeario/kelp/compare/v1.0.0-rc3...HEAD +[v1.0.0-rc3]: https://github.com/lightyeario/kelp/compare/v1.0.0-rc2...v1.0.0-rc3 [v1.0.0-rc2]: https://github.com/lightyeario/kelp/compare/v1.0.0-rc1...v1.0.0-rc2 diff --git a/README.md b/README.md index 672433210..76285a868 100644 --- a/README.md +++ b/README.md @@ -54,24 +54,24 @@ There is **one** binary associated with this project: `kelp`. Once the binary is You can find the pre-compiled binary for your platform from the [Github Releases Page][github-releases]. -Here is a list of binaries for the most recent release **v1.0.0-rc2**: +Here is a list of binaries for the most recent release **v1.0.0-rc3**: | Platform | Architecture | Binary File Name | | -------------- | ------------ | ---------------- | -| MacOS (Darwin) | 64-bit | [kelp-v1.0.0-rc2-darwin-amd64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc2/kelp-v1.0.0-rc2-darwin-amd64.tar) | -| Windows | 64-bit | [kelp-v1.0.0-rc2-windows-amd64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc2/kelp-v1.0.0-rc2-windows-amd64.tar) | -| Linux | 64-bit | [kelp-v1.0.0-rc2-linux-amd64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc2/kelp-v1.0.0-rc2-linux-amd64.tar) | -| Linux | 64-bit arm | [kelp-v1.0.0-rc2-linux-arm64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc2/kelp-v1.0.0-rc2-linux-arm64.tar) | -| Linux | 32-bit arm5 | [kelp-v1.0.0-rc2-linux-arm5.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc2/kelp-v1.0.0-rc2-linux-arm5.tar) | -| Linux | 32-bit arm6 | [kelp-v1.0.0-rc2-linux-arm6.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc2/kelp-v1.0.0-rc2-linux-arm6.tar) | -| Linux | 32-bit arm7 | [kelp-v1.0.0-rc2-linux-arm7.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc2/kelp-v1.0.0-rc2-linux-arm7.tar) | +| MacOS (Darwin) | 64-bit | [kelp-v1.0.0-rc3-darwin-amd64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc3/kelp-v1.0.0-rc3-darwin-amd64.tar) | +| Windows | 64-bit | [kelp-v1.0.0-rc3-windows-amd64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc3/kelp-v1.0.0-rc3-windows-amd64.tar) | +| Linux | 64-bit | [kelp-v1.0.0-rc3-linux-amd64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc3/kelp-v1.0.0-rc3-linux-amd64.tar) | +| Linux | 64-bit arm | [kelp-v1.0.0-rc3-linux-arm64.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc3/kelp-v1.0.0-rc3-linux-arm64.tar) | +| Linux | 32-bit arm5 | [kelp-v1.0.0-rc3-linux-arm5.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc3/kelp-v1.0.0-rc3-linux-arm5.tar) | +| Linux | 32-bit arm6 | [kelp-v1.0.0-rc3-linux-arm6.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc3/kelp-v1.0.0-rc3-linux-arm6.tar) | +| Linux | 32-bit arm7 | [kelp-v1.0.0-rc3-linux-arm7.tar](https://github.com/lightyeario/kelp/releases/download/v1.0.0-rc3/kelp-v1.0.0-rc3-linux-arm7.tar) | -After you _untar_ the downloaded file, change to the generated directory (`kelp-v1.0.0-rc2`) and invoke the `kelp` binary. +After you _untar_ the downloaded file, change to the generated directory (`kelp-v1.0.0-rc3`) and invoke the `kelp` binary. Here's an example to get you started (replace `filename` with the name of the file that you download): tar xvf filename - cd kelp-v1.0.0-rc2 + cd kelp-v1.0.0-rc3 ./kelp To run the bot in simulation mode, try this command: From 09ae14cf9f2579540858f4366f0aa9878171214e Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 1 Oct 2018 12:25:22 -0700 Subject: [PATCH 28/31] remove default rate offset percent from sample strategy config files --- examples/configs/trader/sample_buysell.cfg | 2 +- examples/configs/trader/sample_sell.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/configs/trader/sample_buysell.cfg b/examples/configs/trader/sample_buysell.cfg index aa3338725..79b23b94b 100644 --- a/examples/configs/trader/sample_buysell.cfg +++ b/examples/configs/trader/sample_buysell.cfg @@ -37,7 +37,7 @@ AMOUNT_TOLERANCE=0.001 # how much percent to offset your rates by, specified as a decimal (ex: 0.05 = 5%). Can be used in conjunction with RATE_OFFSET below. # A positive value indicates that your base asset (ASSET_A) has a higher rate than the rate received from your price feed # A negative value indicates that your base asset (ASSET_A) has a lower rate than the rate received from your price feed -RATE_OFFSET_PERCENT=0.05 +RATE_OFFSET_PERCENT=0.0 # how much to offset your rates by, specified in number of units of the quote asset (ASSET_B) as a decimal. # Can be used in conjunction with RATE_OFFSET_PERCENT above. # A positive value indicates that your base asset (ASSET_A) has a higher rate than the rate received from your price feed diff --git a/examples/configs/trader/sample_sell.cfg b/examples/configs/trader/sample_sell.cfg index d8f00d434..3f74d49e5 100644 --- a/examples/configs/trader/sample_sell.cfg +++ b/examples/configs/trader/sample_sell.cfg @@ -39,7 +39,7 @@ AMOUNT_TOLERANCE=0.001 # how much percent to offset your rates by, specified as a decimal (ex: 0.05 = 5%). Can be used in conjunction with RATE_OFFSET below. # A positive value indicates that your base asset (ASSET_A) has a higher rate than the rate received from your price feed # A negative value indicates that your base asset (ASSET_A) has a lower rate than the rate received from your price feed -RATE_OFFSET_PERCENT=0.05 +RATE_OFFSET_PERCENT=0.0 # how much to offset your rates by, specified in number of units of the quote asset (ASSET_B) as a decimal. # Can be used in conjunction with RATE_OFFSET_PERCENT above. # A positive value indicates that your base asset (ASSET_A) has a higher rate than the rate received from your price feed From f19c610f53432b2dba25a14db93ccbffa1afa38e Mon Sep 17 00:00:00 2001 From: Reid McCamish <43561569+Reidmcc@users.noreply.github.com> Date: Tue, 2 Oct 2018 10:35:33 -0500 Subject: [PATCH 29/31] Print strat (#15) * Update trade.go * Update trade.go * Update trade.go --- cmd/trade.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/trade.go b/cmd/trade.go index 4d12c337a..cf04f4cdc 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -68,6 +68,9 @@ func init() { } log.Printf("Trading %s:%s for %s:%s\n", botConfig.ASSET_CODE_A, botConfig.ISSUER_A, botConfig.ASSET_CODE_B, botConfig.ISSUER_B) + //Add current strategy to the log + log.Printf("Current strategy: %s\n", *strategy) + client := &horizon.Client{ URL: botConfig.HORIZON_URL, HTTP: http.DefaultClient, From 9c62c8039abe50340d6658c1c823d964884d920f Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Tue, 2 Oct 2018 10:15:58 -0700 Subject: [PATCH 30/31] enable Travis CI, closes #17 (#18) * enable Travis CI * go_import_path * travis: ./bin/kelp version --- .travis.yml | 17 +++++++++++++++++ README.md | 1 + 2 files changed, 18 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..21fef2837 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: go + +go_import_path: github.com/lightyeario/kelp + +go: +- "1.10.x" +- "1.11" + +before_install: +- curl https://glide.sh/get | sh + +install: +- glide install + +script: +- ./scripts/build.sh +- ./bin/kelp version diff --git a/README.md b/README.md index 76285a868..1c7f80b89 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Github All Releases](https://img.shields.io/github/downloads/lightyeario/kelp/total.svg?style=for-the-badge)][github-releases] [![license](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=for-the-badge&longCache=true)][license-apache] +[![Build Status](https://travis-ci.com/lightyeario/kelp.svg?branch=master)](https://travis-ci.com/lightyeario/kelp) [![GitHub issues](https://img.shields.io/github/issues/lightyeario/kelp.svg?style=flat-square&longCache=true)][github-issues] [![GitHub closed issues](https://img.shields.io/github/issues-closed/lightyeario/kelp.svg?style=flat-square&longCache=true)][github-issues-closed] [![GitHub pull requests](https://img.shields.io/github/issues-pr/lightyeario/kelp.svg?style=flat-square&longCache=true)][github-pulls] From 92324c368ea783e0e1ca859c5518c9dcffe0bf86 Mon Sep 17 00:00:00 2001 From: Reid McCamish <43561569+Reidmcc@users.noreply.github.com> Date: Tue, 2 Oct 2018 12:19:23 -0500 Subject: [PATCH 31/31] doc clarify full path needed for configs (#16) --- README.md | 6 +++--- cmd/trade.go | 4 ++-- examples/walkthroughs/trader/balanced.md | 2 +- examples/walkthroughs/trader/buysell.md | 2 +- examples/walkthroughs/trader/sell.md | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1c7f80b89..b9497fcce 100644 --- a/README.md +++ b/README.md @@ -108,13 +108,13 @@ These are the following commands available from the `kelp` binary: The `trade` command has three parameters which are: -- **botConf**: _.cfg_ file with the account details, [sample file here](examples/configs/trader/sample_trader.cfg). +- **botConf**: full path to the _.cfg_ file with the account details, [sample file here](examples/configs/trader/sample_trader.cfg). - **strategy**: the strategy you want to run (_sell_, _buysell_, _balanced_, _mirror_, _delete_). -- **stratConf**: _.cfg_ file specific to your chosen strategy, [sample files here](examples/configs/trader/). +- **stratConf**: full path to the _.cfg_ file specific to your chosen strategy, [sample files here](examples/configs/trader/). Here's an example of how to start the trading bot with the _buysell_ strategy: -`kelp trade --botConf trader.cfg --strategy buysell --stratConf buysell.cfg` +`kelp trade --botConf ./path/trader.cfg --strategy buysell --stratConf ./path/buysell.cfg` If you are ever stuck, just invoke the `kelp` binary directly or type `kelp help [command]` for help with a specific command. diff --git a/cmd/trade.go b/cmd/trade.go index cf04f4cdc..34d42fd0d 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -14,8 +14,8 @@ import ( "github.com/stellar/go/support/config" ) -const tradeExamples = ` kelp trade --botConf trader.cfg --strategy buysell --stratConf buysell.cfg - kelp trade --botConf trader.cfg --strategy buysell --stratConf buysell.cfg --sim` +const tradeExamples = ` kelp trade --botConf ./path/trader.cfg --strategy buysell --stratConf ./path/buysell.cfg + kelp trade --botConf ./path/trader.cfg --strategy buysell --stratConf ./path/buysell.cfg --sim` var tradeCmd = &cobra.Command{ Use: "trade", diff --git a/examples/walkthroughs/trader/balanced.md b/examples/walkthroughs/trader/balanced.md index 15cf3943f..0b85aea87 100644 --- a/examples/walkthroughs/trader/balanced.md +++ b/examples/walkthroughs/trader/balanced.md @@ -64,7 +64,7 @@ The virtual balance combined with the actual balance the bot has in its account Assuming your botConfig is called `trader.cfg` and your strategy config is called `balanced.cfg`, you can run `kelp` with the following command: ``` -kelp trade --botConf trader.cfg --strategy balanced --stratConf balanced.cfg +kelp trade --botConf ./path/trader.cfg --strategy balanced --stratConf ./path/balanced.cfg ``` # Above and Beyond diff --git a/examples/walkthroughs/trader/buysell.md b/examples/walkthroughs/trader/buysell.md index 8234dd443..1c48a385e 100644 --- a/examples/walkthroughs/trader/buysell.md +++ b/examples/walkthroughs/trader/buysell.md @@ -43,7 +43,7 @@ A level defines a [layer](https://en.wikipedia.org/wiki/Layering_(finance)) that Assuming your botConfig is called `trader.cfg` and your strategy config is called `buysell.cfg`, you can run `kelp` with the following command: ``` -kelp trade --botConf trader.cfg --strategy buysell --stratConf buysell.cfg +kelp trade --botConf ./path/trader.cfg --strategy buysell --stratConf ./path/buysell.cfg ``` # Above and Beyond diff --git a/examples/walkthroughs/trader/sell.md b/examples/walkthroughs/trader/sell.md index a052c3094..7244f959e 100644 --- a/examples/walkthroughs/trader/sell.md +++ b/examples/walkthroughs/trader/sell.md @@ -48,7 +48,7 @@ A level defines a [layer](https://en.wikipedia.org/wiki/Layering_(finance)) that Assuming your botConfig is called `trader.cfg` and your strategy config is called `sell.cfg`, you can run `kelp` with the following command: ``` -kelp trade --botConf trader.cfg --strategy sell --stratConf sell.cfg +kelp trade --botConf ./path/trader.cfg --strategy sell --stratConf ./path/sell.cfg ``` # Above and Beyond