426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
928 lines
32 KiB
Python
928 lines
32 KiB
Python
"""
|
|
Fast Simulator - BENCHMARK EVALUATION ONLY
|
|
|
|
=================================================================
|
|
WARNING: This module is for BENCHMARK/EVALUATION purposes only!
|
|
Do NOT use for training data generation.
|
|
|
|
Training data MUST come from:
|
|
1. TrajectoryGenerator (real agents with real LLM calls)
|
|
2. Database trajectories (real production data)
|
|
|
|
This simulator generates SYNTHETIC market data which is useful for:
|
|
- Consistent benchmark comparisons
|
|
- Deterministic testing
|
|
- Performance evaluation
|
|
|
|
It should NEVER be used to generate training data because:
|
|
- Market data is synthetic (not real)
|
|
- Price movements are random (not realistic)
|
|
- Agent interactions are simulated (not real LLM calls)
|
|
=================================================================
|
|
|
|
Key features:
|
|
- Zero artificial delays
|
|
- Minimal memory allocations
|
|
- Async batch processing
|
|
- Deterministic replay of benchmark snapshots
|
|
|
|
Usage:
|
|
# For benchmarking ONLY
|
|
simulator = FastSimulator.for_benchmark(snapshot)
|
|
results = await simulator.run_benchmark(agents)
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import time
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from typing import Literal, Protocol
|
|
|
|
import asyncpg
|
|
|
|
from .models import (
|
|
FeedTrajectory,
|
|
EnvironmentState,
|
|
)
|
|
from .quality_utils import (
|
|
build_trajectory_from_ticks,
|
|
calculate_trajectory_quality_score,
|
|
)
|
|
from .rollout_generator import AgentTickData, RolloutResult
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class SimulatorConfig:
|
|
"""Configuration for the fast simulator"""
|
|
|
|
# Mode
|
|
mode: Literal["benchmark", "data_generation"] = "data_generation"
|
|
|
|
# Speed settings
|
|
max_concurrent_agents: int = 8
|
|
batch_size: int = 4 # Number of agents to process per batch
|
|
|
|
# Tick settings
|
|
ticks_per_window: int = 60 # Ticks in a 1-hour window
|
|
max_ticks: int = 1000
|
|
|
|
# Database
|
|
database_url: str = ""
|
|
save_to_db: bool = True
|
|
|
|
# Quality settings
|
|
min_actions_per_trajectory: int = 5
|
|
|
|
# Benchmark settings (if mode='benchmark')
|
|
benchmark_snapshot: dict | None = None
|
|
ground_truth: dict | None = None
|
|
|
|
|
|
@dataclass
|
|
class SimulatorMetrics:
|
|
"""Metrics collected during simulation"""
|
|
|
|
start_time: float = 0.0
|
|
end_time: float = 0.0
|
|
|
|
total_ticks: int = 0
|
|
total_agents: int = 0
|
|
total_llm_calls: int = 0
|
|
total_actions: int = 0
|
|
|
|
successful_trajectories: int = 0
|
|
failed_trajectories: int = 0
|
|
|
|
avg_tick_duration_ms: float = 0.0
|
|
ticks_per_second: float = 0.0
|
|
|
|
# Benchmark specific
|
|
total_pnl: float = 0.0
|
|
avg_accuracy: float = 0.0
|
|
avg_optimality: float = 0.0
|
|
|
|
def finalize(self) -> None:
|
|
"""Calculate final metrics"""
|
|
duration = self.end_time - self.start_time
|
|
if duration > 0:
|
|
self.ticks_per_second = self.total_ticks / duration
|
|
if self.total_ticks > 0:
|
|
self.avg_tick_duration_ms = (duration / self.total_ticks) * 1000
|
|
|
|
|
|
class AgentRunner(Protocol):
|
|
"""Protocol for running agents"""
|
|
|
|
async def run_tick(
|
|
self,
|
|
agent_id: str,
|
|
observation: dict,
|
|
env_state: EnvironmentState,
|
|
) -> AgentTickData:
|
|
"""Run a single agent tick"""
|
|
...
|
|
|
|
|
|
@dataclass
|
|
class GameState:
|
|
"""
|
|
Rich game state for realistic simulation.
|
|
|
|
Provides detailed market data, orderbooks, news, and social context
|
|
that mirrors production environment for high-quality training data.
|
|
"""
|
|
|
|
tick: int = 0
|
|
time: int = 0
|
|
|
|
# Markets with detailed state
|
|
markets: list[dict] = field(default_factory=list)
|
|
perpetuals: list[dict] = field(default_factory=list)
|
|
|
|
# Market microstructure
|
|
orderbooks: dict[str, dict] = field(default_factory=dict) # market_id -> orderbook
|
|
|
|
# News/Social
|
|
news: list[dict] = field(default_factory=list)
|
|
posts: list[dict] = field(default_factory=list)
|
|
|
|
# Agent states
|
|
portfolios: dict[str, dict] = field(default_factory=dict)
|
|
|
|
# Price history for technical analysis
|
|
price_history: dict[str, list[float]] = field(default_factory=dict)
|
|
|
|
def _generate_realistic_markets(self) -> list[dict]:
|
|
"""Generate realistic prediction markets if none exist"""
|
|
import random
|
|
|
|
if self.markets:
|
|
return self.markets
|
|
|
|
market_templates = [
|
|
{"question": "Will BTC hit $120,000 by end of month?", "category": "crypto"},
|
|
{"question": "Will ETH outperform BTC this week?", "category": "crypto"},
|
|
{"question": "Will the Fed announce rate cuts?", "category": "macro"},
|
|
{"question": "Will NVIDIA stock reach new ATH?", "category": "stocks"},
|
|
{"question": "Will total crypto market cap exceed $4T?", "category": "crypto"},
|
|
]
|
|
|
|
markets = []
|
|
for i, template in enumerate(market_templates):
|
|
yes_prob = random.uniform(0.2, 0.8)
|
|
markets.append(
|
|
{
|
|
"id": f"market-{i + 1}",
|
|
"question": template["question"],
|
|
"category": template["category"],
|
|
"yesPrice": round(yes_prob, 2),
|
|
"noPrice": round(1 - yes_prob, 2),
|
|
"volume24h": random.randint(10000, 500000),
|
|
"liquidity": random.randint(50000, 1000000),
|
|
"expiresAt": self.time + random.randint(86400000, 604800000), # 1-7 days
|
|
"status": "active",
|
|
}
|
|
)
|
|
|
|
return markets
|
|
|
|
def _generate_realistic_perpetuals(self) -> list[dict]:
|
|
"""Generate realistic perpetual markets"""
|
|
import random
|
|
|
|
if self.perpetuals:
|
|
return self.perpetuals
|
|
|
|
tickers = ["BTC", "ETH", "SOL", "DOGE", "AVAX"]
|
|
base_prices = {"BTC": 100000, "ETH": 3500, "SOL": 180, "DOGE": 0.35, "AVAX": 40}
|
|
|
|
perpetuals = []
|
|
for ticker in tickers:
|
|
base = base_prices.get(ticker, 100)
|
|
price = base * (1 + random.uniform(-0.05, 0.05))
|
|
|
|
perpetuals.append(
|
|
{
|
|
"ticker": ticker,
|
|
"markPrice": round(price, 2),
|
|
"indexPrice": round(price * (1 + random.uniform(-0.001, 0.001)), 2),
|
|
"fundingRate": round(random.uniform(-0.001, 0.001), 6),
|
|
"openInterest": random.randint(1000000, 50000000),
|
|
"volume24h": random.randint(5000000, 100000000),
|
|
"change24h": round(random.uniform(-0.1, 0.1), 4),
|
|
"high24h": round(price * 1.05, 2),
|
|
"low24h": round(price * 0.95, 2),
|
|
}
|
|
)
|
|
|
|
return perpetuals
|
|
|
|
def _generate_orderbook(self, market_id: str, mid_price: float) -> dict:
|
|
"""Generate realistic orderbook data"""
|
|
import random
|
|
|
|
bids = []
|
|
asks = []
|
|
|
|
for i in range(5):
|
|
spread = 0.01 * (i + 1)
|
|
bid_price = round(mid_price * (1 - spread), 4)
|
|
ask_price = round(mid_price * (1 + spread), 4)
|
|
|
|
bids.append(
|
|
{
|
|
"price": bid_price,
|
|
"size": random.randint(100, 5000),
|
|
"total": random.randint(1000, 10000),
|
|
}
|
|
)
|
|
asks.append(
|
|
{
|
|
"price": ask_price,
|
|
"size": random.randint(100, 5000),
|
|
"total": random.randint(1000, 10000),
|
|
}
|
|
)
|
|
|
|
return {
|
|
"market_id": market_id,
|
|
"bids": sorted(bids, key=lambda x: -x["price"]),
|
|
"asks": sorted(asks, key=lambda x: x["price"]),
|
|
"spread": round((asks[0]["price"] - bids[0]["price"]) / mid_price * 100, 2),
|
|
"mid_price": mid_price,
|
|
}
|
|
|
|
def _generate_news(self) -> list[dict]:
|
|
"""Generate realistic news items"""
|
|
import random
|
|
|
|
if self.news:
|
|
return self.news
|
|
|
|
news_templates = [
|
|
{
|
|
"headline": "Bitcoin Approaches Key Resistance Level",
|
|
"sentiment": "bullish",
|
|
"impact": "high",
|
|
},
|
|
{
|
|
"headline": "Federal Reserve Hints at Policy Shift",
|
|
"sentiment": "neutral",
|
|
"impact": "high",
|
|
},
|
|
{
|
|
"headline": "Major Exchange Reports Record Trading Volume",
|
|
"sentiment": "bullish",
|
|
"impact": "medium",
|
|
},
|
|
{
|
|
"headline": "Regulatory Clarity Expected Next Month",
|
|
"sentiment": "neutral",
|
|
"impact": "medium",
|
|
},
|
|
{
|
|
"headline": "Whale Alert: Large Transfer Detected",
|
|
"sentiment": "bearish",
|
|
"impact": "low",
|
|
},
|
|
{
|
|
"headline": "New DeFi Protocol Launches with $50M TVL",
|
|
"sentiment": "bullish",
|
|
"impact": "low",
|
|
},
|
|
{
|
|
"headline": "Mining Difficulty Reaches New High",
|
|
"sentiment": "neutral",
|
|
"impact": "low",
|
|
},
|
|
]
|
|
|
|
news = []
|
|
selected = random.sample(news_templates, min(5, len(news_templates)))
|
|
for i, template in enumerate(selected):
|
|
news.append(
|
|
{
|
|
"id": f"news-{self.tick}-{i}",
|
|
"headline": template["headline"],
|
|
"sentiment": template["sentiment"],
|
|
"impact": template["impact"],
|
|
"source": random.choice(["CoinDesk", "Bloomberg", "Reuters", "CryptoNews"]),
|
|
"timestamp": self.time - random.randint(0, 3600000), # Last hour
|
|
"relevance_score": random.uniform(0.5, 1.0),
|
|
}
|
|
)
|
|
|
|
return news
|
|
|
|
def _generate_social_posts(self) -> list[dict]:
|
|
"""Generate realistic social posts"""
|
|
import random
|
|
|
|
if self.posts:
|
|
return self.posts
|
|
|
|
post_templates = [
|
|
{"content": "Just went long on BTC, looking bullish 🚀", "sentiment": "bullish"},
|
|
{"content": "Taking profits here, market looks overextended", "sentiment": "bearish"},
|
|
{"content": "Anyone else seeing this pattern on the 4H chart?", "sentiment": "neutral"},
|
|
{"content": "New ATH incoming, calling it now 💎🙌", "sentiment": "bullish"},
|
|
{"content": "Be careful, volume is declining", "sentiment": "bearish"},
|
|
{"content": "Great entry opportunity if you missed the dip", "sentiment": "bullish"},
|
|
{"content": "Liquidation cascade might be coming", "sentiment": "bearish"},
|
|
]
|
|
|
|
posts = []
|
|
selected = random.sample(post_templates, min(6, len(post_templates)))
|
|
for i, template in enumerate(selected):
|
|
posts.append(
|
|
{
|
|
"id": f"post-{self.tick}-{i}",
|
|
"author": f"trader_{random.randint(100, 999)}",
|
|
"content": template["content"],
|
|
"sentiment": template["sentiment"],
|
|
"likes": random.randint(0, 500),
|
|
"replies": random.randint(0, 50),
|
|
"timestamp": self.time - random.randint(0, 1800000), # Last 30 min
|
|
"verified": random.random() > 0.7,
|
|
}
|
|
)
|
|
|
|
return posts
|
|
|
|
def to_observation(self) -> dict:
|
|
"""
|
|
Convert to rich agent observation.
|
|
|
|
Provides all data an agent needs for informed decision-making:
|
|
- Current market state with prices and volume
|
|
- Orderbook depth for execution planning
|
|
- Recent news with sentiment
|
|
- Social feed for market pulse
|
|
- Technical indicators (price history)
|
|
"""
|
|
# Ensure we have markets
|
|
markets = self._generate_realistic_markets() if not self.markets else self.markets
|
|
perpetuals = (
|
|
self._generate_realistic_perpetuals() if not self.perpetuals else self.perpetuals
|
|
)
|
|
news = self._generate_news() if not self.news else self.news
|
|
posts = self._generate_social_posts() if not self.posts else self.posts
|
|
|
|
# Generate orderbooks for each market
|
|
orderbooks = {}
|
|
for market in markets:
|
|
mid_price = market.get("yesPrice", 0.5)
|
|
orderbooks[market["id"]] = self._generate_orderbook(market["id"], mid_price)
|
|
|
|
return {
|
|
"tick": self.tick,
|
|
"time": self.time,
|
|
"timestamp_human": datetime.fromtimestamp(
|
|
self.time / 1000, tz=timezone.utc
|
|
).isoformat(),
|
|
# Market data
|
|
"markets": markets,
|
|
"perpetuals": perpetuals,
|
|
"orderbooks": orderbooks,
|
|
# Information sources
|
|
"news": news[:5],
|
|
"social_feed": posts[:10],
|
|
# Market summary
|
|
"market_summary": {
|
|
"total_markets": len(markets),
|
|
"total_perpetuals": len(perpetuals),
|
|
"avg_sentiment": self._calculate_avg_sentiment(news, posts),
|
|
"market_momentum": self._calculate_momentum(),
|
|
},
|
|
}
|
|
|
|
def _calculate_avg_sentiment(self, news: list[dict], posts: list[dict]) -> str:
|
|
"""Calculate overall market sentiment"""
|
|
sentiment_scores = {"bullish": 1, "neutral": 0, "bearish": -1}
|
|
|
|
scores = []
|
|
for item in news + posts:
|
|
sentiment = item.get("sentiment", "neutral")
|
|
scores.append(sentiment_scores.get(sentiment, 0))
|
|
|
|
if not scores:
|
|
return "neutral"
|
|
|
|
avg = sum(scores) / len(scores)
|
|
if avg > 0.3:
|
|
return "bullish"
|
|
elif avg < -0.3:
|
|
return "bearish"
|
|
return "neutral"
|
|
|
|
def _calculate_momentum(self) -> str:
|
|
"""Calculate market momentum from price history"""
|
|
import random
|
|
|
|
# In production, this would use actual price history
|
|
return random.choice(["strong_bullish", "bullish", "neutral", "bearish", "strong_bearish"])
|
|
|
|
def get_env_state(self, agent_id: str) -> EnvironmentState:
|
|
"""Get environment state for an agent"""
|
|
portfolio = self.portfolios.get(agent_id, {})
|
|
return EnvironmentState(
|
|
agent_balance=portfolio.get("balance", 10000.0),
|
|
agent_pnl=portfolio.get("pnl", 0.0),
|
|
open_positions=portfolio.get("positions", 0),
|
|
active_markets=len(self.markets) if self.markets else 5,
|
|
)
|
|
|
|
|
|
class FastSimulator:
|
|
"""
|
|
Fast simulator for benchmarking and data generation.
|
|
|
|
Optimized for throughput with minimal overhead.
|
|
"""
|
|
|
|
def __init__(self, config: SimulatorConfig):
|
|
self.config = config
|
|
self.metrics = SimulatorMetrics()
|
|
self.db_pool: asyncpg.Pool | None = None
|
|
|
|
# State
|
|
self.current_tick = 0
|
|
self.game_state = GameState()
|
|
self.agent_trajectories: dict[str, list[AgentTickData]] = {}
|
|
|
|
# Benchmark data (if applicable)
|
|
self.benchmark_ticks: list[dict] = []
|
|
self.ground_truth: dict = {}
|
|
|
|
@classmethod
|
|
def for_benchmark(cls, snapshot: dict) -> "FastSimulator":
|
|
"""Create simulator for benchmarking"""
|
|
config = SimulatorConfig(
|
|
mode="benchmark",
|
|
benchmark_snapshot=snapshot,
|
|
ground_truth=snapshot.get("groundTruth", {}),
|
|
max_ticks=len(snapshot.get("ticks", [])),
|
|
)
|
|
|
|
sim = cls(config)
|
|
sim.benchmark_ticks = snapshot.get("ticks", [])
|
|
sim.ground_truth = snapshot.get("groundTruth", {})
|
|
sim.game_state = cls._parse_initial_state(snapshot.get("initialState", {}))
|
|
|
|
return sim
|
|
|
|
@staticmethod
|
|
def _parse_initial_state(state_dict: dict) -> GameState:
|
|
"""Parse initial state from snapshot"""
|
|
return GameState(
|
|
tick=0,
|
|
time=state_dict.get("currentTime", int(time.time() * 1000)),
|
|
markets=state_dict.get("predictionMarkets", []),
|
|
perpetuals=state_dict.get("perpetualMarkets", []),
|
|
news=state_dict.get("news", []),
|
|
posts=state_dict.get("socialFeed", []),
|
|
)
|
|
|
|
async def initialize(self) -> None:
|
|
"""Initialize simulator"""
|
|
if self.config.database_url and self.config.save_to_db:
|
|
self.db_pool = await asyncpg.create_pool(
|
|
self.config.database_url,
|
|
min_size=2,
|
|
max_size=10,
|
|
)
|
|
logger.info("Database connection pool created")
|
|
|
|
self.metrics.start_time = time.time()
|
|
self.current_tick = 0
|
|
self.agent_trajectories = {}
|
|
|
|
logger.info(
|
|
f"Simulator initialized: mode={self.config.mode}, max_ticks={self.config.max_ticks}"
|
|
)
|
|
|
|
async def cleanup(self) -> None:
|
|
"""Clean up resources"""
|
|
if self.db_pool:
|
|
await self.db_pool.close()
|
|
self.db_pool = None
|
|
|
|
def is_complete(self) -> bool:
|
|
"""Check if simulation is complete"""
|
|
if self.config.mode == "benchmark":
|
|
return self.current_tick >= len(self.benchmark_ticks)
|
|
return self.current_tick >= self.config.max_ticks
|
|
|
|
def get_observation(self) -> dict:
|
|
"""Get current observation for agents"""
|
|
return self.game_state.to_observation()
|
|
|
|
def get_env_state(self, agent_id: str) -> EnvironmentState:
|
|
"""Get environment state for an agent"""
|
|
return self.game_state.get_env_state(agent_id)
|
|
|
|
async def run_tick(
|
|
self,
|
|
agent_runners: dict[str, AgentRunner],
|
|
) -> dict[str, AgentTickData]:
|
|
"""
|
|
Run one simulation tick for all agents in parallel.
|
|
|
|
This is the core fast path - uses asyncio.gather for true parallel execution.
|
|
"""
|
|
# Get observation once (shared across agents)
|
|
observation = self.get_observation()
|
|
|
|
# Create coroutines for all agents
|
|
agent_ids = list(agent_runners.keys())
|
|
coros = []
|
|
for agent_id in agent_ids:
|
|
runner = agent_runners[agent_id]
|
|
env_state = self.get_env_state(agent_id)
|
|
coros.append(runner.run_tick(agent_id, observation, env_state))
|
|
|
|
# Run ALL agents truly in parallel
|
|
tick_results = await asyncio.gather(*coros, return_exceptions=True)
|
|
|
|
# Process results
|
|
results = {}
|
|
current_time = int(time.time() * 1000)
|
|
|
|
for agent_id, tick_data in zip(agent_ids, tick_results, strict=False):
|
|
# Handle exceptions from individual agents
|
|
if isinstance(tick_data, Exception):
|
|
logger.error(f"Agent {agent_id} tick failed: {tick_data}")
|
|
continue
|
|
|
|
tick_data.tick_number = self.current_tick
|
|
tick_data.timestamp = current_time
|
|
results[agent_id] = tick_data
|
|
|
|
# Track in trajectory
|
|
if agent_id not in self.agent_trajectories:
|
|
self.agent_trajectories[agent_id] = []
|
|
self.agent_trajectories[agent_id].append(tick_data)
|
|
|
|
# Update metrics
|
|
self.metrics.total_llm_calls += len(tick_data.llm_calls)
|
|
if tick_data.action:
|
|
self.metrics.total_actions += 1
|
|
|
|
# Apply actions to game state
|
|
await self._apply_actions(results)
|
|
|
|
# Update state for next tick
|
|
self._advance_tick()
|
|
|
|
# Track timing
|
|
self.metrics.total_ticks += 1
|
|
|
|
return results
|
|
|
|
async def _apply_actions(self, tick_results: dict[str, AgentTickData]) -> None:
|
|
"""Apply agent actions to game state"""
|
|
for agent_id, tick_data in tick_results.items():
|
|
if not tick_data.action:
|
|
continue
|
|
|
|
action = tick_data.action
|
|
portfolio = self.game_state.portfolios.get(
|
|
agent_id,
|
|
{
|
|
"balance": 10000.0,
|
|
"pnl": 0.0,
|
|
"positions": 0,
|
|
},
|
|
)
|
|
|
|
# Process action (simplified)
|
|
if action.action_type in ["buy", "buy_prediction", "open_perp"]:
|
|
portfolio["positions"] += 1
|
|
# Simulate some cost
|
|
cost = action.parameters.get("amount", 100)
|
|
portfolio["balance"] -= cost
|
|
|
|
elif action.action_type in ["sell", "sell_prediction", "close_perp"]:
|
|
portfolio["positions"] = max(0, portfolio["positions"] - 1)
|
|
# Simulate some profit/loss
|
|
pnl = action.parameters.get("pnl", 0)
|
|
portfolio["pnl"] += pnl
|
|
portfolio["balance"] += pnl
|
|
|
|
self.game_state.portfolios[agent_id] = portfolio
|
|
# Don't override agent's success flag - agent determines trade success
|
|
|
|
def _advance_tick(self) -> None:
|
|
"""Advance to next tick"""
|
|
self.current_tick += 1
|
|
|
|
if self.config.mode == "benchmark" and self.current_tick < len(self.benchmark_ticks):
|
|
# Load next tick's state from benchmark
|
|
tick_data = self.benchmark_ticks[self.current_tick]
|
|
state = tick_data.get("state", {})
|
|
self.game_state.tick = self.current_tick
|
|
self.game_state.time = state.get("currentTime", self.game_state.time + 1000)
|
|
self.game_state.markets = state.get("predictionMarkets", self.game_state.markets)
|
|
self.game_state.perpetuals = state.get("perpetualMarkets", self.game_state.perpetuals)
|
|
self.game_state.news = state.get("news", self.game_state.news)
|
|
self.game_state.posts = state.get("socialFeed", self.game_state.posts)
|
|
else:
|
|
# Increment time
|
|
self.game_state.tick = self.current_tick
|
|
self.game_state.time += 1000 # 1 second per tick
|
|
|
|
async def run_benchmark(
|
|
self,
|
|
agent_runners: dict[str, AgentRunner],
|
|
progress_callback: Callable[[int, int], None] | None = None,
|
|
) -> dict[str, RolloutResult]:
|
|
"""
|
|
Run complete benchmark for all agents.
|
|
|
|
Args:
|
|
agent_runners: Dict of agent_id -> AgentRunner
|
|
progress_callback: Optional callback (current_tick, total_ticks)
|
|
|
|
Returns:
|
|
Dict of agent_id -> RolloutResult
|
|
"""
|
|
await self.initialize()
|
|
|
|
self.metrics.total_agents = len(agent_runners)
|
|
total_ticks = len(self.benchmark_ticks)
|
|
|
|
logger.info(f"Starting benchmark: {len(agent_runners)} agents, {total_ticks} ticks")
|
|
|
|
# Run through all ticks
|
|
while not self.is_complete():
|
|
await self.run_tick(agent_runners)
|
|
|
|
if progress_callback and self.current_tick % 10 == 0:
|
|
progress_callback(self.current_tick, total_ticks)
|
|
|
|
# Calculate results
|
|
results = {}
|
|
for agent_id, ticks in self.agent_trajectories.items():
|
|
result = self._calculate_benchmark_result(agent_id, ticks)
|
|
results[agent_id] = result
|
|
|
|
self.metrics.end_time = time.time()
|
|
self.metrics.finalize()
|
|
|
|
# Log summary
|
|
logger.info(
|
|
f"Benchmark complete: {self.metrics.total_ticks} ticks "
|
|
f"at {self.metrics.ticks_per_second:.1f} ticks/s"
|
|
)
|
|
|
|
await self.cleanup()
|
|
|
|
return results
|
|
|
|
async def generate_data(
|
|
self,
|
|
agent_runners: dict[str, AgentRunner],
|
|
num_ticks: int | None = None,
|
|
) -> list[FeedTrajectory]:
|
|
"""
|
|
Generate training data by running agents.
|
|
|
|
Args:
|
|
agent_runners: Dict of agent_id -> AgentRunner
|
|
num_ticks: Number of ticks to run (default: config.max_ticks)
|
|
|
|
Returns:
|
|
List of trajectories
|
|
"""
|
|
await self.initialize()
|
|
|
|
self.metrics.total_agents = len(agent_runners)
|
|
max_ticks = num_ticks or self.config.max_ticks
|
|
|
|
logger.info(f"Starting data generation: {len(agent_runners)} agents, {max_ticks} ticks")
|
|
|
|
# Run through ticks
|
|
while self.current_tick < max_ticks:
|
|
await self.run_tick(agent_runners)
|
|
|
|
if self.current_tick % 100 == 0:
|
|
logger.info(f"Progress: {self.current_tick}/{max_ticks} ticks")
|
|
|
|
# Build trajectories
|
|
trajectories = []
|
|
for agent_id, ticks in self.agent_trajectories.items():
|
|
trajectory_id = f"traj-{agent_id}-{int(time.time() * 1000)}"
|
|
trajectory = build_trajectory_from_ticks(
|
|
trajectory_id=trajectory_id,
|
|
agent_id=agent_id,
|
|
ticks=ticks,
|
|
min_steps=self.config.min_actions_per_trajectory,
|
|
)
|
|
if trajectory:
|
|
trajectories.append(trajectory)
|
|
self.metrics.successful_trajectories += 1
|
|
else:
|
|
self.metrics.failed_trajectories += 1
|
|
|
|
# Save to database if configured
|
|
if self.config.save_to_db and self.db_pool:
|
|
await self._save_trajectories(trajectories)
|
|
|
|
self.metrics.end_time = time.time()
|
|
self.metrics.finalize()
|
|
|
|
logger.info(
|
|
f"Data generation complete: {len(trajectories)} trajectories, "
|
|
f"{self.metrics.ticks_per_second:.1f} ticks/s"
|
|
)
|
|
|
|
await self.cleanup()
|
|
|
|
return trajectories
|
|
|
|
def _calculate_benchmark_result(
|
|
self,
|
|
agent_id: str,
|
|
ticks: list[AgentTickData],
|
|
) -> RolloutResult:
|
|
"""Calculate benchmark result for an agent"""
|
|
if not ticks:
|
|
return RolloutResult(
|
|
agent_id=agent_id,
|
|
trajectory_id=f"bench-{agent_id}",
|
|
ticks_completed=0,
|
|
total_duration_ms=0,
|
|
avg_tick_duration_ms=0,
|
|
total_llm_calls=0,
|
|
total_reward=0,
|
|
final_pnl=0,
|
|
quality_score=0,
|
|
)
|
|
|
|
total_llm_calls = sum(len(t.llm_calls) for t in ticks)
|
|
total_reward = sum(t.reward for t in ticks)
|
|
final_pnl = ticks[-1].environment_state.agent_pnl
|
|
|
|
# Calculate quality score
|
|
quality_score = calculate_trajectory_quality_score(ticks)
|
|
|
|
# Calculate accuracy against ground truth
|
|
if self.ground_truth:
|
|
self._evaluate_against_ground_truth(agent_id, ticks)
|
|
|
|
duration = (ticks[-1].timestamp - ticks[0].timestamp) if len(ticks) > 1 else 0
|
|
trajectory_id = f"bench-{agent_id}-{int(time.time())}"
|
|
|
|
return RolloutResult(
|
|
agent_id=agent_id,
|
|
trajectory_id=trajectory_id,
|
|
ticks_completed=len(ticks),
|
|
total_duration_ms=duration,
|
|
avg_tick_duration_ms=duration / len(ticks) if ticks else 0,
|
|
total_llm_calls=total_llm_calls,
|
|
total_reward=total_reward,
|
|
final_pnl=final_pnl,
|
|
quality_score=quality_score,
|
|
trajectory=build_trajectory_from_ticks(
|
|
trajectory_id=trajectory_id,
|
|
agent_id=agent_id,
|
|
ticks=ticks,
|
|
min_steps=self.config.min_actions_per_trajectory,
|
|
),
|
|
)
|
|
|
|
def _evaluate_against_ground_truth(
|
|
self,
|
|
agent_id: str,
|
|
ticks: list[AgentTickData],
|
|
) -> None:
|
|
"""Evaluate agent actions against ground truth"""
|
|
market_outcomes = self.ground_truth.get("marketOutcomes", {})
|
|
|
|
correct_predictions = 0
|
|
total_predictions = 0
|
|
|
|
for tick in ticks:
|
|
if not tick.action:
|
|
continue
|
|
|
|
if tick.action.action_type == "buy_prediction":
|
|
market_id = tick.action.parameters.get("marketId")
|
|
predicted = tick.action.parameters.get("outcome") == "YES"
|
|
|
|
if market_id in market_outcomes:
|
|
actual = market_outcomes[market_id]
|
|
if predicted == actual:
|
|
correct_predictions += 1
|
|
total_predictions += 1
|
|
|
|
if total_predictions > 0:
|
|
accuracy = correct_predictions / total_predictions
|
|
self.metrics.avg_accuracy = (
|
|
self.metrics.avg_accuracy * (self.metrics.total_agents - 1) + accuracy
|
|
) / self.metrics.total_agents
|
|
|
|
async def _save_trajectories(self, trajectories: list[FeedTrajectory]) -> None:
|
|
"""Save trajectories to database"""
|
|
if not self.db_pool:
|
|
return
|
|
|
|
async with self.db_pool.acquire() as conn:
|
|
for traj in trajectories:
|
|
steps_json = json.dumps(
|
|
[
|
|
{
|
|
"stepNumber": s.step_number,
|
|
"timestamp": s.timestamp,
|
|
"environmentState": {
|
|
"agentBalance": s.environment_state.agent_balance,
|
|
"agentPnL": s.environment_state.agent_pnl,
|
|
"openPositions": s.environment_state.open_positions,
|
|
},
|
|
"llmCalls": [
|
|
{
|
|
"model": c.model,
|
|
"systemPrompt": c.system_prompt,
|
|
"userPrompt": c.user_prompt,
|
|
"response": c.response,
|
|
"reasoning": c.reasoning,
|
|
"temperature": c.temperature,
|
|
"maxTokens": c.max_tokens,
|
|
"purpose": c.purpose,
|
|
}
|
|
for c in s.llm_calls
|
|
],
|
|
"action": {
|
|
"actionType": s.action.action_type,
|
|
"parameters": s.action.parameters,
|
|
"success": s.action.success,
|
|
"reasoning": s.action.reasoning,
|
|
},
|
|
"reward": s.reward,
|
|
}
|
|
for s in traj.steps
|
|
]
|
|
)
|
|
|
|
await conn.execute(
|
|
"""
|
|
INSERT INTO trajectories (
|
|
id, "trajectoryId", "agentId", "windowId",
|
|
"startTime", "endTime", "durationMs",
|
|
"stepsJson", "totalReward", "finalPnL", "finalBalance",
|
|
"tradesExecuted", "episodeLength", "finalStatus",
|
|
"isTrainingData", "createdAt", "updatedAt"
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17
|
|
)
|
|
""",
|
|
traj.id,
|
|
traj.trajectory_id,
|
|
traj.agent_id,
|
|
traj.window_id,
|
|
traj.start_time,
|
|
traj.end_time,
|
|
traj.duration_ms,
|
|
steps_json,
|
|
traj.total_reward,
|
|
traj.final_pnl,
|
|
traj.final_balance,
|
|
traj.trades_executed,
|
|
traj.episode_length,
|
|
traj.final_status,
|
|
True, # isTrainingData
|
|
datetime.now(timezone.utc),
|
|
datetime.now(timezone.utc),
|
|
)
|
|
|
|
logger.info(f"Saved {len(trajectories)} trajectories to database")
|
|
|
|
def get_metrics(self) -> SimulatorMetrics:
|
|
"""Get current metrics"""
|
|
return self.metrics
|
|
|
|
def print_metrics(self) -> None:
|
|
"""Print metrics summary"""
|
|
m = self.metrics
|
|
print("\n" + "=" * 60)
|
|
print(" SIMULATOR METRICS")
|
|
print("=" * 60)
|
|
print(f" Mode: {self.config.mode}")
|
|
print(f" Total ticks: {m.total_ticks}")
|
|
print(f" Total agents: {m.total_agents}")
|
|
print(f" Ticks/second: {m.ticks_per_second:.1f}")
|
|
print(f" Avg tick duration: {m.avg_tick_duration_ms:.2f}ms")
|
|
print(f" Total LLM calls: {m.total_llm_calls}")
|
|
print(f" Total actions: {m.total_actions}")
|
|
if m.successful_trajectories or m.failed_trajectories:
|
|
print(f" Successful trajectories: {m.successful_trajectories}")
|
|
print(f" Failed trajectories: {m.failed_trajectories}")
|
|
if m.avg_accuracy > 0:
|
|
print(f" Average accuracy: {m.avg_accuracy:.2%}")
|
|
print("=" * 60 + "\n")
|