"""BingX dialect tests — venue idioms are law (spec §4-17). Each asserts a rule a real venue reject taught us. Mutation notes inline. Run: /home/dolphin/siloqy_env/bin/python3 -m pytest prod/exec_unified/test_dialect.py -q """ from __future__ import annotations from decimal import Decimal import pytest from prod.exec_unified.contract import Side from prod.exec_unified.dialect import ( CLIENT_ORDER_ID_MAX, build_order_payload, client_order_id, dash_symbol, quantize_price, quantize_size, ) from prod.exec_unified.router import ExecutionMethod # ── symbol dashing ─────────────────────────────────────────────────────────── @pytest.mark.parametrize("undashed,wire", [ ("BTCUSDT", "BTC-USDT"), ("ETHUSDT", "ETH-USDT"), ("1000SHIBUSDT", "1000SHIB-USDT"), ("USDCUSDT", "USDC-USDT"), ("BTC-USDT", "BTC-USDT"), # already dashed → idempotent ]) def test_dash_symbol(undashed, wire): assert dash_symbol(undashed) == wire # ── clientOrderId (audit H4 / BI-1) ────────────────────────────────────────── def test_client_order_id_format_and_per_attempt(): assert client_order_id("req", 0) == "u-req-0" assert client_order_id("req", 1) == "u-req-1" # retry gets a DIFFERENT id assert client_order_id("req", 0) != client_order_id("req", 1) def test_client_order_id_sanitizes_charset(): cid = client_order_id("BTC/USDT:long #7", 0) # slashes, colon, space, hash assert all(c.isalnum() or c in "_-" for c in cid) def test_client_order_id_length_capped_but_still_unique_per_attempt(): long_req = "x" * 200 a, b = client_order_id(long_req, 0), client_order_id(long_req, 1) assert len(a) <= CLIENT_ORDER_ID_MAX and len(b) <= CLIENT_ORDER_ID_MAX assert a != b # digest preserves per-attempt distinctness assert client_order_id(long_req, 0) == a # deterministic (no RNG) def test_client_order_id_rejects_negative_attempt(): with pytest.raises(ValueError): client_order_id("req", -1) # ── quantization (§4-15) ───────────────────────────────────────────────────── def test_quantize_price_conservative_by_side(): # BUY floors (never up into the ask); SELL ceils (never down into the bid). assert quantize_price(Decimal("100.017"), Decimal("0.01"), Side.BUY) == Decimal("100.01") assert quantize_price(Decimal("100.011"), Decimal("0.01"), Side.SELL) == Decimal("100.02") def test_quantize_size_floors_never_overshoots(): assert quantize_size(Decimal("31.446"), Decimal("1")) == Decimal("31") assert quantize_size(Decimal("0.00019"), Decimal("0.0001")) == Decimal("0.0001") def test_quantize_guards_nonpositive(): with pytest.raises(ValueError): quantize_price(Decimal("100"), Decimal("0"), Side.BUY) with pytest.raises(ValueError): quantize_size(Decimal("1"), Decimal("0")) # ── payload assembly (venue idioms) ────────────────────────────────────────── def _maker(): return build_order_payload( asset="BTCUSDT", side=Side.BUY, method=ExecutionMethod.MAKER, limit_price=Decimal("64547.53"), size=Decimal("0.00019"), reduce_only=False, client_order_id="u-req-0", tick=Decimal("0.1"), step=Decimal("0.0001"), ) def test_maker_payload_postonly_is_timeInForce_not_type(): # THE mm_ L7 correction. Mutation: put "POST_ONLY" in type -> this goes RED. p = _maker() assert p["type"] == "LIMIT" # type is LIMIT|MARKET ONLY assert p["timeInForce"] == "PostOnly" # PostOnly rides timeInForce assert "POST_ONLY" not in p["type"] and "PostOnly" not in p["type"] def test_maker_payload_quantized_and_shaped(): p = _maker() assert p["symbol"] == "BTC-USDT" assert p["price"] == "64547.5" # floored to tick 0.1 (BUY) assert p["quantity"] == "0.0001" # floored to step assert p["positionSide"] == "BOTH" # one-way flatten idiom assert p["reduceOnly"] == "false" assert p["clientOrderID"] == "u-req-0" def test_market_payload_has_no_price_or_tif(): p = build_order_payload( asset="ETHUSDT", side=Side.SELL, method=ExecutionMethod.TAKER, limit_price=Decimal("0"), size=Decimal("0.5"), reduce_only=True, client_order_id="u-req-0", tick=Decimal("0.01"), step=Decimal("0.001"), ) assert p["type"] == "MARKET" assert "price" not in p and "timeInForce" not in p # a market cross carries neither assert p["reduceOnly"] == "true" # exit reduces def test_payload_refuses_dust_below_step(): # size that floors to zero must not silently submit a 0-qty order. with pytest.raises(ValueError): build_order_payload( asset="BTCUSDT", side=Side.BUY, method=ExecutionMethod.TAKER, limit_price=Decimal("0"), size=Decimal("0.00005"), reduce_only=False, client_order_id="u-req-0", tick=Decimal("0.1"), step=Decimal("0.0001"), )