malkhut: P0 guard + P1 BingX protective strings + tests fixed

P0 (safety): adapter.py rejects unmapped types (OCO, TP_SL) via
  is_type_available() guard. Returns None instead of silent LIMIT fallback.

P1 (BingX strings): STOP_MARKET → STOP_MARKET (protective, reduce-only)
  STOP_LIMIT → STOP (protective)
  TRIGGER_MARKET stays generic MIT
  TRAILING_STOP → TRAILING_STOP_MARKET

Tests updated to match corrected mappings.
This commit is contained in:
Codex
2026-07-18 15:08:57 +02:00
parent 041c879e82
commit 5503aafa28
3 changed files with 18 additions and 9 deletions

View File

@@ -213,8 +213,8 @@ class TestExchangeOrderTypeMap:
for ex in ("binance", "bingx", "bybit"):
assert EXCHANGE_ORDER_TYPE_MAP[ex]["MARKET"] == "MARKET"
def test_bingx_stop_market_is_trigger_market(self):
assert EXCHANGE_ORDER_TYPE_MAP["bingx"]["STOP_MARKET"] == "TRIGGER_MARKET"
def test_bingx_stop_market_is_protective(self):
assert EXCHANGE_ORDER_TYPE_MAP["bingx"]["STOP_MARKET"] == "STOP_MARKET"
def test_binance_stop_market_is_stop_market(self):
assert EXCHANGE_ORDER_TYPE_MAP["binance"]["STOP_MARKET"] == "STOP_MARKET"
@@ -234,8 +234,8 @@ class TestExchangeOrderTypeMap:
def test_binance_stop_limit_is_stop_loss_limit(self):
assert EXCHANGE_ORDER_TYPE_MAP["binance"]["STOP_LIMIT"] == "STOP_LOSS_LIMIT"
def test_bingx_stop_limit_is_trigger_limit(self):
assert EXCHANGE_ORDER_TYPE_MAP["bingx"]["STOP_LIMIT"] == "TRIGGER_LIMIT"
def test_bingx_stop_limit_is_protective(self):
assert EXCHANGE_ORDER_TYPE_MAP["bingx"]["STOP_LIMIT"] == "STOP"
def test_binance_trigger_market_is_take_profit(self):
assert EXCHANGE_ORDER_TYPE_MAP["binance"]["TRIGGER_MARKET"] == "TAKE_PROFIT"
@@ -289,7 +289,7 @@ class TestNormalizeTypeToExchange:
assert result is not None, f"{t.value} not mapped for {ex}"
def test_bingx_stop_market(self):
assert normalize_type_to_exchange(StdOrderType.STOP_MARKET, "bingx") == "TRIGGER_MARKET"
assert normalize_type_to_exchange(StdOrderType.STOP_MARKET, "bingx") == "STOP_MARKET"
def test_bingx_trailing_stop(self):
assert normalize_type_to_exchange(StdOrderType.TRAILING_STOP, "bingx") == "TRAILING_STOP_MARKET"

View File

@@ -92,8 +92,10 @@ EXCHANGE_ORDER_TYPE_MAP: Dict[str, Dict[str, str]] = {
},
"bingx": {
"LIMIT": "LIMIT", "MARKET": "MARKET",
"STOP_MARKET": "TRIGGER_MARKET", "STOP_LIMIT": "TRIGGER_LIMIT",
"TRIGGER_MARKET": "TRIGGER_MARKET", "TRIGGER_LIMIT": "TRIGGER_LIMIT",
"STOP_MARKET": "STOP_MARKET", # protective exit (reduce-only, position-tied)
"STOP_LIMIT": "STOP", # BingX protective stop-limit
"TRIGGER_MARKET": "TRIGGER_MARKET", # generic MIT (plan order by stopPrice)
"TRIGGER_LIMIT": "TRIGGER_LIMIT",
"TRAILING_STOP": "TRAILING_STOP_MARKET",
},
"bybit": {

View File

@@ -179,10 +179,17 @@ class BingXVenueAdapter:
# Determine order type — use standardized OrderType, map to BingX-native
from malkhut.training.order_types import (
normalize_type_to_exchange, normalize_tif_to_exchange,
normalize_type_to_exchange, normalize_tif_to_exchange, is_type_available,
)
if action.order_type is not None:
order_type = normalize_type_to_exchange(action.order_type, "bingx") or "LIMIT"
if not is_type_available(action.order_type, "bingx"):
import logging
logging.getLogger(__name__).error(
"REJECT: %s unavailable on bingx — not silently downgrading to LIMIT",
action.order_type.value,
)
return None # reject — do NOT submit
order_type = normalize_type_to_exchange(action.order_type, "bingx")
elif action.kind.value == "CROSS_SPREAD":
order_type = "MARKET"
else: