Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions backend_api_python/app/services/live_trading/binance_spot.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,12 @@ def _decimal_places_from_step(step: Decimal) -> Optional[int]:
if st <= 0:
return None
try:
normalized = st.normalize()
step_str = str(normalized)
if "." in step_str:
return min(18, max(0, len(step_str.split(".")[1])))
return 0
# normalize() renders small steps in scientific notation ("1E-8"),
# so derive the decimal places from the exponent instead.
exp = st.normalize().as_tuple().exponent
if not isinstance(exp, int):
return None
return min(18, max(0, -exp))
except Exception:
return None

Expand Down
27 changes: 6 additions & 21 deletions backend_api_python/app/services/live_trading/bitget.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,17 +535,9 @@ def _normalize_size(self, *, symbol: str, product_type: str, base_size: float) -
# Infer precision from step if not already set
if size_precision is None:
try:
step_normalized = step.normalize()
step_str = str(step_normalized)
if '.' in step_str:
decimal_part = step_str.split('.')[1]
size_precision = len(decimal_part)
if size_precision < 0:
size_precision = 0
if size_precision > 18:
size_precision = 18
else:
size_precision = 0
exp = step.normalize().as_tuple().exponent
if isinstance(exp, int):
size_precision = min(max(0, -exp), 18)
except Exception:
pass

Expand Down Expand Up @@ -645,16 +637,9 @@ def _normalize_price(self, *, symbol: str, product_type: str, price: float) -> T
px = self._floor_to_step(px, step)
if price_precision is None:
try:
step_normalized = step.normalize()
step_str = str(step_normalized)
if "." in step_str:
price_precision = len(step_str.split(".")[1])
if price_precision < 0:
price_precision = 0
if price_precision > 18:
price_precision = 18
else:
price_precision = 0
exp = step.normalize().as_tuple().exponent
if isinstance(exp, int):
price_precision = min(max(0, -exp), 18)
except Exception:
pass

Expand Down
30 changes: 8 additions & 22 deletions backend_api_python/app/services/live_trading/bybit.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,21 +528,14 @@ def _normalize_qty(self, *, symbol: str, qty: float) -> Tuple[Decimal, Optional[
if step > 0:
q = self._floor_to_step(q, step)

# Infer precision from qtyStep
# Infer precision from qtyStep. Decimal.normalize() renders small steps in
# scientific notation, so derive precision from the Decimal exponent.
qty_precision = None
if step > 0:
try:
step_normalized = step.normalize()
step_str = str(step_normalized)
if '.' in step_str:
decimal_part = step_str.split('.')[1]
qty_precision = len(decimal_part)
if qty_precision < 0:
qty_precision = 0
if qty_precision > 18:
qty_precision = 18
else:
qty_precision = 0
exp = step.normalize().as_tuple().exponent
if isinstance(exp, int):
qty_precision = min(max(0, -exp), 18)
except Exception:
pass

Expand Down Expand Up @@ -637,16 +630,9 @@ def _normalize_price(self, *, symbol: str, price: float) -> Tuple[Decimal, Optio
price_precision = None
if tick > 0:
try:
tick_normalized = tick.normalize()
tick_str = str(tick_normalized)
if "." in tick_str:
price_precision = len(tick_str.split(".")[1])
if price_precision < 0:
price_precision = 0
if price_precision > 18:
price_precision = 18
else:
price_precision = 0
exp = tick.normalize().as_tuple().exponent
if isinstance(exp, int):
price_precision = min(max(0, -exp), 18)
except Exception:
pass
return (p, price_precision)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Precision inference from exchange step sizes below 1E-6.

Decimal.normalize() renders steps such as 0.0000001 as "1E-7". Parsing that
string for a '.' misreads the precision as 0, which truncates prices and
quantities to integers when they are rendered for the exchange.
"""

import time
from decimal import Decimal

from app.services.live_trading.binance_spot import BinanceSpotClient
from app.services.live_trading.bitget import BitgetMixClient
from app.services.live_trading.bybit import BybitClient


def _bybit_client(price_tick: str, qty_step: str) -> BybitClient:
client = BybitClient(api_key="k", secret_key="s", category="linear")
client._inst_cache["linear:1000PEPEUSDT"] = (
time.time(),
{
"priceFilter": {"tickSize": price_tick},
"lotSizeFilter": {"qtyStep": qty_step, "minOrderQty": "0"},
},
)
return client


def _bitget_client(contract: dict) -> BitgetMixClient:
client = BitgetMixClient.__new__(BitgetMixClient)
client._contract_cache = {}
client._contract_cache_ttl_sec = 300.0
client.get_contract = lambda **_kwargs: contract
return client


def test_bybit_small_tick_keeps_price_decimals():
client = _bybit_client("0.0000001", "100")
price, precision = client._normalize_price(symbol="1000PEPE/USDT", price=0.01234567)
assert precision == 7
assert client._dec_str(price, strict_precision=precision) == "0.0123456"


def test_bybit_small_qty_step_keeps_qty_decimals():
client = _bybit_client("0.01", "0.00000001")
qty, precision = client._normalize_qty(symbol="1000PEPE/USDT", qty=0.123456789)
assert precision == 8
assert client._dec_str(qty, strict_precision=precision) == "0.12345678"


def test_bitget_small_price_step_keeps_price_decimals():
client = _bitget_client({"priceStep": "0.0000001"})
price, precision = client._normalize_price(
symbol="PEPE/USDT", product_type="USDT-FUTURES", price=0.01234567
)
assert precision == 7
assert client._dec_str(price, strict_precision=precision) == "0.0123456"


def test_bitget_small_size_step_keeps_size_decimals():
client = _bitget_client({"sizeMultiplier": "0.00000001"})
size, precision = client._normalize_size(
symbol="BTC/USDT", product_type="USDT-FUTURES", base_size=0.123456789
)
assert precision == 8
assert client._dec_str(size, strict_precision=precision) == "0.12345678"


def test_binance_decimal_places_from_small_step():
assert BinanceSpotClient._decimal_places_from_step(Decimal("0.00000001")) == 8
assert BinanceSpotClient._decimal_places_from_step(Decimal("0.00001000")) == 5
assert BinanceSpotClient._decimal_places_from_step(Decimal("10")) == 0