from enum import StrEnum from typing import Annotated from pydantic import BaseModel, Field, BeforeValidator, PlainSerializer # --------------------------------------------------------------------------- # Scalar coercion helpers # --------------------------------------------------------------------------- def _to_int(v: int | str) -> int: return int(v, 0) if isinstance(v, str) else int(v) def _to_float(v: float | int | str) -> float: return float(v) _int_to_str = PlainSerializer(str, return_type=str, when_used="json") _float_to_str = PlainSerializer(str, return_type=str, when_used="json") # Always stored as Python int; accepts int or string on input; serialises to string in JSON. type Uint8 = Annotated[int, BeforeValidator(_to_int), _int_to_str] type Uint16 = Annotated[int, BeforeValidator(_to_int), _int_to_str] type Uint24 = Annotated[int, BeforeValidator(_to_int), _int_to_str] type Uint32 = Annotated[int, BeforeValidator(_to_int), _int_to_str] type Uint64 = Annotated[int, BeforeValidator(_to_int), _int_to_str] type Uint256 = Annotated[int, BeforeValidator(_to_int), _int_to_str] type Float = Annotated[float, BeforeValidator(_to_float), _float_to_str] ETH_ADDRESS_PATTERN = r"^0x[0-9a-fA-F]{40}$" # --------------------------------------------------------------------------- # Enums # --------------------------------------------------------------------------- class Exchange(StrEnum): UNISWAP_V2 = "UniswapV2" UNISWAP_V3 = "UniswapV3" class Side(StrEnum): """Order side: buy or sell""" BUY = "BUY" SELL = "SELL" class AmountType(StrEnum): """Whether the order amount refers to base or quote currency""" BASE = "BASE" # Amount is in base currency (e.g., BTC in BTC/USD) QUOTE = "QUOTE" # Amount is in quote currency (e.g., USD in BTC/USD) class TimeInForce(StrEnum): """Order lifetime specification""" GTC = "GTC" # Good Till Cancel IOC = "IOC" # Immediate or Cancel FOK = "FOK" # Fill or Kill DAY = "DAY" # Good for trading day GTD = "GTD" # Good Till Date class ConditionalOrderMode(StrEnum): """How conditional orders behave on partial fills""" NEW_PER_FILL = "NEW_PER_FILL" # Create new conditional order per each fill UNIFIED_ADJUSTING = "UNIFIED_ADJUSTING" # Single conditional order that adjusts amount class TriggerType(StrEnum): """Type of conditional trigger""" STOP_LOSS = "STOP_LOSS" TAKE_PROFIT = "TAKE_PROFIT" STOP_LIMIT = "STOP_LIMIT" TRAILING_STOP = "TRAILING_STOP" class TickSpacingMode(StrEnum): """How price tick spacing is determined""" FIXED = "FIXED" # Fixed tick size DYNAMIC = "DYNAMIC" # Tick size varies by price level CONTINUOUS = "CONTINUOUS" # No tick restrictions class AssetType(StrEnum): """Type of tradeable asset""" SPOT = "SPOT" # Spot/cash market MARGIN = "MARGIN" # Margin trading PERP = "PERP" # Perpetual futures FUTURE = "FUTURE" # Dated futures OPTION = "OPTION" # Options SYNTHETIC = "SYNTHETIC" # Synthetic/derived instruments class OcoMode(StrEnum): NO_OCO = "NO_OCO" CANCEL_ON_PARTIAL_FILL = "CANCEL_ON_PARTIAL_FILL" CANCEL_ON_COMPLETION = "CANCEL_ON_COMPLETION" # --------------------------------------------------------------------------- # Supporting models # --------------------------------------------------------------------------- class Route(BaseModel): model_config = {"extra": "forbid"} exchange: Exchange fee: Uint24 = Field(description="Pool fee tier; also used as maxFee on UniswapV3") class Line(BaseModel): """Price line: price = intercept + slope * time. Both zero means line is disabled.""" model_config = {"extra": "forbid"} intercept: Float slope: Float class Tranche(BaseModel): model_config = {"extra": "forbid"} fraction: Uint16 = Field(description="Fraction of total order amount; MAX_FRACTION (65535) = 100%") startTimeIsRelative: bool endTimeIsRelative: bool minIsBarrier: bool = Field(description="Not yet supported") maxIsBarrier: bool = Field(description="Not yet supported") marketOrder: bool = Field( description="If true, min/max lines ignored; minLine intercept treated as max slippage" ) minIsRatio: bool maxIsRatio: bool rateLimitFraction: Uint16 = Field(description="Max fraction of this tranche's amount per rate-limited execution") rateLimitPeriod: Uint24 = Field(description="Seconds between rate limit resets") startTime: Uint32 = Field(description="Unix timestamp; 0 (DISTANT_PAST) effectively disables") endTime: Uint32 = Field(description="Unix timestamp; 4294967295 (DISTANT_FUTURE) effectively disables") minLine: Line = Field(description="Traditional limit order constraint; can be diagonal") maxLine: Line = Field(description="Upper price boundary (too-good-a-price guard)") class TrancheStatus(BaseModel): model_config = {"extra": "forbid"} filled: Uint256 = Field(description="Amount filled by this tranche") activationTime: Uint32 = Field(description="Earliest time this tranche can execute; 0 = not yet concrete") startTime: Uint32 = Field(description="Concrete start timestamp") endTime: Uint32 = Field(description="Concrete end timestamp") # --------------------------------------------------------------------------- # Standard Order Models # --------------------------------------------------------------------------- class ConditionalTrigger(BaseModel): """Conditional order trigger (stop-loss, take-profit, etc.)""" model_config = {"extra": "forbid"} trigger_type: TriggerType trigger_price: Float = Field(description="Price at which conditional order activates") trailing_delta: Float | None = Field(default=None, description="For trailing stops: delta from peak/trough") class AmountConstraints(BaseModel): """Constraints on order amounts for a symbol""" model_config = {"extra": "forbid"} min_amount: Float = Field(description="Minimum order amount") max_amount: Float = Field(description="Maximum order amount") step_size: Float = Field(description="Amount increment granularity") class PriceConstraints(BaseModel): """Constraints on order pricing for a symbol""" model_config = {"extra": "forbid"} tick_spacing_mode: TickSpacingMode tick_size: Float | None = Field(default=None, description="Fixed tick size (if FIXED mode)") min_price: Float | None = Field(default=None, description="Minimum allowed price") max_price: Float | None = Field(default=None, description="Maximum allowed price") class MarketCapabilities(BaseModel): """Describes what order features a market supports""" model_config = {"extra": "forbid"} supported_sides: list[Side] = Field(description="Supported order sides (usually both)") supported_amount_types: list[AmountType] = Field(description="Whether BASE, QUOTE, or both amounts are supported") supports_market_orders: bool = Field(description="Whether market orders are supported") supports_limit_orders: bool = Field(description="Whether limit orders are supported") supported_time_in_force: list[TimeInForce] = Field(description="Supported order lifetimes") supports_conditional_orders: bool = Field(description="Whether stop-loss/take-profit are supported") supported_trigger_types: list[TriggerType] = Field(default_factory=list, description="Supported trigger types") supports_post_only: bool = Field(default=False, description="Whether post-only orders are supported") supports_reduce_only: bool = Field(default=False, description="Whether reduce-only orders are supported") supports_iceberg: bool = Field(default=False, description="Whether iceberg orders are supported") market_order_amount_type: AmountType | None = Field( default=None, description="Required amount type for market orders (some DEXs require exact-in)" ) class SymbolMetadata(BaseModel): """Complete metadata describing a tradeable symbol/market""" model_config = {"extra": "forbid"} symbol_id: str = Field(description="Unique symbol identifier") base_asset: str = Field(description="Base asset (e.g., 'BTC')") quote_asset: str = Field(description="Quote asset (e.g., 'USD')") asset_type: AssetType = Field(description="Type of market") exchange: str = Field(description="Exchange identifier") amount_constraints: AmountConstraints price_constraints: PriceConstraints capabilities: MarketCapabilities contract_size: Float | None = Field(default=None, description="For futures/options: contract multiplier") settlement_asset: str | None = Field(default=None, description="For derivatives: settlement currency") expiry_timestamp: Uint64 | None = Field(default=None, description="For dated futures/options: expiration") class StandardOrder(BaseModel): """Standard order specification for exchange kernels""" model_config = {"extra": "forbid"} symbol_id: str = Field(description="Symbol to trade") side: Side = Field(description="Buy or sell") amount: Float = Field(description="Order amount") amount_type: AmountType = Field(description="Whether amount is BASE or QUOTE currency") limit_price: Float | None = Field(default=None, description="Limit price (None = market order)") time_in_force: TimeInForce = Field(default=TimeInForce.GTC, description="Order lifetime") good_till_date: Uint64 | None = Field(default=None, description="Expiry timestamp for GTD orders") conditional_trigger: ConditionalTrigger | None = Field( default=None, description="Stop-loss/take-profit trigger" ) conditional_mode: ConditionalOrderMode | None = Field( default=None, description="How conditional orders behave on partial fills" ) reduce_only: bool = Field(default=False, description="Only reduce existing position") post_only: bool = Field(default=False, description="Only make, never take") iceberg_qty: Float | None = Field(default=None, description="Visible amount for iceberg orders") client_order_id: str | None = Field(default=None, description="Client-specified order ID") class StandardOrderStatus(BaseModel): """Current status of a standard order""" model_config = {"extra": "forbid"} order: StandardOrder order_id: str = Field(description="Exchange-assigned order ID") status: str = Field(description="Order status: NEW, PARTIALLY_FILLED, FILLED, CANCELED, REJECTED, EXPIRED") filled_amount: Float = Field(description="Amount filled so far") average_fill_price: Float = Field(description="Average execution price") created_at: Uint64 = Field(description="Order creation timestamp") updated_at: Uint64 = Field(description="Last update timestamp") # --------------------------------------------------------------------------- # Order models # --------------------------------------------------------------------------- class SwapOrder(BaseModel): model_config = {"extra": "forbid"} tokenIn: str = Field(pattern=ETH_ADDRESS_PATTERN, description="ERC-20 input token address") tokenOut: str = Field(pattern=ETH_ADDRESS_PATTERN, description="ERC-20 output token address") route: Route amount: Uint256 = Field(description="Maximum quantity to fill") minFillAmount: Uint256 = Field(description="Minimum tranche amount before tranche is considered complete") amountIsInput: bool = Field(description="true = amount is tokenIn quantity; false = tokenOut") outputDirectlyToOwner: bool = Field(description="true = proceeds go to vault owner; false = vault") inverted: bool = Field(description="false = tokenIn/tokenOut price direction (Uniswap natural)") conditionalOrder: Uint64 = Field( description="NO_CONDITIONAL_ORDER = 2^64-1; high bit set = relative index within placement group" ) tranches: list[Tranche] = Field(min_length=1) class StandardOrderGroup(BaseModel): """Group of orders with OCO (One-Cancels-Other) relationship""" model_config = {"extra": "forbid"} mode: OcoMode orders: list[StandardOrder] = Field(min_length=1) # --------------------------------------------------------------------------- # Legacy swap order models (kept for backward compatibility) # --------------------------------------------------------------------------- class OcoGroup(BaseModel): """DEPRECATED: Use StandardOrderGroup instead""" model_config = {"extra": "forbid"} mode: OcoMode orders: list[SwapOrder] = Field(min_length=1) class SwapOrderStatus(BaseModel): model_config = {"extra": "forbid"} order: SwapOrder fillFeeHalfBps: Uint8 = Field(description="Fill fee in half-bps (1/20000); max 255 = 1.275%") canceled: bool = Field(description="If true, order is canceled regardless of cancelAllIndex") startTime: Uint32 = Field(description="Earliest block.timestamp at which order may execute") ocoGroup: Uint64 = Field(description="Index into ocoGroups; NO_OCO_INDEX = 2^64-1") originalOrder: Uint64 = Field(description="Index of the original order in the orders array") startPrice: Uint256 = Field(description="Price at order start") filled: Uint256 = Field(description="Total amount filled so far") trancheStatus: list[TrancheStatus]