Compare commits

...

11 Commits

Author SHA1 Message Date
shokollm
a280217254 feat: implement chat interface with CrewAI integration
- Create MiniMax LLM connector for CrewAI integration
- Implement TradingCrew with trading_designer, strategy_validator, strategy_explainer
- Add strategy parsing from natural language to strategy_config JSON
- Update chat endpoint with CrewAI integration and conversation context
- Add strategy validation logic
- Add explanation generation for user-friendly responses
- Add BotChatRequest/BotChatResponse schemas

Fixes #6
2026-04-08 06:29:05 +00:00
0cc3327991 Merge pull request '[Backend] Bot CRUD - Bot Management with Max 3 Limit' (#16) from fix/issue-5 into main 2026-04-08 08:16:19 +02:00
shokollm
429d46c6d0 feat: implement bot CRUD with 3-bot limit per user 2026-04-08 06:05:43 +00:00
a2f0c9a0e9 Merge pull request '[Backend] Auth System - JWT Authentication' (#15) from fix/issue-4 into main 2026-04-08 08:01:24 +02:00
shokollm
42640679c7 feat: implement JWT authentication system
- Add register endpoint with bcrypt password hashing
- Add login endpoint returning JWT tokens
- Add logout endpoint with token blacklisting
- Add /me endpoint for current user info
- Add rate limiting (5/minute) for login attempts using slowapi
- Add user settings GET and PATCH endpoints
- Create auth middleware via get_current_user dependency
- Add UserSettings and UserSettingsUpdate schemas
2026-04-08 05:48:38 +00:00
f59e595ffd Merge pull request '[Backend] Database Models - Add missing Pydantic schemas' (#14) from fix/issue-3 into main 2026-04-08 06:57:46 +02:00
shokollm
a5e41ab449 Add missing Pydantic schemas for BotConversation and Signal
Based on IMPLEMENTATION_PLAN.md Section 4 schema, the existing schemas.py
was missing schemas for:
- BotConversationCreate/Response (for bot_conversations table)
- SignalResponse (for signals table)

These were identified as gaps during issue #3 review.
2026-04-08 04:41:31 +00:00
6977203748 Merge pull request '[Backend] Project Setup - FastAPI Structure and Dependencies' (#13) from fix/issue-2 into main 2026-04-08 06:35:53 +02:00
shokollm
7dfd79dea2 fix: remove pycache and .env from git tracking
These files are listed in .gitignore but were accidentally committed.
Removing them from git while keeping them locally.
2026-04-08 03:54:26 +00:00
shokollm
a03304f9ef chore: update .gitignore to exclude Python artifacts 2026-04-08 03:49:06 +00:00
shokollm
f2b5bd5f45 feat: backend project setup with FastAPI structure and dependencies
- Create directory structure per IMPLEMENTATION_PLAN.md Section 12
- Add requirements.txt with FastAPI, SQLAlchemy, CrewAI, etc.
- Add core/config.py for environment variable configuration
- Add core/database.py for SQLite connection
- Add core/security.py for password hashing and JWT
- Add FastAPI app entry point (main.py) with all API routers
- Add Uvicorn runner (run.py)
- Add API route stubs (auth, bots, backtest, simulate, config)
- Add db/models.py with SQLAlchemy models
- Add db/schemas.py with Pydantic schemas
- Add service stubs (ai_agent, backtest, simulate engines)
- Add .env.example with all required environment variables
- Verify server starts correctly
2026-04-08 03:48:21 +00:00
28 changed files with 1386 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
.kugetsu/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info/
dist/
build/
.env
venv/
.venv/
*.db
data/

11
src/backend/.env.example Normal file
View File

@@ -0,0 +1,11 @@
DATABASE_URL=sqlite:///./data/app.db
SECRET_KEY=your-super-secret-key-change-in-production
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=1440
MINIMAX_API_KEY=your-minimax-api-key
MINIMAX_MODEL=MiniMax-Text-01
AVE_API_KEY=your-ave-cloud-api-key
AVE_API_PLAN=free
HOST=0.0.0.0
PORT=8000
DEBUG=false

View File

143
src/backend/app/api/auth.py Normal file
View File

@@ -0,0 +1,143 @@
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from typing import Annotated
from ..core.database import get_db
from ..core.security import (
get_password_hash,
verify_password,
create_access_token,
verify_token,
)
from ..core.config import get_settings
from ..core.limiter import limiter
from ..db.schemas import (
UserCreate,
UserResponse,
Token,
UserSettings,
UserSettingsUpdate,
)
from ..db.models import User
router = APIRouter()
settings = get_settings()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
TOKEN_BLACKLIST = set()
def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_db)
) -> User:
if token in TOKEN_BLACKLIST:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has been revoked",
)
payload = verify_token(token)
if payload is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
)
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token payload",
)
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found",
)
return user
@router.post(
"/register", response_model=UserResponse, 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()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered",
)
hashed_password = get_password_hash(user.password)
db_user = User(
email=user.email,
password_hash=hashed_password,
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
@router.post("/login", response_model=Token)
@limiter.limit("5/minute")
def login(
request: Request,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: Session = Depends(get_db),
):
user = db.query(User).filter(User.email == form_data.username).first()
if not user or not verify_password(form_data.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
)
access_token = create_access_token(data={"sub": user.id})
return Token(access_token=access_token, token_type="bearer")
@router.post("/logout")
def logout(
current_user: Annotated[User, Depends(get_current_user)],
token: Annotated[str, Depends(oauth2_scheme)],
):
TOKEN_BLACKLIST.add(token)
return {"message": "Successfully logged out"}
@router.get("/me", response_model=UserResponse)
def get_me(
current_user: Annotated[User, Depends(get_current_user)],
):
return current_user
@router.get("/settings", response_model=UserSettings)
def get_settings_endpoint(
current_user: Annotated[User, Depends(get_current_user)],
):
return UserSettings(email=current_user.email)
@router.patch("/settings", response_model=UserSettings)
def update_settings(
current_user: Annotated[User, Depends(get_current_user)],
settings_update: UserSettingsUpdate,
db: Session = Depends(get_db),
):
if settings_update.email:
existing = (
db.query(User)
.filter(User.email == settings_update.email, User.id != current_user.id)
.first()
)
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already in use",
)
current_user.email = settings_update.email
if settings_update.password:
current_user.password_hash = get_password_hash(settings_update.password)
db.commit()
db.refresh(current_user)
return UserSettings(email=current_user.email)

View File

@@ -0,0 +1,36 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from ..core.database import get_db
from ..db.schemas import BacktestCreate, BacktestResponse
router = APIRouter()
@router.post("/bots/{bot_id}/backtest", response_model=BacktestResponse)
def start_backtest(bot_id: str, config: BacktestCreate, db: Session = Depends(get_db)):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
)
@router.get("/bots/{bot_id}/backtest/{run_id}", response_model=BacktestResponse)
def get_backtest(bot_id: str, run_id: str, db: Session = Depends(get_db)):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
)
@router.get("/bots/{bot_id}/backtests", response_model=List[BacktestResponse])
def list_backtests(bot_id: str, db: Session = Depends(get_db)):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
)
@router.post("/bots/{bot_id}/backtest/{run_id}/stop")
def stop_backtest(bot_id: str, run_id: str, db: Session = Depends(get_db)):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
)

275
src/backend/app/api/bots.py Normal file
View File

@@ -0,0 +1,275 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Annotated
from .auth import get_current_user
from ..core.database import get_db
from ..core.config import get_settings
from ..db.schemas import (
BotCreate,
BotUpdate,
BotResponse,
BotConversationCreate,
BotConversationResponse,
BotChatRequest,
BotChatResponse,
)
from ..db.models import Bot, BotConversation, User
from ..services.ai_agent.crew import get_trading_crew
router = APIRouter()
MAX_BOTS_PER_USER = 3
@router.get("", response_model=List[BotResponse])
def list_bots(
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bots = db.query(Bot).filter(Bot.user_id == current_user.id).all()
return bots
@router.post("", response_model=BotResponse, status_code=status.HTTP_201_CREATED)
def create_bot(
bot_data: BotCreate,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
user_bot_count = db.query(Bot).filter(Bot.user_id == current_user.id).count()
if user_bot_count >= MAX_BOTS_PER_USER:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Maximum of {MAX_BOTS_PER_USER} bots per user exceeded",
)
existing_bot = (
db.query(Bot)
.filter(Bot.user_id == current_user.id, Bot.name == bot_data.name)
.first()
)
if existing_bot:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Bot name must be unique per user",
)
db_bot = Bot(
user_id=current_user.id,
name=bot_data.name,
description=bot_data.description,
strategy_config=bot_data.strategy_config,
llm_config=bot_data.llm_config,
)
db.add(db_bot)
db.commit()
db.refresh(db_bot)
return db_bot
@router.get("/{bot_id}", response_model=BotResponse)
def get_bot(
bot_id: str,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Bot not found",
)
if bot.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to access this bot",
)
return bot
@router.put("/{bot_id}", response_model=BotResponse)
def update_bot(
bot_id: str,
bot_data: BotUpdate,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Bot not found",
)
if bot.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to update this bot",
)
if bot_data.name is not None:
existing_bot = (
db.query(Bot)
.filter(
Bot.user_id == current_user.id,
Bot.name == bot_data.name,
Bot.id != bot_id,
)
.first()
)
if existing_bot:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Bot name must be unique per user",
)
bot.name = bot_data.name
if bot_data.description is not None:
bot.description = bot_data.description
if bot_data.strategy_config is not None:
bot.strategy_config = bot_data.strategy_config
if bot_data.llm_config is not None:
bot.llm_config = bot_data.llm_config
if bot_data.status is not None:
bot.status = bot_data.status
db.commit()
db.refresh(bot)
return bot
@router.delete("/{bot_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_bot(
bot_id: str,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Bot not found",
)
if bot.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to delete this bot",
)
db.delete(bot)
db.commit()
@router.post("/{bot_id}/chat", response_model=BotChatResponse)
def chat(
bot_id: str,
request: BotChatRequest,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Bot not found",
)
if bot.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to chat with this bot",
)
conversation_history = (
db.query(BotConversation)
.filter(BotConversation.bot_id == bot_id)
.order_by(BotConversation.created_at)
.all()
)
history_for_crew = [
{"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)
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()
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),
)
else:
crew = get_trading_crew()
result = crew.chat(user_message, history_for_crew)
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),
)
@router.get("/{bot_id}/history", response_model=List[BotConversationResponse])
def get_history(
bot_id: str,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
bot = db.query(Bot).filter(Bot.id == bot_id).first()
if not bot:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Bot not found",
)
if bot.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not authorized to access this bot's history",
)
conversations = (
db.query(BotConversation)
.filter(BotConversation.bot_id == bot_id)
.order_by(BotConversation.created_at)
.all()
)
return conversations

View File

@@ -0,0 +1,13 @@
from fastapi import APIRouter
router = APIRouter()
@router.get("/chains")
def get_chains():
return {"chains": []}
@router.get("/tokens")
def get_tokens():
return {"tokens": []}

View File

@@ -0,0 +1,38 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from ..core.database import get_db
from ..db.schemas import SimulationCreate, SimulationResponse
router = APIRouter()
@router.post("/bots/{bot_id}/simulate", response_model=SimulationResponse)
def start_simulation(
bot_id: str, config: SimulationCreate, db: Session = Depends(get_db)
):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
)
@router.get("/bots/{bot_id}/simulate/{run_id}", response_model=SimulationResponse)
def get_simulation(bot_id: str, run_id: str, db: Session = Depends(get_db)):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
)
@router.get("/bots/{bot_id}/simulations", response_model=List[SimulationResponse])
def list_simulations(bot_id: str, db: Session = Depends(get_db)):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
)
@router.post("/bots/{bot_id}/simulate/{run_id}/stop")
def stop_simulation(bot_id: str, run_id: str, db: Session = Depends(get_db)):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
)

View File

View File

@@ -0,0 +1,25 @@
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
DATABASE_URL: str = "sqlite:///./data/app.db"
SECRET_KEY: str
JWT_ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440
MINIMAX_API_KEY: str
MINIMAX_MODEL: str = "MiniMax-Text-01"
AVE_API_KEY: str
AVE_API_PLAN: str = "free"
HOST: str = "0.0.0.0"
PORT: int = 8000
DEBUG: bool = False
class Config:
env_file = ".env"
extra = "allow"
@lru_cache()
def get_settings() -> Settings:
return Settings()

View File

@@ -0,0 +1,40 @@
import os
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy.engine import Engine
from .config import get_settings
settings = get_settings()
if settings.DATABASE_URL.startswith("sqlite"):
db_path = settings.DATABASE_URL.replace("sqlite:///", "")
os.makedirs(
os.path.dirname(db_path) if os.path.dirname(db_path) else ".", exist_ok=True
)
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_conn, connection_record):
cursor = dbapi_conn.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
engine = create_engine(
settings.DATABASE_URL,
connect_args={"check_same_thread": False}
if settings.DATABASE_URL.startswith("sqlite")
else {},
echo=settings.DEBUG,
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

View File

@@ -0,0 +1,4 @@
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)

View File

@@ -0,0 +1,42 @@
from datetime import datetime, timedelta
from typing import Any, Optional
from jose import jwt, JWTError
from passlib.context import CryptContext
from .config import get_settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
to_encode = data.copy()
settings = get_settings()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(
to_encode, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM
)
return encoded_jwt
def verify_token(token: str) -> Optional[dict[str, Any]]:
settings = get_settings()
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM]
)
return payload
except JWTError:
return None
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)

View File

View File

@@ -0,0 +1,121 @@
import uuid
from datetime import datetime
from sqlalchemy import (
Column,
String,
Text,
Float,
Boolean,
DateTime,
ForeignKey,
Index,
JSON,
)
from sqlalchemy.orm import relationship
from ..core.database import Base
def generate_uuid():
return str(uuid.uuid4())
class User(Base):
__tablename__ = "users"
id = Column(String, primary_key=True, default=generate_uuid)
email = Column(String, unique=True, nullable=False)
password_hash = Column(String, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
bots = relationship("Bot", back_populates="user", cascade="all, delete-orphan")
class Bot(Base):
__tablename__ = "bots"
id = Column(String, primary_key=True, default=generate_uuid)
user_id = Column(String, ForeignKey("users.id"), nullable=False)
name = Column(String, nullable=False)
description = Column(Text)
strategy_config = Column(JSON, nullable=False)
llm_config = Column(JSON, nullable=False)
status = Column(String, default="draft")
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user = relationship("User", back_populates="bots")
conversations = relationship(
"BotConversation", back_populates="bot", cascade="all, delete-orphan"
)
backtests = relationship(
"Backtest", back_populates="bot", cascade="all, delete-orphan"
)
simulations = relationship(
"Simulation", back_populates="bot", cascade="all, delete-orphan"
)
signals = relationship("Signal", back_populates="bot", cascade="all, delete-orphan")
class BotConversation(Base):
__tablename__ = "bot_conversations"
id = Column(String, primary_key=True, default=generate_uuid)
bot_id = Column(String, ForeignKey("bots.id"), nullable=False)
role = Column(String, nullable=False)
content = Column(Text, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
bot = relationship("Bot", back_populates="conversations")
class Backtest(Base):
__tablename__ = "backtests"
id = Column(String, primary_key=True, default=generate_uuid)
bot_id = Column(String, ForeignKey("bots.id"), nullable=False)
started_at = Column(DateTime, nullable=False)
ended_at = Column(DateTime)
status = Column(String, nullable=False)
config = Column(JSON, nullable=False)
result = Column(JSON)
bot = relationship("Bot", back_populates="backtests")
class Simulation(Base):
__tablename__ = "simulations"
id = Column(String, primary_key=True, default=generate_uuid)
bot_id = Column(String, ForeignKey("bots.id"), nullable=False)
started_at = Column(DateTime, nullable=False)
status = Column(String, nullable=False)
config = Column(JSON, nullable=False)
signals = Column(JSON)
bot = relationship("Bot", back_populates="simulations")
class Signal(Base):
__tablename__ = "signals"
id = Column(String, primary_key=True, default=generate_uuid)
bot_id = Column(String, ForeignKey("bots.id"), nullable=False)
run_id = Column(String, nullable=False)
signal_type = Column(String, nullable=False)
token = Column(String, nullable=False)
price = Column(Float, nullable=False)
confidence = Column(Float)
reasoning = Column(Text)
executed = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
bot = relationship("Bot", back_populates="signals")
Index("idx_bots_user_id", Bot.user_id)
Index("idx_conversations_bot_id", BotConversation.bot_id)
Index("idx_backtests_bot_id", Backtest.bot_id)
Index("idx_simulations_bot_id", Simulation.bot_id)
Index("idx_signals_bot_id", Signal.bot_id)
Index("idx_signals_run_id", Signal.run_id)

View File

@@ -0,0 +1,145 @@
from pydantic import BaseModel, EmailStr
from typing import Optional, List, Any
from datetime import datetime
class UserCreate(BaseModel):
email: EmailStr
password: str
class UserResponse(BaseModel):
id: str
email: str
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class Token(BaseModel):
access_token: str
token_type: str
class UserSettings(BaseModel):
email: EmailStr
class UserSettingsUpdate(BaseModel):
email: Optional[EmailStr] = None
password: Optional[str] = None
class BotCreate(BaseModel):
name: str
description: Optional[str] = None
strategy_config: dict
llm_config: dict
class BotUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
strategy_config: Optional[dict] = None
llm_config: Optional[dict] = None
status: Optional[str] = None
class BotResponse(BaseModel):
id: str
user_id: str
name: str
description: Optional[str]
strategy_config: dict
llm_config: dict
status: str
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class BacktestCreate(BaseModel):
token: str
chain: str
timeframe: str
start_date: str
end_date: str
class BacktestResponse(BaseModel):
id: str
bot_id: str
started_at: datetime
ended_at: Optional[datetime]
status: str
config: dict
result: Optional[dict]
class Config:
from_attributes = True
class SimulationCreate(BaseModel):
token: str
chain: str
duration_seconds: int = 3600
auto_execute: bool = False
class SimulationResponse(BaseModel):
id: str
bot_id: str
started_at: datetime
status: str
config: dict
signals: Optional[List[dict]]
class Config:
from_attributes = True
class BotConversationCreate(BaseModel):
role: str
content: str
class BotConversationResponse(BaseModel):
id: str
bot_id: str
role: str
content: str
created_at: datetime
class Config:
from_attributes = True
class BotChatRequest(BaseModel):
message: str
strategy_config: Optional[bool] = False
class BotChatResponse(BaseModel):
response: str
strategy_config: Optional[dict] = None
success: bool = False
class SignalResponse(BaseModel):
id: str
bot_id: str
run_id: str
signal_type: str
token: str
price: float
confidence: Optional[float]
reasoning: Optional[str]
executed: bool
created_at: datetime
class Config:
from_attributes = True

38
src/backend/app/main.py Normal file
View File

@@ -0,0 +1,38 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from slowapi import Limiter
from slowapi.util import get_remote_address
from .api import auth, bots, backtest, simulate, config
from .core.limiter import limiter
app = FastAPI(
title="Randebu Trading Bot API",
description="AI-powered trading bot platform API",
version="0.1.0",
)
app.state.limiter = limiter
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
app.include_router(bots.router, prefix="/api/bots", tags=["bots"])
app.include_router(backtest.router, prefix="/api", tags=["backtest"])
app.include_router(simulate.router, prefix="/api", tags=["simulate"])
app.include_router(config.router, prefix="/api/config", tags=["config"])
@app.get("/")
def root():
return {"status": "ok", "message": "Randebu Trading Bot API"}
@app.get("/health")
def health():
return {"status": "healthy"}

View File

View File

@@ -0,0 +1,4 @@
from .crew import CrewAgent
from .llm_connector import LLMConnector
__all__ = ["CrewAgent", "LLMConnector"]

View File

@@ -0,0 +1,247 @@
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
class StrategyValidator:
SUPPORTED_CONDITIONS = ["price_drop", "price_rise", "volume_spike", "price_level"]
SUPPORTED_ACTIONS = ["buy", "sell", "notify"]
def validate(self, strategy_config: dict) -> tuple[bool, list[str]]:
errors = []
if "conditions" not in strategy_config:
errors.append("Missing 'conditions' in strategy config")
return False, errors
if not isinstance(strategy_config["conditions"], list):
errors.append("'conditions' must be a list")
return False, errors
if len(strategy_config["conditions"]) == 0:
errors.append("At least one condition is required")
return False, errors
for i, condition in enumerate(strategy_config["conditions"]):
if "type" not in condition:
errors.append(f"Condition {i}: missing 'type'")
continue
cond_type = condition.get("type")
if cond_type not in self.SUPPORTED_CONDITIONS:
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:
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"
)
elif cond_type == "price_level":
if "token" not in params:
errors.append(f"Condition {i}: missing 'token'")
if "price" not in params:
errors.append(f"Condition {i}: missing 'price'")
if "direction" not in params:
errors.append(f"Condition {i}: missing 'direction'")
elif params["direction"] not in ["above", "below"]:
errors.append(
f"Condition {i}: direction must be 'above' or 'below'"
)
if "actions" in strategy_config:
if not isinstance(strategy_config["actions"], list):
errors.append("'actions' must be a list")
else:
for i, action in enumerate(strategy_config["actions"]):
if "type" not in action:
errors.append(f"Action {i}: missing 'type'")
elif action["type"] not in self.SUPPORTED_ACTIONS:
errors.append(
f"Action {i}: unsupported type '{action['type']}'"
)
return len(errors) == 0, errors
class StrategyExplainer:
def explain(self, strategy_config: dict) -> str:
explanations = []
if "conditions" in strategy_config:
cond_list = strategy_config["conditions"]
if cond_list:
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")
if cond_type == "price_drop":
pct = params.get("threshold_percent", 0)
explanations.append(f" - {token} price drops by {pct}%")
elif cond_type == "price_rise":
pct = params.get("threshold_percent", 0)
explanations.append(f" - {token} price rises by {pct}%")
elif cond_type == "volume_spike":
pct = params.get("threshold_percent", 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")
explanations.append(
f" - {token} price crosses {direction} ${price}"
)
if "actions" in strategy_config:
actions = strategy_config.get("actions", [])
if actions:
explanations.append("\nWhen triggered, the strategy will:")
for action in actions:
action_type = action.get("type")
if action_type == "buy":
explanations.append(" - Buy the token")
elif action_type == "sell":
explanations.append(" - Sell the token")
elif action_type == "notify":
explanations.append(" - Send a notification")
if not explanations:
explanations.append("Strategy configuration is empty or invalid.")
return "\n".join(explanations)
def create_trading_designer_agent(
api_key: str, model: str = "MiniMax-Text-01"
) -> Agent:
connector = MiniMaxConnector(api_key=api_key, model=model)
system_prompt = """You are a Trading Strategy Designer AI. Your role is to parse user requests
for trading strategies into structured JSON configuration.
Supported conditions (MVP):
- price_drop: Triggers when a token's price drops by a specified percentage
- price_rise: Triggers when a token's price rises by a specified percentage
- volume_spike: Triggers when trading volume increases by a specified percentage
- price_level: Triggers when price crosses above or below a specified level
Always ask clarifying questions if the user's request is ambiguous.
Output strategy_config in valid JSON format only when you have all required information.
"""
return 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),
verbose=True,
)
def create_strategy_validator_agent(
api_key: str, model: str = "MiniMax-Text-01"
) -> Agent:
return Agent(
role="Strategy Validator",
goal="Validate trading strategy configurations for feasibility and identify potential issues",
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),
verbose=True,
)
def create_strategy_explainer_agent(
api_key: str, model: str = "MiniMax-Text-01"
) -> Agent:
return Agent(
role="Strategy Explainer",
goal="Generate clear, user-friendly explanations of trading strategies",
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),
verbose=True,
)
class TradingCrew:
def __init__(self, api_key: str, model: str = "MiniMax-Text-01"):
self.api_key = api_key
self.model = model
self.validator = StrategyValidator()
self.explainer = StrategyExplainer()
self.connector = MiniMaxConnector(api_key=api_key, model=model)
def parse_strategy(
self, user_message: str, conversation_history: list[dict] = None
) -> dict:
strategy_config = self.connector.parse_strategy(
user_message, conversation_history
)
if "error" in strategy_config:
return strategy_config
is_valid, errors = self.validator.validate(strategy_config)
if not is_valid:
return {
"error": "Strategy validation failed",
"validation_errors": errors,
"partial_config": strategy_config,
}
return strategy_config
def explain_strategy(self, strategy_config: dict) -> str:
return self.explainer.explain(strategy_config)
def chat(self, user_message: str, conversation_history: list[dict] = None) -> dict:
strategy_config = self.parse_strategy(user_message, conversation_history)
if "error" in strategy_config:
explanation = f"I had trouble understanding your strategy: {strategy_config.get('error', 'Unknown error')}"
if "validation_errors" in strategy_config:
explanation += "\n\nValidation issues:"
for err in strategy_config["validation_errors"]:
explanation += f"\n - {err}"
return {
"response": explanation,
"strategy_config": strategy_config.get("partial_config"),
"success": False,
}
explanation = self.explain_strategy(strategy_config)
return {
"response": f"I've configured your strategy:\n\n{explanation}",
"strategy_config": strategy_config,
"success": True,
}
def get_trading_crew(
api_key: Optional[str] = None, model: Optional[str] = None
) -> TradingCrew:
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 TradingCrew(api_key=api_key, model=model)

View File

@@ -0,0 +1,108 @@
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)
self.api_key = api_key
self.model = model
self.base_url = "https://api.minimax.chat/v1"
def _call(self, messages: List[Dict[str, str]], **kwargs) -> str:
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
payload = {
"model": self.model,
"messages": messages,
"temperature": kwargs.get("temperature", 0.7),
"max_tokens": kwargs.get("max_tokens", 2048),
}
with httpx.Client(timeout=60.0) as client:
response = client.post(
f"{self.base_url}/chat/completions",
headers=headers,
json=payload,
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
def call(self, messages: List[Dict[str, str]], **kwargs) -> str:
return self._call(messages, **kwargs)
class MiniMaxConnector:
def __init__(self, api_key: str, model: str = "MiniMax-Text-01"):
self.api_key = api_key
self.model = model
def chat(self, messages: list[dict], **kwargs) -> str:
formatted_messages = []
for msg in messages:
if isinstance(msg, dict):
formatted_messages.append(
{
"role": msg.get("role", "user"),
"content": msg.get("content", str(msg)),
}
)
else:
formatted_messages.append({"role": "user", "content": str(msg)})
llm = MiniMaxLLM(api_key=self.api_key, model=self.model)
return llm.call(formatted_messages, **kwargs)
def parse_strategy(
self, user_message: str, conversation_history: list[dict] = None
) -> dict:
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_level: Price crosses above/below X (requires: token, price, direction)
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
}
}
],
"actions": [
{
"type": "buy|sell|notify",
"params": {}
}
]
}
If the user wants a condition not in the supported list, ask for clarification.
"""
messages = [{"role": "system", "content": system_prompt}]
if conversation_history:
for msg in conversation_history:
messages.append(
{"role": msg.get("role", "user"), "content": msg.get("content", "")}
)
messages.append({"role": "user", "content": user_message})
response = self.chat(messages)
try:
import json
result = json.loads(response)
return result
except json.JSONDecodeError:
return {"error": "Failed to parse strategy", "raw_response": response}

View File

@@ -0,0 +1,3 @@
from .engine import BacktestEngine
__all__ = ["BacktestEngine"]

View File

@@ -0,0 +1,15 @@
from typing import Optional, Dict, Any
class BacktestEngine:
def __init__(self, config: Dict[str, Any]):
self.config = config
async def run(self) -> Dict[str, Any]:
raise NotImplementedError("Backtest engine not yet implemented")
async def stop(self):
raise NotImplementedError("Backtest stop not yet implemented")
def get_results(self) -> Dict[str, Any]:
raise NotImplementedError("Backtest results not yet implemented")

View File

@@ -0,0 +1,3 @@
from .engine import SimulateEngine
__all__ = ["SimulateEngine"]

View File

@@ -0,0 +1,15 @@
from typing import Optional, Dict, Any, List
class SimulateEngine:
def __init__(self, config: Dict[str, Any]):
self.config = config
async def run(self) -> List[Dict[str, Any]]:
raise NotImplementedError("Simulation engine not yet implemented")
async def stop(self):
raise NotImplementedError("Simulation stop not yet implemented")
def get_signals(self) -> List[Dict[str, Any]]:
raise NotImplementedError("Simulation signals not yet implemented")

View File

@@ -0,0 +1,13 @@
fastapi>=0.109.0
uvicorn>=0.27.0
sqlalchemy>=2.0.0
pydantic>=2.5.0
pydantic-settings>=2.1.0
email-validator>=2.0.0
python-jose[cryptography]>=3.3.0
passlib[bcrypt]>=1.7.4
crewai>=0.1.0
anthropic>=0.18.0
httpx>=0.26.0
python-multipart>=0.0.6
slowapi>=0.1.9

11
src/backend/run.py Normal file
View File

@@ -0,0 +1,11 @@
import uvicorn
from app.core.config import get_settings
if __name__ == "__main__":
settings = get_settings()
uvicorn.run(
"app.main:app",
host=settings.HOST,
port=settings.PORT,
reload=settings.DEBUG,
)

20
src/backend/setup.py Normal file
View File

@@ -0,0 +1,20 @@
from setuptools import setup, find_packages
setup(
name="randebu-backend",
version="0.1.0",
packages=find_packages(),
install_requires=[
"fastapi>=0.109.0",
"uvicorn>=0.27.0",
"sqlalchemy>=2.0.0",
"pydantic>=2.5.0",
"pydantic-settings>=2.1.0",
"python-jose[cryptography]>=3.3.0",
"passlib[bcrypt]>=1.7.4",
"crewai>=0.1.0",
"anthropic>=0.18.0",
"httpx>=0.26.0",
"python-multipart>=0.0.6",
],
)