1"""Core typed records for the exchange.23Conventions (exact integer arithmetic everywhere):4- prices are integers in *ticks* (tick_size in currency units is a property of the asset);5- quantities are integers in *lots*;6- cash is an integer in *cents* (1 currency unit = 100 cents); a trade's notional in cents is7 price_ticks * tick_cents * qty, where tick_cents = tick_size * 100 must be an integer.8"""9from __future__ import annotations1011from dataclasses import dataclass, field12from enum import Enum, IntEnum13from typing import Literal141516class Side(IntEnum):17 BUY = 118 SELL = -11920 @property21 def opposite(self) -> "Side":22 return Side.SELL if self is Side.BUY else Side.BUY2324 @property25 def label(self) -> str:26 return "buy" if self is Side.BUY else "sell"272829class OrderType(str, Enum):30 LIMIT = "limit"31 MARKET = "market"323334class OrderStatus(str, Enum):35 PENDING = "pending" # in the latency queue, not yet at the exchange36 OPEN = "open" # resting in the book (possibly partially filled)37 FILLED = "filled"38 CANCELLED = "cancelled" # cancelled by the owner (possibly after partial fills)39 REJECTED = "rejected" # never entered the book404142@dataclass(slots=True)43class Order:44 order_id: int45 agent_id: int46 asset: int47 side: Side48 order_type: OrderType49 qty: int # original quantity (lots)50 price: int | None # limit price in ticks (None for market orders)51 submit_time: float # agent decision time (ticks)52 arrival_time: float # exchange arrival time = submit_time + latency53 remaining: int = 054 status: OrderStatus = OrderStatus.PENDING55 reserved_cash: int = 0 # cents reserved at acceptance (bids)56 reserved_qty: int = 0 # lots reserved at acceptance (asks)57 tag: str = "" # free-form (policy name, 'liquidation', ...)58 reject_reason: str = ""5960 def __post_init__(self) -> None:61 if self.remaining == 0 and self.status is OrderStatus.PENDING:62 self.remaining = self.qty6364 @property65 def filled(self) -> int:66 return self.qty - self.remaining676869@dataclass(slots=True)70class Trade:71 trade_id: int72 time: float73 asset: int74 price: int # ticks (the resting order's price)75 qty: int76 buy_order_id: int77 sell_order_id: int78 buyer_id: int79 seller_id: int80 aggressor: Side # side of the incoming (taker) order81 maker_fee: int # cents paid by the maker (may be negative = rebate)82 taker_fee: int # cents paid by the taker8384 @property85 def taker_id(self) -> int:86 return self.buyer_id if self.aggressor is Side.BUY else self.seller_id8788 @property89 def maker_id(self) -> int:90 return self.seller_id if self.aggressor is Side.BUY else self.buyer_id919293EventKind = Literal[94 "submit", "accept", "reject", "trade", "cancel", "expire", "halt", "resume",95 "dividend", "margin_call", "liquidate", "bankrupt", "tick", "auction",96]979899@dataclass(slots=True)100class Event:101 """One row of the event-sourced log. `payload` is a flat dict of scalars (parquet-friendly)."""102 seq: int103 time: float104 kind: str105 payload: dict = field(default_factory=dict)106107108@dataclass(slots=True)109class OrderIntent:110 """What an agent *wants* to do; converted to an Order by the exchange on arrival."""111 asset: int112 side: Side113 order_type: OrderType114 qty: int115 price: int | None = None # ticks116 tag: str = ""117118119@dataclass(slots=True)120class CancelIntent:121 order_id: int122123124@dataclass(slots=True)125class Action:126 """A bundle of intents emitted by one agent at one decision time."""127 orders: list[OrderIntent] = field(default_factory=list)128 cancels: list[CancelIntent] = field(default_factory=list)129 meta: dict = field(default_factory=dict) # e.g. LLM rationale/confidence/validity flags130131 @property132 def is_hold(self) -> bool:133 return not self.orders and not self.cancels134