malkhut: standardized order types — FIX/CCXT-aligned, multi-exchange mapping

order_types.py: Five-layer taxonomy normalized to industry standards:
  Layer 1: Base types (FIX Tag 40) — MARKET, LIMIT
  Layer 2: Time-in-force (FIX Tag 59) — GTC, IOC, FOK, GTD
  Layer 3: Conditional/Trigger (FIX Tag 3/4+MIT) — STOP_MARKET, STOP_LIMIT,
    TRIGGER_MARKET, TRIGGER_LIMIT, TRAILING_STOP
  Layer 4: Instructions (FIX Tag 18) — POST_ONLY, REDUCE_ONLY, HIDDEN, ICEBERG
  Layer 5: Compound (exchange-specific) — OCO, TP_SL

Cross-exchange mapping: BingX ↔ Binance ↔ Bybit (from CCXT source code).
Standards: FIX 4.4 Tag 40/59/18, CCXT unified API, ISO 10383 (MIC).

Transferability: strategy PARAMETERS transfer. ORDER TYPE NAMES are
venue-specific but semantics identical (LIMIT = LIMIT everywhere).

14 tests. README updated with full mapping table and standards references.
This commit is contained in:
Codex
2026-07-14 12:02:01 +02:00
parent 13811cc789
commit 53e02c84ec
3 changed files with 251 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
"""
Tests for standardized order types — FIX/CCXT-aligned, multi-exchange.
"""
import pytest
from malkhut.training.order_types import (
BaseType, TimeInForce, ConditionalType, OrderInstruction, CompoundOrder,
EXCHANGE_ORDER_MAP, normalize_to_exchange, get_supported_types, is_type_available,
)
class TestOrderTypeEnums:
def test_base_types(self):
assert BaseType.MARKET.value == "market"
assert BaseType.LIMIT.value == "limit"
def test_time_in_force(self):
assert TimeInForce.GTC.value == "gtc"
assert TimeInForce.IOC.value == "ioc"
assert TimeInForce.FOK.value == "fok"
def test_conditional_types(self):
assert ConditionalType.STOP_MARKET.value == "stop_market"
assert ConditionalType.STOP_LIMIT.value == "stop_limit"
assert ConditionalType.TRAILING_STOP.value == "trailing_stop"
def test_instructions(self):
assert OrderInstruction.POST_ONLY.value == "post_only"
assert OrderInstruction.REDUCE_ONLY.value == "reduce_only"
def test_compound(self):
assert CompoundOrder.OCO.value == "oco"
assert CompoundOrder.TP_SL.value == "tp_sl"
class TestExchangeMapping:
def test_binance_has_all_types(self):
types = get_supported_types("binance")
assert "limit" in types
assert "market" in types
assert "stop_market" in types
assert "post_only" in types
def test_bingx_has_all_types(self):
types = get_supported_types("bingx")
assert "limit" in types
assert "market" in types
assert "trigger_market" in types
def test_bybit_has_all_types(self):
types = get_supported_types("bybit")
assert "limit" in types
assert "market" in types
assert "stop_market" in types
def test_normalize_limit_all_exchanges(self):
"""LIMIT should map to LIMIT on all exchanges."""
for ex in ["binance", "bingx", "bybit"]:
assert normalize_to_exchange("limit", ex) == "LIMIT"
def test_normalize_market_all_exchanges(self):
"""MARKET should map to MARKET on all exchanges."""
for ex in ["binance", "bingx", "bybit"]:
assert normalize_to_exchange("market", ex) == "MARKET"
def test_normalize_post_only_varies(self):
"""POST_ONLY maps differently across exchanges."""
assert normalize_to_exchange("post_only", "binance") == "LIMIT_MAKER"
assert normalize_to_exchange("post_only", "bingx") == "POST_ONLY"
assert normalize_to_exchange("post_only", "bybit") == "POST_ONLY"
def test_normalize_unknown_type(self):
assert normalize_to_exchange("invalid_type", "binance") is None
def test_is_type_available(self):
assert is_type_available("limit", "binance")
assert is_type_available("stop_market", "bingx")
assert not is_type_available("invalid", "binance")
def test_get_supported_types_unique(self):
"""Each exchange should support at least 8 types."""
for ex in ["binance", "bingx", "bybit"]:
types = get_supported_types(ex)
assert len(types) >= 8

View File

@@ -0,0 +1,120 @@
"""
Standardized Order Types — FIX/CCXT-aligned, multi-exchange compatible.
Five-layer taxonomy normalized to industry standards:
Layer 1: Base types (FIX Tag 40 OrdType)
Layer 2: Time-in-force (FIX Tag 59)
Layer 3: Conditional/Trigger (FIX Tag 3/4 + MIT)
Layer 4: Instructions (FIX Tag 18)
Layer 5: Compound (exchange-specific)
Transferability: strategy PARAMETERS transfer across exchanges.
Order type NAMES are venue-specific but semantics are identical.
The venue adapter translates normalized → exchange-native at submission.
"""
from __future__ import annotations
from enum import Enum
from typing import Dict, Optional
# ==============================================================================
# Layer 1: Base Types (FIX Tag 40 OrdType)
# ==============================================================================
class BaseType(str, Enum):
MARKET = "market" # FIX: 1 — immediate execution at best available price
LIMIT = "limit" # FIX: 2 — execute only at specified price or better
# ==============================================================================
# Layer 2: Time-in-Force (FIX Tag 59)
# ==============================================================================
class TimeInForce(str, Enum):
GTC = "gtc" # FIX: 1 — Good Till Cancel
IOC = "ioc" # FIX: 3 — Immediate or Cancel (partial fill OK)
FOK = "fok" # FIX: 4 — Fill or Kill (all-or-nothing)
GTD = "gtd" # FIX: 6 — Good Till Date
# ==============================================================================
# Layer 3: Conditional/Trigger (FIX Tag 3/4 + MIT)
# ==============================================================================
class ConditionalType(str, Enum):
STOP_MARKET = "stop_market" # FIX: 3 — trigger → market
STOP_LIMIT = "stop_limit" # FIX: 4 — trigger → limit
TRIGGER_MARKET = "trigger_market" # FIX: J (MIT) — market if touched
TRIGGER_LIMIT = "trigger_limit" # FIX: 4 — limit if touched
TRAILING_STOP = "trailing_stop" # FIX: P + ExecInst — trailing stop
# ==============================================================================
# Layer 4: Instructions (FIX Tag 18 ExecInst)
# ==============================================================================
class OrderInstruction(str, Enum):
POST_ONLY = "post_only" # FIX: ExecInst — maker-only, reject if crossing
REDUCE_ONLY = "reduce_only" # FIX: ExecInst — only reduce existing position
HIDDEN = "hidden" # FIX: ExecInst — iceberg/hidden order
ICEBERG = "iceberg" # FIX: ExecInst — visible portion only
# ==============================================================================
# Layer 5: Compound (exchange-specific, no FIX equivalent)
# ==============================================================================
class CompoundOrder(str, Enum):
OCO = "oco" # One-Cancels-Other (linked orders)
TP_SL = "tp_sl" # Take-Profit / Stop-Loss (attached to position)
# ==============================================================================
# Exchange-specific mapping tables
# ==============================================================================
EXCHANGE_ORDER_MAP: Dict[str, Dict[str, str]] = {
"binance": {
"limit": "LIMIT", "market": "MARKET",
"stop_market": "STOP_MARKET", "stop_limit": "STOP_LOSS_LIMIT",
"trigger_market": "TAKE_PROFIT", "trigger_limit": "TAKE_PROFIT_LIMIT",
"trailing_stop": "TRAILING_STOP_MARKET",
"post_only": "LIMIT_MAKER", "ioc": "IOC", "fok": "FOK",
"reduce_only": "REDUCE_ONLY",
},
"bingx": {
"limit": "LIMIT", "market": "MARKET",
"stop_market": "TRIGGER_MARKET", "stop_limit": "TRIGGER_LIMIT",
"trigger_market": "TRIGGER_MARKET", "trigger_limit": "TRIGGER_LIMIT",
"trailing_stop": "TRIGGER_MARKET",
"post_only": "POST_ONLY", "ioc": "IOC", "fok": "FOK",
"reduce_only": "REDUCE_ONLY",
},
"bybit": {
"limit": "LIMIT", "market": "MARKET",
"stop_market": "STOP_MARKET", "stop_limit": "STOP_LIMIT",
"trigger_market": "TAKE_PROFIT_MARKET", "trigger_limit": "TAKE_PROFIT_LIMIT",
"trailing_stop": "TRAILING_STOP",
"post_only": "POST_ONLY", "ioc": "IOC", "fok": "FOK",
"reduce_only": "REDUCE_ONLY",
},
}
def normalize_to_exchange(normalized_type: str, exchange_id: str) -> Optional[str]:
"""Convert a normalized order type to exchange-native string."""
mapping = EXCHANGE_ORDER_MAP.get(exchange_id, {})
return mapping.get(normalized_type)
def get_supported_types(exchange_id: str) -> list[str]:
"""Get all normalized order types supported by an exchange."""
mapping = EXCHANGE_ORDER_MAP.get(exchange_id, {})
return list(mapping.keys())
def is_type_available(normalized_type: str, exchange_id: str) -> bool:
"""Check if a normalized order type is available on an exchange."""
mapping = EXCHANGE_ORDER_MAP.get(exchange_id, {})
return normalized_type in mapping