from __future__ import annotations from decimal import Decimal from typing import Literal from pydantic import BaseModel, Field # REST AssetPairs still reports v1 names (XBT/USD, XDG/USD); WebSocket v2 uses BTC/USD, DOGE/USD. _V2_RENAMES = {"XBT": "BTC", "XDG": "DOGE"} def normalize_symbol(wsname: str) -> str: base, _, quote = wsname.partition("/") return f"{_V2_RENAMES.get(base, base)}/{_V2_RENAMES.get(quote, quote)}" if quote else wsname PairStatus = Literal["online", "cancel_only", "post_only", "limit_only", "reduce_only", "unknown"] class PairMeta(BaseModel): """Tradable pair metadata from /0/public/AssetPairs. Exchange capabilities are *discovered*, not assumed.""" pair_id: str = Field(description="Kraken internal key, e.g. XXBTZUSD") altname: str wsname: str = Field(description="WebSocket v2 symbol, e.g. BTC/USD") base: str quote: str status: PairStatus = "unknown" ordermin: Decimal = Decimal("0") costmin: Decimal = Decimal("0") tick_size: Decimal = Decimal("0") pair_decimals: int = 8 lot_decimals: int = 8 cost_decimals: int = 8 lot_multiplier: int = 1 leverage_buy: list[int] = Field(default_factory=list) leverage_sell: list[int] = Field(default_factory=list) long_position_limit: int | None = None short_position_limit: int | None = None aclass_base: str = "currency" aclass_quote: str = "currency" execution_venue: str | None = None @classmethod def from_kraken(cls, key: str, d: dict) -> PairMeta: return cls( pair_id=key, altname=d.get("altname", key), wsname=normalize_symbol(d.get("wsname", d.get("altname", key))), base=d.get("base", ""), quote=d.get("quote", ""), status=d.get("status", "unknown") if d.get("status") in ("online", "cancel_only", "post_only", "limit_only", "reduce_only") else "unknown", ordermin=Decimal(str(d.get("ordermin", "0"))), costmin=Decimal(str(d.get("costmin", "0"))), tick_size=Decimal(str(d.get("tick_size", "0"))), pair_decimals=int(d.get("pair_decimals", 8)), lot_decimals=int(d.get("lot_decimals", 8)), cost_decimals=int(d.get("cost_decimals", 8)), lot_multiplier=int(d.get("lot_multiplier", 1)), leverage_buy=[int(x) for x in d.get("leverage_buy", [])], leverage_sell=[int(x) for x in d.get("leverage_sell", [])], long_position_limit=d.get("long_position_limit"), short_position_limit=d.get("short_position_limit"), aclass_base=d.get("aclass_base", "currency"), aclass_quote=d.get("aclass_quote", "currency"), execution_venue=d.get("execution_venue"), ) class Candle(BaseModel): """OHLC bar. `ts` is the bar OPEN time (UTC seconds). The bar is only *known* at ts + interval*60.""" ts: int open: float high: float low: float close: float vwap: float volume: float count: int @property def close_time(self) -> int: # requires interval; see Candle.closed_at raise NotImplementedError def closed_at(self, interval_min: int) -> int: return self.ts + interval_min * 60 class Trade(BaseModel): ts: float price: float volume: float side: Literal["buy", "sell"] order_type: Literal["market", "limit"] trade_id: int | None = None class BookLevel(BaseModel): price: Decimal qty: Decimal class Ticker(BaseModel): symbol: str bid: float bid_qty: float ask: float ask_qty: float last: float volume: float vwap: float low: float high: float change: float change_pct: float ts: float class SystemStatus(BaseModel): status: str timestamp: str