130 lines
5.3 KiB
Python
130 lines
5.3 KiB
Python
|
|
"""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"),
|
||
|
|
)
|