From cfd0cbdd1dda7285527ac98a138ea21f69f51339 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 19 Dec 2019 16:55:12 +0530 Subject: [PATCH 01/25] 1 - inject submitFilters --- cmd/trade.go | 25 ++++++++++++++++++------- plugins/makerModeFilter.go | 13 +++++-------- trader/trader.go | 15 +++------------ 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/cmd/trade.go b/cmd/trade.go index 170d19359..83eda7f4d 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -344,30 +344,41 @@ func makeBot( // we want to delete all the offers and exit here since there is something wrong with our setup deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) } - dataKey := model.MakeSortedBotKey(botConfig.AssetBase(), botConfig.AssetQuote()) + + assetBase := botConfig.AssetBase() + assetQuote := botConfig.AssetQuote() + dataKey := model.MakeSortedBotKey(assetBase, assetQuote) alert, e := monitoring.MakeAlert(botConfig.AlertType, botConfig.AlertAPIKey) if e != nil { l.Infof("Unable to set up monitoring for alert type '%s' with the given API key\n", botConfig.AlertType) } - bot := trader.MakeBot( + + submitFilters := []plugins.SubmitFilter{ + plugins.MakeFilterOrderConstraints(exchangeShim.GetOrderConstraints(tradingPair), assetBase, assetQuote), + } + if submitMode == api.SubmitModeMakerOnly { + submitFilters = append(submitFilters, + plugins.MakeFilterMakerMode(exchangeShim, sdex, tradingPair), + ) + } + + return trader.MakeTrader( client, ieif, - botConfig.AssetBase(), - botConfig.AssetQuote(), - tradingPair, + assetBase, + assetQuote, botConfig.TradingAccount(), sdex, exchangeShim, strategy, timeController, botConfig.DeleteCyclesThreshold, - submitMode, + submitFilters, threadTracker, options.fixedIterations, dataKey, alert, ) - return bot } func convertDeprecatedBotConfigValues(l logger.Logger, botConfig trader.BotConfig) trader.BotConfig { diff --git a/plugins/makerModeFilter.go b/plugins/makerModeFilter.go index 188f8728e..2a5ebd1d8 100644 --- a/plugins/makerModeFilter.go +++ b/plugins/makerModeFilter.go @@ -19,15 +19,12 @@ type makerModeFilter struct { } // MakeFilterMakerMode makes a submit filter based on the passed in submitMode -func MakeFilterMakerMode(submitMode api.SubmitMode, exchangeShim api.ExchangeShim, sdex *SDEX, tradingPair *model.TradingPair) SubmitFilter { - if submitMode == api.SubmitModeMakerOnly { - return &makerModeFilter{ - tradingPair: tradingPair, - exchangeShim: exchangeShim, - sdex: sdex, - } +func MakeFilterMakerMode(exchangeShim api.ExchangeShim, sdex *SDEX, tradingPair *model.TradingPair) SubmitFilter { + return &makerModeFilter{ + tradingPair: tradingPair, + exchangeShim: exchangeShim, + sdex: sdex, } - return nil } var _ SubmitFilter = &makerModeFilter{} diff --git a/trader/trader.go b/trader/trader.go index b177c1048..1459f13d3 100644 --- a/trader/trader.go +++ b/trader/trader.go @@ -50,33 +50,24 @@ type Trader struct { sellingAOffers []hProtocol.Offer // quoted B/A } -// MakeBot is the factory method for the Trader struct -func MakeBot( +// MakeTrader is the factory method for the Trader struct +func MakeTrader( api *horizonclient.Client, ieif *plugins.IEIF, assetBase hProtocol.Asset, assetQuote hProtocol.Asset, - tradingPair *model.TradingPair, tradingAccount string, sdex *plugins.SDEX, exchangeShim api.ExchangeShim, strategy api.Strategy, timeController api.TimeController, deleteCyclesThreshold int64, - submitMode api.SubmitMode, + submitFilters []plugins.SubmitFilter, threadTracker *multithreading.ThreadTracker, fixedIterations *uint64, dataKey *model.BotKey, alert api.Alert, ) *Trader { - submitFilters := []plugins.SubmitFilter{ - plugins.MakeFilterOrderConstraints(exchangeShim.GetOrderConstraints(tradingPair), assetBase, assetQuote), - } - sdexSubmitFilter := plugins.MakeFilterMakerMode(submitMode, exchangeShim, sdex, tradingPair) - if sdexSubmitFilter != nil { - submitFilters = append(submitFilters, sdexSubmitFilter) - } - return &Trader{ api: api, ieif: ieif, From a45541c10994755273675f9302321a7ee788d642 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Fri, 20 Dec 2019 17:58:21 +0530 Subject: [PATCH 02/25] 2 - inject db instance --- cmd/trade.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/cmd/trade.go b/cmd/trade.go index 83eda7f4d..c0bb10da4 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -1,6 +1,7 @@ package cmd import ( + "database/sql" "fmt" "io" "log" @@ -329,6 +330,7 @@ func makeBot( exchangeShim api.ExchangeShim, ieif *plugins.IEIF, tradingPair *model.TradingPair, + db *sql.DB, strategy api.Strategy, threadTracker *multithreading.ThreadTracker, options inputs, @@ -445,6 +447,16 @@ func runTradeCmd(options inputs) { tradingPair.Base: botConfig.AssetBase(), tradingPair.Quote: botConfig.AssetQuote(), } + + var db *sql.DB + if botConfig.PostgresDbConfig != nil { + var e error + db, e = database.ConnectInitializedDatabase(botConfig.PostgresDbConfig) + if e != nil { + logger.Fatal(l, fmt.Errorf("problem encountered while initializing the db: %s", e)) + } + log.Printf("made db instance with config: %s\n", botConfig.PostgresDbConfig.MakeConnectString()) + } exchangeShim, sdex := makeExchangeShimSdex( l, botConfig, @@ -478,6 +490,7 @@ func runTradeCmd(options inputs) { exchangeShim, ieif, tradingPair, + db, strategy, threadTracker, options, @@ -508,6 +521,7 @@ func runTradeCmd(options inputs) { exchangeShim, tradingPair, sdexAssetMap, + db, threadTracker, ) startQueryServer( @@ -577,6 +591,7 @@ func startFillTracking( exchangeShim api.ExchangeShim, tradingPair *model.TradingPair, sdexAssetMap map[model.Asset]hProtocol.Asset, + db *sql.DB, threadTracker *multithreading.ThreadTracker, ) { strategyFillHandlers, e := strategy.GetFillHandlers() @@ -591,15 +606,7 @@ func startFillTracking( fillTracker := plugins.MakeFillTracker(tradingPair, threadTracker, exchangeShim, botConfig.FillTrackerSleepMillis, botConfig.FillTrackerDeleteCyclesThreshold) fillLogger := plugins.MakeFillLogger() fillTracker.RegisterHandler(fillLogger) - if botConfig.PostgresDbConfig != nil { - db, e := database.ConnectInitializedDatabase(botConfig.PostgresDbConfig) - if e != nil { - l.Info("") - l.Errorf("problem encountered while initializing the db: %s", e) - deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) - } - log.Printf("made db instance with config: %s\n", botConfig.PostgresDbConfig.MakeConnectString()) - + if db != nil { assetDisplayFn := model.MakePassthroughAssetDisplayFn() if botConfig.IsTradingSdex() { assetDisplayFn = model.MakeSdexMappedAssetDisplayFn(sdexAssetMap) From 57f83200f721244934c1b03d48b41e6b4f313553 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 23 Dec 2019 18:36:45 +0530 Subject: [PATCH 03/25] 3 - extract filtering infrastructure logic from makerModeFilter for reuse --- plugins/makerModeFilter.go | 77 +++++++++----------------------------- plugins/submitFilter.go | 47 +++++++++++++++++++++++ 2 files changed, 64 insertions(+), 60 deletions(-) diff --git a/plugins/makerModeFilter.go b/plugins/makerModeFilter.go index 2a5ebd1d8..c73017638 100644 --- a/plugins/makerModeFilter.go +++ b/plugins/makerModeFilter.go @@ -35,7 +35,23 @@ func (f *makerModeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProto return nil, fmt.Errorf("could not fetch orderbook: %s", e) } - ops, e = f.filterOps(ops, ob, sellingOffers, buyingOffers) + innerFn := func(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { + baseAsset, quoteAsset, e := f.sdex.Assets() + if e != nil { + return nil, false, fmt.Errorf("could not get assets: %s", e) + } + topBidPrice, e := f.topOrderPriceExcludingTrader(ob.Bids(), buyingOffers, false) + if e != nil { + return nil, false, fmt.Errorf("could not get topOrderPriceExcludingTrader for bids: %s", e) + } + topAskPrice, e := f.topOrderPriceExcludingTrader(ob.Asks(), sellingOffers, true) + if e != nil { + return nil, false, fmt.Errorf("could not get topOrderPriceExcludingTrader for asks: %s", e) + } + + return f.transformOfferMakerMode(baseAsset, quoteAsset, topBidPrice, topAskPrice, op) + } + ops, e = filterOps(ops, sellingOffers, buyingOffers, innerFn) if e != nil { return nil, fmt.Errorf("could not apply filter: %s", e) } @@ -106,65 +122,6 @@ func (f *makerModeFilter) topOrderPriceExcludingTrader(obSide []model.Order, tra return nil, nil } -func (f *makerModeFilter) filterOps( - ops []txnbuild.Operation, - ob *model.OrderBook, - sellingOffers []hProtocol.Offer, - buyingOffers []hProtocol.Offer, -) ([]txnbuild.Operation, error) { - baseAsset, quoteAsset, e := f.sdex.Assets() - if e != nil { - return nil, fmt.Errorf("could not get assets: %s", e) - } - - topBidPrice, e := f.topOrderPriceExcludingTrader(ob.Bids(), buyingOffers, false) - if e != nil { - return nil, fmt.Errorf("could not get topOrderPriceExcludingTrader for bids: %s", e) - } - topAskPrice, e := f.topOrderPriceExcludingTrader(ob.Asks(), sellingOffers, true) - if e != nil { - return nil, fmt.Errorf("could not get topOrderPriceExcludingTrader for asks: %s", e) - } - - numKeep := 0 - numDropped := 0 - numTransformed := 0 - filteredOps := []txnbuild.Operation{} - for _, op := range ops { - var newOp txnbuild.Operation - var keep bool - switch o := op.(type) { - case *txnbuild.ManageSellOffer: - newOp, keep, e = f.transformOfferMakerMode(baseAsset, quoteAsset, topBidPrice, topAskPrice, o) - if e != nil { - return nil, fmt.Errorf("could not transform offer (pointer case): %s", e) - } - default: - newOp = o - keep = true - } - - isNewOpNil := newOp == nil || fmt.Sprintf("%v", newOp) == "" - if keep { - if isNewOpNil { - return nil, fmt.Errorf("we want to keep op but newOp was nil (programmer error?)") - } - filteredOps = append(filteredOps, newOp) - numKeep++ - } else { - if !isNewOpNil { - // newOp can be a transformed op to change the op to an effectively "dropped" state - filteredOps = append(filteredOps, newOp) - numTransformed++ - } else { - numDropped++ - } - } - } - log.Printf("makerModeFilter: dropped %d, transformed %d, kept %d ops from original %d ops, len(filteredOps) = %d\n", numDropped, numTransformed, numKeep, len(ops), len(filteredOps)) - return filteredOps, nil -} - func (f *makerModeFilter) transformOfferMakerMode( baseAsset hProtocol.Asset, quoteAsset hProtocol.Asset, diff --git a/plugins/submitFilter.go b/plugins/submitFilter.go index e47269c4d..1f7f34da1 100644 --- a/plugins/submitFilter.go +++ b/plugins/submitFilter.go @@ -1,6 +1,9 @@ package plugins import ( + "fmt" + "log" + hProtocol "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/txnbuild" ) @@ -13,3 +16,47 @@ type SubmitFilter interface { buyingOffers []hProtocol.Offer, // quoted base/quote ) ([]txnbuild.Operation, error) } + +type filterFn func(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) + +func filterOps(ops []txnbuild.Operation, fn filterFn) ([]txnbuild.Operation, error) { + numKeep := 0 + numDropped := 0 + numTransformed := 0 + filteredOps := []txnbuild.Operation{} + for _, op := range ops { + var newOp txnbuild.Operation + var keep bool + switch o := op.(type) { + case *txnbuild.ManageSellOffer: + var e error + newOp, keep, e = fn(o) + if e != nil { + return nil, fmt.Errorf("could not transform offer (pointer case): %s", e) + } + default: + newOp = o + keep = true + } + + isNewOpNil := newOp == nil || fmt.Sprintf("%v", newOp) == "" + if keep { + if isNewOpNil { + return nil, fmt.Errorf("we want to keep op but newOp was nil (programmer error?)") + } + filteredOps = append(filteredOps, newOp) + numKeep++ + } else { + if !isNewOpNil { + // newOp can be a transformed op to change the op to an effectively "dropped" state + filteredOps = append(filteredOps, newOp) + numTransformed++ + } else { + numDropped++ + } + } + } + + log.Printf("filter result: dropped %d, transformed %d, kept %d ops from original %d ops, len(filteredOps) = %d\n", numDropped, numTransformed, numKeep, len(ops), len(filteredOps)) + return filteredOps, nil +} From 189c68d57ab0414cb5821fb2e2a790689fdf584d Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 23 Dec 2019 18:38:03 +0530 Subject: [PATCH 04/25] 4 - separate out TimestampFormatString and DateFormatString --- database/upgrade.go | 2 +- plugins/fillDBWriter.go | 2 +- plugins/makerModeFilter.go | 2 +- support/postgresdb/functions.go | 7 +++++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/database/upgrade.go b/database/upgrade.go index 489d4863c..606c8738b 100644 --- a/database/upgrade.go +++ b/database/upgrade.go @@ -104,7 +104,7 @@ func runUpgradeScripts(db *sql.DB, scripts []*UpgradeScript) error { // add entry to db_version table sqlInsertDbVersion := fmt.Sprintf(sqlDbVersionTableInsertTemplate, script.version, - startTime.Format(postgresdb.DateFormatString), + startTime.Format(postgresdb.TimestampFormatString), len(script.commands), elapsedMillis, ) diff --git a/plugins/fillDBWriter.go b/plugins/fillDBWriter.go index 73c64d6ae..b185d4192 100644 --- a/plugins/fillDBWriter.go +++ b/plugins/fillDBWriter.go @@ -156,7 +156,7 @@ func (f *FillDBWriter) HandleFill(trade model.Trade) error { txid := utils.CheckedString(trade.TransactionID) timeSeconds := trade.Timestamp.AsInt64() / 1000 date := time.Unix(timeSeconds, 0).UTC() - dateString := date.Format(postgresdb.DateFormatString) + dateString := date.Format(postgresdb.TimestampFormatString) market, e := f.fetchOrRegisterMarket(trade) if e != nil { diff --git a/plugins/makerModeFilter.go b/plugins/makerModeFilter.go index c73017638..9ba81b7d5 100644 --- a/plugins/makerModeFilter.go +++ b/plugins/makerModeFilter.go @@ -51,7 +51,7 @@ func (f *makerModeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProto return f.transformOfferMakerMode(baseAsset, quoteAsset, topBidPrice, topAskPrice, op) } - ops, e = filterOps(ops, sellingOffers, buyingOffers, innerFn) + ops, e = filterOps(ops, innerFn) if e != nil { return nil, fmt.Errorf("could not apply filter: %s", e) } diff --git a/support/postgresdb/functions.go b/support/postgresdb/functions.go index 4fce2e5ea..47ba57ec9 100644 --- a/support/postgresdb/functions.go +++ b/support/postgresdb/functions.go @@ -6,8 +6,11 @@ import ( "strings" ) -// DateFormatString is the format to be used when inserting dates in the database -const DateFormatString = "2006/01/02 15:04:05 MST" +// TimestampFormatString is the format to be used when inserting timestamps in the database +const TimestampFormatString = "2006/01/02 15:04:05 MST" + +// DateFormatString is the format to be used when converting a timestamp to a date +const DateFormatString = "2006/01/02" // CreateDatabaseIfNotExists returns whether the db was created and an error if creation failed func CreateDatabaseIfNotExists(postgresDbConfig *Config) (bool, error) { From 50fe00ea9f40f5158235374afdfa2ad30b2efffa Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 23 Dec 2019 20:20:41 +0530 Subject: [PATCH 05/25] 5 - first draft of volumeFilter.go (untested, many TODOs) --- database/schema.go | 4 + plugins/volumeFilter.go | 230 +++++++++++++++++++++++++++++++++++++ support/utils/functions.go | 13 +++ 3 files changed, 247 insertions(+) create mode 100644 plugins/volumeFilter.go diff --git a/database/schema.go b/database/schema.go index 0ec1d6375..9d05f9f22 100644 --- a/database/schema.go +++ b/database/schema.go @@ -16,6 +16,7 @@ const sqlTradesTableCreate = "CREATE TABLE IF NOT EXISTS trades (market_id TEXT /* indexes */ +// TODO need index on date only not full timestamp const sqlTradesIndexCreate = "CREATE INDEX IF NOT EXISTS date ON trades (market_id, date_utc)" /* @@ -39,6 +40,9 @@ const SqlQueryMarketsById = "SELECT market_id, exchange_name, base, quote FROM m // sqlQueryDbVersion queries the db_version table const sqlQueryDbVersion = "SELECT version FROM db_version ORDER BY version desc LIMIT 1" +// SqlQueryDailyValues queries the trades table to get the values for a given day +const SqlQueryDailyValues = "SELECT SUM(base_volume) as total_base_volume, SUM(counter_cost) as total_counter_volume FROM trades WHERE market_id = $1 AND DATE(date_utc) = $2 and action = $3 group by DATE(date_utc)" + /* query helper functions */ diff --git a/plugins/volumeFilter.go b/plugins/volumeFilter.go new file mode 100644 index 000000000..06e79e96c --- /dev/null +++ b/plugins/volumeFilter.go @@ -0,0 +1,230 @@ +package plugins + +import ( + "database/sql" + "fmt" + "log" + "strconv" + "time" + + hProtocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/txnbuild" + "github.com/stellar/kelp/database" + "github.com/stellar/kelp/support/postgresdb" + "github.com/stellar/kelp/support/utils" +) + +// volumeFilterConfig ensures that any one constraint that is hit will result in deleting all offers and pausing until limits are no longer constrained +type volumeFilterConfig struct { + sellBaseAssetCapInBaseUnits *float64 + sellBaseAssetCapInQuoteUnits *float64 + // buyBaseAssetCapInBaseUnits *float64 + // buyBaseAssetCapInQuoteUnits *float64 +} + +type volumeFilter struct { + baseAsset hProtocol.Asset + quoteAsset hProtocol.Asset + marketID string + config *volumeFilterConfig + db *sql.DB +} + +// MakeVolumeFilterConfig makes the config for the volume filter +func MakeVolumeFilterConfig( + sellBaseAssetCapInBaseUnits string, + sellBaseAssetCapInQuoteUnits string, + // buyBaseAssetCapInBaseUnits string, + // buyBaseAssetCapInQuoteUnits string, +) (*volumeFilterConfig, error) { + sellBaseBase, e := utils.ParseMaybeFloat(sellBaseAssetCapInBaseUnits) + if e != nil { + return nil, fmt.Errorf("could not parse sellBaseAssetCapInBaseUnits: %s", e) + } + sellBaseQuote, e := utils.ParseMaybeFloat(sellBaseAssetCapInQuoteUnits) + if e != nil { + return nil, fmt.Errorf("could not parse sellBaseAssetCapInQuoteUnits: %s", e) + } + // buyBaseBase, e := utils.ParseMaybeFloat(buyBaseAssetCapInBaseUnits) + // if e != nil { + // return nil, fmt.Errorf("could not parse buyBaseAssetCapInBaseUnits: %s", e) + // } + // buyBaseQuote, e := utils.ParseMaybeFloat(buyBaseAssetCapInQuoteUnits) + // if e != nil { + // return nil, fmt.Errorf("could not parse buyBaseAssetCapInQuoteUnits: %s", e) + // } + + return &volumeFilterConfig{ + sellBaseAssetCapInBaseUnits: sellBaseBase, + sellBaseAssetCapInQuoteUnits: sellBaseQuote, + // buyBaseAssetCapInBaseUnits: buyBaseBase, + // buyBaseAssetCapInQuoteUnits: buyBaseQuote, + }, nil +} + +// MakeFilterVolume makes a submit filter that limits orders placed based on the daily volume traded +func MakeFilterVolume( + baseAsset hProtocol.Asset, + quoteAsset hProtocol.Asset, + config *volumeFilterConfig, + db *sql.DB, +) (SubmitFilter, error) { + if db == nil { + return nil, fmt.Errorf("the provided db should be non-nil") + } + + // TODO fetch marketID for the baseAsset and quoteAsset market + marketID := "" + return &volumeFilter{ + baseAsset: baseAsset, + quoteAsset: quoteAsset, + marketID: marketID, + config: config, + db: db, + }, nil +} + +var _ SubmitFilter = &volumeFilter{} + +func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol.Offer, buyingOffers []hProtocol.Offer) ([]txnbuild.Operation, error) { + if f.config.isEmpty() { + log.Printf("the volumeFilterConfig was empty so not running through the volumeFilter\n") + return ops, nil + } + + dateString := time.Now().UTC().Format(postgresdb.DateFormatString) + // TODO do for buying base and also for a flipped marketID + dailyValuesBaseSold, e := f.dailyValuesByDate(f.marketID, dateString, "sell") + if e != nil { + return nil, fmt.Errorf("could not load dailyValuesByDate for today (%s): %s", dateString, e) + } + + log.Printf("dailyValuesByDate for today (%s): baseSoldUnits = %.8f %s, quoteCostUnits = %.8f %s (config = %+v)\n", + dateString, dailyValuesBaseSold.baseVol, f.baseAsset, dailyValuesBaseSold.quoteVol, f.quoteAsset, f.config) + + // daily on-the-books + dailyOTB := &volumeFilterConfig{ + sellBaseAssetCapInBaseUnits: &dailyValuesBaseSold.baseVol, + sellBaseAssetCapInQuoteUnits: &dailyValuesBaseSold.quoteVol, + } + // daily to-be-booked starts out as empty and accumulates the values of the operations + dailyTBB := &volumeFilterConfig{} + + innerFn := func(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { + return f.volumeFilterFn(dailyOTB, dailyTBB, op) + } + ops, e = filterOps(ops, innerFn) + if e != nil { + return nil, fmt.Errorf("could not apply filter: %s", e) + } + return ops, nil +} + +func (f *volumeFilter) volumeFilterFn(dailyOTB *volumeFilterConfig, dailyTBB *volumeFilterConfig, op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { + // delete operations should never be dropped + if op.Amount == "0" { + return op, true, nil + } + + isSell, e := utils.IsSelling(f.baseAsset, f.quoteAsset, op.Selling, op.Buying) + if e != nil { + return nil, false, fmt.Errorf("error when running the isSelling check: %s", e) + } + + sellPrice, e := strconv.ParseFloat(op.Price, 64) + if e != nil { + return nil, false, fmt.Errorf("could not convert price (%s) to float: %s", op.Price, e) + } + + amountValueUnitsBeingSold, e := strconv.ParseFloat(op.Amount, 64) + if e != nil { + return nil, false, fmt.Errorf("could not convert amount (%s) to float: %s", op.Amount, e) + } + amountValueUnitsBeingBought := amountValueUnitsBeingSold * sellPrice + + var keep bool + if isSell { + var keepSellingBase bool + var keepSellingQuote bool + if f.config.sellBaseAssetCapInBaseUnits != nil { + projectedSoldInBaseUnits := *dailyOTB.sellBaseAssetCapInBaseUnits + *dailyTBB.sellBaseAssetCapInBaseUnits + amountValueUnitsBeingSold + keepSellingBase := projectedSoldInBaseUnits < *f.config.sellBaseAssetCapInBaseUnits + log.Printf("volumeFilter: selling (base units), keep = (projectedSoldInBaseUnits) %.7f < %.7f (config.sellBaseAssetCapInBaseUnits): keepSellingBase = %v", projectedSoldInBaseUnits, *f.config.sellBaseAssetCapInBaseUnits, keepSellingBase) + } + + if f.config.sellBaseAssetCapInQuoteUnits != nil { + projectedSoldInQuoteUnits := *dailyOTB.sellBaseAssetCapInQuoteUnits + *dailyTBB.sellBaseAssetCapInQuoteUnits + amountValueUnitsBeingBought + keepSellingQuote = projectedSoldInQuoteUnits < *f.config.sellBaseAssetCapInQuoteUnits + log.Printf("volumeFilter: selling (quote units), keep = (projectedSoldInQuoteUnits) %.7f < %.7f (config.sellBaseAssetCapInQuoteUnits): keepSellingQuote = %v", projectedSoldInQuoteUnits, *f.config.sellBaseAssetCapInQuoteUnits, keepSellingQuote) + } + + keep = keepSellingBase && keepSellingQuote + } else { + // TODO buying side + } + + if keep { + // update the dailyTBB to include the additional amounts so they can be used in the calculation of the next operation + *dailyTBB.sellBaseAssetCapInBaseUnits += amountValueUnitsBeingSold + *dailyTBB.sellBaseAssetCapInQuoteUnits += amountValueUnitsBeingBought + return op, true, nil + } + + // TODO - reduce amount in offer so we can just meet the capacity limit, instead of dropping + // convert the offer to a dropped state + if op.OfferID == 0 { + // new offers can be dropped + return nil, false, nil + } else if op.Amount != "0" { + // modify offers should be converted to delete offers + opCopy := *op + opCopy.Amount = "0" + return &opCopy, false, nil + } + return nil, keep, fmt.Errorf("unable to transform manageOffer operation: offerID=%d, amount=%s, price=%.7f", op.OfferID, op.Amount, sellPrice) +} + +func (c *volumeFilterConfig) isEmpty() bool { + if c.sellBaseAssetCapInBaseUnits != nil { + return false + } + if c.sellBaseAssetCapInQuoteUnits != nil { + return false + } + // if buyBaseAssetCapInBaseUnits != nil { + // return false + // } + // if buyBaseAssetCapInQuoteUnits != nil { + // return false + // } + return true +} + +// dailyValues represents any volume value which can be either bought or sold depending on the query +type dailyValues struct { + baseVol float64 + quoteVol float64 +} + +func (f *volumeFilter) dailyValuesByDate(marketID string, dateUTC string, action string) (*dailyValues, error) { + row := f.db.QueryRow(database.SqlQueryDailyValues, marketID, dateUTC, action) + + var baseVol sql.NullFloat64 + var quoteVol sql.NullFloat64 + e := row.Scan(&baseVol, "eVol) + if e != nil { + return nil, fmt.Errorf("could not read data from SqlQueryDailyValues query: %s", e) + } + + if !baseVol.Valid { + return nil, fmt.Errorf("baseVol was invalid") + } + if !quoteVol.Valid { + return nil, fmt.Errorf("quoteVol was invalid") + } + + return &dailyValues{ + baseVol: baseVol.Float64, + quoteVol: quoteVol.Float64, + }, nil +} diff --git a/support/utils/functions.go b/support/utils/functions.go index 541f60ed1..c8bf1d0d9 100644 --- a/support/utils/functions.go +++ b/support/utils/functions.go @@ -381,3 +381,16 @@ func PrintErrorHintf(message string, args ...interface{}) { log.Printf("*************************************** /HINT ****************************************\n") log.Printf("\n") } + +// ParseMaybeFloat parses an optional string value as a float pointer +func ParseMaybeFloat(valueString string) (*float64, error) { + if valueString == "" { + return nil, nil + } + + valueFloat, e := strconv.ParseFloat(valueString, 64) + if e != nil { + return nil, fmt.Errorf("unable to parse value '%s' as float: %s", valueString, e) + } + return &valueFloat, nil +} From b7d662a9d632bb14582f130dbd829f02cbc48018 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Tue, 24 Dec 2019 14:16:14 +0530 Subject: [PATCH 06/25] 6 - fetch marketID in volumeFilter --- plugins/fillDBWriter.go | 9 ++++++--- plugins/volumeFilter.go | 19 ++++++++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/plugins/fillDBWriter.go b/plugins/fillDBWriter.go index b185d4192..1e27e2170 100644 --- a/plugins/fillDBWriter.go +++ b/plugins/fillDBWriter.go @@ -46,14 +46,17 @@ type FillDBWriter struct { market *tradingMarket } -// makeTradingMarket makes a market along with the ID field -func makeTradingMarket(exchangeName string, baseAsset string, quoteAsset string) *tradingMarket { +func makeMarketID(exchangeName string, baseAsset string, quoteAsset string) string { idString := fmt.Sprintf("%s_%s_%s", exchangeName, baseAsset, quoteAsset) h := sha256.New() h.Write([]byte(idString)) sha256Hash := fmt.Sprintf("%x", h.Sum(nil)) - sha256HashPrefix := sha256Hash[0:marketIdHashLength] + return sha256Hash[0:marketIdHashLength] +} +// makeTradingMarket makes a market along with the ID field +func makeTradingMarket(exchangeName string, baseAsset string, quoteAsset string) *tradingMarket { + sha256HashPrefix := makeMarketID(exchangeName, baseAsset, quoteAsset) return &tradingMarket{ ID: sha256HashPrefix, ExchangeName: exchangeName, diff --git a/plugins/volumeFilter.go b/plugins/volumeFilter.go index 06e79e96c..23bb13d19 100644 --- a/plugins/volumeFilter.go +++ b/plugins/volumeFilter.go @@ -10,6 +10,7 @@ import ( hProtocol "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/txnbuild" "github.com/stellar/kelp/database" + "github.com/stellar/kelp/model" "github.com/stellar/kelp/support/postgresdb" "github.com/stellar/kelp/support/utils" ) @@ -64,6 +65,9 @@ func MakeVolumeFilterConfig( // MakeFilterVolume makes a submit filter that limits orders placed based on the daily volume traded func MakeFilterVolume( + exchangeName string, + tradingPair *model.TradingPair, + assetDisplayFn model.AssetDisplayFn, baseAsset hProtocol.Asset, quoteAsset hProtocol.Asset, config *volumeFilterConfig, @@ -73,8 +77,17 @@ func MakeFilterVolume( return nil, fmt.Errorf("the provided db should be non-nil") } - // TODO fetch marketID for the baseAsset and quoteAsset market - marketID := "" + // use assetDisplayFn to make baseAssetString and quoteAssetString because it is issuer independent for non-sdex exchanges keeping a consistent marketID + baseAssetString, e := assetDisplayFn(tradingPair.Base) + if e != nil { + return nil, fmt.Errorf("could not convert base asset (%s) from trading pair via the passed in assetDisplayFn: %s", string(tradingPair.Base), e) + } + quoteAssetString, e := assetDisplayFn(tradingPair.Quote) + if e != nil { + return nil, fmt.Errorf("could not convert quote asset (%s) from trading pair via the passed in assetDisplayFn: %s", string(tradingPair.Quote), e) + } + marketID := makeMarketID(exchangeName, baseAssetString, quoteAssetString) + return &volumeFilter{ baseAsset: baseAsset, quoteAsset: quoteAsset, @@ -100,7 +113,7 @@ func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol } log.Printf("dailyValuesByDate for today (%s): baseSoldUnits = %.8f %s, quoteCostUnits = %.8f %s (config = %+v)\n", - dateString, dailyValuesBaseSold.baseVol, f.baseAsset, dailyValuesBaseSold.quoteVol, f.quoteAsset, f.config) + dateString, dailyValuesBaseSold.baseVol, utils.Asset2String(f.baseAsset), dailyValuesBaseSold.quoteVol, utils.Asset2String(f.quoteAsset), f.config) // daily on-the-books dailyOTB := &volumeFilterConfig{ From afbd65407a26bf44e94c14f9f43ba70e21642f8e Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Tue, 24 Dec 2019 17:26:40 +0530 Subject: [PATCH 07/25] 7 - better index for daily trades table query --- database/schema.go | 3 ++- database/upgrade.go | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/database/schema.go b/database/schema.go index 9d05f9f22..26507b8e1 100644 --- a/database/schema.go +++ b/database/schema.go @@ -16,8 +16,9 @@ const sqlTradesTableCreate = "CREATE TABLE IF NOT EXISTS trades (market_id TEXT /* indexes */ -// TODO need index on date only not full timestamp const sqlTradesIndexCreate = "CREATE INDEX IF NOT EXISTS date ON trades (market_id, date_utc)" +const sqlTradesIndexDrop = "DROP INDEX IF EXISTS date" +const sqlTradesIndexCreate2 = "CREATE INDEX IF NOT EXISTS trades_mdd ON trades (market_id, DATE(date_utc), date_utc)" /* insert statements diff --git a/database/upgrade.go b/database/upgrade.go index 606c8738b..9cb5c6eb5 100644 --- a/database/upgrade.go +++ b/database/upgrade.go @@ -18,6 +18,10 @@ var upgradeScripts = []*UpgradeScript{ sqlTradesTableCreate, sqlTradesIndexCreate, ), + makeUpgradeScript(3, + sqlTradesIndexDrop, + sqlTradesIndexCreate2, + ), } // UpgradeScript encapsulates a script to be run to upgrade the database from one version to the next From c15f9602042983a67c8c8bf8e6ab3f5e54f5c951 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Wed, 25 Dec 2019 17:44:16 +0530 Subject: [PATCH 08/25] 8 - take in volumeFilter config values --- cmd/trade.go | 40 +++++++++++++++--- plugins/volumeFilter.go | 91 ++++++++++++++--------------------------- trader/config.go | 34 +++++++-------- 3 files changed, 83 insertions(+), 82 deletions(-) diff --git a/cmd/trade.go b/cmd/trade.go index c0bb10da4..6af6a4e8b 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -332,6 +332,7 @@ func makeBot( tradingPair *model.TradingPair, db *sql.DB, strategy api.Strategy, + assetDisplayFn model.AssetDisplayFn, threadTracker *multithreading.ThreadTracker, options inputs, ) *trader.Trader { @@ -363,6 +364,32 @@ func makeBot( plugins.MakeFilterMakerMode(exchangeShim, sdex, tradingPair), ) } + if botConfig.VolumeFilterConfig != nil { + if e := botConfig.VolumeFilterConfig.Validate(); e != nil { + log.Println() + log.Println(e) + // we want to delete all the offers and exit here since there is something wrong with our setup + deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) + } + + volFilter, e := plugins.MakeFilterVolume( + botConfig.TradingExchangeName(), + tradingPair, + assetDisplayFn, + assetBase, + assetQuote, + botConfig.VolumeFilterConfig, + db, + ) + if e != nil { + log.Println() + log.Println(e) + // we want to delete all the offers and exit here since there is something wrong with our setup + deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) + } + + submitFilters = append(submitFilters, volFilter) + } return trader.MakeTrader( client, @@ -447,6 +474,10 @@ func runTradeCmd(options inputs) { tradingPair.Base: botConfig.AssetBase(), tradingPair.Quote: botConfig.AssetQuote(), } + assetDisplayFn := model.MakePassthroughAssetDisplayFn() + if botConfig.IsTradingSdex() { + assetDisplayFn = model.MakeSdexMappedAssetDisplayFn(sdexAssetMap) + } var db *sql.DB if botConfig.PostgresDbConfig != nil { @@ -492,6 +523,7 @@ func runTradeCmd(options inputs) { tradingPair, db, strategy, + assetDisplayFn, threadTracker, options, ) @@ -520,7 +552,7 @@ func runTradeCmd(options inputs) { sdex, exchangeShim, tradingPair, - sdexAssetMap, + assetDisplayFn, db, threadTracker, ) @@ -590,7 +622,7 @@ func startFillTracking( sdex *plugins.SDEX, exchangeShim api.ExchangeShim, tradingPair *model.TradingPair, - sdexAssetMap map[model.Asset]hProtocol.Asset, + assetDisplayFn model.AssetDisplayFn, db *sql.DB, threadTracker *multithreading.ThreadTracker, ) { @@ -607,10 +639,6 @@ func startFillTracking( fillLogger := plugins.MakeFillLogger() fillTracker.RegisterHandler(fillLogger) if db != nil { - assetDisplayFn := model.MakePassthroughAssetDisplayFn() - if botConfig.IsTradingSdex() { - assetDisplayFn = model.MakeSdexMappedAssetDisplayFn(sdexAssetMap) - } fillDBWriter := plugins.MakeFillDBWriter(db, assetDisplayFn, botConfig.TradingExchangeName()) fillTracker.RegisterHandler(fillDBWriter) } diff --git a/plugins/volumeFilter.go b/plugins/volumeFilter.go index 23bb13d19..ebedaaf00 100644 --- a/plugins/volumeFilter.go +++ b/plugins/volumeFilter.go @@ -15,10 +15,10 @@ import ( "github.com/stellar/kelp/support/utils" ) -// volumeFilterConfig ensures that any one constraint that is hit will result in deleting all offers and pausing until limits are no longer constrained -type volumeFilterConfig struct { - sellBaseAssetCapInBaseUnits *float64 - sellBaseAssetCapInQuoteUnits *float64 +// VolumeFilterConfig ensures that any one constraint that is hit will result in deleting all offers and pausing until limits are no longer constrained +type VolumeFilterConfig struct { + SellBaseAssetCapInBaseUnits *float64 `valid:"-" toml:"SELL_BASE_ASSET_CAP_IN_BASE_UNITS" json:"sell_base_asset_cap_in_base_units"` + SellBaseAssetCapInQuoteUnits *float64 `valid:"-" toml:"SELL_BASE_ASSET_CAP_IN_QUOTE_UNITS" json:"sell_base_asset_cap_in_quote_units"` // buyBaseAssetCapInBaseUnits *float64 // buyBaseAssetCapInQuoteUnits *float64 } @@ -27,42 +27,10 @@ type volumeFilter struct { baseAsset hProtocol.Asset quoteAsset hProtocol.Asset marketID string - config *volumeFilterConfig + config *VolumeFilterConfig db *sql.DB } -// MakeVolumeFilterConfig makes the config for the volume filter -func MakeVolumeFilterConfig( - sellBaseAssetCapInBaseUnits string, - sellBaseAssetCapInQuoteUnits string, - // buyBaseAssetCapInBaseUnits string, - // buyBaseAssetCapInQuoteUnits string, -) (*volumeFilterConfig, error) { - sellBaseBase, e := utils.ParseMaybeFloat(sellBaseAssetCapInBaseUnits) - if e != nil { - return nil, fmt.Errorf("could not parse sellBaseAssetCapInBaseUnits: %s", e) - } - sellBaseQuote, e := utils.ParseMaybeFloat(sellBaseAssetCapInQuoteUnits) - if e != nil { - return nil, fmt.Errorf("could not parse sellBaseAssetCapInQuoteUnits: %s", e) - } - // buyBaseBase, e := utils.ParseMaybeFloat(buyBaseAssetCapInBaseUnits) - // if e != nil { - // return nil, fmt.Errorf("could not parse buyBaseAssetCapInBaseUnits: %s", e) - // } - // buyBaseQuote, e := utils.ParseMaybeFloat(buyBaseAssetCapInQuoteUnits) - // if e != nil { - // return nil, fmt.Errorf("could not parse buyBaseAssetCapInQuoteUnits: %s", e) - // } - - return &volumeFilterConfig{ - sellBaseAssetCapInBaseUnits: sellBaseBase, - sellBaseAssetCapInQuoteUnits: sellBaseQuote, - // buyBaseAssetCapInBaseUnits: buyBaseBase, - // buyBaseAssetCapInQuoteUnits: buyBaseQuote, - }, nil -} - // MakeFilterVolume makes a submit filter that limits orders placed based on the daily volume traded func MakeFilterVolume( exchangeName string, @@ -70,7 +38,7 @@ func MakeFilterVolume( assetDisplayFn model.AssetDisplayFn, baseAsset hProtocol.Asset, quoteAsset hProtocol.Asset, - config *volumeFilterConfig, + config *VolumeFilterConfig, db *sql.DB, ) (SubmitFilter, error) { if db == nil { @@ -99,12 +67,15 @@ func MakeFilterVolume( var _ SubmitFilter = &volumeFilter{} -func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol.Offer, buyingOffers []hProtocol.Offer) ([]txnbuild.Operation, error) { - if f.config.isEmpty() { - log.Printf("the volumeFilterConfig was empty so not running through the volumeFilter\n") - return ops, nil +// Validate ensures validity +func (c *VolumeFilterConfig) Validate() error { + if c.isEmpty() { + return fmt.Errorf("the volumeFilterConfig was empty\n") } + return nil +} +func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol.Offer, buyingOffers []hProtocol.Offer) ([]txnbuild.Operation, error) { dateString := time.Now().UTC().Format(postgresdb.DateFormatString) // TODO do for buying base and also for a flipped marketID dailyValuesBaseSold, e := f.dailyValuesByDate(f.marketID, dateString, "sell") @@ -116,12 +87,12 @@ func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol dateString, dailyValuesBaseSold.baseVol, utils.Asset2String(f.baseAsset), dailyValuesBaseSold.quoteVol, utils.Asset2String(f.quoteAsset), f.config) // daily on-the-books - dailyOTB := &volumeFilterConfig{ - sellBaseAssetCapInBaseUnits: &dailyValuesBaseSold.baseVol, - sellBaseAssetCapInQuoteUnits: &dailyValuesBaseSold.quoteVol, + dailyOTB := &VolumeFilterConfig{ + SellBaseAssetCapInBaseUnits: &dailyValuesBaseSold.baseVol, + SellBaseAssetCapInQuoteUnits: &dailyValuesBaseSold.quoteVol, } // daily to-be-booked starts out as empty and accumulates the values of the operations - dailyTBB := &volumeFilterConfig{} + dailyTBB := &VolumeFilterConfig{} innerFn := func(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { return f.volumeFilterFn(dailyOTB, dailyTBB, op) @@ -133,7 +104,7 @@ func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol return ops, nil } -func (f *volumeFilter) volumeFilterFn(dailyOTB *volumeFilterConfig, dailyTBB *volumeFilterConfig, op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { +func (f *volumeFilter) volumeFilterFn(dailyOTB *VolumeFilterConfig, dailyTBB *VolumeFilterConfig, op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { // delete operations should never be dropped if op.Amount == "0" { return op, true, nil @@ -159,16 +130,16 @@ func (f *volumeFilter) volumeFilterFn(dailyOTB *volumeFilterConfig, dailyTBB *vo if isSell { var keepSellingBase bool var keepSellingQuote bool - if f.config.sellBaseAssetCapInBaseUnits != nil { - projectedSoldInBaseUnits := *dailyOTB.sellBaseAssetCapInBaseUnits + *dailyTBB.sellBaseAssetCapInBaseUnits + amountValueUnitsBeingSold - keepSellingBase := projectedSoldInBaseUnits < *f.config.sellBaseAssetCapInBaseUnits - log.Printf("volumeFilter: selling (base units), keep = (projectedSoldInBaseUnits) %.7f < %.7f (config.sellBaseAssetCapInBaseUnits): keepSellingBase = %v", projectedSoldInBaseUnits, *f.config.sellBaseAssetCapInBaseUnits, keepSellingBase) + if f.config.SellBaseAssetCapInBaseUnits != nil { + projectedSoldInBaseUnits := *dailyOTB.SellBaseAssetCapInBaseUnits + *dailyTBB.SellBaseAssetCapInBaseUnits + amountValueUnitsBeingSold + keepSellingBase := projectedSoldInBaseUnits < *f.config.SellBaseAssetCapInBaseUnits + log.Printf("volumeFilter: selling (base units), keep = (projectedSoldInBaseUnits) %.7f < %.7f (config.SellBaseAssetCapInBaseUnits): keepSellingBase = %v", projectedSoldInBaseUnits, *f.config.SellBaseAssetCapInBaseUnits, keepSellingBase) } - if f.config.sellBaseAssetCapInQuoteUnits != nil { - projectedSoldInQuoteUnits := *dailyOTB.sellBaseAssetCapInQuoteUnits + *dailyTBB.sellBaseAssetCapInQuoteUnits + amountValueUnitsBeingBought - keepSellingQuote = projectedSoldInQuoteUnits < *f.config.sellBaseAssetCapInQuoteUnits - log.Printf("volumeFilter: selling (quote units), keep = (projectedSoldInQuoteUnits) %.7f < %.7f (config.sellBaseAssetCapInQuoteUnits): keepSellingQuote = %v", projectedSoldInQuoteUnits, *f.config.sellBaseAssetCapInQuoteUnits, keepSellingQuote) + if f.config.SellBaseAssetCapInQuoteUnits != nil { + projectedSoldInQuoteUnits := *dailyOTB.SellBaseAssetCapInQuoteUnits + *dailyTBB.SellBaseAssetCapInQuoteUnits + amountValueUnitsBeingBought + keepSellingQuote = projectedSoldInQuoteUnits < *f.config.SellBaseAssetCapInQuoteUnits + log.Printf("volumeFilter: selling (quote units), keep = (projectedSoldInQuoteUnits) %.7f < %.7f (config.SellBaseAssetCapInQuoteUnits): keepSellingQuote = %v", projectedSoldInQuoteUnits, *f.config.SellBaseAssetCapInQuoteUnits, keepSellingQuote) } keep = keepSellingBase && keepSellingQuote @@ -178,8 +149,8 @@ func (f *volumeFilter) volumeFilterFn(dailyOTB *volumeFilterConfig, dailyTBB *vo if keep { // update the dailyTBB to include the additional amounts so they can be used in the calculation of the next operation - *dailyTBB.sellBaseAssetCapInBaseUnits += amountValueUnitsBeingSold - *dailyTBB.sellBaseAssetCapInQuoteUnits += amountValueUnitsBeingBought + *dailyTBB.SellBaseAssetCapInBaseUnits += amountValueUnitsBeingSold + *dailyTBB.SellBaseAssetCapInQuoteUnits += amountValueUnitsBeingBought return op, true, nil } @@ -197,11 +168,11 @@ func (f *volumeFilter) volumeFilterFn(dailyOTB *volumeFilterConfig, dailyTBB *vo return nil, keep, fmt.Errorf("unable to transform manageOffer operation: offerID=%d, amount=%s, price=%.7f", op.OfferID, op.Amount, sellPrice) } -func (c *volumeFilterConfig) isEmpty() bool { - if c.sellBaseAssetCapInBaseUnits != nil { +func (c *VolumeFilterConfig) isEmpty() bool { + if c.SellBaseAssetCapInBaseUnits != nil { return false } - if c.sellBaseAssetCapInQuoteUnits != nil { + if c.SellBaseAssetCapInQuoteUnits != nil { return false } // if buyBaseAssetCapInBaseUnits != nil { diff --git a/trader/config.go b/trader/config.go index dfeb432be..e5aec6bb6 100644 --- a/trader/config.go +++ b/trader/config.go @@ -4,6 +4,7 @@ import ( "fmt" hProtocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/kelp/plugins" "github.com/stellar/kelp/support/postgresdb" "github.com/stellar/kelp/support/toml" "github.com/stellar/kelp/support/utils" @@ -39,22 +40,23 @@ type BotConfig struct { CentralizedPricePrecisionOverride *int8 `valid:"-" toml:"CENTRALIZED_PRICE_PRECISION_OVERRIDE" json:"centralized_price_precision_override"` CentralizedVolumePrecisionOverride *int8 `valid:"-" toml:"CENTRALIZED_VOLUME_PRECISION_OVERRIDE" json:"centralized_volume_precision_override"` // Deprecated: use CENTRALIZED_MIN_BASE_VOLUME_OVERRIDE instead - MinCentralizedBaseVolumeDeprecated *float64 `valid:"-" toml:"MIN_CENTRALIZED_BASE_VOLUME" deprecated:"true" json:"min_centralized_base_volume"` - CentralizedMinBaseVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_BASE_VOLUME_OVERRIDE" json:"centralized_min_base_volume_override"` - CentralizedMinQuoteVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_QUOTE_VOLUME_OVERRIDE" json:"centralized_min_quote_volume_override"` - PostgresDbConfig *postgresdb.Config `valid:"-" toml:"POSTGRES_DB"` - AlertType string `valid:"-" toml:"ALERT_TYPE" json:"alert_type"` - AlertAPIKey string `valid:"-" toml:"ALERT_API_KEY" json:"alert_api_key"` - MonitoringPort uint16 `valid:"-" toml:"MONITORING_PORT" json:"monitoring_port"` - MonitoringTLSCert string `valid:"-" toml:"MONITORING_TLS_CERT" json:"monitoring_tls_cert"` - MonitoringTLSKey string `valid:"-" toml:"MONITORING_TLS_KEY" json:"monitoring_tls_key"` - GoogleClientID string `valid:"-" toml:"GOOGLE_CLIENT_ID" json:"google_client_id"` - GoogleClientSecret string `valid:"-" toml:"GOOGLE_CLIENT_SECRET" json:"google_client_secret"` - AcceptableEmails string `valid:"-" toml:"ACCEPTABLE_GOOGLE_EMAILS" json:"acceptable_google_emails"` - TradingExchange string `valid:"-" toml:"TRADING_EXCHANGE" json:"trading_exchange"` - ExchangeAPIKeys toml.ExchangeAPIKeysToml `valid:"-" toml:"EXCHANGE_API_KEYS" json:"exchange_api_keys"` - ExchangeParams toml.ExchangeParamsToml `valid:"-" toml:"EXCHANGE_PARAMS" json:"exchange_params"` - ExchangeHeaders toml.ExchangeHeadersToml `valid:"-" toml:"EXCHANGE_HEADERS" json:"exchange_headers"` + MinCentralizedBaseVolumeDeprecated *float64 `valid:"-" toml:"MIN_CENTRALIZED_BASE_VOLUME" deprecated:"true" json:"min_centralized_base_volume"` + CentralizedMinBaseVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_BASE_VOLUME_OVERRIDE" json:"centralized_min_base_volume_override"` + CentralizedMinQuoteVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_QUOTE_VOLUME_OVERRIDE" json:"centralized_min_quote_volume_override"` + PostgresDbConfig *postgresdb.Config `valid:"-" toml:"POSTGRES_DB" json:"postgres_db"` + VolumeFilterConfig *plugins.VolumeFilterConfig `valid:"-" toml:"VOLUME_FILTER" json:"volume_filter"` + AlertType string `valid:"-" toml:"ALERT_TYPE" json:"alert_type"` + AlertAPIKey string `valid:"-" toml:"ALERT_API_KEY" json:"alert_api_key"` + MonitoringPort uint16 `valid:"-" toml:"MONITORING_PORT" json:"monitoring_port"` + MonitoringTLSCert string `valid:"-" toml:"MONITORING_TLS_CERT" json:"monitoring_tls_cert"` + MonitoringTLSKey string `valid:"-" toml:"MONITORING_TLS_KEY" json:"monitoring_tls_key"` + GoogleClientID string `valid:"-" toml:"GOOGLE_CLIENT_ID" json:"google_client_id"` + GoogleClientSecret string `valid:"-" toml:"GOOGLE_CLIENT_SECRET" json:"google_client_secret"` + AcceptableEmails string `valid:"-" toml:"ACCEPTABLE_GOOGLE_EMAILS" json:"acceptable_google_emails"` + TradingExchange string `valid:"-" toml:"TRADING_EXCHANGE" json:"trading_exchange"` + ExchangeAPIKeys toml.ExchangeAPIKeysToml `valid:"-" toml:"EXCHANGE_API_KEYS" json:"exchange_api_keys"` + ExchangeParams toml.ExchangeParamsToml `valid:"-" toml:"EXCHANGE_PARAMS" json:"exchange_params"` + ExchangeHeaders toml.ExchangeHeadersToml `valid:"-" toml:"EXCHANGE_HEADERS" json:"exchange_headers"` // initialized later tradingAccount *string From a8cc49463b2dc7d92841eec4351e9382362b0563 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Wed, 25 Dec 2019 19:36:57 +0530 Subject: [PATCH 09/25] 9 - update sample_trader.cfg to include sample for volumeFilterConfig --- examples/configs/trader/sample_trader.cfg | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples/configs/trader/sample_trader.cfg b/examples/configs/trader/sample_trader.cfg index 946183e33..e777163ff 100644 --- a/examples/configs/trader/sample_trader.cfg +++ b/examples/configs/trader/sample_trader.cfg @@ -108,6 +108,13 @@ MAX_OP_FEE_STROOPS=5000 #PASSWORD="" #SSL_ENABLE=false +# uncomment to limit daily trades based on daily trading volume denominated in either the base or quote asset, or both +#[VOLUME_FILTER] +# limits the amount of the base asset that is sold, denominated in units of the base asset +#SELL_BASE_ASSET_CAP_IN_BASE_UNITS=100000.0 +# limits the amount of the base asset that is sold, denominated in units of the quote asset +#SELL_BASE_ASSET_CAP_IN_QUOTE_UNITS=100000.0 + # uncomment lines below to use kraken. Can use "sdex" or leave out to trade on the Stellar Decentralized Exchange. # can alternatively use any of the ccxt-exchanges marked as "Trading" (run `kelp exchanges` for full list) #TRADING_EXCHANGE="kraken" From 46d44391e2b439ef0769626c7c27839aa59089db Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Wed, 25 Dec 2019 19:37:29 +0530 Subject: [PATCH 10/25] 10 - StructString displays values in structs in a cleaner way + automatic pointer unwrapping --- plugins/balancedStrategy.go | 2 +- plugins/buysellStrategy.go | 2 +- plugins/mirrorStrategy.go | 13 ++++------ plugins/sellStrategy.go | 2 +- support/utils/configs.go | 48 +++++++++++++++++++------------------ terminator/config.go | 2 +- trader/config.go | 25 ++++++++----------- 7 files changed, 43 insertions(+), 51 deletions(-) diff --git a/plugins/balancedStrategy.go b/plugins/balancedStrategy.go index e312fbb2f..f2b42e3f5 100644 --- a/plugins/balancedStrategy.go +++ b/plugins/balancedStrategy.go @@ -26,7 +26,7 @@ type balancedConfig struct { // String impl. func (c balancedConfig) String() string { - return utils.StructString(c, nil) + return utils.StructString(c, 0, nil) } // makeBalancedStrategy is a factory method for balancedStrategy diff --git a/plugins/buysellStrategy.go b/plugins/buysellStrategy.go index bd7769c82..8e7068836 100644 --- a/plugins/buysellStrategy.go +++ b/plugins/buysellStrategy.go @@ -55,7 +55,7 @@ func MakeBuysellConfig( // String impl. func (c BuySellConfig) String() string { - return utils.StructString(c, nil) + return utils.StructString(c, 0, nil) } // makeBuySellStrategy is a factory method diff --git a/plugins/mirrorStrategy.go b/plugins/mirrorStrategy.go index cd05af6c4..4928c1064 100644 --- a/plugins/mirrorStrategy.go +++ b/plugins/mirrorStrategy.go @@ -36,15 +36,10 @@ type mirrorConfig struct { // String impl. func (c mirrorConfig) String() string { - return utils.StructString(c, map[string]func(interface{}) interface{}{ - "EXCHANGE_API_KEYS": utils.Hide, - "EXCHANGE_PARAMS": utils.Hide, - "EXCHANGE_HEADERS": utils.Hide, - "PRICE_PRECISION_OVERRIDE": utils.UnwrapInt8Pointer, - "VOLUME_PRECISION_OVERRIDE": utils.UnwrapInt8Pointer, - "MIN_BASE_VOLUME": utils.UnwrapFloat64Pointer, - "MIN_BASE_VOLUME_OVERRIDE": utils.UnwrapFloat64Pointer, - "MIN_QUOTE_VOLUME_OVERRIDE": utils.UnwrapFloat64Pointer, + return utils.StructString(c, 0, map[string]func(interface{}) interface{}{ + "EXCHANGE_API_KEYS": utils.Hide, + "EXCHANGE_PARAMS": utils.Hide, + "EXCHANGE_HEADERS": utils.Hide, }) } diff --git a/plugins/sellStrategy.go b/plugins/sellStrategy.go index 240c231fa..2a3ac851c 100644 --- a/plugins/sellStrategy.go +++ b/plugins/sellStrategy.go @@ -27,7 +27,7 @@ type sellConfig struct { // String impl. func (c sellConfig) String() string { - return utils.StructString(c, nil) + return utils.StructString(c, 0, nil) } // makeSellStrategy is a factory method for SellStrategy diff --git a/support/utils/configs.go b/support/utils/configs.go index 667ca305a..5ec084b54 100644 --- a/support/utils/configs.go +++ b/support/utils/configs.go @@ -25,8 +25,9 @@ func LogConfig(cfg fmt.Stringer) { } } -// StructString is a helper method that -func StructString(s interface{}, transforms map[string]func(interface{}) interface{}) string { +// StructString is a helper method that serizlies configs; the transform keys are always flattened, +// i.e specify the key meant to be on an inner object at a top level key on the transform map +func StructString(s interface{}, indentLevel uint8, transforms map[string]func(interface{}) interface{}) string { var buf bytes.Buffer numFields := reflect.TypeOf(s).NumField() for i := 0; i < numFields; i++ { @@ -46,13 +47,32 @@ func StructString(s interface{}, transforms map[string]func(interface{}) interfa if reflect.ValueOf(s).Field(i).CanInterface() { if !isDeprecated || !reflect.ValueOf(s).Field(i).IsNil() { - value := reflect.ValueOf(s).Field(i).Interface() - transformedValue := transformFn(value) deprecatedWarning := "" if isDeprecated { deprecatedWarning = " (deprecated)" } - buf.WriteString(fmt.Sprintf("%s: %+v%s\n", fieldDisplayName, transformedValue, deprecatedWarning)) + + currentField := reflect.ValueOf(s).Field(i) + value := currentField.Interface() + kind := currentField.Kind() + if kind == reflect.Ptr { + derefField := reflect.Indirect(currentField) + if !currentField.IsZero() { + value = derefField.Interface() + kind = derefField.Kind() + } + } + + for indentIdx := 0; indentIdx < int(indentLevel); indentIdx++ { + buf.WriteString(" ") + } + if kind == reflect.Struct { + subString := StructString(value, indentLevel+1, transforms) + buf.WriteString(fmt.Sprintf("%s: %s\n%s", fieldDisplayName, deprecatedWarning, subString)) + } else { + transformedValue := transformFn(value) + buf.WriteString(fmt.Sprintf("%s: %+v%s\n", fieldDisplayName, transformedValue, deprecatedWarning)) + } } } } @@ -86,21 +106,3 @@ func passthrough(i interface{}) interface{} { func Hide(i interface{}) interface{} { return "" } - -// UnwrapFloat64Pointer unwraps a float64 pointer -func UnwrapFloat64Pointer(i interface{}) interface{} { - p := i.(*float64) - if p == nil { - return "" - } - return *p -} - -// UnwrapInt8Pointer unwraps a int8 pointer -func UnwrapInt8Pointer(i interface{}) interface{} { - p := i.(*int8) - if p == nil { - return "" - } - return *p -} diff --git a/terminator/config.go b/terminator/config.go index 054e6b403..f43c5852d 100644 --- a/terminator/config.go +++ b/terminator/config.go @@ -20,7 +20,7 @@ type Config struct { // String impl. func (c Config) String() string { - return utils.StructString(c, map[string]func(interface{}) interface{}{ + return utils.StructString(c, 0, map[string]func(interface{}) interface{}{ "SOURCE_SECRET_SEED": utils.SecretKey2PublicKey, "TRADING_SECRET_SEED": utils.SecretKey2PublicKey, }) diff --git a/trader/config.go b/trader/config.go index e5aec6bb6..0133c6445 100644 --- a/trader/config.go +++ b/trader/config.go @@ -113,21 +113,16 @@ func MakeBotConfig( // String impl. func (b BotConfig) String() string { - return utils.StructString(b, map[string]func(interface{}) interface{}{ - "EXCHANGE_API_KEYS": utils.Hide, - "EXCHANGE_PARAMS": utils.Hide, - "EXCHANGE_HEADERS": utils.Hide, - "SOURCE_SECRET_SEED": utils.SecretKey2PublicKey, - "TRADING_SECRET_SEED": utils.SecretKey2PublicKey, - "ALERT_API_KEY": utils.Hide, - "GOOGLE_CLIENT_ID": utils.Hide, - "GOOGLE_CLIENT_SECRET": utils.Hide, - "ACCEPTABLE_GOOGLE_EMAILS": utils.Hide, - "CENTRALIZED_PRICE_PRECISION_OVERRIDE": utils.UnwrapInt8Pointer, - "CENTRALIZED_VOLUME_PRECISION_OVERRIDE": utils.UnwrapInt8Pointer, - "MIN_CENTRALIZED_BASE_VOLUME": utils.UnwrapFloat64Pointer, - "CENTRALIZED_MIN_BASE_VOLUME_OVERRIDE": utils.UnwrapFloat64Pointer, - "CENTRALIZED_MIN_QUOTE_VOLUME_OVERRIDE": utils.UnwrapFloat64Pointer, + return utils.StructString(b, 0, map[string]func(interface{}) interface{}{ + "EXCHANGE_API_KEYS": utils.Hide, + "EXCHANGE_PARAMS": utils.Hide, + "EXCHANGE_HEADERS": utils.Hide, + "SOURCE_SECRET_SEED": utils.SecretKey2PublicKey, + "TRADING_SECRET_SEED": utils.SecretKey2PublicKey, + "ALERT_API_KEY": utils.Hide, + "GOOGLE_CLIENT_ID": utils.Hide, + "GOOGLE_CLIENT_SECRET": utils.Hide, + "ACCEPTABLE_GOOGLE_EMAILS": utils.Hide, }) } From 2cd2a90350402c09b1b04d83405ce49edba712bb Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Wed, 25 Dec 2019 20:02:58 +0530 Subject: [PATCH 11/25] 11 - treat values as 0 when there are no recorded trades in the db for the day --- plugins/volumeFilter.go | 15 ++++++++++++++- support/utils/functions.go | 8 ++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/plugins/volumeFilter.go b/plugins/volumeFilter.go index ebedaaf00..f8f44fb74 100644 --- a/plugins/volumeFilter.go +++ b/plugins/volumeFilter.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "strconv" + "strings" "time" hProtocol "github.com/stellar/go/protocols/horizon" @@ -75,6 +76,12 @@ func (c *VolumeFilterConfig) Validate() error { return nil } +// String is the stringer method +func (c *VolumeFilterConfig) String() string { + return fmt.Sprintf("VolumeFilterConfig[SellBaseAssetCapInBaseUnits=%s, SellBaseAssetCapInQuoteUnits=%s]", + utils.CheckedFloatPtr(c.SellBaseAssetCapInBaseUnits), utils.CheckedFloatPtr(c.SellBaseAssetCapInQuoteUnits)) +} + func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol.Offer, buyingOffers []hProtocol.Offer) ([]txnbuild.Operation, error) { dateString := time.Now().UTC().Format(postgresdb.DateFormatString) // TODO do for buying base and also for a flipped marketID @@ -83,7 +90,7 @@ func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol return nil, fmt.Errorf("could not load dailyValuesByDate for today (%s): %s", dateString, e) } - log.Printf("dailyValuesByDate for today (%s): baseSoldUnits = %.8f %s, quoteCostUnits = %.8f %s (config = %+v)\n", + log.Printf("dailyValuesByDate for today (%s): baseSoldUnits = %.8f %s, quoteCostUnits = %.8f %s (%s)\n", dateString, dailyValuesBaseSold.baseVol, utils.Asset2String(f.baseAsset), dailyValuesBaseSold.quoteVol, utils.Asset2String(f.quoteAsset), f.config) // daily on-the-books @@ -197,6 +204,12 @@ func (f *volumeFilter) dailyValuesByDate(marketID string, dateUTC string, action var quoteVol sql.NullFloat64 e := row.Scan(&baseVol, "eVol) if e != nil { + if strings.Contains(e.Error(), "no rows in result set") { + return &dailyValues{ + baseVol: 0, + quoteVol: 0, + }, nil + } return nil, fmt.Errorf("could not read data from SqlQueryDailyValues query: %s", e) } diff --git a/support/utils/functions.go b/support/utils/functions.go index c8bf1d0d9..84d7e9557 100644 --- a/support/utils/functions.go +++ b/support/utils/functions.go @@ -251,6 +251,14 @@ func CheckedString(v interface{}) string { return fmt.Sprintf("%v", v) } +// CheckedFloatPtr returns "" if the object is nil, otherwise calls the String() function on the object +func CheckedFloatPtr(v *float64) string { + if v == nil { + return "" + } + return fmt.Sprintf("%.10f", *v) +} + // ParseAsset returns a horizon asset a string func ParseAsset(code string, issuer string) (*hProtocol.Asset, error) { if code != "XLM" && issuer == "" { From 3c2810add692380a8e52456dbb79c0a390d0aecb Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Wed, 25 Dec 2019 20:17:54 +0530 Subject: [PATCH 12/25] 12 - fix initialization of dailyTBB to avoid a null pointer dereference --- plugins/volumeFilter.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/volumeFilter.go b/plugins/volumeFilter.go index f8f44fb74..6a7328d42 100644 --- a/plugins/volumeFilter.go +++ b/plugins/volumeFilter.go @@ -99,7 +99,12 @@ func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol SellBaseAssetCapInQuoteUnits: &dailyValuesBaseSold.quoteVol, } // daily to-be-booked starts out as empty and accumulates the values of the operations - dailyTBB := &VolumeFilterConfig{} + dailyTbbSellBase := 0.0 + dailyTbbSellQuote := 0.0 + dailyTBB := &VolumeFilterConfig{ + SellBaseAssetCapInBaseUnits: &dailyTbbSellBase, + SellBaseAssetCapInQuoteUnits: &dailyTbbSellQuote, + } innerFn := func(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { return f.volumeFilterFn(dailyOTB, dailyTBB, op) From 1d493137e60be5610af8cc7d77340de1c42f0886 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Wed, 25 Dec 2019 20:29:50 +0530 Subject: [PATCH 13/25] 13 - fix bug caused by variable shadowing in volumeFilter.go --- plugins/volumeFilter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/volumeFilter.go b/plugins/volumeFilter.go index 6a7328d42..d5f4d9d49 100644 --- a/plugins/volumeFilter.go +++ b/plugins/volumeFilter.go @@ -144,7 +144,7 @@ func (f *volumeFilter) volumeFilterFn(dailyOTB *VolumeFilterConfig, dailyTBB *Vo var keepSellingQuote bool if f.config.SellBaseAssetCapInBaseUnits != nil { projectedSoldInBaseUnits := *dailyOTB.SellBaseAssetCapInBaseUnits + *dailyTBB.SellBaseAssetCapInBaseUnits + amountValueUnitsBeingSold - keepSellingBase := projectedSoldInBaseUnits < *f.config.SellBaseAssetCapInBaseUnits + keepSellingBase = projectedSoldInBaseUnits < *f.config.SellBaseAssetCapInBaseUnits log.Printf("volumeFilter: selling (base units), keep = (projectedSoldInBaseUnits) %.7f < %.7f (config.SellBaseAssetCapInBaseUnits): keepSellingBase = %v", projectedSoldInBaseUnits, *f.config.SellBaseAssetCapInBaseUnits, keepSellingBase) } From 524b7df8662d279256fea322e867ca343b71938c Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Wed, 25 Dec 2019 20:48:10 +0530 Subject: [PATCH 14/25] 14 - limit check should be lte, not lt check --- plugins/volumeFilter.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/volumeFilter.go b/plugins/volumeFilter.go index d5f4d9d49..9ba72d9d5 100644 --- a/plugins/volumeFilter.go +++ b/plugins/volumeFilter.go @@ -144,14 +144,14 @@ func (f *volumeFilter) volumeFilterFn(dailyOTB *VolumeFilterConfig, dailyTBB *Vo var keepSellingQuote bool if f.config.SellBaseAssetCapInBaseUnits != nil { projectedSoldInBaseUnits := *dailyOTB.SellBaseAssetCapInBaseUnits + *dailyTBB.SellBaseAssetCapInBaseUnits + amountValueUnitsBeingSold - keepSellingBase = projectedSoldInBaseUnits < *f.config.SellBaseAssetCapInBaseUnits - log.Printf("volumeFilter: selling (base units), keep = (projectedSoldInBaseUnits) %.7f < %.7f (config.SellBaseAssetCapInBaseUnits): keepSellingBase = %v", projectedSoldInBaseUnits, *f.config.SellBaseAssetCapInBaseUnits, keepSellingBase) + keepSellingBase = projectedSoldInBaseUnits <= *f.config.SellBaseAssetCapInBaseUnits + log.Printf("volumeFilter: selling (base units), keep = (projectedSoldInBaseUnits) %.7f <= %.7f (config.SellBaseAssetCapInBaseUnits): keepSellingBase = %v", projectedSoldInBaseUnits, *f.config.SellBaseAssetCapInBaseUnits, keepSellingBase) } if f.config.SellBaseAssetCapInQuoteUnits != nil { projectedSoldInQuoteUnits := *dailyOTB.SellBaseAssetCapInQuoteUnits + *dailyTBB.SellBaseAssetCapInQuoteUnits + amountValueUnitsBeingBought - keepSellingQuote = projectedSoldInQuoteUnits < *f.config.SellBaseAssetCapInQuoteUnits - log.Printf("volumeFilter: selling (quote units), keep = (projectedSoldInQuoteUnits) %.7f < %.7f (config.SellBaseAssetCapInQuoteUnits): keepSellingQuote = %v", projectedSoldInQuoteUnits, *f.config.SellBaseAssetCapInQuoteUnits, keepSellingQuote) + keepSellingQuote = projectedSoldInQuoteUnits <= *f.config.SellBaseAssetCapInQuoteUnits + log.Printf("volumeFilter: selling (quote units), keep = (projectedSoldInQuoteUnits) %.7f <= %.7f (config.SellBaseAssetCapInQuoteUnits): keepSellingQuote = %v", projectedSoldInQuoteUnits, *f.config.SellBaseAssetCapInQuoteUnits, keepSellingQuote) } keep = keepSellingBase && keepSellingQuote From 7ea2f55ea6eabf19b99618eaa50e480272d0f1e3 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Sun, 29 Dec 2019 12:18:12 +0530 Subject: [PATCH 15/25] 15 - update filterOps() to run existing offers through filter function converts existing offers to the equivalent operation so it can be run through the filter function --- plugins/makerModeFilter.go | 11 ++- plugins/submitFilter.go | 191 ++++++++++++++++++++++++++++++++++--- plugins/volumeFilter.go | 6 +- 3 files changed, 187 insertions(+), 21 deletions(-) diff --git a/plugins/makerModeFilter.go b/plugins/makerModeFilter.go index 9ba81b7d5..cb29cf215 100644 --- a/plugins/makerModeFilter.go +++ b/plugins/makerModeFilter.go @@ -35,11 +35,12 @@ func (f *makerModeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProto return nil, fmt.Errorf("could not fetch orderbook: %s", e) } + baseAsset, quoteAsset, e := f.sdex.Assets() + if e != nil { + return nil, fmt.Errorf("could not get assets: %s", e) + } + innerFn := func(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { - baseAsset, quoteAsset, e := f.sdex.Assets() - if e != nil { - return nil, false, fmt.Errorf("could not get assets: %s", e) - } topBidPrice, e := f.topOrderPriceExcludingTrader(ob.Bids(), buyingOffers, false) if e != nil { return nil, false, fmt.Errorf("could not get topOrderPriceExcludingTrader for bids: %s", e) @@ -51,7 +52,7 @@ func (f *makerModeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProto return f.transformOfferMakerMode(baseAsset, quoteAsset, topBidPrice, topAskPrice, op) } - ops, e = filterOps(ops, innerFn) + ops, e = filterOps(baseAsset, quoteAsset, sellingOffers, buyingOffers, ops, innerFn) if e != nil { return nil, fmt.Errorf("could not apply filter: %s", e) } diff --git a/plugins/submitFilter.go b/plugins/submitFilter.go index 1f7f34da1..d32af8015 100644 --- a/plugins/submitFilter.go +++ b/plugins/submitFilter.go @@ -3,9 +3,11 @@ package plugins import ( "fmt" "log" + "strconv" hProtocol "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/txnbuild" + "github.com/stellar/kelp/support/utils" ) // SubmitFilter allows you to filter out operations before submitting to the network @@ -19,18 +21,131 @@ type SubmitFilter interface { type filterFn func(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) -func filterOps(ops []txnbuild.Operation, fn filterFn) ([]txnbuild.Operation, error) { - numKeep := 0 - numDropped := 0 - numTransformed := 0 - filteredOps := []txnbuild.Operation{} +type filterCounter struct { + idx int + kept uint8 + dropped uint8 + transformed uint8 +} + +// build a list of the existing offers that have a corresponding operation so we ignore these offers and only consider the operation version +func ignoreOfferIDs(ops []txnbuild.Operation) map[int64]bool { + ignoreOfferIDs := map[int64]bool{} for _, op := range ops { + switch o := op.(type) { + case *txnbuild.ManageSellOffer: + ignoreOfferIDs[o.OfferID] = true + default: + continue + } + } + return ignoreOfferIDs +} + +/* +What filterOps() does and why: + +Solving the "existing offers problem": +Problem: We need to run the existing offers against the filter as well since they may no longer be compliant. +Solution: Do a merge of two "sorted" lists (operations list, offers list) to create a new list of operations. + When sorted by price, this will ensure that we delete any spurious existing offers to meet the filter's + needs. This also serves the purpose of "interleaving" the operations related to the offers and ops. + +Solving the "ordering problem": +Problem: The incoming operations list combines both buy and sell operations. We want to run it though the filter + without modifying the order of the buy or sell segments, or modify operations within the segments since that + ordering is dictated by the strategy logic. +Solution: Since both these segments of buy/sell offers are contiguous, i.e. buy offers are all together and sell + offers are all together, we can identify the "cutover point" in each list of operations and offers, and then + advance the iteration index to the next segment for both segments in both lists by converting the remaining + offers and operations to delete operations. This will not affect the order of operations, but any new delete + operations created should be placed at the beginning of the respective buy and sell segments as is a requirement + on sdex (see sellSideStrategy.go for details on why we need to start off with the delete operations). + +Possible Question: Why do we not reuse the same logic that is in sellSideStrategy.go to "delete remaining offers"? +Answer: The logic that could possibly be reused is minimal -- it's just a for loop. The logic that converts offers + to the associated delete operation is reused, which is the main crux of the "business logic" that we want to + avoid rewriting. The logic in sellSideStrategy.go also only works on offers, here we work on offers and ops. + +Solving the "increase price problem": +Problem: If we increase the price off a sell offer (or decrease price of a buy offer) then we will see the offer + with an incorrect price before we see the update to the offer. This will result in an incorrect calculation, + since we will later on see the updated offer and make adjustments, which would result in runtime complexity + worse than O(N). +Solution: We first "dedupe" the offers and operations, by removing any offers that have a corresponding operation + update based on offerID. This has an additional overhead on runtime complexity of O(N). +*/ +func filterOps( + baseAsset hProtocol.Asset, + quoteAsset hProtocol.Asset, + sellingOffers []hProtocol.Offer, + buyingOffers []hProtocol.Offer, + ops []txnbuild.Operation, + fn filterFn, +) ([]txnbuild.Operation, error) { + ignoreOfferIds := ignoreOfferIDs(ops) + + opCounter := filterCounter{} + buyCounter := filterCounter{} + sellCounter := filterCounter{} + ignoredSellOffers, ignoredBuyOffers := 0, 0 + filteredOps := []txnbuild.Operation{} + for opCounter.idx < len(ops) { + op := ops[opCounter.idx] + var offerList []hProtocol.Offer + var offerCounter *filterCounter + var originalOffer *txnbuild.ManageSellOffer var newOp txnbuild.Operation var keep bool switch o := op.(type) { case *txnbuild.ManageSellOffer: - var e error - newOp, keep, e = fn(o) + isSellOp, e := utils.IsSelling(baseAsset, quoteAsset, o.Selling, o.Buying) + if e != nil { + return nil, fmt.Errorf("could not check whether the ManageSellOffer was selling or buying: %s", e) + } + if isSellOp { + offerList = sellingOffers + offerCounter = &sellCounter + } else { + offerList = buyingOffers + offerCounter = &buyCounter + } + + opPrice, e := strconv.ParseFloat(o.Price, 64) + if e != nil { + return nil, fmt.Errorf("could not parse price as float64: %s", e) + } + + var opToTransform *txnbuild.ManageSellOffer + if offerCounter.idx >= len(offerList) { + opToTransform = o + opCounter.idx++ + } else { + existingOffer := offerList[offerCounter.idx] + if _, ignoreOffer := ignoreOfferIds[existingOffer.ID]; ignoreOffer { + // we want to only compare against valid offers so go to the next offer in the list + offerCounter.idx++ + if isSellOp { + ignoredSellOffers++ + } else { + ignoredBuyOffers++ + } + continue + } + + offerPrice := float64(existingOffer.PriceR.N) / float64(existingOffer.PriceR.D) + // use the existing offer if the price is the same so we don't recreate an offer unnecessarily + if opPrice < offerPrice { + opToTransform = o + opCounter.idx++ + } else { + opToTransform = convertOffer2MSO(existingOffer) + offerCounter.idx++ + originalOffer = convertOffer2MSO(existingOffer) + } + } + + newOp, keep, e = fn(opToTransform) if e != nil { return nil, fmt.Errorf("could not transform offer (pointer case): %s", e) } @@ -44,19 +159,69 @@ func filterOps(ops []txnbuild.Operation, fn filterFn) ([]txnbuild.Operation, err if isNewOpNil { return nil, fmt.Errorf("we want to keep op but newOp was nil (programmer error?)") } - filteredOps = append(filteredOps, newOp) - numKeep++ + + newOpMSO := newOp.(*txnbuild.ManageSellOffer) + if originalOffer != nil && originalOffer.Price == newOpMSO.Price && originalOffer.Amount == newOpMSO.Amount { + // do not append to filteredOps because this is an existing offer that we want to keep as-is + offerCounter.kept++ + } else if originalOffer != nil { + // we were dealing with an existing offer that was changed + filteredOps = append(filteredOps, newOp) + offerCounter.transformed++ + } else { + // we were dealing with an operation + filteredOps = append(filteredOps, newOp) + opCounter.kept++ + } } else { if !isNewOpNil { // newOp can be a transformed op to change the op to an effectively "dropped" state - filteredOps = append(filteredOps, newOp) - numTransformed++ + // prepend this so we always have delete commands at the beginning of the operation list + filteredOps = append([]txnbuild.Operation{newOp}, filteredOps...) + if originalOffer != nil { + // we are dealing with an existing offer that needs dropping + offerCounter.dropped++ + } else { + // we are dealing with an operation that had updated an offer which now needs dropping + opCounter.transformed++ + } } else { - numDropped++ + // newOp will never be nil for an original offer since it has an offerID + opCounter.dropped++ } } } - log.Printf("filter result: dropped %d, transformed %d, kept %d ops from original %d ops, len(filteredOps) = %d\n", numDropped, numTransformed, numKeep, len(ops), len(filteredOps)) + // convert all remaining buy and sell offers to delete offers + for sellCounter.idx < len(sellingOffers) { + dropOp := convertOffer2MSO(sellingOffers[sellCounter.idx]) + dropOp.Amount = "0" + filteredOps = append([]txnbuild.Operation{dropOp}, filteredOps...) + sellCounter.dropped++ + sellCounter.idx++ + } + for buyCounter.idx < len(buyingOffers) { + dropOp := convertOffer2MSO(buyingOffers[buyCounter.idx]) + dropOp.Amount = "0" + filteredOps = append([]txnbuild.Operation{dropOp}, filteredOps...) + buyCounter.dropped++ + buyCounter.idx++ + } + + log.Printf("filter result A: dropped %d, transformed %d, kept %d ops from original %d ops\n", opCounter.dropped, opCounter.transformed, opCounter.kept, len(ops)) + log.Printf("filter result B: dropped %d, transformed %d, kept %d, ignored %d sell offers from original %d sell offers\n", sellCounter.dropped, sellCounter.transformed, sellCounter.kept, ignoredSellOffers, len(sellingOffers)) + log.Printf("filter result C: dropped %d, transformed %d, kept %d, ignored %d buy offers from original %d buy offers\n", buyCounter.dropped, buyCounter.transformed, buyCounter.kept, ignoredBuyOffers, len(buyingOffers)) + log.Printf("filter result D: len(filteredOps) = %d\n", len(filteredOps)) return filteredOps, nil } + +func convertOffer2MSO(offer hProtocol.Offer) *txnbuild.ManageSellOffer { + return &txnbuild.ManageSellOffer{ + Selling: utils.Asset2Asset(offer.Selling), + Buying: utils.Asset2Asset(offer.Buying), + Amount: offer.Amount, + Price: offer.Price, + OfferID: offer.ID, + SourceAccount: &txnbuild.SimpleAccount{AccountID: offer.Seller}, + } +} diff --git a/plugins/volumeFilter.go b/plugins/volumeFilter.go index 9ba72d9d5..c797621a6 100644 --- a/plugins/volumeFilter.go +++ b/plugins/volumeFilter.go @@ -109,7 +109,7 @@ func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol innerFn := func(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { return f.volumeFilterFn(dailyOTB, dailyTBB, op) } - ops, e = filterOps(ops, innerFn) + ops, e = filterOps(f.baseAsset, f.quoteAsset, sellingOffers, buyingOffers, ops, innerFn) if e != nil { return nil, fmt.Errorf("could not apply filter: %s", e) } @@ -145,13 +145,13 @@ func (f *volumeFilter) volumeFilterFn(dailyOTB *VolumeFilterConfig, dailyTBB *Vo if f.config.SellBaseAssetCapInBaseUnits != nil { projectedSoldInBaseUnits := *dailyOTB.SellBaseAssetCapInBaseUnits + *dailyTBB.SellBaseAssetCapInBaseUnits + amountValueUnitsBeingSold keepSellingBase = projectedSoldInBaseUnits <= *f.config.SellBaseAssetCapInBaseUnits - log.Printf("volumeFilter: selling (base units), keep = (projectedSoldInBaseUnits) %.7f <= %.7f (config.SellBaseAssetCapInBaseUnits): keepSellingBase = %v", projectedSoldInBaseUnits, *f.config.SellBaseAssetCapInBaseUnits, keepSellingBase) + log.Printf("volumeFilter: selling (base units), price=%.8f amount=%.8f, keep = (projectedSoldInBaseUnits) %.7f <= %.7f (config.SellBaseAssetCapInBaseUnits): keepSellingBase = %v", sellPrice, amountValueUnitsBeingSold, projectedSoldInBaseUnits, *f.config.SellBaseAssetCapInBaseUnits, keepSellingBase) } if f.config.SellBaseAssetCapInQuoteUnits != nil { projectedSoldInQuoteUnits := *dailyOTB.SellBaseAssetCapInQuoteUnits + *dailyTBB.SellBaseAssetCapInQuoteUnits + amountValueUnitsBeingBought keepSellingQuote = projectedSoldInQuoteUnits <= *f.config.SellBaseAssetCapInQuoteUnits - log.Printf("volumeFilter: selling (quote units), keep = (projectedSoldInQuoteUnits) %.7f <= %.7f (config.SellBaseAssetCapInQuoteUnits): keepSellingQuote = %v", projectedSoldInQuoteUnits, *f.config.SellBaseAssetCapInQuoteUnits, keepSellingQuote) + log.Printf("volumeFilter: selling (quote units), price=%.8f amount=%.8f, keep = (projectedSoldInQuoteUnits) %.7f <= %.7f (config.SellBaseAssetCapInQuoteUnits): keepSellingQuote = %v", sellPrice, amountValueUnitsBeingSold, projectedSoldInQuoteUnits, *f.config.SellBaseAssetCapInQuoteUnits, keepSellingQuote) } keep = keepSellingBase && keepSellingQuote From eb166be539134cc1224340198ba08fb9859e0266 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Sun, 29 Dec 2019 12:24:09 +0530 Subject: [PATCH 16/25] 16 - added TODO for simplification of filterOps --- plugins/submitFilter.go | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/submitFilter.go b/plugins/submitFilter.go index d32af8015..d827bd661 100644 --- a/plugins/submitFilter.go +++ b/plugins/submitFilter.go @@ -42,6 +42,7 @@ func ignoreOfferIDs(ops []txnbuild.Operation) map[int64]bool { return ignoreOfferIDs } +// TODO - simplify filterOps by separating out logic to convert into a single list of operations from transforming the operations /* What filterOps() does and why: From cd8d66e426d41b2fb7fa050b477af0b2eba9ed17 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Sun, 29 Dec 2019 12:26:57 +0530 Subject: [PATCH 17/25] 17 - update sample config to denote that volumeFilter only works with the sell strategy and requires POSTGRES_DB config --- examples/configs/trader/sample_trader.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/configs/trader/sample_trader.cfg b/examples/configs/trader/sample_trader.cfg index e777163ff..474ea7eb3 100644 --- a/examples/configs/trader/sample_trader.cfg +++ b/examples/configs/trader/sample_trader.cfg @@ -109,6 +109,8 @@ MAX_OP_FEE_STROOPS=5000 #SSL_ENABLE=false # uncomment to limit daily trades based on daily trading volume denominated in either the base or quote asset, or both +# Note: requires the POSTGRES_DB config to work +# Note: only works with sell strategy for now #[VOLUME_FILTER] # limits the amount of the base asset that is sold, denominated in units of the base asset #SELL_BASE_ASSET_CAP_IN_BASE_UNITS=100000.0 From 2dd9e79171b6941b6695909e9702cda3044b6077 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Sun, 29 Dec 2019 16:26:09 +0530 Subject: [PATCH 18/25] 18 - minPrice filter --- cmd/trade.go | 18 +++++ examples/configs/trader/sample_trader.cfg | 5 ++ plugins/minPriceFilter.go | 80 +++++++++++++++++++++++ trader/config.go | 35 +++++----- 4 files changed, 121 insertions(+), 17 deletions(-) create mode 100644 plugins/minPriceFilter.go diff --git a/cmd/trade.go b/cmd/trade.go index 6af6a4e8b..edfc28811 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -364,6 +364,24 @@ func makeBot( plugins.MakeFilterMakerMode(exchangeShim, sdex, tradingPair), ) } + if botConfig.MinPriceFilterConfig != nil { + if e := botConfig.MinPriceFilterConfig.Validate(); e != nil { + log.Println() + log.Println(e) + // we want to delete all the offers and exit here since there is something wrong with our setup + deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) + } + + minPriceFilter, e := plugins.MakeFilterMinPrice(botConfig.MinPriceFilterConfig, assetBase, assetQuote) + if e != nil { + log.Println() + log.Println(e) + // we want to delete all the offers and exit here since there is something wrong with our setup + deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) + } + + submitFilters = append(submitFilters, minPriceFilter) + } if botConfig.VolumeFilterConfig != nil { if e := botConfig.VolumeFilterConfig.Validate(); e != nil { log.Println() diff --git a/examples/configs/trader/sample_trader.cfg b/examples/configs/trader/sample_trader.cfg index 474ea7eb3..32116a34f 100644 --- a/examples/configs/trader/sample_trader.cfg +++ b/examples/configs/trader/sample_trader.cfg @@ -117,6 +117,11 @@ MAX_OP_FEE_STROOPS=5000 # limits the amount of the base asset that is sold, denominated in units of the quote asset #SELL_BASE_ASSET_CAP_IN_QUOTE_UNITS=100000.0 +# uncomment to limit offers based on a minimim price requirement +# Note: only works with sell strategy for now +[MIN_PRICE_FILTER] +MIN_PRICE=0.04 + # uncomment lines below to use kraken. Can use "sdex" or leave out to trade on the Stellar Decentralized Exchange. # can alternatively use any of the ccxt-exchanges marked as "Trading" (run `kelp exchanges` for full list) #TRADING_EXCHANGE="kraken" diff --git a/plugins/minPriceFilter.go b/plugins/minPriceFilter.go new file mode 100644 index 000000000..80b9f8a23 --- /dev/null +++ b/plugins/minPriceFilter.go @@ -0,0 +1,80 @@ +package plugins + +import ( + "fmt" + "strconv" + + hProtocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/txnbuild" + "github.com/stellar/kelp/support/utils" +) + +// MinPriceFilterConfig ensures that any one constraint that is hit will result in deleting all offers and pausing until limits are no longer constrained +type MinPriceFilterConfig struct { + MinPrice *float64 `valid:"-" toml:"MIN_PRICE" json:"min_price"` +} + +type minPriceFilter struct { + config *MinPriceFilterConfig + baseAsset hProtocol.Asset + quoteAsset hProtocol.Asset +} + +// MakeFilterMinPrice makes a submit filter that limits orders placed based on the price +func MakeFilterMinPrice(config *MinPriceFilterConfig, baseAsset hProtocol.Asset, quoteAsset hProtocol.Asset) (SubmitFilter, error) { + return &minPriceFilter{ + config: config, + baseAsset: baseAsset, + quoteAsset: quoteAsset, + }, nil +} + +var _ SubmitFilter = &minPriceFilter{} + +// Validate ensures validity +func (c *MinPriceFilterConfig) Validate() error { + if c.MinPrice == nil { + return fmt.Errorf("needs a minPrice config value") + } + return nil +} + +// String is the stringer method +func (c *MinPriceFilterConfig) String() string { + return fmt.Sprintf("MinPriceFilterConfig[MinPrice=%s]", utils.CheckedFloatPtr(c.MinPrice)) +} + +func (f *minPriceFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol.Offer, buyingOffers []hProtocol.Offer) ([]txnbuild.Operation, error) { + ops, e := filterOps(f.baseAsset, f.quoteAsset, sellingOffers, buyingOffers, ops, f.minPriceFilterFn) + if e != nil { + return nil, fmt.Errorf("could not apply filter: %s", e) + } + return ops, nil +} + +func (f *minPriceFilter) minPriceFilterFn(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { + // delete operations should never be dropped + if op.Amount == "0" { + return op, true, nil + } + + isSell, e := utils.IsSelling(f.baseAsset, f.quoteAsset, op.Selling, op.Buying) + if e != nil { + return nil, false, fmt.Errorf("error when running the isSelling check: %s", e) + } + + sellPrice, e := strconv.ParseFloat(op.Price, 64) + if e != nil { + return nil, false, fmt.Errorf("could not convert price (%s) to float: %s", op.Price, e) + } + + if isSell { + if sellPrice < *f.config.MinPrice { + return nil, false, nil + } + return op, true, nil + } + + // TODO for buy side + return op, true, nil +} diff --git a/trader/config.go b/trader/config.go index 0133c6445..540dbaee9 100644 --- a/trader/config.go +++ b/trader/config.go @@ -40,23 +40,24 @@ type BotConfig struct { CentralizedPricePrecisionOverride *int8 `valid:"-" toml:"CENTRALIZED_PRICE_PRECISION_OVERRIDE" json:"centralized_price_precision_override"` CentralizedVolumePrecisionOverride *int8 `valid:"-" toml:"CENTRALIZED_VOLUME_PRECISION_OVERRIDE" json:"centralized_volume_precision_override"` // Deprecated: use CENTRALIZED_MIN_BASE_VOLUME_OVERRIDE instead - MinCentralizedBaseVolumeDeprecated *float64 `valid:"-" toml:"MIN_CENTRALIZED_BASE_VOLUME" deprecated:"true" json:"min_centralized_base_volume"` - CentralizedMinBaseVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_BASE_VOLUME_OVERRIDE" json:"centralized_min_base_volume_override"` - CentralizedMinQuoteVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_QUOTE_VOLUME_OVERRIDE" json:"centralized_min_quote_volume_override"` - PostgresDbConfig *postgresdb.Config `valid:"-" toml:"POSTGRES_DB" json:"postgres_db"` - VolumeFilterConfig *plugins.VolumeFilterConfig `valid:"-" toml:"VOLUME_FILTER" json:"volume_filter"` - AlertType string `valid:"-" toml:"ALERT_TYPE" json:"alert_type"` - AlertAPIKey string `valid:"-" toml:"ALERT_API_KEY" json:"alert_api_key"` - MonitoringPort uint16 `valid:"-" toml:"MONITORING_PORT" json:"monitoring_port"` - MonitoringTLSCert string `valid:"-" toml:"MONITORING_TLS_CERT" json:"monitoring_tls_cert"` - MonitoringTLSKey string `valid:"-" toml:"MONITORING_TLS_KEY" json:"monitoring_tls_key"` - GoogleClientID string `valid:"-" toml:"GOOGLE_CLIENT_ID" json:"google_client_id"` - GoogleClientSecret string `valid:"-" toml:"GOOGLE_CLIENT_SECRET" json:"google_client_secret"` - AcceptableEmails string `valid:"-" toml:"ACCEPTABLE_GOOGLE_EMAILS" json:"acceptable_google_emails"` - TradingExchange string `valid:"-" toml:"TRADING_EXCHANGE" json:"trading_exchange"` - ExchangeAPIKeys toml.ExchangeAPIKeysToml `valid:"-" toml:"EXCHANGE_API_KEYS" json:"exchange_api_keys"` - ExchangeParams toml.ExchangeParamsToml `valid:"-" toml:"EXCHANGE_PARAMS" json:"exchange_params"` - ExchangeHeaders toml.ExchangeHeadersToml `valid:"-" toml:"EXCHANGE_HEADERS" json:"exchange_headers"` + MinCentralizedBaseVolumeDeprecated *float64 `valid:"-" toml:"MIN_CENTRALIZED_BASE_VOLUME" deprecated:"true" json:"min_centralized_base_volume"` + CentralizedMinBaseVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_BASE_VOLUME_OVERRIDE" json:"centralized_min_base_volume_override"` + CentralizedMinQuoteVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_QUOTE_VOLUME_OVERRIDE" json:"centralized_min_quote_volume_override"` + PostgresDbConfig *postgresdb.Config `valid:"-" toml:"POSTGRES_DB" json:"postgres_db"` + VolumeFilterConfig *plugins.VolumeFilterConfig `valid:"-" toml:"VOLUME_FILTER" json:"volume_filter"` + MinPriceFilterConfig *plugins.MinPriceFilterConfig `valid:"-" toml:"MIN_PRICE_FILTER" json:"min_price_filter"` + AlertType string `valid:"-" toml:"ALERT_TYPE" json:"alert_type"` + AlertAPIKey string `valid:"-" toml:"ALERT_API_KEY" json:"alert_api_key"` + MonitoringPort uint16 `valid:"-" toml:"MONITORING_PORT" json:"monitoring_port"` + MonitoringTLSCert string `valid:"-" toml:"MONITORING_TLS_CERT" json:"monitoring_tls_cert"` + MonitoringTLSKey string `valid:"-" toml:"MONITORING_TLS_KEY" json:"monitoring_tls_key"` + GoogleClientID string `valid:"-" toml:"GOOGLE_CLIENT_ID" json:"google_client_id"` + GoogleClientSecret string `valid:"-" toml:"GOOGLE_CLIENT_SECRET" json:"google_client_secret"` + AcceptableEmails string `valid:"-" toml:"ACCEPTABLE_GOOGLE_EMAILS" json:"acceptable_google_emails"` + TradingExchange string `valid:"-" toml:"TRADING_EXCHANGE" json:"trading_exchange"` + ExchangeAPIKeys toml.ExchangeAPIKeysToml `valid:"-" toml:"EXCHANGE_API_KEYS" json:"exchange_api_keys"` + ExchangeParams toml.ExchangeParamsToml `valid:"-" toml:"EXCHANGE_PARAMS" json:"exchange_params"` + ExchangeHeaders toml.ExchangeHeadersToml `valid:"-" toml:"EXCHANGE_HEADERS" json:"exchange_headers"` // initialized later tradingAccount *string From f35253c7f020a450e13bda502398024c1e4851e4 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Sun, 29 Dec 2019 16:35:12 +0530 Subject: [PATCH 19/25] 19 - add filterName to filterOps log line --- plugins/makerModeFilter.go | 4 +++- plugins/minPriceFilter.go | 4 +++- plugins/submitFilter.go | 9 +++++---- plugins/volumeFilter.go | 4 +++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/plugins/makerModeFilter.go b/plugins/makerModeFilter.go index cb29cf215..51a28d287 100644 --- a/plugins/makerModeFilter.go +++ b/plugins/makerModeFilter.go @@ -13,6 +13,7 @@ import ( ) type makerModeFilter struct { + name string tradingPair *model.TradingPair exchangeShim api.ExchangeShim sdex *SDEX @@ -21,6 +22,7 @@ type makerModeFilter struct { // MakeFilterMakerMode makes a submit filter based on the passed in submitMode func MakeFilterMakerMode(exchangeShim api.ExchangeShim, sdex *SDEX, tradingPair *model.TradingPair) SubmitFilter { return &makerModeFilter{ + name: "makeModeFilter", tradingPair: tradingPair, exchangeShim: exchangeShim, sdex: sdex, @@ -52,7 +54,7 @@ func (f *makerModeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProto return f.transformOfferMakerMode(baseAsset, quoteAsset, topBidPrice, topAskPrice, op) } - ops, e = filterOps(baseAsset, quoteAsset, sellingOffers, buyingOffers, ops, innerFn) + ops, e = filterOps(f.name, baseAsset, quoteAsset, sellingOffers, buyingOffers, ops, innerFn) if e != nil { return nil, fmt.Errorf("could not apply filter: %s", e) } diff --git a/plugins/minPriceFilter.go b/plugins/minPriceFilter.go index 80b9f8a23..c5ba0c9da 100644 --- a/plugins/minPriceFilter.go +++ b/plugins/minPriceFilter.go @@ -15,6 +15,7 @@ type MinPriceFilterConfig struct { } type minPriceFilter struct { + name string config *MinPriceFilterConfig baseAsset hProtocol.Asset quoteAsset hProtocol.Asset @@ -23,6 +24,7 @@ type minPriceFilter struct { // MakeFilterMinPrice makes a submit filter that limits orders placed based on the price func MakeFilterMinPrice(config *MinPriceFilterConfig, baseAsset hProtocol.Asset, quoteAsset hProtocol.Asset) (SubmitFilter, error) { return &minPriceFilter{ + name: "minPriceFilter", config: config, baseAsset: baseAsset, quoteAsset: quoteAsset, @@ -45,7 +47,7 @@ func (c *MinPriceFilterConfig) String() string { } func (f *minPriceFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol.Offer, buyingOffers []hProtocol.Offer) ([]txnbuild.Operation, error) { - ops, e := filterOps(f.baseAsset, f.quoteAsset, sellingOffers, buyingOffers, ops, f.minPriceFilterFn) + ops, e := filterOps(f.name, f.baseAsset, f.quoteAsset, sellingOffers, buyingOffers, ops, f.minPriceFilterFn) if e != nil { return nil, fmt.Errorf("could not apply filter: %s", e) } diff --git a/plugins/submitFilter.go b/plugins/submitFilter.go index d827bd661..3fbef05c5 100644 --- a/plugins/submitFilter.go +++ b/plugins/submitFilter.go @@ -77,6 +77,7 @@ Solution: We first "dedupe" the offers and operations, by removing any offers th update based on offerID. This has an additional overhead on runtime complexity of O(N). */ func filterOps( + filterName string, baseAsset hProtocol.Asset, quoteAsset hProtocol.Asset, sellingOffers []hProtocol.Offer, @@ -209,10 +210,10 @@ func filterOps( buyCounter.idx++ } - log.Printf("filter result A: dropped %d, transformed %d, kept %d ops from original %d ops\n", opCounter.dropped, opCounter.transformed, opCounter.kept, len(ops)) - log.Printf("filter result B: dropped %d, transformed %d, kept %d, ignored %d sell offers from original %d sell offers\n", sellCounter.dropped, sellCounter.transformed, sellCounter.kept, ignoredSellOffers, len(sellingOffers)) - log.Printf("filter result C: dropped %d, transformed %d, kept %d, ignored %d buy offers from original %d buy offers\n", buyCounter.dropped, buyCounter.transformed, buyCounter.kept, ignoredBuyOffers, len(buyingOffers)) - log.Printf("filter result D: len(filteredOps) = %d\n", len(filteredOps)) + log.Printf("filter \"%s\" result A: dropped %d, transformed %d, kept %d ops from original %d ops\n", filterName, opCounter.dropped, opCounter.transformed, opCounter.kept, len(ops)) + log.Printf("filter \"%s\" result B: dropped %d, transformed %d, kept %d, ignored %d sell offers from original %d sell offers\n", filterName, sellCounter.dropped, sellCounter.transformed, sellCounter.kept, ignoredSellOffers, len(sellingOffers)) + log.Printf("filter \"%s\" result C: dropped %d, transformed %d, kept %d, ignored %d buy offers from original %d buy offers\n", filterName, buyCounter.dropped, buyCounter.transformed, buyCounter.kept, ignoredBuyOffers, len(buyingOffers)) + log.Printf("filter \"%s\" result D: len(filteredOps) = %d\n", filterName, len(filteredOps)) return filteredOps, nil } diff --git a/plugins/volumeFilter.go b/plugins/volumeFilter.go index c797621a6..98d220199 100644 --- a/plugins/volumeFilter.go +++ b/plugins/volumeFilter.go @@ -25,6 +25,7 @@ type VolumeFilterConfig struct { } type volumeFilter struct { + name string baseAsset hProtocol.Asset quoteAsset hProtocol.Asset marketID string @@ -58,6 +59,7 @@ func MakeFilterVolume( marketID := makeMarketID(exchangeName, baseAssetString, quoteAssetString) return &volumeFilter{ + name: "volumeFilter", baseAsset: baseAsset, quoteAsset: quoteAsset, marketID: marketID, @@ -109,7 +111,7 @@ func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol innerFn := func(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { return f.volumeFilterFn(dailyOTB, dailyTBB, op) } - ops, e = filterOps(f.baseAsset, f.quoteAsset, sellingOffers, buyingOffers, ops, innerFn) + ops, e = filterOps(f.name, f.baseAsset, f.quoteAsset, sellingOffers, buyingOffers, ops, innerFn) if e != nil { return nil, fmt.Errorf("could not apply filter: %s", e) } From 234adb81f9e1bf7c18545725124e4980ed870c15 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Sun, 29 Dec 2019 16:45:53 +0530 Subject: [PATCH 20/25] 20 - MAX_PRICE filter --- cmd/trade.go | 18 +++++ examples/configs/trader/sample_trader.cfg | 5 ++ plugins/maxPriceFilter.go | 82 +++++++++++++++++++++++ trader/config.go | 1 + 4 files changed, 106 insertions(+) create mode 100644 plugins/maxPriceFilter.go diff --git a/cmd/trade.go b/cmd/trade.go index edfc28811..55276cb00 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -382,6 +382,24 @@ func makeBot( submitFilters = append(submitFilters, minPriceFilter) } + if botConfig.MaxPriceFilterConfig != nil { + if e := botConfig.MaxPriceFilterConfig.Validate(); e != nil { + log.Println() + log.Println(e) + // we want to delete all the offers and exit here since there is something wrong with our setup + deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) + } + + maxPriceFilter, e := plugins.MakeFilterMaxPrice(botConfig.MaxPriceFilterConfig, assetBase, assetQuote) + if e != nil { + log.Println() + log.Println(e) + // we want to delete all the offers and exit here since there is something wrong with our setup + deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) + } + + submitFilters = append(submitFilters, maxPriceFilter) + } if botConfig.VolumeFilterConfig != nil { if e := botConfig.VolumeFilterConfig.Validate(); e != nil { log.Println() diff --git a/examples/configs/trader/sample_trader.cfg b/examples/configs/trader/sample_trader.cfg index 32116a34f..cef4c83e0 100644 --- a/examples/configs/trader/sample_trader.cfg +++ b/examples/configs/trader/sample_trader.cfg @@ -122,6 +122,11 @@ MAX_OP_FEE_STROOPS=5000 [MIN_PRICE_FILTER] MIN_PRICE=0.04 +# uncomment to limit offers based on a maximum price requirement +# Note: only works with sell strategy for now +[MAX_PRICE_FILTER] +MAX_PRICE=1.00 + # uncomment lines below to use kraken. Can use "sdex" or leave out to trade on the Stellar Decentralized Exchange. # can alternatively use any of the ccxt-exchanges marked as "Trading" (run `kelp exchanges` for full list) #TRADING_EXCHANGE="kraken" diff --git a/plugins/maxPriceFilter.go b/plugins/maxPriceFilter.go new file mode 100644 index 000000000..5e20bb5ea --- /dev/null +++ b/plugins/maxPriceFilter.go @@ -0,0 +1,82 @@ +package plugins + +import ( + "fmt" + "strconv" + + hProtocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/txnbuild" + "github.com/stellar/kelp/support/utils" +) + +// MaxPriceFilterConfig ensures that any one constraint that is hit will result in deleting all offers and pausing until limits are no longer constrained +type MaxPriceFilterConfig struct { + MaxPrice *float64 `valid:"-" toml:"MAX_PRICE" json:"max_price"` +} + +type maxPriceFilter struct { + name string + config *MaxPriceFilterConfig + baseAsset hProtocol.Asset + quoteAsset hProtocol.Asset +} + +// MakeFilterMaxPrice makes a submit filter that limits orders placed based on the price +func MakeFilterMaxPrice(config *MaxPriceFilterConfig, baseAsset hProtocol.Asset, quoteAsset hProtocol.Asset) (SubmitFilter, error) { + return &maxPriceFilter{ + name: "maxPriceFilter", + config: config, + baseAsset: baseAsset, + quoteAsset: quoteAsset, + }, nil +} + +var _ SubmitFilter = &maxPriceFilter{} + +// Validate ensures validity +func (c *MaxPriceFilterConfig) Validate() error { + if c.MaxPrice == nil { + return fmt.Errorf("needs a maxPrice config value") + } + return nil +} + +// String is the stringer method +func (c *MaxPriceFilterConfig) String() string { + return fmt.Sprintf("MaxPriceFilterConfig[MaxPrice=%s]", utils.CheckedFloatPtr(c.MaxPrice)) +} + +func (f *maxPriceFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol.Offer, buyingOffers []hProtocol.Offer) ([]txnbuild.Operation, error) { + ops, e := filterOps(f.name, f.baseAsset, f.quoteAsset, sellingOffers, buyingOffers, ops, f.maxPriceFilterFn) + if e != nil { + return nil, fmt.Errorf("could not apply filter: %s", e) + } + return ops, nil +} + +func (f *maxPriceFilter) maxPriceFilterFn(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { + // delete operations should never be dropped + if op.Amount == "0" { + return op, true, nil + } + + isSell, e := utils.IsSelling(f.baseAsset, f.quoteAsset, op.Selling, op.Buying) + if e != nil { + return nil, false, fmt.Errorf("error when running the isSelling check: %s", e) + } + + sellPrice, e := strconv.ParseFloat(op.Price, 64) + if e != nil { + return nil, false, fmt.Errorf("could not convert price (%s) to float: %s", op.Price, e) + } + + if isSell { + if sellPrice > *f.config.MaxPrice { + return nil, false, nil + } + return op, true, nil + } + + // TODO for buy side + return op, true, nil +} diff --git a/trader/config.go b/trader/config.go index 540dbaee9..1adb3a4cd 100644 --- a/trader/config.go +++ b/trader/config.go @@ -46,6 +46,7 @@ type BotConfig struct { PostgresDbConfig *postgresdb.Config `valid:"-" toml:"POSTGRES_DB" json:"postgres_db"` VolumeFilterConfig *plugins.VolumeFilterConfig `valid:"-" toml:"VOLUME_FILTER" json:"volume_filter"` MinPriceFilterConfig *plugins.MinPriceFilterConfig `valid:"-" toml:"MIN_PRICE_FILTER" json:"min_price_filter"` + MaxPriceFilterConfig *plugins.MaxPriceFilterConfig `valid:"-" toml:"MAX_PRICE_FILTER" json:"max_price_filter"` AlertType string `valid:"-" toml:"ALERT_TYPE" json:"alert_type"` AlertAPIKey string `valid:"-" toml:"ALERT_API_KEY" json:"alert_api_key"` MonitoringPort uint16 `valid:"-" toml:"MONITORING_PORT" json:"monitoring_port"` From d80510141521754e6798f4972ca7e644166486c0 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Sun, 29 Dec 2019 16:58:03 +0530 Subject: [PATCH 21/25] 21 - new format of filters in config file with corresponding filterFactory.go --- cmd/trade.go | 66 +++------------ examples/configs/trader/sample_trader.cfg | 52 ++++++------ plugins/filterFactory.go | 98 +++++++++++++++++++++++ plugins/maxPriceFilter.go | 4 +- plugins/minPriceFilter.go | 4 +- plugins/submitFilter.go | 2 +- plugins/volumeFilter.go | 14 ++-- trader/config.go | 37 ++++----- 8 files changed, 164 insertions(+), 113 deletions(-) create mode 100644 plugins/filterFactory.go diff --git a/cmd/trade.go b/cmd/trade.go index 55276cb00..9b8ed3cc9 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -364,67 +364,23 @@ func makeBot( plugins.MakeFilterMakerMode(exchangeShim, sdex, tradingPair), ) } - if botConfig.MinPriceFilterConfig != nil { - if e := botConfig.MinPriceFilterConfig.Validate(); e != nil { - log.Println() - log.Println(e) - // we want to delete all the offers and exit here since there is something wrong with our setup - deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) - } - - minPriceFilter, e := plugins.MakeFilterMinPrice(botConfig.MinPriceFilterConfig, assetBase, assetQuote) - if e != nil { - log.Println() - log.Println(e) - // we want to delete all the offers and exit here since there is something wrong with our setup - deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) - } - - submitFilters = append(submitFilters, minPriceFilter) - } - if botConfig.MaxPriceFilterConfig != nil { - if e := botConfig.MaxPriceFilterConfig.Validate(); e != nil { - log.Println() - log.Println(e) - // we want to delete all the offers and exit here since there is something wrong with our setup - deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) - } - - maxPriceFilter, e := plugins.MakeFilterMaxPrice(botConfig.MaxPriceFilterConfig, assetBase, assetQuote) - if e != nil { - log.Println() - log.Println(e) - // we want to delete all the offers and exit here since there is something wrong with our setup - deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) - } - - submitFilters = append(submitFilters, maxPriceFilter) - } - if botConfig.VolumeFilterConfig != nil { - if e := botConfig.VolumeFilterConfig.Validate(); e != nil { - log.Println() - log.Println(e) - // we want to delete all the offers and exit here since there is something wrong with our setup - deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) - } - - volFilter, e := plugins.MakeFilterVolume( - botConfig.TradingExchangeName(), - tradingPair, - assetDisplayFn, - assetBase, - assetQuote, - botConfig.VolumeFilterConfig, - db, - ) + filterFactory := plugins.FilterFactory{ + ExchangeName: botConfig.TradingExchangeName(), + TradingPair: tradingPair, + AssetDisplayFn: assetDisplayFn, + BaseAsset: assetBase, + QuoteAsset: assetQuote, + DB: db, + } + for _, filterString := range botConfig.Filters { + filter, e := filterFactory.MakeFilter(filterString) if e != nil { log.Println() log.Println(e) // we want to delete all the offers and exit here since there is something wrong with our setup deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) } - - submitFilters = append(submitFilters, volFilter) + submitFilters = append(submitFilters, filter) } return trader.MakeTrader( diff --git a/examples/configs/trader/sample_trader.cfg b/examples/configs/trader/sample_trader.cfg index cef4c83e0..6044dcd9d 100644 --- a/examples/configs/trader/sample_trader.cfg +++ b/examples/configs/trader/sample_trader.cfg @@ -51,15 +51,6 @@ HORIZON_URL="https://horizon-testnet.stellar.org" # the URL to use for your CCXT-rest instance. Defaults to http://localhost:3000 if unset #CCXT_REST_URL="http://localhost:3000" -# specify parameters for how we compute the operation fee from the /fee_stats endpoint -[FEE] -# trigger when "ledger_capacity_usage" in /fee_stats is >= this value -CAPACITY_TRIGGER=0.8 -# percentile computation to use from /fee_stats (10, 20, ..., 90, 95, 99) -PERCENTILE=90 -# max fee in stroops per operation to use -MAX_OP_FEE_STROOPS=5000 - # uncomment below to add support for monitoring. # type of alerting system to use, currently only "PagerDuty" is supported. #ALERT_TYPE="PagerDuty" @@ -99,6 +90,30 @@ MAX_OP_FEE_STROOPS=5000 # (optional) minimum volume of quote units needed to place an order on the non-sdex (centralized) exchange #CENTRALIZED_MIN_QUOTE_VOLUME_OVERRIDE=10.0 +# uncomment to include these filters in order (these filters only work with sell strategy for now) +#FILTERS = [ +# # limit the amount of the base asset that is sold every day, denominated in units of the base asset (needs POSTGRES_DB) +# "volume/sell/base/3500.0", +# +# # limit the amount of the base asset that is sold every day, denominated in units of the quote asset (needs POSTGRES_DB) +# "volume/sell/quote/1000.0", +# +# # limit offers based on a minimim price requirement +# "price/min/0.04", +# +# # limit offers based on a maximum price requirement +# "price/max/1.00", +#] + +# specify parameters for how we compute the operation fee from the /fee_stats endpoint +[FEE] +# trigger when "ledger_capacity_usage" in /fee_stats is >= this value +CAPACITY_TRIGGER=0.8 +# percentile computation to use from /fee_stats (10, 20, ..., 90, 95, 99) +PERCENTILE=90 +# max fee in stroops per operation to use +MAX_OP_FEE_STROOPS=5000 + # uncomment if you want to track fills in a postgres db #[POSTGRES_DB] #HOST="localhost" @@ -108,25 +123,6 @@ MAX_OP_FEE_STROOPS=5000 #PASSWORD="" #SSL_ENABLE=false -# uncomment to limit daily trades based on daily trading volume denominated in either the base or quote asset, or both -# Note: requires the POSTGRES_DB config to work -# Note: only works with sell strategy for now -#[VOLUME_FILTER] -# limits the amount of the base asset that is sold, denominated in units of the base asset -#SELL_BASE_ASSET_CAP_IN_BASE_UNITS=100000.0 -# limits the amount of the base asset that is sold, denominated in units of the quote asset -#SELL_BASE_ASSET_CAP_IN_QUOTE_UNITS=100000.0 - -# uncomment to limit offers based on a minimim price requirement -# Note: only works with sell strategy for now -[MIN_PRICE_FILTER] -MIN_PRICE=0.04 - -# uncomment to limit offers based on a maximum price requirement -# Note: only works with sell strategy for now -[MAX_PRICE_FILTER] -MAX_PRICE=1.00 - # uncomment lines below to use kraken. Can use "sdex" or leave out to trade on the Stellar Decentralized Exchange. # can alternatively use any of the ccxt-exchanges marked as "Trading" (run `kelp exchanges` for full list) #TRADING_EXCHANGE="kraken" diff --git a/plugins/filterFactory.go b/plugins/filterFactory.go new file mode 100644 index 000000000..d6a69e3c7 --- /dev/null +++ b/plugins/filterFactory.go @@ -0,0 +1,98 @@ +package plugins + +import ( + "database/sql" + "fmt" + "strconv" + "strings" + + hProtocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/kelp/model" +) + +var filterMap = map[string]func(f *FilterFactory, configInput string) (SubmitFilter, error){ + "volume": filterVolume, + "price": filterPrice, +} + +// FilterFactory is a struct that handles creating all the filters +type FilterFactory struct { + ExchangeName string + TradingPair *model.TradingPair + AssetDisplayFn model.AssetDisplayFn + BaseAsset hProtocol.Asset + QuoteAsset hProtocol.Asset + DB *sql.DB +} + +// MakeFilter is the function that makes the required filters +func (f *FilterFactory) MakeFilter(configInput string) (SubmitFilter, error) { + parts := strings.Split(configInput, "/") + if len(parts) <= 0 { + return nil, fmt.Errorf("invalid input (%s), needs at least 1 delimiter (/)", configInput) + } + + filterName := parts[0] + factoryMethod, ok := filterMap[filterName] + if !ok { + return nil, fmt.Errorf("could not find filter of type '%s'", filterName) + } + + return factoryMethod(f, configInput) +} + +func filterVolume(f *FilterFactory, configInput string) (SubmitFilter, error) { + parts := strings.Split(configInput, "/") + if len(parts) != 4 { + return nil, fmt.Errorf("invalid input (%s), needs 4 parts separated by the delimiter (/)", configInput) + } + + config := &VolumeFilterConfig{} + if parts[1] != "sell" { + return nil, fmt.Errorf("invalid input (%s), the second part needs to be \"sell\"", configInput) + } + limit, e := strconv.ParseFloat(parts[3], 64) + if e != nil { + return nil, fmt.Errorf("could not parse the fourth part as a float value from config value (%s): %s", configInput, e) + } + if parts[2] == "base" { + config.SellBaseAssetCapInBaseUnits = &limit + } else if parts[2] == "quote" { + config.SellBaseAssetCapInQuoteUnits = &limit + } else { + return nil, fmt.Errorf("invalid input (%s), the third part needs to be \"base\" or \"quote\"", configInput) + } + if e := config.Validate(); e != nil { + return nil, fmt.Errorf("invalid input (%s), did not pass validation: %s", configInput, e) + } + + return makeFilterVolume( + f.ExchangeName, + f.TradingPair, + f.AssetDisplayFn, + f.BaseAsset, + f.QuoteAsset, + f.DB, + config, + ) +} + +func filterPrice(f *FilterFactory, configInput string) (SubmitFilter, error) { + parts := strings.Split(configInput, "/") + if len(parts) != 3 { + return nil, fmt.Errorf("invalid input (%s), needs 3 parts separated by the delimiter (/)", configInput) + } + + limit, e := strconv.ParseFloat(parts[2], 64) + if e != nil { + return nil, fmt.Errorf("could not parse the third part as a float value from config value (%s): %s", configInput, e) + } + if parts[1] == "min" { + config := MinPriceFilterConfig{MinPrice: &limit} + return MakeFilterMinPrice(f.BaseAsset, f.QuoteAsset, &config) + } else if parts[1] == "max" { + config := MaxPriceFilterConfig{MaxPrice: &limit} + return MakeFilterMaxPrice(f.BaseAsset, f.QuoteAsset, &config) + } + return nil, fmt.Errorf("invalid price filter type in second argument (%s)", configInput, e) +} diff --git a/plugins/maxPriceFilter.go b/plugins/maxPriceFilter.go index 5e20bb5ea..b1ba3ba08 100644 --- a/plugins/maxPriceFilter.go +++ b/plugins/maxPriceFilter.go @@ -11,7 +11,7 @@ import ( // MaxPriceFilterConfig ensures that any one constraint that is hit will result in deleting all offers and pausing until limits are no longer constrained type MaxPriceFilterConfig struct { - MaxPrice *float64 `valid:"-" toml:"MAX_PRICE" json:"max_price"` + MaxPrice *float64 } type maxPriceFilter struct { @@ -22,7 +22,7 @@ type maxPriceFilter struct { } // MakeFilterMaxPrice makes a submit filter that limits orders placed based on the price -func MakeFilterMaxPrice(config *MaxPriceFilterConfig, baseAsset hProtocol.Asset, quoteAsset hProtocol.Asset) (SubmitFilter, error) { +func MakeFilterMaxPrice(baseAsset hProtocol.Asset, quoteAsset hProtocol.Asset, config *MaxPriceFilterConfig) (SubmitFilter, error) { return &maxPriceFilter{ name: "maxPriceFilter", config: config, diff --git a/plugins/minPriceFilter.go b/plugins/minPriceFilter.go index c5ba0c9da..17dc3c035 100644 --- a/plugins/minPriceFilter.go +++ b/plugins/minPriceFilter.go @@ -11,7 +11,7 @@ import ( // MinPriceFilterConfig ensures that any one constraint that is hit will result in deleting all offers and pausing until limits are no longer constrained type MinPriceFilterConfig struct { - MinPrice *float64 `valid:"-" toml:"MIN_PRICE" json:"min_price"` + MinPrice *float64 } type minPriceFilter struct { @@ -22,7 +22,7 @@ type minPriceFilter struct { } // MakeFilterMinPrice makes a submit filter that limits orders placed based on the price -func MakeFilterMinPrice(config *MinPriceFilterConfig, baseAsset hProtocol.Asset, quoteAsset hProtocol.Asset) (SubmitFilter, error) { +func MakeFilterMinPrice(baseAsset hProtocol.Asset, quoteAsset hProtocol.Asset, config *MinPriceFilterConfig) (SubmitFilter, error) { return &minPriceFilter{ name: "minPriceFilter", config: config, diff --git a/plugins/submitFilter.go b/plugins/submitFilter.go index 3fbef05c5..0106a0832 100644 --- a/plugins/submitFilter.go +++ b/plugins/submitFilter.go @@ -210,7 +210,7 @@ func filterOps( buyCounter.idx++ } - log.Printf("filter \"%s\" result A: dropped %d, transformed %d, kept %d ops from original %d ops\n", filterName, opCounter.dropped, opCounter.transformed, opCounter.kept, len(ops)) + log.Printf("filter \"%s\" result A: dropped %d, transformed %d, kept %d ops from the %d ops passed in\n", filterName, opCounter.dropped, opCounter.transformed, opCounter.kept, len(ops)) log.Printf("filter \"%s\" result B: dropped %d, transformed %d, kept %d, ignored %d sell offers from original %d sell offers\n", filterName, sellCounter.dropped, sellCounter.transformed, sellCounter.kept, ignoredSellOffers, len(sellingOffers)) log.Printf("filter \"%s\" result C: dropped %d, transformed %d, kept %d, ignored %d buy offers from original %d buy offers\n", filterName, buyCounter.dropped, buyCounter.transformed, buyCounter.kept, ignoredBuyOffers, len(buyingOffers)) log.Printf("filter \"%s\" result D: len(filteredOps) = %d\n", filterName, len(filteredOps)) diff --git a/plugins/volumeFilter.go b/plugins/volumeFilter.go index 98d220199..26d1b8370 100644 --- a/plugins/volumeFilter.go +++ b/plugins/volumeFilter.go @@ -18,8 +18,8 @@ import ( // VolumeFilterConfig ensures that any one constraint that is hit will result in deleting all offers and pausing until limits are no longer constrained type VolumeFilterConfig struct { - SellBaseAssetCapInBaseUnits *float64 `valid:"-" toml:"SELL_BASE_ASSET_CAP_IN_BASE_UNITS" json:"sell_base_asset_cap_in_base_units"` - SellBaseAssetCapInQuoteUnits *float64 `valid:"-" toml:"SELL_BASE_ASSET_CAP_IN_QUOTE_UNITS" json:"sell_base_asset_cap_in_quote_units"` + SellBaseAssetCapInBaseUnits *float64 + SellBaseAssetCapInQuoteUnits *float64 // buyBaseAssetCapInBaseUnits *float64 // buyBaseAssetCapInQuoteUnits *float64 } @@ -33,15 +33,15 @@ type volumeFilter struct { db *sql.DB } -// MakeFilterVolume makes a submit filter that limits orders placed based on the daily volume traded -func MakeFilterVolume( +// makeFilterVolume makes a submit filter that limits orders placed based on the daily volume traded +func makeFilterVolume( exchangeName string, tradingPair *model.TradingPair, assetDisplayFn model.AssetDisplayFn, baseAsset hProtocol.Asset, quoteAsset hProtocol.Asset, - config *VolumeFilterConfig, db *sql.DB, + config *VolumeFilterConfig, ) (SubmitFilter, error) { if db == nil { return nil, fmt.Errorf("the provided db should be non-nil") @@ -148,12 +148,16 @@ func (f *volumeFilter) volumeFilterFn(dailyOTB *VolumeFilterConfig, dailyTBB *Vo projectedSoldInBaseUnits := *dailyOTB.SellBaseAssetCapInBaseUnits + *dailyTBB.SellBaseAssetCapInBaseUnits + amountValueUnitsBeingSold keepSellingBase = projectedSoldInBaseUnits <= *f.config.SellBaseAssetCapInBaseUnits log.Printf("volumeFilter: selling (base units), price=%.8f amount=%.8f, keep = (projectedSoldInBaseUnits) %.7f <= %.7f (config.SellBaseAssetCapInBaseUnits): keepSellingBase = %v", sellPrice, amountValueUnitsBeingSold, projectedSoldInBaseUnits, *f.config.SellBaseAssetCapInBaseUnits, keepSellingBase) + } else { + keepSellingBase = true } if f.config.SellBaseAssetCapInQuoteUnits != nil { projectedSoldInQuoteUnits := *dailyOTB.SellBaseAssetCapInQuoteUnits + *dailyTBB.SellBaseAssetCapInQuoteUnits + amountValueUnitsBeingBought keepSellingQuote = projectedSoldInQuoteUnits <= *f.config.SellBaseAssetCapInQuoteUnits log.Printf("volumeFilter: selling (quote units), price=%.8f amount=%.8f, keep = (projectedSoldInQuoteUnits) %.7f <= %.7f (config.SellBaseAssetCapInQuoteUnits): keepSellingQuote = %v", sellPrice, amountValueUnitsBeingSold, projectedSoldInQuoteUnits, *f.config.SellBaseAssetCapInQuoteUnits, keepSellingQuote) + } else { + keepSellingQuote = true } keep = keepSellingBase && keepSellingQuote diff --git a/trader/config.go b/trader/config.go index 1adb3a4cd..505f6f453 100644 --- a/trader/config.go +++ b/trader/config.go @@ -4,7 +4,6 @@ import ( "fmt" hProtocol "github.com/stellar/go/protocols/horizon" - "github.com/stellar/kelp/plugins" "github.com/stellar/kelp/support/postgresdb" "github.com/stellar/kelp/support/toml" "github.com/stellar/kelp/support/utils" @@ -40,25 +39,23 @@ type BotConfig struct { CentralizedPricePrecisionOverride *int8 `valid:"-" toml:"CENTRALIZED_PRICE_PRECISION_OVERRIDE" json:"centralized_price_precision_override"` CentralizedVolumePrecisionOverride *int8 `valid:"-" toml:"CENTRALIZED_VOLUME_PRECISION_OVERRIDE" json:"centralized_volume_precision_override"` // Deprecated: use CENTRALIZED_MIN_BASE_VOLUME_OVERRIDE instead - MinCentralizedBaseVolumeDeprecated *float64 `valid:"-" toml:"MIN_CENTRALIZED_BASE_VOLUME" deprecated:"true" json:"min_centralized_base_volume"` - CentralizedMinBaseVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_BASE_VOLUME_OVERRIDE" json:"centralized_min_base_volume_override"` - CentralizedMinQuoteVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_QUOTE_VOLUME_OVERRIDE" json:"centralized_min_quote_volume_override"` - PostgresDbConfig *postgresdb.Config `valid:"-" toml:"POSTGRES_DB" json:"postgres_db"` - VolumeFilterConfig *plugins.VolumeFilterConfig `valid:"-" toml:"VOLUME_FILTER" json:"volume_filter"` - MinPriceFilterConfig *plugins.MinPriceFilterConfig `valid:"-" toml:"MIN_PRICE_FILTER" json:"min_price_filter"` - MaxPriceFilterConfig *plugins.MaxPriceFilterConfig `valid:"-" toml:"MAX_PRICE_FILTER" json:"max_price_filter"` - AlertType string `valid:"-" toml:"ALERT_TYPE" json:"alert_type"` - AlertAPIKey string `valid:"-" toml:"ALERT_API_KEY" json:"alert_api_key"` - MonitoringPort uint16 `valid:"-" toml:"MONITORING_PORT" json:"monitoring_port"` - MonitoringTLSCert string `valid:"-" toml:"MONITORING_TLS_CERT" json:"monitoring_tls_cert"` - MonitoringTLSKey string `valid:"-" toml:"MONITORING_TLS_KEY" json:"monitoring_tls_key"` - GoogleClientID string `valid:"-" toml:"GOOGLE_CLIENT_ID" json:"google_client_id"` - GoogleClientSecret string `valid:"-" toml:"GOOGLE_CLIENT_SECRET" json:"google_client_secret"` - AcceptableEmails string `valid:"-" toml:"ACCEPTABLE_GOOGLE_EMAILS" json:"acceptable_google_emails"` - TradingExchange string `valid:"-" toml:"TRADING_EXCHANGE" json:"trading_exchange"` - ExchangeAPIKeys toml.ExchangeAPIKeysToml `valid:"-" toml:"EXCHANGE_API_KEYS" json:"exchange_api_keys"` - ExchangeParams toml.ExchangeParamsToml `valid:"-" toml:"EXCHANGE_PARAMS" json:"exchange_params"` - ExchangeHeaders toml.ExchangeHeadersToml `valid:"-" toml:"EXCHANGE_HEADERS" json:"exchange_headers"` + MinCentralizedBaseVolumeDeprecated *float64 `valid:"-" toml:"MIN_CENTRALIZED_BASE_VOLUME" deprecated:"true" json:"min_centralized_base_volume"` + CentralizedMinBaseVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_BASE_VOLUME_OVERRIDE" json:"centralized_min_base_volume_override"` + CentralizedMinQuoteVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_QUOTE_VOLUME_OVERRIDE" json:"centralized_min_quote_volume_override"` + PostgresDbConfig *postgresdb.Config `valid:"-" toml:"POSTGRES_DB" json:"postgres_db"` + Filters []string `valid:"-" toml:"FILTERS" json:"filters"` + AlertType string `valid:"-" toml:"ALERT_TYPE" json:"alert_type"` + AlertAPIKey string `valid:"-" toml:"ALERT_API_KEY" json:"alert_api_key"` + MonitoringPort uint16 `valid:"-" toml:"MONITORING_PORT" json:"monitoring_port"` + MonitoringTLSCert string `valid:"-" toml:"MONITORING_TLS_CERT" json:"monitoring_tls_cert"` + MonitoringTLSKey string `valid:"-" toml:"MONITORING_TLS_KEY" json:"monitoring_tls_key"` + GoogleClientID string `valid:"-" toml:"GOOGLE_CLIENT_ID" json:"google_client_id"` + GoogleClientSecret string `valid:"-" toml:"GOOGLE_CLIENT_SECRET" json:"google_client_secret"` + AcceptableEmails string `valid:"-" toml:"ACCEPTABLE_GOOGLE_EMAILS" json:"acceptable_google_emails"` + TradingExchange string `valid:"-" toml:"TRADING_EXCHANGE" json:"trading_exchange"` + ExchangeAPIKeys toml.ExchangeAPIKeysToml `valid:"-" toml:"EXCHANGE_API_KEYS" json:"exchange_api_keys"` + ExchangeParams toml.ExchangeParamsToml `valid:"-" toml:"EXCHANGE_PARAMS" json:"exchange_params"` + ExchangeHeaders toml.ExchangeHeadersToml `valid:"-" toml:"EXCHANGE_HEADERS" json:"exchange_headers"` // initialized later tradingAccount *string From 04965b84f04f6e5ad2de5f6df395d62d2813005a Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 30 Dec 2019 15:49:04 +0530 Subject: [PATCH 22/25] 22 - force fail if any new filters are specified without sell or delete strategies --- cmd/trade.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmd/trade.go b/cmd/trade.go index 9b8ed3cc9..6faa038a8 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -356,6 +356,7 @@ func makeBot( l.Infof("Unable to set up monitoring for alert type '%s' with the given API key\n", botConfig.AlertType) } + // start make filters submitFilters := []plugins.SubmitFilter{ plugins.MakeFilterOrderConstraints(exchangeShim.GetOrderConstraints(tradingPair), assetBase, assetQuote), } @@ -364,6 +365,12 @@ func makeBot( plugins.MakeFilterMakerMode(exchangeShim, sdex, tradingPair), ) } + if len(botConfig.Filters) > 0 && *options.strategy != "sell" && *options.strategy != "delete" { + log.Println() + utils.PrintErrorHintf("FILTERS currently only supported on 'sell' and 'delete' strategies, remove FILTERS from the trader config file") + // we want to delete all the offers and exit here since there is something wrong with our setup + deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) + } filterFactory := plugins.FilterFactory{ ExchangeName: botConfig.TradingExchangeName(), TradingPair: tradingPair, @@ -382,6 +389,7 @@ func makeBot( } submitFilters = append(submitFilters, filter) } + // end make filters return trader.MakeTrader( client, From f98f6b1e6e7291dd1b58c3543134f7d4670ad2d1 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 30 Dec 2019 15:54:53 +0530 Subject: [PATCH 23/25] 23 - extract logic that never drops delete operations into filterOps() --- plugins/maxPriceFilter.go | 5 ----- plugins/minPriceFilter.go | 5 ----- plugins/submitFilter.go | 11 ++++++++--- plugins/volumeFilter.go | 5 ----- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/plugins/maxPriceFilter.go b/plugins/maxPriceFilter.go index b1ba3ba08..7907d87b6 100644 --- a/plugins/maxPriceFilter.go +++ b/plugins/maxPriceFilter.go @@ -55,11 +55,6 @@ func (f *maxPriceFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtoc } func (f *maxPriceFilter) maxPriceFilterFn(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { - // delete operations should never be dropped - if op.Amount == "0" { - return op, true, nil - } - isSell, e := utils.IsSelling(f.baseAsset, f.quoteAsset, op.Selling, op.Buying) if e != nil { return nil, false, fmt.Errorf("error when running the isSelling check: %s", e) diff --git a/plugins/minPriceFilter.go b/plugins/minPriceFilter.go index 17dc3c035..049b1890e 100644 --- a/plugins/minPriceFilter.go +++ b/plugins/minPriceFilter.go @@ -55,11 +55,6 @@ func (f *minPriceFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtoc } func (f *minPriceFilter) minPriceFilterFn(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { - // delete operations should never be dropped - if op.Amount == "0" { - return op, true, nil - } - isSell, e := utils.IsSelling(f.baseAsset, f.quoteAsset, op.Selling, op.Buying) if e != nil { return nil, false, fmt.Errorf("error when running the isSelling check: %s", e) diff --git a/plugins/submitFilter.go b/plugins/submitFilter.go index 0106a0832..516c9f14d 100644 --- a/plugins/submitFilter.go +++ b/plugins/submitFilter.go @@ -147,9 +147,14 @@ func filterOps( } } - newOp, keep, e = fn(opToTransform) - if e != nil { - return nil, fmt.Errorf("could not transform offer (pointer case): %s", e) + // delete operations should never be dropped + if opToTransform.Amount == "0" { + newOp, keep = opToTransform, true + } else { + newOp, keep, e = fn(opToTransform) + if e != nil { + return nil, fmt.Errorf("could not transform offer (pointer case): %s", e) + } } default: newOp = o diff --git a/plugins/volumeFilter.go b/plugins/volumeFilter.go index 26d1b8370..d93c1e608 100644 --- a/plugins/volumeFilter.go +++ b/plugins/volumeFilter.go @@ -119,11 +119,6 @@ func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol } func (f *volumeFilter) volumeFilterFn(dailyOTB *VolumeFilterConfig, dailyTBB *VolumeFilterConfig, op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, bool, error) { - // delete operations should never be dropped - if op.Amount == "0" { - return op, true, nil - } - isSell, e := utils.IsSelling(f.baseAsset, f.quoteAsset, op.Selling, op.Buying) if e != nil { return nil, false, fmt.Errorf("error when running the isSelling check: %s", e) From e1e1a577e2e4162ccdaeae0f365c095ff6a374b6 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 30 Dec 2019 16:08:04 +0530 Subject: [PATCH 24/25] 24 - included additional explanation in FILTERS section of sample config file --- examples/configs/trader/sample_trader.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/configs/trader/sample_trader.cfg b/examples/configs/trader/sample_trader.cfg index 6044dcd9d..fdcccf311 100644 --- a/examples/configs/trader/sample_trader.cfg +++ b/examples/configs/trader/sample_trader.cfg @@ -91,6 +91,9 @@ HORIZON_URL="https://horizon-testnet.stellar.org" #CENTRALIZED_MIN_QUOTE_VOLUME_OVERRIDE=10.0 # uncomment to include these filters in order (these filters only work with sell strategy for now) +# these are the only four filters available for now via this new filtration method and any new filters added will include a +# corresponding sample entry with an explanation. +# the best way to use these filters is to uncomment the one you want to use and update the price (last param) accordingly. #FILTERS = [ # # limit the amount of the base asset that is sold every day, denominated in units of the base asset (needs POSTGRES_DB) # "volume/sell/base/3500.0", From eca836a4afa87825cb031dc550af56007cb231f8 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 30 Dec 2019 16:11:50 +0530 Subject: [PATCH 25/25] 25 - fix Errorf statement --- plugins/filterFactory.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/filterFactory.go b/plugins/filterFactory.go index d6a69e3c7..71c44d144 100644 --- a/plugins/filterFactory.go +++ b/plugins/filterFactory.go @@ -94,5 +94,5 @@ func filterPrice(f *FilterFactory, configInput string) (SubmitFilter, error) { config := MaxPriceFilterConfig{MaxPrice: &limit} return MakeFilterMaxPrice(f.BaseAsset, f.QuoteAsset, &config) } - return nil, fmt.Errorf("invalid price filter type in second argument (%s)", configInput, e) + return nil, fmt.Errorf("invalid price filter type in second argument (%s)", configInput) }