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

Commit

Permalink
Smarter pruning in sellSideStrategy, closes #55 (#56)
Browse files Browse the repository at this point in the history
  • Loading branch information
nikhilsaraf authored Nov 17, 2018
1 parent 36079c3 commit 78f7f8c
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 12 deletions.
20 changes: 20 additions & 0 deletions model/number.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package model

import (
"fmt"
"log"
"math"
"strconv"
Expand All @@ -27,6 +28,25 @@ func (n Number) AsString() string {
return strconv.FormatFloat(n.AsFloat(), 'f', int(n.Precision()), 64)
}

// AsRatio returns an integer numerator and denominator
func (n Number) AsRatio() (int32, int32, error) {
denominator := int32(math.Pow(10, float64(n.Precision())))

// add an adjustment because the computed value should not have any digits beyond the decimal
// and we want to roll over values that are not computed correctly rather than using the expensive math/big library
adjustment := 0.1
if n.AsFloat() < 0 {
adjustment = -0.1
}
numerator := int32(n.AsFloat()*float64(denominator) + adjustment)

if float64(numerator)/float64(denominator) != n.AsFloat() {
return 0, 0, fmt.Errorf("invalid conversion to a ratio probably caused by an overflow, float input: %f, numerator: %d, denominator: %d", n.AsFloat(), numerator, denominator)
}

return numerator, denominator, nil
}

// String is the Stringer interface impl.
func (n Number) String() string {
return n.AsString()
Expand Down
71 changes: 71 additions & 0 deletions model/number_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package model

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

func TestAsRatio(t *testing.T) {
testCases := []struct {
n *Number
wantN int32
wantD int32
}{
{
n: NumberFromFloat(0.251523, 6),
wantN: 251523,
wantD: 1000000,
}, {
n: NumberFromFloat(-0.251523, 6),
wantN: -251523,
wantD: 1000000,
}, {
n: NumberFromFloat(0.251841, 6),
wantN: 251841,
wantD: 1000000,
}, {
n: NumberFromFloat(-0.251841, 6),
wantN: -251841,
wantD: 1000000,
},
}

for _, kase := range testCases {
t.Run(kase.n.AsString(), func(t *testing.T) {
num, den, e := kase.n.AsRatio()
if !assert.NoError(t, e) {
return
}

if !assert.Equal(t, kase.wantN, num) {
return
}
assert.Equal(t, kase.wantD, den)
})
}
}

func TestAsRatio_Error(t *testing.T) {
testCases := []struct {
n *Number
}{
{
n: NumberFromFloat(1.0, 10),
}, {
n: NumberFromFloat(2.5, 9),
}, {
n: NumberFromFloat(-2.5, 9),
},
}

for _, kase := range testCases {
t.Run(kase.n.AsString(), func(t *testing.T) {
num, den, e := kase.n.AsRatio()
if !assert.Error(t, e, fmt.Sprintf("got back num=%d, den=%d", num, den)) {
return
}
})
}
}
61 changes: 49 additions & 12 deletions plugins/sellSideStrategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,29 +62,66 @@ func makeSellSideStrategy(

// PruneExistingOffers impl
func (s *sellSideStrategy) PruneExistingOffers(offers []horizon.Offer) ([]build.TransactionMutator, []horizon.Offer) {
// figure out which offers we want to prune
shouldPrune := computeOffersToPrune(offers, s.currentLevels)

pruneOps := []build.TransactionMutator{}
for i := 0; i < len(offers); i++ {
curAmount := utils.AmountStringAsFloat(offers[i].Amount)
curPrice := utils.GetPrice(offers[i])
updatedOffers := []horizon.Offer{}
for i, offer := range offers {
isPruning := shouldPrune[i]
if isPruning {
pOp := s.sdex.DeleteOffer(offer)
pruneOps = append(pruneOps, &pOp)
} else {
updatedOffers = append(updatedOffers, offer)
}

curAmount := utils.AmountStringAsFloat(offer.Amount)
curPrice := utils.GetPrice(offer)
if s.divideAmountByPrice {
curAmount = curAmount * curPrice
curPrice = 1 / curPrice
}

// base and quote here refers to the bot's base and quote, not the base and quote of the sellSideStrategy
isPruning := i >= len(s.currentLevels)
log.Printf("offer | %s | level=%d | curPriceQuote=%.7f | curAmtBase=%.7f | pruning=%v\n", s.action, i+1, curPrice, curAmount, isPruning)
}
return pruneOps, updatedOffers
}

if isPruning {
pOp := s.sdex.DeleteOffer(offers[i])
pruneOps = append(pruneOps, &pOp)
}
// computeOffersToPrune returns a list of bools representing whether we should prune the offer at that position or not
func computeOffersToPrune(offers []horizon.Offer, levels []api.Level) []bool {
numToPrune := len(offers) - len(levels)
if numToPrune <= 0 {
return make([]bool, len(offers))
}

if len(offers) > len(s.currentLevels) {
offers = offers[:len(s.currentLevels)]
offerIdx := 0
levelIdx := 0
shouldPrune := make([]bool, len(offers))
for numToPrune > 0 {
if offerIdx == len(offers) || levelIdx == len(levels) {
// prune remaining offers (from the back as a convention)
for i := 0; i < numToPrune; i++ {
shouldPrune[len(offers)-1-i] = true
}
return shouldPrune
}

offerPrice := float64(offers[offerIdx].PriceR.N) / float64(offers[offerIdx].PriceR.D)
levelPrice := levels[levelIdx].Price.AsFloat()
if offerPrice < levelPrice {
shouldPrune[offerIdx] = true
numToPrune--
offerIdx++
} else if offerPrice == levelPrice {
shouldPrune[offerIdx] = false
offerIdx++
// do not increment levelIdx because we could have two offers or levels at the same price. This will resolve in the next iteration automatically.
} else {
levelIdx++
}
}
return pruneOps, offers
return shouldPrune
}

// PreUpdate impl
Expand Down
112 changes: 112 additions & 0 deletions plugins/sellSideStrategy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package plugins

import (
"fmt"
"testing"

"github.com/lightyeario/kelp/api"
"github.com/lightyeario/kelp/model"
"github.com/stellar/go/clients/horizon"
"github.com/stretchr/testify/assert"
)

func TestComputeOffersToPrune(t *testing.T) {
testCases := []struct {
offerPrices []float64
levelPrices []float64
want []bool
}{
{
offerPrices: []float64{},
levelPrices: []float64{},
want: []bool{},
}, {
offerPrices: []float64{},
levelPrices: []float64{1.0},
want: []bool{},
}, {
offerPrices: []float64{1.0},
levelPrices: []float64{1.0},
want: []bool{false},
}, {
offerPrices: []float64{1.0},
levelPrices: []float64{1.0, 1.2},
want: []bool{false},
}, {
offerPrices: []float64{0.9},
levelPrices: []float64{1.0, 1.2},
want: []bool{false},
}, {
offerPrices: []float64{1.0, 1.2},
levelPrices: []float64{1.0},
want: []bool{false, true},
}, {
offerPrices: []float64{0.9, 1.0},
levelPrices: []float64{1.0},
want: []bool{true, false},
}, {
offerPrices: []float64{10.0, 11.0},
levelPrices: []float64{1.0},
want: []bool{false, true},
}, {
offerPrices: []float64{0.9, 1.2},
levelPrices: []float64{1.0},
want: []bool{true, false},
}, {
offerPrices: []float64{1.0, 1.0},
levelPrices: []float64{1.0},
want: []bool{false, true},
}, {
offerPrices: []float64{1.0, 1.0, 1.2},
levelPrices: []float64{1.0},
want: []bool{false, true, true},
}, {
offerPrices: []float64{1.0, 1.0},
levelPrices: []float64{1.0, 1.0},
want: []bool{false, false},
}, {
offerPrices: []float64{1.0, 1.2},
levelPrices: []float64{1.0, 1.2},
want: []bool{false, false},
}, {
offerPrices: []float64{1.0},
levelPrices: []float64{1.0, 1.0},
want: []bool{false},
}, {
offerPrices: []float64{1.0},
levelPrices: []float64{1.0, 1.2},
want: []bool{false},
},
}

for i, kase := range testCases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
if !assert.Equal(t, len(kase.want), len(kase.offerPrices), "invalid test case") {
return
}

offers := []horizon.Offer{}
for _, p := range kase.offerPrices {
num, den, e := model.NumberFromFloat(p, 8).AsRatio()
if !assert.NoError(t, e) {
return
}
offer := horizon.Offer{}
offer.PriceR.N = num
offer.PriceR.D = den
offers = append(offers, offer)
}

levels := []api.Level{}
for _, p := range kase.levelPrices {
levels = append(levels, api.Level{
Price: *model.NumberFromFloat(p, 8),
Amount: *model.NumberFromFloat(1, 8),
})
}

result := computeOffersToPrune(offers, levels)
assert.Equal(t, kase.want, result)
})
}
}

0 comments on commit 78f7f8c

Please sign in to comment.