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

Commit

Permalink
Add buy infrastructure to volume filter (part of #522) (#586)
Browse files Browse the repository at this point in the history
* Add hint msg for missing db

* Add buytwap config

* Update db query struct to support buy queries

* Update volumeFilter to allow buy side

* Modify filterFactory and test to allow buy side changes.

* Fix build issues

* Add action to makeFilterVolume

* Address review - nikhilsaraf

* Delete all actions

* Check action.
  • Loading branch information
debnil committed Dec 1, 2020
1 parent d696d42 commit d8e7d2d
Show file tree
Hide file tree
Showing 7 changed files with 337 additions and 265 deletions.
30 changes: 18 additions & 12 deletions plugins/filterFactory.go
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
}

Expand All @@ -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])
Expand All @@ -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)
}
Expand Down
106 changes: 68 additions & 38 deletions plugins/filterFactory_test.go
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/openlyinc/pointy"
"github.com/stellar/kelp/queries"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -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)
})
}
}
}

Expand All @@ -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)
Expand Down
81 changes: 37 additions & 44 deletions plugins/volumeFilter.go
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
Expand Down Expand Up @@ -179,50 +178,50 @@ 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)
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, *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)
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, *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
Expand All @@ -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
}

0 comments on commit d8e7d2d

Please sign in to comment.