@@ -47,33 +47,67 @@ def _to_dec(x: Any) -> Decimal:
4747 return Decimal ("0" )
4848
4949 @staticmethod
50- def _dec_str (d : Decimal , max_decimals : int = 18 ) -> str :
50+ def _dec_str (d : Decimal , max_decimals : int = 18 , strict_precision : Optional [ int ] = None ) -> str :
5151 """
5252 Convert Decimal to string with controlled precision.
5353 Binance requires quantities/prices to match LOT_SIZE/PRICE_FILTER precision.
5454 This method ensures the output string doesn't exceed the required precision.
55+
56+ Args:
57+ d: Decimal value to format
58+ max_decimals: Maximum decimal places (fallback if strict_precision not provided)
59+ strict_precision: If provided, strictly limit to this many decimal places (no trailing zero removal)
5560 """
5661 try :
5762 if d == 0 :
5863 return "0"
5964 # Normalize to remove unnecessary trailing zeros from internal representation
6065 normalized = d .normalize ()
66+
67+ # If strict_precision is provided, use it and strictly limit decimal places
68+ # This ensures we match the stepSize requirement exactly
69+ if strict_precision is not None :
70+ try :
71+ prec = int (strict_precision )
72+ if prec < 0 :
73+ prec = 0
74+ if prec > 18 :
75+ prec = 18
76+ # Use quantize to ensure exact precision (round down to match stepSize)
77+ q = Decimal ("1" ).scaleb (- prec )
78+ quantized = normalized .quantize (q , rounding = ROUND_DOWN )
79+ # Format with exact precision - this will produce at most 'prec' decimal places
80+ s = format (quantized , f".{ prec } f" )
81+ # Remove trailing zeros and decimal point if not needed
82+ if '.' in s :
83+ s = s .rstrip ('0' ).rstrip ('.' )
84+ return s if s else "0"
85+ except Exception :
86+ pass
87+
88+ # Fallback to original logic if strict_precision not provided or failed
6189 # Convert to string using fixed-point notation
62- # Use a reasonable max_decimals to avoid excessive precision
63- # Binance typically uses 8 decimal places for most symbols
6490 s = format (normalized , f".{ max_decimals } f" )
6591 # Remove trailing zeros and decimal point if not needed
66- # This ensures we don't send "0.02874400" when "0.028744" is sufficient
6792 if '.' in s :
6893 s = s .rstrip ('0' ).rstrip ('.' )
6994 return s if s else "0"
7095 except Exception :
7196 # Fallback: try to convert safely
7297 try :
73- # If Decimal conversion fails, try float with limited precision
7498 f = float (d )
7599 if f == 0 :
76100 return "0"
101+ if strict_precision is not None :
102+ try :
103+ prec = int (strict_precision )
104+ if 0 <= prec <= 18 :
105+ s = format (f , f".{ prec } f" )
106+ if '.' in s :
107+ s = s .rstrip ('0' ).rstrip ('.' )
108+ return s if s else "0"
109+ except Exception :
110+ pass
77111 # Format with max_decimals and remove trailing zeros
78112 s = format (f , f".{ max_decimals } f" )
79113 if '.' in s :
@@ -86,6 +120,16 @@ def _dec_str(d: Decimal, max_decimals: int = 18) -> str:
86120 if 'e' in s .lower () or 'E' in s :
87121 try :
88122 f = float (s )
123+ if strict_precision is not None :
124+ try :
125+ prec = int (strict_precision )
126+ if 0 <= prec <= 18 :
127+ s = format (f , f".{ prec } f" )
128+ if '.' in s :
129+ s = s .rstrip ('0' ).rstrip ('.' )
130+ return s if s else "0"
131+ except Exception :
132+ pass
89133 s = format (f , f".{ max_decimals } f" )
90134 if '.' in s :
91135 s = s .rstrip ('0' ).rstrip ('.' )
@@ -256,13 +300,16 @@ def _normalize_price(self, *, symbol: str, price: float) -> Decimal:
256300 return Decimal ("0" )
257301 return px
258302
259- def _normalize_quantity (self , * , symbol : str , quantity : float , for_market : bool ) -> Decimal :
303+ def _normalize_quantity (self , * , symbol : str , quantity : float , for_market : bool ) -> Tuple [ Decimal , Optional [ int ]] :
260304 """
261305 Normalize futures order quantity using LOT_SIZE / MARKET_LOT_SIZE filters (best-effort).
306+
307+ Returns:
308+ Tuple of (normalized_quantity, precision) where precision is the number of decimal places required.
262309 """
263310 q = self ._to_dec (quantity )
264311 if q <= 0 :
265- return Decimal ("0" )
312+ return ( Decimal ("0" ), None )
266313 fdict : Dict [str , Any ] = {}
267314 try :
268315 fdict = self .get_symbol_filters (symbol = symbol ) or {}
@@ -292,9 +339,18 @@ def _normalize_quantity(self, *, symbol: str, quantity: float, for_market: bool)
292339 if qty_precision is None and step > 0 :
293340 try :
294341 # stepSize like "0.001" means 3 decimal places
295- step_str = str (step ).rstrip ('0' )
342+ # Use normalize() to remove trailing zeros, then count decimal places
343+ step_normalized = step .normalize ()
344+ step_str = str (step_normalized )
296345 if '.' in step_str :
297- qty_precision = len (step_str .split ('.' )[1 ])
346+ # Count decimal places after removing trailing zeros
347+ decimal_part = step_str .split ('.' )[1 ]
348+ qty_precision = len (decimal_part )
349+ # Ensure precision is at least 0 and at most 18
350+ if qty_precision < 0 :
351+ qty_precision = 0
352+ if qty_precision > 18 :
353+ qty_precision = 18
298354 else :
299355 # If stepSize is 1 or larger, precision is 0
300356 qty_precision = 0
@@ -306,8 +362,8 @@ def _normalize_quantity(self, *, symbol: str, quantity: float, for_market: bool)
306362 q = self ._floor_to_precision (q , qty_precision )
307363
308364 if min_qty > 0 and q < min_qty :
309- return Decimal ("0" )
310- return q
365+ return ( Decimal ("0" ), qty_precision )
366+ return ( q , qty_precision )
311367
312368 def ping (self ) -> bool :
313369 code , data , _ = self ._request ("GET" , "/fapi/v1/time" )
@@ -555,7 +611,7 @@ def place_market_order(
555611 if sd not in ("BUY" , "SELL" ):
556612 raise LiveTradingError (f"Invalid side: { side } " )
557613 q_req = float (quantity or 0.0 )
558- q_dec = self ._normalize_quantity (symbol = symbol , quantity = q_req , for_market = True )
614+ q_dec , qty_precision = self ._normalize_quantity (symbol = symbol , quantity = q_req , for_market = True )
559615 if float (q_dec or 0 ) <= 0 :
560616 raise LiveTradingError (f"Invalid quantity (below step/minQty): requested={ q_req } " )
561617
@@ -575,7 +631,7 @@ def place_market_order(
575631 if notional < min_notional :
576632 raise LiveTradingError (
577633 "Order notional is below MIN_NOTIONAL. "
578- f"symbol={ sym } side={ sd } qty={ self ._dec_str (q_dec )} "
634+ f"symbol={ sym } side={ sd } qty={ self ._dec_str (q_dec , strict_precision = qty_precision )} "
579635 f"markPrice={ mark_price } notional={ self ._dec_str (notional )} "
580636 f"minNotional={ self ._dec_str (min_notional )} "
581637 )
@@ -589,7 +645,7 @@ def place_market_order(
589645 "symbol" : sym ,
590646 "side" : sd ,
591647 "type" : "MARKET" ,
592- "quantity" : self ._dec_str (q_dec ),
648+ "quantity" : self ._dec_str (q_dec , strict_precision = qty_precision ),
593649 }
594650 if reduce_only :
595651 params ["reduceOnly" ] = "true"
@@ -675,7 +731,7 @@ def place_market_order(
675731 pass
676732 raise LiveTradingError (
677733 f"{ e } | debug: symbol={ sym } side={ sd } "
678- f"qty_req={ q_req } qty_norm={ self ._dec_str (q_dec )} "
734+ f"qty_req={ q_req } qty_norm={ self ._dec_str (q_dec , strict_precision = qty_precision )} "
679735 f"base_url={ self .base_url } filtersSymbol={ filt_symbol } contractType={ contract_type } "
680736 f"stepSize={ step } quantityPrecision={ qty_prec } minNotional={ min_not } "
681737 f"dualSidePosition={ dual_mode } positionSide={ pos_side_used } "
@@ -714,7 +770,7 @@ def place_limit_order(
714770 px = float (price or 0.0 )
715771 if q_req <= 0 or px <= 0 :
716772 raise LiveTradingError ("Invalid quantity/price" )
717- q_dec = self ._normalize_quantity (symbol = symbol , quantity = q_req , for_market = False )
773+ q_dec , qty_precision = self ._normalize_quantity (symbol = symbol , quantity = q_req , for_market = False )
718774 if float (q_dec or 0 ) <= 0 :
719775 raise LiveTradingError (f"Invalid quantity (below step/minQty): requested={ q_req } " )
720776 px_dec = self ._normalize_price (symbol = symbol , price = px )
@@ -726,7 +782,7 @@ def place_limit_order(
726782 "side" : sd ,
727783 "type" : "LIMIT" ,
728784 "timeInForce" : "GTC" ,
729- "quantity" : self ._dec_str (q_dec ),
785+ "quantity" : self ._dec_str (q_dec , strict_precision = qty_precision ),
730786 "price" : self ._dec_str (px_dec ),
731787 }
732788 if reduce_only :
@@ -777,7 +833,7 @@ def place_limit_order(
777833 pass
778834 raise LiveTradingError (
779835 f"{ e } | debug: symbol={ sym } side={ sd } "
780- f"qty_req={ q_req } qty_norm={ self ._dec_str (q_dec )} "
836+ f"qty_req={ q_req } qty_norm={ self ._dec_str (q_dec , strict_precision = qty_precision )} "
781837 f"price_req={ px } price_norm={ self ._dec_str (px_dec )} "
782838 )
783839 exchange_order_id = str (raw .get ("orderId" ) or raw .get ("clientOrderId" ) or "" )
0 commit comments