fix: use dynamic quantity/price precision per symbol from exchange info

Hardcoded round(qty, 1) caused -1111 Precision errors for TRXUSDT and
DOGEUSDT (stepSize=1, requires integers). Now lazily loads quantityPrecision
and pricePrecision from Binance futures_exchange_info per symbol. SL/TP
prices also use symbol-specific precision instead of hardcoded 4 decimals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
21in7
2026-03-08 13:07:23 +09:00
parent 97aef14d6c
commit c6c60b274c
3 changed files with 53 additions and 4 deletions

View File

@@ -248,14 +248,14 @@ class TradingBot:
side=sl_side,
quantity=quantity,
order_type="STOP_MARKET",
stop_price=round(stop_loss, 4),
stop_price=self.exchange._round_price(stop_loss),
reduce_only=True,
)
await self.exchange.place_order(
side=sl_side,
quantity=quantity,
order_type="TAKE_PROFIT_MARKET",
stop_price=round(take_profit, 4),
stop_price=self.exchange._round_price(take_profit),
reduce_only=True,
)

View File

@@ -1,4 +1,5 @@
import asyncio
import math
from binance.client import Client
from binance.exceptions import BinanceAPIException
from loguru import logger
@@ -13,18 +14,62 @@ class BinanceFuturesClient:
api_key=config.api_key,
api_secret=config.api_secret,
)
self._qty_precision: int | None = None
self._price_precision: int | None = None
MIN_NOTIONAL = 5.0 # 바이낸스 선물 최소 명목금액 (USDT)
def _load_symbol_precision(self) -> None:
"""바이낸스 exchange info에서 심볼별 수량/가격 정밀도를 로드한다."""
try:
info = self.client.futures_exchange_info()
for s in info["symbols"]:
if s["symbol"] == self.symbol:
self._qty_precision = s.get("quantityPrecision", 1)
self._price_precision = s.get("pricePrecision", 2)
logger.info(
f"[{self.symbol}] 정밀도 로드: qty={self._qty_precision}, price={self._price_precision}"
)
return
logger.warning(f"[{self.symbol}] exchange info에서 심볼 미발견, 기본 정밀도 사용")
self._qty_precision = 1
self._price_precision = 2
except Exception as e:
logger.warning(f"[{self.symbol}] exchange info 조회 실패 ({e}), 기본 정밀도 사용")
self._qty_precision = 1
self._price_precision = 2
@property
def qty_precision(self) -> int:
if self._qty_precision is None:
self._load_symbol_precision()
return self._qty_precision
@property
def price_precision(self) -> int:
if self._price_precision is None:
self._load_symbol_precision()
return self._price_precision
def _round_qty(self, qty: float) -> float:
"""심볼의 quantityPrecision에 맞춰 수량을 내림(truncate)한다."""
p = self.qty_precision
factor = 10 ** p
return math.floor(qty * factor) / factor
def _round_price(self, price: float) -> float:
"""심볼의 pricePrecision에 맞춰 가격을 반올림한다."""
return round(price, self.price_precision)
def calculate_quantity(self, balance: float, price: float, leverage: int, margin_ratio: float) -> float:
"""동적 증거금 비율 기반 포지션 크기 계산 (최소 명목금액 $5 보장)"""
notional = balance * margin_ratio * leverage
if notional < self.MIN_NOTIONAL:
notional = self.MIN_NOTIONAL
quantity = notional / price
qty_rounded = round(quantity, 1)
qty_rounded = self._round_qty(quantity)
if qty_rounded * price < self.MIN_NOTIONAL:
qty_rounded = round(self.MIN_NOTIONAL / price + 0.05, 1)
qty_rounded = self._round_qty(self.MIN_NOTIONAL / price + 10 ** -self.qty_precision)
return qty_rounded
async def set_leverage(self, leverage: int) -> dict:

View File

@@ -23,6 +23,8 @@ def client():
c = BinanceFuturesClient.__new__(BinanceFuturesClient)
c.config = config
c.symbol = config.symbol
c._qty_precision = 1
c._price_precision = 4
return c
@@ -39,6 +41,8 @@ def exchange():
c.config = config
c.symbol = config.symbol
c.client = MagicMock()
c._qty_precision = 1
c._price_precision = 4
return c