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

Commit

Permalink
Fix rounding issues in mirror strategy causing offers to not be placed,
Browse files Browse the repository at this point in the history
closes #541 (#544)

* 1 - add tests for rounding logic of checkBalance

* 2 - add rounding truncation logic to number library + test updates

* 3 - use a larger precision for internal calculations and eventually round down in checkBalance

* 4 - fix tests that result in smaller values based on the new truncation based rounding in checkBalance
  • Loading branch information
nikhilsaraf authored Oct 18, 2020
1 parent dcd59c3 commit c1d86f2
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 77 deletions.
55 changes: 47 additions & 8 deletions model/number.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ var NumberConstants = struct {
// InvertPrecision is the precision of the number after it is inverted
const InvertPrecision = 15

// InternalCalculationsPrecision is the precision to be used for internal calculations in a function
const InternalCalculationsPrecision = 15

// Number abstraction
type Number struct {
value float64
Expand Down Expand Up @@ -76,18 +79,30 @@ func (n Number) Subtract(n2 Number) *Number {
return NumberFromFloat(n.AsFloat()-n2.AsFloat(), newPrecision)
}

// Multiply returns a new Number after multiplying with the passed in Number
// Multiply returns a new Number after multiplying with the passed in Number by rounding up based on the smaller precision
func (n Number) Multiply(n2 Number) *Number {
newPrecision := minPrecision(n, n2)
return NumberFromFloat(n.AsFloat()*n2.AsFloat(), newPrecision)
}

// Divide returns a new Number after dividing by the passed in Number
// MultiplyRoundTruncate returns a new Number after multiplying with the passed in Number by truncating based on the smaller precision
func (n Number) MultiplyRoundTruncate(n2 Number) *Number {
newPrecision := minPrecision(n, n2)
return NumberFromFloatRoundTruncate(n.AsFloat()*n2.AsFloat(), newPrecision)
}

// Divide returns a new Number after dividing by the passed in Number by rounding up based on the smaller precision
func (n Number) Divide(n2 Number) *Number {
newPrecision := minPrecision(n, n2)
return NumberFromFloat(n.AsFloat()/n2.AsFloat(), newPrecision)
}

// DivideRoundTruncate returns a new Number after dividing by the passed in Number by truncating based on the smaller precision
func (n Number) DivideRoundTruncate(n2 Number) *Number {
newPrecision := minPrecision(n, n2)
return NumberFromFloatRoundTruncate(n.AsFloat()/n2.AsFloat(), newPrecision)
}

// Scale takes in a scalar with which to multiply the number using the same precision of the original number
func (n Number) Scale(scaleFactor float64) *Number {
return NumberFromFloat(n.AsFloat()*scaleFactor, n.precision)
Expand All @@ -103,10 +118,18 @@ func (n Number) String() string {
return n.AsString()
}

// NumberFromFloat makes a Number from a float
// NumberFromFloat makes a Number from a float by rounding up
func NumberFromFloat(f float64, precision int8) *Number {
return &Number{
value: toFixed(f, precision),
value: toFixed(f, precision, RoundUp),
precision: precision,
}
}

// NumberFromFloatRoundTruncate makes a Number from a float by truncating beyond the specified precision
func NumberFromFloatRoundTruncate(f float64, precision int8) *Number {
return &Number{
value: toFixed(f, precision, RoundTruncate),
precision: precision,
}
}
Expand Down Expand Up @@ -145,13 +168,29 @@ func NumberByCappingPrecision(n *Number, precision int8) *Number {
return n
}

func round(num float64) int64 {
return int64(num + math.Copysign(0.5, num))
func round(num float64, rounding Rounding) int64 {
if rounding == RoundUp {
return int64(num + math.Copysign(0.5, num))
} else if rounding == RoundTruncate {
return int64(num)
} else {
// error
return -1
}
}

func toFixed(num float64, precision int8) float64 {
// Rounding is a type that defines various approaching to rounding numbers
type Rounding int

// Rounding types
const (
RoundUp Rounding = iota
RoundTruncate
)

func toFixed(num float64, precision int8, rounding Rounding) float64 {
output := math.Pow(10, float64(precision))
return float64(round(num*output)) / output
return float64(round(num*output, rounding)) / output
}

func minPrecision(n1 Number, n2 Number) int8 {
Expand Down
132 changes: 96 additions & 36 deletions model/number_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,50 +55,110 @@ func TestNumberFromFloat(t *testing.T) {
}
}

func TestNumberFromFloatRoundTruncate(t *testing.T) {
testCases := []struct {
f float64
precision int8
wantString string
wantFloat float64
}{
{
f: 1.1,
precision: 1,
wantString: "1.1",
wantFloat: 1.1,
}, {
f: 1.1,
precision: 2,
wantString: "1.10",
wantFloat: 1.10,
}, {
f: 1.12,
precision: 1,
wantString: "1.1",
wantFloat: 1.1,
}, {
f: 1.15,
precision: 1,
wantString: "1.1",
wantFloat: 1.1,
}, {
f: 0.12,
precision: 1,
wantString: "0.1",
wantFloat: 0.1,
},
}

for _, kase := range testCases {
t.Run(fmt.Sprintf("%f_%d", kase.f, kase.precision), func(t *testing.T) {
n := NumberFromFloatRoundTruncate(kase.f, kase.precision)
if !assert.Equal(t, kase.wantString, n.AsString()) {
return
}
if !assert.Equal(t, kase.wantFloat, n.AsFloat()) {
return
}
})
}
}

func TestMath(t *testing.T) {
testCases := []struct {
n1 *Number
n2 *Number
wantAdd float64
wantSubtract float64
wantMultiply float64
wantDivide float64
n1 *Number
n2 *Number
wantAdd float64
wantSubtract float64
wantMultiply float64
wantMultiplyRoundTruncate float64
wantDivide float64
wantDivideRoundTruncate float64
}{
{
n1: NumberFromFloat(1.1, 1),
n2: NumberFromFloat(2.1, 1),
wantAdd: 3.2,
wantSubtract: -1.0,
wantMultiply: 2.3,
wantDivide: 0.5,
n1: NumberFromFloat(1.1, 1),
n2: NumberFromFloat(2.1, 1),
wantAdd: 3.2,
wantSubtract: -1.0,
wantMultiply: 2.3,
wantMultiplyRoundTruncate: 2.3,
wantDivide: 0.5,
wantDivideRoundTruncate: 0.5,
}, {
n1: NumberFromFloat(1.15, 1),
n2: NumberFromFloat(2.1, 1),
wantAdd: 3.3,
wantSubtract: -0.9,
wantMultiply: 2.5,
wantDivide: 0.6,
n1: NumberFromFloat(1.15, 1),
n2: NumberFromFloat(2.1, 1),
wantAdd: 3.3,
wantSubtract: -0.9,
wantMultiply: 2.5,
wantMultiplyRoundTruncate: 2.5,
wantDivide: 0.6,
wantDivideRoundTruncate: 0.5,
}, {
n1: NumberFromFloat(1.15, 2),
n2: NumberFromFloat(2.1, 1),
wantAdd: 3.3,
wantSubtract: -1.0,
wantMultiply: 2.4,
wantDivide: 0.5,
n1: NumberFromFloat(1.15, 2),
n2: NumberFromFloat(2.1, 1),
wantAdd: 3.3,
wantSubtract: -1.0,
wantMultiply: 2.4,
wantMultiplyRoundTruncate: 2.4,
wantDivide: 0.5,
wantDivideRoundTruncate: 0.5,
}, {
n1: NumberFromFloat(1.15, 2),
n2: NumberFromFloat(2.1, 2),
wantAdd: 3.25,
wantSubtract: -0.95,
wantMultiply: 2.42,
wantDivide: 0.55,
n1: NumberFromFloat(1.15, 2),
n2: NumberFromFloat(2.1, 2),
wantAdd: 3.25,
wantSubtract: -0.95,
wantMultiply: 2.42,
wantMultiplyRoundTruncate: 2.41,
wantDivide: 0.55,
wantDivideRoundTruncate: 0.54,
}, {
n1: NumberFromFloat(1.12, 2),
n2: NumberFromFloat(2.1, 1),
wantAdd: 3.2,
wantSubtract: -1.0,
wantMultiply: 2.4,
wantDivide: 0.5,
n1: NumberFromFloat(1.12, 2),
n2: NumberFromFloat(2.1, 1),
wantAdd: 3.2,
wantSubtract: -1.0,
wantMultiply: 2.4,
wantMultiplyRoundTruncate: 2.3,
wantDivide: 0.5,
wantDivideRoundTruncate: 0.5,
},
}

Expand Down
44 changes: 25 additions & 19 deletions plugins/mirrorStrategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -828,12 +828,17 @@ func (b *balanceCoordinator) getPlacedBackingUnits() *model.Number {
return b.placedBackingUnits
}

// checkBalance uses a larger precision for internal calculations because of this bug: https://github.com/stellar/kelp/issues/541
// it eventually rounds back to the precision of the passed in volume
func (b *balanceCoordinator) checkBalance(vol *model.Number, price *model.Number) (bool /*hasBackingBalance*/, *model.Number /*newBaseVolume*/, *model.Number /*newQuoteVolume*/) {
expandedPrecisionVolume := model.NumberFromFloat(vol.AsFloat(), model.InternalCalculationsPrecision)
expandedPrecisionPrice := model.NumberFromFloat(price.AsFloat(), model.InternalCalculationsPrecision)

// we want to constrain units on primary exchange to ensure we can mirror correctly
additionalPrimaryUnits := vol
additionalPrimaryUnits := expandedPrecisionVolume
if b.isPrimaryBuy { // buying base on primary, selling base on backing
// convert to quote units since we are selling quote on primary
additionalPrimaryUnits = vol.Multiply(*price)
additionalPrimaryUnits = vol.MultiplyRoundTruncate(*expandedPrecisionPrice)
}
remainingPrimaryUnits := b.primaryBalance.Subtract(*b.placedPrimaryUnits)
if remainingPrimaryUnits.AsFloat() < 0.0000002 {
Expand All @@ -846,10 +851,10 @@ func (b *balanceCoordinator) checkBalance(vol *model.Number, price *model.Number
}

// now do for backing exchange to ensure we can offset trades correctly
additionalBackingUnits := vol
additionalBackingUnits := expandedPrecisionVolume
if !b.isPrimaryBuy { // selling base on primary, buying base on backing
// convert to quote units since we are selling quote on backing
additionalBackingUnits = vol.Multiply(*price)
additionalBackingUnits = vol.MultiplyRoundTruncate(*expandedPrecisionPrice)
}
remainingBackingUnits := b.backingBalance.Subtract(*b.placedBackingUnits)
if remainingBackingUnits.AsFloat() < 0.0000002 {
Expand All @@ -865,9 +870,9 @@ func (b *balanceCoordinator) checkBalance(vol *model.Number, price *model.Number
normalizedPrimaryUnits := additionalPrimaryUnits
normalizedBackingUnits := additionalBackingUnits
if b.isPrimaryBuy {
normalizedPrimaryUnits = normalizedPrimaryUnits.Divide(*price)
normalizedPrimaryUnits = normalizedPrimaryUnits.DivideRoundTruncate(*expandedPrecisionPrice)
} else {
normalizedBackingUnits = normalizedBackingUnits.Divide(*price)
normalizedBackingUnits = normalizedBackingUnits.DivideRoundTruncate(*expandedPrecisionPrice)
}
var minBaseUnitsFloat float64
if normalizedBackingUnits.AsFloat() < normalizedPrimaryUnits.AsFloat() {
Expand All @@ -880,21 +885,22 @@ func (b *balanceCoordinator) checkBalance(vol *model.Number, price *model.Number
minBaseUnitsFloat = normalizedPrimaryUnits.AsFloat()
log.Printf("balanceCoordinator: nothing to constrain since normalizedPrimaryUnits and normalizedBackingUnits were equal (%.10f)\n", normalizedPrimaryUnits.AsFloat())
}
minPrecision := normalizedPrimaryUnits.Precision()
if normalizedBackingUnits.Precision() < normalizedPrimaryUnits.Precision() {
minPrecision = normalizedBackingUnits.Precision()
}
minBaseUnits := model.NumberFromFloat(minBaseUnitsFloat, minPrecision)
// finally convert back to quote units where necessary
minPrimaryUnits := minBaseUnits
minBackingUnits := minBaseUnits
minBaseUnits := model.NumberFromFloat(minBaseUnitsFloat, model.InternalCalculationsPrecision)

// convert to final precision values now
minBaseUnitsFinalPrecision := model.NumberFromFloatRoundTruncate(minBaseUnits.AsFloat(), vol.Precision())
minQuoteUnits := minBaseUnitsFinalPrecision.MultiplyRoundTruncate(*expandedPrecisionPrice)
minQuoteUnitsFinalPrecision := model.NumberFromFloatRoundTruncate(minQuoteUnits.AsFloat(), vol.Precision())
var minPrimaryUnitsFinalPrecision, minBackingUnitsFinalPrecision *model.Number
if b.isPrimaryBuy {
minPrimaryUnits = minPrimaryUnits.Multiply(*price)
minPrimaryUnitsFinalPrecision = minQuoteUnitsFinalPrecision
minBackingUnitsFinalPrecision = minBaseUnitsFinalPrecision
} else {
minBackingUnits = minBackingUnits.Multiply(*price)
minPrimaryUnitsFinalPrecision = minBaseUnitsFinalPrecision
minBackingUnitsFinalPrecision = minQuoteUnitsFinalPrecision
}

b.placedPrimaryUnits = b.placedPrimaryUnits.Add(*minPrimaryUnits)
b.placedBackingUnits = b.placedBackingUnits.Add(*minBackingUnits)
return true, minBaseUnits, minBaseUnits.Multiply(*price)
b.placedPrimaryUnits = b.placedPrimaryUnits.Add(*minPrimaryUnitsFinalPrecision)
b.placedBackingUnits = b.placedBackingUnits.Add(*minBackingUnitsFinalPrecision)
return true, minBaseUnitsFinalPrecision, minQuoteUnitsFinalPrecision
}
Loading

0 comments on commit c1d86f2

Please sign in to comment.