Compare commits

..

18 Commits

Author SHA1 Message Date
a6e4d28aa7 Merge pull request 'fix: add bcrypt version constraint for passlib compatibility' (#40) from fix/bcrypt-compatibility into main 2026-04-10 02:55:36 +02:00
shokollm
8693946cb8 fix: add bcrypt version constraint for passlib compatibility
bcrypt 5.0.0 is incompatible with passlib 1.7.x - passlib tries to
access bcrypt.__about__.__version__ which was removed in bcrypt 5.x.

Constrain bcrypt to >=4.0,<5.0 to maintain compatibility.
2026-04-10 00:55:18 +00:00
a2f549c056 Merge pull request 'fix: correct import paths in ai_agent module' (#39) from fix/ai-agent-imports into main 2026-04-09 17:32:21 +02:00
shokollm
ad6e57655d fix: correct import paths in ai_agent module
- Fix relative import path in crew.py (from ..core to ...core)
- Update __init__.py exports to match actual class names
- Remove incorrect CrewAgent and LLMConnector exports
2026-04-09 15:27:09 +00:00
ac5e9d8b81 Merge pull request 'fix: add error logging to simulate engine to prevent silent failures' (#38) from fix/issue-30 into main 2026-04-09 12:19:36 +02:00
shokollm
81f3342365 fix: add error logging to simulate engine to prevent silent failures
Errors during price fetching are now logged and stored in an errors list,
allowing users to see error count/warnings in simulation results.

Acceptance Criteria:
- [x] Errors are logged (not silently swallowed)
- [x] User can see error count/warnings in simulation results
- [x] Simulation completes even if some price fetches fail (graceful degradation)
2026-04-09 10:16:22 +00:00
6adad0701d Merge pull request 'fix: consolidate AveCloudClient to single implementation' (#37) from fix/issue-29 into main 2026-04-09 12:11:59 +02:00
shokollm
405b35c3ba fix: consolidate AveCloudClient to single implementation in services/ave/client.py 2026-04-09 10:06:16 +00:00
dd25d38e7e Merge pull request 'feat: implement stop-loss and take-profit risk management' (#36) from fix/issue-28 into main 2026-04-09 11:39:50 +02:00
shokollm
da8327c0e0 feat: implement stop-loss and take-profit in backtest and simulate engines 2026-04-09 09:14:08 +00:00
8d33ea9a44 Merge pull request 'fix: flatten strategy config schema (backtesting broken)' (#35) from fix/issue-25 into main 2026-04-09 09:32:49 +02:00
shokollm
d81464b869 fix: flatten strategy config schema to match engine expectations
LLM was outputting nested params structure but engines expect flat fields.
This caused backtesting and simulation to never trigger any trades.

Changes:
- llm_connector.py: Update prompt to output flat condition structure
- crew.py: Update StrategyValidator to validate flat structure
- crew.py: Update StrategyExplainer to read flat structure

Fixes #25
2026-04-09 07:31:09 +00:00
55b008d4e8 Merge pull request 'fix: validate chain is 'bsc' for Phase 1' (#34) from fix/issue-31 into main 2026-04-09 09:10:55 +02:00
shokollm
04e4c1a487 fix: validate chain is 'bsc' for BacktestCreate and SimulationCreate 2026-04-09 06:58:16 +00:00
feb65131fa Merge pull request 'fix: populate config endpoints with chain and token data' (#33) from fix/issue-27 into main 2026-04-09 08:23:43 +02:00
shokollm
50af4e0722 fix: reduce tokens limit to 20 per review 2026-04-09 06:18:31 +00:00
shokollm
786e964e32 fix: return bsc chain and tokens from AVE API in config endpoints 2026-04-09 06:02:05 +00:00
41b699f9ee Merge pull request 'fix: make strategy_config and llm_config optional in BotCreate' (#32) from fix/issue-26 into main 2026-04-09 07:54:20 +02:00
10 changed files with 258 additions and 129 deletions

View File

@@ -1,13 +1,19 @@
from fastapi import APIRouter from fastapi import APIRouter
from ..core.config import get_settings
from ..services.ave import AveCloudClient
router = APIRouter() router = APIRouter()
@router.get("/chains") @router.get("/chains")
def get_chains(): def get_chains():
return {"chains": []} return {"chains": ["bsc"]}
@router.get("/tokens") @router.get("/tokens")
def get_tokens(): async def get_tokens():
return {"tokens": []} settings = get_settings()
client = AveCloudClient(api_key=settings.AVE_API_KEY, plan=settings.AVE_API_PLAN)
tokens = await client.get_tokens(chain="bsc", limit=20)
return {"tokens": tokens}

View File

@@ -1,5 +1,5 @@
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr, field_validator
from typing import Optional, List, Any from typing import Optional, List, Any, Dict
from datetime import datetime from datetime import datetime
@@ -69,6 +69,13 @@ class BacktestCreate(BaseModel):
start_date: str start_date: str
end_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): class BacktestResponse(BaseModel):
id: str id: str
@@ -90,6 +97,13 @@ class SimulationCreate(BaseModel):
check_interval: int = 60 check_interval: int = 60
auto_execute: bool = False 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): class SimulationResponse(BaseModel):
id: str id: str

View File

@@ -1,4 +1,4 @@
from .crew import CrewAgent from .crew import TradingCrew, get_trading_crew
from .llm_connector import LLMConnector from .llm_connector import MiniMaxLLM, MiniMaxConnector
__all__ = ["CrewAgent", "LLMConnector"] __all__ = ["TradingCrew", "get_trading_crew", "MiniMaxLLM", "MiniMaxConnector"]

View File

@@ -1,7 +1,7 @@
from typing import List, Optional, Dict, Any from typing import List, Optional, Dict, Any
from crewai import Agent, Task, Crew from crewai import Agent, Task, Crew
from .llm_connector import MiniMaxConnector, MiniMaxLLM from .llm_connector import MiniMaxConnector, MiniMaxLLM
from ..core.config import get_settings from ...core.config import get_settings
class StrategyValidator: class StrategyValidator:
@@ -33,29 +33,24 @@ class StrategyValidator:
errors.append(f"Condition {i}: unsupported type '{cond_type}'") errors.append(f"Condition {i}: unsupported type '{cond_type}'")
continue continue
params = condition.get("params", {})
if cond_type in ["price_drop", "price_rise", "volume_spike"]: 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'") errors.append(f"Condition {i}: missing 'token'")
if "threshold_percent" not in params: if "threshold" not in condition:
errors.append(f"Condition {i}: missing 'threshold_percent'") errors.append(f"Condition {i}: missing 'threshold'")
elif not isinstance(params["threshold_percent"], (int, float)): elif not isinstance(condition["threshold"], (int, float)):
errors.append( errors.append(f"Condition {i}: 'threshold' must be a number")
f"Condition {i}: 'threshold_percent' must be a number" elif condition["threshold"] <= 0:
) errors.append(f"Condition {i}: 'threshold' must be positive")
elif params["threshold_percent"] <= 0:
errors.append(
f"Condition {i}: 'threshold_percent' must be positive"
)
elif cond_type == "price_level": elif cond_type == "price_level":
if "token" not in params: if "token" not in condition:
errors.append(f"Condition {i}: missing 'token'") errors.append(f"Condition {i}: missing 'token'")
if "price" not in params: if "price" not in condition:
errors.append(f"Condition {i}: missing 'price'") errors.append(f"Condition {i}: missing 'price'")
if "direction" not in params: if "direction" not in condition:
errors.append(f"Condition {i}: missing 'direction'") errors.append(f"Condition {i}: missing 'direction'")
elif params["direction"] not in ["above", "below"]: elif condition["direction"] not in ["above", "below"]:
errors.append( errors.append(
f"Condition {i}: direction must be 'above' or 'below'" f"Condition {i}: direction must be 'above' or 'below'"
) )
@@ -85,23 +80,22 @@ class StrategyExplainer:
explanations.append("This strategy will trigger when:") explanations.append("This strategy will trigger when:")
for cond in cond_list: for cond in cond_list:
cond_type = cond.get("type") cond_type = cond.get("type")
params = cond.get("params", {}) token = cond.get("token", "the token")
token = params.get("token", "the token")
if cond_type == "price_drop": if cond_type == "price_drop":
pct = params.get("threshold_percent", 0) pct = cond.get("threshold", 0)
explanations.append(f" - {token} price drops by {pct}%") explanations.append(f" - {token} price drops by {pct}%")
elif cond_type == "price_rise": elif cond_type == "price_rise":
pct = params.get("threshold_percent", 0) pct = cond.get("threshold", 0)
explanations.append(f" - {token} price rises by {pct}%") explanations.append(f" - {token} price rises by {pct}%")
elif cond_type == "volume_spike": elif cond_type == "volume_spike":
pct = params.get("threshold_percent", 0) pct = cond.get("threshold", 0)
explanations.append( explanations.append(
f" - {token} trading volume increases by {pct}%" f" - {token} trading volume increases by {pct}%"
) )
elif cond_type == "price_level": elif cond_type == "price_level":
price = params.get("price", 0) price = cond.get("price", 0)
direction = params.get("direction", "unknown") direction = cond.get("direction", "unknown")
explanations.append( explanations.append(
f" - {token} price crosses {direction} ${price}" f" - {token} price crosses {direction} ${price}"
) )

View File

@@ -61,9 +61,9 @@ class MiniMaxConnector:
system_prompt = """You are a trading strategy designer. Parse the user's natural language request into a JSON strategy_config object. system_prompt = """You are a trading strategy designer. Parse the user's natural language request into a JSON strategy_config object.
Supported conditions (MVP): Supported conditions (MVP):
- price_drop: Token price drops by 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_percent) - price_rise: Token price rises by X% (requires: token, threshold)
- volume_spike: Trading volume increases X% (requires: token, threshold_percent) - volume_spike: Trading volume increases X% (requires: token, threshold)
- price_level: Price crosses above/below X (requires: token, price, direction) - price_level: Price crosses above/below X (requires: token, price, direction)
Output ONLY valid JSON with this schema: Output ONLY valid JSON with this schema:
@@ -71,18 +71,17 @@ Output ONLY valid JSON with this schema:
"conditions": [ "conditions": [
{ {
"type": "price_drop|price_rise|volume_spike|price_level", "type": "price_drop|price_rise|volume_spike|price_level",
"params": { "token": "TOKEN_SYMBOL",
"token": "TOKEN_SYMBOL", "chain": "bsc",
"threshold_percent": number, // for price_drop, price_rise, volume_spike "threshold": number, // for price_drop, price_rise, volume_spike
"price": number, // for price_level "price": number, // for price_level
"direction": "above|below" // for price_level "direction": "above|below", // for price_level
} "timeframe": "1h"
} }
], ],
"actions": [ "actions": [
{ {
"type": "buy|sell|notify", "type": "buy|sell|notify"
"params": {}
} }
] ]
} }

View File

@@ -90,6 +90,22 @@ class AveCloudClient:
return data.get("data", []) return data.get("data", [])
raise Exception(f"Failed to fetch klines: {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( async def get_trending_tokens(
self, chain: Optional[str] = None, limit: int = 20 self, chain: Optional[str] = None, limit: int = 20
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:

View File

@@ -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 {}

View File

@@ -2,7 +2,7 @@ import uuid
import asyncio import asyncio
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from .ave_client import AveCloudClient from ..ave.client import AveCloudClient
class BacktestEngine: class BacktestEngine:
@@ -20,10 +20,15 @@ class BacktestEngine:
self.strategy_config = config.get("strategy_config", {}) self.strategy_config = config.get("strategy_config", {})
self.conditions = self.strategy_config.get("conditions", []) self.conditions = self.strategy_config.get("conditions", [])
self.actions = self.strategy_config.get("actions", []) 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.initial_balance = config.get("initial_balance", 10000.0)
self.current_balance = self.initial_balance self.current_balance = self.initial_balance
self.position = 0.0 self.position = 0.0
self.position_token = "" self.position_token = ""
self.entry_price: Optional[float] = None
self.entry_time: Optional[int] = None
self.trades: List[Dict[str, Any]] = [] self.trades: List[Dict[str, Any]] = []
self.running = False self.running = False
@@ -103,11 +108,73 @@ class BacktestEngine:
timestamp = kline.get("timestamp", 0) 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: for condition in self.conditions:
if self._check_condition(condition, klines, i, price): if self._check_condition(condition, klines, i, price):
await self._execute_actions(price, timestamp, condition) await self._execute_actions(price, timestamp, condition)
break 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( def _check_condition(
self, self,
condition: Dict[str, Any], condition: Dict[str, Any],
@@ -173,6 +240,8 @@ class BacktestEngine:
self.position += amount / price self.position += amount / price
self.current_balance -= amount self.current_balance -= amount
self.position_token = token self.position_token = token
self.entry_price = price
self.entry_time = timestamp
self.trades.append( self.trades.append(
{ {
"type": "buy", "type": "buy",
@@ -209,9 +278,12 @@ class BacktestEngine:
"amount": sell_amount, "amount": sell_amount,
"quantity": self.position, "quantity": self.position,
"timestamp": timestamp, "timestamp": timestamp,
"exit_reason": "manual",
} }
) )
self.position = 0 self.position = 0
self.entry_price = None
self.entry_time = None
self.signals.append( self.signals.append(
{ {
"id": str(uuid.uuid4()), "id": str(uuid.uuid4()),

View File

@@ -1,8 +1,11 @@
import uuid import uuid
import asyncio import asyncio
import logging
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from ..backtest.ave_client import AveCloudClient from ..ave.client import AveCloudClient
logger = logging.getLogger(__name__)
class SimulateEngine: class SimulateEngine:
@@ -20,6 +23,9 @@ class SimulateEngine:
self.strategy_config = config.get("strategy_config", {}) self.strategy_config = config.get("strategy_config", {})
self.conditions = self.strategy_config.get("conditions", []) self.conditions = self.strategy_config.get("conditions", [])
self.actions = self.strategy_config.get("actions", []) 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.check_interval = config.get("check_interval", 60)
self.duration_seconds = config.get("duration_seconds", 3600) self.duration_seconds = config.get("duration_seconds", 3600)
self.auto_execute = config.get("auto_execute", False) self.auto_execute = config.get("auto_execute", False)
@@ -29,6 +35,13 @@ class SimulateEngine:
self.started_at: Optional[datetime] = None self.started_at: Optional[datetime] = None
self.last_price: Optional[float] = None self.last_price: Optional[float] = None
self.last_volume: 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]: async def run(self) -> Dict[str, Any]:
self.running = True self.running = True
@@ -65,7 +78,9 @@ class SimulateEngine:
self.last_volume = current_volume self.last_volume = current_volume
except Exception as e: 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): for _ in range(self.check_interval):
if not self.running: if not self.running:
@@ -83,6 +98,8 @@ class SimulateEngine:
self.results = self.results or {} self.results = self.results or {}
self.results["total_signals"] = len(self.signals) 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["signals"] = self.signals
self.results["started_at"] = self.started_at self.results["started_at"] = self.started_at
self.results["ended_at"] = datetime.utcnow() self.results["ended_at"] = datetime.utcnow()
@@ -94,11 +111,70 @@ class SimulateEngine:
): ):
timestamp = int(datetime.utcnow().timestamp() * 1000) 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: for condition in self.conditions:
if self._check_condition(condition, current_price, current_volume): if self._check_condition(condition, current_price, current_volume):
await self._execute_actions(current_price, timestamp, condition) await self._execute_actions(current_price, timestamp, condition)
break 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( def _check_condition(
self, self,
condition: Dict[str, Any], condition: Dict[str, Any],
@@ -146,20 +222,41 @@ class SimulateEngine:
token = matched_condition.get("token", self.token) token = matched_condition.get("token", self.token)
reasoning = f"Condition {matched_condition.get('type')} triggered" reasoning = f"Condition {matched_condition.get('type')} triggered"
signal = { for action in self.actions:
"id": str(uuid.uuid4()), action_type = action.get("type", "")
"bot_id": self.bot_id, if action_type == "buy":
"run_id": self.run_id, amount_percent = action.get("amount_percent", 10)
"signal_type": "signal", amount = self.current_balance * (amount_percent / 100)
"token": token, self.position += amount / price
"price": price, self.position_token = token
"confidence": 0.8, self.entry_price = price
"reasoning": reasoning, self.entry_time = timestamp
"executed": self.auto_execute, self.current_balance -= amount
"created_at": datetime.utcnow(), 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): async def stop(self):
self.running = False self.running = False

View File

@@ -6,6 +6,7 @@ pydantic-settings>=2.1.0
email-validator>=2.0.0 email-validator>=2.0.0
python-jose[cryptography]>=3.3.0 python-jose[cryptography]>=3.3.0
passlib[bcrypt]>=1.7.4 passlib[bcrypt]>=1.7.4
bcrypt>=4.0,<5.0 # Required for passlib compatibility
crewai>=0.1.0 crewai>=0.1.0
anthropic>=0.18.0 anthropic>=0.18.0
httpx>=0.26.0 httpx>=0.26.0