From 66ea6105938434c090b28d3b7cb65d32d5100a62 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 2 Jan 2020 20:49:15 +0530 Subject: [PATCH] volumeFilter should be configurable to include "exact" amounts (closes #327) (#328) * 1 - add exact and ignore params to the volume filter in sample_trader.cfg * 2 - read in exact/ignore mode to volume filter config * 3 - implement exact mode in volumeFilter * 4 - update filterOps to correctly count kept vs. transformed operations with the newly added capability in volumeFilter * 5 - separate out handling for non-offer-ops which makes things a little simpler with typing * 6 - remove TODO about this issue form volumeFilter * 7 - fix big bug which resulted in submission failures (non-increment of index) * 8 - update comment * 9 - add comment to filterOps about "no update operations problem" * 10 - refactor filterOps: add "ignored" field to filterCounter for buy/sell offers * 11 - refactor filterOps: fold original amount and price into originalMSO variable * 12 - refactor filterOps: extract out selectBuySellList and selectOpOrOffer * 13 - refactor filterOps: extract out logic that handles calling and result of the filterFn for reuse * 14 - fix bug with originalMSO getting modified along with newOp * 15 - refactor filterOps: add fix to handle remainingOffers correctly with common function for buy/sell sides includes logic to ignore offers in ignore list * 16 - moved exchange constraints filter to be last --- cmd/trade.go | 9 +- examples/configs/trader/sample_trader.cfg | 18 +- plugins/filterFactory.go | 10 +- plugins/submitFilter.go | 353 +++++++++++++++------- plugins/volumeFilter.go | 65 +++- 5 files changed, 327 insertions(+), 128 deletions(-) diff --git a/cmd/trade.go b/cmd/trade.go index 6faa038a8..107e9d1f4 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -357,9 +357,7 @@ func makeBot( } // start make filters - submitFilters := []plugins.SubmitFilter{ - plugins.MakeFilterOrderConstraints(exchangeShim.GetOrderConstraints(tradingPair), assetBase, assetQuote), - } + submitFilters := []plugins.SubmitFilter{} if submitMode == api.SubmitModeMakerOnly { submitFilters = append(submitFilters, plugins.MakeFilterMakerMode(exchangeShim, sdex, tradingPair), @@ -389,6 +387,11 @@ func makeBot( } submitFilters = append(submitFilters, filter) } + // exchange constraints filter is last so we catch any modifications made by previous filters. this ensures that the exchange is + // less likely to reject our updates + submitFilters = append(submitFilters, + plugins.MakeFilterOrderConstraints(exchangeShim.GetOrderConstraints(tradingPair), assetBase, assetQuote), + ) // end make filters return trader.MakeTrader( diff --git a/examples/configs/trader/sample_trader.cfg b/examples/configs/trader/sample_trader.cfg index fdcccf311..75dfda375 100644 --- a/examples/configs/trader/sample_trader.cfg +++ b/examples/configs/trader/sample_trader.cfg @@ -96,10 +96,24 @@ HORIZON_URL="https://horizon-testnet.stellar.org" # 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", +# # The fifth param can be either "exact" or "ignore" ("exact" is recommended): +# # - "exact" indicates that the volume filter should modify the amount of the offer that will cause the capacity limit +# # to be exceeded (when daily sold amounts are close to the limit). This will result in the exact number of units of +# # the asset to be sold for the given day. +# # - "ignore" indicates that the volume filter should not modify the values of any offer and the offer which will cause +# # the capacity limit to be exceeded should be dropped or ignored. This will result in a less than or equal amount +# # of the asset to be sold for the given day. +# "volume/sell/base/3500.0/exact", # # # 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", +# # The fifth param can be either "exact" or "ignore" ("exact" is recommended): +# # - "exact" indicates that the volume filter should modify the amount of the offer that will cause the capacity limit +# # to be exceeded (when daily sold amounts are close to the limit). This will result in the exact number of units of +# # the asset to be sold for the given day. +# # - "ignore" indicates that the volume filter should not modify the values of any offer and the offer which will cause +# # the capacity limit to be exceeded should be dropped or ignored. This will result in a less than or equal amount +# # of the asset to be sold for the given day. +# "volume/sell/quote/1000.0/ignore", # # # limit offers based on a minimim price requirement # "price/min/0.04", diff --git a/plugins/filterFactory.go b/plugins/filterFactory.go index 71c44d144..f00fed180 100644 --- a/plugins/filterFactory.go +++ b/plugins/filterFactory.go @@ -43,11 +43,15 @@ func (f *FilterFactory) MakeFilter(configInput string) (SubmitFilter, error) { 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) + if len(parts) != 5 { + return nil, fmt.Errorf("invalid input (%s), needs 5 parts separated by the delimiter (/)", configInput) } - config := &VolumeFilterConfig{} + mode, e := parseVolumeFilterMode(parts[4]) + if e != nil { + return nil, fmt.Errorf("could not parse volume filter mode from input (%s): %s", configInput, e) + } + config := &VolumeFilterConfig{mode: mode} if parts[1] != "sell" { return nil, fmt.Errorf("invalid input (%s), the second part needs to be \"sell\"", configInput) } diff --git a/plugins/submitFilter.go b/plugins/submitFilter.go index 516c9f14d..4bf8d8d84 100644 --- a/plugins/submitFilter.go +++ b/plugins/submitFilter.go @@ -26,6 +26,15 @@ type filterCounter struct { kept uint8 dropped uint8 transformed uint8 + ignored uint8 +} + +func (f *filterCounter) add(other filterCounter) { + f.idx += other.idx + f.kept += other.kept + f.dropped += other.dropped + f.transformed += other.transformed + f.ignored += other.ignored } // build a list of the existing offers that have a corresponding operation so we ignore these offers and only consider the operation version @@ -75,6 +84,14 @@ Problem: If we increase the price off a sell offer (or decrease price of a buy o 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). + +Solving the "no update operations problem": +Problem: if our trading strategy produces no operations for a given update cycle, indicating that the state of the + orderbook is correct, then we will not enter the for-loop which is conditioned on operations. This would result + in control going straight to the post-operations logic which should correctly consider the existing offers. This + logic would be the same as what happens inside the for loop and we should ensure there is no repetition. +Solution: Refactor the code inside the for loop to clearly allow for reuse of functions and evaluation of existing + offers outside the for loop. */ func filterOps( filterName string, @@ -86,142 +103,268 @@ func filterOps( 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: - isSellOp, e := utils.IsSelling(baseAsset, quoteAsset, o.Selling, o.Buying) + offerList, offerCounter, e := selectBuySellList( + baseAsset, + quoteAsset, + o, + sellingOffers, + buyingOffers, + &sellCounter, + &buyCounter, + ) 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 + return nil, fmt.Errorf("unable to pick between whether the op was a buy or sell op: %s", e) } - opPrice, e := strconv.ParseFloat(o.Price, 64) + opToTransform, originalOffer, filterCounterToIncrement, isIgnoredOffer, e := selectOpOrOffer( + offerList, + offerCounter, + o, + &opCounter, + ignoreOfferIds, + ) if e != nil { - return nil, fmt.Errorf("could not parse price as float64: %s", e) + return nil, fmt.Errorf("error while picking op or offer: %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) - } + filterCounterToIncrement.idx++ + if isIgnoredOffer { + filterCounterToIncrement.ignored++ + continue } - // 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) - } + newOpToAppend, newOpToPrepend, filterCounterToIncrement, incrementValues, e := runInnerFilterFn( + *opToTransform, // pass copy + fn, + originalOffer, + *o, // pass copy + offerCounter, + &opCounter, + ) + if e != nil { + return nil, fmt.Errorf("error while running inner filter function: %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?)") + if newOpToAppend != nil { + filteredOps = append(filteredOps, newOpToAppend) } - - 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++ + if newOpToPrepend != nil { + filteredOps = append([]txnbuild.Operation{newOpToPrepend}, filteredOps...) } - } else { - if !isNewOpNil { - // newOp can be a transformed op to change the op to an effectively "dropped" state - // 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 { - // newOp will never be nil for an original offer since it has an offerID - opCounter.dropped++ + if filterCounterToIncrement != nil && incrementValues != nil { + filterCounterToIncrement.add(*incrementValues) } + default: + filteredOps = append(filteredOps, o) + opCounter.kept++ + opCounter.idx++ } } // 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++ + filteredOps, e := handleRemainingOffers( + &sellCounter, + sellingOffers, + &opCounter, + ignoreOfferIds, + filteredOps, + fn, + ) + if e != nil { + return nil, fmt.Errorf("error when handling remaining sell offers: %s", e) } - for buyCounter.idx < len(buyingOffers) { - dropOp := convertOffer2MSO(buyingOffers[buyCounter.idx]) - dropOp.Amount = "0" - filteredOps = append([]txnbuild.Operation{dropOp}, filteredOps...) - buyCounter.dropped++ - buyCounter.idx++ + filteredOps, e = handleRemainingOffers( + &buyCounter, + buyingOffers, + &opCounter, + ignoreOfferIds, + filteredOps, + fn, + ) + if e != nil { + return nil, fmt.Errorf("error when handling remaining buy offers: %s", e) } 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 B: dropped %d, transformed %d, kept %d, ignored %d sell offers (corresponding op update) from original %d sell offers\n", filterName, sellCounter.dropped, sellCounter.transformed, sellCounter.kept, sellCounter.ignored, len(sellingOffers)) + log.Printf("filter \"%s\" result C: dropped %d, transformed %d, kept %d, ignored %d buy offers (corresponding op update) from original %d buy offers\n", filterName, buyCounter.dropped, buyCounter.transformed, buyCounter.kept, buyCounter.ignored, len(buyingOffers)) log.Printf("filter \"%s\" result D: len(filteredOps) = %d\n", filterName, len(filteredOps)) return filteredOps, nil } +func selectBuySellList( + baseAsset hProtocol.Asset, + quoteAsset hProtocol.Asset, + mso *txnbuild.ManageSellOffer, + sellingOffers []hProtocol.Offer, + buyingOffers []hProtocol.Offer, + sellCounter *filterCounter, + buyCounter *filterCounter, +) ([]hProtocol.Offer, *filterCounter, error) { + isSellOp, e := utils.IsSelling(baseAsset, quoteAsset, mso.Selling, mso.Buying) + if e != nil { + return nil, nil, fmt.Errorf("could not check whether the ManageSellOffer was selling or buying: %s", e) + } + + if isSellOp { + return sellingOffers, sellCounter, nil + } + return buyingOffers, buyCounter, nil +} + +func selectOpOrOffer( + offerList []hProtocol.Offer, + offerCounter *filterCounter, + mso *txnbuild.ManageSellOffer, + opCounter *filterCounter, + ignoreOfferIds map[int64]bool, +) ( + opToTransform *txnbuild.ManageSellOffer, + originalOfferAsOp *txnbuild.ManageSellOffer, + c *filterCounter, + isIgnoredOffer bool, + err error, +) { + if offerCounter.idx >= len(offerList) { + return mso, nil, opCounter, false, nil + } + + existingOffer := offerList[offerCounter.idx] + if _, ignoreOffer := ignoreOfferIds[existingOffer.ID]; ignoreOffer { + // we want to only compare against valid offers so skip this offer by returning ignored = true + return nil, nil, offerCounter, true, nil + } + + offerPrice := float64(existingOffer.PriceR.N) / float64(existingOffer.PriceR.D) + opPrice, e := strconv.ParseFloat(mso.Price, 64) + if e != nil { + return nil, nil, nil, false, fmt.Errorf("could not parse price as float64: %s", e) + } + + // use the existing offer if the price is the same so we don't recreate an offer unnecessarily + if opPrice < offerPrice { + return mso, nil, opCounter, false, nil + } + + offerAsOp := convertOffer2MSO(existingOffer) + offerAsOpCopy := *offerAsOp + return offerAsOp, &offerAsOpCopy, offerCounter, false, nil +} + +func runInnerFilterFn( + opToTransform txnbuild.ManageSellOffer, // passed by value so it doesn't change + fn filterFn, + originalOfferAsOp *txnbuild.ManageSellOffer, + originalMSO txnbuild.ManageSellOffer, // passed by value so it doesn't change + offerCounter *filterCounter, + opCounter *filterCounter, +) ( + newOpToAppend *txnbuild.ManageSellOffer, + newOpToPrepend *txnbuild.ManageSellOffer, + filterCounterToIncrement *filterCounter, + incrementValues *filterCounter, + err error, +) { + var keep bool + var newOp *txnbuild.ManageSellOffer + var e error + + // 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, nil, nil, nil, fmt.Errorf("error in inner filter fn: %s", e) + } + } + + isNewOpNil := newOp == nil || fmt.Sprintf("%v", newOp) == "" + if keep && isNewOpNil { + return nil, nil, nil, nil, fmt.Errorf("we want to keep op but newOp was nil (programmer error?)") + } else if keep { + if originalOfferAsOp != nil && originalOfferAsOp.Price == newOp.Price && originalOfferAsOp.Amount == newOp.Amount { + // do not append to filteredOps because this is an existing offer that we want to keep as-is + return nil, nil, offerCounter, &filterCounter{kept: 1}, nil + } else if originalOfferAsOp != nil { + // we were dealing with an existing offer that was modified + return newOp, nil, offerCounter, &filterCounter{transformed: 1}, nil + } else { + // we were dealing with an operation + opModified := originalMSO.Price != newOp.Price || originalMSO.Amount != newOp.Amount + if opModified { + return newOp, nil, opCounter, &filterCounter{transformed: 1}, nil + } + return newOp, nil, opCounter, &filterCounter{kept: 1}, nil + } + } else if isNewOpNil { + // newOp will never be nil for an original offer since we will return the original non-nil offer + return nil, nil, opCounter, &filterCounter{dropped: 1}, nil + } else { + // newOp can be a transformed op to change the op to an effectively "dropped" state + // prepend this so we always have delete commands at the beginning of the operation list + if originalOfferAsOp != nil { + // we are dealing with an existing offer that needs dropping + return nil, newOp, offerCounter, &filterCounter{dropped: 1}, nil + } else { + // we are dealing with an operation that had updated an offer which now needs dropping + // using the transformed counter here because we are changing the actual intent of the operation + // from an update existing offer to delete existing offer logic + return nil, newOp, opCounter, &filterCounter{transformed: 1}, nil + } + } +} + +func handleRemainingOffers( + offerCounter *filterCounter, + offers []hProtocol.Offer, + opCounter *filterCounter, + ignoreOfferIds map[int64]bool, + filteredOps []txnbuild.Operation, + fn filterFn, +) ([]txnbuild.Operation, error) { + for offerCounter.idx < len(offers) { + if _, ignoreOffer := ignoreOfferIds[offers[offerCounter.idx].ID]; ignoreOffer { + // we want to only compare against valid offers so ignore this offer + offerCounter.ignored++ + offerCounter.idx++ + continue + } + + originalOfferAsOp := convertOffer2MSO(offers[offerCounter.idx]) + newOpToAppend, newOpToPrepend, filterCounterToIncrement, incrementValues, e := runInnerFilterFn( + *originalOfferAsOp, // pass copy + fn, + originalOfferAsOp, + *originalOfferAsOp, // pass copy + offerCounter, + opCounter, + ) + if e != nil { + return nil, fmt.Errorf("error while running inner filter function for remaining offers: %s", e) + } + if newOpToAppend != nil { + filteredOps = append(filteredOps, newOpToAppend) + } + if newOpToPrepend != nil { + filteredOps = append([]txnbuild.Operation{newOpToPrepend}, filteredOps...) + } + if filterCounterToIncrement != nil && incrementValues != nil { + filterCounterToIncrement.add(*incrementValues) + } + offerCounter.idx++ + } + return filteredOps, nil +} + func convertOffer2MSO(offer hProtocol.Offer) *txnbuild.ManageSellOffer { return &txnbuild.ManageSellOffer{ Selling: utils.Asset2Asset(offer.Selling), diff --git a/plugins/volumeFilter.go b/plugins/volumeFilter.go index d93c1e608..9c725ceed 100644 --- a/plugins/volumeFilter.go +++ b/plugins/volumeFilter.go @@ -16,10 +16,28 @@ import ( "github.com/stellar/kelp/support/utils" ) +type volumeFilterMode string + +// type of volumeFilterMode +const ( + volumeFilterModeExact volumeFilterMode = "exact" + volumeFilterModeIgnore volumeFilterMode = "ignore" +) + +func parseVolumeFilterMode(mode string) (volumeFilterMode, error) { + if mode == string(volumeFilterModeExact) { + return volumeFilterModeExact, nil + } else if mode == string(volumeFilterModeIgnore) { + return volumeFilterModeIgnore, nil + } + return volumeFilterModeExact, fmt.Errorf("invalid input mode '%s'", mode) +} + // 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 + mode volumeFilterMode // buyBaseAssetCapInBaseUnits *float64 // buyBaseAssetCapInQuoteUnits *float64 } @@ -133,41 +151,58 @@ func (f *volumeFilter) volumeFilterFn(dailyOTB *VolumeFilterConfig, dailyTBB *Vo 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 { + opToReturn := op + newAmountBeingSold := amountValueUnitsBeingSold 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), price=%.8f amount=%.8f, keep = (projectedSoldInBaseUnits) %.7f <= %.7f (config.SellBaseAssetCapInBaseUnits): keepSellingBase = %v", sellPrice, amountValueUnitsBeingSold, projectedSoldInBaseUnits, *f.config.SellBaseAssetCapInBaseUnits, keepSellingBase) + newAmountString := "" + if f.config.mode == volumeFilterModeExact && !keepSellingBase { + newAmount := *f.config.SellBaseAssetCapInBaseUnits - *dailyOTB.SellBaseAssetCapInBaseUnits - *dailyTBB.SellBaseAssetCapInBaseUnits + if newAmount > 0 { + newAmountBeingSold = newAmount + opToReturn.Amount = fmt.Sprintf("%.7f", newAmountBeingSold) + keepSellingBase = true + newAmountString = ", newAmountString = " + opToReturn.Amount + } + } + log.Printf("volumeFilter: selling (base units), price=%.8f amount=%.8f, keep = (projectedSoldInBaseUnits) %.7f <= %.7f (config.SellBaseAssetCapInBaseUnits): keepSellingBase = %v%s", sellPrice, amountValueUnitsBeingSold, projectedSoldInBaseUnits, *f.config.SellBaseAssetCapInBaseUnits, keepSellingBase, newAmountString) } else { keepSellingBase = true } if f.config.SellBaseAssetCapInQuoteUnits != nil { - projectedSoldInQuoteUnits := *dailyOTB.SellBaseAssetCapInQuoteUnits + *dailyTBB.SellBaseAssetCapInQuoteUnits + amountValueUnitsBeingBought + projectedSoldInQuoteUnits := *dailyOTB.SellBaseAssetCapInQuoteUnits + *dailyTBB.SellBaseAssetCapInQuoteUnits + (newAmountBeingSold * sellPrice) 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) + newAmountString := "" + if f.config.mode == volumeFilterModeExact && !keepSellingQuote { + newAmount := (*f.config.SellBaseAssetCapInQuoteUnits - *dailyOTB.SellBaseAssetCapInQuoteUnits - *dailyTBB.SellBaseAssetCapInQuoteUnits) / sellPrice + if newAmount > 0 { + newAmountBeingSold = newAmount + opToReturn.Amount = fmt.Sprintf("%.7f", newAmountBeingSold) + keepSellingQuote = true + newAmountString = ", newAmountString = " + opToReturn.Amount + } + } + log.Printf("volumeFilter: selling (quote units), price=%.8f amount=%.8f, keep = (projectedSoldInQuoteUnits) %.7f <= %.7f (config.SellBaseAssetCapInQuoteUnits): keepSellingQuote = %v%s", sellPrice, amountValueUnitsBeingSold, projectedSoldInQuoteUnits, *f.config.SellBaseAssetCapInQuoteUnits, keepSellingQuote, newAmountString) } else { keepSellingQuote = true } - keep = keepSellingBase && keepSellingQuote + if keepSellingBase && keepSellingQuote { + // update the dailyTBB to include the additional amounts so they can be used in the calculation of the next operation + *dailyTBB.SellBaseAssetCapInBaseUnits += newAmountBeingSold + *dailyTBB.SellBaseAssetCapInQuoteUnits += (newAmountBeingSold * sellPrice) + return opToReturn, true, nil + } } 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 @@ -178,7 +213,7 @@ func (f *volumeFilter) volumeFilterFn(dailyOTB *VolumeFilterConfig, dailyTBB *Vo 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) + return nil, false, fmt.Errorf("unable to transform manageOffer operation: offerID=%d, amount=%s, price=%.7f", op.OfferID, op.Amount, sellPrice) } func (c *VolumeFilterConfig) isEmpty() bool {