diff --git a/py_clob_client_v2/order_builder/helpers.py b/py_clob_client_v2/order_builder/helpers.py index c686aee..2357c3c 100644 --- a/py_clob_client_v2/order_builder/helpers.py +++ b/py_clob_client_v2/order_builder/helpers.py @@ -1,25 +1,37 @@ -from math import floor, ceil -from decimal import Decimal +from decimal import Decimal, ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_UP + + +TOKEN_DECIMAL_SCALE = Decimal("1000000") + + +def _as_decimal(x: float) -> Decimal: + return Decimal(str(x)) + + +def _quantum(sig_digits: int) -> Decimal: + return Decimal(1).scaleb(-sig_digits) def round_down(x: float, sig_digits: int) -> float: - return floor(x * (10**sig_digits)) / (10**sig_digits) + return float(_as_decimal(x).quantize(_quantum(sig_digits), rounding=ROUND_FLOOR)) def round_normal(x: float, sig_digits: int) -> float: - return round(x * (10**sig_digits)) / (10**sig_digits) + return float(_as_decimal(x).quantize(_quantum(sig_digits), rounding=ROUND_HALF_UP)) def round_up(x: float, sig_digits: int) -> float: - return ceil(x * (10**sig_digits)) / (10**sig_digits) + return float(_as_decimal(x).quantize(_quantum(sig_digits), rounding=ROUND_CEILING)) def to_token_decimals(x: float) -> int: - f = (10**6) * x - if decimal_places(f) > 0: - f = round_normal(f, 0) - return int(f) + return int( + (_as_decimal(x) * TOKEN_DECIMAL_SCALE).quantize( + Decimal("1"), rounding=ROUND_HALF_UP + ) + ) def decimal_places(x: float) -> int: - return abs(Decimal(x.__str__()).as_tuple().exponent) + exponent = _as_decimal(x).as_tuple().exponent + return abs(exponent) if exponent < 0 else 0 diff --git a/tests/order_builder/test_decimal_order_amounts.py b/tests/order_builder/test_decimal_order_amounts.py new file mode 100644 index 0000000..1f26f89 --- /dev/null +++ b/tests/order_builder/test_decimal_order_amounts.py @@ -0,0 +1,44 @@ +from unittest import TestCase + +from py_clob_client_v2.order_builder.builder import OrderBuilder, ROUNDING_CONFIG +from py_clob_client_v2.order_builder.constants import BUY, SELL +from py_clob_client_v2.order_utils.model import Side + + +class TestDecimalOrderAmounts(TestCase): + def setUp(self): + self.builder = OrderBuilder(signer=None, funder="0x" + "0" * 40) + + def test_limit_buy_preserves_cent_boundary_sizes(self): + cases = [ + (16.90, 0.30, 5_070_000, 16_900_000), + (33.30, 0.30, 9_990_000, 33_300_000), + (66.60, 0.15, 9_990_000, 66_600_000), + ] + + for size, price, expected_maker, expected_taker in cases: + with self.subTest(size=size, price=price): + side, maker, taker = self.builder.get_order_amounts( + BUY, size, price, ROUNDING_CONFIG["0.01"] + ) + + self.assertEqual(side, Side.BUY) + self.assertEqual(maker, expected_maker) + self.assertEqual(taker, expected_taker) + + def test_limit_sell_preserves_cent_boundary_sizes(self): + cases = [ + (16.90, 0.30, 16_900_000, 5_070_000), + (33.30, 0.30, 33_300_000, 9_990_000), + (66.60, 0.15, 66_600_000, 9_990_000), + ] + + for size, price, expected_maker, expected_taker in cases: + with self.subTest(size=size, price=price): + side, maker, taker = self.builder.get_order_amounts( + SELL, size, price, ROUNDING_CONFIG["0.01"] + ) + + self.assertEqual(side, Side.SELL) + self.assertEqual(maker, expected_maker) + self.assertEqual(taker, expected_taker)