Skip to content
This repository has been archived by the owner on Feb 1, 2024. It is now read-only.

Add tests for the volume filter (part of #483) #552

Merged
merged 19 commits into from Nov 12, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 27 additions & 13 deletions plugins/volumeFilter.go
Expand Up @@ -44,6 +44,12 @@ type VolumeFilterConfig struct {
// buyBaseAssetCapInQuoteUnits *float64
}

type limitParameters struct {
sellBaseAssetCapInBaseUnits *float64
sellBaseAssetCapInQuoteUnits *float64
mode volumeFilterMode
}

type volumeFilter struct {
name string
configValue string
Expand Down Expand Up @@ -73,13 +79,16 @@ func makeFilterVolume(
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)
marketIDs := utils.Dedupe(append([]string{marketID}, config.additionalMarketIDs...))
dailyVolumeByDateQuery, e := queries.MakeDailyVolumeByDateForMarketIdsAction(db, marketIDs, "sell", config.optionalAccountIDs)
if e != nil {
return nil, fmt.Errorf("could not make daily volume by date Query: %s", e)
}

// TODO DS Validate the config, to have exactly one asset cap defined; a valid mode; non-nil market IDs; and non-nil optional account IDs.
debnil marked this conversation as resolved.
Show resolved Hide resolved

return &volumeFilter{
name: "volumeFilter",
configValue: configValue,
Expand Down Expand Up @@ -135,7 +144,12 @@ func (f *volumeFilter) Apply(ops []txnbuild.Operation, sellingOffers []hProtocol
}

innerFn := func(op *txnbuild.ManageSellOffer) (*txnbuild.ManageSellOffer, error) {
return f.volumeFilterFn(dailyOTB, dailyTBB, op)
limitParameters := limitParameters{
debnil marked this conversation as resolved.
Show resolved Hide resolved
sellBaseAssetCapInBaseUnits: f.config.SellBaseAssetCapInBaseUnits,
sellBaseAssetCapInQuoteUnits: f.config.SellBaseAssetCapInQuoteUnits,
mode: f.config.mode,
}
return volumeFilterFn(dailyOTB, dailyTBB, op, f.baseAsset, f.quoteAsset, limitParameters)
}
ops, e = filterOps(f.name, f.baseAsset, f.quoteAsset, sellingOffers, buyingOffers, ops, innerFn)
if e != nil {
Expand All @@ -144,8 +158,8 @@ 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, error) {
isSell, e := utils.IsSelling(f.baseAsset, f.quoteAsset, op.Selling, op.Buying)
func volumeFilterFn(dailyOTB *VolumeFilterConfig, dailyTBB *VolumeFilterConfig, op *txnbuild.ManageSellOffer, baseAsset hProtocol.Asset, quoteAsset hProtocol.Asset, lp limitParameters) (*txnbuild.ManageSellOffer, error) {
isSell, e := utils.IsSelling(baseAsset, quoteAsset, op.Selling, op.Buying)
if e != nil {
return nil, fmt.Errorf("error when running the isSelling check for offer '%+v': %s", *op, e)
}
Expand All @@ -165,38 +179,38 @@ func (f *volumeFilter) volumeFilterFn(dailyOTB *VolumeFilterConfig, dailyTBB *Vo
newAmountBeingSold := amountValueUnitsBeingSold
var keepSellingBase bool
var keepSellingQuote bool
if f.config.SellBaseAssetCapInBaseUnits != nil {
if lp.sellBaseAssetCapInBaseUnits != nil {
projectedSoldInBaseUnits := *dailyOTB.SellBaseAssetCapInBaseUnits + *dailyTBB.SellBaseAssetCapInBaseUnits + amountValueUnitsBeingSold
keepSellingBase = projectedSoldInBaseUnits <= *f.config.SellBaseAssetCapInBaseUnits
keepSellingBase = projectedSoldInBaseUnits <= *lp.sellBaseAssetCapInBaseUnits
newAmountString := ""
if f.config.mode == volumeFilterModeExact && !keepSellingBase {
newAmount := *f.config.SellBaseAssetCapInBaseUnits - *dailyOTB.SellBaseAssetCapInBaseUnits - *dailyTBB.SellBaseAssetCapInBaseUnits
if lp.mode == volumeFilterModeExact && !keepSellingBase {
newAmount := *lp.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)
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)
} else {
keepSellingBase = true
}

if f.config.SellBaseAssetCapInQuoteUnits != nil {
if lp.sellBaseAssetCapInQuoteUnits != nil {
projectedSoldInQuoteUnits := *dailyOTB.SellBaseAssetCapInQuoteUnits + *dailyTBB.SellBaseAssetCapInQuoteUnits + (newAmountBeingSold * sellPrice)
keepSellingQuote = projectedSoldInQuoteUnits <= *f.config.SellBaseAssetCapInQuoteUnits
keepSellingQuote = projectedSoldInQuoteUnits <= *lp.sellBaseAssetCapInQuoteUnits
newAmountString := ""
if f.config.mode == volumeFilterModeExact && !keepSellingQuote {
newAmount := (*f.config.SellBaseAssetCapInQuoteUnits - *dailyOTB.SellBaseAssetCapInQuoteUnits - *dailyTBB.SellBaseAssetCapInQuoteUnits) / sellPrice
if lp.mode == volumeFilterModeExact && !keepSellingQuote {
newAmount := (*lp.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)
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)
} else {
keepSellingQuote = true
}
Expand Down
268 changes: 268 additions & 0 deletions plugins/volumeFilter_test.go
@@ -0,0 +1,268 @@
package plugins

import (
"database/sql"
"fmt"
"testing"

"github.com/openlyinc/pointy"
"github.com/stellar/kelp/queries"
"github.com/stellar/kelp/support/utils"

"github.com/stellar/go/txnbuild"

hProtocol "github.com/stellar/go/protocols/horizon"
"github.com/stellar/kelp/model"
"github.com/stretchr/testify/assert"
)

func makeTestVolumeFilterConfig(baseCapInBase, baseCapInQuote float64, additionalMarketIDs, optionalAccountIDs []string, mode volumeFilterMode) *VolumeFilterConfig {
debnil marked this conversation as resolved.
Show resolved Hide resolved
var baseCapInBasePtr *float64
if baseCapInBase >= 0 {
baseCapInBasePtr = pointy.Float64(baseCapInBase)
}

var baseCapInQuotePtr *float64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this negative value sentinel makes the code a lot more complex.
we now need to keep in our minds the idea of an alternative representation (instruction) of how to create a volume filter config.

we also only use it in ~5 places. not sure i understand the benefit of this -- I had thought that we agreed to remove these sentinel values but I may be mistaken

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we decided either way. Definitely agreed that it's not amazing, but we don't have a great alternative yet, and I'm totally open to another way we can vary this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's ok to use pointers (via pointy) directly instead of the -1 sentinels. I think this will help readability.

if baseCapInQuote >= 0 {
baseCapInQuotePtr = pointy.Float64(baseCapInQuote)
}

return &VolumeFilterConfig{
SellBaseAssetCapInBaseUnits: baseCapInBasePtr,
SellBaseAssetCapInQuoteUnits: baseCapInQuotePtr,
mode: mode,
additionalMarketIDs: additionalMarketIDs,
optionalAccountIDs: optionalAccountIDs,
}
}

func makeWantVolumeFilter(config *VolumeFilterConfig, firstMarketID string, marketIDs []string, optionalAccountIDs []string, action string) *volumeFilter {
queryMarketIDs := utils.Dedupe(append([]string{firstMarketID}, marketIDs...))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete line 40 (see below)

query, e := queries.MakeDailyVolumeByDateForMarketIdsAction(&sql.DB{}, queryMarketIDs, action, optionalAccountIDs)
if e != nil {
panic(e)
}

return &volumeFilter{
name: "volumeFilter",
debnil marked this conversation as resolved.
Show resolved Hide resolved
baseAsset: utils.NativeAsset,
quoteAsset: utils.NativeAsset,
config: config,
dailyVolumeByDateQuery: query,
}
}

func TestMakeFilterVolume(t *testing.T) {
testAssetDisplayFn := model.MakeSdexMappedAssetDisplayFn(map[model.Asset]hProtocol.Asset{model.Asset("XLM"): utils.NativeAsset})
configValue := ""
exchangeName := ""
tradingPair := &model.TradingPair{Base: "XLM", Quote: "XLM"}
modes := []volumeFilterMode{volumeFilterModeExact, volumeFilterModeIgnore}
firstMarketID := MakeMarketID(exchangeName, "native", "native")

testCases := []struct {
debnil marked this conversation as resolved.
Show resolved Hide resolved
name string
marketIDs []string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exchange name should be part of the input -- we don't need the exchangeName variable -- if we use a common variable for the inputs then we're not really changing the inputs to the function

want marketID should be part of the input (if we are constructing the wantVolumeFilter in the test function)

accountIDs []string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test cases should take a want variable. the inputs should be structured so the want values are different

Copy link
Contributor Author

@debnil debnil Oct 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do this, but it's a bit messy. Within our current test structure, we'd have to define the testCases within the inner loop of our array for this to work.

The want variable requires a volumeFilterConfig. In a prior comment, you suggested changing the wantFilter method to take config as parameter - that code change has been made. So, our current approach defines the testCases and then loops over configs and modes. This forecloses referencing the config in the testCases array - the config doesn't exist at this point - so we cannot construct the wantVolumeFilter in the tests as of now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's ok to create the test cases inside the for loop in this situation

wantFilter *volumeFilter
}{
// TODO DS Confirm the empty config fails once validation is added to the constructor
debnil marked this conversation as resolved.
Show resolved Hide resolved
{
name: "1 market id",
marketIDs: []string{"marketID"},
accountIDs: []string{},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a want value defined here -- where is the expected output for this test?

},
{
name: "2 market ids",
marketIDs: []string{"marketID1", "marketID2"},
accountIDs: []string{},
},
{
name: "2 dupe market ids, 1 distinct",
marketIDs: []string{"marketID1", "marketID1", "marketID2"},
accountIDs: []string{},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wantMarketIDs should be []string{"marketID1", "marketID2"},

},
{
name: "1 account id",
marketIDs: []string{},
accountIDs: []string{"accountID"},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wantMarketIDs should be a non-empty string array here I think (would be the string inlined value in firstMarketID)?

},
{
name: "2 account ids",
marketIDs: []string{},
accountIDs: []string{"accountID1", "accountID2"},
},
{
name: "account and market ids",
marketIDs: []string{"marketID"},
accountIDs: []string{"accountID"},
},
}

for _, k := range testCases {
// this lets us test both types of modes when varying the market and account ids
for _, m := range modes {
debnil marked this conversation as resolved.
Show resolved Hide resolved
// this lets us test both constraints within the config
baseCapInBaseConfig := makeTestVolumeFilterConfig(1.0, -1.0, k.marketIDs, k.accountIDs, m)
baseCapInQuoteConfig := makeTestVolumeFilterConfig(-1.0, 1.0, k.marketIDs, k.accountIDs, m)

for _, config := range []*VolumeFilterConfig{baseCapInBaseConfig, baseCapInQuoteConfig} {
// configType is used to represent the type of config when printing test name
var configType string
debnil marked this conversation as resolved.
Show resolved Hide resolved
if config.SellBaseAssetCapInBaseUnits != nil {
configType = "base"
} else {
configType = "quote"
}

// TODO DS Vary filter action between buy and sell, once buy logic is implemented.
wantFilter := makeWantVolumeFilter(config, firstMarketID, k.marketIDs, k.accountIDs, "sell")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use a string such as "bgeq7c8v" as the marketID. i.e. inline the value contained in firstMarketID

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wantFilter should go on the test cases (in the way described above)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get the feeling we had discussed this in the last review -- this cannot use the same marketIDs and accountIDs variable from the input to construct the want value.

simple example:
input marketIDs = []string{"marketID1", "marketID1"}
wantMarketIDs = []string{"marketID1"}

therefore these variables cannot be the same.

Copy link
Contributor Author

@debnil debnil Nov 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. The variables aren't the same, because the firstMarketID is de-duped and pre-pended to k.marketIDs before the comparison. We did talk about this before - I changed the test logic to push it into makeWantVolumeFilter. I'm also open to directly inlining this though, that makes sense to me and solves this - will make that change.

  2. The reason wantFilter cannot go on the test cases is here: Add tests for the volume filter (part of #483) #552 (comment)

There's no way for us to construct the config, which we loop over, if the marketIDs and accountIDs aren't already defined. But those are defined within the test case. And we can't construct the want value without the config. Thoughts on how to solve this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's discuss on a call early next week on Monday or Tuesday and merge this PR in a pair programming session

t.Run(fmt.Sprintf("%s/%s/%s", k.name, configType, m), func(t *testing.T) {
actual, e := makeFilterVolume(
configValue,
exchangeName,
tradingPair,
testAssetDisplayFn,
utils.NativeAsset,
utils.NativeAsset,
&sql.DB{},
config,
)

if !assert.Nil(t, e) {
return
}

assert.Equal(t, wantFilter, actual)
})
}
}
}
}

func makeManageSellOffer(price, amount string) *txnbuild.ManageSellOffer {
debnil marked this conversation as resolved.
Show resolved Hide resolved
if amount == "" {
return nil
}

return &txnbuild.ManageSellOffer{
Buying: txnbuild.NativeAsset{},
Selling: txnbuild.NativeAsset{},
Price: price,
Amount: amount,
}
}

func TestVolumeFilterFn(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reviewing later once we have the factory method well tested -- I think we should split these up into separate PRs

testCases := []struct {
name string
filter *volumeFilter
sellBaseCapInBase *float64
sellBaseCapInQuote *float64
otbBaseCap float64
otbQuoteCap float64
tbbBaseCap float64
tbbQuoteCap float64
price string
inputAmount string
wantAmount string
wantTbbBaseCap float64
wantTbbQuoteCap float64
}{
{
name: "selling, base units sell cap, don't keep selling base",
sellBaseCapInBase: pointy.Float64(0.0),
sellBaseCapInQuote: nil,
otbBaseCap: 0.0,
otbQuoteCap: 0.0,
tbbBaseCap: 0.0,
tbbQuoteCap: 0.0,
price: "2.0",
inputAmount: "100.0",
wantAmount: "",
wantTbbBaseCap: 0.0,
wantTbbQuoteCap: 0.0,
},
{
name: "selling, base units sell cap, keep selling base",
sellBaseCapInBase: pointy.Float64(1.0),
sellBaseCapInQuote: nil,
otbBaseCap: 0.0,
otbQuoteCap: 0.0,
tbbBaseCap: 0.0,
tbbQuoteCap: 0.0,
price: "2.0",
inputAmount: "100.0",
wantAmount: "1.0000000",
wantTbbBaseCap: 1.0,
wantTbbQuoteCap: 2.0,
},
{
name: "selling, quote units sell cap, don't keep selling quote",
sellBaseCapInBase: nil,
sellBaseCapInQuote: pointy.Float64(0),
otbBaseCap: 0.0,
otbQuoteCap: 0.0,
tbbBaseCap: 0.0,
tbbQuoteCap: 0.0,
price: "2.0",
inputAmount: "100.0",
wantAmount: "",
wantTbbBaseCap: 0.0,
wantTbbQuoteCap: 0.0,
},
{
name: "selling, quote units sell cap, keep selling quote",
sellBaseCapInBase: nil,
sellBaseCapInQuote: pointy.Float64(1.),
otbBaseCap: 0.0,
otbQuoteCap: 0.0,
tbbBaseCap: 0.0,
tbbQuoteCap: 0.0,
price: "2.0",
inputAmount: "100.0",
wantAmount: "0.5000000",
wantTbbBaseCap: 0.5,
wantTbbQuoteCap: 1.0,
},
{
name: "selling, base and quote units sell cap, keep selling base and quote",
sellBaseCapInBase: pointy.Float64(1.),
sellBaseCapInQuote: pointy.Float64(1.),
otbBaseCap: 0.0,
otbQuoteCap: 0.0,
tbbBaseCap: 0.0,
tbbQuoteCap: 0.0,
price: "2.0",
inputAmount: "100.0",
wantAmount: "0.5000000",
wantTbbBaseCap: 0.5,
wantTbbQuoteCap: 1.0,
},
}

for _, k := range testCases {
t.Run(k.name, func(t *testing.T) {
marketIDs := []string{}
accountIDs := []string{}
mode := volumeFilterModeExact
dailyOTB := makeTestVolumeFilterConfig(k.otbBaseCap, k.otbQuoteCap, marketIDs, accountIDs, mode)
dailyTBB := makeTestVolumeFilterConfig(k.tbbBaseCap, k.tbbQuoteCap, marketIDs, accountIDs, mode)
wantTBB := makeTestVolumeFilterConfig(k.wantTbbBaseCap, k.wantTbbQuoteCap, marketIDs, accountIDs, mode)
op := makeManageSellOffer(k.price, k.inputAmount)
wantOp := makeManageSellOffer(k.price, k.wantAmount)

lp := limitParameters{
sellBaseAssetCapInBaseUnits: k.sellBaseCapInBase,
sellBaseAssetCapInQuoteUnits: k.sellBaseCapInQuote,
mode: volumeFilterModeExact,
}

actual, e := volumeFilterFn(dailyOTB, dailyTBB, op, utils.NativeAsset, utils.NativeAsset, lp)

assert.Nil(t, e)
assert.Equal(t, wantOp, actual)
assert.Equal(t, wantTBB, dailyTBB)
})
}
}
1 change: 1 addition & 0 deletions support/utils/functions.go
Expand Up @@ -302,6 +302,7 @@ func assetEqualsExact(hAsset hProtocol.Asset, xAsset txnbuild.Asset) (bool, erro
}

// IsSelling helper method
// TODO DS Add tests for the various possible errors.
func IsSelling(sdexBase hProtocol.Asset, sdexQuote hProtocol.Asset, selling txnbuild.Asset, buying txnbuild.Asset) (bool, error) {
sellingBase, e := assetEqualsExact(sdexBase, selling)
if e != nil {
Expand Down