PINK: fix EXIT position not closing — 3 root causes, 368/368 tests green
Root cause 1 (http.py): duplicate signature= in POST body — canonical_query
included signature key after build_signed_params injected it, then body
appended &signature= again. Fix: exclude 'signature' from canonical.
Root cause 2 (bingx_direct + http.py): HTTP retry sent same MARKET order to
backup URL (bingx.pro), which hits the same VST account. Without clientOrderId,
each retry opened a new SHORT position; EXIT BUY 10 only closed one. Fix:
restore clientOrderId in hyphen format p-{e/x}-{base36_ts}-{rand4} (pure
alphanumeric rejected by VST; hyphen format accepted). Adds max_retries_override
+ urls_to_try to _request_json for non-idempotent override path.
Root cause 3 (flat_and_start_pink): k.venue.connect() ran backend.connect()
inside asyncio.run() in a thread-pool. httpx session created there references
a dead event loop; order POSTs raise RuntimeError("Event loop is closed").
Fix: await adapter.connect() directly from main event loop.
Also: enter_wall_ms + tight _is_our_position createTime filter to separate
PINK's position from concurrent strategies on shared VST account. 1.5s
settle sleep before flat check.
New test suite test_bingx_http_safety.py: 20 tests covering idempotency,
retry correctness, backup-URL dedup, event-loop hygiene, signing correctness.
Live result: ENTER 290ms, EXIT 260ms — both sub-second. Position flat.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -643,3 +643,66 @@ class TestCancelBranchAudit:
|
||||
order = _make_order()
|
||||
with pytest.raises(RuntimeError, match="cancel surface"):
|
||||
venue.cancel(order)
|
||||
|
||||
|
||||
class TestHttpSigningBodyNoDuplicateSignature:
|
||||
"""Regression: http.py was appending signature= twice in POST body.
|
||||
|
||||
build_signed_params injects 'signature' into the returned dict.
|
||||
canonical_query(payload) then serialised it, then
|
||||
f"{canonical}&signature={payload['signature']}" appended it again.
|
||||
BingX received body with two signature= fields. Fix: exclude
|
||||
'signature' from canonical before appending.
|
||||
"""
|
||||
|
||||
def test_no_duplicate_signature_in_post_body(self):
|
||||
from prod.bingx.signing import build_signed_params, canonical_query
|
||||
import uuid
|
||||
|
||||
secret = "testsecret1234567890"
|
||||
params = {
|
||||
"symbol": "TRX-USDT",
|
||||
"side": "SELL",
|
||||
"positionSide": "BOTH",
|
||||
"type": "MARKET",
|
||||
"quantity": "10.0",
|
||||
"clientOrderId": uuid.uuid4().hex,
|
||||
}
|
||||
signed = build_signed_params(params, secret, recv_window_ms=5000)
|
||||
|
||||
# Replicate fixed http.py body construction
|
||||
canonical = canonical_query({k: v for k, v in signed.items() if k != "signature"})
|
||||
body = f"{canonical}&signature={signed['signature']}"
|
||||
|
||||
assert body.count("signature") == 1, (
|
||||
"POST body must contain exactly one 'signature=' field"
|
||||
)
|
||||
# signature must be the last field (appended, not embedded)
|
||||
assert body.endswith(f"&signature={signed['signature']}"), (
|
||||
"signature must be the final field in the POST body"
|
||||
)
|
||||
|
||||
def test_canonical_without_signature_matches_hmac_input(self):
|
||||
"""The canonical query we send must match exactly what HMAC was computed over."""
|
||||
from prod.bingx.signing import build_signed_params, canonical_query, sign_query
|
||||
import uuid
|
||||
|
||||
secret = "anothertestsecret99"
|
||||
params = {
|
||||
"symbol": "ETH-USDT",
|
||||
"side": "BUY",
|
||||
"type": "MARKET",
|
||||
"quantity": "1.0",
|
||||
"clientOrderId": uuid.uuid4().hex,
|
||||
}
|
||||
signed = build_signed_params(params, secret, recv_window_ms=5000)
|
||||
|
||||
# The string HMAC was computed over (inside build_signed_params)
|
||||
signed_without_sig = {k: v for k, v in signed.items() if k != "signature"}
|
||||
expected_hmac_input = canonical_query(signed_without_sig)
|
||||
|
||||
# Re-derive HMAC from the canonical we're about to send
|
||||
recomputed_sig = sign_query(secret, expected_hmac_input)
|
||||
assert recomputed_sig == signed["signature"], (
|
||||
"canonical without signature must reproduce the HMAC"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user