#!/usr/bin/env python3 """ run_simulation.py - Run MarketForge with Trained LLM + Baseline Agents ====================================================================== This is the MISSING PIECE that connects the trained model to the live environment. It runs a full simulation where: - Some agents are driven by the trained LLM (reads observation prompt, generates a JSON action) - Other agents use baseline strategies (random, rule-based) for comparison Usage: # Run with trained model driving trader_1 + speculator_1 vs baselines python run_simulation.py # Run with a specific model path python run_simulation.py --model ./market-forge-agent # Run with all agents using the trained model python run_simulation.py --model ./market-forge-agent --llm-agents all # Run baseline-only (no LLM) for comparison python run_simulation.py --baseline-only # Custom rounds and seed python run_simulation.py --rounds 30 --seed 42 """ import argparse import json import random import re import sys import os from dataclasses import asdict from typing import Dict, List, Any, Optional sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from models import MarketAction, MarketObservation from server.market_environment import ( MarketEnvironment, COMMODITIES, COMPOUND_GOODS, BASE_PRICES, AGENT_ROLES, ) from rewards import extract_action # ====================================================================== # Agent Strategies # ====================================================================== class BaseAgent: """Base class for all agent strategies.""" def __init__(self, agent_id: str): self.agent_id = agent_id self.strategy_name = "base" def decide(self, obs: MarketObservation) -> MarketAction: raise NotImplementedError def _make_action(self, **kwargs) -> MarketAction: kwargs.setdefault("agent_id", self.agent_id) kwargs.setdefault("action_type", "pass") return MarketAction(**kwargs) class RandomAgent(BaseAgent): """Baseline: picks a random legal action with random parameters.""" def __init__(self, agent_id: str): super().__init__(agent_id) self.strategy_name = "random" def decide(self, obs: MarketObservation) -> MarketAction: legal = obs.legal_actions or ["buy", "sell", "pass"] action_type = random.choice(legal) if action_type in ("buy", "sell"): commodity = random.choice(COMMODITIES) price = round(random.uniform(3, 30), 1) quantity = random.randint(1, 10) return self._make_action( action_type=action_type, commodity=commodity, price=price, quantity=quantity, ) elif action_type == "produce": good = random.choice(list(COMPOUND_GOODS.keys())) return self._make_action(action_type="produce", compound_good=good) elif action_type == "negotiate": others = [a for a in AGENT_ROLES if a != self.agent_id] target = random.choice(others) commodity = random.choice(COMMODITIES) return self._make_action( action_type="negotiate", target_agent=target, commodity=commodity, price=round(random.uniform(5, 25), 1), quantity=random.randint(1, 8), message=f"Want to trade {commodity}?", ) elif action_type == "propose_coalition": return self._make_action( action_type="propose_coalition", message="Let's form a group", ) elif action_type in ("accept_deal", "reject_deal"): if obs.pending_deals: deal = obs.pending_deals[0] return self._make_action( action_type=action_type, coalition_id=deal.get("deal_id", ""), ) return self._make_action(action_type="pass") elif action_type in ("join_coalition", "leave_coalition"): if obs.coalitions: return self._make_action( action_type=action_type, coalition_id=obs.coalitions[0], ) return self._make_action(action_type="pass") else: return self._make_action(action_type="pass") class RuleBasedAgent(BaseAgent): """Baseline: simple heuristic strategy. - Producers sell their specialty when price > base price - Consumers buy ingredients they need - Traders buy low, sell high based on spread - Speculators react to events """ def __init__(self, agent_id: str): super().__init__(agent_id) self.strategy_name = "rule-based" def decide(self, obs: MarketObservation) -> MarketAction: role = obs.role if role == "producer": return self._producer_strategy(obs) elif role == "consumer": return self._consumer_strategy(obs) elif role == "trader": return self._trader_strategy(obs) elif role == "speculator": return self._speculator_strategy(obs) return self._make_action(action_type="pass") def _producer_strategy(self, obs: MarketObservation) -> MarketAction: # Sell specialty commodity if we have stock for commodity, qty in obs.inventory.items(): if qty >= 5 and commodity in COMMODITIES: base = BASE_PRICES.get(commodity, 10) best_bid = obs.top_of_book.get(commodity, {}).get("best_bid", 0) sell_price = max(best_bid * 1.05, base * 1.1) sell_qty = min(qty // 2, 10) if sell_qty > 0: return self._make_action( action_type="sell", commodity=commodity, price=round(sell_price, 1), quantity=sell_qty, ) return self._make_action(action_type="pass") def _consumer_strategy(self, obs: MarketObservation) -> MarketAction: # Try to produce compound goods if we have ingredients for good, recipe in COMPOUND_GOODS.items(): has_all = all( obs.inventory.get(ing, 0) >= qty for ing, qty in recipe.items() ) if has_all: return self._make_action( action_type="produce", compound_good=good ) # Buy cheapest ingredient we're missing for good, recipe in COMPOUND_GOODS.items(): for ingredient, qty_needed in recipe.items(): if obs.inventory.get(ingredient, 0) < qty_needed: best_ask = obs.top_of_book.get(ingredient, {}).get("best_ask", 0) buy_price = best_ask * 1.05 if best_ask > 0 else BASE_PRICES[ingredient] * 1.1 return self._make_action( action_type="buy", commodity=ingredient, price=round(buy_price, 1), quantity=qty_needed - obs.inventory.get(ingredient, 0), ) return self._make_action(action_type="pass") def _trader_strategy(self, obs: MarketObservation) -> MarketAction: # Look for the best spread to exploit best_spread = 0 best_commodity = None for commodity in COMMODITIES: tob = obs.top_of_book.get(commodity, {}) bid = tob.get("best_bid", 0) ask = tob.get("best_ask", 0) if bid > 0 and ask > 0 and bid > ask: spread = bid - ask if spread > best_spread: best_spread = spread best_commodity = commodity if best_commodity and best_spread > 1.0: tob = obs.top_of_book[best_commodity] if obs.inventory.get(best_commodity, 0) > 2: return self._make_action( action_type="sell", commodity=best_commodity, price=round(tob["best_bid"] * 0.98, 1), quantity=min(3, obs.inventory.get(best_commodity, 0)), ) elif obs.cash > 50: return self._make_action( action_type="buy", commodity=best_commodity, price=round(tob["best_ask"] * 1.02, 1), quantity=3, ) # Fallback: buy cheapest commodity if obs.cash > 100: cheapest = min(COMMODITIES, key=lambda c: obs.top_of_book.get(c, {}).get("best_ask", 999)) ask = obs.top_of_book.get(cheapest, {}).get("best_ask", 0) if ask > 0: return self._make_action( action_type="buy", commodity=cheapest, price=round(ask * 1.05, 1), quantity=3, ) return self._make_action(action_type="pass") def _speculator_strategy(self, obs: MarketObservation) -> MarketAction: event = obs.event.lower() if obs.event else "" # React to events if "drought" in event or "wheat" in event: if obs.cash > 100: return self._make_action( action_type="buy", commodity="wheat", price=round(BASE_PRICES["wheat"] * 1.3, 1), quantity=5, ) elif "embargo" in event or "oil" in event: if obs.cash > 100: return self._make_action( action_type="buy", commodity="oil", price=round(BASE_PRICES["oil"] * 1.3, 1), quantity=5, ) elif "surplus" in event or "timber" in event: if obs.inventory.get("timber", 0) > 3: return self._make_action( action_type="sell", commodity="timber", price=round(BASE_PRICES["timber"] * 0.9, 1), quantity=obs.inventory.get("timber", 0), ) # Default: buy whatever is cheapest, sell whatever is most expensive if obs.cash > 200: cheapest = min( COMMODITIES, key=lambda c: obs.last_trade_prices.get(c, BASE_PRICES[c]) ) price = obs.last_trade_prices.get(cheapest, BASE_PRICES[cheapest]) return self._make_action( action_type="buy", commodity=cheapest, price=round(price * 1.05, 1), quantity=5, ) # Sell most expensive holding holdings = {c: q for c, q in obs.inventory.items() if q > 0 and c in COMMODITIES} if holdings: most_expensive = max( holdings, key=lambda c: obs.last_trade_prices.get(c, BASE_PRICES[c]) ) price = obs.last_trade_prices.get(most_expensive, BASE_PRICES[most_expensive]) return self._make_action( action_type="sell", commodity=most_expensive, price=round(price * 1.1, 1), quantity=min(5, holdings[most_expensive]), ) return self._make_action(action_type="pass") class TrainedLLMAgent(BaseAgent): """Agent driven by a trained HuggingFace model. Loads the GRPO-trained model (e.g. Qwen/Qwen2.5-0.5B-Instruct fine-tuned on MarketForge), feeds it the observation prompt, and parses the JSON action from the model's output. """ def __init__(self, agent_id: str, model_path: str, device: str = "auto", system_prompt: str = None): super().__init__(agent_id) self.strategy_name = f"trained-llm ({os.path.basename(model_path)})" self.model_path = model_path self.system_prompt = system_prompt or self._default_system_prompt() # Load model and tokenizer from transformers import AutoTokenizer, AutoModelForCausalLM import torch print(f" Loading trained model from {model_path} ...") self.tokenizer = AutoTokenizer.from_pretrained(model_path) if self.tokenizer.pad_token is None: self.tokenizer.pad_token = self.tokenizer.eos_token self.model = AutoModelForCausalLM.from_pretrained( model_path, torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32, device_map=device, ) self.model.eval() self.device = self.model.device print(f" Model loaded on {self.device}") @staticmethod def _default_system_prompt() -> str: return ( "You are an autonomous trading agent in a multi-commodity MarketForge.\n" "You trade wheat, iron, timber, and oil. You can produce compound goods.\n\n" "RESPOND WITH EXACTLY ONE JSON OBJECT choosing your action. Valid action_types:\n" " buy, sell, produce, negotiate, propose_coalition, join_coalition, " "accept_deal, pass\n\n" "EXAMPLES:\n" ' {"action_type":"buy","commodity":"wheat","price":10,"quantity":5}\n' ' {"action_type":"sell","commodity":"iron","price":18,"quantity":3}\n' ' {"action_type":"produce","compound_good":"bread"}\n' ' {"action_type":"negotiate","target_agent":"producer_wheat",' '"commodity":"wheat","price":9,"quantity":10,' '"message":"Bulk discount for wheat?"}\n' ' {"action_type":"pass"}\n\n' "STRATEGY: Buy low, sell high. React to events. Negotiate deals. " "Form coalitions. Produce compound goods when profitable." ) def decide(self, obs: MarketObservation) -> MarketAction: import torch messages = [ {"role": "system", "content": self.system_prompt}, {"role": "user", "content": obs.prompt}, ] prompt_text = self.tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=False, ) inputs = self.tokenizer(prompt_text, return_tensors="pt").to(self.device) with torch.no_grad(): output_ids = self.model.generate( **inputs, max_new_tokens=128, do_sample=True, temperature=0.7, top_p=0.9, pad_token_id=self.tokenizer.pad_token_id, ) # Decode only the new tokens generated_ids = output_ids[0][inputs["input_ids"].shape[1]:] raw_output = self.tokenizer.decode(generated_ids, skip_special_tokens=True) # Parse JSON action from model output parsed = extract_action(raw_output) if parsed and "action_type" in parsed: parsed["agent_id"] = self.agent_id # Filter to valid MarketAction fields valid_fields = set(MarketAction.__dataclass_fields__.keys()) filtered = {k: v for k, v in parsed.items() if k in valid_fields} return MarketAction(**filtered) # Fallback if model output is unparseable return self._make_action(action_type="pass") # ====================================================================== # Simulation Runner # ====================================================================== def run_episode( env: MarketEnvironment, agents: Dict[str, BaseAgent], max_rounds: int = 30, seed: int = None, verbose: bool = True, ) -> Dict[str, Any]: """Run one full episode of the market simulation. Each round, every agent gets an observation and decides an action. The environment processes actions one at a time. After all agents act, the round advances. Returns a results dict with awards, leaderboard, and per-agent stats. """ obs = env.reset(max_rounds=max_rounds, seed=seed) agent_ids = list(env.state.agents.keys()) if verbose: print(f"\n{'='*60}") print(f" Episode Start | {len(agent_ids)} agents | {max_rounds} rounds") print(f"{'='*60}") last_obs = {aid: None for aid in agent_ids} round_num = 0 while True: for aid in agent_ids: # Get fresh observation for this agent agent_obs = env._make_observation(aid) if agent_obs.done: # Run one final step to trigger awards computation final_action = MarketAction(agent_id=aid, action_type="pass") final_obs = env.step(final_action) if final_obs.market_summary.get("game_over"): return _extract_results(final_obs, agents, env) continue # Agent decides agent_strategy = agents.get(aid) if agent_strategy is None: # Default to rule-based for agents without an assigned strategy agent_strategy = RuleBasedAgent(aid) agents[aid] = agent_strategy action = agent_strategy.decide(agent_obs) step_obs = env.step(action) last_obs[aid] = step_obs if step_obs.done and step_obs.market_summary.get("game_over"): return _extract_results(step_obs, agents, env) new_round = env.state.round_number if new_round != round_num: round_num = new_round if verbose and round_num % 5 == 0: _print_round_summary(env, round_num, max_rounds) # Should not reach here, but safety fallback return _extract_results(last_obs[agent_ids[0]], agents, env) def _extract_results( final_obs: MarketObservation, agents: Dict[str, BaseAgent], env: MarketEnvironment, ) -> Dict[str, Any]: """Extract structured results from the final observation.""" awards = final_obs.market_summary.get("awards", {}) leaderboard = final_obs.market_summary.get("leaderboard", []) # Annotate leaderboard with strategy names for entry in leaderboard: aid = entry["agent_id"] agent_strategy = agents.get(aid) entry["strategy"] = agent_strategy.strategy_name if agent_strategy else "unknown" return { "awards": awards, "leaderboard": leaderboard, "market_metrics": dict(env.state.market_metrics), "total_rounds": env.state.round_number, } def _print_round_summary(env: MarketEnvironment, round_num: int, max_rounds: int): """Print a brief round summary.""" metrics = env.state.market_metrics print(f" Round {round_num:3d}/{max_rounds} | " f"trades: {metrics.get('total_trades', 0):4d} | " f"coalitions: {metrics.get('coalitions_formed', 0):2d} | " f"compounds: {metrics.get('compound_goods_produced', 0):2d}") # ====================================================================== # Main # ====================================================================== def build_agents( model_path: Optional[str], llm_agent_ids: List[str], baseline_type: str = "rule-based", ) -> Dict[str, BaseAgent]: """Create agent strategy instances. Args: model_path: Path to the trained model (None = all baselines) llm_agent_ids: Which agents the trained model should control baseline_type: "random" or "rule-based" for non-LLM agents """ all_agent_ids = list(AGENT_ROLES.keys()) agents = {} # Create LLM-driven agents llm_agent = None if model_path and llm_agent_ids: llm_agent = TrainedLLMAgent( agent_id=llm_agent_ids[0], # shares model across agents model_path=model_path, ) for aid in all_agent_ids: if model_path and aid in llm_agent_ids: if aid == llm_agent_ids[0]: agents[aid] = llm_agent else: # Share model, just change agent_id for actions shared = TrainedLLMAgent.__new__(TrainedLLMAgent) shared.__dict__.update(llm_agent.__dict__) shared.agent_id = aid agents[aid] = shared else: if baseline_type == "random": agents[aid] = RandomAgent(aid) else: agents[aid] = RuleBasedAgent(aid) return agents def main(): parser = argparse.ArgumentParser(description="Run MarketForge simulation") parser.add_argument("--model", type=str, default=None, help="Path to trained model (default: use base model ID)") parser.add_argument("--base-model", type=str, default="Qwen/Qwen2.5-0.5B-Instruct", help="HuggingFace model ID to use if --model not provided") parser.add_argument("--llm-agents", type=str, default="trader_1,speculator_1", help="Comma-separated agent IDs for the LLM to control, or 'all'") parser.add_argument("--baseline-only", action="store_true", help="Run with only baseline agents (no LLM)") parser.add_argument("--baseline-type", choices=["random", "rule-based"], default="rule-based", help="Baseline strategy type") parser.add_argument("--rounds", type=int, default=30, help="Number of rounds per episode") parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducibility") parser.add_argument("--quiet", action="store_true", help="Suppress per-round output") args = parser.parse_args() # Determine model path model_path = None if not args.baseline_only: model_path = args.model or args.base_model # Determine which agents the LLM controls if args.llm_agents == "all": llm_agent_ids = list(AGENT_ROLES.keys()) else: llm_agent_ids = [a.strip() for a in args.llm_agents.split(",")] print("MarketForge Simulation") print("-" * 40) if model_path: print(f" LLM Model: {model_path}") print(f" LLM Controls: {', '.join(llm_agent_ids)}") else: print(f" Mode: baseline-only ({args.baseline_type})") print(f" Rounds: {args.rounds}") if args.seed is not None: print(f" Seed: {args.seed}") # Build agents agents = build_agents( model_path=model_path, llm_agent_ids=llm_agent_ids if model_path else [], baseline_type=args.baseline_type, ) # Print agent roster print(f"\n Agent Roster:") for aid in AGENT_ROLES: strategy = agents[aid].strategy_name role = AGENT_ROLES[aid]["role"] print(f" {aid:20s} | {role:12s} | {strategy}") # Run simulation env = MarketEnvironment() results = run_episode( env=env, agents=agents, max_rounds=args.rounds, seed=args.seed, verbose=not args.quiet, ) # Print results print(f"\n{'='*60}") print(" FINAL RESULTS") print(f"{'='*60}") awards = results["awards"] if "market_champion" in awards: champ = awards["market_champion"] print(f"\n Award 1 - Market Champion: {champ['agent_id']}") print(f" Total Wealth: ${champ['total_wealth']:.2f}") print(f" Wealth Growth: ${champ.get('wealth_growth', 0):.2f}") champ_strategy = agents.get(champ["agent_id"]) if champ_strategy: print(f" Strategy: {champ_strategy.strategy_name}") if "master_strategist" in awards: strat = awards["master_strategist"] print(f"\n Award 2 - Master Strategist: {strat['agent_id']}") print(f" Strategic Score: {strat['strategic_score']:.4f}") if "breakdown" in strat: bd = strat["breakdown"] print(f" Trade Efficiency: {bd.get('trade_efficiency', 0):.4f}") print(f" Negotiation Mastery: {bd.get('negotiation_mastery', 0):.4f}") print(f" Cooperation Index: {bd.get('cooperation_index', 0):.4f}") print(f" Event Adaptability: {bd.get('event_adaptability', 0):.4f}") strat_strategy = agents.get(strat["agent_id"]) if strat_strategy: print(f" Strategy: {strat_strategy.strategy_name}") print(f"\n Leaderboard:") print(f" {'Rank':<5} {'Agent':<20} {'Strategy':<18} {'Wealth':>10} {'Growth':>10} {'Strategic':>10} {'Awards'}") print(f" {'-'*95}") for entry in results.get("leaderboard", []): awards_str = ", ".join(entry.get("awards_won", [])) or "-" print(f" {entry['rank']:<5} {entry['agent_id']:<20} {entry.get('strategy', '?'):<18} " f"${entry['total_wealth']:>9.2f} ${entry['wealth_growth']:>9.2f} " f"{entry['strategic_score']:>10.4f} {awards_str}") metrics = results.get("market_metrics", {}) print(f"\n Market Activity:") print(f" Total Trades: {metrics.get('total_trades', 0)}") print(f" Total Volume: ${metrics.get('total_volume', 0):.2f}") print(f" Coalitions Formed:{metrics.get('coalitions_formed', 0)}") print(f" Compounds Made: {metrics.get('compound_goods_produced', 0)}") print(f" Deals Negotiated: {metrics.get('deals_negotiated', 0)}") return results if __name__ == "__main__": main()