PINK Phase 2 (G3): ExchangeEvent seam + BingxUserStream + mode-parity

exchange_event.py: abstract ExchangeEvent/ExchangeEventKind seam
venue.py: VenueAdapter extended with subscribe()/account_snapshot()
bingx_user_stream.py: PINK-only WS client with listenKey lifecycle,
  gzip, ping/pong, 24h rotation sentinel, reconnect backoff, gap-backfill
mock_venue.py: subscribe()/account_snapshot() for offline tests

Gate G3 mode-parity: WS and poll paths produce identical k_capital,
fees, realized PnL, reconcile status for same logical event sequence.
89/89 total offline tests pass.
This commit is contained in:
Codex
2026-06-01 20:33:44 +02:00
parent 615d24386e
commit 8135a4ae17
5 changed files with 1289 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
"""Venue adapter contracts for DITAv2."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, AsyncIterator, Dict, List, Optional, Protocol
from .contracts import (
KernelCommandType,
KernelIntent,
KernelEventKind,
TradeSide,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
from .exchange_event import ExchangeEvent
class VenueAdapter(Protocol):
"""Abstract venue adapter used by the kernel."""
def submit(self, intent: KernelIntent) -> List[VenueEvent]:
...
def cancel(self, order: VenueOrder, *, reason: str = "") -> List[VenueEvent]:
...
def open_orders(self) -> List[VenueOrder]:
...
def open_positions(self) -> List[Dict[str, Any]]:
...
def reconcile(self) -> List[VenueEvent]:
...
# ------------------------------------------------------------------
# Phase 2 — stream seam (spec G3)
# ------------------------------------------------------------------
async def subscribe(self) -> AsyncIterator[ExchangeEvent]:
"""
Yield ExchangeEvent instances in arrival order. Implementations
must handle reconnection, keepalive, and 24h rotation internally.
The iterator never terminates normally — callers cancel it on
shutdown. Both the WS and poll-failover paths implement this
interface so the kernel layer is source-agnostic.
"""
... # pragma: no cover
async def account_snapshot(self) -> ExchangeEvent:
"""
Return a single ACCOUNT_UPDATE + POSITION_UPDATE merged event
by calling the exchange REST API. Used for gap-backfill on
reconnect and as the poll-failover path.
"""
... # pragma: no cover