1864 lines
84 KiB
Python
1864 lines
84 KiB
Python
"""Conversational trading agent."""
|
|
|
|
import json
|
|
import re
|
|
from typing import List, Optional, Dict, Any
|
|
from datetime import timedelta
|
|
|
|
from .client import MiniMaxClient, SYSTEM_PROMPT_WITH_TOOLS, TOOLS
|
|
from .help import (
|
|
format_tools_list,
|
|
format_general_help,
|
|
format_tool_help,
|
|
format_skill_acknowledgment,
|
|
)
|
|
from .tools import get_tool_registry
|
|
from ...core.config import get_settings
|
|
from ...db.models import Bot, Simulation
|
|
|
|
|
|
class ConversationalAgent:
|
|
def __init__(self, api_key: str, model: str = "MiniMax-M2.7", bot_id: str = None):
|
|
self.api_key = api_key
|
|
self.model = model
|
|
self.bot_id = bot_id
|
|
|
|
self.client = MiniMaxClient(api_key, model)
|
|
|
|
self.pending_command = None
|
|
self.recent_search_results = []
|
|
|
|
def _is_error_output(self, code: int, output: str) -> bool:
|
|
"""Check if the command output contains an error."""
|
|
if code != 0:
|
|
return True
|
|
if (
|
|
output.startswith("Error:")
|
|
or "API error" in output
|
|
or "api key invalid" in output.lower()
|
|
):
|
|
return True
|
|
return False
|
|
|
|
def _handle_slash_command(self, user_message: str) -> Dict[str, Any]:
|
|
"""Handle slash command help requests."""
|
|
cmd = user_message.strip().lower()
|
|
|
|
if cmd == "/":
|
|
return {
|
|
"response": format_tools_list(),
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
elif cmd == "/help":
|
|
return {
|
|
"response": format_general_help(),
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
elif cmd.startswith("/"):
|
|
parts = cmd[1:].split()
|
|
tool_name = parts[0].lower() if parts else ""
|
|
has_args = len(parts) > 1
|
|
|
|
for category in ["randebu", "ave"]:
|
|
for tool in get_tool_registry().get(category, []):
|
|
if tool["name"].lower() == tool_name:
|
|
if tool_name == "strategy" and not has_args:
|
|
return self._get_strategy_response()
|
|
if tool_name == "trending" and not has_args:
|
|
return self._execute_trending()
|
|
if tool_name == "backtest":
|
|
return self._execute_backtest_direct(
|
|
user_message if has_args else ""
|
|
)
|
|
if tool_name == "simulate":
|
|
return self._execute_simulate_direct(
|
|
user_message if has_args else ""
|
|
)
|
|
if not has_args:
|
|
self.pending_command = tool_name
|
|
return {
|
|
"response": format_skill_acknowledgment(
|
|
tool["name"], tool["description"]
|
|
),
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
return None
|
|
|
|
return {
|
|
"response": f"Unknown command '{tool_name}'. Type / to see available tools.",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
return {
|
|
"response": "Unknown command. Type / for available tools or /help for general help.",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
def _get_strategy_response(self) -> Dict[str, Any]:
|
|
"""Fetch and format the current strategy from the database."""
|
|
if not self.bot_id:
|
|
return {
|
|
"response": "No bot selected. Please select a bot first.",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
try:
|
|
from ...core.database import get_db
|
|
|
|
db = next(get_db())
|
|
try:
|
|
bot = db.query(Bot).filter(Bot.id == self.bot_id).first()
|
|
if not bot:
|
|
return {
|
|
"response": "Bot not found.",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
strategy_config = bot.strategy_config
|
|
if not strategy_config:
|
|
return {
|
|
"response": "📝 **Your Current Strategy**\n\nNo strategy has been configured yet. Tell me what trading strategy you'd like to use, and I'll set it up for you!\n\nExample: \"Buy PEPE when it drops 5%\"",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
conditions = strategy_config.get("conditions", [])
|
|
actions = strategy_config.get("actions", [])
|
|
risk = strategy_config.get("risk_management", {})
|
|
|
|
response = "📝 **Your Current Strategy**\n\n"
|
|
|
|
if conditions:
|
|
response += "**Conditions:**\n"
|
|
for cond in conditions:
|
|
cond_type = cond.get("type", "unknown")
|
|
token = cond.get("token", "")
|
|
threshold = cond.get("threshold", 0)
|
|
timeframe = cond.get("timeframe", "")
|
|
if cond_type == "price_drop":
|
|
response += f"- Buy when {token} drops {threshold}%"
|
|
elif cond_type == "price_rise":
|
|
response += f"- Sell when {token} rises {threshold}%"
|
|
elif cond_type == "volume_spike":
|
|
response += f"- Buy when volume spikes {threshold}%"
|
|
elif cond_type == "price_level":
|
|
response += f"- Buy/sell at price level {threshold}"
|
|
else:
|
|
response += f"- {cond_type}: {token} {threshold}"
|
|
if timeframe:
|
|
response += f" within {timeframe}"
|
|
response += "\n"
|
|
response += "\n"
|
|
|
|
if actions:
|
|
response += "**Actions:**\n"
|
|
for action in actions:
|
|
action_type = action.get("type", "unknown")
|
|
amount = action.get("amount_percent", 0)
|
|
response += (
|
|
f"- {action_type.capitalize()} {amount}% of balance\n"
|
|
)
|
|
response += "\n"
|
|
|
|
if risk:
|
|
response += "**Risk Management:**\n"
|
|
stop_loss = risk.get("stop_loss_percent", 0)
|
|
take_profit = risk.get("take_profit_percent", 0)
|
|
if stop_loss:
|
|
response += f"- Stop loss: {stop_loss}%\n"
|
|
if take_profit:
|
|
response += f"- Take profit: {take_profit}%\n"
|
|
|
|
response += "\nWould you like to modify this strategy?"
|
|
|
|
return {
|
|
"response": response,
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
finally:
|
|
db.close()
|
|
except Exception as e:
|
|
return {
|
|
"response": f"Error fetching strategy: {str(e)}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
def _execute_trending(self) -> Dict[str, Any]:
|
|
"""Execute the trending tokens command and return results."""
|
|
try:
|
|
code, output = self._call_ave_script(
|
|
"trending",
|
|
["--chain", "bsc", "--page-size", "10"],
|
|
)
|
|
if self._is_error_output(code, output):
|
|
return {
|
|
"response": f"Failed to get trending tokens: {output}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
try:
|
|
data = json.loads(output)
|
|
data_field = data.get("data", [])
|
|
if isinstance(data_field, list):
|
|
tokens = data_field
|
|
else:
|
|
tokens = data_field.get("tokens", [])
|
|
if tokens:
|
|
token_list = ""
|
|
for t in tokens[:10]:
|
|
addr = t.get("token", "")
|
|
symbol = t.get("symbol", "")
|
|
name = t.get("name", "")
|
|
price_change = t.get("token_price_change_24h", "N/A")
|
|
mc = t.get("market_cap", "N/A")
|
|
try:
|
|
mc_str = f"${float(mc):,.0f}"
|
|
except (ValueError, TypeError):
|
|
mc_str = str(mc)
|
|
token_list += f"- **{symbol}** ({name}): `{addr}` - MC: {mc_str} - 24h: {price_change}%\n"
|
|
return {
|
|
"response": f"📈 **Trending Tokens on BSC:**\n\n{token_list}\nWould you like me to set up a strategy for any of these?",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
else:
|
|
return {
|
|
"response": "No trending tokens found on BSC right now. Try again later!",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
except json.JSONDecodeError:
|
|
return {
|
|
"response": f"Failed to parse trending data: {output[:200]}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"response": f"Error getting trending tokens: {str(e)}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
def _execute_search(self, keyword: str) -> Dict[str, Any]:
|
|
"""Execute search with the given keyword."""
|
|
try:
|
|
code, output = self._call_ave_script(
|
|
"search",
|
|
["--keyword", keyword.strip(), "--chain", "bsc", "--limit", "10"],
|
|
)
|
|
if self._is_error_output(code, output):
|
|
return {
|
|
"response": f"Failed to search tokens: {output}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
try:
|
|
data = json.loads(output)
|
|
data_field = data.get("data", [])
|
|
if isinstance(data_field, list):
|
|
tokens = data_field
|
|
else:
|
|
tokens = data_field.get("tokens", [])
|
|
if tokens:
|
|
self.recent_search_results = []
|
|
token_list = ""
|
|
for t in tokens[:10]:
|
|
addr = t.get("token", "")
|
|
symbol = t.get("symbol", "")
|
|
name = t.get("name", "")
|
|
price_change = (
|
|
t.get("price_change_24h")
|
|
or t.get("token_price_change_24h")
|
|
or "N/A"
|
|
)
|
|
mc = t.get("market_cap", "N/A")
|
|
if addr and symbol:
|
|
self.recent_search_results.append(
|
|
{
|
|
"symbol": symbol,
|
|
"name": name,
|
|
"address": addr,
|
|
"chain": "bsc",
|
|
}
|
|
)
|
|
try:
|
|
mc_str = f"${float(mc):,.0f}"
|
|
except (ValueError, TypeError):
|
|
mc_str = str(mc)
|
|
token_list += f"- **{symbol}** ({name}): `{addr}` - MC: {mc_str} - 24h: {price_change}%\n"
|
|
return {
|
|
"response": f"🔍 **Search Results for '{keyword}':**\n\n{token_list}\nWould you like me to set up a strategy for any of these?",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
else:
|
|
self.recent_search_results = []
|
|
return {
|
|
"response": f"No tokens found for '{keyword}'. Try a different keyword.",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
except json.JSONDecodeError:
|
|
return {
|
|
"response": f"Failed to parse search results: {output[:200]}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"response": f"Error searching tokens: {str(e)}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
def _execute_risk(self, address: str) -> Dict[str, Any]:
|
|
"""Execute risk analysis for the given token address."""
|
|
try:
|
|
code, output = self._call_ave_script(
|
|
"risk",
|
|
["--address", address.strip(), "--chain", "bsc"],
|
|
)
|
|
if self._is_error_output(code, output):
|
|
return {
|
|
"response": f"Failed to get risk data: {output}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
try:
|
|
data = json.loads(output)
|
|
data_field = data.get("data")
|
|
risk_data = data_field if isinstance(data_field, dict) else {}
|
|
if risk_data:
|
|
is_honeypot = risk_data.get("is_honeypot", "unknown")
|
|
buy_tax = risk_data.get("buy_tax", 0)
|
|
sell_tax = risk_data.get("sell_tax", 0)
|
|
risk_level = risk_data.get("risk_level", 0)
|
|
risk_score = risk_data.get("risk_score", "N/A")
|
|
token_symbol = risk_data.get("token_symbol", "")
|
|
token_name = risk_data.get("token_name", "")
|
|
|
|
if token_symbol:
|
|
token_label = f"**{token_symbol}** ({token_name}) - `{address}`"
|
|
else:
|
|
token_label = f"`{address}`"
|
|
|
|
if isinstance(is_honeypot, bool):
|
|
is_honeypot_str = str(is_honeypot).lower()
|
|
elif isinstance(is_honeypot, int):
|
|
if is_honeypot == 1:
|
|
is_honeypot_str = "true"
|
|
elif is_honeypot == 0:
|
|
is_honeypot_str = "false"
|
|
else:
|
|
is_honeypot_str = "Unknown (could not determine)"
|
|
else:
|
|
is_honeypot_str = (
|
|
str(is_honeypot).lower()
|
|
if is_honeypot
|
|
else "Unknown (could not determine)"
|
|
)
|
|
|
|
try:
|
|
buy_tax_val = (
|
|
float(buy_tax) if buy_tax not in (None, "N/A") else 0
|
|
)
|
|
except (ValueError, TypeError):
|
|
buy_tax_val = 0
|
|
try:
|
|
sell_tax_val = (
|
|
float(sell_tax) if sell_tax not in (None, "N/A") else 0
|
|
)
|
|
except (ValueError, TypeError):
|
|
sell_tax_val = 0
|
|
|
|
risk_level_str = (
|
|
"Low"
|
|
if risk_level == 0
|
|
else "Medium"
|
|
if risk_level == 1
|
|
else "High"
|
|
if risk_level == 2
|
|
else "Unknown"
|
|
)
|
|
|
|
risk_text = f"🛡️ **Risk Analysis for {token_label}**\n\n"
|
|
risk_text += (
|
|
f"- Risk Level: {risk_level_str} (Score: {risk_score})\n"
|
|
)
|
|
risk_text += f"- Honeypot: {is_honeypot_str}\n"
|
|
risk_text += f"- Buy Tax: {buy_tax}%\n"
|
|
risk_text += f"- Sell Tax: {sell_tax}%\n"
|
|
|
|
if is_honeypot_str == "true":
|
|
risk_text += "\n⚠️ **Warning: This token appears to be a honeypot. Do not buy!**"
|
|
elif buy_tax_val > 10 or sell_tax_val > 10:
|
|
risk_text += (
|
|
"\n⚠️ **Warning: High tax detected. Trade with caution!**"
|
|
)
|
|
else:
|
|
risk_text += "\n✅ This token appears safe to trade."
|
|
return {
|
|
"response": risk_text,
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
else:
|
|
return {
|
|
"response": f"No risk data available for `{address}`",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
except json.JSONDecodeError:
|
|
return {
|
|
"response": f"Failed to parse risk data: {output[:200]}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"response": f"Error getting risk data: {str(e)}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
def _get_token_info(self, address: str) -> Dict[str, str]:
|
|
"""Get basic token info (symbol, name) without formatting for display."""
|
|
try:
|
|
code, output = self._call_ave_script(
|
|
"token",
|
|
["--address", address.strip(), "--chain", "bsc"],
|
|
)
|
|
if code == 0:
|
|
try:
|
|
data = json.loads(output)
|
|
data_field = data.get("data")
|
|
token_data = data_field if isinstance(data_field, dict) else {}
|
|
token_info = token_data.get("token", token_data)
|
|
symbol = token_info.get("symbol") or token_data.get("symbol")
|
|
name = token_info.get("name") or token_data.get("name")
|
|
return {"symbol": symbol or "", "name": name or ""}
|
|
except (json.JSONDecodeError, AttributeError):
|
|
return {"symbol": "", "name": ""}
|
|
return {"symbol": "", "name": ""}
|
|
except Exception:
|
|
return {"symbol": "", "name": ""}
|
|
|
|
def _execute_token(self, address: str) -> Dict[str, Any]:
|
|
"""Execute token details for the given address."""
|
|
try:
|
|
code, output = self._call_ave_script(
|
|
"token",
|
|
["--address", address.strip(), "--chain", "bsc"],
|
|
)
|
|
if code == 0:
|
|
try:
|
|
data = json.loads(output)
|
|
data_field = data.get("data")
|
|
token_data = data_field if isinstance(data_field, dict) else {}
|
|
token_info = token_data.get("token", token_data)
|
|
symbol = token_info.get("symbol") or token_data.get("symbol")
|
|
name = token_info.get("name") or token_data.get("name")
|
|
if not symbol or symbol == "N/A" or not name or name == "N/A":
|
|
return {
|
|
"response": f"Token not found for `{address}`.",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
price = (
|
|
token_info.get("current_price_usd")
|
|
or token_info.get("price_usd")
|
|
or token_info.get("price")
|
|
or token_data.get("price")
|
|
or "N/A"
|
|
)
|
|
mc = (
|
|
token_info.get("market_cap")
|
|
or token_info.get("fdv")
|
|
or token_data.get("market_cap")
|
|
or "N/A"
|
|
)
|
|
vol = (
|
|
token_info.get("tx_volume_u_24h")
|
|
or token_info.get("volume_24h")
|
|
or token_data.get("volume_24h")
|
|
or "N/A"
|
|
)
|
|
pairs = (
|
|
token_info.get("top_pairs") or token_data.get("top_pairs") or []
|
|
)
|
|
pairs_text = ""
|
|
if pairs:
|
|
pairs_text = "\n**Top Pairs:**\n"
|
|
for p in pairs[:3]:
|
|
liq = p.get("liquidity", "N/A")
|
|
try:
|
|
liq_str = (
|
|
f"${float(liq):,.0f}"
|
|
if liq and liq != "N/A"
|
|
else liq
|
|
)
|
|
except (ValueError, TypeError):
|
|
liq_str = str(liq)
|
|
pairs_text += (
|
|
f"- {p.get('pair', 'N/A')}: {liq_str} liquidity\n"
|
|
)
|
|
try:
|
|
mc_str = f"${float(mc):,.0f}" if mc != "N/A" else mc
|
|
except (ValueError, TypeError):
|
|
mc_str = str(mc)
|
|
try:
|
|
vol_str = f"${float(vol):,.0f}" if vol != "N/A" else vol
|
|
except (ValueError, TypeError):
|
|
vol_str = str(vol)
|
|
return {
|
|
"response": f"🪙 **{symbol}** ({name})\n\nPrice: ${price}\nMarket Cap: {mc_str}\n24h Volume: {vol_str}{pairs_text}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
except json.JSONDecodeError:
|
|
return {
|
|
"response": f"Failed to parse token data: {output[:200]}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
else:
|
|
return {
|
|
"response": f"Failed to get token details: {output}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"response": f"Error getting token details: {str(e)}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
def _execute_price(self, token_ids: str) -> Dict[str, Any]:
|
|
"""Execute price lookup for the given token IDs."""
|
|
try:
|
|
tokens_list = token_ids.replace(",", " ").split()
|
|
if not tokens_list:
|
|
return {
|
|
"response": "No token provided. Please provide a token address (e.g., '0x...-bsc') or use /search to find a token first.",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
token_input = tokens_list[0].lower()
|
|
matched_address = None
|
|
for result in self.recent_search_results:
|
|
if (
|
|
result["symbol"].lower() == token_input
|
|
or result["name"].lower() == token_input
|
|
or result["address"].lower() == token_input
|
|
):
|
|
matched_address = f"{result['address']}-{result['chain']}"
|
|
break
|
|
|
|
price_tokens = [matched_address] if matched_address else tokens_list
|
|
|
|
code, output = self._call_ave_script(
|
|
"price",
|
|
["--tokens"] + price_tokens,
|
|
)
|
|
if self._is_error_output(code, output):
|
|
return {
|
|
"response": f"Failed to get prices: {output}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
try:
|
|
data = json.loads(output)
|
|
prices = data.get("data", {})
|
|
if not isinstance(prices, dict):
|
|
prices = {}
|
|
if prices:
|
|
price_text = "💰 **Token Prices:**\n"
|
|
for token_id, price_data in prices.items():
|
|
price = (
|
|
price_data.get("price", "N/A")
|
|
if isinstance(price_data, dict)
|
|
else "N/A"
|
|
)
|
|
change_24h = (
|
|
price_data.get("token_price_change_24h", "N/A")
|
|
if isinstance(price_data, dict)
|
|
else "N/A"
|
|
)
|
|
mc = (
|
|
price_data.get("market_cap", "N/A")
|
|
if isinstance(price_data, dict)
|
|
else "N/A"
|
|
)
|
|
try:
|
|
price_str = (
|
|
f"${float(price):,.6f}"
|
|
if price and price != "N/A"
|
|
else price
|
|
)
|
|
except (ValueError, TypeError):
|
|
price_str = str(price) if price else "N/A"
|
|
try:
|
|
mc_str = f"${float(mc):,.0f}" if mc and mc != "N/A" else mc
|
|
except (ValueError, TypeError):
|
|
mc_str = str(mc) if mc else "N/A"
|
|
price_text += f"- **{token_id}**: {price_str} (MC: {mc_str})\n"
|
|
return {
|
|
"response": price_text,
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
else:
|
|
if matched_address:
|
|
return {
|
|
"response": f"No price data available for {matched_address}. Try using /search to find the token first.",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
return {
|
|
"response": "No price data available. The /price tool requires a token contract address (e.g., '0x6982508145454Ce125dDE157d8d64a26D53f60a2-bsc'). Use /search to find a token first, then use its contract address with /price.",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
except json.JSONDecodeError:
|
|
return {
|
|
"response": f"Failed to parse price data: {output[:200]}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"response": f"Error getting prices: {str(e)}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
def _execute_backtest_direct(self, message: str) -> Dict[str, Any]:
|
|
"""Execute backtest directly using token from strategy or message."""
|
|
parts = message.split()
|
|
token_address = None
|
|
timeframe = "1d"
|
|
start_date = None
|
|
end_date = None
|
|
|
|
for i, part in enumerate(parts[1:], 1):
|
|
if part.startswith("0x") and len(part) > 20:
|
|
token_address = part
|
|
elif part in ["1d", "4h", "1h", "15m"]:
|
|
timeframe = part
|
|
elif part.startswith("20") and len(part) == 10:
|
|
if not start_date:
|
|
start_date = part
|
|
else:
|
|
end_date = part
|
|
|
|
if not token_address and self.bot_id:
|
|
try:
|
|
from ...core.database import get_db
|
|
|
|
db = next(get_db())
|
|
try:
|
|
bot = db.query(Bot).filter(Bot.id == self.bot_id).first()
|
|
if bot and bot.strategy_config:
|
|
conditions = bot.strategy_config.get("conditions", [])
|
|
for cond in conditions:
|
|
addr = cond.get("token_address")
|
|
if addr:
|
|
token_address = addr
|
|
break
|
|
finally:
|
|
db.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if not token_address:
|
|
return {
|
|
"response": "📊 **Backtest**\n\nI need a token address to run a backtest. Please provide:\n- Token contract address (e.g., `0x...`)\n- Timeframe (1d, 4h, 1h, 15m) - default is 1d\n- Start and end dates (YYYY-MM-DD) - optional, defaults to last 30 days",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
token_info = self._get_token_info(token_address)
|
|
token_label = f"`{token_address}`"
|
|
if token_info.get("symbol"):
|
|
token_label = f"**{token_info['symbol']}** ({token_info.get('name', 'Unknown')}) - `{token_address}`"
|
|
|
|
result = self._execute_backtest(
|
|
token_address=token_address,
|
|
timeframe=timeframe,
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
)
|
|
return {
|
|
"response": f"📊 **Backtest for {token_label}**\n\n{result}",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
def _execute_simulate_direct(self, message: str) -> Dict[str, Any]:
|
|
"""Execute simulate directly using token from strategy or message."""
|
|
parts = message.split()
|
|
action = None
|
|
token_address = None
|
|
kline_interval = "1m"
|
|
|
|
for i, part in enumerate(parts[1:], 1):
|
|
if part in ["start", "stop", "status", "results"]:
|
|
action = part
|
|
elif part.startswith("0x") and len(part) > 20:
|
|
token_address = part
|
|
elif part in ["1m", "5m", "15m", "1h", "4h"]:
|
|
kline_interval = part
|
|
|
|
if not token_address and self.bot_id and action == "start":
|
|
try:
|
|
from ...core.database import get_db
|
|
|
|
db = next(get_db())
|
|
try:
|
|
bot = db.query(Bot).filter(Bot.id == self.bot_id).first()
|
|
if bot and bot.strategy_config:
|
|
conditions = bot.strategy_config.get("conditions", [])
|
|
for cond in conditions:
|
|
addr = cond.get("token_address")
|
|
if addr:
|
|
token_address = addr
|
|
break
|
|
finally:
|
|
db.close()
|
|
except Exception:
|
|
pass
|
|
|
|
if action == "start" and not token_address:
|
|
return {
|
|
"response": "🎮 **Simulation**\n\nI need a token address to start a simulation. Please provide:\n- Token contract address (e.g., `0x...`)\n- Kline interval (1m, 5m, 15m, 1h, 4h) - default is 1m",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
if not action:
|
|
return {
|
|
"response": "🎮 **Simulation**\n\nPlease specify an action:\n- `/simulate start [token_address]` - Start new simulation\n- `/simulate stop` - Stop running simulation\n- `/simulate status` - Check simulation status\n- `/simulate results` - Get simulation results",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
result = self._manage_simulation(
|
|
action=action,
|
|
token_address=token_address,
|
|
kline_interval=kline_interval,
|
|
)
|
|
return {
|
|
"response": result,
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
def chat(
|
|
self, user_message: str, conversation_history: List[Dict] = None
|
|
) -> Dict[str, Any]:
|
|
"""Process a user message and return a structured response."""
|
|
try:
|
|
if user_message.startswith("/"):
|
|
result = self._handle_slash_command(user_message)
|
|
if result is not None:
|
|
return result
|
|
|
|
if self.pending_command:
|
|
pending = self.pending_command
|
|
self.pending_command = None
|
|
|
|
if pending == "search":
|
|
return self._execute_search(user_message)
|
|
elif pending == "risk":
|
|
return self._execute_risk(user_message)
|
|
elif pending == "token":
|
|
return self._execute_token(user_message)
|
|
elif pending == "price":
|
|
return self._execute_price(user_message)
|
|
|
|
if user_message.startswith("/backtest"):
|
|
return self._execute_backtest_direct(user_message)
|
|
elif user_message.startswith("/simulate"):
|
|
return self._execute_simulate_direct(user_message)
|
|
|
|
messages = [{"role": "user", "content": user_message}]
|
|
|
|
if conversation_history:
|
|
for msg in conversation_history[-10:]:
|
|
role = "assistant" if msg.get("role") == "assistant" else "user"
|
|
messages.insert(
|
|
0, {"role": role, "content": msg.get("content", "")}
|
|
)
|
|
|
|
resp = self.client.chat(
|
|
messages=messages,
|
|
system_prompt=SYSTEM_PROMPT_WITH_TOOLS,
|
|
tools=TOOLS,
|
|
)
|
|
|
|
result = resp
|
|
|
|
# Initialize thinking to None to handle cases where API response
|
|
# doesn't have the expected message structure (intermittent issue)
|
|
thinking = None
|
|
|
|
if result.get("choices") and len(result.get("choices", [])) > 0:
|
|
choice = result["choices"][0]
|
|
if "message" in choice:
|
|
message = choice["message"]
|
|
thinking = message.get("reasoning_content")
|
|
|
|
tool_calls = message.get("tool_calls", [])
|
|
if tool_calls:
|
|
for tool_call in tool_calls:
|
|
func = tool_call.get("function", {})
|
|
func_name = func.get("name", "")
|
|
args = json.loads(func.get("arguments", "{}"))
|
|
|
|
if func_name == "search_tokens":
|
|
keyword = args.get("keyword", "")
|
|
limit = args.get("limit", 10)
|
|
|
|
code, output = self._call_ave_script(
|
|
"search",
|
|
[
|
|
"--keyword",
|
|
keyword,
|
|
"--chain",
|
|
"bsc",
|
|
"--limit",
|
|
str(limit),
|
|
],
|
|
)
|
|
if code == 0:
|
|
try:
|
|
data = json.loads(output)
|
|
data_field = data.get("data", [])
|
|
if isinstance(data_field, list):
|
|
tokens = data_field
|
|
else:
|
|
tokens = data_field.get("tokens", [])
|
|
if tokens:
|
|
token_list = ""
|
|
for t in tokens[:limit]:
|
|
addr = t.get("token", "")
|
|
symbol = t.get("symbol", "")
|
|
name = t.get("name", "")
|
|
price_change = (
|
|
t.get("price_change_24h")
|
|
or t.get("token_price_change_24h")
|
|
or "N/A"
|
|
)
|
|
mc = t.get("market_cap", "N/A")
|
|
try:
|
|
mc_str = f"${float(mc):,.0f}"
|
|
except (ValueError, TypeError):
|
|
mc_str = str(mc)
|
|
token_list += f"- **{symbol}** ({name}): `{addr}` - MC: {mc_str} - 24h: {price_change}%\n"
|
|
response_text = f"Here are the search results for '{keyword}' on BSC:\n\n{token_list}\nWould you like me to set up a strategy for any of these?"
|
|
else:
|
|
response_text = f"No tokens found for '{keyword}'. Try a different keyword."
|
|
except json.JSONDecodeError:
|
|
response_text = (
|
|
"Failed to parse search results."
|
|
)
|
|
else:
|
|
response_text = f"Failed to search tokens: {output}"
|
|
|
|
return {
|
|
"response": response_text,
|
|
"thinking": thinking,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
elif func_name == "get_token":
|
|
address = args.get("address", "")
|
|
chain = args.get("chain", "bsc")
|
|
|
|
code, output = self._call_ave_script(
|
|
"token", ["--address", address, "--chain", chain]
|
|
)
|
|
if code == 0:
|
|
try:
|
|
data = json.loads(output)
|
|
data_field = data.get("data")
|
|
token_data = (
|
|
data_field
|
|
if isinstance(data_field, dict)
|
|
else {}
|
|
)
|
|
token_info = token_data.get("token", token_data)
|
|
symbol = token_info.get(
|
|
"symbol"
|
|
) or token_data.get("symbol")
|
|
name = token_info.get("name") or token_data.get(
|
|
"name"
|
|
)
|
|
if (
|
|
not symbol
|
|
or symbol == "N/A"
|
|
or not name
|
|
or name == "N/A"
|
|
):
|
|
response_text = f"Token not found for {address}. Raw response: {output[:500]}"
|
|
else:
|
|
price = (
|
|
token_info.get("current_price_usd")
|
|
or token_info.get("price_usd")
|
|
or token_info.get("price")
|
|
or token_data.get("price")
|
|
or "N/A"
|
|
)
|
|
mc = (
|
|
token_info.get("market_cap")
|
|
or token_info.get("fdv")
|
|
or token_data.get("market_cap")
|
|
or "N/A"
|
|
)
|
|
vol = (
|
|
token_info.get("tx_volume_u_24h")
|
|
or token_info.get("volume_24h")
|
|
or token_data.get("volume_24h")
|
|
or "N/A"
|
|
)
|
|
pairs = (
|
|
token_info.get("top_pairs")
|
|
or token_data.get("top_pairs")
|
|
or []
|
|
)
|
|
pairs_text = ""
|
|
if pairs:
|
|
pairs_text = "\n**Top Pairs:**\n"
|
|
for p in pairs[:3]:
|
|
liq = p.get("liquidity", "N/A")
|
|
try:
|
|
liq_str = f"${float(liq):,.0f}"
|
|
except (ValueError, TypeError):
|
|
liq_str = str(liq)
|
|
pairs_text += f"- {p.get('pair', 'N/A')}: {liq_str} liquidity\n"
|
|
try:
|
|
mc_str = (
|
|
f"${float(mc):,.0f}"
|
|
if mc != "N/A"
|
|
else "N/A"
|
|
)
|
|
except (ValueError, TypeError):
|
|
mc_str = str(mc)
|
|
try:
|
|
vol_str = (
|
|
f"${float(vol):,.0f}"
|
|
if vol != "N/A"
|
|
else "N/A"
|
|
)
|
|
except (ValueError, TypeError):
|
|
vol_str = str(vol)
|
|
response_text = f"**{symbol}** ({name})\n\nPrice: ${price}\nMarket Cap: {mc_str}\n24h Volume: {vol_str}{pairs_text}"
|
|
except json.JSONDecodeError:
|
|
response_text = "Failed to parse token data."
|
|
else:
|
|
response_text = (
|
|
f"Failed to get token details: {output}"
|
|
)
|
|
|
|
return {
|
|
"response": response_text,
|
|
"thinking": thinking,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
elif func_name == "get_price":
|
|
token_ids = args.get("token_ids", "")
|
|
|
|
tokens_list = token_ids.replace(",", " ").split()
|
|
if not tokens_list:
|
|
response_text = "No token IDs provided."
|
|
else:
|
|
code, output = self._call_ave_script(
|
|
"price", ["--tokens"] + tokens_list
|
|
)
|
|
if code == 0:
|
|
try:
|
|
data = json.loads(output)
|
|
prices = data.get("data", {})
|
|
if not isinstance(prices, dict):
|
|
prices = {}
|
|
if prices:
|
|
price_text = "**Token Prices:**\n"
|
|
for (
|
|
token_id,
|
|
price_data,
|
|
) in prices.items():
|
|
price = price_data.get(
|
|
"price", "N/A"
|
|
)
|
|
change_24h = price_data.get(
|
|
"token_price_change_24h", "N/A"
|
|
)
|
|
price_text += f"- {token_id}: ${price} (24h: {change_24h}%)\n"
|
|
response_text = price_text
|
|
else:
|
|
response_text = (
|
|
"No price data available."
|
|
)
|
|
except json.JSONDecodeError:
|
|
response_text = (
|
|
"Failed to parse price data."
|
|
)
|
|
else:
|
|
response_text = (
|
|
f"Failed to get prices: {output}"
|
|
)
|
|
|
|
return {
|
|
"response": response_text,
|
|
"thinking": thinking,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
elif func_name == "get_risk":
|
|
address = args.get("address", "")
|
|
chain = args.get("chain", "bsc")
|
|
|
|
code, output = self._call_ave_script(
|
|
"risk", ["--address", address, "--chain", chain]
|
|
)
|
|
if code == 0:
|
|
try:
|
|
data = json.loads(output)
|
|
data_field = data.get("data")
|
|
risk_data = (
|
|
data_field
|
|
if isinstance(data_field, dict)
|
|
else {}
|
|
)
|
|
if risk_data:
|
|
is_honeypot = risk_data.get(
|
|
"is_honeypot", "unknown"
|
|
)
|
|
buy_tax = risk_data.get("buy_tax", 0)
|
|
sell_tax = risk_data.get("sell_tax", 0)
|
|
status = risk_data.get("status", "unknown")
|
|
if isinstance(is_honeypot, bool):
|
|
is_honeypot_str = str(
|
|
is_honeypot
|
|
).lower()
|
|
elif isinstance(is_honeypot, int):
|
|
if is_honeypot == 1:
|
|
is_honeypot_str = "true"
|
|
elif is_honeypot == 0:
|
|
is_honeypot_str = "false"
|
|
else:
|
|
is_honeypot_str = "unknown"
|
|
else:
|
|
is_honeypot_str = (
|
|
str(is_honeypot).lower()
|
|
if is_honeypot
|
|
else "unknown"
|
|
)
|
|
|
|
if is_honeypot_str == "unknown":
|
|
honeypot_display = (
|
|
"Unknown (could not determine)"
|
|
)
|
|
else:
|
|
honeypot_display = is_honeypot_str
|
|
try:
|
|
buy_tax_val = (
|
|
float(buy_tax)
|
|
if buy_tax not in (None, "N/A")
|
|
else 0
|
|
)
|
|
except (ValueError, TypeError):
|
|
buy_tax_val = 0
|
|
try:
|
|
sell_tax_val = (
|
|
float(sell_tax)
|
|
if sell_tax not in (None, "N/A")
|
|
else 0
|
|
)
|
|
except (ValueError, TypeError):
|
|
sell_tax_val = 0
|
|
risk_text = (
|
|
f"**Risk Analysis for {address}**\n\n"
|
|
)
|
|
risk_text += f"- Status: {status}\n"
|
|
risk_text += (
|
|
f"- Honeypot: {honeypot_display}\n"
|
|
)
|
|
risk_text += f"- Buy Tax: {buy_tax}%\n"
|
|
risk_text += f"- Sell Tax: {sell_tax}%\n"
|
|
if is_honeypot_str == "true":
|
|
risk_text += "\n⚠️ **Warning: This token appears to be a honeypot. Do not buy!**"
|
|
elif buy_tax_val > 10 or sell_tax_val > 10:
|
|
risk_text += "\n⚠️ **Warning: High tax detected. Trade with caution!**"
|
|
else:
|
|
risk_text += "\n✅ This token appears safe to trade."
|
|
response_text = risk_text
|
|
else:
|
|
response_text = (
|
|
f"No risk data available for {address}"
|
|
)
|
|
except json.JSONDecodeError:
|
|
response_text = "Failed to parse risk data."
|
|
else:
|
|
response_text = f"Failed to get risk data: {output}"
|
|
|
|
return {
|
|
"response": response_text,
|
|
"thinking": thinking,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
elif func_name == "get_trending":
|
|
chain = args.get("chain", "bsc")
|
|
limit = args.get("limit", 10)
|
|
|
|
code, output = self._call_ave_script(
|
|
"trending",
|
|
[
|
|
"--chain",
|
|
chain,
|
|
"--page-size",
|
|
str(min(limit, 50)),
|
|
],
|
|
)
|
|
if code == 0:
|
|
try:
|
|
data = json.loads(output)
|
|
data_field = data.get("data")
|
|
tokens = (
|
|
data_field
|
|
if isinstance(data_field, list)
|
|
else data_field.get("tokens", [])
|
|
)
|
|
if tokens:
|
|
token_list = ""
|
|
for t in tokens[:limit]:
|
|
addr = t.get("token", "")
|
|
symbol = t.get("symbol", "")
|
|
name = t.get("name", "")
|
|
price_change = t.get(
|
|
"token_price_change_24h", "N/A"
|
|
)
|
|
mc = t.get("market_cap", "N/A")
|
|
try:
|
|
mc_str = f"${float(mc):,.0f}"
|
|
except (ValueError, TypeError):
|
|
mc_str = str(mc)
|
|
token_list += f"- **{symbol}** ({name}): `{addr}` - MC: {mc_str} - 24h: {price_change}%\n"
|
|
response_text = f"🔥 Trending tokens on {chain.upper()}:\n\n{token_list}\nWould you like me to set up a strategy for any of these?"
|
|
else:
|
|
response_text = f"No trending tokens found on {chain.upper()}."
|
|
except json.JSONDecodeError:
|
|
response_text = "Failed to parse trending data."
|
|
else:
|
|
response_text = (
|
|
f"Failed to get trending tokens: {output}"
|
|
)
|
|
|
|
return {
|
|
"response": response_text,
|
|
"thinking": thinking,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
elif func_name == "run_backtest":
|
|
token_address = args.get("token_address")
|
|
timeframe = args.get("timeframe", "1d")
|
|
start_date = args.get("start_date")
|
|
end_date = args.get("end_date")
|
|
|
|
backtest_result = self._execute_backtest(
|
|
token_address=token_address,
|
|
timeframe=timeframe,
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
)
|
|
|
|
return {
|
|
"response": backtest_result,
|
|
"thinking": thinking,
|
|
"strategy_updated": False,
|
|
"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")
|
|
|
|
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,
|
|
}
|
|
|
|
content = (
|
|
result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
)
|
|
|
|
thinking_field = None
|
|
response_text = content
|
|
strategy_update = None
|
|
|
|
json_match = re.search(
|
|
r"```(?:json)?\s*(\{.*?\})\s*```", content, re.DOTALL
|
|
)
|
|
if json_match:
|
|
json_str = json_match.group(1)
|
|
else:
|
|
json_match = re.search(r"\{.*\}", content, re.DOTALL)
|
|
if json_match:
|
|
json_str = json_match.group(0)
|
|
else:
|
|
json_str = None
|
|
|
|
if json_str:
|
|
try:
|
|
parsed = json.loads(json_str)
|
|
thinking_field = parsed.get("thinking", "")
|
|
response_text = parsed.get("response", content)
|
|
strategy_update = parsed.get("strategy_update")
|
|
|
|
tool_call = parsed.get("tool_call")
|
|
if tool_call and tool_call.get("name") == "search_tokens":
|
|
args = tool_call.get("arguments", {})
|
|
keyword = args.get("keyword", "")
|
|
limit = args.get("limit", 10)
|
|
|
|
code, output = self._call_ave_script(
|
|
"search",
|
|
[
|
|
"--keyword",
|
|
keyword,
|
|
"--chain",
|
|
"bsc",
|
|
"--limit",
|
|
str(limit),
|
|
],
|
|
)
|
|
if code == 0:
|
|
try:
|
|
data = json.loads(output)
|
|
data_field = data.get("data", [])
|
|
if isinstance(data_field, list):
|
|
tokens = data_field
|
|
else:
|
|
tokens = data_field.get("tokens", [])
|
|
if tokens:
|
|
token_list = ""
|
|
for t in tokens[:limit]:
|
|
addr = t.get("token", "")
|
|
symbol = t.get("symbol", "")
|
|
name = t.get("name", "")
|
|
price_change = t.get(
|
|
"token_price_change_24h", "N/A"
|
|
)
|
|
mc = t.get("market_cap", "N/A")
|
|
try:
|
|
mc_str = f"${float(mc):,.0f}"
|
|
except (ValueError, TypeError):
|
|
mc_str = str(mc)
|
|
token_list += f"- **{symbol}** ({name}): `{addr}` - MC: {mc_str} - 24h: {price_change}%\n"
|
|
response_text = f"Here are the search results for '{keyword}' on BSC:\n\n{token_list}\nWould you like me to set up a strategy for any of these?"
|
|
else:
|
|
response_text = f"No tokens found for '{keyword}'. Try a different keyword."
|
|
except json.JSONDecodeError:
|
|
response_text = "Failed to parse search results."
|
|
else:
|
|
response_text = f"Failed to search tokens: {output}"
|
|
|
|
strategy_update = None
|
|
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
final_thinking = thinking or thinking_field
|
|
|
|
strategy_needs_confirmation = False
|
|
token_search_results = None
|
|
|
|
if strategy_update:
|
|
token_name = None
|
|
for cond in strategy_update.get("conditions") or []:
|
|
if not cond.get("token_address") and cond.get("token"):
|
|
token_name = cond.get("token")
|
|
strategy_needs_confirmation = True
|
|
break
|
|
|
|
if strategy_needs_confirmation and token_name:
|
|
try:
|
|
code, output = self._call_ave_script(
|
|
"search",
|
|
["--keyword", token_name, "--chain", "bsc", "--limit", "5"],
|
|
)
|
|
if code == 0:
|
|
data = json.loads(output)
|
|
data_field = data.get("data", [])
|
|
if isinstance(data_field, list):
|
|
tokens = data_field
|
|
else:
|
|
tokens = data_field.get("tokens", [])
|
|
if tokens:
|
|
token_search_results = [
|
|
{
|
|
"symbol": t.get("symbol", ""),
|
|
"name": t.get("name", ""),
|
|
"address": t.get("token", ""),
|
|
"chain": t.get("chain", "bsc"),
|
|
}
|
|
for t in tokens
|
|
]
|
|
except Exception as e:
|
|
print(f"Token search error: {e}")
|
|
|
|
if strategy_update and strategy_needs_confirmation:
|
|
return {
|
|
"response": response_text,
|
|
"thinking": final_thinking,
|
|
"strategy_updated": False,
|
|
"strategy_needs_confirmation": True,
|
|
"strategy_data": strategy_update,
|
|
"token_search_results": token_search_results,
|
|
"success": True,
|
|
}
|
|
|
|
if strategy_update and self.bot_id:
|
|
self._update_strategy(strategy_update)
|
|
|
|
return {
|
|
"response": response_text,
|
|
"thinking": final_thinking,
|
|
"strategy_updated": strategy_update is not None,
|
|
"strategy_needs_confirmation": False,
|
|
"success": True,
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"response": f"I encountered an error: {str(e)}. Please try again.",
|
|
"thinking": None,
|
|
"strategy_updated": False,
|
|
"success": False,
|
|
}
|
|
|
|
def _execute_backtest(
|
|
self,
|
|
token_address: str,
|
|
timeframe: str = "1d",
|
|
start_date: str = None,
|
|
end_date: str = None,
|
|
) -> str:
|
|
"""Execute a backtest using the bot's current strategy."""
|
|
try:
|
|
import asyncio
|
|
from ...core.database import get_db
|
|
from ...db.models import Backtest
|
|
from ...services.backtest.engine import BacktestEngine
|
|
from ...core.config import get_settings
|
|
from datetime import datetime
|
|
import uuid
|
|
|
|
settings = get_settings()
|
|
db = next(get_db())
|
|
|
|
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 not end_date:
|
|
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
if not start_date:
|
|
start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
|
|
|
|
backtest_config = {
|
|
"bot_id": self.bot_id,
|
|
"token": token_address,
|
|
"chain": "bsc",
|
|
"timeframe": timeframe,
|
|
"start_date": start_date,
|
|
"end_date": end_date,
|
|
"strategy_config": bot.strategy_config,
|
|
"ave_api_key": settings.AVE_API_KEY,
|
|
"ave_api_plan": settings.AVE_API_PLAN,
|
|
"initial_balance": 10000.0,
|
|
}
|
|
|
|
engine = BacktestEngine(backtest_config)
|
|
results = asyncio.run(engine.run())
|
|
|
|
if "error" in results:
|
|
return f"Backtest failed: {results['error']}"
|
|
|
|
total_return = results.get("total_return", 0)
|
|
win_rate = results.get("win_rate", 0)
|
|
total_trades = results.get("total_trades", 0)
|
|
max_drawdown = results.get("max_drawdown", 0)
|
|
sharpe_ratio = results.get("sharpe_ratio", 0)
|
|
final_balance = results.get("final_balance", 10000)
|
|
|
|
return_emoji = "📈" if total_return >= 0 else "📉"
|
|
return_str = (
|
|
f"+{total_return:.2f}%" if total_return >= 0 else f"{total_return:.2f}%"
|
|
)
|
|
|
|
drawdown_emoji = "⚠️" if abs(max_drawdown) > 10 else "✅"
|
|
|
|
response = f"""Here's the backtest result for {token_address}:
|
|
|
|
**Performance Summary**
|
|
{return_emoji} Total Return: {return_str}
|
|
💰 Final Balance: ${final_balance:,.2f}
|
|
📊 Total Trades: {total_trades}
|
|
🎯 Win Rate: {win_rate:.1f}%
|
|
|
|
**Risk Metrics**
|
|
{drawdown_emoji} Max Drawdown: {max_drawdown:.2f}%
|
|
📉 Sharpe Ratio: {sharpe_ratio:.2f}
|
|
|
|
**Period**: {start_date} to {end_date} ({timeframe})
|
|
|
|
Would you like me to adjust the strategy parameters based on these results?"""
|
|
|
|
return response
|
|
|
|
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:
|
|
import asyncio
|
|
import threading
|
|
import uuid
|
|
from ...core.database import SessionLocal
|
|
from ...services.simulate.engine import SimulateEngine
|
|
from ...core.config import get_settings
|
|
from datetime import datetime
|
|
|
|
db = SessionLocal()
|
|
settings = get_settings()
|
|
|
|
try:
|
|
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?"
|
|
|
|
running_sim = (
|
|
db.query(Simulation)
|
|
.filter(
|
|
Simulation.bot_id == self.bot_id,
|
|
Simulation.status == "running",
|
|
)
|
|
.first()
|
|
)
|
|
|
|
if running_sim:
|
|
self._stop_simulation_db(running_sim.id)
|
|
|
|
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()
|
|
|
|
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,
|
|
}
|
|
|
|
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":
|
|
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)
|
|
|
|
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":
|
|
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":
|
|
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 += (
|
|
"\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:
|
|
from ...core.database import get_db
|
|
|
|
db = next(get_db())
|
|
bot = db.query(Bot).filter(Bot.id == self.bot_id).first()
|
|
if bot:
|
|
bot.strategy_config = strategy_update
|
|
db.commit()
|
|
return True
|
|
except Exception as e:
|
|
print(f"Error updating strategy: {e}")
|
|
return False
|
|
|
|
def _call_ave_script(self, command: str, args: list) -> tuple[int, str]:
|
|
"""Call an ave-cloud-skill CLI script and return (status_code, stdout)."""
|
|
import os
|
|
import subprocess
|
|
from ...core.config import get_settings
|
|
|
|
settings = get_settings()
|
|
repo_root = os.path.dirname(
|
|
os.path.dirname(
|
|
os.path.dirname(
|
|
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
)
|
|
)
|
|
)
|
|
ave_skill_path = os.path.join(
|
|
repo_root, "ave-cloud-skill", "scripts", "ave_data_rest.py"
|
|
)
|
|
|
|
env = os.environ.copy()
|
|
env["AVE_API_KEY"] = settings.AVE_API_KEY
|
|
env["API_PLAN"] = settings.AVE_API_PLAN
|
|
env["AVE_USE_DOCKER"] = "false"
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
["python3", ave_skill_path, command] + args,
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
timeout=30,
|
|
)
|
|
output = result.stdout
|
|
if result.returncode != 0 and result.stderr:
|
|
output = f"{output}\n{result.stderr}".strip()
|
|
return result.returncode, output
|
|
except subprocess.TimeoutExpired:
|
|
return 1, "Error: Command timed out"
|
|
except Exception as e:
|
|
return 1, f"Error: {str(e)}"
|
|
|
|
|
|
def get_conversational_agent(
|
|
api_key: str = None, model: str = None, bot_id: str = None
|
|
) -> ConversationalAgent:
|
|
"""Get or create a ConversationalAgent instance."""
|
|
if api_key is None:
|
|
settings = get_settings()
|
|
api_key = settings.MINIMAX_API_KEY
|
|
if model is None:
|
|
settings = get_settings()
|
|
model = settings.MINIMAX_MODEL
|
|
|
|
return ConversationalAgent(api_key=api_key, model=model, bot_id=bot_id)
|