diff --git a/model/number.go b/model/number.go index ece246944..1137faecf 100644 --- a/model/number.go +++ b/model/number.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "math" + "math/big" "strconv" "github.com/stellar/go/price" @@ -19,7 +20,8 @@ var NumberConstants = struct { } // InvertPrecision is the precision of the number after it is inverted -const InvertPrecision = 15 +// this is only 11 becuase if we keep it larger such as 15 then inversions are inaccurate for larger numbers such as inverting 0.00002 +const InvertPrecision = 11 // InternalCalculationsPrecision is the precision to be used for internal calculations in a function const InternalCalculationsPrecision = 15 @@ -157,7 +159,19 @@ func InvertNumber(n *Number) *Number { if n == nil { return nil } - return NumberFromFloat(1.0/n.AsFloat(), InvertPrecision) + + // return 0 for the inverse of 0 to keep it safe + if n.AsFloat() == 0 { + log.Printf("trying to invert the number 0, returning the same number to keep it safe") + return n + } + + bigNum := big.NewRat(1, 1) + bigNum = bigNum.SetFloat64(n.AsFloat()) + bigInv := bigNum.Inv(bigNum) + + bigInvFloat64, _ := bigInv.Float64() + return NumberFromFloat(bigInvFloat64, InvertPrecision) } // NumberByCappingPrecision returns a number with a precision that is at max the passed in precision @@ -174,8 +188,7 @@ func round(num float64, rounding Rounding) int64 { } else if rounding == RoundTruncate { return int64(num) } else { - // error - return -1 + panic(fmt.Sprintf("unknown rounding type %v", rounding)) } } @@ -189,8 +202,26 @@ const ( ) func toFixed(num float64, precision int8, rounding Rounding) float64 { - output := math.Pow(10, float64(precision)) - return float64(round(num*output, rounding)) / output + bigNum := big.NewRat(1, 1) + bigNum = bigNum.SetFloat64(num) + bigPow := big.NewRat(1, 1) + bigPow = bigPow.SetFloat64(math.Pow(10, float64(precision))) + + // multiply + bigMultiply := bigNum.Mul(bigNum, bigPow) + + // convert to int after rounding + bigMultiplyFloat64, _ := bigMultiply.Float64() + roundedInt64 := round(bigMultiplyFloat64, rounding) + bigMultiplyIntFloat64 := big.NewRat(1, 1) + bigMultiplyIntFloat64 = bigMultiplyIntFloat64.SetInt64(roundedInt64) + + // divide it + bigPowInverse := bigPow.Inv(bigPow) + bigResult := bigMultiply.Mul(bigMultiplyIntFloat64, bigPowInverse) + + br, _ := bigResult.Float64() + return br } func minPrecision(n1 Number, n2 Number) int8 { diff --git a/model/number_test.go b/model/number_test.go index 22023ddf6..5248495a6 100644 --- a/model/number_test.go +++ b/model/number_test.go @@ -39,6 +39,11 @@ func TestNumberFromFloat(t *testing.T) { precision: 1, wantString: "0.1", wantFloat: 0.1, + }, { + f: 50000.0, + precision: 14, + wantString: "50000.00000000000000", + wantFloat: 50000.0, }, } @@ -103,6 +108,68 @@ func TestNumberFromFloatRoundTruncate(t *testing.T) { } } +func TestToFixed(t *testing.T) { + testCases := []struct { + num float64 + precision int8 + rounding Rounding + wantOut float64 + }{ + // precision 5 + { + num: 50000.12345, + precision: 5, + rounding: RoundUp, + wantOut: 50000.12345, + }, { + num: 50000.12345, + precision: 5, + rounding: RoundTruncate, + wantOut: 50000.12345, + }, { + num: 0.00002, + precision: 5, + rounding: RoundUp, + wantOut: 0.00002, + }, { + num: 0.00002, + precision: 5, + rounding: RoundTruncate, + wantOut: 0.00002, + }, + // precision 4 + { + num: 50000.12345, + precision: 4, + rounding: RoundUp, + wantOut: 50000.1235, + }, { + num: 50000.12345, + precision: 4, + rounding: RoundTruncate, + wantOut: 50000.1234, + }, { + num: 0.00002, + precision: 4, + rounding: RoundUp, + wantOut: 0.0000, // we do not round the 2 up, if it was a 5 then we would round it up + }, { + num: 0.00002, + precision: 4, + rounding: RoundTruncate, + wantOut: 0.0000, + }, + } + + for i, k := range testCases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + actual := toFixed(k.num, k.precision, k.rounding) + + assert.Equal(t, k.wantOut, actual) + }) + } +} + func TestMath(t *testing.T) { testCases := []struct { n1 *Number @@ -301,7 +368,12 @@ func TestUnaryOperations(t *testing.T) { n: NumberFromFloat(0.2812, 3), wantAbs: 0.281, wantNegate: -0.281, - wantInvert: 3.558718861209964, + wantInvert: 3.55871886121, + }, { + n: NumberFromFloat(0.00002, 10), + wantAbs: 0.00002, + wantNegate: -0.00002, + wantInvert: 50000.0, }, } @@ -321,7 +393,7 @@ func TestUnaryOperations(t *testing.T) { if !assert.Equal(t, kase.wantInvert, inverted.AsFloat()) { return } - if !assert.Equal(t, int8(15), inverted.precision) { + if !assert.Equal(t, int8(11), inverted.precision) { return } }) diff --git a/plugins/batchedExchange_test.go b/plugins/batchedExchange_test.go new file mode 100644 index 000000000..a43b731b2 --- /dev/null +++ b/plugins/batchedExchange_test.go @@ -0,0 +1,50 @@ +package plugins + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/txnbuild" + "github.com/stellar/kelp/model" + "github.com/stellar/kelp/support/utils" +) + +func TestManageOffer2Order(t *testing.T) { + testCases := []struct { + op *txnbuild.ManageSellOffer + oc *model.OrderConstraints + wantAction model.OrderAction + wantAmount float64 + wantPrice float64 + }{ + { + op: makeSellOpAmtPrice(0.0018, 50500.0), + oc: model.MakeOrderConstraints(2, 4, 0.001), + wantAction: model.OrderActionSell, + wantAmount: 0.0018, + wantPrice: 50500.0, + }, { + op: makeBuyOpAmtPrice(0.0018, 50500.0), + oc: model.MakeOrderConstraints(2, 4, 0.001), + wantAction: model.OrderActionBuy, + wantAmount: 0.0018, + // 1/50500.0 = 0.000019801980198, we need to reduce it to 7 decimals precision because of sdex op, giving 0.0000198 which when inverted is 50505.05 at price precision = 2 + wantPrice: 50505.05, + }, + } + + for _, k := range testCases { + baseAsset := utils.Asset2Asset2(testBaseAsset) + quoteAsset := utils.Asset2Asset2(testQuoteAsset) + order, e := manageOffer2Order(k.op, baseAsset, quoteAsset, k.oc) + if !assert.NoError(t, e) { + return + } + + assert.Equal(t, k.wantAction, order.OrderAction, fmt.Sprintf("expected '%s' but got '%s'", k.wantAction.String(), order.OrderAction.String())) + assert.Equal(t, k.wantPrice, order.Price.AsFloat()) + assert.Equal(t, k.wantAmount, order.Volume.AsFloat()) + } +}