328 lines
13 KiB
Python
328 lines
13 KiB
Python
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]
|
|
|
|
|