Orders rejected with Filter failure: LOT_SIZE and PRICE_FILTER: rounding to the exchange grid
Our rebalancing bot computed order sizes as fractions of a portfolio and about one order in five was rejected with Filter failure: LOT_SIZE or PRICE_FILTER. Quantities and prices must sit on each symbol's step and tick grid. Reading exchangeInfo filters and rounding with Decimal fixed it.
We run a rebalancing bot that keeps a basket of assets at target weights. It computes the difference between current and target value for each asset and places limit orders for the difference. After adding new pairs, rejections appeared regularly:
ERROR place_order SOLUSDT side=BUY qty=3.4182917 price=161.2347
{"code":-1013,"msg":"Filter failure: LOT_SIZE"}
ERROR place_order ADAUSDT side=SELL qty=1520.6 price=0.354981
{"code":-1013,"msg":"Filter failure: PRICE_FILTER"}
ERROR place_order DOGEUSDT side=BUY qty=12 price=0.1612
{"code":-1013,"msg":"Filter failure: NOTIONAL"}
About 20 percent of rebalancing orders failed, so baskets drifted further from targets every day.
Reproducing it
Fetching the symbol rules:
info = requests.get("https://api.binance.com/api/v3/exchangeInfo", params={"symbol": "SOLUSDT"}).json()
for f in info["symbols"][0]["filters"]:
print(f)
{'filterType': 'PRICE_FILTER', 'minPrice': '0.01000000', 'maxPrice': '10000.00000000', 'tickSize': '0.01000000'}
{'filterType': 'LOT_SIZE', 'minQty': '0.00100000', 'maxQty': '9000.00000000', 'stepSize': '0.00100000'}
{'filterType': 'NOTIONAL', 'minNotional': '5.00000000', 'applyMinToMarket': True, 'maxNotional': '9000000.00000000', ...}
Our quantity 3.4182917 had seven decimals; the step size allows three. Our price 161.2347 had four decimals; the tick size allows two.
What we ruled out
- Insufficient balance. That returns a different error.
- Market closed or symbol trading halted. Other orders for the same symbol were accepted.
Root cause
The bot used a hardcoded rounding of 6 decimals for quantity and price, tuned for the first pairs we traded. Every symbol has its own price tick size, quantity step size, minimum and maximum quantity, and minimum order value (notional). Values that are not exact multiples of the step and tick, or that fall below the minimum notional, are rejected. New pairs had different rules.
What is happening under the hood
Exchanges enforce a grid for each symbol:
- PRICE_FILTER: price must be within min and max and
(price - minPrice) % tickSize == 0. - LOT_SIZE: quantity must be within min and max and
(quantity - minQty) % stepSize == 0.MARKET_LOT_SIZEapplies to market orders. - NOTIONAL (older symbols used MIN_NOTIONAL): price times quantity must be within bounds.
- Other filters exist, such as PERCENT_PRICE_BY_SIDE (price must be close to the current market) and MAX_NUM_ORDERS.
These rules change over time: exchanges adjust tick sizes when prices move a lot. Hardcoding them breaks eventually.
Floating point makes the arithmetic subtle: 3.418 / 0.001 in binary floating point is not exactly 3418, so naive checks fail at the edges.
Environment
| Component | Version / value |
|---|---|
| Bot | Python 3.12, own REST client |
| Exchange | Binance Spot |
| Symbols | 14 USDT pairs |
| Rounding | Hardcoded 6 decimals, float arithmetic |
Minimal reproduction
qty = round(target_value / price, 6) # 3.418291 -> not on 0.001 step
price = round(mid_price * 0.999, 6) # 161.234700 -> not on 0.01 tick
The fix
1. Load filters from exchangeInfo and cache them, refreshing every hour and when an order is rejected with a filter failure.
2. Round with Decimal, in the safe direction. Quantities round down (never spend more than intended). Buy prices round down, sell prices round up, so a limit order is never more aggressive than intended.
from decimal import Decimal, ROUND_DOWN, ROUND_UP
def floor_to_step(value: Decimal, step: Decimal) -> Decimal:
return (value / step).to_integral_value(rounding=ROUND_DOWN) * step
def ceil_to_step(value: Decimal, step: Decimal) -> Decimal:
return (value / step).to_integral_value(rounding=ROUND_UP) * step
def prepare_order(symbol: str, side: str, qty: Decimal, price: Decimal) -> tuple[str, str] | None:
f = FILTERS[symbol]
price = floor_to_step(price, f.tick) if side == "BUY" else ceil_to_step(price, f.tick)
qty = floor_to_step(qty, f.step)
if qty < f.min_qty or qty * price < f.min_notional:
return None # too small to place; carry to next cycle
if qty > f.max_qty:
qty = floor_to_step(f.max_qty, f.step)
return format(qty.normalize(), "f"), format(price.normalize(), "f")
All values are Decimal built from the exchange's strings, never from floats, and are sent as strings, so 0.001 stays exactly 0.001.
3. Skip dust instead of failing. Orders below the minimum notional are not sent; the remainder is carried into the next rebalance.
Alternatives and trade-offs
- Client libraries with built-in precision helpers (for example ccxt's
amount_to_precisionandprice_to_precision). Good if you use them consistently; check their rounding direction. - Round to nearest. Simpler, but can round a quantity up above available balance or make a limit price more aggressive.
How to test the fix
Property-based tests generate random quantities and prices for each symbol's real filters and assert that every prepared order satisfies all filter rules, and that rounded quantity never exceeds the input. We also placed orders on the exchange's spot testnet for all 14 symbols with awkward values.
Confirming in production
- Filter failure rejections went from about 20 percent of rebalancing orders to zero over the next month.
- Basket tracking error against targets dropped from 1.8 percent to 0.3 percent.
Regression guard
- Alert on any -1013 response.
- Filters reloaded hourly and on rejection; a change in a symbol's filters is logged.
- The property tests run against a fresh exchangeInfo snapshot in CI weekly.
Security implications
Correct rounding direction is a risk control: rounding a quantity up could exceed intended exposure, and rounding a buy price up makes the order pay more than planned. The bot's key has no withdrawal permission and is IP-restricted. Nothing in this post is advice about what or when to trade; it is about placing the orders you intended.
Prevention checklist
- Never hardcode precision; read filters from the exchange.
- Use Decimal from exchange strings, send strings.
- Round quantities down and prices in the passive direction.
- Check minimum notional before sending.
- Refresh filters periodically and after rejections.
References
- Binance Spot API filters — https://developers.binance.com/docs/binance-spot-api-docs/filters
- Binance exchangeInfo endpoint — https://developers.binance.com/docs/binance-spot-api-docs/rest-api/general-endpoints#exchange-information
- Binance Spot API error codes — https://developers.binance.com/docs/binance-spot-api-docs/errors
- Python decimal module — https://docs.python.org/3/library/decimal.html
Add a label
Embed this post
<iframe src="https://hibug.me/exchange-filter-failure-lot-size-price-filter" width="700" height="420" frameborder="0" loading="lazy"></iframe>
I build and run the platforms other engineers ship on: CI/CD pipelines, Kubernetes clusters, GPU inference services, LLM features in production, and the cloud bills that come with all of it. I have also spent years writing and operating automated trading systems, where a rounding bug or a missed websocket message is not a log line but a position you did not intend to hold. Each post here starts from a real failure: the alert, the exact error, what we ruled out, the root cause, and how we proved the fix. Same path every time: Investigate, Explain, Fix, Verify. No hype, no price predictions, just engineering.
Related Stories
Binance -1015 Too many new orders during volatility: order count limits, not request weight
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.