"""Core typed records for the exchange. Conventions (exact integer arithmetic everywhere): - prices are integers in *ticks* (tick_size in currency units is a property of the asset); - quantities are integers in *lots*; - cash is an integer in *cents* (1 currency unit = 100 cents); a trade's notional in cents is price_ticks * tick_cents * qty, where tick_cents = tick_size * 100 must be an integer. """ from __future__ import annotations from dataclasses import dataclass, field from enum import Enum, IntEnum from typing import Literal class Side(IntEnum): BUY = 1 SELL = -1 @property def opposite(self) -> "Side": return Side.SELL if self is Side.BUY else Side.BUY @property def label(self) -> str: return "buy" if self is Side.BUY else "sell" class OrderType(str, Enum): LIMIT = "limit" MARKET = "market" class OrderStatus(str, Enum): PENDING = "pending" # in the latency queue, not yet at the exchange OPEN = "open" # resting in the book (possibly partially filled) FILLED = "filled" CANCELLED = "cancelled" # cancelled by the owner (possibly after partial fills) REJECTED = "rejected" # never entered the book @dataclass(slots=True) class Order: order_id: int agent_id: int asset: int side: Side order_type: OrderType qty: int # original quantity (lots) price: int | None # limit price in ticks (None for market orders) submit_time: float # agent decision time (ticks) arrival_time: float # exchange arrival time = submit_time + latency remaining: int = 0 status: OrderStatus = OrderStatus.PENDING reserved_cash: int = 0 # cents reserved at acceptance (bids) reserved_qty: int = 0 # lots reserved at acceptance (asks) tag: str = "" # free-form (policy name, 'liquidation', ...) reject_reason: str = "" def __post_init__(self) -> None: if self.remaining == 0 and self.status is OrderStatus.PENDING: self.remaining = self.qty @property def filled(self) -> int: return self.qty - self.remaining @dataclass(slots=True) class Trade: trade_id: int time: float asset: int price: int # ticks (the resting order's price) qty: int buy_order_id: int sell_order_id: int buyer_id: int seller_id: int aggressor: Side # side of the incoming (taker) order maker_fee: int # cents paid by the maker (may be negative = rebate) taker_fee: int # cents paid by the taker @property def taker_id(self) -> int: return self.buyer_id if self.aggressor is Side.BUY else self.seller_id @property def maker_id(self) -> int: return self.seller_id if self.aggressor is Side.BUY else self.buyer_id EventKind = Literal[ "submit", "accept", "reject", "trade", "cancel", "expire", "halt", "resume", "dividend", "margin_call", "liquidate", "bankrupt", "tick", "auction", ] @dataclass(slots=True) class Event: """One row of the event-sourced log. `payload` is a flat dict of scalars (parquet-friendly).""" seq: int time: float kind: str payload: dict = field(default_factory=dict) @dataclass(slots=True) class OrderIntent: """What an agent *wants* to do; converted to an Order by the exchange on arrival.""" asset: int side: Side order_type: OrderType qty: int price: int | None = None # ticks tag: str = "" @dataclass(slots=True) class CancelIntent: order_id: int @dataclass(slots=True) class Action: """A bundle of intents emitted by one agent at one decision time.""" orders: list[OrderIntent] = field(default_factory=list) cancels: list[CancelIntent] = field(default_factory=list) meta: dict = field(default_factory=dict) # e.g. LLM rationale/confidence/validity flags @property def is_hold(self) -> bool: return not self.orders and not self.cancels