Skip to content

Commit

Permalink
rfqmath: add FixedPoint[T].WithinTolerance method
Browse files Browse the repository at this point in the history
This commit introduces the WithinTolerance method to FixedPoint[T].

The method checks if two fixed-point numbers are within a given
tolerance, specified in Parts Per Million (PPM), and returns true if
they are.
  • Loading branch information
ffranr committed Sep 25, 2024
1 parent 867a72c commit dfa1c7f
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 0 deletions.
2 changes: 2 additions & 0 deletions rfq/negotiator.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,8 @@ func expiryWithinBounds(expiryUnixTimestamp uint64,

// priceWithinBounds returns true if the difference between the first price and
// the second price is within the given tolerance (in parts per million (PPM)).
//
// TODO(ffranr): Replace with FixedPoint[T].WithinTolerance.
func pricesWithinBounds(firstPrice lnwire.MilliSatoshi,
secondPrice lnwire.MilliSatoshi, tolerancePpm uint64) bool {

Expand Down
47 changes: 47 additions & 0 deletions rfqmath/fixed_point.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,53 @@ func (f FixedPoint[T]) Equals(other FixedPoint[T]) bool {
return f.Coefficient.Equals(other.Coefficient) && f.Scale == other.Scale
}

// WithinTolerance returns true if the two FixedPoint values are within the
// given tolerance (in parts per million (PPM)).
func (f FixedPoint[T]) WithinTolerance(
other FixedPoint[T], tolerancePpm T) bool {

// Determine the larger scale between the two fixed-point numbers.
// Both values will be scaled to this larger scale to ensure a
// consistent comparison.
var largerScale uint8
if f.Scale > other.Scale {
largerScale = f.Scale
} else {
largerScale = other.Scale
}

subjectFp := f.ScaleTo(largerScale)
otherFp := other.ScaleTo(largerScale)

var (
// delta will be the absolute difference between the two
// coefficients.
delta T

// maxCoefficient is the larger of the two coefficients.
maxCoefficient T
)
if subjectFp.Coefficient.Gt(otherFp.Coefficient) {
delta = subjectFp.Coefficient.Sub(otherFp.Coefficient)
maxCoefficient = subjectFp.Coefficient
} else {
delta = otherFp.Coefficient.Sub(subjectFp.Coefficient)
maxCoefficient = otherFp.Coefficient
}

// Calculate the tolerance in absolute terms based on the largest
// coefficient.
//
// tolerancePpm is parts per million, therefore we multiply the delta by
// 1,000,000 instead of dividing the tolerance.
scaledDelta := delta.Mul(NewInt[T]().FromUint64(1_000_000))

// Compare the scaled delta to the product of the maximum coefficient
// and the tolerance.
toleranceCoefficient := maxCoefficient.Mul(tolerancePpm)
return toleranceCoefficient.Gte(scaledDelta)
}

// FixedPointFromUint64 creates a new FixedPoint from the given integer and
// scale. Note that the input here should be *unscaled*.
func FixedPointFromUint64[N Int[N]](value uint64, scale uint8) FixedPoint[N] {
Expand Down
165 changes: 165 additions & 0 deletions rfqmath/fixed_point_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,169 @@ func testFromUint64[N Int[N]](t *rapid.T) {
require.Equal(t, coefficient, scaledBack.Coefficient.ToUint64())
}

// TestWithinTolerance tests that the FixedPoint.WithinTolerance method works as
// expected.
func testWithinTolerance[N Int[N]](t *rapid.T) {
type testCase struct {
// firstFp is the fixed-point to compare with secondFp.
firstFp FixedPoint[N]

// secondFp is the fixed-point to compare with firstFp.
secondFp FixedPoint[N]

// tolerancePpm is the tolerance in parts per million (PPM) that
// the second price can deviate from the first price and still
// be considered within bounds.
tolerancePpm uint64

// withinBounds is the expected result of the bounds check.
withinBounds bool
}

testCases := []testCase{
{
// Case where secondFp is 10% less than firstFp,
// tolerance allows 11.11% (111111 PPM). Diff within
// bounds.
firstFp: FixedPointFromUint64[N](1000000, 1),
secondFp: FixedPointFromUint64[N](900000, 1),
tolerancePpm: 111111, // 11.11% tolerance in PPM
withinBounds: true,
},
{
// Case where firstFp is 15% less than secondFp,
// tolerance allows 17.65% (176470 PPM). Diff within
// bounds.
firstFp: FixedPointFromUint64[N](8_500_00, 1),
secondFp: FixedPointFromUint64[N](1_000_000, 1),
tolerancePpm: 176470, // 17.65% tolerance in PPM
withinBounds: true,
},
{
// Case where firstFp is 15% less than secondFp,
// tolerance allows 17.65% (176470 PPM). Diff within
// bounds.
firstFp: FixedPointFromUint64[N](85_000, 3),
secondFp: FixedPointFromUint64[N](100_000, 2),
tolerancePpm: 176470, // 17.65% tolerance in PPM
withinBounds: true,
},
{
// Case where secondFp is 15% less than firstFp,
// tolerance allows 10% (100000 PPM). Diff outside
// bounds.
firstFp: FixedPointFromUint64[N](85_000, 3),
secondFp: FixedPointFromUint64[N](100_000, 2),
tolerancePpm: 100000, // 10% tolerance in PPM
withinBounds: false,
},
{
// Case where firstFp and secondFp are equal,
// tolerance is 0 PPM. Diff within bounds.
firstFp: FixedPointFromUint64[N](100_000, 2),
secondFp: FixedPointFromUint64[N](100_000, 4),
tolerancePpm: 0, // 0% tolerance in PPM
withinBounds: true,
},
{
// Case where firstFp and secondFp are equal,
// tolerance is 0 PPM. Diff within bounds.
firstFp: FixedPointFromUint64[N](100_000, 2),
secondFp: FixedPointFromUint64[N](100_000, 2),
tolerancePpm: 0, // 0% tolerance in PPM
withinBounds: true,
},
{
// Case where secondFp is 1% more than firstFp,
// tolerance allows 0.99% (9900 PPM). Diff outside
// bounds.
firstFp: FixedPointFromUint64[N](100_000, 2),
secondFp: FixedPointFromUint64[N](101_000, 3),
tolerancePpm: 9900, // 0.99% tolerance in PPM
withinBounds: false,
},
{
// Case where secondFp is 5% less than firstFp,
// tolerance allows 5% (50000 PPM). Diff within bounds.
firstFp: FixedPointFromUint64[N](100_000, 1),
secondFp: FixedPointFromUint64[N](95000, 2),
tolerancePpm: 50000, // 5% tolerance in PPM
withinBounds: true,
},
{
// Case where secondFp is greater than firstFp,
// tolerance allows 5% (50000 PPM). Diff within bounds.
firstFp: FixedPoint[N]{
Coefficient: NewInt[N]().FromUint64(314),
Scale: 2,
},
secondFp: FixedPoint[N]{
Coefficient: NewInt[N]().FromUint64(
314_159_265_359,
),
Scale: 11,
},

tolerancePpm: 50000, // 5% tolerance in PPM
withinBounds: true,
},
{
// Case where secondFp is 10% less than firstFp,
// tolerance allows 9% (90000 PPM). Diff outside bounds.
firstFp: FixedPointFromUint64[N](100_000, 3),
secondFp: FixedPointFromUint64[N](90_000, 1),
tolerancePpm: 90000, // 9% tolerance in PPM
withinBounds: false,
},
{
// Case where secondFp is 9% less than firstFp,
// tolerance allows 10% (100000 PPM). Diff within
// bounds.
firstFp: FixedPointFromUint64[N](100_000, 4),
secondFp: FixedPointFromUint64[N](91_000, 1),
tolerancePpm: 100000, // 10% tolerance in PPM
withinBounds: true,
},
{
// Case where both prices are zero, should be within
// bounds.
firstFp: FixedPointFromUint64[N](0, 0),
secondFp: FixedPointFromUint64[N](0, 0),
tolerancePpm: 100000, // any tolerance in PPM
withinBounds: true,
},
{
// Case where firstFp is zero and secondFp is
// non-zero, should not be within bounds.
firstFp: FixedPointFromUint64[N](0, 0),
secondFp: FixedPointFromUint64[N](100_000, 4),
tolerancePpm: 100000, // any tolerance in PPM
withinBounds: false,
},
{
// Case where secondFp is zero and firstFp is
// non-zero, should not be within bounds.
firstFp: FixedPointFromUint64[N](100_000, 4),
secondFp: FixedPointFromUint64[N](0, 0),
tolerancePpm: 100000, // any tolerance in PPM
withinBounds: false,
},
}

// Run the test cases.
for idx, tc := range testCases {
result := tc.firstFp.WithinTolerance(
tc.secondFp, NewInt[N]().FromUint64(tc.tolerancePpm),
)

// Compare bounds check result with expected test case within
// bounds flag.
require.Equal(
t, tc.withinBounds, result, "Test case %d failed", idx,
)
}
}

// TestFixedPoint runs a series of property-based tests on the FixedPoint type
// exercising key invariant properties.
func TestFixedPoint(t *testing.T) {
Expand All @@ -175,4 +338,6 @@ func TestFixedPoint(t *testing.T) {
t.Run("equality", rapid.MakeCheck(testEquality[BigInt]))

t.Run("from_uint64", rapid.MakeCheck(testFromUint64[BigInt]))

t.Run("within_tolerance", rapid.MakeCheck(testWithinTolerance[BigInt]))
}

0 comments on commit dfa1c7f

Please sign in to comment.