\section{Prompts} \label{app:prompts} This appendix reproduces verbatim the frozen prompt version \code{v1} of \code{llm/prompting.py}, one rendered example, the JSON schema sent to the providers, and the validation rules of \code{llm/policy.py}. The system prompt is the persona sentence followed by the rules block; the user message is a deterministic rendering of the agent's \code{Observation}. Nothing in either text depends on the latent state. \lstset{literate={≈}{{$\approx$ }}2} \subsection*{Personas (\code{ROLE\_PERSONAS})} The single-prompt design uses \code{generalist} for every LLM slot; the diverse-prompt design assigns the eight personas cyclically in slot order. \begin{lstlisting} generalist : You are a professional trader managing your own account in a single-stock electronic market. value : You are a value investor: you compare the price to the present value of expected dividends and trade mispricings patiently. momentum : You are a momentum trader: you follow recent price trends and cut positions when the trend reverses. contrarian : You are a contrarian trader: you fade sharp moves that are not justified by dividend news. liquidity : You are a liquidity provider: you prefer resting limit orders near the best quotes and keep your inventory small. risk_averse: You are a risk-averse trader: you keep positions small, avoid market orders and prefer to hold when uncertain. macro : You are a news-driven trader: you weigh public announcements and dividend surprises and act when the market has not yet reacted. quant : You are a quantitative trader: you reason about expected value per share, spread costs and inventory before each order. \end{lstlisting} \subsection*{Rules template (\code{RULES})} Placeholders in braces are filled from the configuration and the observation (\code{div\_interval}, \code{mu}, \code{r\_pct}, \code{pv\_mean}, \code{rho}, \code{tick}, \code{taker\_bps}, \code{ttl}, \code{max\_qty}, \code{interval}); doubled braces are Python format escapes and render as single braces. \begin{lstlisting} Market mechanics (read carefully): - 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}. - 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. - 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. - You cannot short sell and cannot spend more than your free cash. Maximum order size: {max_qty} shares. - You decide every {interval} ticks. Between decisions the market keeps trading. Respond with ONLY a JSON object with exactly these keys: {{"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"}} "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. \end{lstlisting} \noindent The placeholder \code{interval} is filled with the dividend interval (\code{obs.div\_interval}), not with \code{decision\_interval}; the two coincide (20) in every reported design. \subsection*{Rendered example} The following system prompt and user message were produced by running the prompt builder on the observation of an informed LLM slot (agent~9, persona \code{generalist}, tick~110) in a mock run of the baseline configuration with $\varphi=0.25$, seed~1.\footnote{Generated with \code{system\_prompt()} and \code{user\_message()} of \code{llm/prompting.py}; the observation was captured from \code{Simulation.\_observe}. The mid-price history is sub-sampled to every sixth tick of the 61-tick window.} \begin{lstlisting} You are a professional trader managing your own account in a single-stock electronic market. Market mechanics (read carefully): - One risky stock paying a cash dividend every 20 ticks; the long-run mean dividend is 0.50 per share per period and the per-period discount rate is 0.25%, so a share paying the mean dividend forever is worth about 200.00. - Dividends deviate persistently from the mean (autocorrelation 0.98 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. - Prices are in currency units with a tick of 0.01. Limit orders rest in the book; market orders execute immediately against the best quotes (taker fee 2.0 bps). Orders may be partially filled. Unfilled limit orders expire after 50 ticks. - You cannot short sell and cannot spend more than your free cash. Maximum order size: 50 shares. - You decide every 20 ticks. Between decisions the market keeps trading. Respond with ONLY a JSON object with exactly these keys: {"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"} "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. \end{lstlisting} \begin{lstlisting} Time: tick 110. Next dividend in 10 ticks. Best bid 200.80 / best ask 201.12 / mid 200.96 / last trade 201.12. Bid depth (price x shares): 200.80x14, 200.70x28, 200.69x13, 200.68x14, 200.65x12. Ask depth: 201.12x14, 201.15x4, 201.19x8, 201.20x14, 201.23x4. Mid-price history (oldest to newest, 61 ticks sampled): 200.20, 200.29, 200.48, 200.09, 200.54, 200.66, 200.64, 200.64, 200.60, 200.73, 200.96. Change over the window: +0.38%. Recent volatility (std of tick returns): 0.042%. Last 30 trades: volume 168 shares, 57% buyer-initiated. Last dividend paid: 0.55 per share. Public estimate of the next dividend's deviation from its mean: public estimate +5.0 cents. Present value of expected dividends using only public information: 202.23. Your private signal: next dividend deviation estimated at +3.6 cents (signal precision 2.0; higher is more reliable). Your account: cash 18634.09 (free 18634.09), holdings 92 shares, equity 37122.41. You have no open orders. Decide now. Reply with the JSON object only. \end{lstlisting} \noindent\sloppy Optional lines, in the order in which they are inserted when the corresponding treatment is active: \code{TRADING IS HALTED on this stock (orders will be rejected).} after the first line; up to three \code{Public news (tick $t$): \dots} lines after the public-value line; \code{Your open orders: id \dots: buy/sell remaining/qty @ price (age)} (at most six) instead of \code{You have no open orders.}; \code{Feedback: your equity changed by $\pm x$\% since your last decision (cumulative $\pm y$\%).} (adaptive feedback); \code{Your recent decisions and outcomes:} followed by up to five bullet lines (memory); \code{Messages from other traders in your group (unverified opinions):} followed by up to five bullet lines (communication channel). \subsection*{JSON schema (\code{DECISION\_JSON\_SCHEMA})} Sent as \code{text.format} (OpenAI, \code{strict: true}) and \code{output\_config.format} (Anthropic). Every key is required; \code{null} is allowed where indicated; no additional properties. \begin{lstlisting} { "type": "object", "properties": { "action": {"type": "string", "enum": ["hold", "place", "cancel"]}, "side": {"type": ["string", "null"], "enum": ["buy", "sell", null]}, "order_type": {"type": ["string", "null"], "enum": ["limit", "market", null]}, "quantity": {"type": ["integer", "null"]}, "limit_price": {"type": ["number", "null"]}, "cancel_order_ids": {"type": "array", "items": {"type": "integer"}}, "confidence": {"type": ["number", "null"]}, "rationale": {"type": "string"} }, "required": ["action", "side", "order_type", "quantity", "limit_price", "cancel_order_ids", "confidence", "rationale"], "additionalProperties": false } \end{lstlisting} \subsection*{Parsing and validation rules} The reply is first parsed, then validated against the pydantic model \code{Decision}, then checked for feasibility by \code{LLMPolicy.to\_action}. Any failure yields a hold action with \code{valid = False} and the reason string below; a provider error or a budget halt yields a hold with the error string as reason. \begin{enumerate}\sloppy \item \textbf{Parsing} (\code{parse\_decision}): whitespace and a surrounding \code{```}/\code{```json} fence are stripped; if the text is not valid JSON, the first \code{\{\dots\}} block is tried; failure $\to$ reason \code{json: }. \item \textbf{Schema} (\code{Decision}): \code{action} $\in$ \{hold, place, cancel\}; \code{side} $\in$ \{buy, sell, null\}; \code{order\_type} $\in$ \{limit, market, null\}; \code{quantity} integer $\ge 0$ or null; \code{limit\_price} number $>0$ or null (currency units); \code{cancel\_order\_ids} list of integers; \code{confidence} in $[0,1]$ or null; \code{rationale} trimmed to 400 characters; failure $\to$ reason \code{schema: : } (first three errors). \item \textbf{hold}: always valid; no intent. \item \textbf{cancel}: the ids are intersected with the agent's open orders (resting and in flight); empty intersection $\to$ \code{cancel: no valid order ids}; otherwise one cancel intent per surviving id. \item \textbf{place}, in this order: \begin{enumerate} \item side, order type and quantity must all be present (\code{place: missing side/order\_type/quantity}); \item quantity $>0$ (\code{place: quantity <= 0}) and $\le$ \code{max\_order\_qty} $=50$ (\code{place: quantity q > max 50}); \item the asset must not be halted (\code{place: market halted}); \item limit orders need a price (\code{place: limit without price}); the price is converted to ticks by $\operatorname{round}(100\,\text{price}/\text{tick\_cents})$ and must be positive and within $\pm20\%$ of the midpoint (\code{place: limit price outside ±20\% of mid}); \item market orders need a non-empty opposite side (\code{place: market order with empty opposite side}); \item buys: $\lfloor \text{ref}\times\text{tick\_cents}\times q\times1.001\rfloor \le$ free cash, where ref is the limit price or else the best ask or midpoint (\code{place: cost c > free cash f}); \item sells: $q$ is clipped to holdings minus the remaining quantity of the agent's resting sells; if nothing is free the decision is invalid (\code{place: no free shares to sell}); a clipped sell is recorded as valid. \end{enumerate} \item The surviving intent is tagged \code{llm} and submitted to the exchange, which applies its own checks (price band, cash reservation, position limit, leverage); an exchange rejection is logged in the event log, not in the decision log. \end{enumerate}