|
| 1 | +"""The information-disclosure difficulty axis (observation richness). |
| 2 | +
|
| 3 | +These exercise the native ``PyMarketClearing`` directly (no pettingzoo needed): the |
| 4 | +``richness`` tier is orthogonal to ``distribution_mode`` and controls how much of the |
| 5 | +market each observation surfaces. ``standard`` must reproduce the historical disclosure |
| 6 | +byte-for-byte; ``data_poor`` withholds bars and optional fields; ``data_rich`` surfaces |
| 7 | +more bars plus fundamentals and news. None of them ever reveal a future bar. |
| 8 | +""" |
| 9 | + |
| 10 | +import json |
| 11 | + |
| 12 | +import pytest |
| 13 | + |
| 14 | +from openoutcry.openoutcry_py import PyMarketClearing |
| 15 | + |
| 16 | + |
| 17 | +def _rollout(market, orders, steps=6): |
| 18 | + """The reset observations plus each step's observations, as parsed JSON.""" |
| 19 | + log = [json.loads(market.reset_market())["observations"]] |
| 20 | + for _ in range(steps): |
| 21 | + result = json.loads(market.step_market(json.dumps(orders))) |
| 22 | + log.append(result["observations"]) |
| 23 | + if result["done"]: |
| 24 | + break |
| 25 | + return log |
| 26 | + |
| 27 | + |
| 28 | +def _flat_orders(n_agents, n_symbols): |
| 29 | + return [[0.0] * n_symbols for _ in range(n_agents)] |
| 30 | + |
| 31 | + |
| 32 | +def test_default_richness_is_standard_and_byte_identical(): |
| 33 | + """Omitting ``richness`` and passing ``standard`` clear a byte-identical stream.""" |
| 34 | + common = dict(n_symbols=3, n_days=60, seed=4, n_agents=2, capital=1.0) |
| 35 | + orders = [[0.3, 0.3, 0.3], [0.3, 0.3, 0.3]] |
| 36 | + default_market = PyMarketClearing(**common) |
| 37 | + standard_market = PyMarketClearing(**common, richness="standard") |
| 38 | + assert _rollout(default_market, orders) == _rollout(standard_market, orders) |
| 39 | + |
| 40 | + |
| 41 | +def test_richness_getter_reports_the_active_disclosure(): |
| 42 | + poor = json.loads(PyMarketClearing(n_symbols=2, n_days=40, richness="data_poor").richness) |
| 43 | + std = json.loads(PyMarketClearing(n_symbols=2, n_days=40, richness="standard").richness) |
| 44 | + rich = json.loads(PyMarketClearing(n_symbols=2, n_days=40, richness="data_rich").richness) |
| 45 | + assert poor == {"lookback": 3, "fundamentals": False, "news": False} |
| 46 | + assert std == {"lookback": 20, "fundamentals": False, "news": False} |
| 47 | + assert rich == {"lookback": 50, "fundamentals": True, "news": True} |
| 48 | + |
| 49 | + |
| 50 | +def test_data_poor_withholds_bars_and_optional_fields(): |
| 51 | + market = PyMarketClearing(n_symbols=2, n_days=60, seed=7, n_agents=2, richness="data_poor") |
| 52 | + obs = json.loads(market.reset_market())["observations"] |
| 53 | + for agent_obs in obs: |
| 54 | + for snap in agent_obs["symbols"]: |
| 55 | + assert len(snap["close_history"]) <= 3 |
| 56 | + assert snap["fundamentals"] == {} |
| 57 | + assert snap["news"] == [] |
| 58 | + |
| 59 | + |
| 60 | +def test_data_rich_surfaces_more_bars_and_populates_fields(): |
| 61 | + rich = PyMarketClearing(n_symbols=2, n_days=120, seed=9, n_agents=2, richness="data_rich") |
| 62 | + standard = PyMarketClearing(n_symbols=2, n_days=120, seed=9, n_agents=2, richness="standard") |
| 63 | + rich_obs = json.loads(rich.reset_market())["observations"] |
| 64 | + std_obs = json.loads(standard.reset_market())["observations"] |
| 65 | + for ro, so in zip(rich_obs, std_obs): |
| 66 | + for rs, ss in zip(ro["symbols"], so["symbols"]): |
| 67 | + assert len(rs["close_history"]) > len(ss["close_history"]) |
| 68 | + assert len(rs["close_history"]) <= 50 |
| 69 | + assert set(rs["fundamentals"]) == {"trailing_return", "window_high", "window_low"} |
| 70 | + assert len(rs["news"]) == 1 |
| 71 | + assert rs["symbol"] in rs["news"][0] |
| 72 | + |
| 73 | + |
| 74 | +def test_every_tier_is_leak_free_last_close_is_this_bars_cleared_mid(): |
| 75 | + """Under every tier the last surfaced close equals this bar's cleared mid (never a |
| 76 | + future bar), and the surfaced window never exceeds the cleared-bar count.""" |
| 77 | + for tier in ("data_poor", "standard", "data_rich"): |
| 78 | + market = PyMarketClearing( |
| 79 | + n_symbols=3, n_days=80, seed=2, n_agents=2, richness=tier |
| 80 | + ) |
| 81 | + meta = json.loads(market.reset_market()) |
| 82 | + cleared_bars = meta["start_bar"] |
| 83 | + orders = _flat_orders(2, 3) |
| 84 | + while True: |
| 85 | + result = json.loads(market.step_market(json.dumps(orders))) |
| 86 | + cleared_bars += 1 |
| 87 | + mids = result["cleared_mids"] |
| 88 | + for agent_obs in result["observations"]: |
| 89 | + for s, snap in enumerate(agent_obs["symbols"]): |
| 90 | + assert snap["close_history"][-1] == mids[s] |
| 91 | + assert len(snap["close_history"]) <= cleared_bars |
| 92 | + if result["done"]: |
| 93 | + break |
| 94 | + |
| 95 | + |
| 96 | +def test_unknown_richness_raises(): |
| 97 | + with pytest.raises(ValueError, match="unknown richness"): |
| 98 | + PyMarketClearing(n_symbols=2, n_days=40, richness="bogus") |
0 commit comments