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

Commit

Permalink
mirror strategy: deprecate VOLUME_DIVIDE_BY and use BID and ASK versi…
Browse files Browse the repository at this point in the history
…ons instead, closes #545 (#546)

* 1 - refactor scaling of price and volume

* 2 - deprecate VOLUME_DIVIDE_BY into BID and ASK versions, along with compat and defaulting logic

* 3 - fix transformOrders with test

* 4 - update sample config, support -1.0 to represent empty side of orderbook
  • Loading branch information
nikhilsaraf authored Oct 18, 2020
1 parent c1d86f2 commit 2466352
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 34 deletions.
8 changes: 6 additions & 2 deletions examples/configs/trader/sample_mirror.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ EXCHANGE_QUOTE="ZUSD"
# maximum depth of order levels that we want to create on the orderbook on each side
ORDERBOOK_DEPTH=5

# number to divide volume by when placing orders so we can scale volume as needed
VOLUME_DIVIDE_BY=4.0
# number to divide bid volume by when placing orders so we can scale volume as needed
# use -1.0 if you want an empty side for the bids
BID_VOLUME_DIVIDE_BY=4.0
# number to divide ask volume by when placing orders so we can scale volume as needed
# use -1.0 if you want an empty side for the asks
ASK_VOLUME_DIVIDE_BY=5.0

# spread % we should maintain per level between the mirrored exchange and SDEX (0 < spread < 1.0). This moves the price away from the center price on SDEX so we can cover the position on the external exchange, i.e. if this value is > 0 then the spread you provide on SDEX will be more than the spread on the exchange you are mirroring.
# in this example the spread is 0.5%
Expand Down
120 changes: 88 additions & 32 deletions plugins/mirrorStrategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ const debugLogOffersOrders = true

// mirrorConfig contains the configuration params for this strategy
type mirrorConfig struct {
Exchange string `valid:"-" toml:"EXCHANGE"`
ExchangeBase string `valid:"-" toml:"EXCHANGE_BASE"`
ExchangeQuote string `valid:"-" toml:"EXCHANGE_QUOTE"`
OrderbookDepth int32 `valid:"-" toml:"ORDERBOOK_DEPTH"`
VolumeDivideBy float64 `valid:"-" toml:"VOLUME_DIVIDE_BY"`
PerLevelSpread float64 `valid:"-" toml:"PER_LEVEL_SPREAD"`
PricePrecisionOverride *int8 `valid:"-" toml:"PRICE_PRECISION_OVERRIDE"`
VolumePrecisionOverride *int8 `valid:"-" toml:"VOLUME_PRECISION_OVERRIDE"`
Exchange string `valid:"-" toml:"EXCHANGE"`
ExchangeBase string `valid:"-" toml:"EXCHANGE_BASE"`
ExchangeQuote string `valid:"-" toml:"EXCHANGE_QUOTE"`
OrderbookDepth int32 `valid:"-" toml:"ORDERBOOK_DEPTH"`
// Deprecated: use BID_VOLUME_DIVIDE_BY and ASK_VOLUME_DIVIDE_BY instead
VolumeDivideByDeprecated *float64 `valid:"-" toml:"VOLUME_DIVIDE_BY" deprecated:"true"`
BidVolumeDivideBy *float64 `valid:"-" toml:"BID_VOLUME_DIVIDE_BY"`
AskVolumeDivideBy *float64 `valid:"-" toml:"ASK_VOLUME_DIVIDE_BY"`
PerLevelSpread float64 `valid:"-" toml:"PER_LEVEL_SPREAD"`
PricePrecisionOverride *int8 `valid:"-" toml:"PRICE_PRECISION_OVERRIDE"`
VolumePrecisionOverride *int8 `valid:"-" toml:"VOLUME_PRECISION_OVERRIDE"`
// Deprecated: use MIN_BASE_VOLUME_OVERRIDE instead
MinBaseVolumeDeprecated *float64 `valid:"-" toml:"MIN_BASE_VOLUME" deprecated:"true"`
MinBaseVolumeOverride *float64 `valid:"-" toml:"MIN_BASE_VOLUME_OVERRIDE"`
Expand Down Expand Up @@ -84,7 +87,8 @@ type mirrorStrategy struct {
strategyMirrorTradeTriggerExistsQuery *queries.StrategyMirrorTradeTriggerExists
orderbookDepth int32
perLevelSpread float64
volumeDivideBy float64
bidVolumeDivideBy float64
askVolumeDivideBy float64
exchange api.Exchange
offsetTrades bool
mutex *sync.Mutex
Expand All @@ -111,6 +115,19 @@ func convertDeprecatedMirrorConfigValues(config *mirrorConfig) {
if config.MinBaseVolumeOverride == nil {
config.MinBaseVolumeOverride = config.MinBaseVolumeDeprecated
}

if (config.BidVolumeDivideBy != nil || config.AskVolumeDivideBy != nil) && config.VolumeDivideByDeprecated != nil {
log.Printf("deprecation warning: cannot set both '%s' (deprecated) and ('%s' / '%s') in the mirror strategy config, overriding with values set from '%s' and '%s'\n", "VOLUME_DIVIDE_BY", "BID_VOLUME_DIVIDE_BY", "ASK_VOLUME_DIVIDE_BY", "BID_VOLUME_DIVIDE_BY", "ASK_VOLUME_DIVIDE_BY")
} else if config.VolumeDivideByDeprecated != nil {
log.Printf("deprecation warning: '%s' is deprecated, use the fields '%s' and '%s' in the mirror strategy config instead, see sample_mirror.cfg as an example\n", "VOLUME_DIVIDE_BY", "BID_VOLUME_DIVIDE_BY", "ASK_VOLUME_DIVIDE_BY")
}
// if only one is specified, we will use the deprecated value for the unspecified value right now
if config.BidVolumeDivideBy == nil {
config.BidVolumeDivideBy = config.VolumeDivideByDeprecated
}
if config.AskVolumeDivideBy == nil {
config.AskVolumeDivideBy = config.VolumeDivideByDeprecated
}
}

// makeMirrorStrategy is a factory method
Expand All @@ -126,6 +143,31 @@ func makeMirrorStrategy(
simMode bool,
) (api.Strategy, error) {
convertDeprecatedMirrorConfigValues(config)
var bidVolumeDivideBy float64
var askVolumeDivideBy float64
if config.BidVolumeDivideBy == nil {
bidVolumeDivideBy = 1.0
} else {
bidVolumeDivideBy = *config.BidVolumeDivideBy
}
if config.AskVolumeDivideBy == nil {
askVolumeDivideBy = 1.0
} else {
askVolumeDivideBy = *config.AskVolumeDivideBy
}
if bidVolumeDivideBy == -1.0 && askVolumeDivideBy == -1.0 {
utils.PrintErrorHintf("both BID_VOLUME_DIVIDE_BY and ASK_VOLUME_DIVIDE_BY cannot be -1.0")
return nil, fmt.Errorf("invalid mirror strategy config file, cannot set both BID_VOLUME_DIVIDE_BY and ASK_VOLUME_DIVIDE_BY to -1.0")
}
if bidVolumeDivideBy != -1.0 && bidVolumeDivideBy <= 0 {
utils.PrintErrorHintf("need to set a valid value for BID_VOLUME_DIVIDE_BY, needs to be -1.0 or > 0")
return nil, fmt.Errorf("invalid mirror strategy config file, BID_VOLUME_DIVIDE_BY needs to be -1.0 or > 0")
}
if askVolumeDivideBy != -1.0 && askVolumeDivideBy <= 0 {
utils.PrintErrorHintf("need to set a valid value for ASK_VOLUME_DIVIDE_BY, needs to be -1.0 or > 0")
return nil, fmt.Errorf("invalid mirror strategy config file, ASK_VOLUME_DIVIDE_BY needs to be -1.0 or > 0")
}

var exchange api.Exchange
var e error
var strategyMirrorTradeTriggerExistsQuery *queries.StrategyMirrorTradeTriggerExists
Expand Down Expand Up @@ -269,7 +311,8 @@ func makeMirrorStrategy(
strategyMirrorTradeTriggerExistsQuery: strategyMirrorTradeTriggerExistsQuery,
orderbookDepth: config.OrderbookDepth,
perLevelSpread: config.PerLevelSpread,
volumeDivideBy: config.VolumeDivideBy,
bidVolumeDivideBy: bidVolumeDivideBy,
askVolumeDivideBy: askVolumeDivideBy,
exchange: exchange,
offsetTrades: config.OffsetTrades,
mutex: &sync.Mutex{},
Expand Down Expand Up @@ -364,22 +407,28 @@ func (s *mirrorStrategy) UpdateWithOps(
if len(asks) > 50 {
asks = asks[:50]
}
log.Printf("backing orderbook (before transformations):\n")
printBidsAndAsks(bids, asks)

log.Printf("bids on backing exchange:\n")
for _, o := range bids {
log.Printf(" price=%s, amount=%s\n", o.Price.AsString(), o.Volume.AsString())
// we modify the bids and ask to represent the new orders to place so we reduce unnecessary memory allocations
if s.bidVolumeDivideBy == -1.0 {
bids = []model.Order{}
} else {
transformOrders(bids, (1 - s.perLevelSpread), (1.0 / s.bidVolumeDivideBy))
}
log.Printf("asks on backing exchange:\n")
for _, o := range asks {
log.Printf(" price=%s, amount=%s\n", o.Price.AsString(), o.Volume.AsString())
if s.askVolumeDivideBy == -1.0 {
asks = []model.Order{}
} else {
transformOrders(asks, (1 + s.perLevelSpread), (1.0 / s.askVolumeDivideBy))
}
log.Printf("new orders (orderbook after transformations):\n")
printBidsAndAsks(bids, asks)

deleteBuyOps, buyOps, e := s.updateLevels(
buyingAOffers,
bids,
s.sdex.ModifyBuyOffer,
s.sdex.CreateBuyOffer,
(1 - s.perLevelSpread),
true,
s.buyOnPrimaryBalanceCoordinator, // we sell on the backing exchange to offset trades that are bought on the primary exchange
)
Expand All @@ -393,7 +442,6 @@ func (s *mirrorStrategy) UpdateWithOps(
asks,
s.sdex.ModifySellOffer,
s.sdex.CreateSellOffer,
(1 + s.perLevelSpread),
false,
s.sellOnPrimaryBalanceCoordinator, // we buy on the backing exchange to offset trades that are sold on the primary exchange
)
Expand Down Expand Up @@ -434,6 +482,24 @@ func (s *mirrorStrategy) UpdateWithOps(
return api.ConvertOperation2TM(ops), nil
}

func transformOrders(orders []model.Order, priceMultiplier float64, volumeMultiplier float64) {
for _, o := range orders {
*o.Price = *o.Price.Scale(priceMultiplier)
*o.Volume = *o.Volume.Scale(volumeMultiplier)
}
}

func printBidsAndAsks(bids []model.Order, asks []model.Order) {
log.Printf(" bids on backing exchange:\n")
for _, o := range bids {
log.Printf(" price=%s, amount=%s\n", o.Price.AsString(), o.Volume.AsString())
}
log.Printf(" asks on backing exchange:\n")
for _, o := range asks {
log.Printf(" price=%s, amount=%s\n", o.Price.AsString(), o.Volume.AsString())
}
}

func printDebugOffersAndOps(
buyingAOffers []hProtocol.Offer,
sellingAOffers []hProtocol.Offer,
Expand Down Expand Up @@ -486,18 +552,13 @@ func (s *mirrorStrategy) updateLevels(
newOrders []model.Order,
modifyOffer func(offer hProtocol.Offer, price float64, amount float64, incrementalNativeAmountRaw float64) (*txnbuild.ManageSellOffer, error),
createOffer func(baseAsset hProtocol.Asset, quoteAsset hProtocol.Asset, price float64, amount float64, incrementalNativeAmountRaw float64) (*txnbuild.ManageSellOffer, error),
priceMultiplier float64,
hackPriceInvertForBuyOrderChangeCheck bool, // needed because createBuy and modBuy inverts price so we need this for price comparison in doModifyOffer
bc *balanceCoordinator,
) ([]txnbuild.Operation /*deleteOps*/, []txnbuild.Operation /*ops*/, error) {
ops := []txnbuild.Operation{}
deleteOps := []txnbuild.Operation{}
if len(newOrders) >= len(oldOffers) {
for i := 0; i < len(oldOffers); i++ {
// TODO NS - don't modify existing variables
newOrders[i].Price = newOrders[i].Price.Scale(priceMultiplier)
newOrders[i].Volume = newOrders[i].Volume.Scale(1.0 / s.volumeDivideBy)

if s.offsetTrades {
hasBackingBalance, newBaseVolume, _ := bc.checkBalance(newOrders[i].Volume, newOrders[i].Price)
if !hasBackingBalance {
Expand All @@ -507,7 +568,7 @@ func (s *mirrorStrategy) updateLevels(
newOrders[i].Volume = newBaseVolume
}

modifyOp, deleteOp, e := s.doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, modifyOffer, hackPriceInvertForBuyOrderChangeCheck)
modifyOp, deleteOp, e := s.doModifyOffer(oldOffers[i], newOrders[i], modifyOffer, hackPriceInvertForBuyOrderChangeCheck)
if e != nil {
return nil, nil, e
}
Expand All @@ -522,8 +583,8 @@ func (s *mirrorStrategy) updateLevels(

// create offers for remaining new bids
for i := len(oldOffers); i < len(newOrders); i++ {
price := newOrders[i].Price.Scale(priceMultiplier)
vol := newOrders[i].Volume.Scale(1.0 / s.volumeDivideBy)
price := newOrders[i].Price
vol := newOrders[i].Volume
if s.offsetTrades {
hasBackingBalance, newBaseVol, _ := bc.checkBalance(vol, price)
if !hasBackingBalance {
Expand Down Expand Up @@ -555,10 +616,6 @@ func (s *mirrorStrategy) updateLevels(
}
} else {
for i := 0; i < len(newOrders); i++ {
// TODO NS - don't modify existing variables
newOrders[i].Price = newOrders[i].Price.Scale(priceMultiplier)
newOrders[i].Volume = newOrders[i].Volume.Scale(1.0 / s.volumeDivideBy)

if s.offsetTrades {
hasBackingBalance, newBaseVolume, _ := bc.checkBalance(newOrders[i].Volume, newOrders[i].Price)
if !hasBackingBalance {
Expand All @@ -568,7 +625,7 @@ func (s *mirrorStrategy) updateLevels(
newOrders[i].Volume = newBaseVolume
}

modifyOp, deleteOp, e := s.doModifyOffer(oldOffers[i], newOrders[i], priceMultiplier, modifyOffer, hackPriceInvertForBuyOrderChangeCheck)
modifyOp, deleteOp, e := s.doModifyOffer(oldOffers[i], newOrders[i], modifyOffer, hackPriceInvertForBuyOrderChangeCheck)
if e != nil {
return nil, nil, e
}
Expand All @@ -595,7 +652,6 @@ func (s *mirrorStrategy) updateLevels(
func (s *mirrorStrategy) doModifyOffer(
oldOffer hProtocol.Offer,
newOrder model.Order,
priceMultiplier float64,
modifyOffer func(offer hProtocol.Offer, price float64, amount float64, incrementalNativeAmountRaw float64) (*txnbuild.ManageSellOffer, error),
hackPriceInvertForBuyOrderChangeCheck bool, // needed because createBuy and modBuy inverts price so we need this for price comparison in doModifyOffer
) (txnbuild.Operation, txnbuild.Operation, error) {
Expand Down
36 changes: 36 additions & 0 deletions plugins/mirrorStrategy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,42 @@ import (
"github.com/stellar/kelp/model"
)

func TestTransformOrders(t *testing.T) {
// setup
orders := []model.Order{
{
Pair: &model.TradingPair{Base: model.XLM, Quote: model.USDT},
OrderAction: model.OrderActionBuy,
OrderType: model.OrderTypeLimit,
Price: model.NumberFromFloat(0.15, 6),
Volume: model.NumberFromFloat(51.5, 5),
}, {
Pair: &model.TradingPair{Base: model.XLM, Quote: model.USDT},
OrderAction: model.OrderActionSell,
OrderType: model.OrderTypeLimit,
Price: model.NumberFromFloat(1.15, 6),
Volume: model.NumberFromFloat(1.5123, 5),
},
}

// run
transformOrders(orders, 0.90, 0.25)

// validate
order := orders[0]
assert.Equal(t, &model.TradingPair{Base: model.XLM, Quote: model.USDT}, order.Pair)
assert.Equal(t, model.OrderActionBuy, order.OrderAction)
assert.Equal(t, model.OrderTypeLimit, order.OrderType)
assert.Equal(t, model.NumberFromFloat(0.135, 6), order.Price)
assert.Equal(t, model.NumberFromFloat(12.875, 5), order.Volume)
order = orders[1]
assert.Equal(t, &model.TradingPair{Base: model.XLM, Quote: model.USDT}, order.Pair)
assert.Equal(t, model.OrderActionSell, order.OrderAction)
assert.Equal(t, model.OrderTypeLimit, order.OrderType)
assert.Equal(t, model.NumberFromFloat(1.035, 6), order.Price)
assert.Equal(t, model.NumberFromFloat(0.37808, 5), order.Volume) // round up
}

func TestBalanceCoordinatorCheckBalance(t *testing.T) {
// imagine prices such that we are trading base asset as XLM and quote asset as USD
testCases := []struct {
Expand Down

0 comments on commit 2466352

Please sign in to comment.