Compare commits
1 Commits
fix/issue-
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eb623f022 |
@@ -1,219 +1,36 @@
|
|||||||
import uuid
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
import asyncio
|
|
||||||
from datetime import datetime
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
|
||||||
from .auth import get_current_user
|
|
||||||
from ..core.database import get_db
|
from ..core.database import get_db
|
||||||
from ..core.config import get_settings
|
|
||||||
from ..db.schemas import BacktestCreate, BacktestResponse
|
from ..db.schemas import BacktestCreate, BacktestResponse
|
||||||
from ..db.models import Bot, Backtest, Signal, User
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
running_backtests: Dict[str, Any] = {}
|
|
||||||
executor = ThreadPoolExecutor(max_workers=4)
|
|
||||||
|
|
||||||
|
@router.post("/bots/{bot_id}/backtest", response_model=BacktestResponse)
|
||||||
def run_backtest_sync(
|
def start_backtest(bot_id: str, config: BacktestCreate, db: Session = Depends(get_db)):
|
||||||
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Bot not found"
|
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
|
||||||
)
|
)
|
||||||
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)
|
@router.get("/bots/{bot_id}/backtest/{run_id}", response_model=BacktestResponse)
|
||||||
def get_backtest(
|
def get_backtest(bot_id: str, run_id: str, db: Session = Depends(get_db)):
|
||||||
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Bot not found"
|
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
|
||||||
)
|
)
|
||||||
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])
|
@router.get("/bots/{bot_id}/backtests", response_model=List[BacktestResponse])
|
||||||
def list_backtests(
|
def list_backtests(bot_id: str, db: Session = Depends(get_db)):
|
||||||
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Bot not found"
|
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
|
||||||
)
|
)
|
||||||
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")
|
@router.post("/bots/{bot_id}/backtest/{run_id}/stop")
|
||||||
def stop_backtest(
|
def stop_backtest(bot_id: str, run_id: str, db: Session = Depends(get_db)):
|
||||||
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Bot not found"
|
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
|
||||||
)
|
)
|
||||||
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}
|
|
||||||
|
|||||||
@@ -4,18 +4,14 @@ from typing import List, Annotated
|
|||||||
|
|
||||||
from .auth import get_current_user
|
from .auth import get_current_user
|
||||||
from ..core.database import get_db
|
from ..core.database import get_db
|
||||||
from ..core.config import get_settings
|
|
||||||
from ..db.schemas import (
|
from ..db.schemas import (
|
||||||
BotCreate,
|
BotCreate,
|
||||||
BotUpdate,
|
BotUpdate,
|
||||||
BotResponse,
|
BotResponse,
|
||||||
BotConversationCreate,
|
BotConversationCreate,
|
||||||
BotConversationResponse,
|
BotConversationResponse,
|
||||||
BotChatRequest,
|
|
||||||
BotChatResponse,
|
|
||||||
)
|
)
|
||||||
from ..db.models import Bot, BotConversation, User
|
from ..db.models import Bot, BotConversation, User
|
||||||
from ..services.ai_agent.crew import get_trading_crew
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
MAX_BOTS_PER_USER = 3
|
MAX_BOTS_PER_USER = 3
|
||||||
@@ -158,10 +154,10 @@ def delete_bot(
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{bot_id}/chat", response_model=BotChatResponse)
|
@router.post("/{bot_id}/chat", response_model=BotConversationResponse)
|
||||||
def chat(
|
def chat(
|
||||||
bot_id: str,
|
bot_id: str,
|
||||||
request: BotChatRequest,
|
message: BotConversationCreate,
|
||||||
current_user: Annotated[User, Depends(get_current_user)],
|
current_user: Annotated[User, Depends(get_current_user)],
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -177,75 +173,15 @@ def chat(
|
|||||||
detail="Not authorized to chat with this bot",
|
detail="Not authorized to chat with this bot",
|
||||||
)
|
)
|
||||||
|
|
||||||
conversation_history = (
|
|
||||||
db.query(BotConversation)
|
|
||||||
.filter(BotConversation.bot_id == bot_id)
|
|
||||||
.order_by(BotConversation.created_at)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
history_for_crew = [
|
|
||||||
{"role": conv.role, "content": conv.content}
|
|
||||||
for conv in conversation_history[-10:]
|
|
||||||
]
|
|
||||||
|
|
||||||
user_message = request.message
|
|
||||||
if request.strategy_config:
|
|
||||||
crew = get_trading_crew()
|
|
||||||
result = crew.chat(user_message, history_for_crew)
|
|
||||||
|
|
||||||
assistant_content = result.get("response", "I couldn't process your request.")
|
|
||||||
if result.get("success") and result.get("strategy_config"):
|
|
||||||
bot.strategy_config = result["strategy_config"]
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
db_conversation = BotConversation(
|
db_conversation = BotConversation(
|
||||||
bot_id=bot_id,
|
bot_id=bot_id,
|
||||||
role="user",
|
role=message.role,
|
||||||
content=user_message,
|
content=message.content,
|
||||||
)
|
)
|
||||||
db.add(db_conversation)
|
db.add(db_conversation)
|
||||||
|
|
||||||
db_assistant = BotConversation(
|
|
||||||
bot_id=bot_id,
|
|
||||||
role="assistant",
|
|
||||||
content=assistant_content,
|
|
||||||
)
|
|
||||||
db.add(db_assistant)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_assistant)
|
db.refresh(db_conversation)
|
||||||
|
return db_conversation
|
||||||
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])
|
@router.get("/{bot_id}/history", response_model=List[BotConversationResponse])
|
||||||
|
|||||||
@@ -118,17 +118,6 @@ class BotConversationResponse(BaseModel):
|
|||||||
from_attributes = True
|
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):
|
class SignalResponse(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
bot_id: str
|
bot_id: str
|
||||||
|
|||||||
@@ -1,247 +1,15 @@
|
|||||||
from typing import List, Optional, Dict, Any
|
from typing import List, Optional
|
||||||
from crewai import Agent, Task, Crew
|
|
||||||
from .llm_connector import MiniMaxConnector, MiniMaxLLM
|
|
||||||
from ..core.config import get_settings
|
|
||||||
|
|
||||||
|
|
||||||
class StrategyValidator:
|
class CrewAgent:
|
||||||
SUPPORTED_CONDITIONS = ["price_drop", "price_rise", "volume_spike", "price_level"]
|
def __init__(self, role: str, goal: str, backstory: str):
|
||||||
SUPPORTED_ACTIONS = ["buy", "sell", "notify"]
|
self.role = role
|
||||||
|
self.goal = goal
|
||||||
|
self.backstory = backstory
|
||||||
|
|
||||||
def validate(self, strategy_config: dict) -> tuple[bool, list[str]]:
|
def execute_task(self, task: str) -> str:
|
||||||
errors = []
|
raise NotImplementedError("CrewAI agent not yet implemented")
|
||||||
|
|
||||||
if "conditions" not in strategy_config:
|
|
||||||
errors.append("Missing 'conditions' in strategy config")
|
|
||||||
return False, errors
|
|
||||||
|
|
||||||
if not isinstance(strategy_config["conditions"], list):
|
|
||||||
errors.append("'conditions' must be a list")
|
|
||||||
return False, errors
|
|
||||||
|
|
||||||
if len(strategy_config["conditions"]) == 0:
|
|
||||||
errors.append("At least one condition is required")
|
|
||||||
return False, errors
|
|
||||||
|
|
||||||
for i, condition in enumerate(strategy_config["conditions"]):
|
|
||||||
if "type" not in condition:
|
|
||||||
errors.append(f"Condition {i}: missing 'type'")
|
|
||||||
continue
|
|
||||||
|
|
||||||
cond_type = condition.get("type")
|
|
||||||
if cond_type not in self.SUPPORTED_CONDITIONS:
|
|
||||||
errors.append(f"Condition {i}: unsupported type '{cond_type}'")
|
|
||||||
continue
|
|
||||||
|
|
||||||
params = condition.get("params", {})
|
|
||||||
if cond_type in ["price_drop", "price_rise", "volume_spike"]:
|
|
||||||
if "token" not in params:
|
|
||||||
errors.append(f"Condition {i}: missing 'token'")
|
|
||||||
if "threshold_percent" not in params:
|
|
||||||
errors.append(f"Condition {i}: missing 'threshold_percent'")
|
|
||||||
elif not isinstance(params["threshold_percent"], (int, float)):
|
|
||||||
errors.append(
|
|
||||||
f"Condition {i}: 'threshold_percent' must be a number"
|
|
||||||
)
|
|
||||||
elif params["threshold_percent"] <= 0:
|
|
||||||
errors.append(
|
|
||||||
f"Condition {i}: 'threshold_percent' must be positive"
|
|
||||||
)
|
|
||||||
|
|
||||||
elif cond_type == "price_level":
|
|
||||||
if "token" not in params:
|
|
||||||
errors.append(f"Condition {i}: missing 'token'")
|
|
||||||
if "price" not in params:
|
|
||||||
errors.append(f"Condition {i}: missing 'price'")
|
|
||||||
if "direction" not in params:
|
|
||||||
errors.append(f"Condition {i}: missing 'direction'")
|
|
||||||
elif params["direction"] not in ["above", "below"]:
|
|
||||||
errors.append(
|
|
||||||
f"Condition {i}: direction must be 'above' or 'below'"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "actions" in strategy_config:
|
|
||||||
if not isinstance(strategy_config["actions"], list):
|
|
||||||
errors.append("'actions' must be a list")
|
|
||||||
else:
|
|
||||||
for i, action in enumerate(strategy_config["actions"]):
|
|
||||||
if "type" not in action:
|
|
||||||
errors.append(f"Action {i}: missing 'type'")
|
|
||||||
elif action["type"] not in self.SUPPORTED_ACTIONS:
|
|
||||||
errors.append(
|
|
||||||
f"Action {i}: unsupported type '{action['type']}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
return len(errors) == 0, errors
|
|
||||||
|
|
||||||
|
|
||||||
class StrategyExplainer:
|
def get_trading_crew():
|
||||||
def explain(self, strategy_config: dict) -> str:
|
raise NotImplementedError("Trading crew not yet implemented")
|
||||||
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)
|
|
||||||
|
|||||||
@@ -1,108 +1,13 @@
|
|||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional
|
||||||
import httpx
|
|
||||||
from crewai import LLM
|
|
||||||
|
|
||||||
|
|
||||||
class MiniMaxLLM(LLM):
|
class LLMConnector:
|
||||||
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"):
|
def __init__(self, api_key: str, model: str = "MiniMax-Text-01"):
|
||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.model = model
|
self.model = model
|
||||||
|
|
||||||
def chat(self, messages: list[dict], **kwargs) -> str:
|
def chat(self, messages: list[dict], **kwargs):
|
||||||
formatted_messages = []
|
raise NotImplementedError("LLM integration not yet implemented")
|
||||||
for msg in messages:
|
|
||||||
if isinstance(msg, dict):
|
|
||||||
formatted_messages.append(
|
|
||||||
{
|
|
||||||
"role": msg.get("role", "user"),
|
|
||||||
"content": msg.get("content", str(msg)),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
formatted_messages.append({"role": "user", "content": str(msg)})
|
|
||||||
|
|
||||||
llm = MiniMaxLLM(api_key=self.api_key, model=self.model)
|
def parse_strategy(self, user_message: str) -> dict:
|
||||||
return llm.call(formatted_messages, **kwargs)
|
raise NotImplementedError("Strategy parsing not yet implemented")
|
||||||
|
|
||||||
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}
|
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
import httpx
|
|
||||||
from typing import List, Dict, Any, Optional
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
class AveCloudClient:
|
|
||||||
BASE_URL = "https://prod.ave-api.com"
|
|
||||||
|
|
||||||
def __init__(self, api_key: str, plan: str = "free"):
|
|
||||||
self.api_key = api_key
|
|
||||||
self.plan = plan
|
|
||||||
|
|
||||||
def _headers(self) -> Dict[str, str]:
|
|
||||||
return {"X-API-KEY": self.api_key}
|
|
||||||
|
|
||||||
async def get_klines(
|
|
||||||
self,
|
|
||||||
token_id: str,
|
|
||||||
interval: str = "1h",
|
|
||||||
limit: int = 100,
|
|
||||||
start_time: Optional[int] = None,
|
|
||||||
end_time: Optional[int] = None,
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
url = f"{self.BASE_URL}/v2/klines/token/{token_id}"
|
|
||||||
params = {"interval": interval, "limit": limit}
|
|
||||||
if start_time:
|
|
||||||
params["start_time"] = start_time
|
|
||||||
if end_time:
|
|
||||||
params["end_time"] = end_time
|
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.get(
|
|
||||||
url, headers=self._headers(), params=params, timeout=30.0
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
if data.get("status") == 200:
|
|
||||||
return data.get("data", [])
|
|
||||||
raise Exception(f"Failed to fetch klines: {data}")
|
|
||||||
|
|
||||||
async def get_token_price(self, token_id: str) -> Optional[Dict[str, Any]]:
|
|
||||||
url = f"{self.BASE_URL}/v2/tokens/price"
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.post(
|
|
||||||
url,
|
|
||||||
headers=self._headers(),
|
|
||||||
json={"token_ids": [token_id]},
|
|
||||||
timeout=30.0,
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
if data.get("status") == 200:
|
|
||||||
prices = data.get("data", {})
|
|
||||||
return prices.get(token_id)
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_batch_prices(self, token_ids: List[str]) -> Dict[str, Dict[str, Any]]:
|
|
||||||
url = f"{self.BASE_URL}/v2/tokens/price"
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.post(
|
|
||||||
url,
|
|
||||||
headers=self._headers(),
|
|
||||||
json={"token_ids": token_ids},
|
|
||||||
timeout=30.0,
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
data = response.json()
|
|
||||||
if data.get("status") == 200:
|
|
||||||
return data.get("data", {})
|
|
||||||
return {}
|
|
||||||
@@ -1,324 +1,15 @@
|
|||||||
import uuid
|
from typing import Optional, Dict, Any
|
||||||
import asyncio
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Dict, Any, List, Optional
|
|
||||||
from .ave_client import AveCloudClient
|
|
||||||
|
|
||||||
|
|
||||||
class BacktestEngine:
|
class BacktestEngine:
|
||||||
def __init__(self, config: Dict[str, Any]):
|
def __init__(self, config: Dict[str, Any]):
|
||||||
self.config = config
|
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]:
|
async def run(self) -> Dict[str, Any]:
|
||||||
self.running = True
|
raise NotImplementedError("Backtest engine not yet implemented")
|
||||||
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):
|
async def stop(self):
|
||||||
self.running = False
|
raise NotImplementedError("Backtest stop not yet implemented")
|
||||||
self.status = "stopped"
|
|
||||||
self._calculate_metrics()
|
|
||||||
|
|
||||||
def get_results(self) -> Dict[str, Any]:
|
def get_results(self) -> Dict[str, Any]:
|
||||||
return {
|
raise NotImplementedError("Backtest results not yet implemented")
|
||||||
"id": self.run_id,
|
|
||||||
"status": self.status,
|
|
||||||
"results": self.results,
|
|
||||||
"signals": self.signals,
|
|
||||||
}
|
|
||||||
|
|||||||
2
src/frontend/.env.example
Normal file
2
src/frontend/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_API_URL=http://localhost:8000/api
|
||||||
|
VITE_WS_URL=ws://localhost:8000/ws
|
||||||
23
src/frontend/.gitignore
vendored
Normal file
23
src/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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-*
|
||||||
1
src/frontend/.npmrc
Normal file
1
src/frontend/.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
||||||
3
src/frontend/.vscode/extensions.json
vendored
Normal file
3
src/frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["svelte.svelte-vscode"]
|
||||||
|
}
|
||||||
42
src/frontend/README.md
Normal file
42
src/frontend/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 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.
|
||||||
1383
src/frontend/package-lock.json
generated
Normal file
1383
src/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
src/frontend/package.json
Normal file
23
src/frontend/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/frontend/src/app.d.ts
vendored
Normal file
13
src/frontend/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// 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 {};
|
||||||
12
src/frontend/src/app.html
Normal file
12
src/frontend/src/app.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!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>
|
||||||
209
src/frontend/src/lib/api/client.ts
Normal file
209
src/frontend/src/lib/api/client.ts
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
2
src/frontend/src/lib/api/index.ts
Normal file
2
src/frontend/src/lib/api/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { api } from './client';
|
||||||
|
export * from './types';
|
||||||
128
src/frontend/src/lib/api/types.ts
Normal file
128
src/frontend/src/lib/api/types.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
1
src/frontend/src/lib/assets/favicon.svg
Normal file
1
src/frontend/src/lib/assets/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
1
src/frontend/src/lib/index.ts
Normal file
1
src/frontend/src/lib/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
50
src/frontend/src/lib/stores/authStore.ts
Normal file
50
src/frontend/src/lib/stores/authStore.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
45
src/frontend/src/lib/stores/backtestStore.ts
Normal file
45
src/frontend/src/lib/stores/backtestStore.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
24
src/frontend/src/lib/stores/botsStore.ts
Normal file
24
src/frontend/src/lib/stores/botsStore.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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([]);
|
||||||
|
}
|
||||||
33
src/frontend/src/lib/stores/chatStore.ts
Normal file
33
src/frontend/src/lib/stores/chatStore.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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([]);
|
||||||
|
}
|
||||||
12
src/frontend/src/lib/stores/currentBotStore.ts
Normal file
12
src/frontend/src/lib/stores/currentBotStore.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
30
src/frontend/src/lib/stores/index.ts
Normal file
30
src/frontend/src/lib/stores/index.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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';
|
||||||
45
src/frontend/src/lib/stores/simulationStore.ts
Normal file
45
src/frontend/src/lib/stores/simulationStore.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
12
src/frontend/src/lib/stores/userStore.ts
Normal file
12
src/frontend/src/lib/stores/userStore.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
54
src/frontend/src/routes/+layout.svelte
Normal file
54
src/frontend/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<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>
|
||||||
135
src/frontend/src/routes/+page.svelte
Normal file
135
src/frontend/src/routes/+page.svelte
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<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>
|
||||||
354
src/frontend/src/routes/bot/[id]/+page.svelte
Normal file
354
src/frontend/src/routes/bot/[id]/+page.svelte
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
<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>
|
||||||
391
src/frontend/src/routes/bot/[id]/backtest/+page.svelte
Normal file
391
src/frontend/src/routes/bot/[id]/backtest/+page.svelte
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
<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>
|
||||||
426
src/frontend/src/routes/bot/[id]/simulate/+page.svelte
Normal file
426
src/frontend/src/routes/bot/[id]/simulate/+page.svelte
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
<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>
|
||||||
359
src/frontend/src/routes/dashboard/+page.svelte
Normal file
359
src/frontend/src/routes/dashboard/+page.svelte
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
<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>
|
||||||
176
src/frontend/src/routes/login/+page.svelte
Normal file
176
src/frontend/src/routes/login/+page.svelte
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<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>
|
||||||
193
src/frontend/src/routes/register/+page.svelte
Normal file
193
src/frontend/src/routes/register/+page.svelte
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<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>
|
||||||
281
src/frontend/src/routes/settings/+page.svelte
Normal file
281
src/frontend/src/routes/settings/+page.svelte
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
<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>
|
||||||
3
src/frontend/static/robots.txt
Normal file
3
src/frontend/static/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# allow crawling everything by default
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
17
src/frontend/svelte.config.js
Normal file
17
src/frontend/svelte.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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;
|
||||||
20
src/frontend/tsconfig.json
Normal file
20
src/frontend/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
6
src/frontend/vite.config.ts
Normal file
6
src/frontend/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()]
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user