From 632e1bf524f4ecd5d3bf00f14959d3b649d6caaf Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Sun, 12 Apr 2026 14:21:32 +0000 Subject: [PATCH] feat: add simulation management as agent skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent can now manage simulations via conversation: - 'start' (run simulation on token) - 'stop' (stop running simulation) - 'status' (check if running, show progress) - 'results' (get current/latest simulation results) Example conversations: - 'Run a simulation on GRIAN' → starts simulation - 'How's the simulation going?' → shows status - 'Stop the simulation' → stops and shows results - 'Show me the results' → shows simulation results --- .../app/services/ai_agent/conversational.py | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) diff --git a/src/backend/app/services/ai_agent/conversational.py b/src/backend/app/services/ai_agent/conversational.py index 6480948..ba3d5dd 100644 --- a/src/backend/app/services/ai_agent/conversational.py +++ b/src/backend/app/services/ai_agent/conversational.py @@ -127,6 +127,33 @@ TOOLS = [ "required": ["token_address"] } } + }, + { + "type": "function", + "function": { + "name": "manage_simulation", + "description": "Manage trading simulations: start, stop, or check status. Simulations run on real-time klines and show live portfolio updates. Use when user asks to run simulation, check simulation status, or stop simulation.", + "parameters": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["start", "stop", "status", "results"], + "description": "Action to perform: 'start' (begin new simulation), 'stop' (stop running simulation), 'status' (check if simulation is running), 'results' (get results from current or latest simulation)" + }, + "token_address": { + "type": "string", + "description": "Token contract address for simulation (required for 'start' action)" + }, + "kline_interval": { + "type": "string", + "description": "Kline interval: '1m', '5m', '15m', '1h' (default: '1m')", + "default": "1m" + } + }, + "required": ["action"] + } + } } ] @@ -135,6 +162,7 @@ SYSTEM_PROMPT_WITH_TOOLS = SYSTEM_PROMPT + """ You have access to tools: - search_tokens(chain, limit): Search for trending tokens on a blockchain. Use it when user asks for token recommendations or trending tokens. - run_backtest(token_address, timeframe, start_date, end_date): Run a backtest on historical data. Returns performance metrics. Use when user asks to backtest or check historical performance. +- manage_simulation(action, token_address, kline_interval): Manage trading simulations. Actions: 'start' (begin new), 'stop' (stop running), 'status' (check if running), 'results' (get current/latest results). When you want to use a tool, respond with: { @@ -274,6 +302,26 @@ class ConversationalAgent: "strategy_needs_confirmation": False, "success": True } + + elif func_name == "manage_simulation": + action = args.get("action") + token_address = args.get("token_address") + kline_interval = args.get("kline_interval", "1m") + + # Execute simulation management + sim_result = self._manage_simulation( + action=action, + token_address=token_address, + kline_interval=kline_interval + ) + + return { + "response": sim_result, + "thinking": thinking, + "strategy_updated": False, + "strategy_needs_confirmation": False, + "success": True + } # Get the main response content content = result.get("choices", [{}])[0].get("message", {}).get("content", "") @@ -501,6 +549,230 @@ Would you like me to adjust the strategy parameters based on these results?""" except Exception as e: return f"I encountered an error running the backtest: {str(e)}" + def _manage_simulation( + self, + action: str, + token_address: str = None, + kline_interval: str = "1m" + ) -> str: + """Manage trading simulations: start, stop, status, or results.""" + try: + from ...core.database import SessionLocal + from ...db.models import Simulation + from ...services.simulate.engine import SimulateEngine + from ...core.config import get_settings + from datetime import datetime + import asyncio + import uuid + import threading + + db = SessionLocal() + settings = get_settings() + + try: + # Get the bot + bot = db.query(Bot).filter(Bot.id == self.bot_id).first() + if not bot: + return "I couldn't find the bot. Please try again." + + if action == "start": + if not token_address: + return "I need a token address to start a simulation. Which token would you like to simulate?" + + # Check if there's already a running simulation + running_sim = db.query(Simulation).filter( + Simulation.bot_id == self.bot_id, + Simulation.status == "running" + ).first() + + if running_sim: + # Stop the existing one first + self._stop_simulation_db(running_sim.id) + + # Create new simulation + sim_id = str(uuid.uuid4()) + simulation = Simulation( + id=sim_id, + bot_id=self.bot_id, + started_at=datetime.utcnow(), + status="running", + config={ + "token": token_address, + "chain": "bsc", + "kline_interval": kline_interval + }, + signals=[], + klines=[], + trade_log=[] + ) + db.add(simulation) + db.commit() + + # Start the simulation in background + sim_config = { + "bot_id": self.bot_id, + "token": token_address, + "chain": "bsc", + "kline_interval": kline_interval, + "max_candles": 100, + "candle_delay": 30 if kline_interval == "1m" else 60, + "strategy_config": bot.strategy_config, + "ave_api_key": settings.AVE_API_KEY, + "ave_api_plan": settings.AVE_API_PLAN, + "initial_balance": 10000.0, + } + + # Run simulation in background thread + def run_sim(): + asyncio.run(self._run_simulation_sync(sim_id, settings.DATABASE_URL, sim_config)) + + thread = threading.Thread(target=run_sim) + thread.daemon = True + thread.start() + + return f"Started simulation on {token_address} using {kline_interval} klines. The simulation is running and will process up to 100 candles. Ask me for status or results anytime!" + + elif action == "stop": + # Find running simulation + running_sim = db.query(Simulation).filter( + Simulation.bot_id == self.bot_id, + Simulation.status == "running" + ).first() + + if not running_sim: + return "No simulation is currently running." + + self._stop_simulation_db(running_sim.id) + + # Get final results + portfolio = running_sim.portfolio or {} + current_balance = portfolio.get("current_balance", 10000) + initial_balance = portfolio.get("initial_balance", 10000) + pnl = current_balance - initial_balance + pnl_pct = (pnl / initial_balance) * 100 if initial_balance > 0 else 0 + + return f"Simulation stopped!\n\nFinal Results:\nšŸ’° Final Balance: ${current_balance:,.2f}\nšŸ“ˆ P&L: {'+' if pnl >= 0 else ''}${pnl:,.2f} ({'+' if pnl_pct >= 0 else ''}{pnl_pct:.2f}%)\nšŸ“Š Trades: {len(running_sim.trade_log or [])}" + + elif action == "status": + # Find running simulation + running_sim = db.query(Simulation).filter( + Simulation.bot_id == self.bot_id, + Simulation.status == "running" + ).first() + + if not running_sim: + return "No simulation is currently running." + + portfolio = running_sim.portfolio or {} + klines_count = len(running_sim.klines or []) + trade_count = len(running_sim.trade_log or []) + + status = f"**Simulation Status: Running**\n\n" + status += f"šŸ“Š Candles processed: ~{klines_count}\n" + status += f"šŸ“ˆ Trades executed: {trade_count}\n" + + if portfolio.get("position", 0) > 0: + status += f"šŸ’° Position: {portfolio['position']:.4f} {portfolio.get('position_token', 'TOKEN')}\n" + status += f"šŸ’° Cash: ${portfolio.get('current_balance', 0):,.2f}\n" + else: + status += f"šŸ’° Cash: ${portfolio.get('current_balance', 10000):,.2f}\n" + + status += "\nAsk me to stop or get full results anytime!" + return status + + elif action == "results": + # Find running or most recent simulation + simulation = db.query(Simulation).filter( + Simulation.bot_id == self.bot_id + ).order_by(Simulation.started_at.desc()).first() + + if not simulation: + return "No simulation found. Start a simulation first!" + + portfolio = simulation.portfolio or {} + current_balance = portfolio.get("current_balance", 10000) + initial_balance = portfolio.get("initial_balance", 10000) + pnl = current_balance - initial_balance + pnl_pct = (pnl / initial_balance) * 100 if initial_balance > 0 else 0 + trade_log = simulation.trade_log or [] + + status_emoji = "🟢" if simulation.status == "running" else "⚪" + status_text = "Running" if simulation.status == "running" else "Completed/Stopped" + + results = f"**Simulation Results** {status_emoji} ({status_text})\n\n" + results += f"šŸ’° Final Balance: ${current_balance:,.2f}\n" + results += f"šŸ“ˆ P&L: {'+' if pnl >= 0 else ''}${pnl:,.2f} ({'+' if pnl_pct >= 0 else ''}{pnl_pct:.2f}%)\n" + results += f"šŸ“Š Total Trades: {len(trade_log)}\n" + + if simulation.status == "running": + results += f"\nā³ Simulation still running... (refresh for latest)" + + return results + + else: + return f"Unknown action: {action}. Use 'start', 'stop', 'status', or 'results'." + + finally: + db.close() + + except Exception as e: + return f"I encountered an error managing the simulation: {str(e)}" + + def _stop_simulation_db(self, simulation_id: str): + """Stop a simulation in the database.""" + from ...core.database import SessionLocal + db = SessionLocal() + try: + simulation = db.query(Simulation).filter(Simulation.id == simulation_id).first() + if simulation: + simulation.status = "stopped" + db.commit() + finally: + db.close() + + async def _run_simulation_sync(self, simulation_id: str, db_url: str, config: dict): + """Run simulation synchronously in background.""" + from ...services.simulate.engine import SimulateEngine + from ...core.database import SessionLocal + + async def _run(): + engine = SimulateEngine(config) + engine.run_id = simulation_id + + def serialize_signal(s): + created = s.get("created_at") + if hasattr(created, "isoformat"): + created = created.isoformat() + return {**s, "created_at": created} + + def save_progress(): + db = SessionLocal() + try: + sim = db.query(Simulation).filter(Simulation.id == simulation_id).first() + if sim: + sim.status = engine.status + sim.signals = [serialize_signal(s) for s in engine.signals] + sim.klines = [{"time": k.get("time"), "close": k.get("close")} for k in engine.klines] + sim.trade_log = engine.trade_log + sim.portfolio = { + "initial_balance": config.get("initial_balance", 10000), + "current_balance": engine.current_balance, + "position": engine.position, + "position_token": engine.position_token, + "entry_price": engine.entry_price, + "current_price": engine.last_close, + } + db.commit() + finally: + db.close() + + try: + await engine.run() + finally: + save_progress() + + asyncio.run(_run()) + def _update_strategy(self, strategy_update: Dict) -> bool: """Update the bot's strategy in the database.""" try: