1"""Observation -> prompt. One frozen system prompt per (prompt_version, prompt_id) and a canonical,2deterministic user message built from the Observation. Nothing in the prompt comes from latent state."""3from __future__ import annotations45from ..agents.base import Observation67PROMPT_VERSION = "v1"89ROLE_PERSONAS: dict[str, str] = {10 "generalist": "You are a professional trader managing your own account in a single-stock electronic market.",11 "value": "You are a value investor: you compare the price to the present value of expected dividends and trade mispricings patiently.",12 "momentum": "You are a momentum trader: you follow recent price trends and cut positions when the trend reverses.",13 "contrarian": "You are a contrarian trader: you fade sharp moves that are not justified by dividend news.",14 "liquidity": "You are a liquidity provider: you prefer resting limit orders near the best quotes and keep your inventory small.",15 "risk_averse": "You are a risk-averse trader: you keep positions small, avoid market orders and prefer to hold when uncertain.",16 "macro": "You are a news-driven trader: you weigh public announcements and dividend surprises and act when the market has not yet reacted.",17 "quant": "You are a quantitative trader: you reason about expected value per share, spread costs and inventory before each order.",18}1920RULES = """21Market mechanics (read carefully):22- One risky stock paying a cash dividend every {div_interval} ticks; the long-run mean dividend is {mu:.2f} per share per period and the per-period discount rate is {r_pct:.2f}%, so a share paying the mean dividend forever is worth about {pv_mean:.2f}.23- Dividends deviate persistently from the mean (autocorrelation {rho:.2f} per period). The deviation of the NEXT dividend is not yet announced; public news and, when you have one, your private signal are noisy estimates of it (in cents). The present value of a share rises by about 1/(1+r-rho) ≈ 44 cents per cent of next-dividend deviation.24- Prices are in currency units with a tick of {tick:.2f}. Limit orders rest in the book; market orders execute immediately against the best quotes (taker fee {taker_bps:.1f} bps). Orders may be partially filled. Unfilled limit orders expire after {ttl} ticks.25- You cannot short sell and cannot spend more than your free cash. Maximum order size: {max_qty} shares.26- You decide every {interval} ticks. Between decisions the market keeps trading.2728Respond with ONLY a JSON object with exactly these keys:29{{"action": "hold"|"place"|"cancel", "side": "buy"|"sell"|null, "order_type": "limit"|"market"|null, "quantity": integer|null, "limit_price": number|null, "cancel_order_ids": [integers], "confidence": number in [0,1]|null, "rationale": "one sentence, max 200 characters"}}30"place" requires side, order_type and quantity (and limit_price for a limit order). "cancel" requires cancel_order_ids. Anything else is treated as hold.31""".strip()323334def system_prompt(prompt_id: str, obs: Observation, mu_cents: float, taker_bps: float, ttl: int | None, decision_interval: int | None = None) -> str:35 persona = ROLE_PERSONAS.get(prompt_id, ROLE_PERSONAS["generalist"])36 tick = obs.tick_cents[0] / 100.037 pv_mean = (mu_cents / obs.r_period) / 100.038 return persona + "\n\n" + RULES.format(39 div_interval=obs.div_interval, mu=mu_cents / 100.0, r_pct=obs.r_period * 100.0, pv_mean=pv_mean,40 rho=0.98, tick=tick, taker_bps=taker_bps, ttl=ttl if ttl is not None else 10_000,41 max_qty=obs.max_order_qty, interval=max(1, decision_interval or obs.div_interval))424344def user_message(obs: Observation, asset: int = 0, adaptive: bool = False) -> str:45 b = obs.books[asset]46 tc = obs.tick_cents[asset]47 u = lambda ticks: f"{ticks * tc / 100.0:.2f}" # noqa: E73148 lines = [f"Time: tick {obs.t}. Next dividend in {obs.ticks_to_dividend} ticks."]49 if b.halted:50 lines.append("TRADING IS HALTED on this stock (orders will be rejected).")51 bb = u(b.best_bid) if b.best_bid is not None else "none"52 ba = u(b.best_ask) if b.best_ask is not None else "none"53 lines.append(f"Best bid {bb} / best ask {ba} / mid {u(b.mid)} / last trade {u(b.last_price) if b.last_price else 'none'}.")54 depth_b = ", ".join(f"{u(p)}x{q}" for p, q in b.bids[:5]) or "empty"55 depth_a = ", ".join(f"{u(p)}x{q}" for p, q in b.asks[:5]) or "empty"56 lines.append(f"Bid depth (price x shares): {depth_b}. Ask depth: {depth_a}.")57 h = obs.mid_history[asset]58 if len(h) >= 2:59 pts = [u(x) for x in h[::-max(1, len(h) // 10)][::-1]]60 lines.append(f"Mid-price history (oldest to newest, {len(h)} ticks sampled): " + ", ".join(pts) + ".")61 r = (h[-1] / h[0] - 1.0) * 100.062 lines.append(f"Change over the window: {r:+.2f}%. Recent volatility (std of tick returns): {100 * (h[1:] / h[:-1] - 1).std():.3f}%.")63 tr = obs.recent_trades.get(asset, [])64 if tr:65 vol = sum(q for _, _, q, _ in tr)66 buyv = sum(q for _, _, q, ag in tr if ag > 0)67 lines.append(f"Last {len(tr)} trades: volume {vol} shares, {100 * buyv / max(vol, 1):.0f}% buyer-initiated.")68 lines.append(f"Last dividend paid: {obs.last_dividend[asset] / 100.0:.2f} per share. Public estimate of the next dividend's deviation "69 f"from its mean: public estimate {obs.x_hat_public.get(asset, 0.0):+.1f} cents. "70 f"Present value of expected dividends using only public information: {obs.public_value[asset] / 100.0:.2f}.")71 if obs.news:72 for n in obs.news[-3:]:73 lines.append(f"Public news (tick {n.time}): {n.text}")74 if asset in obs.private_signals:75 lines.append(f"Your private signal: next dividend deviation estimated at {obs.private_signals[asset]:+.1f} cents "76 f"(signal precision {obs.signal_precision.get(asset, 0):.1f}; higher is more reliable).")77 else:78 lines.append("You have no private signal this period.")79 lines.append(f"Your account: cash {obs.cash / 100.0:.2f} (free {obs.free_cash / 100.0:.2f}), "80 f"holdings {obs.positions.get(asset, 0)} shares, equity {obs.equity / 100.0:.2f}.")81 if obs.open_orders:82 oo = "; ".join(f"id {o.order_id}: {'buy' if o.side > 0 else 'sell'} {o.remaining}/{o.qty} @ {u(o.price) if o.price else 'mkt'} (age {o.age:.0f})"83 for o in obs.open_orders[:6])84 lines.append(f"Your open orders: {oo}.")85 else:86 lines.append("You have no open orders.")87 if adaptive and obs.realized_pnl_history:88 lines.append(f"Feedback: your equity changed by {obs.realized_pnl_history[-1]:+.2f}% since your last decision "89 f"(cumulative {sum(obs.realized_pnl_history):+.2f}%).")90 if obs.memory:91 lines.append("Your recent decisions and outcomes:")92 lines += [f" - {m}" for m in obs.memory]93 if obs.channel:94 lines.append("Messages from other traders in your group (unverified opinions):")95 lines += [f" - {m}" for m in obs.channel]96 lines.append("Decide now. Reply with the JSON object only.")97 return "\n".join(lines)98