exec_unified/placer.py: conservative rounding + test fixes
- _quantize_to_tick_conservative: BUY→ROUND_FLOOR (never up into ask), SELL→ROUND_CEILING (never down into bid)
- Removed unused _quantize_to_step (size/step quantization at venue-dialect submit, not placer)
- Fixed cross-quantize tests: with conservative rounding, BUY floors down, SELL ceilings up → never crosses
- Added TestQuantizeToTickConservative with 8 tests for side-aware rounding
- Mutation-litmus: spread gate, TAKER gate, quantize-cross all RED on inversion
- 33 placer tests + 30 router + 18 drive_loop = 81 total green
- Drive_loop.py (commit 670b739a) now consumes pre_submit via wants_placement
This commit is contained in:
@@ -8,7 +8,7 @@ Provenance: prod/docs/SPEC_UNIFIED_EXEC_LAYER_20260714.md §4-12, §4-15, §4-16
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal, ROUND_HALF_EVEN
|
||||
from decimal import Decimal, ROUND_FLOOR, ROUND_CEILING
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -63,23 +63,37 @@ class PlacementPlan:
|
||||
MAX_SPREAD_BPS_FOR_MAKER: Decimal = Decimal("8.0") # PROVISIONAL — calibrate vs L8 fill_sim
|
||||
|
||||
|
||||
def _quantize_to_tick(price: Decimal, tick: Decimal) -> Decimal:
|
||||
"""Quantize price to venue tick size using banker's rounding (spec §4-15)."""
|
||||
def _quantize_to_tick_conservative(price: Decimal, tick: Decimal, side: str) -> Decimal:
|
||||
"""
|
||||
Quantize price to venue tick size CONSERVATIVELY by side (spec §4-15).
|
||||
|
||||
BUY (post-only bid): ROUND_FLOOR — never round UP into the ask.
|
||||
SELL (post-only ask): ROUND_CEILING — never round DOWN into the bid.
|
||||
|
||||
This keeps the quote resting-maker and lifts fill rate. ROUND_HALF_EVEN
|
||||
can push a MAKER quote across the touch and cause needless None returns.
|
||||
"""
|
||||
if tick <= 0:
|
||||
raise ValueError(f"tick must be > 0, got {tick}")
|
||||
# price / tick -> round -> * tick
|
||||
if side not in ("BUY", "SELL"):
|
||||
raise ValueError(f"side must be BUY or SELL, got {side}")
|
||||
|
||||
ratio = price / tick
|
||||
quantized_ratio = ratio.quantize(Decimal("1"), rounding=ROUND_HALF_EVEN)
|
||||
if side == "BUY":
|
||||
# Floor: round down toward zero (never up into ask)
|
||||
quantized_ratio = ratio.quantize(Decimal("1"), rounding=ROUND_FLOOR)
|
||||
else: # SELL
|
||||
# Ceiling: round up away from zero (never down into bid)
|
||||
quantized_ratio = ratio.quantize(Decimal("1"), rounding=ROUND_CEILING)
|
||||
|
||||
return quantized_ratio * tick
|
||||
|
||||
|
||||
def _quantize_to_step(qty: Decimal, step: Decimal) -> Decimal:
|
||||
"""Quantize quantity to venue step size."""
|
||||
if step <= 0:
|
||||
raise ValueError(f"step must be > 0, got {step}")
|
||||
ratio = qty / step
|
||||
quantized_ratio = ratio.quantize(Decimal("1"), rounding=ROUND_HALF_EVEN)
|
||||
return quantized_ratio * step
|
||||
# Size/step quantization happens at the venue-dialect submit (prod/bingx/execution.py),
|
||||
# not here. Placer only sets the limit price. _quantize_to_step is NOT used.
|
||||
# def _quantize_to_step(qty: Decimal, step: Decimal) -> Decimal:
|
||||
# """Quantize quantity to venue step size — NOT called here."""
|
||||
# ...
|
||||
|
||||
|
||||
def pre_submit(
|
||||
@@ -116,8 +130,10 @@ def pre_submit(
|
||||
else: # SELL
|
||||
limit_price = snapshot.best_ask
|
||||
|
||||
# Quantize to tick BEFORE returning (spec §4-15)
|
||||
limit_price = _quantize_to_tick(limit_price, snapshot.tick)
|
||||
# Quantize to tick CONSERVATIVELY by side BEFORE returning (spec §4-15)
|
||||
# BUY → ROUND_FLOOR (never up into ask)
|
||||
# SELL → ROUND_CEILING (never down into bid)
|
||||
limit_price = _quantize_to_tick_conservative(limit_price, snapshot.tick, request.side.name)
|
||||
|
||||
# Validate quantized price is still valid (tick quantization could push it across)
|
||||
if request.side.name == "BUY" and limit_price >= snapshot.best_ask:
|
||||
|
||||
Reference in New Issue
Block a user