Files
ai/backend.old/src/schema/order_spec.py
2026-03-11 18:47:11 -04:00

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]