1import numpy as np23from market_agents.agents.base import BookView, Observation, OwnOrderView4from market_agents.llm.policy import LLMPolicy, LLMPolicyConfig5from market_agents.llm.schema import parse_decision678def obs(cash=1_000_000, pos=100, open_orders=()):9 b = BookView(best_bid=19990, best_ask=20010, bids=[(19990, 10)], asks=[(20010, 10)], mid=20000.0, last_price=20000, halted=False)10 return Observation(t=10, agent_id=1, books={0: b}, mid_history={0: np.array([20000.0, 20005.0])}, recent_trades={0: []},11 last_dividend={0: 50.0}, public_value={0: 20000.0}, x_hat_public={0: 0.0}, news=[], private_signals={}, signal_precision={},12 cash=cash, free_cash=cash, positions={0: pos}, equity=cash + pos * 20000, open_orders=list(open_orders),13 leverage_cap=1.0, short_allowed=False, max_order_qty=50, tick_cents={0: 1}, div_interval=20,14 ticks_to_dividend=10, r_period=0.0025)151617def pol():18 return LLMPolicy(LLMPolicyConfig(models={"mock": "mock-v1"}, use_cache=False))192021def test_parse_lenient_and_strict():22 d, err = parse_decision('```json\n{"action":"hold"}\n```')23 assert d is not None and d.action == "hold" and err == ""24 d, err = parse_decision("not json")25 assert d is None and err.startswith("json")26 d, err = parse_decision('{"action":"place","quantity":-1}')27 assert d is None and err.startswith("schema")282930def test_validation_rules():31 p = pol()32 d, _ = parse_decision('{"action":"place","side":"buy","order_type":"limit","quantity":10,"limit_price":200.05}')33 a, ok, why = p.to_action(d, obs())34 assert ok and a.orders[0].price == 20005 and a.orders[0].qty == 1035 d, _ = parse_decision('{"action":"place","side":"buy","order_type":"limit","quantity":10,"limit_price":300.0}')36 assert not p.to_action(d, obs())[1] # outside ±20 %37 d, _ = parse_decision('{"action":"place","side":"buy","order_type":"market","quantity":60}')38 assert "max" in p.to_action(d, obs())[2]39 d, _ = parse_decision('{"action":"place","side":"buy","order_type":"market","quantity":10}')40 assert not p.to_action(d, obs(cash=1000))[1] # cannot afford41 d, _ = parse_decision('{"action":"place","side":"sell","order_type":"market","quantity":10}')42 assert not p.to_action(d, obs(pos=0))[1] # no shares, no shorting43 d, _ = parse_decision('{"action":"cancel","cancel_order_ids":[5]}')44 assert not p.to_action(d, obs())[1]45 assert p.to_action(d, obs(open_orders=[OwnOrderView(5, 0, 1, 10, 10, 19990, 3.0)]))[1]464748def test_budget_cap_halts_calls():49 p = LLMPolicy(LLMPolicyConfig(models={"mock": "mock-v1"}, use_cache=False, usd_cap=0.0))50 a, rec = p.decide_one(obs(), "mock", "mock-v1", "generalist")51 assert not rec.valid and "cap" in rec.error and p.halted_for_budget52