Skip to content

fix(orders): reject prices that are not a multiple of the tick size#181

Merged
cesarenaldi merged 2 commits into
mainfrom
fix/dev-414-tick-grid-price-validation
Jul 21, 2026
Merged

fix(orders): reject prices that are not a multiple of the tick size#181
cesarenaldi merged 2 commits into
mainfrom
fix/dev-414-tick-grid-price-validation

Conversation

@cesarenaldi

@cesarenaldi cesarenaldi commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

What

_resolve_price (limit orders) and _resolve_protected_market_price (market orders) validated prices by decimal-place count only. Decimal count matches tick-grid membership for power-of-ten ticks, but diverges on 0.005 and 0.0025 tick markets: a price like 0.007 has 3 decimals, passes validation, gets signed into the EIP-712 order, and is then rejected by the exchange. The client-side guard should fail fast instead.

Fixes #162.

How

Add a tick-grid membership check after the existing decimal-count check in both validators:

if price % tick_size != 0:
    raise UserInputError(f"price {price} must be a multiple of tick size {tick_size}.")

Purely additive: every previously valid price is a tick multiple and still passes; only off-grid prices that the exchange would reject anyway are now caught client-side.

Tests

  • test_order_limit.py: parametrized on/off-grid cases for _resolve_price across 0.005, 0.0025, and 0.01 ticks including range boundaries, plus an end-to-end prepare_limit_order_draft test on a mocked 0.005 tick market.
  • test_order_market.py: parametrized on/off-grid cases for _resolve_protected_market_price.

Verification

  • uv run ruff format --check and uv run ruff check clean
  • uv run pyright 0 errors
  • uv run pytest tests/unit 1854 passed

Credit to @GiulioDER for the report and analysis in #162.


Note

Medium Risk
Stricter pre-sign validation changes which limit prices are accepted (no silent rounding) and affects EIP-712 order construction for tick sizes like 0.005/0.0025.

Overview
Introduces shared validate_price_on_tick_grid for limit price and market max_price / min_price, replacing duplicated _resolve_price and _resolve_protected_market_price logic.

Validation now enforces the allowed [tick, 1−tick] range, decimal precision derived from the tick (not RoundingConfig.price), and price % tick_size == 0 so off-grid values like 0.007 on a 0.005 tick fail client-side instead of being signed and rejected by the exchange. Valid prices are returned unchanged—limit order prep no longer round_normal-snaps user prices.

Unit coverage in test_order_context.py exercises on-grid acceptance, off-grid multiples, over-precision inputs, and out-of-range values across all supported ticks.

Reviewed by Cursor Bugbot for commit e090e7d. Bugbot is set up for automated code reviews on this repo. Configure here.

Unify _resolve_price and _resolve_protected_market_price into a single
validate_price_on_tick_grid helper in context.py, shared by limit and
market order preparation.

- Price validation no longer consults RoundingConfig: grid membership
  is fully determined by the tick size, matching the exchange's own
  check (price divided by the minimum tick must be an integer).
  RoundingConfig remains scoped to maker/taker amount computation.
- Consolidate coverage into an exhaustive characterization suite in
  test_order_context.py: every on-grid price accepted and returned
  unchanged across all six ticks, every off-grid price at tick
  precision rejected on the half-step ticks, precision and range
  rejections.
@cesarenaldi
cesarenaldi merged commit 2578124 into main Jul 21, 2026
7 checks passed
@cesarenaldi
cesarenaldi deleted the fix/dev-414-tick-grid-price-validation branch July 21, 2026 07:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client-side tick validation accepts off-grid prices on 0.005 / 0.0025 tick markets

1 participant