From 797afbf4d1fce2217470d98038d282ed3c2dd6e0 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Sun, 25 Oct 2020 13:05:15 +0530 Subject: [PATCH] mirror max base volume cap, closes #556 (#557) * 1 - mirror strategy config: MAX_ORDER_BASE_CAP * 2 - usage of new config field + tests * 3 - fix bug in max base volume cap + test --- examples/configs/trader/sample_mirror.cfg | 4 + plugins/mirrorStrategy.go | 17 +++- plugins/mirrorStrategy_test.go | 115 ++++++++++++++++------ 3 files changed, 105 insertions(+), 31 deletions(-) diff --git a/examples/configs/trader/sample_mirror.cfg b/examples/configs/trader/sample_mirror.cfg index eda5a8371..a69a491a3 100644 --- a/examples/configs/trader/sample_mirror.cfg +++ b/examples/configs/trader/sample_mirror.cfg @@ -35,6 +35,10 @@ BID_VOLUME_DIVIDE_BY=4.0 # use -1.0 if you want an empty side for the asks ASK_VOLUME_DIVIDE_BY=5.0 +# uncomment this to set a cap on the size of the order in base units. If the backing order after dividing is larger then the bot will cap it to this amount. +# this config param helps you control your risk so you do not place large orders if the backing exchange has one big order. +#MAX_ORDER_BASE_CAP=10000.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% PER_LEVEL_SPREAD=0.005 diff --git a/plugins/mirrorStrategy.go b/plugins/mirrorStrategy.go index e80073cb5..b94262b53 100644 --- a/plugins/mirrorStrategy.go +++ b/plugins/mirrorStrategy.go @@ -33,6 +33,7 @@ type mirrorConfig struct { 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"` + MaxOrderBaseCap float64 `valid:"-" toml:"MAX_ORDER_BASE_CAP"` PerLevelSpread float64 `valid:"-" toml:"PER_LEVEL_SPREAD"` PricePrecisionOverride *int8 `valid:"-" toml:"PRICE_PRECISION_OVERRIDE"` VolumePrecisionOverride *int8 `valid:"-" toml:"VOLUME_PRECISION_OVERRIDE"` @@ -89,6 +90,7 @@ type mirrorStrategy struct { perLevelSpread float64 bidVolumeDivideBy float64 askVolumeDivideBy float64 + maxOrderBaseCap float64 exchange api.Exchange offsetTrades bool mutex *sync.Mutex @@ -168,6 +170,11 @@ func makeMirrorStrategy( return nil, fmt.Errorf("invalid mirror strategy config file, ASK_VOLUME_DIVIDE_BY needs to be -1.0 or > 0") } + if config.MaxOrderBaseCap < 0.0 { + utils.PrintErrorHintf("need to set a valid value for MAX_ORDER_BASE_CAP, needs to be >= 0.0") + return nil, fmt.Errorf("invalid mirror strategy config file, MAX_ORDER_BASE_CAP needs to be >= 0.0") + } + var exchange api.Exchange var e error var strategyMirrorTradeTriggerExistsQuery *queries.StrategyMirrorTradeTriggerExists @@ -313,6 +320,7 @@ func makeMirrorStrategy( perLevelSpread: config.PerLevelSpread, bidVolumeDivideBy: bidVolumeDivideBy, askVolumeDivideBy: askVolumeDivideBy, + maxOrderBaseCap: config.MaxOrderBaseCap, exchange: exchange, offsetTrades: config.OffsetTrades, mutex: &sync.Mutex{}, @@ -414,12 +422,12 @@ func (s *mirrorStrategy) UpdateWithOps( if s.bidVolumeDivideBy == -1.0 { bids = []model.Order{} } else { - transformOrders(bids, (1 - s.perLevelSpread), (1.0 / s.bidVolumeDivideBy)) + transformOrders(bids, (1 - s.perLevelSpread), (1.0 / s.bidVolumeDivideBy), s.maxOrderBaseCap) } if s.askVolumeDivideBy == -1.0 { asks = []model.Order{} } else { - transformOrders(asks, (1 + s.perLevelSpread), (1.0 / s.askVolumeDivideBy)) + transformOrders(asks, (1 + s.perLevelSpread), (1.0 / s.askVolumeDivideBy), s.maxOrderBaseCap) } log.Printf("new orders (orderbook after transformations):\n") printBidsAndAsks(bids, asks) @@ -482,10 +490,13 @@ func (s *mirrorStrategy) UpdateWithOps( return api.ConvertOperation2TM(ops), nil } -func transformOrders(orders []model.Order, priceMultiplier float64, volumeMultiplier float64) { +func transformOrders(orders []model.Order, priceMultiplier float64, volumeMultiplier float64, maxVolumeCap float64) { for _, o := range orders { *o.Price = *o.Price.Scale(priceMultiplier) *o.Volume = *o.Volume.Scale(volumeMultiplier) + if maxVolumeCap > 0.0 && o.Volume.AsFloat() > maxVolumeCap { + *o.Volume = *model.NumberFromFloat(maxVolumeCap, o.Volume.Precision()) + } } } diff --git a/plugins/mirrorStrategy_test.go b/plugins/mirrorStrategy_test.go index bdea1364c..45079a39f 100644 --- a/plugins/mirrorStrategy_test.go +++ b/plugins/mirrorStrategy_test.go @@ -9,39 +9,98 @@ import ( ) func TestTransformOrders(t *testing.T) { - // setup - orders := []model.Order{ + testCases := []struct { + name string + inputPrice *model.Number + inputVolume *model.Number + orderAction model.OrderAction + priceMultiplier float64 + volumeMultiplier float64 + maxVolumeBaseCap float64 + wantPrice *model.Number + wantVolume *model.Number + }{ { - 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), + name: "buy below capped", + inputPrice: model.NumberFromFloat(0.15, 6), + inputVolume: model.NumberFromFloat(51.5, 5), + orderAction: model.OrderActionBuy, + priceMultiplier: 0.90, + volumeMultiplier: 0.25, + maxVolumeBaseCap: 15.0, + wantPrice: model.NumberFromFloat(0.135, 6), + wantVolume: model.NumberFromFloat(12.875, 5), + }, { + name: "sell below capped", + inputPrice: model.NumberFromFloat(1.15, 6), + inputVolume: model.NumberFromFloat(1.5123, 5), + orderAction: model.OrderActionSell, + priceMultiplier: 0.90, + volumeMultiplier: 0.25, + maxVolumeBaseCap: 15.0, + wantPrice: model.NumberFromFloat(1.035, 6), + wantVolume: model.NumberFromFloat(0.37808, 5), // round up + }, { + name: "buy above capped", + inputPrice: model.NumberFromFloat(0.15, 6), + inputVolume: model.NumberFromFloat(80.0, 5), + orderAction: model.OrderActionBuy, + priceMultiplier: 0.90, + volumeMultiplier: 0.25, + maxVolumeBaseCap: 15.0, + wantPrice: model.NumberFromFloat(0.135, 6), + wantVolume: model.NumberFromFloat(15.0, 5), + }, { + name: "sell above capped", + inputPrice: model.NumberFromFloat(1.15, 6), + inputVolume: model.NumberFromFloat(151.23, 5), + orderAction: model.OrderActionSell, + priceMultiplier: 0.90, + volumeMultiplier: 0.25, + maxVolumeBaseCap: 15.0, + wantPrice: model.NumberFromFloat(1.035, 6), + wantVolume: model.NumberFromFloat(15.0, 5), + }, { + name: "buy with 0 cap", + inputPrice: model.NumberFromFloat(0.15, 6), + inputVolume: model.NumberFromFloat(80.0, 5), + orderAction: model.OrderActionBuy, + priceMultiplier: 0.90, + volumeMultiplier: 0.25, + maxVolumeBaseCap: 0.0, + wantPrice: model.NumberFromFloat(0.135, 6), + wantVolume: model.NumberFromFloat(20.0, 5), + }, { + name: "sell with 0 cap", + inputPrice: model.NumberFromFloat(1.15, 6), + inputVolume: model.NumberFromFloat(151.23, 5), + orderAction: model.OrderActionSell, + priceMultiplier: 0.90, + volumeMultiplier: 0.25, + maxVolumeBaseCap: 0.0, + wantPrice: model.NumberFromFloat(1.035, 6), + wantVolume: model.NumberFromFloat(37.8075, 5), }, } - // run - transformOrders(orders, 0.90, 0.25) + for _, k := range testCases { + t.Run(k.name, func(t *testing.T) { + order := model.Order{ + Pair: &model.TradingPair{Base: model.XLM, Quote: model.USDT}, + OrderAction: k.orderAction, + OrderType: model.OrderTypeLimit, + Price: k.inputPrice, + Volume: k.inputVolume, + } + transformOrders([]model.Order{order}, k.priceMultiplier, k.volumeMultiplier, k.maxVolumeBaseCap) - // 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 + assert.Equal(t, &model.TradingPair{Base: model.XLM, Quote: model.USDT}, order.Pair) + assert.Equal(t, k.orderAction, order.OrderAction) + assert.Equal(t, model.OrderTypeLimit, order.OrderType) + assert.Equal(t, k.wantPrice, order.Price) + assert.Equal(t, k.wantVolume, order.Volume) + }) + } } func TestBalanceCoordinatorCheckBalance(t *testing.T) {