Skip to content

Commit 3336e4e

Browse files
committed
Harden ABIDES order intent normalization
1 parent 526a714 commit 3336e4e

2 files changed

Lines changed: 84 additions & 2 deletions

File tree

codeclash/arenas/abides/runtime/run_abides.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,18 @@ def make_random_state() -> np.random.RandomState:
103103
return np.random.RandomState(seed=np.random.randint(low=0, high=2**32, dtype="uint64"))
104104

105105

106+
def normalize_int_field(value, field: str) -> int:
107+
if isinstance(value, np.generic):
108+
value = value.item()
109+
if isinstance(value, bool):
110+
raise ValueError(f"order {field} must be an integer")
111+
if isinstance(value, float) and value.is_integer():
112+
value = int(value)
113+
if not isinstance(value, int):
114+
raise ValueError(f"order {field} must be an integer")
115+
return value
116+
117+
106118
def normalize_order_intents(raw_orders) -> list[dict]:
107119
if raw_orders is None:
108120
return []
@@ -123,9 +135,13 @@ def normalize_order_intents(raw_orders) -> list[dict]:
123135
if side not in {"buy", "sell"}:
124136
raise ValueError("order side must be 'buy' or 'sell'")
125137

126-
quantity = min(max(int(raw_order.get("quantity", 0)), 1), MAX_ORDER_QUANTITY)
138+
quantity = min(max(normalize_int_field(raw_order.get("quantity", 0), "quantity"), 1), MAX_ORDER_QUANTITY)
127139
limit_price = min(
128-
max(int(raw_order.get("limit_price", raw_order.get("price", 0))), MIN_LIMIT_PRICE), MAX_LIMIT_PRICE
140+
max(
141+
normalize_int_field(raw_order.get("limit_price", raw_order.get("price", 0)), "limit_price"),
142+
MIN_LIMIT_PRICE,
143+
),
144+
MAX_LIMIT_PRICE,
129145
)
130146
normalized.append(
131147
{

tests/arenas/test_abides.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,56 @@
1+
import importlib.util
12
import json
23
import subprocess
4+
import sys
5+
import types
36
from pathlib import Path
47

8+
import pytest
9+
510
from codeclash.arenas.abides.abides import CRASH_SCORE, ABIDESArena
611
from codeclash.arenas.arena import RoundStats
712
from codeclash.constants import RESULT_TIE
813

914
from .conftest import MockEnvironment, MockPlayer
1015

1116

17+
def load_runtime_module(monkeypatch):
18+
class FakeTradingAgent:
19+
pass
20+
21+
fake_modules = {
22+
"agent": types.ModuleType("agent"),
23+
"agent.ExchangeAgent": types.ModuleType("agent.ExchangeAgent"),
24+
"agent.market_makers": types.ModuleType("agent.market_makers"),
25+
"agent.market_makers.MarketMakerAgent": types.ModuleType("agent.market_makers.MarketMakerAgent"),
26+
"agent.TradingAgent": types.ModuleType("agent.TradingAgent"),
27+
"agent.ZeroIntelligenceAgent": types.ModuleType("agent.ZeroIntelligenceAgent"),
28+
"Kernel": types.ModuleType("Kernel"),
29+
"util": types.ModuleType("util"),
30+
"util.oracle": types.ModuleType("util.oracle"),
31+
"util.oracle.SparseMeanRevertingOracle": types.ModuleType("util.oracle.SparseMeanRevertingOracle"),
32+
"util.order": types.ModuleType("util.order"),
33+
}
34+
fake_modules["agent.ExchangeAgent"].ExchangeAgent = type("ExchangeAgent", (), {})
35+
fake_modules["agent.market_makers.MarketMakerAgent"].MarketMakerAgent = type("MarketMakerAgent", (), {})
36+
fake_modules["agent.TradingAgent"].TradingAgent = FakeTradingAgent
37+
fake_modules["agent.ZeroIntelligenceAgent"].ZeroIntelligenceAgent = type("ZeroIntelligenceAgent", (), {})
38+
fake_modules["Kernel"].Kernel = type("Kernel", (), {})
39+
fake_modules["util"].util = types.SimpleNamespace(silent_mode=False)
40+
fake_modules["util.oracle.SparseMeanRevertingOracle"].SparseMeanRevertingOracle = type(
41+
"SparseMeanRevertingOracle", (), {}
42+
)
43+
fake_modules["util.order"].LimitOrder = type("LimitOrder", (), {"silent_mode": False})
44+
for name, module in fake_modules.items():
45+
monkeypatch.setitem(sys.modules, name, module)
46+
47+
runtime_path = Path(__file__).parents[2] / "codeclash/arenas/abides/runtime/run_abides.py"
48+
spec = importlib.util.spec_from_file_location("run_abides_test", runtime_path)
49+
module = importlib.util.module_from_spec(spec)
50+
spec.loader.exec_module(module)
51+
return module
52+
53+
1254
class TestABIDESValidation:
1355
def test_valid_agent(self, mock_player_factory):
1456
arena = ABIDESArena.__new__(ABIDESArena)
@@ -252,3 +294,27 @@ def execute(self, cmd, cwd=None, timeout=None):
252294
assert "--agent Alice=/Alice/abides_agent.py" in cmd
253295
assert "--agent Bob=/Bob/abides_agent.py" in cmd
254296
assert arena.environment.timeout == 17
297+
298+
299+
class TestABIDESRuntimeProtocol:
300+
def test_normalize_order_intents_rejects_boolean_numeric_fields(self, monkeypatch):
301+
runtime = load_runtime_module(monkeypatch)
302+
303+
with pytest.raises(ValueError, match="quantity"):
304+
runtime.normalize_order_intents({"side": "buy", "quantity": True, "limit_price": 100_000})
305+
306+
with pytest.raises(ValueError, match="limit_price"):
307+
runtime.normalize_order_intents({"side": "buy", "quantity": 1, "limit_price": False})
308+
309+
def test_normalize_order_intents_accepts_whole_number_floats_and_clamps(self, monkeypatch):
310+
runtime = load_runtime_module(monkeypatch)
311+
312+
assert runtime.normalize_order_intents({"side": "buy", "quantity": 25.0, "limit_price": 2_000_000.0}) == [
313+
{"side": "buy", "quantity": runtime.MAX_ORDER_QUANTITY, "limit_price": runtime.MAX_LIMIT_PRICE}
314+
]
315+
316+
def test_normalize_order_intents_rejects_non_integral_floats(self, monkeypatch):
317+
runtime = load_runtime_module(monkeypatch)
318+
319+
with pytest.raises(ValueError, match="quantity"):
320+
runtime.normalize_order_intents({"side": "buy", "quantity": 1.5, "limit_price": 100_000})

0 commit comments

Comments
 (0)