Compare commits

..

5 Commits

Author SHA1 Message Date
shokollm
965efa122b feat: implement Simulate Engine for real-time signal detection
Implement simulate engine for real-time signal detection via REST polling.

Changes:
- SimulateEngine service with configurable check interval (default 60s for free tier)
- REST polling for current prices using AveCloudClient
- Condition matching for real-time data (price_drop, price_rise, volume_spike, price_level)
- Signal logging with user-initiated start/stop
- Simulation API endpoints:
  - POST /api/bots/{id}/simulate - Start simulation
  - GET /api/bots/{id}/simulate/{run_id} - Get status/signals
  - GET /api/bots/{id}/simulations - List all simulations
  - POST /api/bots/{id}/simulate/{run_id}/stop - Stop simulation
- Updated SimulationCreate schema with check_interval field
- Free tier limited to 60s minimum check interval
- Signals stored in database for simulation signal history

Depends on issue #7 (Backtest Engine) which was merged in PR #18
2026-04-08 11:25:51 +00:00
0fb16f06e4 Merge pull request '[Backend] Backtest Engine - Historical Data Replay' (#18) from fix/issue-7 into main 2026-04-08 13:21:56 +02:00
shokollm
a461005015 Implement Backtest Engine - Historical Data Replay
Implements issue #7 - Backtest Engine for historical strategy testing.

Changes:
- Created AveCloudClient for fetching klines from AVE Cloud Data API
- Implemented BacktestEngine with condition matching (price_drop, price_rise, volume_spike, price_level)
- Implemented signal generation and portfolio simulation
- Calculates metrics: total_return, win_rate, max_drawdown, sharpe_ratio, total_trades
- Implemented async/background backtest execution via FastAPI BackgroundTasks
- Stores results in backtests table and signals table
- All backtest API endpoints with JWT auth and ownership validation

API Endpoints:
- POST /api/bots/{id}/backtest - Start backtest
- GET /api/bots/{id}/backtest/{run_id} - Get status/results
- GET /api/bots/{id}/backtests - List all backtests
- POST /api/bots/{id}/backtest/{run_id}/stop - Stop running backtest
2026-04-08 09:39:07 +00:00
b0311bc96f Merge pull request '[Backend] Chat Interface + CrewAI Integration' (#17) from fix/issue-6 into main 2026-04-08 08:37:58 +02:00
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
44 changed files with 1388 additions and 4575 deletions

View File

@@ -1,36 +1,219 @@
from fastapi import APIRouter, Depends, HTTPException, status
import uuid
import asyncio
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
from sqlalchemy.orm import Session
from typing import List
from typing import List, Dict, Any, Optional
from concurrent.futures import ThreadPoolExecutor
from .auth import get_current_user
from ..core.database import get_db
from ..core.config import get_settings
from ..db.schemas import BacktestCreate, BacktestResponse
from ..db.models import Bot, Backtest, Signal, User
router = APIRouter()
running_backtests: Dict[str, Any] = {}
executor = ThreadPoolExecutor(max_workers=4)
@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"
def run_backtest_sync(
backtest_id: str, db_url: str, bot_id: str, config: Dict[str, Any]
):
import asyncio
from ..services.backtest.engine import BacktestEngine
from ..core.database import SessionLocal
async def _run():
engine = BacktestEngine(config)
engine.run_id = backtest_id
running_backtests[backtest_id] = engine
try:
results = await engine.run()
db = SessionLocal()
try:
backtest = db.query(Backtest).filter(Backtest.id == backtest_id).first()
if backtest:
backtest.status = engine.status
backtest.ended_at = datetime.utcnow()
backtest.result = results
db.commit()
for signal in engine.signals:
db_signal = Signal(
id=signal["id"],
bot_id=signal["bot_id"],
run_id=signal["run_id"],
signal_type=signal["signal_type"],
token=signal["token"],
price=signal["price"],
confidence=signal.get("confidence"),
reasoning=signal.get("reasoning"),
executed=signal.get("executed", False),
created_at=signal["created_at"],
)
db.add(db_signal)
db.commit()
finally:
db.close()
finally:
if backtest_id in running_backtests:
del running_backtests[backtest_id]
asyncio.run(_run())
@router.post(
"/bots/{bot_id}/backtest",
response_model=BacktestResponse,
status_code=status.HTTP_201_CREATED,
)
async def start_backtest(
bot_id: str,
config: BacktestCreate,
background_tasks: BackgroundTasks,
current_user: 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"
)
settings = get_settings()
backtest_id = str(uuid.uuid4())
backtest_config = {
"bot_id": bot_id,
"token": config.token,
"chain": config.chain,
"timeframe": config.timeframe,
"start_date": config.start_date,
"end_date": config.end_date,
"strategy_config": bot.strategy_config,
"ave_api_key": settings.AVE_API_KEY,
"ave_api_plan": settings.AVE_API_PLAN,
"initial_balance": 10000.0,
}
backtest = Backtest(
id=backtest_id,
bot_id=bot_id,
started_at=datetime.utcnow(),
status="running",
config={
"token": config.token,
"chain": config.chain,
"timeframe": config.timeframe,
"start_date": config.start_date,
"end_date": config.end_date,
},
)
db.add(backtest)
db.commit()
db.refresh(backtest)
db_url = str(settings.DATABASE_URL)
background_tasks.add_task(
run_backtest_sync, backtest_id, db_url, bot_id, backtest_config
)
return backtest
@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"
def get_backtest(
bot_id: str,
run_id: str,
current_user: 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"
)
backtest = (
db.query(Backtest)
.filter(Backtest.id == run_id, Backtest.bot_id == bot_id)
.first()
)
if not backtest:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Backtest not found"
)
return backtest
@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"
def list_backtests(
bot_id: str,
current_user: 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"
)
backtests = (
db.query(Backtest)
.filter(Backtest.bot_id == bot_id)
.order_by(Backtest.started_at.desc())
.all()
)
return backtests
@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"
def stop_backtest(
bot_id: str,
run_id: str,
current_user: 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"
)
backtest = (
db.query(Backtest)
.filter(Backtest.id == run_id, Backtest.bot_id == bot_id)
.first()
)
if not backtest:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Backtest not found"
)
if run_id in running_backtests:
engine = running_backtests[run_id]
asyncio.create_task(engine.stop())
backtest.status = "stopped"
backtest.ended_at = datetime.utcnow()
db.commit()
return {"status": "stopping", "run_id": run_id}

View File

@@ -4,14 +4,18 @@ 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
@@ -154,10 +158,10 @@ def delete_bot(
db.commit()
@router.post("/{bot_id}/chat", response_model=BotConversationResponse)
@router.post("/{bot_id}/chat", response_model=BotChatResponse)
def chat(
bot_id: str,
message: BotConversationCreate,
request: BotChatRequest,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
):
@@ -173,15 +177,75 @@ def chat(
detail="Not authorized to chat with this bot",
)
db_conversation = BotConversation(
bot_id=bot_id,
role=message.role,
content=message.content,
conversation_history = (
db.query(BotConversation)
.filter(BotConversation.bot_id == bot_id)
.order_by(BotConversation.created_at)
.all()
)
db.add(db_conversation)
db.commit()
db.refresh(db_conversation)
return db_conversation
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])

View File

@@ -1,38 +1,233 @@
from fastapi import APIRouter, Depends, HTTPException, status
import uuid
import asyncio
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
from sqlalchemy.orm import Session
from typing import List
from typing import List, Dict, Any, Optional
from concurrent.futures import ThreadPoolExecutor
from .auth import get_current_user
from ..core.database import get_db
from ..core.config import get_settings
from ..db.schemas import SimulationCreate, SimulationResponse
from ..db.models import Bot, Simulation, Signal, User
router = APIRouter()
running_simulations: Dict[str, Any] = {}
executor = ThreadPoolExecutor(max_workers=4)
@router.post("/bots/{bot_id}/simulate", response_model=SimulationResponse)
def start_simulation(
bot_id: str, config: SimulationCreate, db: Session = Depends(get_db)
def run_simulation_sync(
simulation_id: str, db_url: str, bot_id: str, config: Dict[str, Any]
):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
import asyncio
from ..services.simulate.engine import SimulateEngine
from ..core.database import SessionLocal
async def _run():
engine = SimulateEngine(config)
engine.run_id = simulation_id
running_simulations[simulation_id] = engine
try:
results = await engine.run()
db = SessionLocal()
try:
simulation = (
db.query(Simulation).filter(Simulation.id == simulation_id).first()
)
if simulation:
simulation.status = engine.status
simulation.signals = engine.signals
db.commit()
for signal in engine.signals:
db_signal = Signal(
id=signal["id"],
bot_id=signal["bot_id"],
run_id=signal["run_id"],
signal_type=signal["signal_type"],
token=signal["token"],
price=signal["price"],
confidence=signal.get("confidence"),
reasoning=signal.get("reasoning"),
executed=signal.get("executed", False),
created_at=signal["created_at"],
)
db.add(db_signal)
db.commit()
finally:
db.close()
finally:
if simulation_id in running_simulations:
del running_simulations[simulation_id]
asyncio.run(_run())
@router.post(
"/bots/{bot_id}/simulate",
response_model=SimulationResponse,
status_code=status.HTTP_201_CREATED,
)
async def start_simulation(
bot_id: str,
config: SimulationCreate,
background_tasks: BackgroundTasks,
current_user: 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"
)
settings = get_settings()
simulation_id = str(uuid.uuid4())
check_interval = config.check_interval
if settings.AVE_API_PLAN != "pro" and check_interval < 60:
check_interval = 60
simulation_config = {
"bot_id": bot_id,
"token": config.token,
"chain": config.chain,
"duration_seconds": config.duration_seconds,
"check_interval": check_interval,
"auto_execute": config.auto_execute,
"strategy_config": bot.strategy_config,
"ave_api_key": settings.AVE_API_KEY,
"ave_api_plan": settings.AVE_API_PLAN,
}
simulation = Simulation(
id=simulation_id,
bot_id=bot_id,
started_at=datetime.utcnow(),
status="running",
config={
"token": config.token,
"chain": config.chain,
"duration_seconds": config.duration_seconds,
"check_interval": check_interval,
"auto_execute": config.auto_execute,
},
signals=[],
)
db.add(simulation)
db.commit()
db.refresh(simulation)
db_url = str(settings.DATABASE_URL)
background_tasks.add_task(
run_simulation_sync, simulation_id, db_url, bot_id, simulation_config
)
return simulation
@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"
def get_simulation(
bot_id: str,
run_id: str,
current_user: 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"
)
simulation = (
db.query(Simulation)
.filter(Simulation.id == run_id, Simulation.bot_id == bot_id)
.first()
)
if not simulation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Simulation not found"
)
if run_id in running_simulations:
engine = running_simulations[run_id]
simulation.signals = engine.get_signals()
return simulation
@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"
def list_simulations(
bot_id: str,
current_user: 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"
)
simulations = (
db.query(Simulation)
.filter(Simulation.bot_id == bot_id)
.order_by(Simulation.started_at.desc())
.all()
)
for sim in simulations:
if sim.id in running_simulations:
engine = running_simulations[sim.id]
sim.signals = engine.get_signals()
return simulations
@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"
def stop_simulation(
bot_id: str,
run_id: str,
current_user: 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"
)
simulation = (
db.query(Simulation)
.filter(Simulation.id == run_id, Simulation.bot_id == bot_id)
.first()
)
if not simulation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Simulation not found"
)
if run_id in running_simulations:
engine = running_simulations[run_id]
asyncio.create_task(engine.stop())
simulation.status = "stopped"
db.commit()
return {"status": "stopping", "run_id": run_id}

View File

@@ -87,6 +87,7 @@ class SimulationCreate(BaseModel):
token: str
chain: str
duration_seconds: int = 3600
check_interval: int = 60
auto_execute: bool = False
@@ -118,6 +119,17 @@ class BotConversationResponse(BaseModel):
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

View File

@@ -1,15 +1,247 @@
from typing import List, Optional
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 CrewAgent:
def __init__(self, role: str, goal: str, backstory: str):
self.role = role
self.goal = goal
self.backstory = backstory
class StrategyValidator:
SUPPORTED_CONDITIONS = ["price_drop", "price_rise", "volume_spike", "price_level"]
SUPPORTED_ACTIONS = ["buy", "sell", "notify"]
def execute_task(self, task: str) -> str:
raise NotImplementedError("CrewAI agent not yet implemented")
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
def get_trading_crew():
raise NotImplementedError("Trading crew not yet implemented")
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

@@ -1,13 +1,108 @@
from typing import Optional
from typing import Optional, List, Dict, Any
import httpx
from crewai import LLM
class LLMConnector:
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):
raise NotImplementedError("LLM integration not yet implemented")
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)})
def parse_strategy(self, user_message: str) -> dict:
raise NotImplementedError("Strategy parsing not yet implemented")
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,70 @@
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

@@ -1,15 +1,324 @@
from typing import Optional, Dict, Any
import uuid
import asyncio
from datetime import datetime
from typing import Dict, Any, List, Optional
from .ave_client import AveCloudClient
class BacktestEngine:
def __init__(self, config: Dict[str, Any]):
self.config = config
self.run_id = str(uuid.uuid4())
self.status = "pending"
self.results: Optional[Dict[str, Any]] = None
self.signals: List[Dict[str, Any]] = []
self.ave_client = AveCloudClient(
api_key=config.get("ave_api_key", ""),
plan=config.get("ave_api_plan", "free"),
)
self.bot_id = config.get("bot_id")
self.strategy_config = config.get("strategy_config", {})
self.conditions = self.strategy_config.get("conditions", [])
self.actions = self.strategy_config.get("actions", [])
self.initial_balance = config.get("initial_balance", 10000.0)
self.current_balance = self.initial_balance
self.position = 0.0
self.position_token = ""
self.trades: List[Dict[str, Any]] = []
self.running = False
async def run(self) -> Dict[str, Any]:
raise NotImplementedError("Backtest engine not yet implemented")
self.running = True
self.status = "running"
started_at = datetime.utcnow()
try:
token = self.config.get("token", "")
chain = self.config.get("chain", "bsc")
timeframe = self.config.get("timeframe", "1h")
start_date = self.config.get("start_date", "")
end_date = self.config.get("end_date", "")
token_id = (
f"{token}-{chain}"
if token and not token.endswith(f"-{chain}")
else token
)
if not token_id or token_id == f"-{chain}":
raise ValueError("Token ID is required")
start_ts = None
end_ts = None
if start_date:
start_ts = int(
datetime.fromisoformat(
start_date.replace("Z", "+00:00")
).timestamp()
* 1000
)
if end_date:
end_ts = int(
datetime.fromisoformat(end_date.replace("Z", "+00:00")).timestamp()
* 1000
)
klines = await self.ave_client.get_klines(
token_id=token_id,
interval=timeframe,
limit=1000,
start_time=start_ts,
end_time=end_ts,
)
if not klines:
self.status = "failed"
self.results = {"error": "No kline data available"}
return self.results
await self._process_klines(klines)
self._calculate_metrics()
self.status = "completed"
except Exception as e:
self.status = "failed"
self.results = {"error": str(e)}
ended_at = datetime.utcnow()
self.results = self.results or {}
self.results["started_at"] = started_at
self.results["ended_at"] = ended_at
self.results["duration_seconds"] = (ended_at - started_at).total_seconds()
return self.results
async def _process_klines(self, klines: List[Dict[str, Any]]):
for i, kline in enumerate(klines):
if not self.running:
break
price = float(kline.get("close", 0))
if price <= 0:
continue
timestamp = kline.get("timestamp", 0)
for condition in self.conditions:
if self._check_condition(condition, klines, i, price):
await self._execute_actions(price, timestamp, condition)
break
def _check_condition(
self,
condition: Dict[str, Any],
klines: List[Dict[str, Any]],
current_idx: int,
current_price: float,
) -> bool:
cond_type = condition.get("type", "")
threshold = condition.get("threshold", 0)
timeframe = condition.get("timeframe", "1h")
price_level = condition.get("price")
direction = condition.get("direction", "above")
if cond_type == "price_drop":
if current_idx == 0:
return False
prev_price = float(klines[current_idx - 1].get("close", 0))
if prev_price <= 0:
return False
drop_pct = ((prev_price - current_price) / prev_price) * 100
return drop_pct >= threshold
elif cond_type == "price_rise":
if current_idx == 0:
return False
prev_price = float(klines[current_idx - 1].get("close", 0))
if prev_price <= 0:
return False
rise_pct = ((current_price - prev_price) / prev_price) * 100
return rise_pct >= threshold
elif cond_type == "volume_spike":
if current_idx == 0:
return False
prev_volume = float(klines[current_idx - 1].get("volume", 0))
current_volume = float(kline.get("volume", 0))
if prev_volume <= 0:
return False
volume_increase = ((current_volume - prev_volume) / prev_volume) * 100
return volume_increase >= threshold
elif cond_type == "price_level":
if price_level is None:
return False
if direction == "above":
return current_price > price_level
else:
return current_price < price_level
return False
async def _execute_actions(
self, price: float, timestamp: int, matched_condition: Dict[str, Any]
):
token = matched_condition.get("token", self.config.get("token", ""))
for action in self.actions:
action_type = action.get("type", "")
amount_percent = action.get("amount_percent", 10)
amount = self.current_balance * (amount_percent / 100)
if action_type == "buy" and self.current_balance >= amount:
self.position += amount / price
self.current_balance -= amount
self.position_token = token
self.trades.append(
{
"type": "buy",
"token": token,
"price": price,
"amount": amount,
"quantity": amount / price,
"timestamp": timestamp,
}
)
self.signals.append(
{
"id": str(uuid.uuid4()),
"bot_id": self.bot_id,
"run_id": self.run_id,
"signal_type": "buy",
"token": token,
"price": price,
"confidence": 0.8,
"reasoning": f"Condition {matched_condition.get('type')} triggered buy",
"executed": False,
"created_at": datetime.utcnow(),
}
)
elif action_type == "sell" and self.position > 0:
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,
}
)
self.position = 0
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": 0.8,
"reasoning": f"Condition {matched_condition.get('type')} triggered sell",
"executed": False,
"created_at": datetime.utcnow(),
}
)
def _calculate_metrics(self):
final_balance = self.current_balance + (
self.position * self.trades[-1]["price"]
if self.trades and self.position > 0
else 0
)
total_return = (
(final_balance - self.initial_balance) / self.initial_balance
) * 100
buy_trades = [t for t in self.trades if t["type"] == "buy"]
sell_trades = [t for t in self.trades if t["type"] == "sell"]
total_trades = len(buy_trades) + len(sell_trades)
winning_trades = 0
for i, trade in enumerate(sell_trades):
if i < len(buy_trades):
buy_price = buy_trades[i]["price"]
sell_price = trade["price"]
if sell_price > buy_price:
winning_trades += 1
win_rate = (winning_trades / len(sell_trades) * 100) if sell_trades else 0
portfolio_values = []
running_balance = self.initial_balance
running_position = 0.0
current_token = ""
last_price = 0.0
for trade in self.trades:
if trade["type"] == "buy":
running_position = trade["quantity"]
running_balance = trade["amount"]
current_token = trade["token"]
last_price = trade["price"]
else:
running_balance = trade["amount"]
running_position = 0
last_price = trade["price"]
portfolio_value = running_balance + (running_position * last_price)
portfolio_values.append(portfolio_value)
max_value = self.initial_balance
max_drawdown = 0.0
for value in portfolio_values:
if value > max_value:
max_value = value
drawdown = ((max_value - value) / max_value) * 100
if drawdown > max_drawdown:
max_drawdown = drawdown
sharpe_ratio = 0.0
if len(portfolio_values) > 1:
returns = []
for i in range(1, len(portfolio_values)):
ret = (
portfolio_values[i] - portfolio_values[i - 1]
) / portfolio_values[i - 1]
returns.append(ret)
if returns:
avg_return = sum(returns) / len(returns)
variance = sum((r - avg_return) ** 2 for r in returns) / len(returns)
std_dev = variance**0.5
if std_dev > 0:
sharpe_ratio = avg_return / std_dev
buy_signals = len(buy_trades)
sell_signals = len(sell_trades)
self.results = {
"total_return": round(total_return, 2),
"win_rate": round(win_rate, 2),
"total_trades": total_trades,
"buy_signals": buy_signals,
"sell_signals": sell_signals,
"max_drawdown": round(max_drawdown, 2),
"sharpe_ratio": round(sharpe_ratio, 2),
"final_balance": round(final_balance, 2),
"signals": self.signals,
}
async def stop(self):
raise NotImplementedError("Backtest stop not yet implemented")
self.running = False
self.status = "stopped"
self._calculate_metrics()
def get_results(self) -> Dict[str, Any]:
raise NotImplementedError("Backtest results not yet implemented")
return {
"id": self.run_id,
"status": self.status,
"results": self.results,
"signals": self.signals,
}

View File

@@ -1,15 +1,177 @@
from typing import Optional, Dict, Any, List
import uuid
import asyncio
from datetime import datetime
from typing import Dict, Any, List, Optional
from ..backtest.ave_client import AveCloudClient
class SimulateEngine:
def __init__(self, config: Dict[str, Any]):
self.config = config
self.run_id = str(uuid.uuid4())
self.status = "pending"
self.results: Optional[Dict[str, Any]] = None
self.signals: List[Dict[str, Any]] = []
self.ave_client = AveCloudClient(
api_key=config.get("ave_api_key", ""),
plan=config.get("ave_api_plan", "free"),
)
self.bot_id = config.get("bot_id")
self.strategy_config = config.get("strategy_config", {})
self.conditions = self.strategy_config.get("conditions", [])
self.actions = self.strategy_config.get("actions", [])
self.check_interval = config.get("check_interval", 60)
self.duration_seconds = config.get("duration_seconds", 3600)
self.auto_execute = config.get("auto_execute", False)
self.token = config.get("token", "")
self.chain = config.get("chain", "bsc")
self.running = False
self.started_at: Optional[datetime] = None
self.last_price: Optional[float] = None
self.last_volume: Optional[float] = None
async def run(self) -> List[Dict[str, Any]]:
raise NotImplementedError("Simulation engine not yet implemented")
async def run(self) -> Dict[str, Any]:
self.running = True
self.status = "running"
self.started_at = datetime.utcnow()
token_id = (
f"{self.token}-{self.chain}"
if self.token and not self.token.endswith(f"-{self.chain}")
else self.token
)
if not token_id or token_id == f"-{self.chain}":
self.status = "failed"
self.results = {"error": "Token ID is required"}
return self.results
end_time = datetime.utcnow().timestamp() + self.duration_seconds
try:
while self.running and datetime.utcnow().timestamp() < end_time:
try:
price_data = await self.ave_client.get_token_price(token_id)
if price_data:
current_price = float(price_data.get("price", 0))
current_volume = float(price_data.get("volume", 0))
if current_price > 0:
await self._check_conditions(
current_price, current_volume, price_data
)
self.last_price = current_price
self.last_volume = current_volume
except Exception as e:
pass
for _ in range(self.check_interval):
if not self.running:
break
await asyncio.sleep(1)
if self.running:
self.status = "completed"
else:
self.status = "stopped"
except Exception as e:
self.status = "failed"
self.results = {"error": str(e)}
self.results = self.results or {}
self.results["total_signals"] = len(self.signals)
self.results["signals"] = self.signals
self.results["started_at"] = self.started_at
self.results["ended_at"] = datetime.utcnow()
return self.results
async def _check_conditions(
self, current_price: float, current_volume: float, price_data: Dict[str, Any]
):
timestamp = int(datetime.utcnow().timestamp() * 1000)
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_condition(
self,
condition: Dict[str, Any],
current_price: float,
current_volume: float,
) -> bool:
cond_type = condition.get("type", "")
threshold = condition.get("threshold", 0)
price_level = condition.get("price")
direction = condition.get("direction", "above")
if cond_type == "price_drop":
if self.last_price is None or self.last_price <= 0:
return False
drop_pct = ((self.last_price - current_price) / self.last_price) * 100
return drop_pct >= threshold
elif cond_type == "price_rise":
if self.last_price is None or self.last_price <= 0:
return False
rise_pct = ((current_price - self.last_price) / self.last_price) * 100
return rise_pct >= threshold
elif cond_type == "volume_spike":
if self.last_volume is None or self.last_volume <= 0:
return False
volume_increase = (
(current_volume - self.last_volume) / self.last_volume
) * 100
return volume_increase >= threshold
elif cond_type == "price_level":
if price_level is None:
return False
if direction == "above":
return current_price > price_level
else:
return current_price < price_level
return False
async def _execute_actions(
self, price: float, timestamp: int, matched_condition: Dict[str, Any]
):
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(),
}
self.signals.append(signal)
async def stop(self):
raise NotImplementedError("Simulation stop not yet implemented")
self.running = False
self.status = "stopped"
def get_results(self) -> Dict[str, Any]:
return {
"id": self.run_id,
"status": self.status,
"results": self.results,
"signals": self.signals,
}
def get_signals(self) -> List[Dict[str, Any]]:
raise NotImplementedError("Simulation signals not yet implemented")
return self.signals

View File

@@ -1,2 +0,0 @@
VITE_API_URL=http://localhost:8000/api
VITE_WS_URL=ws://localhost:8000/ws

View File

@@ -1,23 +0,0 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View File

@@ -1 +0,0 @@
engine-strict=true

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

View File

@@ -1,42 +0,0 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
npx sv@0.15.0 create --template minimal --types ts --no-install .
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +0,0 @@
{
"name": "frontend",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/kit": "^2.57.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"svelte": "^5.55.2",
"svelte-check": "^4.4.6",
"typescript": "^6.0.2",
"vite": "^8.0.7"
}
}

View File

@@ -1,13 +0,0 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View File

@@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="text-scale" content="scale" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -1,209 +0,0 @@
import type {
User,
Bot,
BotConversation,
Backtest,
Simulation,
Signal,
AuthResponse,
BotChatRequest,
BotChatResponse,
StrategyConfig
} from './types';
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/api';
function getAuthHeaders(): HeadersInit {
const token = localStorage.getItem('token');
return token ? { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } : { 'Content-Type': 'application/json' };
}
async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'An error occurred' }));
throw new Error(error.detail || `HTTP error ${response.status}`);
}
return response.json();
}
export const api = {
auth: {
async register(email: string, password: string): Promise<AuthResponse> {
const response = await fetch(`${API_URL}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
return handleResponse<AuthResponse>(response);
},
async login(email: string, password: string): Promise<AuthResponse> {
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
return handleResponse<AuthResponse>(response);
},
async logout(): Promise<void> {
await fetch(`${API_URL}/auth/logout`, {
method: 'POST',
headers: getAuthHeaders()
});
},
async me(): Promise<User> {
const response = await fetch(`${API_URL}/auth/me`, {
headers: getAuthHeaders()
});
return handleResponse<User>(response);
}
},
bots: {
async list(): Promise<Bot[]> {
const response = await fetch(`${API_URL}/bots`, {
headers: getAuthHeaders()
});
return handleResponse<Bot[]>(response);
},
async create(name: string, description?: string): Promise<Bot> {
const response = await fetch(`${API_URL}/bots`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ name, description })
});
return handleResponse<Bot>(response);
},
async get(id: string): Promise<Bot> {
const response = await fetch(`${API_URL}/bots/${id}`, {
headers: getAuthHeaders()
});
return handleResponse<Bot>(response);
},
async update(id: string, data: Partial<Bot>): Promise<Bot> {
const response = await fetch(`${API_URL}/bots/${id}`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify(data)
});
return handleResponse<Bot>(response);
},
async delete(id: string): Promise<void> {
const response = await fetch(`${API_URL}/bots/${id}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
},
async chat(id: string, message: string): Promise<BotChatResponse> {
const response = await fetch(`${API_URL}/bots/${id}/chat`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ message } as BotChatRequest)
});
return handleResponse<BotChatResponse>(response);
},
async getHistory(id: string): Promise<BotConversation[]> {
const response = await fetch(`${API_URL}/bots/${id}/history`, {
headers: getAuthHeaders()
});
return handleResponse<BotConversation[]>(response);
}
},
backtest: {
async start(botId: string, config: { token: string; timeframe: string; start_date: string; end_date: string }): Promise<Backtest> {
const response = await fetch(`${API_URL}/bots/${botId}/backtest`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(config)
});
return handleResponse<Backtest>(response);
},
async get(botId: string, runId: string): Promise<Backtest> {
const response = await fetch(`${API_URL}/bots/${botId}/backtest/${runId}`, {
headers: getAuthHeaders()
});
return handleResponse<Backtest>(response);
},
async list(botId: string): Promise<Backtest[]> {
const response = await fetch(`${API_URL}/bots/${botId}/backtests`, {
headers: getAuthHeaders()
});
return handleResponse<Backtest[]>(response);
},
async stop(botId: string, runId: string): Promise<void> {
const response = await fetch(`${API_URL}/bots/${botId}/backtest/${runId}/stop`, {
method: 'POST',
headers: getAuthHeaders()
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
}
},
simulate: {
async start(botId: string, config: { token: string; interval_seconds: number; auto_execute: boolean }): Promise<Simulation> {
const response = await fetch(`${API_URL}/bots/${botId}/simulate`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(config)
});
return handleResponse<Simulation>(response);
},
async get(botId: string, runId: string): Promise<Simulation> {
const response = await fetch(`${API_URL}/bots/${botId}/simulate/${runId}`, {
headers: getAuthHeaders()
});
return handleResponse<Simulation>(response);
},
async list(botId: string): Promise<Simulation[]> {
const response = await fetch(`${API_URL}/bots/${botId}/simulations`, {
headers: getAuthHeaders()
});
return handleResponse<Simulation[]>(response);
},
async stop(botId: string, runId: string): Promise<void> {
const response = await fetch(`${API_URL}/bots/${botId}/simulate/${runId}/stop`, {
method: 'POST',
headers: getAuthHeaders()
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
}
},
config: {
async getChains(): Promise<string[]> {
const response = await fetch(`${API_URL}/config/chains`, {
headers: getAuthHeaders()
});
return handleResponse<string[]>(response);
},
async getTokens(): Promise<{ symbol: string; chain: string; name: string }[]> {
const response = await fetch(`${API_URL}/config/tokens`, {
headers: getAuthHeaders()
});
return handleResponse<{ symbol: string; chain: string; name: string }[]>(response);
}
}
};

View File

@@ -1,2 +0,0 @@
export { api } from './client';
export * from './types';

View File

@@ -1,128 +0,0 @@
export interface User {
id: string;
email: string;
created_at: string;
updated_at: string;
}
export interface Bot {
id: string;
user_id: string;
name: string;
description: string | null;
strategy_config: StrategyConfig;
llm_config: LLMConfig;
status: 'draft' | 'active' | 'paused';
created_at: string;
updated_at: string;
}
export interface StrategyConfig {
conditions: Condition[];
actions: Action[];
risk_management?: RiskManagement;
}
export interface Condition {
type: 'price_drop' | 'price_rise' | 'volume_spike' | 'price_level';
token: string;
chain?: string;
threshold?: number;
price?: number;
direction?: 'above' | 'below';
timeframe?: string;
}
export interface Action {
type: 'buy' | 'sell' | 'hold';
amount_percent?: number;
token?: string;
}
export interface RiskManagement {
stop_loss_percent?: number;
take_profit_percent?: number;
}
export interface LLMConfig {
model: string;
temperature: number;
}
export interface BotConversation {
id: string;
bot_id: string;
role: 'user' | 'assistant' | 'system';
content: string;
created_at: string;
}
export interface Backtest {
id: string;
bot_id: string;
started_at: string;
ended_at: string | null;
status: 'running' | 'completed' | 'failed';
config: BacktestConfig;
result: BacktestResult | null;
}
export interface BacktestConfig {
token: string;
timeframe: string;
start_date: string;
end_date: string;
}
export interface BacktestResult {
total_return: number;
win_rate: number;
total_trades: number;
buy_signals: number;
sell_signals: number;
max_drawdown: number;
sharpe_ratio: number;
}
export interface Simulation {
id: string;
bot_id: string;
started_at: string;
status: 'running' | 'stopped';
config: SimulationConfig;
signals: Signal[] | null;
}
export interface SimulationConfig {
token: string;
interval_seconds: number;
auto_execute: boolean;
}
export interface Signal {
id: string;
bot_id: string;
run_id: string;
signal_type: 'buy' | 'sell' | 'hold';
token: string;
price: number;
confidence: number | null;
reasoning: string | null;
executed: boolean;
created_at: string;
}
export interface AuthResponse {
access_token: string;
token_type: string;
}
export interface BotChatRequest {
message: string;
}
export interface BotChatResponse {
response: string;
strategy_config: StrategyConfig | null;
success: boolean;
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@@ -1,50 +0,0 @@
import { writable, get } from 'svelte/store';
import { api } from '$lib/api';
import { setUser, clearUser, clearBots } from './index';
import { clearSimulationState } from './simulationStore';
import { clearBacktestState } from './backtestStore';
export const isAuthenticated = writable(false);
export const isLoading = writable(true);
export async function initAuth() {
isLoading.set(true);
const token = localStorage.getItem('token');
if (token) {
try {
const user = await api.auth.me();
setUser(user);
isAuthenticated.set(true);
} catch {
localStorage.removeItem('token');
isAuthenticated.set(false);
}
}
isLoading.set(false);
}
export async function login(email: string, password: string) {
const response = await api.auth.login(email, password);
localStorage.setItem('token', response.access_token);
const user = await api.auth.me();
setUser(user);
isAuthenticated.set(true);
}
export async function register(email: string, password: string) {
const response = await api.auth.register(email, password);
localStorage.setItem('token', response.access_token);
const user = await api.auth.me();
setUser(user);
isAuthenticated.set(true);
}
export function logout() {
api.auth.logout().catch(() => {});
localStorage.removeItem('token');
clearUser();
clearBots();
clearBacktestState();
clearSimulationState();
isAuthenticated.set(false);
}

View File

@@ -1,45 +0,0 @@
import { writable } from 'svelte/store';
import type { Backtest, BacktestResult } from '$lib/api';
export interface BacktestState {
currentBacktest: Backtest | null;
backtestHistory: Backtest[];
isLoading: boolean;
error: string | null;
}
const initialState: BacktestState = {
currentBacktest: null,
backtestHistory: [],
isLoading: false,
error: null
};
export const backtestStore = writable<BacktestState>(initialState);
export function setCurrentBacktest(backtest: Backtest | null) {
backtestStore.update(state => ({ ...state, currentBacktest: backtest }));
}
export function addBacktestToHistory(backtest: Backtest) {
backtestStore.update(state => ({
...state,
backtestHistory: [backtest, ...state.backtestHistory]
}));
}
export function setBacktestHistory(backtests: Backtest[]) {
backtestStore.update(state => ({ ...state, backtestHistory: backtests }));
}
export function setBacktestLoading(loading: boolean) {
backtestStore.update(state => ({ ...state, isLoading: loading }));
}
export function setBacktestError(error: string | null) {
backtestStore.update(state => ({ ...state, error }));
}
export function clearBacktestState() {
backtestStore.set(initialState);
}

View File

@@ -1,24 +0,0 @@
import { writable } from 'svelte/store';
import type { Bot } from '$lib/api';
export const botsStore = writable<Bot[]>([]);
export function setBots(bots: Bot[]) {
botsStore.set(bots);
}
export function addBot(bot: Bot) {
botsStore.update(bots => [...bots, bot]);
}
export function updateBot(bot: Bot) {
botsStore.update(bots => bots.map(b => b.id === bot.id ? bot : b));
}
export function removeBot(botId: string) {
botsStore.update(bots => bots.filter(b => b.id !== botId));
}
export function clearBots() {
botsStore.set([]);
}

View File

@@ -1,33 +0,0 @@
import { writable } from 'svelte/store';
import type { BotConversation } from '$lib/api';
export interface ChatMessage {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: Date;
}
export const chatStore = writable<ChatMessage[]>([]);
export function addMessage(message: Omit<ChatMessage, 'id' | 'timestamp'>) {
const newMessage: ChatMessage = {
...message,
id: crypto.randomUUID(),
timestamp: new Date()
};
chatStore.update(messages => [...messages, newMessage]);
}
export function setMessages(messages: BotConversation[]) {
chatStore.set(messages.map(m => ({
id: m.id,
role: m.role,
content: m.content,
timestamp: new Date(m.created_at)
})));
}
export function clearChat() {
chatStore.set([]);
}

View File

@@ -1,12 +0,0 @@
import { writable } from 'svelte/store';
import type { Bot } from '$lib/api';
export const currentBotStore = writable<Bot | null>(null);
export function setCurrentBot(bot: Bot | null) {
currentBotStore.set(bot);
}
export function clearCurrentBot() {
currentBotStore.set(null);
}

View File

@@ -1,30 +0,0 @@
export { userStore, setUser, clearUser } from './userStore';
export { botsStore, setBots, addBot, updateBot, removeBot, clearBots } from './botsStore';
export { currentBotStore, setCurrentBot, clearCurrentBot } from './currentBotStore';
export { chatStore, addMessage, setMessages, clearChat } from './chatStore';
export {
backtestStore,
setCurrentBacktest,
addBacktestToHistory,
setBacktestHistory,
setBacktestLoading,
setBacktestError,
clearBacktestState
} from './backtestStore';
export {
simulationStore,
setCurrentSimulation,
addSignals,
clearSignals,
setSimulationLoading,
setSimulationError,
clearSimulationState
} from './simulationStore';
export {
isAuthenticated,
isLoading,
initAuth,
login,
register,
logout
} from './authStore';

View File

@@ -1,45 +0,0 @@
import { writable } from 'svelte/store';
import type { Simulation, Signal } from '$lib/api';
export interface SimulationState {
currentSimulation: Simulation | null;
signals: Signal[];
isLoading: boolean;
error: string | null;
}
const initialState: SimulationState = {
currentSimulation: null,
signals: [],
isLoading: false,
error: null
};
export const simulationStore = writable<SimulationState>(initialState);
export function setCurrentSimulation(simulation: Simulation | null) {
simulationStore.update(state => ({ ...state, currentSimulation: simulation }));
}
export function addSignals(newSignals: Signal[]) {
simulationStore.update(state => ({
...state,
signals: [...state.signals, ...newSignals]
}));
}
export function clearSignals() {
simulationStore.update(state => ({ ...state, signals: [] }));
}
export function setSimulationLoading(loading: boolean) {
simulationStore.update(state => ({ ...state, isLoading: loading }));
}
export function setSimulationError(error: string | null) {
simulationStore.update(state => ({ ...state, error }));
}
export function clearSimulationState() {
simulationStore.set(initialState);
}

View File

@@ -1,12 +0,0 @@
import { writable } from 'svelte/store';
import type { User } from '$lib/api';
export const userStore = writable<User | null>(null);
export function setUser(user: User | null) {
userStore.set(user);
}
export function clearUser() {
userStore.set(null);
}

View File

@@ -1,54 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { initAuth, isLoading } from '$lib/stores';
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
onMount(() => {
initAuth();
});
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
{#if $isLoading}
<div class="loading">
<div class="spinner"></div>
</div>
{:else}
{@render children()}
{/if}
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
.loading {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -1,135 +0,0 @@
<script lang="ts">
import { isAuthenticated } from '$lib/stores';
</script>
<svelte:head>
<title>Randebu - AI Trading Bot Platform</title>
</svelte:head>
{#if $isAuthenticated}
<script>
window.location.href = '/dashboard';
</script>
{:else}
<main>
<div class="hero">
<h1>Randebu</h1>
<p class="tagline">Create trading bots through conversation with AI</p>
<div class="cta">
<a href="/register" class="btn btn-primary">Get Started</a>
<a href="/login" class="btn btn-secondary">Login</a>
</div>
</div>
<section class="features">
<h2>How It Works</h2>
<div class="feature-grid">
<div class="feature">
<h3>1. Describe Your Strategy</h3>
<p>Tell our AI what kind of trading you want to do in plain English</p>
</div>
<div class="feature">
<h3>2. Backtest & Validate</h3>
<p>Test your strategy against historical data before risking real funds</p>
</div>
<div class="feature">
<h3>3. Simulate & Monitor</h3>
<p>Run real-time simulations and watch for trading signals</p>
</div>
</div>
</section>
</main>
{/if}
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
}
.hero {
text-align: center;
max-width: 600px;
}
h1 {
font-size: 3.5rem;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.tagline {
font-size: 1.25rem;
color: #aaa;
margin: 1rem 0 2rem;
}
.cta {
display: flex;
gap: 1rem;
justify-content: center;
}
.btn {
padding: 0.75rem 1.5rem;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
transition: transform 0.2s, opacity 0.2s;
}
.btn:hover {
transform: translateY(-2px);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.features {
margin-top: 4rem;
text-align: center;
}
.features h2 {
font-size: 2rem;
margin-bottom: 2rem;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
max-width: 800px;
}
.feature h3 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.feature p {
color: #888;
font-size: 0.9rem;
}
</style>

View File

@@ -1,354 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, chatStore, addMessage, setMessages, clearChat, currentBotStore, setCurrentBot } from '$lib/stores';
import { api } from '$lib/api';
let botId = $derived($page.params.id);
let messageInput = $state('');
let isSending = $state(false);
let chatContainer: HTMLDivElement;
onMount(async () => {
if (!$isAuthenticated && !$isLoading) {
goto('/login');
return;
}
if ($isAuthenticated && botId) {
await loadBot();
await loadChatHistory();
}
});
async function loadBot() {
try {
const bot = await api.bots.get(botId);
setCurrentBot(bot);
} catch (e) {
goto('/dashboard');
}
}
async function loadChatHistory() {
try {
const history = await api.bots.getHistory(botId);
setMessages(history);
scrollToBottom();
} catch (e) {
console.error('Failed to load chat history:', e);
}
}
async function sendMessage() {
if (!messageInput.trim() || isSending) return;
const userMessage = messageInput;
messageInput = '';
isSending = true;
addMessage({ role: 'user', content: userMessage });
scrollToBottom();
try {
const response = await api.bots.chat(botId, userMessage);
addMessage({ role: 'assistant', content: response.response });
if (response.strategy_config) {
const bot = await api.bots.get(botId);
setCurrentBot(bot);
}
} catch (e) {
addMessage({ role: 'assistant', content: 'Sorry, I encountered an error. Please try again.' });
} finally {
isSending = false;
scrollToBottom();
}
}
function scrollToBottom() {
setTimeout(() => {
if (chatContainer) {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
}, 50);
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
</script>
<svelte:head>
<title>{$currentBotStore?.name || 'Bot'} - Randebu</title>
</svelte:head>
<main>
<header>
<div class="header-left">
<a href="/dashboard" class="back-link">← Dashboard</a>
<h1>{$currentBotStore?.name || 'Loading...'}</h1>
</div>
<div class="header-actions">
<a href="/bot/{botId}/backtest" class="btn btn-secondary">Backtest</a>
<a href="/bot/{botId}/simulate" class="btn btn-secondary">Simulate</a>
</div>
</header>
<div class="chat-container" bind:this={chatContainer}>
{#if $chatStore.length === 0}
<div class="welcome-message">
<p>Welcome to {$currentBotStore?.name}! Describe your trading strategy in plain English.</p>
<p class="hint">Example: "Buy PEPE when the price drops by 5% within 1 hour"</p>
</div>
{/if}
{#each $chatStore as message}
<div class="message {message.role}">
<div class="message-content">
{message.content}
</div>
<div class="message-time">
{message.timestamp.toLocaleTimeString()}
</div>
</div>
{/each}
{#if isSending}
<div class="message assistant">
<div class="message-content typing">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div>
{/if}
</div>
{#if $currentBotStore}
<div class="input-container">
<textarea
bind:value={messageInput}
onkeydown={handleKeydown}
placeholder="Describe your trading strategy..."
rows="1"
disabled={isSending}
></textarea>
<button onclick={sendMessage} disabled={isSending || !messageInput.trim()}>
Send
</button>
</div>
{/if}
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
height: 100vh;
display: flex;
flex-direction: column;
max-width: 900px;
margin: 0 auto;
padding: 1rem;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 1rem;
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.back-link {
color: #667eea;
text-decoration: none;
font-size: 0.9rem;
}
.back-link:hover {
text-decoration: underline;
}
h1 {
margin: 0;
font-size: 1.5rem;
}
.header-actions {
display: flex;
gap: 0.75rem;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
font-size: 0.9rem;
cursor: pointer;
border: none;
transition: transform 0.2s;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn:hover {
transform: translateY(-2px);
}
.chat-container {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
.welcome-message {
text-align: center;
padding: 2rem;
color: #888;
}
.welcome-message .hint {
font-size: 0.85rem;
margin-top: 1rem;
color: #666;
}
.message {
display: flex;
flex-direction: column;
margin-bottom: 1rem;
}
.message.user {
align-items: flex-end;
}
.message.assistant {
align-items: flex-start;
}
.message-content {
max-width: 70%;
padding: 0.75rem 1rem;
border-radius: 12px;
line-height: 1.5;
}
.message.user .message-content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-bottom-right-radius: 4px;
}
.message.assistant .message-content {
background: rgba(255, 255, 255, 0.1);
border-bottom-left-radius: 4px;
}
.message-time {
font-size: 0.7rem;
color: #666;
margin-top: 0.25rem;
padding: 0 0.5rem;
}
.typing {
display: flex;
gap: 4px;
padding: 1rem 1.25rem;
}
.dot {
width: 8px;
height: 8px;
background: #888;
border-radius: 50%;
animation: typing 1.4s infinite;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 60%, 100% {
transform: translateY(0);
opacity: 0.4;
}
30% {
transform: translateY(-4px);
opacity: 1;
}
}
.input-container {
display: flex;
gap: 0.75rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
textarea {
flex: 1;
padding: 0.75rem 1rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
resize: none;
font-family: inherit;
}
textarea:focus {
outline: none;
border-color: #667eea;
}
button {
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>

View File

@@ -1,391 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, backtestStore, setCurrentBacktest, addBacktestToHistory, setBacktestLoading, setBacktestError } from '$lib/stores';
import { api } from '$lib/api';
let botId = $derived($page.params.id);
let token = $state('PEPE');
let timeframe = $state('1h');
let startDate = $state('');
let endDate = $state('');
let isRunning = $state(false);
onMount(async () => {
if (!$isAuthenticated && !$isLoading) {
goto('/login');
return;
}
if ($isAuthenticated && botId) {
await loadBot();
await loadBacktests();
}
});
async function loadBot() {
try {
const bot = await api.bots.get(botId);
setCurrentBot(bot);
} catch (e) {
goto('/dashboard');
}
}
async function loadBacktests() {
try {
const backtests = await api.backtest.list(botId);
setBacktestHistory(backtests);
} catch (e) {
console.error('Failed to load backtests:', e);
}
}
async function startBacktest() {
if (!startDate || !endDate) return;
setBacktestError(null);
setBacktestLoading(true);
isRunning = true;
try {
const backtest = await api.backtest.start(botId, {
token,
timeframe,
start_date: startDate,
end_date: endDate
});
setCurrentBacktest(backtest);
addBacktestToHistory(backtest);
} catch (e) {
setBacktestError(e instanceof Error ? e.message : 'Failed to start backtest');
} finally {
setBacktestLoading(false);
isRunning = false;
}
}
async function stopBacktest(runId: string) {
try {
await api.backtest.stop(botId, runId);
await loadBacktests();
} catch (e) {
console.error('Failed to stop backtest:', e);
}
}
function setBacktestHistory(backtests: any[]) {
backtestStore.update(state => ({ ...state, backtestHistory: backtests }));
}
</script>
<svelte:head>
<title>Backtest - {$currentBotStore?.name || 'Bot'} - Randebu</title>
</svelte:head>
<main>
<header>
<div class="header-left">
<a href="/bot/{botId}" class="back-link">← Back to Chat</a>
<h1>Backtest</h1>
</div>
</header>
<div class="content">
<section class="config-section">
<h2>Configure Backtest</h2>
{#if $backtestStore.error}
<div class="error">{$backtestStore.error}</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); startBacktest(); }}>
<div class="form-row">
<div class="field">
<label for="token">Token</label>
<input type="text" id="token" bind:value={token} required />
</div>
<div class="field">
<label for="timeframe">Timeframe</label>
<select id="timeframe" bind:value={timeframe}>
<option value="1m">1 minute</option>
<option value="5m">5 minutes</option>
<option value="15m">15 minutes</option>
<option value="1h">1 hour</option>
<option value="4h">4 hours</option>
<option value="1d">1 day</option>
</select>
</div>
</div>
<div class="form-row">
<div class="field">
<label for="startDate">Start Date</label>
<input type="date" id="startDate" bind:value={startDate} required />
</div>
<div class="field">
<label for="endDate">End Date</label>
<input type="date" id="endDate" bind:value={endDate} required />
</div>
</div>
<button type="submit" disabled={isRunning || $backtestStore.isLoading}>
{isRunning ? 'Running...' : 'Start Backtest'}
</button>
</form>
</section>
<section class="results-section">
<h2>Backtest History</h2>
{#if $backtestStore.backtestHistory.length === 0}
<p class="empty-state">No backtests yet. Run your first backtest above.</p>
{:else}
<div class="backtest-list">
{#each $backtestStore.backtestHistory as backtest}
<div class="backtest-card">
<div class="backtest-header">
<span class="backtest-status status-{backtest.status}">{backtest.status}</span>
<span class="backtest-date">{new Date(backtest.started_at).toLocaleDateString()}</span>
</div>
{#if backtest.result}
<div class="backtest-results">
<div class="result-item">
<span class="result-label">Total Return</span>
<span class="result-value" class:positive={backtest.result.total_return > 0} class:negative={backtest.result.total_return < 0}>
{backtest.result.total_return.toFixed(2)}%
</span>
</div>
<div class="result-item">
<span class="result-label">Win Rate</span>
<span class="result-value">{backtest.result.win_rate.toFixed(1)}%</span>
</div>
<div class="result-item">
<span class="result-label">Total Trades</span>
<span class="result-value">{backtest.result.total_trades}</span>
</div>
<div class="result-item">
<span class="result-label">Max Drawdown</span>
<span class="result-value negative">{backtest.result.max_drawdown.toFixed(2)}%</span>
</div>
</div>
{/if}
{#if backtest.status === 'running'}
<button onclick={() => stopBacktest(backtest.id)} class="btn btn-danger">Stop</button>
{/if}
</div>
{/each}
</div>
{/if}
</section>
</div>
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
min-height: 100vh;
max-width: 900px;
margin: 0 auto;
padding: 2rem;
}
header {
margin-bottom: 2rem;
}
.header-left {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.back-link {
color: #667eea;
text-decoration: none;
font-size: 0.9rem;
}
h1 {
margin: 0;
font-size: 1.75rem;
}
h2 {
font-size: 1.25rem;
margin: 0 0 1rem;
}
.content {
display: grid;
gap: 2rem;
}
section {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
}
.error {
background: rgba(239, 68, 68, 0.2);
border: 1px solid #ef4444;
color: #fca5a5;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
label {
font-size: 0.9rem;
color: #ccc;
}
input, select {
padding: 0.75rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 0.875rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.empty-state {
color: #888;
text-align: center;
padding: 2rem;
}
.backtest-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.backtest-card {
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 1rem;
}
.backtest-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.backtest-status {
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
.status-running {
background: rgba(59, 130, 246, 0.2);
color: #60a5fa;
}
.status-completed {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.status-failed {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
}
.backtest-date {
color: #888;
font-size: 0.85rem;
}
.backtest-results {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
}
.result-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.result-label {
font-size: 0.75rem;
color: #888;
}
.result-value {
font-size: 1.1rem;
font-weight: 500;
}
.positive {
color: #22c55e;
}
.negative {
color: #ef4444;
}
.btn-danger {
margin-top: 0.75rem;
width: auto;
padding: 0.5rem 1rem;
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.4);
}
</style>

View File

@@ -1,426 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, simulationStore, setCurrentSimulation, addSignals, clearSignals, setSimulationLoading, setSimulationError } from '$lib/stores';
import { api } from '$lib/api';
let botId = $derived($page.params.id);
let token = $state('PEPE');
let intervalSeconds = $state(60);
let autoExecute = $state(false);
let isRunning = $state(false);
onMount(async () => {
if (!$isAuthenticated && !$isLoading) {
goto('/login');
return;
}
if ($isAuthenticated && botId) {
await loadBot();
await loadSimulations();
}
});
async function loadBot() {
try {
const bot = await api.bots.get(botId);
setCurrentBot(bot);
} catch (e) {
goto('/dashboard');
}
}
async function loadSimulations() {
try {
const simulations = await api.simulate.list(botId);
if (simulations.length > 0) {
const latest = simulations[0];
setCurrentSimulation(latest);
if (latest.signals) {
addSignals(latest.signals);
}
if (latest.status === 'running') {
isRunning = true;
}
}
} catch (e) {
console.error('Failed to load simulations:', e);
}
}
async function startSimulation() {
setSimulationError(null);
setSimulationLoading(true);
isRunning = true;
try {
const simulation = await api.simulate.start(botId, {
token,
interval_seconds: intervalSeconds,
auto_execute: autoExecute
});
setCurrentSimulation(simulation);
clearSignals();
} catch (e) {
setSimulationError(e instanceof Error ? e.message : 'Failed to start simulation');
isRunning = false;
} finally {
setSimulationLoading(false);
}
}
async function stopSimulation() {
if (!$simulationStore.currentSimulation) return;
try {
await api.simulate.stop(botId, $simulationStore.currentSimulation.id);
await loadSimulations();
isRunning = false;
} catch (e) {
console.error('Failed to stop simulation:', e);
}
}
</script>
<svelte:head>
<title>Simulate - {$currentBotStore?.name || 'Bot'} - Randebu</title>
</svelte:head>
<main>
<header>
<div class="header-left">
<a href="/bot/{botId}" class="back-link">← Back to Chat</a>
<h1>Simulation</h1>
</div>
</header>
<div class="notice">
<span class="notice-icon">⚠️</span>
<span>Simulation Mode - Using REST polling (every {intervalSeconds}s). For real-time signals, consider upgrading to Pro tier.</span>
</div>
<div class="content">
<section class="config-section">
<h2>Configure Simulation</h2>
{#if $simulationStore.error}
<div class="error">{$simulationStore.error}</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); startSimulation(); }}>
<div class="form-row">
<div class="field">
<label for="token">Token</label>
<input type="text" id="token" bind:value={token} required disabled={isRunning} />
</div>
<div class="field">
<label for="interval">Check Interval (seconds)</label>
<select id="interval" bind:value={intervalSeconds} disabled={isRunning}>
<option value={30}>30 seconds</option>
<option value={60}>60 seconds</option>
<option value={120}>2 minutes</option>
<option value={300}>5 minutes</option>
</select>
</div>
</div>
<div class="field checkbox-field">
<input type="checkbox" id="autoExecute" bind:checked={autoExecute} disabled={isRunning} />
<label for="autoExecute">Auto-execute trades (requires Pro tier)</label>
</div>
{#if isRunning}
<button type="button" onclick={stopSimulation} class="btn btn-danger">
Stop Simulation
</button>
{:else}
<button type="submit" disabled={$simulationStore.isLoading}>
{$simulationStore.isLoading ? 'Starting...' : 'Start Simulation'}
</button>
{/if}
</form>
</section>
<section class="signals-section">
<h2>Signals ({$simulationStore.signals.length})</h2>
{#if $simulationStore.signals.length === 0}
<p class="empty-state">No signals yet. Start a simulation to see trading signals.</p>
{:else}
<div class="signals-list">
{#each $simulationStore.signals as signal}
<div class="signal-card">
<div class="signal-header">
<span class="signal-type type-{signal.signal_type}">{signal.signal_type}</span>
<span class="signal-token">{signal.token}</span>
<span class="signal-price">${signal.price.toFixed(6)}</span>
</div>
{#if signal.confidence}
<div class="signal-confidence">
<span>Confidence: {(signal.confidence * 100).toFixed(1)}%</span>
<div class="confidence-bar">
<div class="confidence-fill" style="width: {signal.confidence * 100}%"></div>
</div>
</div>
{/if}
{#if signal.reasoning}
<p class="signal-reasoning">{signal.reasoning}</p>
{/if}
<div class="signal-time">
{new Date(signal.created_at).toLocaleString()}
</div>
</div>
{/each}
</div>
{/if}
</section>
</div>
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
min-height: 100vh;
max-width: 900px;
margin: 0 auto;
padding: 2rem;
}
header {
margin-bottom: 1.5rem;
}
.header-left {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.back-link {
color: #667eea;
text-decoration: none;
font-size: 0.9rem;
}
h1 {
margin: 0;
font-size: 1.75rem;
}
.notice {
display: flex;
align-items: center;
gap: 0.75rem;
background: rgba(251, 191, 36, 0.1);
border: 1px solid rgba(251, 191, 36, 0.3);
border-radius: 8px;
padding: 0.75rem 1rem;
margin-bottom: 1.5rem;
font-size: 0.9rem;
color: #fbbf24;
}
.notice-icon {
font-size: 1.25rem;
}
.content {
display: grid;
gap: 2rem;
}
section {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
}
h2 {
font-size: 1.25rem;
margin: 0 0 1rem;
}
.error {
background: rgba(239, 68, 68, 0.2);
border: 1px solid #ef4444;
color: #fca5a5;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1rem;
}
.field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.checkbox-field {
flex-direction: row;
align-items: center;
margin-bottom: 1rem;
}
.checkbox-field input {
width: auto;
margin-right: 0.5rem;
}
.checkbox-field label {
margin: 0;
}
label {
font-size: 0.9rem;
color: #ccc;
}
input, select {
padding: 0.75rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 0.875rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-danger {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.4);
}
.empty-state {
color: #888;
text-align: center;
padding: 2rem;
}
.signals-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.signal-card {
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 1rem;
}
.signal-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
}
.signal-type {
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
.type-buy {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.type-sell {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.type-hold {
background: rgba(251, 191, 36, 0.2);
color: #fbbf24;
}
.signal-token {
font-weight: 500;
}
.signal-price {
color: #888;
font-size: 0.9rem;
}
.signal-confidence {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
font-size: 0.85rem;
color: #888;
}
.confidence-bar {
flex: 1;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.confidence-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 2px;
}
.signal-reasoning {
font-size: 0.9rem;
color: #ccc;
margin: 0.5rem 0;
line-height: 1.4;
}
.signal-time {
font-size: 0.75rem;
color: #666;
}
</style>

View File

@@ -1,359 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, botsStore, setBots, addBot, removeBot, userStore, logout } from '$lib/stores';
import { api } from '$lib/api';
let showCreateModal = $state(false);
let newBotName = $state('');
let newBotDescription = $state('');
let isCreating = $state(false);
let createError = $state('');
onMount(async () => {
if (!$isAuthenticated && !$isLoading) {
goto('/login');
return;
}
if ($isAuthenticated) {
await loadBots();
}
});
async function loadBots() {
try {
const bots = await api.bots.list();
setBots(bots);
} catch (e) {
console.error('Failed to load bots:', e);
}
}
async function createBot() {
if (!$newBotName.trim()) return;
createError = '';
isCreating = true;
try {
const bot = await api.bots.create(newBotName, newBotDescription);
addBot(bot);
showCreateModal = false;
newBotName = '';
newBotDescription = '';
goto(`/bot/${bot.id}`);
} catch (e) {
createError = e instanceof Error ? e.message : 'Failed to create bot';
} finally {
isCreating = false;
}
}
async function deleteBot(botId: string) {
if (!confirm('Are you sure you want to delete this bot?')) return;
try {
await api.bots.delete(botId);
removeBot(botId);
} catch (e) {
console.error('Failed to delete bot:', e);
}
}
function handleLogout() {
logout();
goto('/');
}
</script>
<svelte:head>
<title>Dashboard - Randebu</title>
</svelte:head>
<main>
<header>
<h1>Dashboard</h1>
<div class="header-actions">
<span class="user-email">{$userStore?.email}</span>
<a href="/settings" class="btn btn-secondary">Settings</a>
<button onclick={handleLogout} class="btn btn-secondary">Logout</button>
</div>
</header>
<section class="bots-section">
<div class="section-header">
<h2>Your Bots ({$botsStore.length}/3)</h2>
{#if $botsStore.length < 3}
<button onclick={() => showCreateModal = true} class="btn btn-primary">
Create New Bot
</button>
{/if}
</div>
{#if $botsStore.length === 0}
<div class="empty-state">
<p>You haven't created any bots yet.</p>
<p>Create your first bot to start trading!</p>
</div>
{:else}
<div class="bots-grid">
{#each $botsStore as bot}
<div class="bot-card">
<div class="bot-info">
<h3>{bot.name}</h3>
{#if bot.description}
<p class="bot-description">{bot.description}</p>
{/if}
<span class="bot-status status-{bot.status}">{bot.status}</span>
</div>
<div class="bot-actions">
<a href="/bot/{bot.id}" class="btn btn-primary">Open</a>
<button onclick={() => deleteBot(bot.id)} class="btn btn-danger">Delete</button>
</div>
</div>
{/each}
</div>
{/if}
</section>
{#if showCreateModal}
<div class="modal-overlay" onclick={() => showCreateModal = false}>
<div class="modal" onclick={(e) => e.stopPropagation()}>
<h2>Create New Bot</h2>
{#if createError}
<div class="error">{createError}</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); createBot(); }}>
<div class="field">
<label for="botName">Bot Name</label>
<input type="text" id="botName" bind:value={newBotName} required />
</div>
<div class="field">
<label for="botDescription">Description (optional)</label>
<textarea id="botDescription" bind:value={newBotDescription} rows="3"></textarea>
</div>
<div class="modal-actions">
<button type="button" onclick={() => showCreateModal = false} class="btn btn-secondary">
Cancel
</button>
<button type="submit" disabled={isCreating}>
{isCreating ? 'Creating...' : 'Create'}
</button>
</div>
</form>
</div>
</div>
{/if}
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
min-height: 100vh;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
h1 {
margin: 0;
font-size: 2rem;
}
.header-actions {
display: flex;
align-items: center;
gap: 1rem;
}
.user-email {
color: #888;
font-size: 0.9rem;
}
h2 {
margin: 0;
font-size: 1.25rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.bots-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.bot-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
}
.bot-info {
margin-bottom: 1rem;
}
.bot-info h3 {
margin: 0 0 0.5rem;
font-size: 1.25rem;
}
.bot-description {
color: #888;
font-size: 0.9rem;
margin: 0 0 0.75rem;
}
.bot-status {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
.status-draft {
background: rgba(251, 191, 36, 0.2);
color: #fbbf24;
}
.status-active {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.status-paused {
background: rgba(251, 191, 36, 0.2);
color: #fbbf24;
}
.bot-actions {
display: flex;
gap: 0.75rem;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
font-size: 0.9rem;
cursor: pointer;
border: none;
transition: transform 0.2s, opacity 0.2s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.btn-danger {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.4);
}
.btn:hover {
transform: translateY(-2px);
}
.empty-state {
text-align: center;
padding: 3rem;
color: #888;
}
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal {
background: #1a1a1a;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 2rem;
width: 100%;
max-width: 450px;
}
.modal h2 {
margin: 0 0 1.5rem;
}
.error {
background: rgba(239, 68, 68, 0.2);
border: 1px solid #ef4444;
color: #fca5a5;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.field {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #ccc;
font-size: 0.9rem;
}
input, textarea {
width: 100%;
padding: 0.75rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
box-sizing: border-box;
}
input:focus, textarea:focus {
outline: none;
border-color: #667eea;
}
.modal-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 1.5rem;
}
</style>

View File

@@ -1,176 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { login, isAuthenticated } from '$lib/stores';
let email = $state('');
let password = $state('');
let error = $state('');
let isLoading = $state(false);
$effect(() => {
if ($isAuthenticated) {
goto('/dashboard');
}
});
async function handleSubmit() {
error = '';
isLoading = true;
try {
await login(email, password);
goto('/dashboard');
} catch (e) {
error = e instanceof Error ? e.message : 'Login failed';
} finally {
isLoading = false;
}
}
</script>
<svelte:head>
<title>Login - Randebu</title>
</svelte:head>
<main>
<div class="auth-card">
<h1>Login</h1>
<p class="subtitle">Welcome back</p>
{#if error}
<div class="error">{error}</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" bind:value={email} required />
</div>
<div class="field">
<label for="password">Password</label>
<input type="password" id="password" bind:value={password} required />
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</button>
</form>
<p class="footer">
Don't have an account? <a href="/register">Register</a>
</p>
</div>
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.auth-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 2.5rem;
width: 100%;
max-width: 400px;
}
h1 {
margin: 0;
font-size: 1.75rem;
text-align: center;
}
.subtitle {
color: #888;
text-align: center;
margin: 0.5rem 0 2rem;
}
.error {
background: rgba(239, 68, 68, 0.2);
border: 1px solid #ef4444;
color: #fca5a5;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.field {
margin-bottom: 1.25rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #ccc;
font-size: 0.9rem;
}
input {
width: 100%;
padding: 0.75rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 0.875rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.footer {
text-align: center;
margin-top: 1.5rem;
color: #888;
font-size: 0.9rem;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
</style>

View File

@@ -1,193 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { register, isAuthenticated } from '$lib/stores';
let email = $state('');
let password = $state('');
let confirmPassword = $state('');
let error = $state('');
let isLoading = $state(false);
$effect(() => {
if ($isAuthenticated) {
goto('/dashboard');
}
});
async function handleSubmit() {
error = '';
if (password !== confirmPassword) {
error = 'Passwords do not match';
return;
}
if (password.length < 6) {
error = 'Password must be at least 6 characters';
return;
}
isLoading = true;
try {
await register(email, password);
goto('/dashboard');
} catch (e) {
error = e instanceof Error ? e.message : 'Registration failed';
} finally {
isLoading = false;
}
}
</script>
<svelte:head>
<title>Register - Randebu</title>
</svelte:head>
<main>
<div class="auth-card">
<h1>Create Account</h1>
<p class="subtitle">Start creating trading bots</p>
{#if error}
<div class="error">{error}</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" bind:value={email} required />
</div>
<div class="field">
<label for="password">Password</label>
<input type="password" id="password" bind:value={password} required minlength="6" />
</div>
<div class="field">
<label for="confirmPassword">Confirm Password</label>
<input type="password" id="confirmPassword" bind:value={confirmPassword} required />
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Creating account...' : 'Create Account'}
</button>
</form>
<p class="footer">
Already have an account? <a href="/login">Login</a>
</p>
</div>
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.auth-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 2.5rem;
width: 100%;
max-width: 400px;
}
h1 {
margin: 0;
font-size: 1.75rem;
text-align: center;
}
.subtitle {
color: #888;
text-align: center;
margin: 0.5rem 0 2rem;
}
.error {
background: rgba(239, 68, 68, 0.2);
border: 1px solid #ef4444;
color: #fca5a5;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.field {
margin-bottom: 1.25rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #ccc;
font-size: 0.9rem;
}
input {
width: 100%;
padding: 0.75rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 0.875rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.footer {
text-align: center;
margin-top: 1.5rem;
color: #888;
font-size: 0.9rem;
}
.footer a {
color: #667eea;
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
</style>

View File

@@ -1,281 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, userStore, logout } from '$lib/stores';
import { api } from '$lib/api';
let email = $state('');
let currentPassword = $state('');
let newPassword = $state('');
let confirmPassword = $state('');
let isUpdating = $state(false);
let updateSuccess = $state('');
let updateError = $state('');
onMount(async () => {
if (!$isAuthenticated && !$isLoading) {
goto('/login');
return;
}
if ($userStore) {
email = $userStore.email;
}
});
async function updateEmail() {
updateSuccess = '';
updateError = '';
isUpdating = true;
try {
await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8000/api'}/auth/settings`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ email })
});
updateSuccess = 'Email updated successfully';
} catch (e) {
updateError = e instanceof Error ? e.message : 'Failed to update email';
} finally {
isUpdating = false;
}
}
async function updatePassword() {
updateSuccess = '';
updateError = '';
if (newPassword !== confirmPassword) {
updateError = 'Passwords do not match';
return;
}
if (newPassword.length < 6) {
updateError = 'Password must be at least 6 characters';
return;
}
isUpdating = true;
try {
await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8000/api'}/auth/settings`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({ password: newPassword, current_password: currentPassword })
});
updateSuccess = 'Password updated successfully';
currentPassword = '';
newPassword = '';
confirmPassword = '';
} catch (e) {
updateError = e instanceof Error ? e.message : 'Failed to update password';
} finally {
isUpdating = false;
}
}
function handleLogout() {
logout();
goto('/');
}
</script>
<svelte:head>
<title>Settings - Randebu</title>
</svelte:head>
<main>
<header>
<div class="header-left">
<a href="/dashboard" class="back-link">← Dashboard</a>
<h1>Settings</h1>
</div>
</header>
<div class="content">
<section class="settings-section">
<h2>Profile</h2>
{#if updateSuccess}
<div class="success">{updateSuccess}</div>
{/if}
{#if updateError}
<div class="error">{updateError}</div>
{/if}
<form onsubmit={(e) => { e.preventDefault(); updateEmail(); }}>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" bind:value={email} required />
</div>
<button type="submit" disabled={isUpdating}>
{isUpdating ? 'Updating...' : 'Update Email'}
</button>
</form>
</section>
<section class="settings-section">
<h2>Change Password</h2>
<form onsubmit={(e) => { e.preventDefault(); updatePassword(); }}>
<div class="field">
<label for="currentPassword">Current Password</label>
<input type="password" id="currentPassword" bind:value={currentPassword} required />
</div>
<div class="field">
<label for="newPassword">New Password</label>
<input type="password" id="newPassword" bind:value={newPassword} required minlength="6" />
</div>
<div class="field">
<label for="confirmPassword">Confirm New Password</label>
<input type="password" id="confirmPassword" bind:value={confirmPassword} required />
</div>
<button type="submit" disabled={isUpdating}>
{isUpdating ? 'Updating...' : 'Update Password'}
</button>
</form>
</section>
<section class="settings-section danger-section">
<h2>Account</h2>
<button onclick={handleLogout} class="btn btn-danger">
Logout
</button>
</section>
</div>
</main>
<style>
:global(body) {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #fff;
}
main {
min-height: 100vh;
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
header {
margin-bottom: 2rem;
}
.header-left {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.back-link {
color: #667eea;
text-decoration: none;
font-size: 0.9rem;
}
h1 {
margin: 0;
font-size: 1.75rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
section {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
}
h2 {
font-size: 1.1rem;
margin: 0 0 1rem;
}
.success {
background: rgba(34, 197, 94, 0.2);
border: 1px solid #22c55e;
color: #86efac;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.error {
background: rgba(239, 68, 68, 0.2);
border: 1px solid #ef4444;
color: #fca5a5;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.field {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #ccc;
font-size: 0.9rem;
}
input {
width: 100%;
padding: 0.75rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 0.875rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.danger-section button {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.4);
}
</style>

View File

@@ -1,3 +0,0 @@
# allow crawling everything by default
User-agent: *
Disallow:

View File

@@ -1,17 +0,0 @@
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
compilerOptions: {
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true)
},
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

View File

@@ -1,20 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}

View File

@@ -1,6 +0,0 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});