Compare commits
31 Commits
feb65131fa
...
feat/conve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
765e390b9b | ||
| 21ce282cae | |||
|
|
4fa9b0456a | ||
| af9900d0ba | |||
|
|
b3ab004447 | ||
| d394bc0857 | |||
|
|
dfa806ab53 | ||
| 3493775b7f | |||
|
|
82645dfb3b | ||
| c17fa243a1 | |||
|
|
a55ed9cc04 | ||
| d1408b74b4 | |||
|
|
4197475eed | ||
| 87bac8894a | |||
|
|
bef4479675 | ||
| 75970c57e3 | |||
|
|
f23044465a | ||
| a6e4d28aa7 | |||
|
|
8693946cb8 | ||
| a2f549c056 | |||
|
|
ad6e57655d | ||
| ac5e9d8b81 | |||
|
|
81f3342365 | ||
| 6adad0701d | |||
|
|
405b35c3ba | ||
| dd25d38e7e | |||
|
|
da8327c0e0 | ||
| 8d33ea9a44 | |||
|
|
d81464b869 | ||
| 55b008d4e8 | |||
|
|
04e4c1a487 |
@@ -32,7 +32,7 @@ MINIMAX_API_KEY=your-minimax-api-key
|
||||
|
||||
# MiniMax model to use
|
||||
# Common options: MiniMax-Text-01, MiniMax-M2.1
|
||||
MINIMAX_MODEL=MiniMax-Text-01
|
||||
MINIMAX_MODEL=MiniMax-M2.7
|
||||
|
||||
# =============================================================================
|
||||
# AVE CLOUD API
|
||||
|
||||
@@ -58,7 +58,7 @@ def get_current_user(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED
|
||||
"/register", response_model=Token, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
def register(user: UserCreate, db: Session = Depends(get_db)):
|
||||
existing_user = db.query(User).filter(User.email == user.email).first()
|
||||
@@ -75,7 +75,10 @@ def register(user: UserCreate, db: Session = Depends(get_db)):
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
# Generate and return access token so frontend can proceed immediately
|
||||
access_token = create_access_token(data={"sub": db_user.id})
|
||||
return Token(access_token=access_token, token_type="bearer")
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
|
||||
@@ -16,6 +16,7 @@ from ..db.schemas import (
|
||||
)
|
||||
from ..db.models import Bot, BotConversation, User
|
||||
from ..services.ai_agent.crew import get_trading_crew
|
||||
from ..services.ai_agent.conversational import get_conversational_agent
|
||||
|
||||
router = APIRouter()
|
||||
MAX_BOTS_PER_USER = 3
|
||||
@@ -183,69 +184,45 @@ def chat(
|
||||
.order_by(BotConversation.created_at)
|
||||
.all()
|
||||
)
|
||||
history_for_crew = [
|
||||
history_for_agent = [
|
||||
{"role": conv.role, "content": conv.content}
|
||||
for conv in conversation_history[-10:]
|
||||
]
|
||||
|
||||
user_message = request.message
|
||||
if request.strategy_config:
|
||||
crew = get_trading_crew()
|
||||
result = crew.chat(user_message, history_for_crew)
|
||||
|
||||
# Use ConversationalAgent for natural chat with tool-calling
|
||||
agent = get_conversational_agent(bot_id=bot_id)
|
||||
result = agent.chat(user_message, history_for_agent)
|
||||
|
||||
assistant_content = result.get("response", "I couldn't process your request.")
|
||||
if result.get("success") and result.get("strategy_config"):
|
||||
bot.strategy_config = result["strategy_config"]
|
||||
db.commit()
|
||||
assistant_content = result.get("response", "I couldn't process your request.")
|
||||
|
||||
db_conversation = BotConversation(
|
||||
bot_id=bot_id,
|
||||
role="user",
|
||||
content=user_message,
|
||||
)
|
||||
db.add(db_conversation)
|
||||
# Save conversation
|
||||
db_conversation = BotConversation(
|
||||
bot_id=bot_id,
|
||||
role="user",
|
||||
content=user_message,
|
||||
)
|
||||
db.add(db_conversation)
|
||||
|
||||
db_assistant = BotConversation(
|
||||
bot_id=bot_id,
|
||||
role="assistant",
|
||||
content=assistant_content,
|
||||
)
|
||||
db.add(db_assistant)
|
||||
db.commit()
|
||||
db.refresh(db_assistant)
|
||||
db_assistant = BotConversation(
|
||||
bot_id=bot_id,
|
||||
role="assistant",
|
||||
content=assistant_content,
|
||||
)
|
||||
db.add(db_assistant)
|
||||
db.commit()
|
||||
db.refresh(db_assistant)
|
||||
|
||||
return BotChatResponse(
|
||||
response=assistant_content,
|
||||
strategy_config=result.get("strategy_config"),
|
||||
success=result.get("success", False),
|
||||
)
|
||||
else:
|
||||
crew = get_trading_crew()
|
||||
result = crew.chat(user_message, history_for_crew)
|
||||
# If strategy was updated via tool, refresh bot data
|
||||
if result.get("strategy_updated"):
|
||||
db.refresh(bot)
|
||||
|
||||
assistant_content = result.get("response", "I couldn't process your request.")
|
||||
|
||||
db_conversation = BotConversation(
|
||||
bot_id=bot_id,
|
||||
role="user",
|
||||
content=user_message,
|
||||
)
|
||||
db.add(db_conversation)
|
||||
|
||||
db_assistant = BotConversation(
|
||||
bot_id=bot_id,
|
||||
role="assistant",
|
||||
content=assistant_content,
|
||||
)
|
||||
db.add(db_assistant)
|
||||
db.commit()
|
||||
db.refresh(db_assistant)
|
||||
|
||||
return BotChatResponse(
|
||||
response=assistant_content,
|
||||
strategy_config=result.get("strategy_config"),
|
||||
success=result.get("success", False),
|
||||
)
|
||||
return BotChatResponse(
|
||||
response=assistant_content,
|
||||
strategy_config=bot.strategy_config if result.get("strategy_updated") else None,
|
||||
success=result.get("success", False),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{bot_id}/history", response_model=List[BotConversationResponse])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional, List, Any
|
||||
from pydantic import BaseModel, EmailStr, field_validator
|
||||
from typing import Optional, List, Any, Dict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@@ -69,6 +69,13 @@ class BacktestCreate(BaseModel):
|
||||
start_date: str
|
||||
end_date: str
|
||||
|
||||
@field_validator("chain")
|
||||
@classmethod
|
||||
def chain_must_be_bsc(cls, v: str) -> str:
|
||||
if v != "bsc":
|
||||
raise ValueError("Phase 1 only supports BSC (bnb chain)")
|
||||
return v
|
||||
|
||||
|
||||
class BacktestResponse(BaseModel):
|
||||
id: str
|
||||
@@ -90,6 +97,13 @@ class SimulationCreate(BaseModel):
|
||||
check_interval: int = 60
|
||||
auto_execute: bool = False
|
||||
|
||||
@field_validator("chain")
|
||||
@classmethod
|
||||
def chain_must_be_bsc(cls, v: str) -> str:
|
||||
if v != "bsc":
|
||||
raise ValueError("Phase 1 only supports BSC (bnb chain)")
|
||||
return v
|
||||
|
||||
|
||||
class SimulationResponse(BaseModel):
|
||||
id: str
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .crew import CrewAgent
|
||||
from .llm_connector import LLMConnector
|
||||
from .crew import TradingCrew, get_trading_crew
|
||||
from .llm_connector import MiniMaxLLM, MiniMaxConnector
|
||||
|
||||
__all__ = ["CrewAgent", "LLMConnector"]
|
||||
__all__ = ["TradingCrew", "get_trading_crew", "MiniMaxLLM", "MiniMaxConnector"]
|
||||
|
||||
167
src/backend/app/services/ai_agent/conversational.py
Normal file
167
src/backend/app/services/ai_agent/conversational.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Conversational Trading Agent
|
||||
|
||||
This agent can:
|
||||
1. Have normal conversations with users
|
||||
2. Update trading strategies when user provides specific instructions
|
||||
|
||||
Uses CrewAI's tool-calling capabilities for structured updates.
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Dict, Any
|
||||
from crewai import Agent, LLM
|
||||
from crewai.tools import tool
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ...core.config import get_settings
|
||||
from ...db.models import Bot
|
||||
|
||||
|
||||
# Tool definitions
|
||||
@tool
|
||||
def get_current_strategy(bot_id: str) -> str:
|
||||
"""Get the current trading strategy configuration for a bot.
|
||||
|
||||
Use this tool to check the current strategy before making changes.
|
||||
|
||||
Args:
|
||||
bot_id: The ID of the bot to get strategy for
|
||||
|
||||
Returns:
|
||||
JSON string with current strategy configuration
|
||||
"""
|
||||
from ...core.database import get_db
|
||||
from ...db.models import Bot
|
||||
|
||||
db = next(get_db())
|
||||
bot = db.query(Bot).filter(Bot.id == bot_id).first()
|
||||
if not bot:
|
||||
return '{"error": "Bot not found"}'
|
||||
return str(bot.strategy_config)
|
||||
|
||||
|
||||
@tool
|
||||
def update_trading_strategy(
|
||||
bot_id: str,
|
||||
conditions: List[Dict],
|
||||
actions: List[Dict],
|
||||
risk_management: Optional[Dict] = None
|
||||
) -> str:
|
||||
"""Update the trading strategy configuration for a bot.
|
||||
|
||||
Call this tool when the user provides specific trading parameters like:
|
||||
- Buy/sell conditions (price drops, price rises, etc.)
|
||||
- Take profit percentages
|
||||
- Stop loss percentages
|
||||
|
||||
Args:
|
||||
bot_id: The ID of the bot to update
|
||||
conditions: List of trigger conditions (e.g., [{"type": "price_drop", "token": "PEPE", "threshold": 5}])
|
||||
actions: List of actions to take (e.g., [{"type": "buy", "amount_percent": 50}])
|
||||
risk_management: Optional risk settings (e.g., {"stop_loss_percent": 10, "take_profit_percent": 50})
|
||||
|
||||
Returns:
|
||||
Confirmation message with updated strategy
|
||||
"""
|
||||
from ...core.database import get_db
|
||||
from ...db.models import Bot
|
||||
|
||||
db = next(get_db())
|
||||
bot = db.query(Bot).filter(Bot.id == bot_id).first()
|
||||
if not bot:
|
||||
return '{"error": "Bot not found"}'
|
||||
|
||||
new_config = {
|
||||
"conditions": conditions,
|
||||
"actions": actions,
|
||||
}
|
||||
if risk_management:
|
||||
new_config["risk_management"] = risk_management
|
||||
|
||||
bot.strategy_config = new_config
|
||||
db.commit()
|
||||
|
||||
return f'Successfully updated trading strategy. New config: {new_config}'
|
||||
|
||||
|
||||
SYSTEM_PROMPT = """You are a helpful AI trading assistant. You can:
|
||||
|
||||
1. Have normal conversations - answer questions about trading, tokens, strategies, etc.
|
||||
2. Help users configure their trading bots when they provide specific parameters
|
||||
|
||||
When a user asks general questions, just answer conversationally.
|
||||
When a user provides specific trading parameters (like percentages, tokens, conditions),
|
||||
use the update_trading_strategy tool to save their configuration.
|
||||
|
||||
Example conversations:
|
||||
- User: "What is this?" → Answer conversationally about the trading bot platform
|
||||
- User: "I want take profit at 200%" → Use update_trading_strategy with that parameter
|
||||
- User: "Alert me when PEPE drops 5%" → Use update_trading_strategy with that condition
|
||||
|
||||
Be friendly, helpful, and clear in your responses."""
|
||||
|
||||
|
||||
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.llm = LLM(
|
||||
model=model,
|
||||
api_key=api_key,
|
||||
api_base="https://api.minimax.io/v1"
|
||||
)
|
||||
|
||||
# Create agent with tools
|
||||
self.agent = Agent(
|
||||
role="Trading Assistant",
|
||||
goal="Help users with trading strategies and general questions",
|
||||
backstory=SYSTEM_PROMPT,
|
||||
tools=[get_current_strategy, update_trading_strategy],
|
||||
llm=self.llm,
|
||||
verbose=True,
|
||||
allow_delegation=False,
|
||||
)
|
||||
|
||||
def chat(self, user_message: str, conversation_history: List[Dict] = None) -> Dict[str, Any]:
|
||||
"""Process a user message and return a response.
|
||||
|
||||
Args:
|
||||
user_message: The user's message
|
||||
conversation_history: Optional list of previous messages
|
||||
|
||||
Returns:
|
||||
Dict with 'response' (the assistant's reply) and 'strategy_updated' (bool)
|
||||
"""
|
||||
# Execute agent using kickoff
|
||||
try:
|
||||
result = self.agent.kickoff(user_message)
|
||||
|
||||
# Check if strategy was updated
|
||||
result_str = str(result)
|
||||
strategy_updated = "update_trading_strategy" in result_str or \
|
||||
"Successfully updated" in result_str
|
||||
|
||||
return {
|
||||
"response": result_str,
|
||||
"strategy_updated": strategy_updated,
|
||||
"success": True
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"response": f"I encountered an error: {str(e)}. Please try again.",
|
||||
"strategy_updated": False,
|
||||
"success": False
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import List, Optional, Dict, Any
|
||||
from crewai import Agent, Task, Crew
|
||||
from .llm_connector import MiniMaxConnector, MiniMaxLLM
|
||||
from ..core.config import get_settings
|
||||
from crewai import Agent, Task, Crew, LLM
|
||||
from .llm_connector import MiniMaxConnector
|
||||
from ...core.config import get_settings
|
||||
|
||||
|
||||
class StrategyValidator:
|
||||
@@ -33,29 +33,24 @@ class StrategyValidator:
|
||||
errors.append(f"Condition {i}: unsupported type '{cond_type}'")
|
||||
continue
|
||||
|
||||
params = condition.get("params", {})
|
||||
if cond_type in ["price_drop", "price_rise", "volume_spike"]:
|
||||
if "token" not in params:
|
||||
if "token" not in condition:
|
||||
errors.append(f"Condition {i}: missing 'token'")
|
||||
if "threshold_percent" not in params:
|
||||
errors.append(f"Condition {i}: missing 'threshold_percent'")
|
||||
elif not isinstance(params["threshold_percent"], (int, float)):
|
||||
errors.append(
|
||||
f"Condition {i}: 'threshold_percent' must be a number"
|
||||
)
|
||||
elif params["threshold_percent"] <= 0:
|
||||
errors.append(
|
||||
f"Condition {i}: 'threshold_percent' must be positive"
|
||||
)
|
||||
if "threshold" not in condition:
|
||||
errors.append(f"Condition {i}: missing 'threshold'")
|
||||
elif not isinstance(condition["threshold"], (int, float)):
|
||||
errors.append(f"Condition {i}: 'threshold' must be a number")
|
||||
elif condition["threshold"] <= 0:
|
||||
errors.append(f"Condition {i}: 'threshold' must be positive")
|
||||
|
||||
elif cond_type == "price_level":
|
||||
if "token" not in params:
|
||||
if "token" not in condition:
|
||||
errors.append(f"Condition {i}: missing 'token'")
|
||||
if "price" not in params:
|
||||
if "price" not in condition:
|
||||
errors.append(f"Condition {i}: missing 'price'")
|
||||
if "direction" not in params:
|
||||
if "direction" not in condition:
|
||||
errors.append(f"Condition {i}: missing 'direction'")
|
||||
elif params["direction"] not in ["above", "below"]:
|
||||
elif condition["direction"] not in ["above", "below"]:
|
||||
errors.append(
|
||||
f"Condition {i}: direction must be 'above' or 'below'"
|
||||
)
|
||||
@@ -85,23 +80,22 @@ class StrategyExplainer:
|
||||
explanations.append("This strategy will trigger when:")
|
||||
for cond in cond_list:
|
||||
cond_type = cond.get("type")
|
||||
params = cond.get("params", {})
|
||||
token = params.get("token", "the token")
|
||||
token = cond.get("token", "the token")
|
||||
|
||||
if cond_type == "price_drop":
|
||||
pct = params.get("threshold_percent", 0)
|
||||
pct = cond.get("threshold", 0)
|
||||
explanations.append(f" - {token} price drops by {pct}%")
|
||||
elif cond_type == "price_rise":
|
||||
pct = params.get("threshold_percent", 0)
|
||||
pct = cond.get("threshold", 0)
|
||||
explanations.append(f" - {token} price rises by {pct}%")
|
||||
elif cond_type == "volume_spike":
|
||||
pct = params.get("threshold_percent", 0)
|
||||
pct = cond.get("threshold", 0)
|
||||
explanations.append(
|
||||
f" - {token} trading volume increases by {pct}%"
|
||||
)
|
||||
elif cond_type == "price_level":
|
||||
price = params.get("price", 0)
|
||||
direction = params.get("direction", "unknown")
|
||||
price = cond.get("price", 0)
|
||||
direction = cond.get("direction", "unknown")
|
||||
explanations.append(
|
||||
f" - {token} price crosses {direction} ${price}"
|
||||
)
|
||||
@@ -126,7 +120,7 @@ class StrategyExplainer:
|
||||
|
||||
|
||||
def create_trading_designer_agent(
|
||||
api_key: str, model: str = "MiniMax-Text-01"
|
||||
api_key: str, model: str = "MiniMax-M2.7"
|
||||
) -> Agent:
|
||||
connector = MiniMaxConnector(api_key=api_key, model=model)
|
||||
|
||||
@@ -147,13 +141,13 @@ def create_trading_designer_agent(
|
||||
role="Trading Strategy Designer",
|
||||
goal="Convert natural language trading requests into precise strategy configurations",
|
||||
backstory=system_prompt,
|
||||
llm=MiniMaxLLM(api_key=api_key, model=model),
|
||||
llm=LLM(model=model, api_key=api_key, api_base="https://api.minimax.io/v1"),
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
|
||||
def create_strategy_validator_agent(
|
||||
api_key: str, model: str = "MiniMax-Text-01"
|
||||
api_key: str, model: str = "MiniMax-M2.7"
|
||||
) -> Agent:
|
||||
return Agent(
|
||||
role="Strategy Validator",
|
||||
@@ -161,13 +155,13 @@ def create_strategy_validator_agent(
|
||||
backstory="""You are a meticulous strategy validator with expertise in trading systems.
|
||||
You check that all required parameters are present, values are reasonable, and the
|
||||
strategy makes logical sense. You never approve strategies with missing or invalid data.""",
|
||||
llm=MiniMaxLLM(api_key=api_key, model=model),
|
||||
llm=LLM(model=model, api_key=api_key, api_base="https://api.minimax.io/v1"),
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
|
||||
def create_strategy_explainer_agent(
|
||||
api_key: str, model: str = "MiniMax-Text-01"
|
||||
api_key: str, model: str = "MiniMax-M2.7"
|
||||
) -> Agent:
|
||||
return Agent(
|
||||
role="Strategy Explainer",
|
||||
@@ -175,13 +169,13 @@ def create_strategy_explainer_agent(
|
||||
backstory="""You are a patient trading strategy explainer. You translate complex
|
||||
strategy configurations into easy-to-understand language. You help users understand
|
||||
exactly what their strategies will do when triggered.""",
|
||||
llm=MiniMaxLLM(api_key=api_key, model=model),
|
||||
llm=LLM(model=model, api_key=api_key, api_base="https://api.minimax.io/v1"),
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
|
||||
class TradingCrew:
|
||||
def __init__(self, api_key: str, model: str = "MiniMax-Text-01"):
|
||||
def __init__(self, api_key: str, model: str = "MiniMax-M2.7"):
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
self.validator = StrategyValidator()
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
from typing import Optional, List, Dict, Any
|
||||
import httpx
|
||||
from crewai import LLM
|
||||
|
||||
|
||||
class MiniMaxLLM(LLM):
|
||||
def __init__(self, api_key: str, model: str = "MiniMax-Text-01", **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
class MiniMaxLLM:
|
||||
def __init__(self, api_key: str, model: str = "MiniMax-M2.7", **kwargs):
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
self.base_url = "https://api.minimax.chat/v1"
|
||||
self.base_url = "https://api.minimax.io/v1"
|
||||
|
||||
def _call(self, messages: List[Dict[str, str]], **kwargs) -> str:
|
||||
headers = {
|
||||
@@ -23,7 +21,7 @@ class MiniMaxLLM(LLM):
|
||||
}
|
||||
with httpx.Client(timeout=60.0) as client:
|
||||
response = client.post(
|
||||
f"{self.base_url}/chat/completions",
|
||||
f"{self.base_url}/text/chatcompletion_v2",
|
||||
headers=headers,
|
||||
json=payload,
|
||||
)
|
||||
@@ -35,7 +33,7 @@ class MiniMaxLLM(LLM):
|
||||
|
||||
|
||||
class MiniMaxConnector:
|
||||
def __init__(self, api_key: str, model: str = "MiniMax-Text-01"):
|
||||
def __init__(self, api_key: str, model: str = "MiniMax-M2.7"):
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
|
||||
@@ -61,9 +59,9 @@ class MiniMaxConnector:
|
||||
system_prompt = """You are a trading strategy designer. Parse the user's natural language request into a JSON strategy_config object.
|
||||
|
||||
Supported conditions (MVP):
|
||||
- price_drop: Token price drops by X% (requires: token, threshold_percent)
|
||||
- price_rise: Token price rises by X% (requires: token, threshold_percent)
|
||||
- volume_spike: Trading volume increases X% (requires: token, threshold_percent)
|
||||
- price_drop: Token price drops by X% (requires: token, threshold)
|
||||
- price_rise: Token price rises by X% (requires: token, threshold)
|
||||
- volume_spike: Trading volume increases X% (requires: token, threshold)
|
||||
- price_level: Price crosses above/below X (requires: token, price, direction)
|
||||
|
||||
Output ONLY valid JSON with this schema:
|
||||
@@ -71,18 +69,17 @@ Output ONLY valid JSON with this schema:
|
||||
"conditions": [
|
||||
{
|
||||
"type": "price_drop|price_rise|volume_spike|price_level",
|
||||
"params": {
|
||||
"token": "TOKEN_SYMBOL",
|
||||
"threshold_percent": number, // for price_drop, price_rise, volume_spike
|
||||
"price": number, // for price_level
|
||||
"direction": "above|below" // for price_level
|
||||
}
|
||||
"token": "TOKEN_SYMBOL",
|
||||
"chain": "bsc",
|
||||
"threshold": number, // for price_drop, price_rise, volume_spike
|
||||
"price": number, // for price_level
|
||||
"direction": "above|below", // for price_level
|
||||
"timeframe": "1h"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "buy|sell|notify",
|
||||
"params": {}
|
||||
"type": "buy|sell|notify"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -90,6 +90,22 @@ class AveCloudClient:
|
||||
return data.get("data", [])
|
||||
raise Exception(f"Failed to fetch klines: {data}")
|
||||
|
||||
async def get_token_price(self, token_id: str) -> Optional[Dict[str, Any]]:
|
||||
url = f"{self.DATA_API_URL}/v2/tokens/price"
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
url,
|
||||
headers=self._data_headers(),
|
||||
json={"token_ids": [token_id]},
|
||||
timeout=30.0,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get("status") == 200:
|
||||
prices = data.get("data", {})
|
||||
return prices.get(token_id)
|
||||
return None
|
||||
|
||||
async def get_trending_tokens(
|
||||
self, chain: Optional[str] = None, limit: int = 20
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import httpx
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AveCloudClient:
|
||||
BASE_URL = "https://prod.ave-api.com"
|
||||
|
||||
def __init__(self, api_key: str, plan: str = "free"):
|
||||
self.api_key = api_key
|
||||
self.plan = plan
|
||||
|
||||
def _headers(self) -> Dict[str, str]:
|
||||
return {"X-API-KEY": self.api_key}
|
||||
|
||||
async def get_klines(
|
||||
self,
|
||||
token_id: str,
|
||||
interval: str = "1h",
|
||||
limit: int = 100,
|
||||
start_time: Optional[int] = None,
|
||||
end_time: Optional[int] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
url = f"{self.BASE_URL}/v2/klines/token/{token_id}"
|
||||
params = {"interval": interval, "limit": limit}
|
||||
if start_time:
|
||||
params["start_time"] = start_time
|
||||
if end_time:
|
||||
params["end_time"] = end_time
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
url, headers=self._headers(), params=params, timeout=30.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get("status") == 200:
|
||||
return data.get("data", [])
|
||||
raise Exception(f"Failed to fetch klines: {data}")
|
||||
|
||||
async def get_token_price(self, token_id: str) -> Optional[Dict[str, Any]]:
|
||||
url = f"{self.BASE_URL}/v2/tokens/price"
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
url,
|
||||
headers=self._headers(),
|
||||
json={"token_ids": [token_id]},
|
||||
timeout=30.0,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get("status") == 200:
|
||||
prices = data.get("data", {})
|
||||
return prices.get(token_id)
|
||||
return None
|
||||
|
||||
async def get_batch_prices(self, token_ids: List[str]) -> Dict[str, Dict[str, Any]]:
|
||||
url = f"{self.BASE_URL}/v2/tokens/price"
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
url,
|
||||
headers=self._headers(),
|
||||
json={"token_ids": token_ids},
|
||||
timeout=30.0,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get("status") == 200:
|
||||
return data.get("data", {})
|
||||
return {}
|
||||
@@ -2,7 +2,7 @@ import uuid
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List, Optional
|
||||
from .ave_client import AveCloudClient
|
||||
from ..ave.client import AveCloudClient
|
||||
|
||||
|
||||
class BacktestEngine:
|
||||
@@ -20,10 +20,15 @@ class BacktestEngine:
|
||||
self.strategy_config = config.get("strategy_config", {})
|
||||
self.conditions = self.strategy_config.get("conditions", [])
|
||||
self.actions = self.strategy_config.get("actions", [])
|
||||
self.risk_management = self.strategy_config.get("risk_management", {})
|
||||
self.stop_loss_percent = self.risk_management.get("stop_loss_percent")
|
||||
self.take_profit_percent = self.risk_management.get("take_profit_percent")
|
||||
self.initial_balance = config.get("initial_balance", 10000.0)
|
||||
self.current_balance = self.initial_balance
|
||||
self.position = 0.0
|
||||
self.position_token = ""
|
||||
self.entry_price: Optional[float] = None
|
||||
self.entry_time: Optional[int] = None
|
||||
self.trades: List[Dict[str, Any]] = []
|
||||
self.running = False
|
||||
|
||||
@@ -103,11 +108,73 @@ class BacktestEngine:
|
||||
|
||||
timestamp = kline.get("timestamp", 0)
|
||||
|
||||
if self.position > 0 and self.entry_price is not None:
|
||||
exit_info = self._check_risk_management(price, timestamp)
|
||||
if exit_info:
|
||||
await self._execute_risk_exit(price, timestamp, exit_info)
|
||||
continue
|
||||
|
||||
for condition in self.conditions:
|
||||
if self._check_condition(condition, klines, i, price):
|
||||
await self._execute_actions(price, timestamp, condition)
|
||||
break
|
||||
|
||||
def _check_risk_management(
|
||||
self, current_price: float, timestamp: int
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if self.position <= 0 or self.entry_price is None:
|
||||
return None
|
||||
|
||||
if self.stop_loss_percent is not None:
|
||||
stop_loss_price = self.entry_price * (1 - self.stop_loss_percent / 100)
|
||||
if current_price <= stop_loss_price:
|
||||
return {"reason": "stop_loss", "price": stop_loss_price}
|
||||
|
||||
if self.take_profit_percent is not None:
|
||||
take_profit_price = self.entry_price * (1 + self.take_profit_percent / 100)
|
||||
if current_price >= take_profit_price:
|
||||
return {"reason": "take_profit", "price": take_profit_price}
|
||||
|
||||
return None
|
||||
|
||||
async def _execute_risk_exit(
|
||||
self, price: float, timestamp: int, exit_info: Dict[str, Any]
|
||||
):
|
||||
if self.position <= 0:
|
||||
return
|
||||
|
||||
reason = exit_info["reason"]
|
||||
sell_amount = self.position * price
|
||||
self.current_balance += sell_amount
|
||||
self.trades.append(
|
||||
{
|
||||
"type": "sell",
|
||||
"token": self.position_token,
|
||||
"price": price,
|
||||
"amount": sell_amount,
|
||||
"quantity": self.position,
|
||||
"timestamp": timestamp,
|
||||
"exit_reason": reason,
|
||||
}
|
||||
)
|
||||
self.signals.append(
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"bot_id": self.bot_id,
|
||||
"run_id": self.run_id,
|
||||
"signal_type": "sell",
|
||||
"token": self.position_token,
|
||||
"price": price,
|
||||
"confidence": 1.0,
|
||||
"reasoning": f"Risk management triggered {reason}",
|
||||
"executed": False,
|
||||
"created_at": datetime.utcnow(),
|
||||
}
|
||||
)
|
||||
self.position = 0
|
||||
self.entry_price = None
|
||||
self.entry_time = None
|
||||
|
||||
def _check_condition(
|
||||
self,
|
||||
condition: Dict[str, Any],
|
||||
@@ -173,6 +240,8 @@ class BacktestEngine:
|
||||
self.position += amount / price
|
||||
self.current_balance -= amount
|
||||
self.position_token = token
|
||||
self.entry_price = price
|
||||
self.entry_time = timestamp
|
||||
self.trades.append(
|
||||
{
|
||||
"type": "buy",
|
||||
@@ -209,9 +278,12 @@ class BacktestEngine:
|
||||
"amount": sell_amount,
|
||||
"quantity": self.position,
|
||||
"timestamp": timestamp,
|
||||
"exit_reason": "manual",
|
||||
}
|
||||
)
|
||||
self.position = 0
|
||||
self.entry_price = None
|
||||
self.entry_time = None
|
||||
self.signals.append(
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import uuid
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List, Optional
|
||||
from ..backtest.ave_client import AveCloudClient
|
||||
from ..ave.client import AveCloudClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SimulateEngine:
|
||||
@@ -20,6 +23,9 @@ class SimulateEngine:
|
||||
self.strategy_config = config.get("strategy_config", {})
|
||||
self.conditions = self.strategy_config.get("conditions", [])
|
||||
self.actions = self.strategy_config.get("actions", [])
|
||||
self.risk_management = self.strategy_config.get("risk_management", {})
|
||||
self.stop_loss_percent = self.risk_management.get("stop_loss_percent")
|
||||
self.take_profit_percent = self.risk_management.get("take_profit_percent")
|
||||
self.check_interval = config.get("check_interval", 60)
|
||||
self.duration_seconds = config.get("duration_seconds", 3600)
|
||||
self.auto_execute = config.get("auto_execute", False)
|
||||
@@ -29,6 +35,13 @@ class SimulateEngine:
|
||||
self.started_at: Optional[datetime] = None
|
||||
self.last_price: Optional[float] = None
|
||||
self.last_volume: Optional[float] = None
|
||||
self.position: float = 0.0
|
||||
self.position_token: str = ""
|
||||
self.entry_price: Optional[float] = None
|
||||
self.entry_time: Optional[int] = None
|
||||
self.current_balance: float = config.get("initial_balance", 10000.0)
|
||||
self.trades: List[Dict[str, Any]] = []
|
||||
self.errors: List[str] = []
|
||||
|
||||
async def run(self) -> Dict[str, Any]:
|
||||
self.running = True
|
||||
@@ -65,7 +78,9 @@ class SimulateEngine:
|
||||
self.last_volume = current_volume
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
logger.warning(f"Failed to get price for {token_id}: {e}")
|
||||
self.errors.append(f"Price fetch failed for {token_id}: {str(e)}")
|
||||
continue
|
||||
|
||||
for _ in range(self.check_interval):
|
||||
if not self.running:
|
||||
@@ -83,6 +98,8 @@ class SimulateEngine:
|
||||
|
||||
self.results = self.results or {}
|
||||
self.results["total_signals"] = len(self.signals)
|
||||
self.results["total_errors"] = len(self.errors)
|
||||
self.results["errors"] = self.errors
|
||||
self.results["signals"] = self.signals
|
||||
self.results["started_at"] = self.started_at
|
||||
self.results["ended_at"] = datetime.utcnow()
|
||||
@@ -94,11 +111,70 @@ class SimulateEngine:
|
||||
):
|
||||
timestamp = int(datetime.utcnow().timestamp() * 1000)
|
||||
|
||||
if self.position > 0 and self.entry_price is not None:
|
||||
exit_info = self._check_risk_management(current_price, timestamp)
|
||||
if exit_info:
|
||||
await self._execute_risk_exit(current_price, timestamp, exit_info)
|
||||
return
|
||||
|
||||
for condition in self.conditions:
|
||||
if self._check_condition(condition, current_price, current_volume):
|
||||
await self._execute_actions(current_price, timestamp, condition)
|
||||
break
|
||||
|
||||
def _check_risk_management(
|
||||
self, current_price: float, timestamp: int
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if self.position <= 0 or self.entry_price is None:
|
||||
return None
|
||||
|
||||
if self.stop_loss_percent is not None:
|
||||
stop_loss_price = self.entry_price * (1 - self.stop_loss_percent / 100)
|
||||
if current_price <= stop_loss_price:
|
||||
return {"reason": "stop_loss", "price": stop_loss_price}
|
||||
|
||||
if self.take_profit_percent is not None:
|
||||
take_profit_price = self.entry_price * (1 + self.take_profit_percent / 100)
|
||||
if current_price >= take_profit_price:
|
||||
return {"reason": "take_profit", "price": take_profit_price}
|
||||
|
||||
return None
|
||||
|
||||
async def _execute_risk_exit(
|
||||
self, price: float, timestamp: int, exit_info: Dict[str, Any]
|
||||
):
|
||||
if self.position <= 0:
|
||||
return
|
||||
|
||||
reason = exit_info["reason"]
|
||||
self.trades.append(
|
||||
{
|
||||
"type": "sell",
|
||||
"token": self.position_token,
|
||||
"price": price,
|
||||
"quantity": self.position,
|
||||
"timestamp": timestamp,
|
||||
"exit_reason": reason,
|
||||
}
|
||||
)
|
||||
self.signals.append(
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"bot_id": self.bot_id,
|
||||
"run_id": self.run_id,
|
||||
"signal_type": "sell",
|
||||
"token": self.position_token,
|
||||
"price": price,
|
||||
"confidence": 1.0,
|
||||
"reasoning": f"Risk management triggered {reason}",
|
||||
"executed": self.auto_execute,
|
||||
"created_at": datetime.utcnow(),
|
||||
}
|
||||
)
|
||||
self.position = 0
|
||||
self.entry_price = None
|
||||
self.entry_time = None
|
||||
|
||||
def _check_condition(
|
||||
self,
|
||||
condition: Dict[str, Any],
|
||||
@@ -146,20 +222,41 @@ class SimulateEngine:
|
||||
token = matched_condition.get("token", self.token)
|
||||
reasoning = f"Condition {matched_condition.get('type')} triggered"
|
||||
|
||||
signal = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"bot_id": self.bot_id,
|
||||
"run_id": self.run_id,
|
||||
"signal_type": "signal",
|
||||
"token": token,
|
||||
"price": price,
|
||||
"confidence": 0.8,
|
||||
"reasoning": reasoning,
|
||||
"executed": self.auto_execute,
|
||||
"created_at": datetime.utcnow(),
|
||||
}
|
||||
for action in self.actions:
|
||||
action_type = action.get("type", "")
|
||||
if action_type == "buy":
|
||||
amount_percent = action.get("amount_percent", 10)
|
||||
amount = self.current_balance * (amount_percent / 100)
|
||||
self.position += amount / price
|
||||
self.position_token = token
|
||||
self.entry_price = price
|
||||
self.entry_time = timestamp
|
||||
self.current_balance -= amount
|
||||
self.trades.append(
|
||||
{
|
||||
"type": "buy",
|
||||
"token": token,
|
||||
"price": price,
|
||||
"amount": amount,
|
||||
"quantity": amount / price,
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
)
|
||||
|
||||
self.signals.append(signal)
|
||||
signal = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"bot_id": self.bot_id,
|
||||
"run_id": self.run_id,
|
||||
"signal_type": action_type,
|
||||
"token": token,
|
||||
"price": price,
|
||||
"confidence": 0.8,
|
||||
"reasoning": reasoning,
|
||||
"executed": self.auto_execute,
|
||||
"created_at": datetime.utcnow(),
|
||||
}
|
||||
|
||||
self.signals.append(signal)
|
||||
|
||||
async def stop(self):
|
||||
self.running = False
|
||||
|
||||
@@ -6,6 +6,7 @@ pydantic-settings>=2.1.0
|
||||
email-validator>=2.0.0
|
||||
python-jose[cryptography]>=3.3.0
|
||||
passlib[bcrypt]>=1.7.4
|
||||
bcrypt>=4.0,<5.0 # Required for passlib compatibility
|
||||
crewai>=0.1.0
|
||||
anthropic>=0.18.0
|
||||
httpx>=0.26.0
|
||||
|
||||
@@ -104,11 +104,12 @@ export const api = {
|
||||
}
|
||||
},
|
||||
|
||||
async chat(id: string, message: string): Promise<BotChatResponse> {
|
||||
async chat(id: string, message: string, signal?: AbortSignal): Promise<BotChatResponse> {
|
||||
const response = await fetch(`${API_URL}/bots/${id}/chat`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ message } as BotChatRequest)
|
||||
body: JSON.stringify({ message } as BotChatRequest),
|
||||
signal
|
||||
});
|
||||
return handleResponse<BotChatResponse>(response);
|
||||
},
|
||||
|
||||
@@ -8,12 +8,25 @@ export interface ChatMessage {
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
// Fallback UUID generator for environments where crypto.randomUUID is not available
|
||||
function generateId(): string {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
// Fallback: simple UUID v4 implementation
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
export const chatStore = writable<ChatMessage[]>([]);
|
||||
|
||||
export function addMessage(message: Omit<ChatMessage, 'id' | 'timestamp'>) {
|
||||
const newMessage: ChatMessage = {
|
||||
...message,
|
||||
id: crypto.randomUUID(),
|
||||
id: generateId(),
|
||||
timestamp: new Date()
|
||||
};
|
||||
chatStore.update(messages => [...messages, newMessage]);
|
||||
|
||||
@@ -44,8 +44,17 @@
|
||||
|
||||
isSending = true;
|
||||
|
||||
// Add user's message immediately so it shows even before API response
|
||||
addMessage({ role: 'user', content: message });
|
||||
|
||||
try {
|
||||
const response = await api.bots.chat(botId, message);
|
||||
// Add timeout to prevent hanging requests
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
||||
|
||||
const response = await api.bots.chat(botId, message, controller.signal);
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
addMessage({ role: 'assistant', content: response.response });
|
||||
|
||||
if (response.strategy_config) {
|
||||
@@ -53,7 +62,11 @@
|
||||
setCurrentBot(bot);
|
||||
}
|
||||
} catch (e) {
|
||||
addMessage({ role: 'assistant', content: 'Sorry, I encountered an error. Please try again.' });
|
||||
if (e instanceof Error && e.name === 'AbortError') {
|
||||
addMessage({ role: 'assistant', content: 'Request timed out. Please try again.' });
|
||||
} else {
|
||||
addMessage({ role: 'assistant', content: 'Sorry, I encountered an error. Please try again.' });
|
||||
}
|
||||
} finally {
|
||||
isSending = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user