diff --git a/plugins/filterFactory.go b/plugins/filterFactory.go index 3ab4aa872..ae5f3918a 100644 --- a/plugins/filterFactory.go +++ b/plugins/filterFactory.go @@ -9,6 +9,7 @@ import ( hProtocol "github.com/stellar/go/protocols/horizon" "github.com/stellar/kelp/model" + "github.com/stellar/kelp/queries" ) var filterIDRegex *regexp.Regexp @@ -72,18 +73,20 @@ func filterVolume(f *FilterFactory, configInput string) (SubmitFilter, error) { } func makeRawVolumeFilterConfig( - sellBaseAssetCapInBaseUnits *float64, - sellBaseAssetCapInQuoteUnits *float64, + baseAssetCapInBaseUnits *float64, + baseAssetCapInQuoteUnits *float64, + action queries.DailyVolumeAction, mode volumeFilterMode, additionalMarketIDs []string, optionalAccountIDs []string, ) *VolumeFilterConfig { return &VolumeFilterConfig{ - SellBaseAssetCapInBaseUnits: sellBaseAssetCapInBaseUnits, - SellBaseAssetCapInQuoteUnits: sellBaseAssetCapInQuoteUnits, - mode: mode, - additionalMarketIDs: additionalMarketIDs, - optionalAccountIDs: optionalAccountIDs, + BaseAssetCapInBaseUnits: baseAssetCapInBaseUnits, + BaseAssetCapInQuoteUnits: baseAssetCapInQuoteUnits, + action: action, + mode: mode, + additionalMarketIDs: additionalMarketIDs, + optionalAccountIDs: optionalAccountIDs, } } @@ -104,6 +107,12 @@ func makeVolumeFilterConfig(configInput string) (*VolumeFilterConfig, error) { return nil, fmt.Errorf("invalid input (%s), the second part needs to equal or start with \"daily\"", configInput) } + action, e := queries.ParseDailyVolumeAction(parts[2]) + if e != nil { + return nil, fmt.Errorf("could not parse volume filter action from input (%s): %s", configInput, e) + } + config.action = action + errInvalid := fmt.Errorf("invalid input (%s), the modifier for \"daily\" can be either \"market_ids\" or \"account_ids\" like so 'daily:market_ids=[4c19915f47,db4531d586]' or 'daily:account_ids=[account1,account2]' or 'daily:market_ids=[4c19915f47,db4531d586]:account_ids=[account1,account2]'", configInput) if len(limitWindowParts) == 2 { e = addModifierToConfig(config, limitWindowParts[1]) @@ -124,17 +133,14 @@ func makeVolumeFilterConfig(configInput string) (*VolumeFilterConfig, error) { return nil, fmt.Errorf("invalid input (%s), the second part needs to be \"daily\" and can have only one modifier \"market_ids\" like so 'daily:market_ids=[4c19915f47,db4531d586]'", configInput) } - if parts[2] != "sell" { - return nil, fmt.Errorf("invalid input (%s), the third part needs to be \"sell\"", configInput) - } limit, e := strconv.ParseFloat(parts[4], 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[3] == "base" { - config.SellBaseAssetCapInBaseUnits = &limit + config.BaseAssetCapInBaseUnits = &limit } else if parts[3] == "quote" { - config.SellBaseAssetCapInQuoteUnits = &limit + config.BaseAssetCapInQuoteUnits = &limit } else { return nil, fmt.Errorf("invalid input (%s), the third part needs to be \"base\" or \"quote\"", configInput) } diff --git a/plugins/filterFactory_test.go b/plugins/filterFactory_test.go index f905cfce6..b639247f2 100644 --- a/plugins/filterFactory_test.go +++ b/plugins/filterFactory_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/openlyinc/pointy" + "github.com/stellar/kelp/queries" "github.com/stretchr/testify/assert" ) @@ -135,62 +136,90 @@ func TestMakeVolumeFilterConfig(t *testing.T) { wantError error wantConfig *VolumeFilterConfig }{ + // the first %s represents the action (buy or sell), the second %s represents mode (exact or ignore) + // we loop over the actions and modes below and inject them into the input and wantConfig { - configInput: "volume/daily/sell/base/3500.0/exact", + configInput: "volume/daily/%s/base/3500.0/%s", wantConfig: &VolumeFilterConfig{ - SellBaseAssetCapInBaseUnits: pointy.Float64(3500.0), - SellBaseAssetCapInQuoteUnits: nil, - mode: volumeFilterModeExact, - additionalMarketIDs: nil, - optionalAccountIDs: nil, + BaseAssetCapInBaseUnits: pointy.Float64(3500.0), + BaseAssetCapInQuoteUnits: nil, + additionalMarketIDs: nil, + optionalAccountIDs: nil, }, }, { - configInput: "volume/daily/sell/quote/1000.0/ignore", + configInput: "volume/daily/%s/quote/4000.0/%s", wantConfig: &VolumeFilterConfig{ - SellBaseAssetCapInBaseUnits: nil, - SellBaseAssetCapInQuoteUnits: pointy.Float64(1000.0), - mode: volumeFilterModeIgnore, - additionalMarketIDs: nil, - optionalAccountIDs: nil, + BaseAssetCapInBaseUnits: nil, + BaseAssetCapInQuoteUnits: pointy.Float64(4000.0), + additionalMarketIDs: nil, + optionalAccountIDs: nil, + }, + }, + { + configInput: "volume/daily/%s/base/3500.0/%s", + wantConfig: &VolumeFilterConfig{ + BaseAssetCapInBaseUnits: pointy.Float64(3500.0), + BaseAssetCapInQuoteUnits: nil, + additionalMarketIDs: nil, + optionalAccountIDs: nil, + }, + }, { + configInput: "volume/daily/%s/quote/1000.0/%s", + wantConfig: &VolumeFilterConfig{ + BaseAssetCapInBaseUnits: nil, + BaseAssetCapInQuoteUnits: pointy.Float64(1000.0), + additionalMarketIDs: nil, + optionalAccountIDs: nil, }, }, { - configInput: "volume/daily:market_ids=[4c19915f47,db4531d586]/sell/base/3500.0/exact", + configInput: "volume/daily:market_ids=[4c19915f47,db4531d586]/%s/base/3500.0/%s", wantConfig: &VolumeFilterConfig{ - SellBaseAssetCapInBaseUnits: pointy.Float64(3500.0), - SellBaseAssetCapInQuoteUnits: nil, - mode: volumeFilterModeExact, - additionalMarketIDs: []string{"4c19915f47", "db4531d586"}, - optionalAccountIDs: nil, + BaseAssetCapInBaseUnits: pointy.Float64(3500.0), + BaseAssetCapInQuoteUnits: nil, + additionalMarketIDs: []string{"4c19915f47", "db4531d586"}, + optionalAccountIDs: nil, }, }, { - configInput: "volume/daily:account_ids=[account1,account2]/sell/base/3500.0/exact", + configInput: "volume/daily:account_ids=[account1,account2]/%s/base/3500.0/%s", wantConfig: &VolumeFilterConfig{ - SellBaseAssetCapInBaseUnits: pointy.Float64(3500.0), - SellBaseAssetCapInQuoteUnits: nil, - mode: volumeFilterModeExact, - additionalMarketIDs: nil, - optionalAccountIDs: []string{"account1", "account2"}, + BaseAssetCapInBaseUnits: pointy.Float64(3500.0), + BaseAssetCapInQuoteUnits: nil, + additionalMarketIDs: nil, + optionalAccountIDs: []string{"account1", "account2"}, }, }, { - configInput: "volume/daily:market_ids=[4c19915f47,db4531d586]:account_ids=[account1,account2]/sell/base/3500.0/exact", + configInput: "volume/daily:market_ids=[4c19915f47,db4531d586]:account_ids=[account1,account2]/%s/base/3500.0/%s", wantConfig: &VolumeFilterConfig{ - SellBaseAssetCapInBaseUnits: pointy.Float64(3500.0), - SellBaseAssetCapInQuoteUnits: nil, - mode: volumeFilterModeExact, - additionalMarketIDs: []string{"4c19915f47", "db4531d586"}, - optionalAccountIDs: []string{"account1", "account2"}, + BaseAssetCapInBaseUnits: pointy.Float64(3500.0), + BaseAssetCapInQuoteUnits: nil, + additionalMarketIDs: []string{"4c19915f47", "db4531d586"}, + optionalAccountIDs: []string{"account1", "account2"}, }, }, } + modes := []volumeFilterMode{volumeFilterModeExact, volumeFilterModeIgnore} + actions := []queries.DailyVolumeAction{queries.DailyVolumeActionBuy, queries.DailyVolumeActionSell} for _, k := range testCases { - t.Run(k.configInput, func(t *testing.T) { - actual, e := makeVolumeFilterConfig(k.configInput) - if !assert.NoError(t, e) { - return + // loop over both modes, and inject the desired mode in the config + for _, m := range modes { + wantConfig := k.wantConfig + wantConfig.mode = m + + // loop over both actions, and inject the desired action in the config + for _, a := range actions { + wantConfig.action = a + configInput := fmt.Sprintf(k.configInput, a, m) + + t.Run(configInput, func(t *testing.T) { + actual, e := makeVolumeFilterConfig(configInput) + if !assert.NoError(t, e) { + return + } + assertVolumeFilterConfigEqual(t, wantConfig, actual) + }) } - assertVolumeFilterConfigEqual(t, k.wantConfig, actual) - }) + } } } @@ -200,8 +229,9 @@ func assertVolumeFilterConfigEqual(t *testing.T, want *VolumeFilterConfig, actua } else if actual == nil { assert.Fail(t, fmt.Sprintf("actual was nil but expected %v", *want)) } else { - assert.Equal(t, want.SellBaseAssetCapInBaseUnits, actual.SellBaseAssetCapInBaseUnits) - assert.Equal(t, want.SellBaseAssetCapInQuoteUnits, actual.SellBaseAssetCapInQuoteUnits) + assert.Equal(t, want.BaseAssetCapInBaseUnits, actual.BaseAssetCapInBaseUnits) + assert.Equal(t, want.BaseAssetCapInQuoteUnits, actual.BaseAssetCapInQuoteUnits) + assert.Equal(t, want.action, actual.action) assert.Equal(t, want.mode, actual.mode) assert.Equal(t, want.additionalMarketIDs, actual.additionalMarketIDs) assert.Equal(t, want.optionalAccountIDs, actual.optionalAccountIDs) diff --git a/plugins/volumeFilter.go b/plugins/volumeFilter.go index 002236e03..a5ca82ad8 100644 --- a/plugins/volumeFilter.go +++ b/plugins/volumeFilter.go @@ -35,19 +35,18 @@ func parseVolumeFilterMode(mode string) (volumeFilterMode, error) { // 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 - additionalMarketIDs []string - optionalAccountIDs []string - // buyBaseAssetCapInBaseUnits *float64 - // buyBaseAssetCapInQuoteUnits *float64 + BaseAssetCapInBaseUnits *float64 + BaseAssetCapInQuoteUnits *float64 + action queries.DailyVolumeAction + mode volumeFilterMode + additionalMarketIDs []string + optionalAccountIDs []string } type limitParameters struct { - sellBaseAssetCapInBaseUnits *float64 - sellBaseAssetCapInQuoteUnits *float64 - mode volumeFilterMode + baseAssetCapInBaseUnits *float64 + baseAssetCapInQuoteUnits *float64 + mode volumeFilterMode } type volumeFilter struct { @@ -82,7 +81,7 @@ func makeFilterVolume( marketID := MakeMarketID(exchangeName, baseAssetString, quoteAssetString) marketIDs := utils.Dedupe(append([]string{marketID}, config.additionalMarketIDs...)) - dailyVolumeByDateQuery, e := queries.MakeDailyVolumeByDateForMarketIdsAction(db, marketIDs, "sell", config.optionalAccountIDs) + dailyVolumeByDateQuery, e := queries.MakeDailyVolumeByDateForMarketIdsAction(db, marketIDs, config.action, config.optionalAccountIDs) if e != nil { return nil, fmt.Errorf("could not make daily volume by date Query: %s", e) } @@ -111,8 +110,8 @@ func (c *VolumeFilterConfig) Validate() error { // String is the stringer method func (c *VolumeFilterConfig) String() string { - return fmt.Sprintf("VolumeFilterConfig[SellBaseAssetCapInBaseUnits=%s, SellBaseAssetCapInQuoteUnits=%s, mode=%s, additionalMarketIDs=%v, optionalAccountIDs=%v]", - utils.CheckedFloatPtr(c.SellBaseAssetCapInBaseUnits), utils.CheckedFloatPtr(c.SellBaseAssetCapInQuoteUnits), c.mode, c.additionalMarketIDs, c.optionalAccountIDs) + return fmt.Sprintf("VolumeFilterConfig[BaseAssetCapInBaseUnits=%s, BaseAssetCapInQuoteUnits=%s, mode=%s, action=%s, additionalMarketIDs=%v, optionalAccountIDs=%v]", + utils.CheckedFloatPtr(c.BaseAssetCapInBaseUnits), utils.CheckedFloatPtr(c.BaseAssetCapInQuoteUnits), c.mode, c.action, c.additionalMarketIDs, c.optionalAccountIDs) } func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol.Offer, buyingOffers []hProtocol.Offer) ([]txnbuild.Operation, error) { @@ -132,22 +131,22 @@ func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol // daily on-the-books dailyOTB := &VolumeFilterConfig{ - SellBaseAssetCapInBaseUnits: &dailyValuesBaseSold.BaseVol, - SellBaseAssetCapInQuoteUnits: &dailyValuesBaseSold.QuoteVol, + BaseAssetCapInBaseUnits: &dailyValuesBaseSold.BaseVol, + BaseAssetCapInQuoteUnits: &dailyValuesBaseSold.QuoteVol, } // daily to-be-booked starts out as empty and accumulates the values of the operations - dailyTbbSellBase := 0.0 + dailyTbbBase := 0.0 dailyTbbSellQuote := 0.0 dailyTBB := &VolumeFilterConfig{ - SellBaseAssetCapInBaseUnits: &dailyTbbSellBase, - SellBaseAssetCapInQuoteUnits: &dailyTbbSellQuote, + BaseAssetCapInBaseUnits: &dailyTbbBase, + BaseAssetCapInQuoteUnits: &dailyTbbSellQuote, } innerFn := func(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, error) { limitParameters := limitParameters{ - sellBaseAssetCapInBaseUnits: f.config.SellBaseAssetCapInBaseUnits, - sellBaseAssetCapInQuoteUnits: f.config.SellBaseAssetCapInQuoteUnits, - mode: f.config.mode, + baseAssetCapInBaseUnits: f.config.BaseAssetCapInBaseUnits, + baseAssetCapInQuoteUnits: f.config.BaseAssetCapInQuoteUnits, + mode: f.config.mode, } return volumeFilterFn(dailyOTB, dailyTBB, op, f.baseAsset, f.quoteAsset, limitParameters) } @@ -179,12 +178,12 @@ func volumeFilterFn(dailyOTB *VolumeFilterConfig, dailyTBBAccumulator *VolumeFil newAmountBeingSold := amountValueUnitsBeingSold var keepSellingBase bool var keepSellingQuote bool - if lp.sellBaseAssetCapInBaseUnits != nil { - projectedSoldInBaseUnits := *dailyOTB.SellBaseAssetCapInBaseUnits + *dailyTBBAccumulator.SellBaseAssetCapInBaseUnits + amountValueUnitsBeingSold - keepSellingBase = projectedSoldInBaseUnits <= *lp.sellBaseAssetCapInBaseUnits + if lp.baseAssetCapInBaseUnits != nil { + projectedSoldInBaseUnits := *dailyOTB.BaseAssetCapInBaseUnits + *dailyTBBAccumulator.BaseAssetCapInBaseUnits + amountValueUnitsBeingSold + keepSellingBase = projectedSoldInBaseUnits <= *lp.baseAssetCapInBaseUnits newAmountString := "" if lp.mode == volumeFilterModeExact && !keepSellingBase { - newAmount := *lp.sellBaseAssetCapInBaseUnits - *dailyOTB.SellBaseAssetCapInBaseUnits - *dailyTBBAccumulator.SellBaseAssetCapInBaseUnits + newAmount := *lp.baseAssetCapInBaseUnits - *dailyOTB.BaseAssetCapInBaseUnits - *dailyTBBAccumulator.BaseAssetCapInBaseUnits if newAmount > 0 { newAmountBeingSold = newAmount opToReturn.Amount = fmt.Sprintf("%.7f", newAmountBeingSold) @@ -192,17 +191,17 @@ func volumeFilterFn(dailyOTB *VolumeFilterConfig, dailyTBBAccumulator *VolumeFil 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, *lp.sellBaseAssetCapInBaseUnits, keepSellingBase, newAmountString) + log.Printf("volumeFilter: selling (base units), price=%.8f amount=%.8f, keep = (projectedSoldInBaseUnits) %.7f <= %.7f (config.BaseAssetCapInBaseUnits): keepSellingBase = %v%s", sellPrice, amountValueUnitsBeingSold, projectedSoldInBaseUnits, *lp.baseAssetCapInBaseUnits, keepSellingBase, newAmountString) } else { keepSellingBase = true } - if lp.sellBaseAssetCapInQuoteUnits != nil { - projectedSoldInQuoteUnits := *dailyOTB.SellBaseAssetCapInQuoteUnits + *dailyTBBAccumulator.SellBaseAssetCapInQuoteUnits + (newAmountBeingSold * sellPrice) - keepSellingQuote = projectedSoldInQuoteUnits <= *lp.sellBaseAssetCapInQuoteUnits + if lp.baseAssetCapInQuoteUnits != nil { + projectedSoldInQuoteUnits := *dailyOTB.BaseAssetCapInQuoteUnits + *dailyTBBAccumulator.BaseAssetCapInQuoteUnits + (newAmountBeingSold * sellPrice) + keepSellingQuote = projectedSoldInQuoteUnits <= *lp.baseAssetCapInQuoteUnits newAmountString := "" if lp.mode == volumeFilterModeExact && !keepSellingQuote { - newAmount := (*lp.sellBaseAssetCapInQuoteUnits - *dailyOTB.SellBaseAssetCapInQuoteUnits - *dailyTBBAccumulator.SellBaseAssetCapInQuoteUnits) / sellPrice + newAmount := (*lp.baseAssetCapInQuoteUnits - *dailyOTB.BaseAssetCapInQuoteUnits - *dailyTBBAccumulator.BaseAssetCapInQuoteUnits) / sellPrice if newAmount > 0 { newAmountBeingSold = newAmount opToReturn.Amount = fmt.Sprintf("%.7f", newAmountBeingSold) @@ -210,19 +209,19 @@ func volumeFilterFn(dailyOTB *VolumeFilterConfig, dailyTBBAccumulator *VolumeFil 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, *lp.sellBaseAssetCapInQuoteUnits, keepSellingQuote, newAmountString) + log.Printf("volumeFilter: selling (quote units), price=%.8f amount=%.8f, keep = (projectedSoldInQuoteUnits) %.7f <= %.7f (config.BaseAssetCapInQuoteUnits): keepSellingQuote = %v%s", sellPrice, amountValueUnitsBeingSold, projectedSoldInQuoteUnits, *lp.baseAssetCapInQuoteUnits, keepSellingQuote, newAmountString) } else { keepSellingQuote = true } if keepSellingBase && keepSellingQuote { // update the dailyTBB to include the additional amounts so they can be used in the calculation of the next operation - *dailyTBBAccumulator.SellBaseAssetCapInBaseUnits += newAmountBeingSold - *dailyTBBAccumulator.SellBaseAssetCapInQuoteUnits += (newAmountBeingSold * sellPrice) + *dailyTBBAccumulator.BaseAssetCapInBaseUnits += newAmountBeingSold + *dailyTBBAccumulator.BaseAssetCapInQuoteUnits += (newAmountBeingSold * sellPrice) return opToReturn, nil } } else { - // TODO buying side + // TODO buying side - we need to implement this to support buy side filters; extract common logic from the above sell side case } // we don't want to keep it so return the dropped command @@ -240,25 +239,19 @@ func (f *volumeFilter) isSellingBase() bool { } func (f *volumeFilter) mustGetBaseAssetCapInBaseUnits() (float64, error) { - value := f.config.SellBaseAssetCapInBaseUnits + value := f.config.BaseAssetCapInBaseUnits if value == nil { - return 0.0, fmt.Errorf("SellBaseAssetCapInBaseUnits is nil, config = %v", f.config) + return 0.0, fmt.Errorf("BaseAssetCapInBaseUnits is nil, config = %v", f.config) } return *value, nil } func (c *VolumeFilterConfig) isEmpty() bool { - if c.SellBaseAssetCapInBaseUnits != nil { + if c.BaseAssetCapInBaseUnits != nil { return false } - if c.SellBaseAssetCapInQuoteUnits != nil { + if c.BaseAssetCapInQuoteUnits != nil { return false } - // if buyBaseAssetCapInBaseUnits != nil { - // return false - // } - // if buyBaseAssetCapInQuoteUnits != nil { - // return false - // } return true } diff --git a/plugins/volumeFilter_test.go b/plugins/volumeFilter_test.go index 486893427..f3eb9f49f 100644 --- a/plugins/volumeFilter_test.go +++ b/plugins/volumeFilter_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" ) -func makeWantVolumeFilter(config *VolumeFilterConfig, marketIDs []string, accountIDs []string, action string) *volumeFilter { +func makeWantVolumeFilter(config *VolumeFilterConfig, marketIDs []string, accountIDs []string, action queries.DailyVolumeAction) *volumeFilter { query, e := queries.MakeDailyVolumeByDateForMarketIdsAction(&sql.DB{}, marketIDs, action, accountIDs) if e != nil { panic(e) @@ -100,48 +100,54 @@ func TestMakeFilterVolume(t *testing.T) { for _, k := range testCases { // this lets us test both types of modes when varying the market and account ids for _, m := range modes { - // this lets us run the for-loop below for both base and quote units within the config - baseCapInBaseConfig := makeRawVolumeFilterConfig( - pointy.Float64(1.0), - nil, - m, - k.marketIDs, - k.accountIDs, - ) - baseCapInQuoteConfig := makeRawVolumeFilterConfig( - nil, - pointy.Float64(1.0), - m, - k.marketIDs, - k.accountIDs, - ) - for _, config := range []*VolumeFilterConfig{baseCapInBaseConfig, baseCapInQuoteConfig} { - // configType is used to represent the type of config when printing test name - configType := "quote" - if config.SellBaseAssetCapInBaseUnits != nil { - configType = "base" - } + // this lets us test both buy and sell + // TODO DS Add buy action + for _, action := range []queries.DailyVolumeAction{queries.DailyVolumeActionSell} { + // this lets us run the for-loop below for both base and quote units within the config + baseCapInBaseConfig := makeRawVolumeFilterConfig( + pointy.Float64(1.0), + nil, + action, + m, + k.marketIDs, + k.accountIDs, + ) + baseCapInQuoteConfig := makeRawVolumeFilterConfig( + nil, + pointy.Float64(1.0), + action, + m, + k.marketIDs, + k.accountIDs, + ) + for _, config := range []*VolumeFilterConfig{baseCapInBaseConfig, baseCapInQuoteConfig} { + // configType is used to represent the type of config when printing test name + configType := "quote" + if config.BaseAssetCapInBaseUnits != nil { + configType = "base" + } - // TODO DS Vary filter action between buy and sell, once buy logic is implemented. - wantFilter := makeWantVolumeFilter(config, k.wantMarketIDs, k.accountIDs, "sell") - t.Run(fmt.Sprintf("%s/%s/%s", k.name, configType, m), func(t *testing.T) { - actual, e := makeFilterVolume( - configValue, - k.exchangeName, - tradingPair, - testAssetDisplayFn, - utils.NativeAsset, - utils.NativeAsset, - &sql.DB{}, - config, - ) + // TODO DS Vary filter action between buy and sell, once buy logic is implemented. + wantFilter := makeWantVolumeFilter(config, k.wantMarketIDs, k.accountIDs, action) + t.Run(fmt.Sprintf("%s/%s/%s", k.name, configType, m), func(t *testing.T) { + actual, e := makeFilterVolume( + configValue, + k.exchangeName, + tradingPair, + testAssetDisplayFn, + utils.NativeAsset, + utils.NativeAsset, + &sql.DB{}, + config, + ) - if !assert.Nil(t, e) { - return - } + if !assert.Nil(t, e) { + return + } - assert.Equal(t, wantFilter, actual) - }) + assert.Equal(t, wantFilter, actual) + }) + } } } } @@ -281,36 +287,37 @@ func TestVolumeFilterFn(t *testing.T) { accountIDs := []string{} for _, k := range testCases { - t.Run(k.name, func(t *testing.T) { - // exactly one of the two cap values must be set - if k.sellBaseCapInBase == nil && k.sellBaseCapInQuote == nil { - assert.Fail(t, "either one of the two cap values must be set") - return - } - - if k.sellBaseCapInBase != nil && k.sellBaseCapInQuote != nil { - assert.Fail(t, "both of the cap values cannot be set") - return - } + for _, action := range []queries.DailyVolumeAction{queries.DailyVolumeActionSell} { + t.Run(k.name, func(t *testing.T) { + // exactly one of the two cap values must be set + if k.sellBaseCapInBase == nil && k.sellBaseCapInQuote == nil { + assert.Fail(t, "either one of the two cap values must be set") + return + } - dailyOTB := makeRawVolumeFilterConfig(k.otbBase, k.otbQuote, k.mode, marketIDs, accountIDs) - dailyTBBAccumulator := makeRawVolumeFilterConfig(k.tbbBase, k.tbbQuote, k.mode, marketIDs, accountIDs) - lp := limitParameters{ - sellBaseAssetCapInBaseUnits: k.sellBaseCapInBase, - sellBaseAssetCapInQuoteUnits: k.sellBaseCapInQuote, - mode: k.mode, - } + if k.sellBaseCapInBase != nil && k.sellBaseCapInQuote != nil { + assert.Fail(t, "both of the cap values cannot be set") + return + } - actual, e := volumeFilterFn(dailyOTB, dailyTBBAccumulator, k.inputOp, utils.NativeAsset, utils.NativeAsset, lp) - if !assert.Nil(t, e) { - return - } - assert.Equal(t, k.wantOp, actual) + dailyOTB := makeRawVolumeFilterConfig(k.otbBase, k.otbQuote, action, k.mode, marketIDs, accountIDs) + dailyTBBAccumulator := makeRawVolumeFilterConfig(k.tbbBase, k.tbbQuote, action, k.mode, marketIDs, accountIDs) + lp := limitParameters{ + baseAssetCapInBaseUnits: k.sellBaseCapInBase, + baseAssetCapInQuoteUnits: k.sellBaseCapInQuote, + mode: k.mode, + } - wantTBBAccumulator := makeRawVolumeFilterConfig(k.wantTbbBase, k.wantTbbQuote, k.mode, marketIDs, accountIDs) - assert.Equal(t, wantTBBAccumulator, dailyTBBAccumulator) - }) + actual, e := volumeFilterFn(dailyOTB, dailyTBBAccumulator, k.inputOp, utils.NativeAsset, utils.NativeAsset, lp) + if !assert.Nil(t, e) { + return + } + assert.Equal(t, k.wantOp, actual) + wantTBBAccumulator := makeRawVolumeFilterConfig(k.wantTbbBase, k.wantTbbQuote, action, k.mode, marketIDs, accountIDs) + assert.Equal(t, wantTBBAccumulator, dailyTBBAccumulator) + }) + } } } diff --git a/queries/dailyVolumeByDate.go b/queries/dailyVolumeByDate.go index cf1516c1c..07f08dcdc 100644 --- a/queries/dailyVolumeByDate.go +++ b/queries/dailyVolumeByDate.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/stellar/kelp/api" + "github.com/stellar/kelp/support/utils" ) // sqlQueryDailyValuesTemplateAllAccounts queries the trades table to get the values for a given day @@ -14,11 +15,35 @@ const sqlQueryDailyValuesTemplateAllAccounts = "SELECT SUM(base_volume) as total // sqlQueryDailyValuesTemplateSpecificAccounts queries the trades table to get the values for a given day filtered by specific accounts const sqlQueryDailyValuesTemplateSpecificAccounts = "SELECT SUM(base_volume) as total_base_volume, SUM(counter_cost) as total_counter_volume FROM trades WHERE market_id IN (%s) AND account_id IN (%s) AND DATE(date_utc) = $1 and action = $2 group by DATE(date_utc)" +// DailyVolumeAction represents either a sell or a buy +type DailyVolumeAction string + +// type of DailyVolumeAction +const ( + DailyVolumeActionBuy DailyVolumeAction = "buy" + DailyVolumeActionSell DailyVolumeAction = "sell" +) + +// String is the Stringer method impl +func (a DailyVolumeAction) String() string { + return string(a) +} + +// ParseDailyVolumeAction converts a string to a DailyVolumeAction +func ParseDailyVolumeAction(action string) (DailyVolumeAction, error) { + if action == DailyVolumeActionBuy.String() { + return DailyVolumeActionBuy, nil + } else if action == DailyVolumeActionSell.String() { + return DailyVolumeActionSell, nil + } + return DailyVolumeActionSell, fmt.Errorf("invalid action value '%s'", action) +} + // DailyVolumeByDate is a query that fetches the daily volume of sales type DailyVolumeByDate struct { db *sql.DB sqlQuery string - action string + action DailyVolumeAction } var _ api.Query = &DailyVolumeByDate{} @@ -33,10 +58,11 @@ type DailyVolume struct { func MakeDailyVolumeByDateForMarketIdsAction( db *sql.DB, marketIDs []string, - action string, + action DailyVolumeAction, optionalAccountIDs []string, ) (*DailyVolumeByDate, error) { if db == nil { + utils.PrintErrorHintf("the provided POSTGRES_DB config in the trader.cfg file should be non-nil") return nil, fmt.Errorf("the provided db should be non-nil") } @@ -61,7 +87,7 @@ func (q *DailyVolumeByDate) QueryRow(args ...interface{}) (interface{}, error) { return nil, fmt.Errorf("input arg needs to be of type 'string', but was of type '%T'", args[0]) } - row := q.db.QueryRow(q.sqlQuery, args[0], q.action) + row := q.db.QueryRow(q.sqlQuery, args[0], q.action.String()) var baseVol sql.NullFloat64 var quoteVol sql.NullFloat64 diff --git a/queries/dailyVolumeByDate_test.go b/queries/dailyVolumeByDate_test.go index 34ce15586..49a07efdf 100644 --- a/queries/dailyVolumeByDate_test.go +++ b/queries/dailyVolumeByDate_test.go @@ -39,6 +39,7 @@ func connectTestDb() *sql.DB { func TestDailyVolumeByDate_QueryRow(t *testing.T) { testCases := []struct { + action DailyVolumeAction queryByOptionalAccountIDs []string wantYesterdayBase float64 wantYesterdayQuote float64 @@ -47,7 +48,9 @@ func TestDailyVolumeByDate_QueryRow(t *testing.T) { wantTomorrowBase float64 wantTomorrowQuote float64 }{ + // TODO DS add case for buy base/quote and add trade data in test below accordingly { + action: DailyVolumeActionSell, queryByOptionalAccountIDs: []string{}, // accountID1 and accountID2 are the only ones that exists wantYesterdayBase: 100.0, wantYesterdayQuote: 10.0, @@ -56,6 +59,7 @@ func TestDailyVolumeByDate_QueryRow(t *testing.T) { wantTomorrowBase: 102.0, wantTomorrowQuote: 12.24, }, { + action: DailyVolumeActionSell, queryByOptionalAccountIDs: []string{"accountID1", "accountID2"}, // accountID1 and accountID2 are the only ones that exists wantYesterdayBase: 100.0, wantYesterdayQuote: 10.0, @@ -64,6 +68,7 @@ func TestDailyVolumeByDate_QueryRow(t *testing.T) { wantTomorrowBase: 102.0, wantTomorrowQuote: 12.24, }, { + action: DailyVolumeActionSell, queryByOptionalAccountIDs: []string{"accountID1"}, // accountID1 has most of the entries wantYesterdayBase: 100.0, wantYesterdayQuote: 10.0, @@ -72,6 +77,7 @@ func TestDailyVolumeByDate_QueryRow(t *testing.T) { wantTomorrowBase: 102.0, wantTomorrowQuote: 12.24, }, { + action: DailyVolumeActionSell, queryByOptionalAccountIDs: []string{"accountID2"}, //accountID2 has only one entry, which is for today wantYesterdayBase: 0.0, wantYesterdayQuote: 0.0, @@ -80,6 +86,7 @@ func TestDailyVolumeByDate_QueryRow(t *testing.T) { wantTomorrowBase: 0.0, wantTomorrowQuote: 0.0, }, { + action: DailyVolumeActionSell, queryByOptionalAccountIDs: []string{"accountID2", "accountID2"}, // duplicate accountIDs should return same as previous test case wantYesterdayBase: 0.0, wantYesterdayQuote: 0.0, @@ -88,6 +95,7 @@ func TestDailyVolumeByDate_QueryRow(t *testing.T) { wantTomorrowBase: 0.0, wantTomorrowQuote: 0.0, }, { + action: DailyVolumeActionSell, queryByOptionalAccountIDs: []string{"accountID3"}, //accountID3 does not exist wantYesterdayBase: 0.0, wantYesterdayQuote: 0.0, @@ -98,113 +106,113 @@ func TestDailyVolumeByDate_QueryRow(t *testing.T) { }, } - for _, k := range testCases { - t.Run(strings.Replace(fmt.Sprintf("%v", k.queryByOptionalAccountIDs), " ", "_", -1), func(t *testing.T) { - // setup db - yesterday, _ := time.Parse(time.RFC3339, "2020-01-20T15:00:00Z") - today, _ := time.Parse(time.RFC3339, "2020-01-21T15:00:00Z") - tomorrow, _ := time.Parse(time.RFC3339, "2020-01-22T15:00:00Z") - setupStatements := []string{ - kelpdb.SqlTradesTableCreate, - "ALTER TABLE trades DROP COLUMN IF EXISTS account_id", - "ALTER TABLE trades DROP COLUMN IF EXISTS order_id", - kelpdb.SqlTradesTableAlter1, - kelpdb.SqlTradesTableAlter2, - "DELETE FROM trades", // clear table - fmt.Sprintf(kelpdb.SqlTradesInsertTemplate, - "market1", - "1", - yesterday.Format(postgresdb.TimestampFormatString), - model.OrderActionSell.String(), - model.OrderTypeLimit.String(), - 0.10, // price - 100.0, // volume - 10.0, // cost - 0.0, // fee - "accountID1", - "", - ), - fmt.Sprintf(kelpdb.SqlTradesInsertTemplate, - "market1", - "2", - today.Format(postgresdb.TimestampFormatString), - model.OrderActionSell.String(), - model.OrderTypeLimit.String(), - 0.11, // price - 101.0, // volume - 11.11, // cost - 0.0, // fee - "accountID1", - "oid1", - ), - fmt.Sprintf(kelpdb.SqlTradesInsertTemplate, - "market1", - "3", - today.Add(time.Second*1).Format(postgresdb.TimestampFormatString), - model.OrderActionSell.String(), - model.OrderTypeLimit.String(), - 0.12, // price - 6.0, // volume - 0.72, // cost - 0.10, // fee - "accountID1", - "", - ), - fmt.Sprintf(kelpdb.SqlTradesInsertTemplate, - "market1", - "4", - tomorrow.Format(postgresdb.TimestampFormatString), - model.OrderActionSell.String(), - model.OrderTypeLimit.String(), - 0.12, // price - 102.0, // volume - 12.24, // cost - 0.0, // fee - "accountID1", - "", - ), - fmt.Sprintf(kelpdb.SqlTradesInsertTemplate, - "market1", - "5", - tomorrow.Add(time.Second*1).Format(postgresdb.TimestampFormatString), - model.OrderActionBuy.String(), - model.OrderTypeLimit.String(), - 0.12, // price - 102.0, // volume - 12.24, // cost - 0.0, // fee - "accountID1", - "", - ), - // add an extra one for accountID2 - fmt.Sprintf(kelpdb.SqlTradesInsertTemplate, - "market1", - "6", - today.Format(postgresdb.TimestampFormatString), - model.OrderActionSell.String(), - model.OrderTypeLimit.String(), - 0.10, // price - 100.0, // volume - 10.0, // cost - 0.0, // fee - "accountID2", - "", - ), - } - db := connectTestDb() - defer db.Close() - for _, s := range setupStatements { - _, e := db.Exec(s) - if e != nil { - panic(e) - } - } + // setup db + yesterday, _ := time.Parse(time.RFC3339, "2020-01-20T15:00:00Z") + today, _ := time.Parse(time.RFC3339, "2020-01-21T15:00:00Z") + tomorrow, _ := time.Parse(time.RFC3339, "2020-01-22T15:00:00Z") + setupStatements := []string{ + kelpdb.SqlTradesTableCreate, + "ALTER TABLE trades DROP COLUMN IF EXISTS account_id", + "ALTER TABLE trades DROP COLUMN IF EXISTS order_id", + kelpdb.SqlTradesTableAlter1, + kelpdb.SqlTradesTableAlter2, + "DELETE FROM trades", // clear table + fmt.Sprintf(kelpdb.SqlTradesInsertTemplate, + "market1", + "1", + yesterday.Format(postgresdb.TimestampFormatString), + model.OrderActionSell.String(), + model.OrderTypeLimit.String(), + 0.10, // price + 100.0, // volume + 10.0, // cost + 0.0, // fee + "accountID1", + "", + ), + fmt.Sprintf(kelpdb.SqlTradesInsertTemplate, + "market1", + "2", + today.Format(postgresdb.TimestampFormatString), + model.OrderActionSell.String(), + model.OrderTypeLimit.String(), + 0.11, // price + 101.0, // volume + 11.11, // cost + 0.0, // fee + "accountID1", + "oid1", + ), + fmt.Sprintf(kelpdb.SqlTradesInsertTemplate, + "market1", + "3", + today.Add(time.Second*1).Format(postgresdb.TimestampFormatString), + model.OrderActionSell.String(), + model.OrderTypeLimit.String(), + 0.12, // price + 6.0, // volume + 0.72, // cost + 0.10, // fee + "accountID1", + "", + ), + fmt.Sprintf(kelpdb.SqlTradesInsertTemplate, + "market1", + "4", + tomorrow.Format(postgresdb.TimestampFormatString), + model.OrderActionSell.String(), + model.OrderTypeLimit.String(), + 0.12, // price + 102.0, // volume + 12.24, // cost + 0.0, // fee + "accountID1", + "", + ), + fmt.Sprintf(kelpdb.SqlTradesInsertTemplate, + "market1", + "5", + tomorrow.Add(time.Second*1).Format(postgresdb.TimestampFormatString), + model.OrderActionBuy.String(), + model.OrderTypeLimit.String(), + 0.12, // price + 102.0, // volume + 12.24, // cost + 0.0, // fee + "accountID1", + "", + ), + // add an extra one for accountID2 + fmt.Sprintf(kelpdb.SqlTradesInsertTemplate, + "market1", + "6", + today.Format(postgresdb.TimestampFormatString), + model.OrderActionSell.String(), + model.OrderTypeLimit.String(), + 0.10, // price + 100.0, // volume + 10.0, // cost + 0.0, // fee + "accountID2", + "", + ), + } + db := connectTestDb() + defer db.Close() + for _, s := range setupStatements { + _, e := db.Exec(s) + if e != nil { + panic(e) + } + } + for _, k := range testCases { + t.Run(strings.Replace(fmt.Sprintf("%v/%s", k.queryByOptionalAccountIDs, k.action), " ", "_", -1), func(t *testing.T) { // make query being tested dailyVolumeByDateQuery, e := MakeDailyVolumeByDateForMarketIdsAction( db, []string{"market1"}, - "sell", + k.action, k.queryByOptionalAccountIDs, ) if !assert.NoError(t, e) { diff --git a/queries/strategyMirrorTradeTriggerExists.go b/queries/strategyMirrorTradeTriggerExists.go index 0afd5278c..a927eca1b 100644 --- a/queries/strategyMirrorTradeTriggerExists.go +++ b/queries/strategyMirrorTradeTriggerExists.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/stellar/kelp/api" + "github.com/stellar/kelp/support/utils" ) // sqlQueryStrategyMirrorTradeTriggerExists queries the strategy_mirror_trade_triggers table by market_id and txid (primary key) to see if the row exists @@ -23,6 +24,7 @@ var _ api.Query = &StrategyMirrorTradeTriggerExists{} // MakeStrategyMirrorTradeTriggerExists makes the StrategyMirrorTradeTriggerExists query func MakeStrategyMirrorTradeTriggerExists(db *sql.DB, marketID string) (*StrategyMirrorTradeTriggerExists, error) { if db == nil { + utils.PrintErrorHintf("the provided POSTGRES_DB config in the trader.cfg file should be non-nil") return nil, fmt.Errorf("the provided db should be non-nil") }