22import contextlib
33import importlib .util
44import json
5+ import multiprocessing as mp
56import os
7+ import queue
68import random
79import re
10+ import sys
811import tempfile
912import traceback
1013from pathlib import Path
@@ -42,11 +45,17 @@ def safe_module_name(player_name: str, sim_idx: int | None = None) -> str:
4245
4346
4447def load_agent_class (player_name : str , path : str , * , sim_idx : int | None = None ):
48+ agent_dir = str (Path (path ).resolve ().parent )
49+ sys .path .insert (0 , agent_dir )
4550 spec = importlib .util .spec_from_file_location (safe_module_name (player_name , sim_idx ), path )
4651 if spec is None or spec .loader is None :
4752 raise RuntimeError (f"Could not load module spec from { path } " )
4853 module = importlib .util .module_from_spec (spec )
49- spec .loader .exec_module (module )
54+ try :
55+ spec .loader .exec_module (module )
56+ finally :
57+ with contextlib .suppress (ValueError ):
58+ sys .path .remove (agent_dir )
5059 if not hasattr (module , "MyAgent" ):
5160 raise RuntimeError (f"{ path } does not define MyAgent" )
5261 agent_class = module .MyAgent
@@ -178,6 +187,7 @@ def make_world_agents(agent_classes: dict[str, type], *, sim_idx: int, market_mi
178187 agent_id += 1
179188
180189 player_agents = {}
190+ player_ids = {}
181191 player_names = list (agent_classes .keys ())
182192 if player_names :
183193 offset = sim_idx % len (player_names )
@@ -186,22 +196,41 @@ def make_world_agents(agent_classes: dict[str, type], *, sim_idx: int, market_mi
186196 agent = make_player_agent (agent_classes [player_name ], player_name , agent_id )
187197 agents .append (agent )
188198 player_agents [player_name ] = agent
199+ player_ids [agent_id ] = player_name
189200 agent_id += 1
190201
191202 for agent in agents :
192203 agent .log_to_file = False
193204
205+ ledgers = {player : {"CASH" : STARTING_CASH , SYMBOL : 0 } for player in player_agents }
206+ original_exchange_send = exchange .sendMessage
207+
208+ def scored_exchange_send (* args , ** kwargs ):
209+ recipient_id = args [0 ] if args else kwargs .get ("recipientID" , kwargs .get ("recipient_id" ))
210+ msg = args [1 ] if len (args ) > 1 else kwargs .get ("msg" )
211+ player = player_ids .get (recipient_id )
212+ if player and getattr (msg , "body" , {}).get ("msg" ) == "ORDER_EXECUTED" :
213+ order = msg .body ["order" ]
214+ quantity = int (order .quantity )
215+ signed_quantity = quantity if order .is_buy_order else - quantity
216+ ledgers [player ][SYMBOL ] += signed_quantity
217+ ledgers [player ]["CASH" ] -= signed_quantity * int (order .fill_price )
218+ return original_exchange_send (* args , ** kwargs )
219+
220+ exchange .sendMessage = scored_exchange_send
221+
194222 return {
195223 "agents" : agents ,
196224 "player_agents" : player_agents ,
225+ "ledgers" : ledgers ,
197226 "exchange" : exchange ,
198227 "historical_date" : historical_date ,
199228 "mkt_close" : mkt_close ,
200229 "oracle" : oracle ,
201230 }
202231
203232
204- def score_player (agent : TradingAgent , final_price : int ) -> tuple [float , dict ]:
233+ def score_player (agent : TradingAgent , ledger : dict [ str , int ], final_price : int ) -> tuple [float , dict ]:
205234 if getattr (agent , "_codeclash_error" , None ):
206235 return CRASH_SCORE , {
207236 "status" : "error" ,
@@ -210,8 +239,8 @@ def score_player(agent: TradingAgent, final_price: int) -> tuple[float, dict]:
210239 }
211240
212241 try :
213- cash = int (agent . holdings .get ("CASH" , 0 ))
214- shares = int (agent . holdings .get (SYMBOL , 0 ))
242+ cash = int (ledger .get ("CASH" , 0 ))
243+ shares = int (ledger .get (SYMBOL , 0 ))
215244 score = float (cash + shares * final_price - STARTING_CASH )
216245 except Exception as exc :
217246 return CRASH_SCORE , {
@@ -259,7 +288,8 @@ def run_player_market(
259288
260289 final_price = int (world ["exchange" ].order_books [SYMBOL ].last_trade )
261290 agent = world ["player_agents" ][player ]
262- score , score_detail = score_player (agent , final_price )
291+ ledger = world ["ledgers" ][player ]
292+ score , score_detail = score_player (agent , ledger , final_price )
263293 return {
264294 "score" : score ,
265295 "detail" : {
@@ -272,33 +302,108 @@ def run_player_market(
272302 }
273303
274304
275- def run_market (agent_paths : dict [str , str ], * , sim_idx : int , market_minutes : int , background_agents : int ) -> dict :
276- scores = {}
277- details = []
278- for player , path in agent_paths .items ():
279- try :
280- agent_class = load_agent_class (player , path , sim_idx = sim_idx )
281- result = run_player_market (
282- player ,
283- agent_class ,
284- sim_idx = sim_idx ,
285- market_minutes = market_minutes ,
286- background_agents = background_agents ,
305+ def run_player_market_worker (
306+ result_queue : mp .Queue ,
307+ player : str ,
308+ path : str ,
309+ * ,
310+ sim_idx : int ,
311+ market_minutes : int ,
312+ background_agents : int ,
313+ ) -> None :
314+ try :
315+ agent_class = load_agent_class (player , path , sim_idx = sim_idx )
316+ result_queue .put (
317+ run_player_market (
318+ player , agent_class , sim_idx = sim_idx , market_minutes = market_minutes , background_agents = background_agents
287319 )
288- scores [ player ] = result [ "score" ]
289- details . append ( result [ "detail" ])
290- except Exception as exc :
291- scores [ player ] = CRASH_SCORE
292- details . append (
293- {
320+ )
321+ except BaseException as exc :
322+ result_queue . put (
323+ {
324+ "score" : CRASH_SCORE ,
325+ "detail" : {
294326 "sim" : sim_idx ,
295327 "player" : player ,
296328 "score" : CRASH_SCORE ,
297329 "status" : "error" ,
298330 "error" : f"{ type (exc ).__name__ } : { exc } " ,
299331 "traceback" : traceback .format_exc (limit = 5 ),
300- }
301- )
332+ },
333+ }
334+ )
335+
336+
337+ def run_player_market_isolated (
338+ player : str ,
339+ path : str ,
340+ * ,
341+ sim_idx : int ,
342+ market_minutes : int ,
343+ background_agents : int ,
344+ player_timeout : int ,
345+ ) -> dict :
346+ ctx = mp .get_context ("spawn" )
347+ result_queue = ctx .Queue ()
348+ process = ctx .Process (
349+ target = run_player_market_worker ,
350+ args = (result_queue , player , path ),
351+ kwargs = {
352+ "sim_idx" : sim_idx ,
353+ "market_minutes" : market_minutes ,
354+ "background_agents" : background_agents ,
355+ },
356+ )
357+ process .start ()
358+ process .join (player_timeout )
359+ if process .is_alive ():
360+ process .terminate ()
361+ process .join (2 )
362+ if process .is_alive ():
363+ process .kill ()
364+ process .join ()
365+ return {
366+ "score" : CRASH_SCORE ,
367+ "detail" : {
368+ "sim" : sim_idx ,
369+ "player" : player ,
370+ "score" : CRASH_SCORE ,
371+ "status" : "error" ,
372+ "error" : f"player simulation exceeded { player_timeout } s timeout" ,
373+ },
374+ }
375+
376+ try :
377+ return result_queue .get_nowait ()
378+ except queue .Empty :
379+ return {
380+ "score" : CRASH_SCORE ,
381+ "detail" : {
382+ "sim" : sim_idx ,
383+ "player" : player ,
384+ "score" : CRASH_SCORE ,
385+ "status" : "error" ,
386+ "error" : f"player simulation exited with code { process .exitcode } and no result" ,
387+ },
388+ }
389+
390+
391+ def run_market (
392+ agent_paths : dict [str , str ], * , sim_idx : int , market_minutes : int , background_agents : int , player_timeout : int
393+ ) -> dict :
394+ scores = {}
395+ details = []
396+ for player , path in agent_paths .items ():
397+ result = run_player_market_isolated (
398+ player ,
399+ path ,
400+ sim_idx = sim_idx ,
401+ market_minutes = market_minutes ,
402+ background_agents = background_agents ,
403+ player_timeout = player_timeout ,
404+ )
405+ scores [player ] = result ["score" ]
406+ details .append (result ["detail" ])
302407
303408 return {"scores" : scores , "details" : details }
304409
@@ -320,6 +425,7 @@ def main() -> None:
320425 parser .add_argument ("--sims" , type = int , default = 3 )
321426 parser .add_argument ("--market-minutes" , type = int , default = 5 )
322427 parser .add_argument ("--background-agents" , type = int , default = 3 )
428+ parser .add_argument ("--player-timeout" , type = int , default = 60 )
323429 parser .add_argument ("--output" , required = True )
324430 args = parser .parse_args ()
325431
@@ -329,6 +435,8 @@ def main() -> None:
329435 parser .error ("--market-minutes must be at least 1" )
330436 if args .background_agents < 0 :
331437 parser .error ("--background-agents cannot be negative" )
438+ if args .player_timeout < 1 :
439+ parser .error ("--player-timeout must be at least 1" )
332440
333441 agent_names = [name for name , _ in args .agent ]
334442 if len (agent_names ) != len (set (agent_names )):
@@ -345,6 +453,7 @@ def main() -> None:
345453 sim_idx = sim_idx ,
346454 market_minutes = args .market_minutes ,
347455 background_agents = args .background_agents ,
456+ player_timeout = args .player_timeout ,
348457 )
349458 except Exception as exc :
350459 result = {
0 commit comments