
Trade Simulator
- 1 installs
- 1 repo stars
- Updated July 29, 2026
- starchild-ai-agent/community-skills
Run multi-agent market scenario simulations with LLM-driven whale/market-maker/retail participants, cascade analysis, and post-sim interviews.
About
A skill that runs multi-agent market scenario analysis using the MiroFish swarm-intelligence architecture, simulating whale/market-maker/retail participants with LLM reasoning, cascade analysis, and post-simulation interviews. A developer or trader uses it to stress-test market scenarios behaviorally rather than with a spreadsheet.
- Multi-agent market simulation on MiroFish swarm architecture
- LLM-driven participant reasoning, cascade analysis, post-sim interviews
Trade Simulator by the numbers
- 1 all-time installs (skills.sh)
- Ranked #909 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/starchild-ai-agent/community-skills --skill trade-simulatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 29, 2026 |
| Repository | starchild-ai-agent/community-skills ↗ |
What it does
Run multi-agent market scenario simulations with LLM-driven whale/market-maker/retail participants, cascade analysis, and post-sim interviews.
Files
🐟 Trade Simulator (MiroFish Architecture)
Multi-agent scenario analysis for traders. Not a spreadsheet — a behavioral simulation.
Built on MiroFish's swarm intelligence architecture, adapted from social simulation to market simulation.
MiroFish Integration
This skill implements MiroFish's 5-stage prediction pipeline, replacing social media environments with financial markets:
| MiroFish Stage | Original (Social) | Our Adaptation (Markets) |
|---|---|---|
| 1. Graph Construction | Zep knowledge graph from news/docs | Market State Graph from live Coinglass/HL data |
| 2. Environment Setup | Twitter/Reddit agent profiles | Market participant profiles (Whale, MM, Retail, etc.) |
| 3. Simulation | OASIS dual-platform social interaction | Round-based market interaction with LLM reasoning |
| 4. Report Generation | ReACT report with Zep tools | ReACT report with market data tools |
| 5. Deep Interaction | Interview any social agent | Interview any market participant |
Key MiroFish Patterns Used
1. LLM-driven agent reasoning (from oasis_profile_generator.py) — agents don't use if/else rules. Each agent has a persona prompt and "thinks" each round via LLM call 2. Simulation config auto-generation (from simulation_config_generator.py) — describe scenario in natural language, LLM generates agent roster, parameters, event timeline, activity patterns 3. ReACT report generation (from report_agent.py) — multi-step reasoning with tool use: plan outline → generate sections → cite evidence → synthesize predictions 4. Post-simulation interviews (from zep_tools.py Interview system) — chat with any agent after simulation to understand their reasoning 5. Knowledge graph backbone — entities, relationships, and facts structured for agent retrieval (we use in-memory graph instead of Zep Cloud)
What We Don't Use
- ❌ OASIS / camel-ai (social media simulation runtime — irrelevant to markets)
- ❌ Zep Cloud (replaced with local in-memory knowledge graph)
- ❌ Flask frontend (we output to agent conversation)
- ❌ Twitter/Reddit environments (replaced with market environment)
Architecture
skills/trade-simulator/
├── SKILL.md # This file
└── scripts/
├── mirofish_engine.py # Core engine — 5-stage pipeline
├── market_graph.py # Stage 1: Market state graph builder
├── profile_generator.py # Stage 2: LLM agent profile generation
├── simulation_runner.py # Stage 3: Round-based market simulation
├── report_agent.py # Stage 4: ReACT report generation
└── interview.py # Stage 5: Post-sim agent interviewsUsage
Quick Scenario Analysis
Agent: "Run a trade simulation: What happens to my BTC short if ETF inflows spike 500%?"The engine will: 1. Build market state graph from live data (OI, funding, liquidations, whale positions) 2. Auto-generate 5-8 market participant agents calibrated to current conditions 3. Run 6-round simulation where each agent LLM-reasons about their actions 4. Generate ReACT analysis report with turning points, cascade analysis, recommendations 5. Offer interactive interviews with any simulated agent
Supported Scenarios
- Directional shocks: "What if BTC pumps/dumps 10-20%?"
- Catalyst events: "What if ETF inflows spike?" / "What if Tether depegs?"
- Market structure: "What if funding goes extreme?" / "What if OI doubles?"
- Portfolio stress: "How does my portfolio react to a black swan?"
Interview Mode
After any simulation:
Agent: "Interview the whale agent — why did they cover at round 4?"
Agent: "Ask the market maker about their liquidity decision"Data Sources (Live)
| Data | Tool | What It Feeds |
|---|---|---|
| Open Interest | cg_open_interest() | Market leverage state |
| Funding Rates | funding_rate() | Positioning sentiment |
| Liquidation Levels | cg_liquidations() | Cascade trigger points |
| Whale Positions | cg_hyperliquid_whale_positions() | Whale agent calibration |
| Long/Short Ratios | long_short_ratio() | Crowd positioning |
| Orderbook Depth | hl_orderbook() | MM agent calibration |
| ETF Flows | cg_btc_etf_flows() | Institutional flow context |
| Price/OHLC | cg_ohlc_history() | Price context |
| Social Sentiment | lunar_coin() | Retail agent behavior |
Workflow
1. Collect live market data using tools above 2. Run simulation: python3 skills/trade-simulator/scripts/mirofish_engine.py
- Pass market data + scenario + user positions as JSON
- Engine runs all 5 MiroFish stages
- Returns structured results (agent actions, report, interview-ready state)
3. Present results with key insights, PnL impact, risk warnings 4. Offer interviews — user can interrogate any agent
"""
Post-Simulation Interview System — MiroFish Stage 5 Adaptation
MiroFish: zep_tools.py InterviewResult + simulation_runner.py interview mode
- After simulation completes, environment stays alive
- User can interview any agent by ID
- Agent responds from their accumulated memory + persona
- Supports both single and batch interviews
Ours:
- After simulation, agent profiles + memory are preserved
- User can ask any agent about their decisions
- Agent responds using their persona + full round memory
- Supports follow-up questions
"""
import json
from typing import Dict, Any, List, Optional
from dataclasses import dataclass
from .profile_generator import MarketAgentProfile
@dataclass
class InterviewResult:
"""MiroFish equivalent: InterviewResult dataclass"""
agent_name: str
agent_emoji: str
question: str
response: str
confidence: float = 0.5
def to_dict(self):
return {
"agent": f"{self.agent_emoji} {self.agent_name}",
"question": self.question,
"response": self.response,
"confidence": self.confidence,
}
class InterviewSystem:
"""
Post-simulation interview system.
MiroFish pattern:
- Simulation completes but environment persists
- IPC commands send interview requests to running simulation
- Agent reconstructs context from Zep memory and responds
Our adaptation:
- Simulation results + agent profiles kept in memory
- LLM generates responses from agent persona + accumulated memory
- No IPC needed (we're in the same process)
"""
def __init__(self, agents: List[MarketAgentProfile],
simulation_result=None, llm_client=None):
self.agents = {a.agent_id: a for a in agents}
self.agents_by_name = {}
for a in agents:
self.agents_by_name[a.name.lower()] = a
self.agents_by_name[a.archetype.lower()] = a
self.agents_by_name[a.emoji] = a
self.simulation_result = simulation_result
self.llm_client = llm_client
self.conversation_history: Dict[int, List[Dict]] = {}
def list_agents(self) -> List[Dict]:
"""List all agents available for interview."""
return [{"id": a.agent_id, "name": f"{a.emoji} {a.name}",
"archetype": a.archetype, "position": a.position_side,
"rounds_memory": len(a.memory)}
for a in self.agents.values()]
def find_agent(self, query: str) -> Optional[MarketAgentProfile]:
"""Find agent by name, archetype, emoji, or ID."""
q = query.lower().strip()
# Try exact match
if q in self.agents_by_name:
return self.agents_by_name[q]
# Try substring
for key, agent in self.agents_by_name.items():
if q in key:
return agent
# Try ID
try:
return self.agents.get(int(q))
except (ValueError, TypeError):
pass
return None
def interview(self, agent_query: str, question: str) -> InterviewResult:
"""
Interview a simulated agent.
MiroFish: sends IPC CommandType.INTERVIEW to running simulation
Ours: reconstructs agent context and generates LLM response
"""
agent = self.find_agent(agent_query)
if not agent:
return InterviewResult(
agent_name="System", agent_emoji="❌",
question=question,
response=f"Agent '{agent_query}' not found. Available: {', '.join(a.emoji + ' ' + a.name for a in self.agents.values())}",
)
if self.llm_client:
return self._llm_interview(agent, question)
else:
return self._memory_interview(agent, question)
def _llm_interview(self, agent: MarketAgentProfile, question: str) -> InterviewResult:
"""LLM-powered interview (MiroFish approach)."""
# Build conversation history
history = self.conversation_history.get(agent.agent_id, [])
system_prompt = f"""You are {agent.emoji} {agent.name}, being interviewed after a market simulation.
YOUR PERSONA: {agent.persona}
YOUR POSITION: {agent.position_side} ${agent.position_size_usd:,.0f}
YOUR TRADING STYLE: {agent.trading_style}
YOUR MEMORY OF THE SIMULATION:
{chr(10).join(agent.memory) if agent.memory else 'No simulation memory.'}
Answer as this character would — in first person, with their personality and biases.
Be specific about your reasoning during the simulation.
If asked about a decision, explain your thinking process from that round."""
messages = [{"role": "system", "content": system_prompt}]
messages.extend(history)
messages.append({"role": "user", "content": question})
try:
response = self.llm_client.chat(messages=messages, temperature=0.7)
# Update conversation history
history.append({"role": "user", "content": question})
history.append({"role": "assistant", "content": response})
self.conversation_history[agent.agent_id] = history
return InterviewResult(
agent_name=agent.name, agent_emoji=agent.emoji,
question=question, response=response, confidence=0.7,
)
except Exception as e:
return self._memory_interview(agent, question)
def _memory_interview(self, agent: MarketAgentProfile, question: str) -> InterviewResult:
"""Fallback: answer from memory without LLM."""
q = question.lower()
relevant_memories = []
for mem in agent.memory:
if any(kw in q for kw in ["why", "decision", "round", "action", "think"]):
relevant_memories.append(mem)
if not relevant_memories:
relevant_memories = agent.memory[-3:] if agent.memory else ["No simulation memory available."]
response = f"[{agent.emoji} {agent.name} — {agent.archetype}]\n\n"
response += f"My position: {agent.position_side} ${agent.position_size_usd:,.0f}\n\n"
response += "My recollection:\n"
for mem in relevant_memories:
response += f" - {mem}\n"
return InterviewResult(
agent_name=agent.name, agent_emoji=agent.emoji,
question=question, response=response, confidence=0.3,
)
def batch_interview(self, question: str) -> List[InterviewResult]:
"""
Interview ALL agents with the same question.
MiroFish: batch interview mode via IPC.
"""
return [self.interview(str(agent_id), question) for agent_id in self.agents]
"""
Market State Graph — MiroFish Stage 1 Adaptation
MiroFish uses Zep Cloud to build a knowledge graph from seed documents.
We build an equivalent in-memory graph from live market data.
Original: graph_builder.py -> Zep EpisodeData -> nodes/edges
Ours: market data tools -> MarketNode/MarketEdge -> MarketGraph
"""
from dataclasses import dataclass, field
from typing import Dict, Any, List, Optional
from datetime import datetime
import json
@dataclass
class MarketNode:
uuid: str
name: str
node_type: str # asset, exchange, whale, funding_state, etc.
attributes: Dict[str, Any] = field(default_factory=dict)
summary: str = ""
def to_dict(self):
return {"uuid": self.uuid, "name": self.name, "type": self.node_type,
"attributes": self.attributes, "summary": self.summary}
@dataclass
class MarketEdge:
source_uuid: str
target_uuid: str
relation: str
attributes: Dict[str, Any] = field(default_factory=dict)
def to_dict(self):
return {"source": self.source_uuid, "target": self.target_uuid,
"relation": self.relation, "attributes": self.attributes}
class MarketGraph:
"""In-memory knowledge graph replacing MiroFish's Zep Cloud dependency."""
def __init__(self):
self.nodes: Dict[str, MarketNode] = {}
self.edges: List[MarketEdge] = []
self.built_at: Optional[str] = None
def add_node(self, node: MarketNode):
self.nodes[node.uuid] = node
def add_edge(self, edge: MarketEdge):
self.edges.append(edge)
def get_nodes_by_type(self, node_type: str) -> List[MarketNode]:
return [n for n in self.nodes.values() if n.node_type == node_type]
def get_edges_for_node(self, uuid: str) -> List[MarketEdge]:
return [e for e in self.edges if e.source_uuid == uuid or e.target_uuid == uuid]
def query(self, query: str) -> str:
"""Simple text search. MiroFish equivalent: ZepToolsService.quick_search()"""
results = []
q = query.lower()
for node in self.nodes.values():
if q in node.summary.lower() or q in node.name.lower():
results.append(f"[{node.node_type}] {node.name}: {node.summary}")
return "\n".join(results[:20]) if results else f"No results for '{query}'"
def get_full_context(self) -> str:
"""Complete graph as readable text for LLM context."""
parts = [f"=== MARKET STATE GRAPH (built {self.built_at}) ===\n"]
by_type: Dict[str, List[MarketNode]] = {}
for node in self.nodes.values():
by_type.setdefault(node.node_type, []).append(node)
for ntype, nodes in by_type.items():
parts.append(f"\n--- {ntype.upper()} ---")
for node in nodes:
parts.append(f" {node.name}: {node.summary}")
for k, v in node.attributes.items():
parts.append(f" {k}: {v}")
parts.append(f"\n--- RELATIONSHIPS ({len(self.edges)}) ---")
for edge in self.edges:
src = self.nodes.get(edge.source_uuid, MarketNode("?","?","?"))
tgt = self.nodes.get(edge.target_uuid, MarketNode("?","?","?"))
parts.append(f" {src.name} --[{edge.relation}]--> {tgt.name}")
return "\n".join(parts)
def to_dict(self):
return {"built_at": self.built_at, "node_count": len(self.nodes),
"edge_count": len(self.edges),
"nodes": [n.to_dict() for n in self.nodes.values()],
"edges": [e.to_dict() for e in self.edges]}
def build_market_graph(market_data: Dict[str, Any]) -> MarketGraph:
"""
Build market state graph from collected data.
MiroFish equivalent: GraphBuilderService.build_graph_async()
"""
graph = MarketGraph()
graph.built_at = datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")
symbol = market_data.get("symbol", "BTC")
price = market_data.get("price", 0)
# Asset node
asset_id = f"asset_{symbol}"
graph.add_node(MarketNode(uuid=asset_id, name=symbol, node_type="asset",
summary=f"{symbol} at ${price:,.0f}",
attributes={"price": price, "change_24h": market_data.get("price_change_24h", "?")}))
# Open Interest
oi = market_data.get("oi", {})
if oi:
oi_id = f"oi_{symbol}"
total = oi.get("total", 0)
graph.add_node(MarketNode(uuid=oi_id, name=f"{symbol} OI", node_type="open_interest",
summary=f"Total OI: ${total/1e9:.1f}B" if total > 1e9 else f"Total OI: ${total/1e6:.0f}M",
attributes=oi))
graph.add_edge(MarketEdge(asset_id, oi_id, "has_open_interest"))
# Funding
funding = market_data.get("funding", {})
if funding:
fund_id = f"funding_{symbol}"
rate = funding.get("rate", 0)
sent = "bullish" if rate > 0.005 else "bearish" if rate < -0.005 else "neutral"
graph.add_node(MarketNode(uuid=fund_id, name=f"{symbol} Funding", node_type="funding_state",
summary=f"Rate: {rate*100:.4f}% ({sent})", attributes=funding))
graph.add_edge(MarketEdge(asset_id, fund_id, "has_funding_state"))
# Liquidations
liqs = market_data.get("liquidations", {})
if liqs:
liq_id = f"liqs_{symbol}"
long_l = liqs.get("long_liquidations", 0)
short_l = liqs.get("short_liquidations", 0)
dom = "long-dominant" if long_l > short_l else "short-dominant"
graph.add_node(MarketNode(uuid=liq_id, name=f"{symbol} Liquidations", node_type="liquidation_state",
summary=f"24h: ${(long_l+short_l)/1e6:.1f}M ({dom})",
attributes={"long_usd": long_l, "short_usd": short_l}))
graph.add_edge(MarketEdge(asset_id, liq_id, "has_liquidation_state"))
# Long/Short
ls = market_data.get("long_short", {})
if ls:
ls_id = f"ls_{symbol}"
ratio = ls.get("ratio", 1.0)
crowd = "net long" if ratio > 1.1 else "net short" if ratio < 0.9 else "balanced"
graph.add_node(MarketNode(uuid=ls_id, name=f"{symbol} L/S", node_type="positioning",
summary=f"Ratio: {ratio:.2f} ({crowd})", attributes=ls))
graph.add_edge(MarketEdge(asset_id, ls_id, "has_positioning"))
# Whales (top 5)
for i, w in enumerate(market_data.get("whales", [])[:5]):
w_id = f"whale_{i}"
side = w.get("side", "?")
sz = w.get("size_usd", 0)
pnl = w.get("unrealized_pnl", 0)
graph.add_node(MarketNode(uuid=w_id, name=f"Whale#{i+1} ({side})", node_type="whale",
summary=f"${sz/1e6:.1f}M {side}, PnL: ${pnl:,.0f}", attributes=w))
graph.add_edge(MarketEdge(w_id, asset_id, f"holds_{side}"))
if f"liqs_{symbol}" in graph.nodes:
graph.add_edge(MarketEdge(w_id, f"liqs_{symbol}", "could_cascade_if_liquidated"))
# ETF flows
etf = market_data.get("etf_flows", {})
if etf:
etf_id = f"etf_{symbol}"
nf = etf.get("net_flow", 0)
d = "inflow" if nf > 0 else "outflow"
graph.add_node(MarketNode(uuid=etf_id, name=f"{symbol} ETF", node_type="etf",
summary=f"Net: ${nf/1e6:.1f}M ({d})", attributes=etf))
graph.add_edge(MarketEdge(etf_id, asset_id, "institutional_flow"))
# User positions
for i, pos in enumerate(market_data.get("user_positions", [])):
pos_id = f"user_pos_{i}"
side = pos.get("side", "?")
graph.add_node(MarketNode(uuid=pos_id, name=f"Your {pos.get('symbol',symbol)} {side}",
node_type="user_position",
summary=f"{side} {pos.get('size',0)} @ ${pos.get('entry_price',0):,.0f}, liq ${pos.get('liquidation_price',0):,.0f}",
attributes=pos))
graph.add_edge(MarketEdge(pos_id, asset_id, "user_exposure"))
return graph
"""
MiroFish Market Simulation Engine — All 5 Stages
Orchestrates the complete MiroFish pipeline adapted for trading:
1. Graph Construction (market_graph.py)
2. Profile Generation (profile_generator.py)
3. Simulation Run (simulation_runner.py)
4. Report Generation (report_agent.py)
5. Interview System (interview.py)
Usage:
echo '{"scenario": "BTC pumps 10%", "symbol": "BTC", ...}' | \
python3 -m skills.trade-simulator.scripts.mirofish_engine
OR import and use programmatically
"""
import os
import sys
import json
import re
import time
from typing import Dict, Any, List, Optional
# Add workspace to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from skills.trade_simulator.scripts.market_graph import MarketGraph, build_market_graph
from skills.trade_simulator.scripts.profile_generator import MarketAgentProfile, generate_profiles_from_graph
from skills.trade_simulator.scripts.simulation_runner import MarketSimulation, SimulationResult
from skills.trade_simulator.scripts.report_agent import ReportAgent, SimulationReport
from skills.trade_simulator.scripts.interview import InterviewSystem
class LLMClient:
"""
Minimal LLM client matching MiroFish's LLMClient interface.
Uses OpenAI-compatible API (same as MiroFish's backend/app/utils/llm_client.py).
"""
def __init__(self, api_key: str = None, base_url: str = None, model: str = None):
from openai import OpenAI
import httpx
self.api_key = api_key or os.environ.get("LLM_API_KEY") or os.environ.get("OPENROUTER_API_KEY", "")
self.base_url = base_url or os.environ.get("LLM_BASE_URL") or "https://openrouter.ai/api/v1"
self.model = model or os.environ.get("LLM_MODEL_NAME") or "anthropic/claude-sonnet-4"
# Use proxy if available (MiroFish uses direct; we route through sc-proxy)
proxy_host = os.environ.get("PROXY_HOST", "")
proxy_port = os.environ.get("PROXY_PORT", "")
ca_bundle = os.environ.get("REQUESTS_CA_BUNDLE", "")
http_client = None
if proxy_host and proxy_port:
proxy_url = f"http://[{proxy_host}]:{proxy_port}"
http_client = httpx.Client(proxy=proxy_url, verify=ca_bundle if ca_bundle else True)
self.client = OpenAI(api_key=self.api_key, base_url=self.base_url, http_client=http_client)
def chat(self, messages, temperature=0.7, max_tokens=4096):
response = self.client.chat.completions.create(
model=self.model, messages=messages,
temperature=temperature, max_tokens=max_tokens)
content = response.choices[0].message.content
# MiroFish pattern: strip <think> tags from reasoning models
content = re.sub(r'<think>[\s\S]*?</think>', '', content).strip()
return content
def chat_json(self, messages, temperature=0.3, max_tokens=4096):
# Add JSON instruction
if messages:
messages = list(messages)
messages[-1] = dict(messages[-1])
messages[-1]["content"] = messages[-1]["content"] + "\n\nRespond with valid JSON only. No markdown code blocks."
response = self.chat(messages, temperature, max_tokens)
# Clean markdown fences
cleaned = response.strip()
cleaned = re.sub(r'^```(?:json)?\s*\n?', '', cleaned, flags=re.IGNORECASE)
cleaned = re.sub(r'\n?```\s*$', '', cleaned)
cleaned = cleaned.strip()
return json.loads(cleaned)
class MiroFishEngine:
"""
Complete 5-stage pipeline orchestrator.
MiroFish equivalent: The Flask app + simulation_manager.py
that coordinates graph building, profile generation, simulation,
report generation, and interviews.
"""
def __init__(self, use_llm: bool = True):
self.llm_client = None
if use_llm:
try:
self.llm_client = LLMClient()
except Exception as e:
print(f"[WARN] LLM not available: {e}. Using rule-based mode.", file=sys.stderr)
self.graph: Optional[MarketGraph] = None
self.agents: List[MarketAgentProfile] = []
self.simulation_result: Optional[SimulationResult] = None
self.report: Optional[SimulationReport] = None
self.interview_system: Optional[InterviewSystem] = None
def run_full_pipeline(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Execute all 5 MiroFish stages.
Config:
scenario: str - Natural language scenario description
symbol: str - Asset symbol (BTC, ETH, etc.)
shock_pct: float - Scenario price shock percentage
num_rounds: int - Simulation rounds (default 6)
market_data: dict - Live market data (price, oi, funding, etc.)
user_positions: list - User's current positions
events: list - Mid-simulation event injections
"""
scenario = config.get("scenario", "Unknown scenario")
symbol = config.get("symbol", "BTC")
results = {"scenario": scenario, "symbol": symbol, "stages": {}}
t0 = time.time()
# === STAGE 1: Graph Construction ===
print(f"[Stage 1/5] Building market state graph...", file=sys.stderr)
market_data = config.get("market_data", {})
market_data["symbol"] = symbol
self.graph = build_market_graph(market_data)
results["stages"]["graph"] = {
"nodes": len(self.graph.nodes),
"edges": len(self.graph.edges),
"built_at": self.graph.built_at,
}
print(f" -> {len(self.graph.nodes)} nodes, {len(self.graph.edges)} edges", file=sys.stderr)
# === STAGE 2: Profile Generation ===
print(f"[Stage 2/5] Generating agent profiles...", file=sys.stderr)
whales = market_data.get("whales", [])
graph_context = self.graph.get_full_context()
self.agents = generate_profiles_from_graph(
graph_context=graph_context,
whales=whales,
llm_client=self.llm_client
)
results["stages"]["profiles"] = [a.to_dict() for a in self.agents]
print(f" -> {len(self.agents)} agents created", file=sys.stderr)
# === STAGE 3: Simulation ===
print(f"[Stage 3/5] Running simulation ({config.get('num_rounds', 6)} rounds)...", file=sys.stderr)
sim = MarketSimulation(
graph=self.graph,
agents=self.agents,
llm_client=self.llm_client,
num_rounds=config.get("num_rounds", 6),
)
self.simulation_result = sim.run(
scenario=scenario,
initial_price=market_data.get("price", 0),
scenario_shock_pct=config.get("shock_pct", 10.0),
events=config.get("events", []),
progress_callback=lambda r, t, p: print(
f" Round {r}/{t}: ${p:,.0f}", file=sys.stderr),
)
results["stages"]["simulation"] = self.simulation_result.to_dict()
print(f" -> Final price: ${self.simulation_result.final_price:,.0f}", file=sys.stderr)
# === STAGE 4: Report Generation ===
print(f"[Stage 4/5] Generating ReACT report...", file=sys.stderr)
reporter = ReportAgent(llm_client=self.llm_client)
self.report = reporter.generate(
simulation_result=self.simulation_result,
graph=self.graph,
user_positions=config.get("user_positions", []),
)
results["stages"]["report"] = self.report.to_dict()
results["report_text"] = self.report.to_text()
print(f" -> Report: {len(self.report.sections)} sections", file=sys.stderr)
# === STAGE 5: Interview System Ready ===
print(f"[Stage 5/5] Interview system initialized.", file=sys.stderr)
self.interview_system = InterviewSystem(
agents=self.agents,
simulation_result=self.simulation_result,
llm_client=self.llm_client,
)
results["stages"]["interview"] = {
"available_agents": self.interview_system.list_agents(),
"status": "ready",
}
results["elapsed_seconds"] = time.time() - t0
results["llm_powered"] = self.llm_client is not None
print(f"\n✅ All 5 stages complete in {results['elapsed_seconds']:.1f}s", file=sys.stderr)
return results
def interview(self, agent_query: str, question: str) -> Dict:
"""Interview an agent post-simulation."""
if not self.interview_system:
return {"error": "Run simulation first"}
result = self.interview_system.interview(agent_query, question)
return result.to_dict()
def main():
"""CLI entry point — reads config from stdin or file."""
if len(sys.argv) > 1:
with open(sys.argv[1]) as f:
config = json.load(f)
else:
config = json.load(sys.stdin)
engine = MiroFishEngine(use_llm=config.get("use_llm", True))
results = engine.run_full_pipeline(config)
# Output results
print(json.dumps(results, indent=2, default=str))
if __name__ == "__main__":
main()
"""
Agent Profile Generator — MiroFish Stage 2 Adaptation
MiroFish: oasis_profile_generator.py
- Reads Zep graph entities
- LLM generates detailed persona (bio, MBTI, stance, activity patterns)
- Creates OasisAgentProfile with personality traits
Ours:
- Reads MarketGraph nodes (whales, funding state, OI, etc.)
- LLM generates market participant personas calibrated to live data
- Each agent gets behavioral parameters + reasoning prompt
"""
import json
from typing import Dict, Any, List, Optional
from dataclasses import dataclass, field
@dataclass
class MarketAgentProfile:
"""
Equivalent to MiroFish's OasisAgentProfile.
Instead of social media personality, this is a market participant persona.
"""
agent_id: int
name: str
emoji: str
archetype: str # whale, market_maker, retail, funding_arb, liquidation_engine
# LLM-generated persona (MiroFish pattern: detailed bio from graph context)
persona: str = "" # Full persona description
trading_style: str = "" # How they trade
risk_tolerance: str = "" # Conservative / moderate / aggressive
pain_point: str = "" # What would force them to act
# Calibration from live data
position_side: Optional[str] = None # long / short / neutral
position_size_usd: float = 0
entry_price: float = 0
liquidation_price: float = 0
unrealized_pnl: float = 0
# Behavioral params (MiroFish: activity_level, posts_per_hour, etc.)
aggression: float = 0.5 # 0=passive, 1=aggressive
herd_tendency: float = 0.5 # 0=contrarian, 1=follows crowd
panic_threshold: float = 0.1 # price move % that triggers panic
# Memory (MiroFish: zep long-term memory per agent)
memory: List[str] = field(default_factory=list)
def get_system_prompt(self, graph_context: str) -> str:
"""Generate the agent's system prompt for LLM reasoning."""
return f"""You are {self.emoji} {self.name}, a {self.archetype} in the crypto derivatives market.
PERSONA: {self.persona}
TRADING STYLE: {self.trading_style}
RISK TOLERANCE: {self.risk_tolerance}
PAIN POINT: {self.pain_point}
CURRENT POSITION: {self.position_side or 'flat'} ${self.position_size_usd:,.0f}
ENTRY: ${self.entry_price:,.0f} | LIQUIDATION: ${self.liquidation_price:,.0f}
UNREALIZED PnL: ${self.unrealized_pnl:,.0f}
BEHAVIORAL PARAMETERS:
- Aggression: {self.aggression:.1f}/1.0
- Herd tendency: {self.herd_tendency:.1f}/1.0
- Panic threshold: {self.panic_threshold*100:.0f}% move
MARKET CONTEXT:
{graph_context}
MEMORY OF PRIOR ROUNDS:
{chr(10).join(self.memory[-5:]) if self.memory else 'No prior rounds.'}
Each round, you must decide your action. Respond in JSON:
{{
"thinking": "your internal reasoning (2-3 sentences)",
"action": "hold | add | reduce | close | flip",
"size_change_pct": 0-100,
"reasoning_public": "what you'd say on a trading desk (1 sentence)",
"confidence": 0.0-1.0,
"market_impact": "your estimate of how your action affects the market"
}}"""
def to_dict(self):
return {
"agent_id": self.agent_id, "name": self.name, "emoji": self.emoji,
"archetype": self.archetype, "persona": self.persona,
"position_side": self.position_side,
"position_size_usd": self.position_size_usd,
"aggression": self.aggression, "herd_tendency": self.herd_tendency,
"panic_threshold": self.panic_threshold,
}
# Default archetypes — used when LLM generation is not available
DEFAULT_ARCHETYPES = [
{
"archetype": "whale",
"emoji": "🐋",
"name_template": "The {stance} Whale",
"persona_template": "A large-position holder with ${size}M at stake. {stance_desc}. Will defend position aggressively but has clear pain points.",
"aggression": 0.7, "herd_tendency": 0.2, "panic_threshold": 0.15,
},
{
"archetype": "market_maker",
"emoji": "🤖",
"name_template": "MM Desk",
"persona_template": "Systematic market maker providing liquidity. Profits from spread, not direction. Pulls quotes when volatility exceeds risk limits. Has inventory to manage.",
"aggression": 0.3, "herd_tendency": 0.1, "panic_threshold": 0.08,
},
{
"archetype": "retail",
"emoji": "🐑",
"name_template": "Retail Crowd",
"persona_template": "Aggregate retail behavior. Momentum-chasing, high leverage, panic-prone. Enters after moves, exits at worst time. Social media driven.",
"aggression": 0.6, "herd_tendency": 0.9, "panic_threshold": 0.05,
},
{
"archetype": "funding_arb",
"emoji": "📊",
"name_template": "Funding Arbitrageur",
"persona_template": "Delta-neutral trader harvesting funding rate differentials. Only enters when funding is extreme. Stabilizing force in the market.",
"aggression": 0.2, "herd_tendency": 0.0, "panic_threshold": 0.20,
},
{
"archetype": "liquidation_engine",
"emoji": "💀",
"name_template": "Liquidation Cascade Engine",
"persona_template": "Not a trader — represents the exchange liquidation mechanism. When positions breach margin, forced-closes them at market. Creates cascading selling/buying pressure.",
"aggression": 1.0, "herd_tendency": 0.0, "panic_threshold": 0.0,
},
]
def generate_profiles_from_graph(graph_context: str, whales: List[Dict] = None,
llm_client=None) -> List[MarketAgentProfile]:
"""
Generate agent profiles from market graph.
MiroFish equivalent: OasisProfileGenerator.generate_profiles()
- Reads entity nodes from Zep graph
- LLM enriches each entity into a detailed persona
- Returns list of OasisAgentProfile
If llm_client is provided, uses LLM to generate rich personas (MiroFish approach).
Otherwise falls back to template-based generation.
"""
profiles = []
whales = whales or []
for i, archetype in enumerate(DEFAULT_ARCHETYPES):
profile = MarketAgentProfile(
agent_id=i,
name=archetype["name_template"],
emoji=archetype["emoji"],
archetype=archetype["archetype"],
persona=archetype["persona_template"],
aggression=archetype["aggression"],
herd_tendency=archetype["herd_tendency"],
panic_threshold=archetype["panic_threshold"],
)
# Calibrate whale from live data
if archetype["archetype"] == "whale" and whales:
biggest = max(whales, key=lambda w: abs(w.get("size_usd", 0)))
profile.position_side = biggest.get("side", "long")
profile.position_size_usd = abs(biggest.get("size_usd", 0))
profile.entry_price = biggest.get("entry_price", 0)
profile.liquidation_price = biggest.get("liquidation_price", 0)
profile.unrealized_pnl = biggest.get("unrealized_pnl", 0)
stance = "Bearish" if profile.position_side == "short" else "Bullish"
profile.name = f"The {stance} Whale"
profile.persona = archetype["persona_template"].format(
size=profile.position_size_usd/1e6, stance=stance,
stance_desc=f"Currently {profile.position_side} with ${profile.unrealized_pnl:,.0f} unrealized PnL")
# LLM persona enrichment (MiroFish pattern)
if llm_client and archetype["archetype"] != "liquidation_engine":
try:
enrichment = llm_client.chat_json(messages=[
{"role": "system", "content": "You generate detailed trader personas for market simulation. Return JSON with: persona, trading_style, risk_tolerance, pain_point"},
{"role": "user", "content": f"Generate a detailed persona for this market participant:\nArchetype: {archetype['archetype']}\nCurrent market context:\n{graph_context}\n\nMake it specific to current conditions, not generic."}
])
profile.persona = enrichment.get("persona", profile.persona)
profile.trading_style = enrichment.get("trading_style", "")
profile.risk_tolerance = enrichment.get("risk_tolerance", "moderate")
profile.pain_point = enrichment.get("pain_point", "")
except Exception:
pass # Fall back to template
profiles.append(profile)
return profiles
"""
Report Agent — MiroFish Stage 4 Adaptation
MiroFish: report_agent.py
- ReACT pattern: Plan -> Think -> Act (use tools) -> Observe -> Reflect
- Plans report outline first, then generates each section
- Has tool access to query Zep graph during generation
- Multi-round reflection per section
Ours:
- Same ReACT pattern for market analysis
- Plans report structure, generates sections with tool queries
- Tools: market graph queries, agent action analysis, cascade detection
- Produces structured prediction report
"""
import json
from typing import Dict, Any, List, Optional
from dataclasses import dataclass, field
@dataclass
class ReportSection:
title: str
content: str
evidence: List[str] = field(default_factory=list)
confidence: float = 0.5
@dataclass
class SimulationReport:
title: str
executive_summary: str
sections: List[ReportSection]
recommendations: List[str]
risk_warnings: List[str]
overall_confidence: float
def to_text(self) -> str:
parts = [f"# {self.title}\n", f"## Executive Summary\n{self.executive_summary}\n"]
for s in self.sections:
parts.append(f"## {s.title}\n{s.content}\n")
if s.evidence:
parts.append("Evidence:")
for e in s.evidence:
parts.append(f" - {e}")
parts.append("")
if self.recommendations:
parts.append("## Recommendations")
for r in self.recommendations:
parts.append(f"- {r}")
if self.risk_warnings:
parts.append("\n## ⚠️ Risk Warnings")
for w in self.risk_warnings:
parts.append(f"- {w}")
parts.append(f"\n_Overall confidence: {self.overall_confidence:.0%}_")
return "\n".join(parts)
def to_dict(self):
return {
"title": self.title, "summary": self.executive_summary,
"sections": [{"title": s.title, "content": s.content,
"evidence": s.evidence, "confidence": s.confidence}
for s in self.sections],
"recommendations": self.recommendations,
"risk_warnings": self.risk_warnings,
"confidence": self.overall_confidence,
}
class ReportAgent:
"""
MiroFish ReACT Report Agent adapted for market simulation.
MiroFish pattern:
1. Plan outline (LLM generates section structure)
2. For each section: Think -> Act (query graph) -> Observe -> Reflect
3. Final synthesis across all sections
Tool set (MiroFish: InsightForge, PanoramaSearch, QuickSearch, Interview):
- query_graph: Search market state graph
- analyze_actions: Aggregate agent actions by type
- detect_cascades: Find cascade sequences in simulation
- calculate_pnl: Compute portfolio impact
"""
def __init__(self, llm_client=None):
self.llm_client = llm_client
def generate(self, simulation_result, graph, user_positions=None) -> SimulationReport:
"""Generate full report from simulation results."""
sim = simulation_result
# Tool functions (equivalent to MiroFish's Zep tools)
tools = {
"query_graph": lambda q: graph.query(q),
"analyze_actions": lambda: self._analyze_actions(sim),
"detect_cascades": lambda: self._detect_cascades(sim),
"calculate_pnl": lambda: self._calculate_pnl(sim, user_positions or []),
}
if self.llm_client:
return self._llm_report(sim, graph, tools, user_positions)
else:
return self._rule_based_report(sim, graph, tools, user_positions)
def _llm_report(self, sim, graph, tools, positions) -> SimulationReport:
"""LLM-powered ReACT report generation."""
# Gather tool outputs
action_analysis = tools["analyze_actions"]()
cascade_analysis = tools["detect_cascades"]()
pnl_analysis = tools["calculate_pnl"]()
prompt = f"""You are a market simulation report agent using ReACT reasoning.
SIMULATION: {sim.scenario}
PRICE PATH: ${sim.initial_price:,.0f} -> ${sim.final_price:,.0f} ({(sim.final_price/sim.initial_price-1)*100:+.1f}%)
ROUNDS: {sim.total_rounds}
MARKET STATE:
{graph.get_full_context()[:3000]}
AGENT ACTIONS ANALYSIS:
{action_analysis}
CASCADE ANALYSIS:
{cascade_analysis}
PORTFOLIO IMPACT:
{pnl_analysis}
Generate a prediction report in this JSON format:
{{
"title": "report title",
"executive_summary": "2-3 paragraph summary of key findings",
"sections": [
{{"title": "section name", "content": "detailed analysis", "evidence": ["evidence 1", "evidence 2"], "confidence": 0.7}},
],
"recommendations": ["actionable recommendation 1", "..."],
"risk_warnings": ["risk warning 1", "..."],
"overall_confidence": 0.6
}}
Be specific. Cite agent behaviors. Identify turning points. Quantify risks."""
try:
data = self.llm_client.chat_json(messages=[
{"role": "system", "content": "You are a quantitative market analyst generating simulation reports."},
{"role": "user", "content": prompt}
], temperature=0.3, max_tokens=4096)
sections = [ReportSection(title=s["title"], content=s["content"],
evidence=s.get("evidence", []), confidence=s.get("confidence", 0.5))
for s in data.get("sections", [])]
return SimulationReport(
title=data.get("title", sim.scenario),
executive_summary=data.get("executive_summary", ""),
sections=sections,
recommendations=data.get("recommendations", []),
risk_warnings=data.get("risk_warnings", []),
overall_confidence=data.get("overall_confidence", 0.5),
)
except Exception as e:
return self._rule_based_report(sim, graph, {"analyze_actions": lambda: "",
"detect_cascades": lambda: "", "calculate_pnl": lambda: ""}, positions)
def _rule_based_report(self, sim, graph, tools, positions) -> SimulationReport:
"""Fallback rule-based report when no LLM."""
total_chg = (sim.final_price / sim.initial_price - 1) * 100
# Find key turning points
turning_points = []
for r in sim.rounds:
aggressive = [a for a in r.actions if a.action in ("close", "liquidate", "flip")]
if aggressive:
turning_points.append(f"Round {r.round_num}: {', '.join(a.agent_emoji + ' ' + a.action for a in aggressive)}")
# Portfolio impact
pnl_text = self._calculate_pnl(sim, positions or [])
sections = [
ReportSection("Price Evolution",
f"Price moved from ${sim.initial_price:,.0f} to ${sim.final_price:,.0f} ({total_chg:+.1f}%) over {sim.total_rounds} rounds.",
[f"Round {r.round_num}: ${r.price:,.0f} ({r.price_change_pct:+.1f}%)" for r in sim.rounds]),
ReportSection("Agent Behavior", self._analyze_actions(sim)),
ReportSection("Cascade Analysis", self._detect_cascades(sim)),
ReportSection("Portfolio Impact", pnl_text),
]
if turning_points:
sections.append(ReportSection("Turning Points", "\n".join(turning_points)))
return SimulationReport(
title=f"Simulation: {sim.scenario}",
executive_summary=f"In the '{sim.scenario}' scenario, {sim.rounds[0].actions[0].agent_emoji if sim.rounds and sim.rounds[0].actions else ''} price moved {total_chg:+.1f}% from ${sim.initial_price:,.0f} to ${sim.final_price:,.0f}.",
sections=sections,
recommendations=[],
risk_warnings=["This is a simulation, not a prediction. Actual markets are more complex."],
overall_confidence=0.4,
)
def _analyze_actions(self, sim) -> str:
"""Analyze agent actions across all rounds."""
by_agent = {}
for r in sim.rounds:
for a in r.actions:
key = f"{a.agent_emoji} {a.agent_name}"
by_agent.setdefault(key, []).append(a)
lines = []
for agent_name, actions in by_agent.items():
action_seq = " -> ".join(a.action for a in actions)
lines.append(f"{agent_name}: {action_seq}")
# Key moments
for a in actions:
if a.action != "hold":
lines.append(f" R{a.round_num}: {a.action} ({a.thinking[:80]})")
return "\n".join(lines)
def _detect_cascades(self, sim) -> str:
"""Detect liquidation cascades in simulation."""
cascades = []
for r in sim.rounds:
liq_actions = [a for a in r.actions if a.action == "liquidate"]
panic_actions = [a for a in r.actions if a.action == "close" and "panic" in a.thinking.lower()]
if liq_actions or panic_actions:
cascades.append(f"Round {r.round_num} ({r.price_change_pct:+.1f}%): "
f"{len(liq_actions)} liquidations, {len(panic_actions)} panic exits")
if not cascades:
return "No cascades detected — market absorbed the shock."
return "CASCADES DETECTED:\n" + "\n".join(cascades)
def _calculate_pnl(self, sim, positions) -> str:
"""Calculate PnL impact on user positions."""
if not positions:
return "No user positions to evaluate."
total_chg = (sim.final_price / sim.initial_price - 1) * 100
lines = []
total_pnl = 0
for pos in positions:
side = pos.get("side", "long")
size = pos.get("size", 0)
entry = pos.get("entry_price", sim.initial_price)
if side == "long":
pnl = size * (sim.final_price - entry)
else:
pnl = size * (entry - sim.final_price)
total_pnl += pnl
pnl_pct = (pnl / (size * entry)) * 100 if size * entry > 0 else 0
lines.append(f" {pos.get('symbol','BTC')} {side} {size}: ${pnl:+,.0f} ({pnl_pct:+.1f}%)")
liq = pos.get("liquidation_price", 0)
if liq > 0:
dist = abs(sim.final_price - liq) / sim.final_price * 100
if dist < 10:
lines.append(f" ⚠️ DANGER: Only {dist:.1f}% from liquidation at ${liq:,.0f}")
lines.insert(0, f"Total PnL: ${total_pnl:+,.0f}")
return "\n".join(lines)
#!/usr/bin/env python3
"""
Trade Simulator v2 — MiroFish-Style Agentic Market Simulation
Architecture: Seed Data → Market Graph → Agent Profiles → LLM Simulation → Report → Interview
Inspired by MiroFish (github.com/666ghj/MiroFish) swarm intelligence engine.
Adapted from social media simulation to financial market simulation.
"""
import json
import os
import sys
import time
import hashlib
from dataclasses import dataclass, field, asdict
from typing import List, Dict, Any, Optional
from datetime import datetime
# ─── LLM Client ────────────────────────────────────────────
class LLMClient:
"""Lightweight OpenAI-compatible LLM client."""
def __init__(self, api_key=None, base_url=None, model=None):
self.api_key = api_key or os.environ.get("OPENROUTER_API_KEY", "")
self.base_url = base_url or os.environ.get("LLM_BASE_URL", "https://openrouter.ai/api/v1")
self.model = model or os.environ.get("LLM_MODEL_NAME", "anthropic/claude-sonnet-4")
# Configure proxy for workspace scripts (IPv6 needs brackets)
self.proxies = {}
proxy_host = os.environ.get("PROXY_HOST")
proxy_port = os.environ.get("PROXY_PORT")
if proxy_host and proxy_port:
if ":" in proxy_host and not proxy_host.startswith("["):
proxy_host = f"[{proxy_host}]"
proxy_url = f"http://{proxy_host}:{proxy_port}"
self.proxies = {"http": proxy_url, "https": proxy_url}
self.verify = os.environ.get("REQUESTS_CA_BUNDLE", True)
def chat(self, system_prompt: str, user_prompt: str, temperature: float = 0.7, max_tokens: int = 2000) -> str:
"""Send chat completion request."""
import requests
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": self.model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
"temperature": temperature,
"max_tokens": max_tokens
}
resp = requests.post(f"{self.base_url}/chat/completions", headers=headers, json=payload,
timeout=90, proxies=self.proxies, verify=self.verify)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
def chat_json(self, system_prompt: str, user_prompt: str, temperature: float = 0.4, max_tokens: int = 3000) -> dict:
"""Chat and parse JSON from response."""
raw = self.chat(system_prompt, user_prompt, temperature, max_tokens)
# Extract JSON from markdown code blocks if present
if "```json" in raw:
raw = raw.split("```json")[1].split("```")[0]
elif "```" in raw:
raw = raw.split("```")[1].split("```")[0]
return json.loads(raw.strip())
# ─── Market Graph (MiroFish Stage 1) ──────────────────────
@dataclass
class MarketGraph:
"""
Market Knowledge Graph — replaces MiroFish's Zep graph.
Instead of social entities + relationships, we have market entities.
"""
symbol: str
price: float
open_interest: Dict[str, Any] = field(default_factory=dict)
liquidation_data: Dict[str, Any] = field(default_factory=dict)
funding_rates: Dict[str, Any] = field(default_factory=dict)
whale_positions: List[Dict] = field(default_factory=list)
orderbook: Dict[str, Any] = field(default_factory=dict)
long_short_ratio: Dict[str, Any] = field(default_factory=dict)
sentiment: Dict[str, Any] = field(default_factory=dict)
user_positions: List[Dict] = field(default_factory=list)
ohlc_recent: List[Dict] = field(default_factory=list)
def summary(self) -> str:
"""Natural language summary for LLM consumption."""
lines = [f"=== Market Graph: {self.symbol} ==="]
lines.append(f"Current Price: ${self.price:,.2f}")
if self.open_interest:
total_oi = self.open_interest.get("total_oi_usd", 0)
lines.append(f"Total Open Interest: ${total_oi/1e9:.2f}B")
if self.funding_rates:
rate = self.funding_rates.get("current_rate", 0)
lines.append(f"Funding Rate: {rate*100:.4f}%")
if self.liquidation_data:
long_liqs = self.liquidation_data.get("long_liquidations_24h", 0)
short_liqs = self.liquidation_data.get("short_liquidations_24h", 0)
lines.append(f"24h Liquidations: ${long_liqs/1e6:.1f}M longs, ${short_liqs/1e6:.1f}M shorts")
if self.whale_positions:
n = len(self.whale_positions)
total_size = sum(abs(w.get("position_value", 0)) for w in self.whale_positions)
lines.append(f"Tracked Whales: {n} positions, ${total_size/1e6:.1f}M total")
if self.orderbook:
bid_depth = self.orderbook.get("bid_depth_usd", 0)
ask_depth = self.orderbook.get("ask_depth_usd", 0)
lines.append(f"Orderbook: ${bid_depth/1e6:.1f}M bids, ${ask_depth/1e6:.1f}M asks")
if self.long_short_ratio:
ratio = self.long_short_ratio.get("ratio", 1.0)
lines.append(f"Long/Short Ratio: {ratio:.2f}")
if self.sentiment:
score = self.sentiment.get("galaxy_score", 0)
lines.append(f"Galaxy Score (sentiment): {score}/100")
if self.user_positions:
lines.append(f"\nUser Positions:")
for p in self.user_positions:
side = "LONG" if p.get("size", 0) > 0 else "SHORT"
lines.append(f" {p.get('coin','?')} {side} {abs(p.get('size',0))} @ ${p.get('entry_price',0):,.2f} (PnL: ${p.get('unrealized_pnl',0):,.2f})")
return "\n".join(lines)
def to_dict(self) -> dict:
return asdict(self)
# ─── Agent Profiles (MiroFish Stage 2) ────────────────────
@dataclass
class AgentProfile:
"""
LLM-generated agent persona — MiroFish's OasisAgentProfile adapted for markets.
Instead of social media personas, these are market participant profiles.
"""
agent_id: str
name: str
agent_type: str # whale, market_maker, liquidation_engine, funding_arb, retail, user
persona: str # LLM-generated detailed description
stance: str # bullish, bearish, neutral, reactive
risk_tolerance: str # aggressive, moderate, conservative
position_size_usd: float = 0.0
position_side: str = "flat" # long, short, flat
entry_price: float = 0.0
liquidation_price: float = 0.0
pain_threshold_pct: float = 10.0 # % loss before panic action
activity_level: float = 0.5 # 0-1, how likely to act each round
# Memory — accumulates across rounds (MiroFish's temporal memory)
memory: List[str] = field(default_factory=list)
actions_taken: List[Dict] = field(default_factory=list)
def memory_summary(self) -> str:
if not self.memory:
return "No prior actions."
return "\n".join(f"- Round {i+1}: {m}" for i, m in enumerate(self.memory))
def to_dict(self) -> dict:
d = asdict(self)
return d
class AgentProfileGenerator:
"""
MiroFish Stage 2: Auto-generate agent profiles from market data using LLM.
Replaces MiroFish's oasis_profile_generator.py
"""
SYSTEM_PROMPT = """You are a financial market simulation architect. Given real market data,
you generate realistic agent profiles for market participants. Each agent must have:
- A detailed persona (background, trading style, emotional tendencies)
- Calibrated parameters based on the actual market data provided
- A clear stance and behavioral pattern
Output valid JSON only. No markdown, no explanation."""
def __init__(self, llm: LLMClient):
self.llm = llm
def generate_profiles(self, market_graph: MarketGraph, scenario: str) -> List[AgentProfile]:
"""Generate all agent profiles from market data."""
profiles = []
# 1. Generate whale agents from actual whale positions
profiles.extend(self._generate_whale_profiles(market_graph))
# 2. Generate MM agent from orderbook data
profiles.append(self._generate_mm_profile(market_graph))
# 3. Liquidation engine (deterministic, no LLM needed)
profiles.append(self._generate_liq_engine(market_graph))
# 4. Funding arb agent
profiles.append(self._generate_funding_arb(market_graph))
# 5. Retail swarm agent
profiles.append(self._generate_retail_profile(market_graph))
# 6. User's portfolio agent
if market_graph.user_positions:
profiles.append(self._generate_user_agent(market_graph))
return profiles
def _generate_whale_profiles(self, mg: MarketGraph) -> List[AgentProfile]:
"""Generate whale agents from actual Hyperliquid whale positions."""
if not mg.whale_positions:
return [AgentProfile(
agent_id="whale_generic", name="🐋 Generic Whale",
agent_type="whale", persona="Large trader with $10M+ portfolio, trend-following.",
stance="neutral", risk_tolerance="moderate",
position_size_usd=10_000_000, activity_level=0.6
)]
# Take top 3 whales by position size
sorted_whales = sorted(mg.whale_positions, key=lambda w: abs(w.get("position_value", 0)), reverse=True)[:3]
prompt = f"""Based on these real whale positions on Hyperliquid, generate agent profiles.
Market: {mg.symbol} at ${mg.price:,.2f}
Whale Positions:
{json.dumps(sorted_whales, indent=2, default=str)}
For each whale, generate a JSON object with:
- agent_id: unique string
- name: emoji + descriptive name
- persona: 2-3 sentences about their trading style (inferred from position size, leverage, entry)
- stance: "bullish" or "bearish" based on position
- risk_tolerance: "aggressive" if high leverage, "moderate" if medium, "conservative" if low
- position_size_usd: from data
- position_side: "long" or "short"
- entry_price: from data
- liquidation_price: from data (0 if unknown)
- pain_threshold_pct: estimate based on leverage (higher leverage = lower threshold)
- activity_level: 0.3-0.9
Return JSON array of profiles."""
try:
result = self.llm.chat_json(self.SYSTEM_PROMPT, prompt)
profiles = []
items = result if isinstance(result, list) else result.get("profiles", result.get("agents", []))
for item in items[:3]:
profiles.append(AgentProfile(
agent_id=item.get("agent_id", f"whale_{len(profiles)}"),
name=item.get("name", f"🐋 Whale {len(profiles)+1}"),
agent_type="whale",
persona=item.get("persona", "Large institutional trader."),
stance=item.get("stance", "neutral"),
risk_tolerance=item.get("risk_tolerance", "moderate"),
position_size_usd=float(item.get("position_size_usd", 1_000_000)),
position_side=item.get("position_side", "flat"),
entry_price=float(item.get("entry_price", mg.price)),
liquidation_price=float(item.get("liquidation_price", 0)),
pain_threshold_pct=float(item.get("pain_threshold_pct", 10)),
activity_level=float(item.get("activity_level", 0.6))
))
return profiles
except Exception as e:
print(f"[WARN] LLM whale profile generation failed: {e}, using fallback")
return [AgentProfile(
agent_id="whale_fallback", name="🐋 Whale (Fallback)",
agent_type="whale", persona="Large trader, data unavailable.",
stance="neutral", risk_tolerance="moderate", activity_level=0.5
)]
def _generate_mm_profile(self, mg: MarketGraph) -> AgentProfile:
"""Market Maker from orderbook data."""
bid_depth = mg.orderbook.get("bid_depth_usd", 5_000_000)
ask_depth = mg.orderbook.get("ask_depth_usd", 5_000_000)
spread = mg.orderbook.get("spread_bps", 1.0)
return AgentProfile(
agent_id="market_maker",
name="🤖 Market Maker",
agent_type="market_maker",
persona=f"Algorithmic market maker providing ${(bid_depth+ask_depth)/1e6:.0f}M in liquidity. "
f"Current spread: {spread:.1f} bps. Pulls liquidity on >3% moves, widens on >5%. "
f"Risk-neutral by design but can amplify moves by removing liquidity.",
stance="neutral",
risk_tolerance="conservative",
position_size_usd=bid_depth + ask_depth,
activity_level=0.9 # MMs act almost every round
)
def _generate_liq_engine(self, mg: MarketGraph) -> AgentProfile:
"""Liquidation engine — deterministic cascade model."""
long_liqs = mg.liquidation_data.get("long_liquidations_24h", 0)
short_liqs = mg.liquidation_data.get("short_liquidations_24h", 0)
return AgentProfile(
agent_id="liquidation_engine",
name="💀 Liquidation Engine",
agent_type="liquidation_engine",
persona=f"Exchange liquidation system. 24h stats: ${long_liqs/1e6:.0f}M longs, "
f"${short_liqs/1e6:.0f}M shorts liquidated. Cascades trigger when price hits "
f"cluster levels. Each cascade creates forced selling/buying that pushes price further.",
stance="reactive",
risk_tolerance="aggressive",
activity_level=1.0 # Always active when triggered
)
def _generate_funding_arb(self, mg: MarketGraph) -> AgentProfile:
rate = mg.funding_rates.get("current_rate", 0)
return AgentProfile(
agent_id="funding_arb",
name="📊 Funding Arbitrageur",
agent_type="funding_arb",
persona=f"Delta-neutral trader farming funding rates. Current rate: {rate*100:.4f}%. "
f"Opens positions against the crowd when funding is extreme. "
f"Adds selling pressure when funding is very positive, buying when very negative.",
stance="contrarian",
risk_tolerance="moderate",
activity_level=0.4 if abs(rate) < 0.0005 else 0.8
)
def _generate_retail_profile(self, mg: MarketGraph) -> AgentProfile:
ratio = mg.long_short_ratio.get("ratio", 1.0)
sentiment = mg.sentiment.get("galaxy_score", 50)
if ratio > 1.5:
stance = "bullish"
desc = "Retail is heavily long, FOMO-driven"
elif ratio < 0.7:
stance = "bearish"
desc = "Retail is heavily short, fear-driven"
else:
stance = "neutral"
desc = "Retail is balanced"
return AgentProfile(
agent_id="retail_swarm",
name="🐑 Retail Swarm",
agent_type="retail",
persona=f"Aggregate retail trader behavior. L/S ratio: {ratio:.2f}, sentiment: {sentiment}/100. "
f"{desc}. Momentum-following, panic-prone, enters after moves, exits at worst time. "
f"Represents thousands of small traders acting in aggregate.",
stance=stance,
risk_tolerance="aggressive",
activity_level=0.7
)
def _generate_user_agent(self, mg: MarketGraph) -> AgentProfile:
positions_desc = []
total_value = 0
for p in mg.user_positions:
side = "LONG" if p.get("size", 0) > 0 else "SHORT"
val = abs(p.get("position_value", 0))
total_value += val
positions_desc.append(f"{p.get('coin','?')} {side} ${val:,.0f}")
return AgentProfile(
agent_id="user_portfolio",
name="👤 Your Portfolio",
agent_type="user",
persona=f"Your actual positions: {', '.join(positions_desc)}. "
f"Total notional: ${total_value:,.0f}. This agent tracks your PnL impact.",
stance="observer",
risk_tolerance="moderate",
position_size_usd=total_value,
activity_level=0.0 # Passive — just tracks impact
)
# ─── Simulation Engine (MiroFish Stage 3) ─────────────────
@dataclass
class RoundResult:
"""Result of a single simulation round."""
round_num: int
price_before: float
price_after: float
price_change_pct: float
agent_actions: List[Dict[str, Any]]
market_events: List[str]
cumulative_change_pct: float = 0.0
def to_dict(self): return asdict(self)
@dataclass
class SimulationResult:
"""Full simulation output."""
scenario: str
symbol: str
initial_price: float
final_price: float
total_change_pct: float
rounds: List[RoundResult]
agent_profiles: List[Dict]
user_pnl: Dict[str, Any]
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
def to_dict(self):
return {
"scenario": self.scenario,
"symbol": self.symbol,
"initial_price": self.initial_price,
"final_price": self.final_price,
"total_change_pct": self.total_change_pct,
"rounds": [r.to_dict() for r in self.rounds],
"agent_profiles": self.agent_profiles,
"user_pnl": self.user_pnl,
"timestamp": self.timestamp
}
class SimulationEngine:
"""
MiroFish-style round-based simulation with LLM-driven agent reasoning.
Key difference from v1: Instead of rule-based if/else, each agent
THINKS via LLM about what to do given the current state.
"""
AGENT_REASONING_PROMPT = """You are simulating a {agent_type} in a crypto market scenario.
YOUR IDENTITY:
{persona}
CURRENT MARKET STATE:
- {symbol} price: ${price:,.2f} (started at ${initial_price:,.2f}, {total_change:+.2f}% total)
- This is round {round_num} of {total_rounds}
- Scenario trigger: {scenario}
YOUR POSITION:
- Side: {position_side}, Size: ${position_size:,.0f}
- Entry: ${entry_price:,.2f}
{liq_info}
WHAT HAPPENED SO FAR:
{memory}
OTHER AGENTS' RECENT ACTIONS:
{other_actions}
MARKET EVENTS THIS ROUND:
{events}
Based on your persona and the situation, decide your action. You must output valid JSON:
{{
"thinking": "Your internal reasoning (2-3 sentences)",
"action": "one of: hold, buy, sell, add_position, reduce_position, close_position, pull_liquidity, restore_liquidity, panic_sell, fomo_buy, no_action",
"action_size_pct": <0-100, what % of your position/capacity are you deploying>,
"price_impact_bps": <estimated market impact in basis points, -50 to +50>,
"confidence": <0.0-1.0>,
"emotional_state": "one of: calm, anxious, greedy, fearful, panicking, euphoric, resigned"
}}"""
LIQUIDATION_ENGINE_PROMPT = """You are the exchange liquidation engine. This is deterministic, not emotional.
Current {symbol} price: ${price:,.2f} (change from start: {total_change:+.2f}%)
Open Interest: ${oi:,.0f}
Recent liquidation stats: {liq_data}
Estimated liquidation clusters (based on OI distribution):
- 3% below current: ~${liq_3pct_long:,.0f} in long liquidations
- 5% below current: ~${liq_5pct_long:,.0f} in long liquidations
- 10% below current: ~${liq_10pct_long:,.0f} in long liquidations
- 3% above current: ~${liq_3pct_short:,.0f} in short liquidations
- 5% above current: ~${liq_5pct_short:,.0f} in short liquidations
- 10% above current: ~${liq_10pct_short:,.0f} in short liquidations
Price moved {round_change:+.2f}% this round. Calculate which liquidation clusters were hit.
Output JSON:
{{
"thinking": "Which clusters got hit and cascade math",
"action": "liquidate_longs" or "liquidate_shorts" or "no_liquidations",
"liquidated_usd": <total USD forced closed>,
"price_impact_bps": <cascade impact in basis points>,
"cascade_triggered": true/false,
"next_cluster_price": <price of next major cluster>
}}"""
def __init__(self, llm: LLMClient, market_graph: MarketGraph, agents: List[AgentProfile]):
self.llm = llm
self.mg = market_graph
self.agents = {a.agent_id: a for a in agents}
self.initial_price = market_graph.price
self.current_price = market_graph.price
self.rounds: List[RoundResult] = []
self.current_round = 0
def run(self, scenario: str, trigger_pct: float, num_rounds: int = 6) -> SimulationResult:
"""Run full simulation."""
print(f"\n{'='*60}")
print(f"SIMULATION: {scenario}")
print(f"{'='*60}")
print(f"Initial price: ${self.initial_price:,.2f}")
print(f"Trigger: {trigger_pct:+.1f}% → Rounds: {num_rounds}\n")
# Apply initial trigger
self.current_price = self.initial_price * (1 + trigger_pct / 100)
for round_num in range(1, num_rounds + 1):
self.current_round = round_num
result = self._run_round(round_num, num_rounds, scenario, trigger_pct)
self.rounds.append(result)
# Update price for next round
self.current_price = result.price_after
print(f" Round {round_num}: ${result.price_before:,.2f} → ${result.price_after:,.2f} ({result.price_change_pct:+.2f}%)")
for action in result.agent_actions:
print(f" {action['agent_name']}: {action['action']} ({action.get('thinking', '')[:80]})")
# Calculate user PnL
user_pnl = self._calculate_user_pnl()
total_change = (self.current_price - self.initial_price) / self.initial_price * 100
print(f"\n{'='*60}")
print(f"RESULT: ${self.initial_price:,.2f} → ${self.current_price:,.2f} ({total_change:+.2f}%)")
if user_pnl:
print(f"Your PnL: ${user_pnl.get('total_pnl', 0):,.2f}")
print(f"{'='*60}\n")
return SimulationResult(
scenario=scenario,
symbol=self.mg.symbol,
initial_price=self.initial_price,
final_price=self.current_price,
total_change_pct=total_change,
rounds=self.rounds,
agent_profiles=[a.to_dict() for a in self.agents.values()],
user_pnl=user_pnl
)
def _run_round(self, round_num: int, total_rounds: int, scenario: str, trigger_pct: float) -> RoundResult:
"""Execute one simulation round — all agents think and act."""
price_before = self.current_price
total_change = (self.current_price - self.initial_price) / self.initial_price * 100
actions = []
events = []
net_impact_bps = 0
# Collect previous round actions for context
prev_actions = ""
if self.rounds:
prev = self.rounds[-1]
prev_actions = "\n".join(f"- {a['agent_name']}: {a['action']}" for a in prev.agent_actions)
for agent_id, agent in self.agents.items():
if agent.agent_type == "user":
continue # User is passive observer
if agent.agent_type == "liquidation_engine":
action = self._run_liq_engine(round_num, total_change, price_before)
else:
action = self._run_agent(agent, round_num, total_rounds, scenario,
total_change, price_before, prev_actions, events)
if action:
actions.append(action)
net_impact_bps += action.get("price_impact_bps", 0)
# Record action in agent memory
agent.memory.append(
f"Price ${price_before:,.0f} ({total_change:+.1f}%). "
f"I {action['action']}. Feeling: {action.get('emotional_state', 'calm')}. "
f"Reasoning: {action.get('thinking', 'N/A')[:100]}"
)
if action.get("cascade_triggered"):
events.append(f"⚡ LIQUIDATION CASCADE: ${action.get('liquidated_usd', 0)/1e6:.0f}M liquidated")
# Calculate new price from net impact
price_change_pct = net_impact_bps / 100 # bps to pct
price_after = price_before * (1 + price_change_pct / 100)
actual_change = (price_after - price_before) / price_before * 100
cumulative = (price_after - self.initial_price) / self.initial_price * 100
return RoundResult(
round_num=round_num,
price_before=price_before,
price_after=price_after,
price_change_pct=actual_change,
agent_actions=actions,
market_events=events,
cumulative_change_pct=cumulative
)
def _run_agent(self, agent, round_num, total_rounds, scenario, total_change, price, prev_actions, events):
"""LLM-driven agent reasoning — the core MiroFish pattern."""
liq_info = ""
if agent.liquidation_price > 0:
dist = abs(price - agent.liquidation_price) / price * 100
liq_info = f"- Liquidation price: ${agent.liquidation_price:,.2f} ({dist:.1f}% away)"
prompt = self.AGENT_REASONING_PROMPT.format(
agent_type=agent.agent_type,
persona=agent.persona,
symbol=self.mg.symbol,
price=price,
initial_price=self.initial_price,
total_change=total_change,
round_num=round_num,
total_rounds=total_rounds,
scenario=scenario,
position_side=agent.position_side,
position_size=agent.position_size_usd,
entry_price=agent.entry_price,
liq_info=liq_info,
memory=agent.memory_summary(),
other_actions=prev_actions or "First round — no prior actions.",
events="\n".join(events) if events else "No special events."
)
try:
result = self.llm.chat_json(
"You are an agent in a financial market simulation. Output valid JSON only.",
prompt, temperature=0.6, max_tokens=500
)
result["agent_id"] = agent.agent_id
result["agent_name"] = agent.name
result["agent_type"] = agent.agent_type
return result
except Exception as e:
return {
"agent_id": agent.agent_id,
"agent_name": agent.name,
"agent_type": agent.agent_type,
"action": "hold",
"thinking": f"LLM error: {str(e)[:50]}",
"price_impact_bps": 0,
"emotional_state": "calm"
}
def _run_liq_engine(self, round_num, total_change, price):
"""Liquidation engine — hybrid LLM + math."""
oi = self.mg.open_interest.get("total_oi_usd", 20_000_000_000)
liq_data = json.dumps(self.mg.liquidation_data, default=str)[:500]
round_change = 0
if self.rounds:
round_change = self.rounds[-1].price_change_pct
# Estimate clusters based on OI
liq_per_pct = oi * 0.02 # ~2% of OI at each level
prompt = self.LIQUIDATION_ENGINE_PROMPT.format(
symbol=self.mg.symbol,
price=price,
total_change=total_change,
oi=oi,
liq_data=liq_data[:300],
round_change=round_change,
liq_3pct_long=liq_per_pct * 0.5,
liq_5pct_long=liq_per_pct * 1.0,
liq_10pct_long=liq_per_pct * 2.0,
liq_3pct_short=liq_per_pct * 0.5,
liq_5pct_short=liq_per_pct * 1.0,
liq_10pct_short=liq_per_pct * 2.0
)
try:
result = self.llm.chat_json(
"You are a deterministic liquidation engine. Calculate cascades from math, not emotion. Output JSON only.",
prompt, temperature=0.2, max_tokens=400
)
result["agent_id"] = "liquidation_engine"
result["agent_name"] = "💀 Liquidation Engine"
result["agent_type"] = "liquidation_engine"
return result
except Exception as e:
return {
"agent_id": "liquidation_engine",
"agent_name": "💀 Liquidation Engine",
"agent_type": "liquidation_engine",
"action": "no_liquidations",
"thinking": f"Error: {e}",
"price_impact_bps": 0,
"cascade_triggered": False
}
def _calculate_user_pnl(self) -> Dict:
"""Calculate PnL impact on user's actual positions."""
if not self.mg.user_positions:
return {}
total_pnl = 0
position_pnls = []
for p in self.mg.user_positions:
coin = p.get("coin", self.mg.symbol)
size = p.get("size", 0)
entry = p.get("entry_price", self.initial_price)
# Only calculate for the simulated symbol
if coin.upper() == self.mg.symbol.upper():
if size > 0: # Long
pnl = size * (self.current_price - entry)
else: # Short
pnl = abs(size) * (entry - self.current_price)
total_pnl += pnl
position_pnls.append({
"coin": coin,
"side": "long" if size > 0 else "short",
"size": abs(size),
"entry_price": entry,
"exit_price": self.current_price,
"pnl": pnl,
"pnl_pct": (pnl / (abs(size) * entry)) * 100 if entry > 0 else 0
})
return {
"total_pnl": total_pnl,
"positions": position_pnls,
"initial_price": self.initial_price,
"final_price": self.current_price
}
# ─── Report Agent (MiroFish Stage 4) ──────────────────────
class ReportAgent:
"""
MiroFish ReACT-style report generator.
Analyzes full simulation results and produces a structured report.
"""
SYSTEM_PROMPT = """You are an expert market analyst reviewing a multi-agent simulation.
Your job is to produce a clear, actionable report from the simulation results.
Use the simulation data to support your conclusions. Be specific about round numbers,
agent behaviors, and turning points. Write for a trader who needs to make decisions."""
def __init__(self, llm: LLMClient):
self.llm = llm
def generate_report(self, sim_result: SimulationResult) -> str:
"""Generate structured analysis report."""
prompt = f"""Analyze this market simulation and write a trading report.
SCENARIO: {sim_result.scenario}
SYMBOL: {sim_result.symbol}
PRICE: ${sim_result.initial_price:,.2f} → ${sim_result.final_price:,.2f} ({sim_result.total_change_pct:+.2f}%)
ROUND-BY-ROUND:
"""
for r in sim_result.rounds:
prompt += f"\nRound {r.round_num}: ${r.price_before:,.2f} → ${r.price_after:,.2f} ({r.price_change_pct:+.2f}%)"
for a in r.agent_actions:
prompt += f"\n {a.get('agent_name','?')}: {a.get('action','?')} — {a.get('thinking','')[:100]}"
prompt += f" [impact: {a.get('price_impact_bps',0)} bps, mood: {a.get('emotional_state','?')}]"
if r.market_events:
for e in r.market_events:
prompt += f"\n ⚡ {e}"
if sim_result.user_pnl and sim_result.user_pnl.get("positions"):
prompt += f"\n\nUSER PORTFOLIO IMPACT:"
for p in sim_result.user_pnl["positions"]:
prompt += f"\n {p['coin']} {p['side']}: ${p['pnl']:,.2f} ({p['pnl_pct']:+.1f}%)"
prompt += f"\n Total PnL: ${sim_result.user_pnl['total_pnl']:,.2f}"
prompt += """
Write a report with these sections:
1. **Executive Summary** (3 sentences: what happened, why, what it means)
2. **Key Turning Points** (which rounds were pivotal and why)
3. **Agent Behavior Analysis** (what each agent type did and why — highlight emergent/unexpected behaviors)
4. **Cascade Analysis** (if any liquidation cascades occurred, describe the chain reaction)
5. **Portfolio Impact** (specific PnL numbers and risk assessment for the user)
6. **Risk Warnings** (what could go worse than this simulation, and what levels to watch)
7. **Actionable Recommendations** (specific actions the trader should consider)"""
return self.llm.chat(self.SYSTEM_PROMPT, prompt, temperature=0.5, max_tokens=3000)
# ─── Agent Interview (MiroFish Stage 5) ───────────────────
class AgentInterviewer:
"""
Post-simulation agent interview system.
MiroFish's killer feature: chat with any agent about their decisions.
"""
SYSTEM_PROMPT = """You are role-playing as a market participant who just went through a trading simulation.
Stay in character. Answer based on your persona, your memory of what happened,
and the emotions you experienced. Be honest about your reasoning and mistakes.
You are being interviewed by a trader who wants to understand the market dynamics."""
def __init__(self, llm: LLMClient):
self.llm = llm
def interview(self, agent: AgentProfile, sim_result: SimulationResult, question: str) -> str:
"""Interview an agent about their decisions during the simulation."""
# Build context from agent's memory and actions
context = f"""YOUR IDENTITY: {agent.name}
TYPE: {agent.agent_type}
PERSONA: {agent.persona}
YOUR MEMORY OF THE SIMULATION:
{agent.memory_summary()}
THE SCENARIO WAS: {sim_result.scenario}
PRICE WENT: ${sim_result.initial_price:,.2f} → ${sim_result.final_price:,.2f} ({sim_result.total_change_pct:+.2f}%)
YOUR ACTIONS DURING SIMULATION:
"""
for r in sim_result.rounds:
for a in r.agent_actions:
if a.get("agent_id") == agent.agent_id:
context += f"Round {r.round_num}: {a.get('action','')} — {a.get('thinking','')} (feeling: {a.get('emotional_state','')})\n"
context += f"\nTRADER'S QUESTION: {question}"
return self.llm.chat(self.SYSTEM_PROMPT, context, temperature=0.7, max_tokens=1000)
# ─── Main Entry Point ─────────────────────────────────────
def run_from_market_data(market_data: dict, scenario: str, trigger_pct: float,
num_rounds: int = 6, run_report: bool = True) -> dict:
"""
Main entry point — called by the Starchild agent.
Args:
market_data: Dict with market graph data
scenario: Natural language scenario description
trigger_pct: Initial price shock percentage
num_rounds: Number of simulation rounds
run_report: Whether to generate the analysis report
Returns:
Dict with simulation results, report, and agent profiles
"""
llm = LLMClient()
# Stage 1: Build Market Graph
mg = MarketGraph(
symbol=market_data.get("symbol", "BTC"),
price=market_data.get("price", 71500),
open_interest=market_data.get("open_interest", {}),
liquidation_data=market_data.get("liquidation_data", {}),
funding_rates=market_data.get("funding_rates", {}),
whale_positions=market_data.get("whale_positions", []),
orderbook=market_data.get("orderbook", {}),
long_short_ratio=market_data.get("long_short_ratio", {}),
sentiment=market_data.get("sentiment", {}),
user_positions=market_data.get("user_positions", [])
)
print(f"\n📊 Market Graph:\n{mg.summary()}\n")
# Stage 2: Generate Agent Profiles
print("🧬 Generating agent profiles from market data...")
generator = AgentProfileGenerator(llm)
agents = generator.generate_profiles(mg, scenario)
print(f" Created {len(agents)} agents: {', '.join(a.name for a in agents)}\n")
# Stage 3: Run Simulation
print("🎮 Starting simulation...")
engine = SimulationEngine(llm, mg, agents)
sim_result = engine.run(scenario, trigger_pct, num_rounds)
# Stage 4: Generate Report
report_text = ""
if run_report:
print("📝 Generating analysis report...")
reporter = ReportAgent(llm)
report_text = reporter.generate_report(sim_result)
# Package results
output = {
"simulation": sim_result.to_dict(),
"report": report_text,
"agents": {a.agent_id: a.to_dict() for a in agents},
"market_graph_summary": mg.summary()
}
return output
def run_interview(sim_output: dict, agent_id: str, question: str) -> str:
"""Run a post-simulation interview with an agent."""
llm = LLMClient()
interviewer = AgentInterviewer(llm)
# Reconstruct agent from saved data
agent_data = sim_output["agents"].get(agent_id)
if not agent_data:
available = list(sim_output["agents"].keys())
return f"Agent '{agent_id}' not found. Available: {available}"
agent = AgentProfile(**{k: v for k, v in agent_data.items() if k in AgentProfile.__dataclass_fields__})
# Reconstruct minimal sim result for context
sim_data = sim_output["simulation"]
sim_result = SimulationResult(
scenario=sim_data["scenario"],
symbol=sim_data["symbol"],
initial_price=sim_data["initial_price"],
final_price=sim_data["final_price"],
total_change_pct=sim_data["total_change_pct"],
rounds=[RoundResult(**r) for r in sim_data["rounds"]],
agent_profiles=sim_data["agent_profiles"],
user_pnl=sim_data["user_pnl"]
)
return interviewer.interview(agent, sim_result, question)
# ─── CLI ───────────────────────────────────────────────────
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Trade Simulator v2 — MiroFish-Style")
parser.add_argument("--scenario", default="BTC drops 10%", help="Scenario description")
parser.add_argument("--trigger", type=float, default=-10.0, help="Price trigger %")
parser.add_argument("--rounds", type=int, default=6, help="Number of rounds")
parser.add_argument("--market-data", help="Path to market data JSON file")
parser.add_argument("--output", default="/tmp/sim_result.json", help="Output file")
parser.add_argument("--no-report", action="store_true", help="Skip report generation")
# Interview mode
parser.add_argument("--interview", action="store_true", help="Interview mode")
parser.add_argument("--agent", help="Agent ID to interview")
parser.add_argument("--question", help="Question to ask the agent")
parser.add_argument("--sim-result", help="Path to simulation result JSON")
args = parser.parse_args()
if args.interview:
# Interview mode
if not args.sim_result or not args.agent or not args.question:
print("Interview mode requires --sim-result, --agent, and --question")
sys.exit(1)
with open(args.sim_result) as f:
sim_output = json.load(f)
answer = run_interview(sim_output, args.agent, args.question)
print(f"\n🎤 Interview with {args.agent}:\n")
print(answer)
else:
# Simulation mode
if args.market_data:
with open(args.market_data) as f:
market_data = json.load(f)
else:
# Default demo data
market_data = {
"symbol": "BTC",
"price": 71500,
"open_interest": {"total_oi_usd": 20_000_000_000},
"liquidation_data": {"long_liquidations_24h": 50_000_000, "short_liquidations_24h": 30_000_000},
"funding_rates": {"current_rate": 0.0003},
"whale_positions": [],
"orderbook": {"bid_depth_usd": 8_000_000, "ask_depth_usd": 7_500_000, "spread_bps": 0.5},
"long_short_ratio": {"ratio": 1.15},
"sentiment": {"galaxy_score": 62},
"user_positions": [{"coin": "BTC", "size": -0.1, "entry_price": 71500, "position_value": 7150, "unrealized_pnl": 0}]
}
result = run_from_market_data(
market_data, args.scenario, args.trigger,
args.rounds, not args.no_report
)
with open(args.output, "w") as f:
json.dump(result, f, indent=2, default=str)
print(f"\n✅ Results saved to {args.output}")
if result.get("report"):
print(f"\n{'='*60}")
print("📋 ANALYSIS REPORT")
print(f"{'='*60}\n")
print(result["report"])
"""
Market Simulation Runner — MiroFish Stage 3 Adaptation
MiroFish: simulation_runner.py + run_parallel_simulation.py
- Runs OASIS Twitter/Reddit environments in parallel
- Agents take social actions (post, like, reply) each round
- Actions logged to JSONL, state tracked per round
- Time-aware activity: agents more active during peak hours
Ours:
- Single "Market" environment (no social platforms needed)
- Agents take market actions (hold, add, reduce, close) each round
- Each agent LLM-reasons about their action given current state
- Price evolves based on aggregate agent actions + scenario shocks
- All actions logged with thinking traces
"""
import json
import time
from typing import Dict, Any, List, Optional, Callable
from dataclasses import dataclass, field
from datetime import datetime
from .profile_generator import MarketAgentProfile
from .market_graph import MarketGraph
@dataclass
class AgentAction:
"""
MiroFish equivalent: AgentAction dataclass
Records what an agent did in a round with full reasoning trace.
"""
round_num: int
agent_id: int
agent_name: str
agent_emoji: str
thinking: str # Internal reasoning (MiroFish: agent's thought process)
action: str # hold, add, reduce, close, flip
size_change_pct: float
reasoning_public: str # What they'd say publicly
confidence: float
market_impact: str
price_before: float
price_after: float
def to_dict(self):
return {
"round": self.round_num, "agent": f"{self.agent_emoji} {self.agent_name}",
"thinking": self.thinking, "action": self.action,
"size_change_pct": self.size_change_pct,
"public": self.reasoning_public, "confidence": self.confidence,
"impact": self.market_impact,
"price": f"${self.price_before:,.0f} -> ${self.price_after:,.0f}",
}
@dataclass
class RoundResult:
"""State after each simulation round."""
round_num: int
price: float
price_change_pct: float
actions: List[AgentAction]
cumulative_change_pct: float
event_injected: Optional[str] = None
def to_dict(self):
return {
"round": self.round_num, "price": self.price,
"change_pct": f"{self.price_change_pct:+.2f}%",
"cumulative_pct": f"{self.cumulative_change_pct:+.2f}%",
"event": self.event_injected,
"actions": [a.to_dict() for a in self.actions],
}
@dataclass
class SimulationResult:
"""Complete simulation output."""
scenario: str
initial_price: float
final_price: float
total_rounds: int
rounds: List[RoundResult]
agent_profiles: List[Dict]
graph_summary: str
elapsed_seconds: float = 0
def to_dict(self):
return {
"scenario": self.scenario,
"price_path": f"${self.initial_price:,.0f} -> ${self.final_price:,.0f}",
"total_change_pct": f"{(self.final_price/self.initial_price - 1)*100:+.2f}%",
"rounds": [r.to_dict() for r in self.rounds],
"agents": self.agent_profiles,
"elapsed_seconds": self.elapsed_seconds,
}
class MarketSimulation:
"""
MiroFish equivalent: SimulationRunner + run_parallel_simulation
Key MiroFish patterns adapted:
1. Round-based loop with agent actions
2. Time-aware activity (peak/dead hours -> scenario phases)
3. Event injection mid-simulation
4. Action logging to structured format
5. State tracking per round
"""
def __init__(self, graph: MarketGraph, agents: List[MarketAgentProfile],
llm_client=None, num_rounds: int = 6):
self.graph = graph
self.agents = agents
self.llm_client = llm_client
self.num_rounds = num_rounds
self.action_log: List[AgentAction] = []
def run(self, scenario: str, initial_price: float,
scenario_shock_pct: float = 10.0,
events: List[Dict] = None,
progress_callback: Callable = None) -> SimulationResult:
"""
Run the full simulation.
MiroFish parallel: runs Twitter + Reddit simultaneously
Ours: runs all agents sequentially per round (market is single env)
"""
start = time.time()
price = initial_price
rounds = []
events = events or []
# Scenario shock schedule (MiroFish: timed events)
# Distribute the total shock across rounds with front-loading
shock_schedule = self._build_shock_schedule(scenario_shock_pct, self.num_rounds)
graph_context = self.graph.get_full_context()
for round_num in range(self.num_rounds):
price_before = price
# Check for event injection (MiroFish: dynamic event injection)
event_text = None
for evt in events:
if evt.get("round") == round_num:
event_text = evt.get("description", "")
# Apply scenario shock for this round
base_shock = shock_schedule[round_num]
# Each agent reasons and acts
round_actions = []
agent_impacts = []
for agent in self.agents:
action = self._agent_act(agent, round_num, price, base_shock,
graph_context, event_text)
round_actions.append(action)
agent_impacts.append(self._calculate_impact(agent, action))
# Update agent memory (MiroFish: zep memory update per round)
agent.memory.append(
f"Round {round_num+1}: Price ${price:,.0f} ({base_shock:+.1f}% shock). "
f"I decided to {action.action}. {action.reasoning_public}"
)
# Aggregate impacts to determine actual price move
total_impact = sum(agent_impacts)
actual_move_pct = base_shock + total_impact
price = price * (1 + actual_move_pct / 100)
# Update actions with final price
for action in round_actions:
action.price_after = price
cumulative = (price / initial_price - 1) * 100
rounds.append(RoundResult(
round_num=round_num + 1,
price=price,
price_change_pct=actual_move_pct,
actions=round_actions,
cumulative_change_pct=cumulative,
event_injected=event_text,
))
self.action_log.extend(round_actions)
if progress_callback:
progress_callback(round_num + 1, self.num_rounds, price)
return SimulationResult(
scenario=scenario,
initial_price=initial_price,
final_price=price,
total_rounds=self.num_rounds,
rounds=rounds,
agent_profiles=[a.to_dict() for a in self.agents],
graph_summary=self.graph.get_full_context(),
elapsed_seconds=time.time() - start,
)
def _build_shock_schedule(self, total_shock: float, num_rounds: int) -> List[float]:
"""
Distribute scenario shock across rounds.
MiroFish equivalent: time-of-day activity multipliers.
Front-loaded: biggest move in round 1, then diminishing.
"""
if num_rounds <= 0:
return []
weights = [1.0 / (i + 1) for i in range(num_rounds)]
total_w = sum(weights)
return [total_shock * w / total_w for w in weights]
def _agent_act(self, agent: MarketAgentProfile, round_num: int,
price: float, shock_pct: float, graph_context: str,
event: Optional[str] = None) -> AgentAction:
"""
Get an agent's action for this round.
MiroFish pattern: Each agent has an LLM call with their persona prompt.
The agent "thinks" and decides their action.
"""
if self.llm_client and agent.archetype != "liquidation_engine":
try:
system = agent.get_system_prompt(graph_context)
user_msg = f"Round {round_num+1}: Price moved {shock_pct:+.2f}% this round."
if event:
user_msg += f"\n\nBREAKING EVENT: {event}"
user_msg += "\n\nWhat is your action? Respond in JSON format."
response = self.llm_client.chat_json(messages=[
{"role": "system", "content": system},
{"role": "user", "content": user_msg}
], temperature=0.7)
return AgentAction(
round_num=round_num + 1,
agent_id=agent.agent_id,
agent_name=agent.name,
agent_emoji=agent.emoji,
thinking=response.get("thinking", ""),
action=response.get("action", "hold"),
size_change_pct=response.get("size_change_pct", 0),
reasoning_public=response.get("reasoning_public", ""),
confidence=response.get("confidence", 0.5),
market_impact=response.get("market_impact", "minimal"),
price_before=price,
price_after=price, # Updated after all agents act
)
except Exception as e:
pass # Fall through to rule-based
# Rule-based fallback (for liquidation engine or when no LLM)
return self._rule_based_action(agent, round_num, price, shock_pct)
def _rule_based_action(self, agent: MarketAgentProfile, round_num: int,
price: float, shock_pct: float) -> AgentAction:
"""Fallback rule-based action when LLM not available."""
action = "hold"
thinking = ""
size_pct = 0
if agent.archetype == "liquidation_engine":
# Check if shock exceeds liquidation thresholds
if abs(shock_pct) > 5:
action = "liquidate"
size_pct = min(abs(shock_pct) * 10, 80)
thinking = f"Shock of {shock_pct:+.1f}% triggers margin calls"
else:
action = "hold"
thinking = "No margin breaches this round"
elif agent.archetype == "retail":
if shock_pct > agent.panic_threshold * 100:
action = "add"
size_pct = 30
thinking = "FOMO buying into momentum"
elif shock_pct < -agent.panic_threshold * 100:
action = "close"
size_pct = 50
thinking = "Panic selling"
else:
action = "hold"
thinking = "Waiting for clearer signal"
elif agent.archetype == "market_maker":
if abs(shock_pct) > agent.panic_threshold * 100:
action = "reduce"
size_pct = 40
thinking = "Pulling liquidity — vol too high"
else:
action = "hold"
thinking = "Providing liquidity, spread is manageable"
elif agent.archetype == "whale":
if agent.position_side == "short" and shock_pct > 10:
action = "close"
size_pct = 60
thinking = f"Covering short — move against me too large"
elif agent.position_side == "long" and shock_pct < -10:
action = "close"
size_pct = 60
thinking = f"Cutting long — pain threshold reached"
else:
action = "hold"
thinking = "Position within tolerance"
else:
action = "hold"
thinking = "No edge this round"
return AgentAction(
round_num=round_num + 1, agent_id=agent.agent_id,
agent_name=agent.name, agent_emoji=agent.emoji,
thinking=thinking, action=action, size_change_pct=size_pct,
reasoning_public=thinking, confidence=0.5,
market_impact="", price_before=price, price_after=price,
)
def _calculate_impact(self, agent: MarketAgentProfile, action: AgentAction) -> float:
"""Calculate price impact of an agent's action."""
if action.action == "hold":
return 0.0
# Impact scales with position size and action aggressiveness
base_impact = (action.size_change_pct / 100) * agent.aggression
if action.action in ("add", "flip"):
direction = 1.0 # Buying pressure
elif action.action in ("reduce", "close", "liquidate"):
direction = -1.0 # Selling pressure
if agent.archetype == "liquidation_engine":
direction *= 2.0 # Liquidations have outsized impact
else:
direction = 0.0
# Flip direction based on position side
if agent.position_side == "short":
direction *= -1.0 # Short covering = buying
return base_impact * direction * 0.5 # Dampen for realism