1"""Agent interface: a Policy maps a canonical Observation to an Action.23The Observation is the *only* channel from the market to any policy (classical or LLM); it carries4no latent state and no future information. The same object is serialized into the LLM prompt, so5matched conditions see literally the same information.6"""7from __future__ import annotations89from dataclasses import dataclass, field10from typing import Protocol1112import numpy as np1314from ..exchange.orders import Action151617@dataclass(slots=True)18class BookView:19 best_bid: int | None20 best_ask: int | None21 bids: list[tuple[int, int]] # (price_ticks, qty) best first22 asks: list[tuple[int, int]]23 mid: float24 last_price: int | None25 halted: bool262728@dataclass(slots=True)29class OwnOrderView:30 order_id: int31 asset: int32 side: int33 qty: int34 remaining: int35 price: int | None36 age: float373839@dataclass(slots=True)40class Observation:41 t: int42 agent_id: int43 books: dict[int, BookView]44 mid_history: dict[int, np.ndarray] # last `lookback` mids per asset (ticks)45 recent_trades: dict[int, list[tuple[float, int, int, int]]] # (time, price, qty, aggressor)46 last_dividend: dict[int, float] # cents per lot, last paid47 public_value: dict[int, float] # cents per lot: PV under public information (announced dividends + news)48 x_hat_public: dict[int, float] # public posterior mean of the upcoming dividend deviation (cents)49 news: list # NewsItem list visible in window50 private_signals: dict[int, float] # asset -> signal about x_t (only for informed agents)51 signal_precision: dict[int, float]52 cash: int53 free_cash: int54 positions: dict[int, int]55 equity: float56 open_orders: list[OwnOrderView]57 leverage_cap: float58 short_allowed: bool59 max_order_qty: int60 tick_cents: dict[int, int]61 div_interval: int62 ticks_to_dividend: int63 r_period: float64 memory: list[str] = field(default_factory=list) # bounded memory (LLM agents)65 channel: list[str] = field(default_factory=list) # cohort messages (communication treatment)66 realized_pnl_history: list[float] = field(default_factory=list)676869class Policy(Protocol):70 name: str7172 def decide(self, obs: Observation, rng: np.random.Generator) -> Action: ...737475@dataclass(slots=True)76class AgentSpec:77 """Static description of one trader slot (assigned at population build time)."""78 agent_id: int79 policy_name: str80 policy_kwargs: dict81 cash: int82 positions: dict[int, int]83 latency: float84 activity: float # per-tick probability of being activated (classical)85 decision_interval: int = 1 # LLM agents: decide every `decision_interval` ticks86 decision_offset: int = 087 signal_precision: float = 0.0 # 0 => uninformed88 leverage_cap: float = 1.089 short_allowed: bool = False90 stratum: str = "" # wealth/information/latency stratum used for randomization91 provider: str = "" # LLM provider label ('' for classical)92 model: str = ""93 prompt_id: str = ""94 cohort: int = -1 # communication cohort (-1: none)959697def clip_qty(q: float, max_q: int) -> int:98 return int(max(0, min(max_q, round(q))))99