Files
siloqy/prod/clean_arch/dita_v2/exchange_event.py

94 lines
3.1 KiB
Python
Raw Normal View History

"""Abstract exchange-event seam for DITAv2 (spec G3).
ExchangeEvent is the normalised, exchange-agnostic type that flows from
the BingX adapter (or poll-failover synthesizer) into AccountProjectionV2
and the reconcile layer. No BingX field names, URLs, or listenKey
semantics cross this boundary.
Both the WebSocket path and the REST-poll path produce the same
ExchangeEvent types so that the two sources are interchangeable this is
the VSTLIVE symmetry guarantee (gate G3 mode-parity test).
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, Tuple
class ExchangeEventKind(str, Enum):
FULL_FILL = "FULL_FILL"
PARTIAL_FILL = "PARTIAL_FILL"
ORDER_ACK = "ORDER_ACK"
ORDER_REJECT = "ORDER_REJECT"
CANCEL_ACK = "CANCEL_ACK"
CANCEL_REJECT = "CANCEL_REJECT"
ACCOUNT_UPDATE = "ACCOUNT_UPDATE" # wallet balance / margin facts
POSITION_UPDATE = "POSITION_UPDATE" # open-position facts
FUNDING_FEE = "FUNDING_FEE"
RECONNECTED = "RECONNECTED" # adapter internal — stream resumed
UNKNOWN = "UNKNOWN"
@dataclass(frozen=True)
class ExchangePosition:
"""Single open position as normalised by the adapter."""
symbol: str = ""
qty: float = 0.0
entry_price: float = 0.0
mark_price: float = 0.0
unrealized_pnl: float = 0.0
leverage: float = 1.0
side: str = "" # "LONG" | "SHORT"
@dataclass(frozen=True)
class ExchangeEvent:
"""
Normalised exchange event the abstract seam between the adapter
(BingX WS or REST poll) and the kernel/account layer.
Immutable. All exchange-specific field names are resolved by the
adapter before this type is constructed. Callers should check `kind`
before reading kind-specific fields (fill_price/qty, wallet_balance,
funding_amount, etc.) unused fields default to zero/empty.
"""
kind: ExchangeEventKind
event_id: str # dedup key (venue-assigned or synthetic uuid)
exchange_ts: int # exchange-assigned timestamp ms
# --- FILL / PARTIAL_FILL ---
fill_price: float = 0.0
fill_qty: float = 0.0 # incremental fill quantity
fee: float = 0.0
fee_asset: str = ""
realized_pnl: float = 0.0
order_id: str = "" # venue order id
client_order_id: str = ""
symbol: str = ""
# --- ACCOUNT_UPDATE ---
wallet_balance: float = 0.0
available_margin: float = 0.0
used_margin: float = 0.0
maint_margin: float = 0.0
# --- POSITION_UPDATE ---
positions: Tuple[ExchangePosition, ...] = ()
# --- FUNDING_FEE ---
funding_amount: float = 0.0 # positive = received, negative = paid
funding_ts: int = 0
# --- Order type / maker-taker ---
is_maker: bool = False # True when limit order rested and filled (maker)
# --- Source metadata ---
source: str = "ws" # "ws" | "poll"
raw: Dict = field(default_factory=dict) # original frame (debug only)
def is_fill(self) -> bool:
return self.kind in {ExchangeEventKind.FULL_FILL, ExchangeEventKind.PARTIAL_FILL}