nice-bill commited on
Commit
6669884
·
1 Parent(s): 1341087

Add core simulation (Agent, Pool, Simulation)

Browse files
Files changed (4) hide show
  1. core/__init__.py +1 -2
  2. core/agent.py +224 -0
  3. core/defi_mechanics.py +167 -0
  4. core/simulation.py +225 -0
core/__init__.py CHANGED
@@ -3,6 +3,5 @@
3
  from .agent import Agent
4
  from .defi_mechanics import Pool
5
  from .simulation import Simulation
6
- from .analyzer import Analyzer
7
 
8
- __all__ = ["Agent", "Pool", "Simulation", "Analyzer"]
 
3
  from .agent import Agent
4
  from .defi_mechanics import Pool
5
  from .simulation import Simulation
 
6
 
7
+ __all__ = ["Agent", "Pool", "Simulation"]
core/agent.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Agent class for DeFi simulation."""
2
+
3
+ import json
4
+ from typing import Dict, List, Tuple, Optional
5
+ from dataclasses import dataclass, field
6
+
7
+ from api.minimax_client import MiniMaxClient
8
+ from config import INITIAL_TOKENS
9
+
10
+
11
+ @dataclass
12
+ class Agent:
13
+ """DeFi trading agent powered by MiniMax."""
14
+
15
+ name: str
16
+ token_a: float = INITIAL_TOKENS
17
+ token_b: float = INITIAL_TOKENS
18
+ trade_history: List[Dict] = field(default_factory=list)
19
+ learning_summary: str = ""
20
+ alliances: Dict[str, str] = field(default_factory=dict)
21
+
22
+ def __post_init__(self):
23
+ self.client = MiniMaxClient()
24
+
25
+ def get_state(self) -> Dict:
26
+ """Get current state for decision making."""
27
+ return {
28
+ "name": self.name,
29
+ "token_a": round(self.token_a, 2),
30
+ "token_b": round(self.token_b, 2),
31
+ "profit": round(self.calculate_profit(), 2),
32
+ "alliances": self.alliances
33
+ }
34
+
35
+ def decide(self, observation: Dict, pool_state: Dict, other_agents: List["Agent"], turn: int) -> Tuple[Dict, str]:
36
+ """
37
+ Ask MiniMax for a decision based on current state.
38
+
39
+ Returns:
40
+ Tuple of (decision_dict, thinking_text)
41
+ """
42
+ prompt = self._build_prompt(observation, pool_state, other_agents, turn)
43
+
44
+ system_prompt = """You are a strategic DeFi trader in an automated market simulation.
45
+ Analyze the market state and make optimal trading decisions.
46
+ Output ONLY valid JSON with your reasoning."""
47
+
48
+ decision, thinking = self.client.call(prompt, system_prompt)
49
+
50
+ # Log the decision
51
+ self.trade_history.append({
52
+ "turn": turn,
53
+ "action": decision.get("action", decision.get("action_type", "unknown")),
54
+ "reasoning": decision.get("reasoning", ""),
55
+ "thinking": thinking
56
+ })
57
+
58
+ return decision, thinking
59
+
60
+ def _build_prompt(self, observation: Dict, pool_state: Dict, other_agents: List["Agent"], turn: int) -> str:
61
+ """Build the decision prompt."""
62
+ other_states = [a.get_state() for a in other_agents if a.name != self.name]
63
+
64
+ prompt = f"""
65
+ You are {self.name}, an AI agent in a DeFi market simulation.
66
+
67
+ === YOUR STATE ===
68
+ Token A: {self.token_a:.2f}
69
+ Token B: {self.token_b:.2f}
70
+ Profit: {self.calculate_profit():.2f}
71
+
72
+ === MARKET STATE ===
73
+ Pool reserves: A={pool_state.get('reserve_a', 0):.2f}, B={pool_state.get('reserve_b', 0):.2f}
74
+ Price (A/B): {pool_state.get('price_ab', 0):.4f}
75
+
76
+ === OTHER AGENTS ===
77
+ {json.dumps(other_states, indent=2)}
78
+
79
+ === YOUR LEARNING ===
80
+ {self.learning_summary if self.learning_summary else "No previous runs yet."}
81
+
82
+ === AVAILABLE ACTIONS ===
83
+ 1. "swap": Trade tokens (specify from, to, amount)
84
+ 2. "provide_liquidity": Add liquidity to pool (specify amounts)
85
+ 3. "propose_alliance": Suggest collaboration (specify agent name)
86
+ 4. "do_nothing": Wait for better opportunity
87
+
88
+ Output JSON:
89
+ {{
90
+ "action": "swap|provide_liquidity|propose_alliance|do_nothing",
91
+ "reasoning": "your reasoning",
92
+ "payload": {{...action specific data...}}
93
+ }}
94
+ """
95
+ return prompt
96
+
97
+ def calculate_profit(self) -> float:
98
+ """Calculate profit from initial state."""
99
+ return (self.token_a + self.token_b) - (INITIAL_TOKENS * 2)
100
+
101
+ def infer_strategy(self) -> str:
102
+ """Infer the agent's strategy from recent actions."""
103
+ if not self.trade_history:
104
+ return "unknown"
105
+
106
+ recent = self.trade_history[-10:]
107
+ actions = [h["action"] for h in recent if "action" in h]
108
+
109
+ if not actions:
110
+ return "unknown"
111
+
112
+ # Return most common action
113
+ from collections import Counter
114
+ return Counter(actions).most_common(1)[0][0]
115
+
116
+ def update_learning(self, run_number: int, metrics: Dict):
117
+ """Extract learnings after a run completes."""
118
+ prompt = f"""
119
+ You just completed run {run_number}.
120
+
121
+ Your performance: Profit={self.calculate_profit():.2f}, Strategy={self.infer_strategy()}
122
+ Market metrics: Gini={metrics.get('gini_coefficient', 0):.3f}, Avg Profit={metrics.get('avg_agent_profit', 0):.2f}
123
+
124
+ What did you learn in 1-2 sentences?
125
+ Output JSON: {{"learning": "your learning"}}
126
+ """
127
+
128
+ try:
129
+ response, _ = self.client.call(prompt)
130
+ self.learning_summary = response.get("learning", "")
131
+ except Exception:
132
+ self.learning_summary = "Learning extraction failed."
133
+
134
+ def execute_action(self, decision: Dict, pool: "Pool") -> bool:
135
+ """Execute the decided action on the pool."""
136
+ action = decision.get("action", decision.get("action_type", ""))
137
+ payload = decision.get("payload", {})
138
+
139
+ if action == "swap":
140
+ return self._execute_swap(payload, pool)
141
+ elif action == "provide_liquidity":
142
+ return self._execute_liquidity(payload, pool)
143
+ elif action == "propose_alliance":
144
+ return self._execute_alliance(payload)
145
+ else:
146
+ # do_nothing or unknown action - always succeeds
147
+ return True
148
+
149
+ def _execute_swap(self, payload: Dict, pool: "Pool") -> bool:
150
+ """Execute a swap action."""
151
+ amount = payload.get("amount", 0)
152
+ from_token = payload.get("from", "a")
153
+
154
+ if from_token == "a" and self.token_a >= amount:
155
+ output, fee = pool.swap("a", amount, self.name)
156
+ self.token_a -= amount
157
+ self.token_b += output
158
+ return True
159
+ elif from_token == "b" and self.token_b >= amount:
160
+ output, fee = pool.swap("b", amount, self.name)
161
+ self.token_b -= amount
162
+ self.token_a += output
163
+ return True
164
+
165
+ return False
166
+
167
+ def _execute_liquidity(self, payload: Dict, pool: "Pool") -> bool:
168
+ """Execute a provide liquidity action."""
169
+ amount_a = payload.get("amount_a", 0)
170
+ amount_b = payload.get("amount_b", 0)
171
+
172
+ if self.token_a >= amount_a and self.token_b >= amount_b:
173
+ pool.provide_liquidity(amount_a, amount_b, self.name)
174
+ self.token_a -= amount_a
175
+ self.token_b -= amount_b
176
+ return True
177
+
178
+ return False
179
+
180
+ def _execute_alliance(self, payload: Dict) -> bool:
181
+ """Record an alliance proposal."""
182
+ agent_name = payload.get("agent_name", "")
183
+ if agent_name:
184
+ self.alliances[agent_name] = "proposed"
185
+ return True
186
+ return False
187
+
188
+
189
+ def test_agent():
190
+ """Test the Agent class."""
191
+ from core.defi_mechanics import Pool
192
+
193
+ print("Testing Agent class...")
194
+
195
+ # Create agent
196
+ agent = Agent("TestAgent")
197
+ print(f"Created agent: {agent.name}")
198
+ print(f"Initial state: {agent.get_state()}")
199
+
200
+ # Create pool
201
+ pool = Pool(reserve_a=1000, reserve_b=1000)
202
+
203
+ # Get decision
204
+ observation = {"turn": 0, "event": "test"}
205
+ pool_state = pool.__dict__
206
+
207
+ print("\nGetting decision from MiniMax...")
208
+ decision, thinking = agent.decide(observation, pool_state, [], 0)
209
+
210
+ print(f"Decision: {json.dumps(decision, indent=2)}")
211
+ print(f"Thinking length: {len(thinking)}")
212
+ print(f"Profit: {agent.calculate_profit():.2f}")
213
+ print(f"Strategy: {agent.infer_strategy()}")
214
+
215
+ # Test action execution
216
+ if decision.get("action") == "swap":
217
+ agent.execute_action(decision, pool)
218
+ print(f"After swap: {agent.get_state()}")
219
+
220
+ print("\nAgent test complete!")
221
+
222
+
223
+ if __name__ == "__main__":
224
+ test_agent()
core/defi_mechanics.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DeFi mechanics: Constant product AMM pool."""
2
+
3
+ from typing import Dict, Tuple
4
+ from dataclasses import dataclass, field
5
+
6
+ from config import SWAP_FEE
7
+
8
+
9
+ @dataclass
10
+ class Pool:
11
+ """Constant product automated market maker (AMM)."""
12
+
13
+ reserve_a: float = 1000
14
+ reserve_b: float = 1000
15
+ liquidity_providers: Dict[str, float] = field(default_factory=dict)
16
+ _constant_product: float = None
17
+
18
+ def __post_init__(self):
19
+ self._constant_product = self.reserve_a * self.reserve_b
20
+
21
+ def swap(self, token_in: str, amount_in: float, agent_name: str) -> Tuple[float, float]:
22
+ """
23
+ Execute a swap on the pool.
24
+
25
+ Returns:
26
+ Tuple of (amount_out, fee)
27
+ """
28
+ if amount_in <= 0:
29
+ return 0, 0
30
+
31
+ if token_in == "a":
32
+ amount_out = self._calculate_output(amount_in, self.reserve_a, self.reserve_b)
33
+ fee = amount_out * SWAP_FEE
34
+ amount_out -= fee
35
+
36
+ self.reserve_a += amount_in
37
+ self.reserve_b -= amount_out
38
+ else:
39
+ amount_out = self._calculate_output(amount_in, self.reserve_b, self.reserve_a)
40
+ fee = amount_out * SWAP_FEE
41
+ amount_out -= fee
42
+
43
+ self.reserve_b += amount_in
44
+ self.reserve_a -= amount_out
45
+
46
+ self._constant_product = self.reserve_a * self.reserve_b
47
+ return amount_out, fee
48
+
49
+ def provide_liquidity(self, amount_a: float, amount_b: float, agent_name: str) -> float:
50
+ """Add liquidity to the pool and mint LP tokens."""
51
+ if amount_a <= 0 or amount_b <= 0:
52
+ return 0
53
+
54
+ # Calculate LP tokens to mint (simple share model)
55
+ total_liquidity = sum(self.liquidity_providers.values())
56
+ if total_liquidity == 0:
57
+ # Initial liquidity - use geometric mean
58
+ lp_tokens = (amount_a * amount_b) ** 0.5
59
+ else:
60
+ # Proportional to existing liquidity
61
+ share_a = amount_a / self.reserve_a
62
+ share_b = amount_b / self.reserve_b
63
+ share = min(share_a, share_b) # Use smaller share to prevent imbalance
64
+ lp_tokens = share * (self.reserve_a + self.reserve_b)
65
+
66
+ self.reserve_a += amount_a
67
+ self.reserve_b += amount_b
68
+ self.liquidity_providers[agent_name] = (
69
+ self.liquidity_providers.get(agent_name, 0) + lp_tokens
70
+ )
71
+
72
+ self._constant_product = self.reserve_a * self.reserve_b
73
+ return lp_tokens
74
+
75
+ def withdraw_liquidity(self, lp_tokens: float, agent_name: str) -> Tuple[float, float]:
76
+ """Remove liquidity and burn LP tokens."""
77
+ total_lp = sum(self.liquidity_providers.values())
78
+ if total_lp == 0 or lp_tokens <= 0:
79
+ return 0, 0
80
+
81
+ share = lp_tokens / total_lp
82
+ amount_a = self.reserve_a * share
83
+ amount_b = self.reserve_b * share
84
+
85
+ self.reserve_a -= amount_a
86
+ self.reserve_b -= amount_b
87
+ self.liquidity_providers[agent_name] -= lp_tokens
88
+
89
+ self._constant_product = self.reserve_a * self.reserve_b
90
+ return amount_a, amount_b
91
+
92
+ @property
93
+ def price_ab(self) -> float:
94
+ """Get price of A in terms of B."""
95
+ return self.reserve_b / self.reserve_a if self.reserve_a > 0 else 0
96
+
97
+ @property
98
+ def price_ba(self) -> float:
99
+ """Get price of B in terms of A."""
100
+ return self.reserve_a / self.reserve_b if self.reserve_b > 0 else 0
101
+
102
+ @property
103
+ def total_liquidity(self) -> float:
104
+ """Get total liquidity in the pool."""
105
+ return sum(self.liquidity_providers.values())
106
+
107
+ @property
108
+ def constant_product(self) -> float:
109
+ """Get the constant product k = a * b."""
110
+ if self._constant_product is None:
111
+ self._constant_product = self.reserve_a * self.reserve_b
112
+ return self._constant_product
113
+
114
+ @staticmethod
115
+ def _calculate_output(amount_in: float, reserve_in: float, reserve_out: float) -> float:
116
+ """
117
+ Calculate output amount using constant product formula.
118
+ (x + dx) * (y - dy) = x * y
119
+ dy = y * dx / (x + dx)
120
+ """
121
+ if amount_in <= 0 or reserve_in <= 0 or reserve_out <= 0:
122
+ return 0
123
+
124
+ numerator = amount_in * reserve_out
125
+ denominator = reserve_in + amount_in
126
+
127
+ return numerator / denominator
128
+
129
+ def get_state(self) -> Dict:
130
+ """Get pool state for agents."""
131
+ return {
132
+ "reserve_a": self.reserve_a,
133
+ "reserve_b": self.reserve_b,
134
+ "price_ab": self.price_ab,
135
+ "price_ba": self.price_ba,
136
+ "total_liquidity": self.total_liquidity,
137
+ "constant_product": self.constant_product
138
+ }
139
+
140
+
141
+ def test_pool():
142
+ """Test the Pool class."""
143
+ print("Testing Pool class...")
144
+
145
+ # Create pool
146
+ pool = Pool(reserve_a=1000, reserve_b=1000)
147
+ print(f"Initial pool: A={pool.reserve_a}, B={pool.reserve_b}")
148
+ print(f"Price A/B: {pool.price_ab:.4f}")
149
+ print(f"Constant product: {pool.constant_product}")
150
+
151
+ # Test swap
152
+ print("\nTesting swap: 100 A for B...")
153
+ amount_out, fee = pool.swap("a", 100, "TestAgent")
154
+ print(f"Output: {amount_out:.4f} B, Fee: {fee:.4f}")
155
+ print(f"Pool after swap: A={pool.reserve_a:.2f}, B={pool.reserve_b:.2f}")
156
+
157
+ # Test liquidity provision
158
+ print("\nTesting liquidity provision...")
159
+ lp = pool.provide_liquidity(200, 200, "TestAgent")
160
+ print(f"LP tokens minted: {lp:.4f}")
161
+ print(f"Total liquidity: {pool.total_liquidity:.4f}")
162
+
163
+ print("\nPool test complete!")
164
+
165
+
166
+ if __name__ == "__main__":
167
+ test_pool()
core/simulation.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Main simulation engine for DeFi agent market."""
2
+
3
+ import json
4
+ from typing import List, Dict, Optional
5
+ from dataclasses import dataclass
6
+
7
+ from core.agent import Agent
8
+ from core.defi_mechanics import Pool
9
+ from api.supabase_client import (
10
+ SupabaseClient, RunData, AgentStateData, PoolStateData, ActionData, MetricsData
11
+ )
12
+ from config import NUM_AGENTS, TURNS_PER_RUN
13
+
14
+
15
+ @dataclass
16
+ class Simulation:
17
+ """Orchestrates the DeFi agent simulation."""
18
+
19
+ num_agents: int = NUM_AGENTS
20
+ turns_per_run: int = TURNS_PER_RUN
21
+ supabase: Optional[SupabaseClient] = None
22
+
23
+ def __post_init__(self):
24
+ self.agents: List[Agent] = []
25
+ self.pool: Optional[Pool] = None
26
+ self.current_run_id: Optional[int] = None
27
+ self.current_run_number: int = 0
28
+
29
+ if self.supabase is None:
30
+ try:
31
+ self.supabase = SupabaseClient()
32
+ except ValueError:
33
+ print("Warning: Supabase not configured. Running without persistence.")
34
+ self.supabase = None
35
+
36
+ def initialize_run(self, run_number: int = None):
37
+ """Initialize a new run with agents and pool."""
38
+ if run_number is None:
39
+ if self.supabase:
40
+ run_number = self.supabase.get_next_run_number()
41
+ else:
42
+ run_number = self.current_run_number + 1
43
+
44
+ self.current_run_number = run_number
45
+ self.agents = [Agent(f"Agent_{i}") for i in range(self.num_agents)]
46
+ self.pool = Pool()
47
+
48
+ print(f"Initialized run {run_number} with {self.num_agents} agents")
49
+
50
+ if self.supabase:
51
+ self.current_run_id = self.supabase.create_run(run_number)
52
+ print(f"Created run in database: ID {self.current_run_id}")
53
+
54
+ def run(self, run_number: int = None) -> Dict:
55
+ """Execute a complete simulation run."""
56
+ self.initialize_run(run_number)
57
+
58
+ print(f"\nStarting run {self.current_run_number} with {self.turns_per_run} turns...")
59
+
60
+ for turn in range(self.turns_per_run):
61
+ print(f"\n--- Turn {turn + 1}/{self.turns_per_run} ---")
62
+
63
+ # Each agent makes a decision
64
+ for agent in self.agents:
65
+ decision, thinking = self._agent_decide(agent, turn)
66
+
67
+ # Execute action
68
+ if decision:
69
+ success = agent.execute_action(decision, self.pool)
70
+ print(f" {agent.name}: {decision.get('action', 'unknown')} {'OK' if success else 'FAIL'}")
71
+
72
+ # Save action to database
73
+ if self.supabase:
74
+ self._save_action(agent, turn, decision, thinking)
75
+
76
+ # Save state snapshots
77
+ if self.supabase:
78
+ self._save_states(turn)
79
+
80
+ # Calculate and save metrics
81
+ metrics = self._calculate_metrics()
82
+
83
+ if self.supabase:
84
+ self.supabase.complete_run(self.current_run_id)
85
+ self.supabase.save_metrics(
86
+ MetricsData(
87
+ run_id=self.current_run_id,
88
+ gini_coefficient=metrics.get("gini_coefficient", 0),
89
+ cooperation_rate=metrics.get("cooperation_rate", 0),
90
+ betrayal_count=metrics.get("betrayal_count", 0),
91
+ avg_agent_profit=metrics.get("avg_agent_profit", 0),
92
+ pool_stability=metrics.get("pool_stability", 0)
93
+ )
94
+ )
95
+
96
+ # Update agent learning
97
+ for agent in self.agents:
98
+ agent.update_learning(self.current_run_number, metrics)
99
+
100
+ print(f"\n--- Run {self.current_run_number} Complete ---")
101
+ print(f"Final metrics: {json.dumps(metrics, indent=2)}")
102
+
103
+ self.current_run_number += 1
104
+ return metrics
105
+
106
+ def _agent_decide(self, agent: Agent, turn: int) -> tuple:
107
+ """Get decision from agent."""
108
+ observation = {
109
+ "turn": turn,
110
+ "event": "trading"
111
+ }
112
+ pool_state = self.pool.get_state()
113
+
114
+ try:
115
+ decision, thinking = agent.decide(
116
+ observation,
117
+ pool_state,
118
+ self.agents,
119
+ turn
120
+ )
121
+ return decision, thinking
122
+ except Exception as e:
123
+ print(f" {agent.name}: Decision error - {e}")
124
+ return {"action": "do_nothing", "reasoning": f"Error: {e}"}, ""
125
+
126
+ def _save_action(self, agent: Agent, turn: int, decision: Dict, thinking: str):
127
+ """Save agent action to database."""
128
+ self.supabase.save_action(ActionData(
129
+ run_id=self.current_run_id,
130
+ turn=turn,
131
+ agent_name=agent.name,
132
+ action_type=decision.get("action", "unknown"),
133
+ payload=decision.get("payload", {}),
134
+ reasoning_trace=decision.get("reasoning", ""),
135
+ thinking_trace=thinking
136
+ ))
137
+
138
+ def _save_states(self, turn: int):
139
+ """Save agent and pool states to database."""
140
+ # Save agent states
141
+ for agent in self.agents:
142
+ self.supabase.save_agent_state(AgentStateData(
143
+ run_id=self.current_run_id,
144
+ turn=turn,
145
+ agent_name=agent.name,
146
+ token_a_balance=agent.token_a,
147
+ token_b_balance=agent.token_b,
148
+ profit=agent.calculate_profit(),
149
+ strategy=agent.infer_strategy()
150
+ ))
151
+
152
+ # Save pool state
153
+ self.supabase.save_pool_state(PoolStateData(
154
+ run_id=self.current_run_id,
155
+ turn=turn,
156
+ reserve_a=self.pool.reserve_a,
157
+ reserve_b=self.pool.reserve_b,
158
+ price_ab=self.pool.price_ab,
159
+ total_liquidity=self.pool.total_liquidity
160
+ ))
161
+
162
+ def _calculate_metrics(self) -> Dict:
163
+ """Calculate run metrics."""
164
+ if not self.agents:
165
+ return {}
166
+
167
+ profits = [a.calculate_profit() for a in self.agents]
168
+ gini = self._gini_coefficient(profits)
169
+
170
+ return {
171
+ "gini_coefficient": gini,
172
+ "avg_agent_profit": sum(profits) / len(profits),
173
+ "cooperation_rate": self._calculate_cooperation(),
174
+ "betrayal_count": self._count_betrayals(),
175
+ "pool_stability": self.pool.reserve_a * self.pool.reserve_b
176
+ }
177
+
178
+ @staticmethod
179
+ def _gini_coefficient(values: List[float]) -> float:
180
+ """Calculate Gini coefficient for wealth distribution."""
181
+ if not values or sum(values) == 0:
182
+ return 0
183
+
184
+ sorted_vals = sorted(values)
185
+ n = len(sorted_vals)
186
+ cumsum = 0
187
+ for i, val in enumerate(sorted_vals):
188
+ cumsum += (i + 1) * val
189
+
190
+ gini = (2 * cumsum) / (n * sum(sorted_vals)) - (n + 1) / n
191
+ return max(0, gini) # Ensure non-negative
192
+
193
+ def _calculate_cooperation(self) -> float:
194
+ """Calculate cooperation rate (alliances / agents)."""
195
+ total_alliances = sum(len(a.alliances) for a in self.agents)
196
+ return total_alliances / max(len(self.agents), 1)
197
+
198
+ def _count_betrayals(self) -> int:
199
+ """Count betrayal events (placeholder for future implementation)."""
200
+ return 0
201
+
202
+
203
+ def test_simulation():
204
+ """Test the simulation with a short run."""
205
+ print("Testing Simulation class...")
206
+ print("(Running without Supabase for quick test)\n")
207
+
208
+ sim = Simulation(num_agents=3, turns_per_run=3, supabase=None)
209
+ metrics = sim.run()
210
+
211
+ print(f"\nFinal Metrics:")
212
+ print(f" Gini Coefficient: {metrics['gini_coefficient']:.4f}")
213
+ print(f" Avg Agent Profit: {metrics['avg_agent_profit']:.2f}")
214
+ print(f" Pool Stability: {metrics['pool_stability']:.2f}")
215
+
216
+ # Show agent states
217
+ print("\nFinal Agent States:")
218
+ for agent in sim.agents:
219
+ print(f" {agent.name}: A={agent.token_a:.2f}, B={agent.token_b:.2f}, Profit={agent.calculate_profit():.2f}")
220
+
221
+ print("\nSimulation test complete!")
222
+
223
+
224
+ if __name__ == "__main__":
225
+ test_simulation()