Spaces:
Sleeping
Sleeping
Add core simulation (Agent, Pool, Simulation)
Browse files- core/__init__.py +1 -2
- core/agent.py +224 -0
- core/defi_mechanics.py +167 -0
- 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"
|
|
|
|
| 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()
|