"""Agent interface: a Policy maps a canonical Observation to an Action. The Observation is the *only* channel from the market to any policy (classical or LLM); it carries no latent state and no future information. The same object is serialized into the LLM prompt, so matched conditions see literally the same information. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Protocol import numpy as np from ..exchange.orders import Action @dataclass(slots=True) class BookView: best_bid: int | None best_ask: int | None bids: list[tuple[int, int]] # (price_ticks, qty) best first asks: list[tuple[int, int]] mid: float last_price: int | None halted: bool @dataclass(slots=True) class OwnOrderView: order_id: int asset: int side: int qty: int remaining: int price: int | None age: float @dataclass(slots=True) class Observation: t: int agent_id: int books: dict[int, BookView] mid_history: dict[int, np.ndarray] # last `lookback` mids per asset (ticks) recent_trades: dict[int, list[tuple[float, int, int, int]]] # (time, price, qty, aggressor) last_dividend: dict[int, float] # cents per lot, last paid public_value: dict[int, float] # cents per lot: PV under public information (announced dividends + news) x_hat_public: dict[int, float] # public posterior mean of the upcoming dividend deviation (cents) news: list # NewsItem list visible in window private_signals: dict[int, float] # asset -> signal about x_t (only for informed agents) signal_precision: dict[int, float] cash: int free_cash: int positions: dict[int, int] equity: float open_orders: list[OwnOrderView] leverage_cap: float short_allowed: bool max_order_qty: int tick_cents: dict[int, int] div_interval: int ticks_to_dividend: int r_period: float memory: list[str] = field(default_factory=list) # bounded memory (LLM agents) channel: list[str] = field(default_factory=list) # cohort messages (communication treatment) realized_pnl_history: list[float] = field(default_factory=list) class Policy(Protocol): name: str def decide(self, obs: Observation, rng: np.random.Generator) -> Action: ... @dataclass(slots=True) class AgentSpec: """Static description of one trader slot (assigned at population build time).""" agent_id: int policy_name: str policy_kwargs: dict cash: int positions: dict[int, int] latency: float activity: float # per-tick probability of being activated (classical) decision_interval: int = 1 # LLM agents: decide every `decision_interval` ticks decision_offset: int = 0 signal_precision: float = 0.0 # 0 => uninformed leverage_cap: float = 1.0 short_allowed: bool = False stratum: str = "" # wealth/information/latency stratum used for randomization provider: str = "" # LLM provider label ('' for classical) model: str = "" prompt_id: str = "" cohort: int = -1 # communication cohort (-1: none) def clip_qty(q: float, max_q: int) -> int: return int(max(0, min(max_q, round(q))))