Compare commits

...

7 Commits

Author SHA1 Message Date
shokollm
ad17ca6c41 fix: Use git.example.com instead of git.fbrns.co for placeholder
As per review feedback, changed all git instance URLs to use
git.example.com as a placeholder since the code will be hosted
on a different instance.
2026-04-08 23:00:10 +00:00
shokollm
9c8afa396c feat: Add deployment documentation and templates (issue #12)
Created comprehensive deployment guide with:

- DEPLOYMENT.md: Full documentation covering server requirements,
  step-by-step deployment, troubleshooting, and project structure
- scripts/nginx-template.conf: Nginx configuration with SSL, reverse
  proxy to backend, and frontend static file serving
- scripts/systemd-template.service: Systemd service for auto-restart
  and environment variable management
- scripts/deploy.sh: Automated deployment script for rolling updates
2026-04-08 22:34:15 +00:00
875427a0c1 Merge pull request '[Frontend] Project Setup - Svelte with TypeScript' (#20) from fix/issue-9 into main 2026-04-08 15:17:10 +02:00
shokollm
5eb623f022 feat: setup SvelteKit frontend with TypeScript
Implemented issue #9 - Frontend Project Setup with Svelte and TypeScript.

Changes:
- Created SvelteKit project with TypeScript
- Set up routing structure:
  - / (landing page)
  - /login
  - /register
  - /dashboard
  - /bot/[id]
  - /bot/[id]/backtest
  - /bot/[id]/simulate
  - /settings
- Created Svelte stores for state management:
  - userStore - Current user info
  - botsStore - List of user's bots
  - currentBotStore - Selected bot
  - chatStore - Chat messages
  - backtestStore - Backtest results
  - simulationStore - Simulation signals
  - authStore - Authentication state
- Created API client for backend communication
- Set up environment variables (.env.example)
- Created auth store with protected routes and login/register functionality
2026-04-08 12:58:33 +00:00
a59a1ccd97 Merge pull request '[Backend] Simulate Engine - Real-time Signal Detection' (#19) from fix/issue-8 into main 2026-04-08 14:57:28 +02:00
shokollm
965efa122b feat: implement Simulate Engine for real-time signal detection
Implement simulate engine for real-time signal detection via REST polling.

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

Depends on issue #7 (Backtest Engine) which was merged in PR #18
2026-04-08 11:25:51 +00:00
0fb16f06e4 Merge pull request '[Backend] Backtest Engine - Historical Data Replay' (#18) from fix/issue-7 into main 2026-04-08 13:21:56 +02:00
42 changed files with 5238 additions and 21 deletions

223
deployment/DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,223 @@
# Deployment Guide
This document describes how to deploy the Randebu Trading Bot application to a production server.
## Prerequisites
- Debian server with 8GB RAM, 4 cores
- Python 3.10+
- Node.js 18+
- Nginx
- SSL certificate (Let's Encrypt)
- SSH access to server
## Server Structure
```
/var/www/
└── bot/
├── backend/ # Backend application (FastAPI)
├── frontend/ # Frontend static files (SvelteKit build)
└── data/ # SQLite database and app data
```
## Step-by-Step Deployment
### 1. Clone Repository
```bash
ssh user@your-server
sudo mkdir -p /var/www/bot
sudo chown -R $USER:$USER /var/www/bot
cd /var/www/bot
git clone https://git.example.com/shoko/randebu.git .
```
### 2. Setup Backend
```bash
cd /var/www/bot/src/backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
mkdir -p /var/www/bot/data
```
### 3. Configure Environment
Copy and configure the environment file:
```bash
cp src/backend/.env.example /var/www/bot/data/.env
nano /var/www/bot/data/.env
```
Update these values:
- `SECRET_KEY` - Generate a secure key
- `DATABASE_URL` - Update path to `/var/www/bot/data/app.db`
- `MINIMAX_API_KEY` - Your API key
- `AVE_API_KEY` - Your API key
### 4. Build Frontend
```bash
cd /var/www/bot/src/frontend
npm install
npm run build
# Move build to expected location
mkdir -p /var/www/bot/frontend
cp -r build/* /var/www/bot/frontend/
```
### 5. Configure Nginx
Copy the nginx template and modify as needed:
```bash
sudo cp /var/www/bot/deployment/scripts/nginx-template.conf /etc/nginx/sites-available/bot.yourdomain.com
sudo nano /etc/nginx/sites-available/bot.yourdomain.com
```
Update `bot.yourdomain.com` with your actual domain.
Enable the site:
```bash
sudo ln -s /etc/nginx/sites-available/bot.yourdomain.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
### 6. Setup SSL Certificate
```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d bot.yourdomain.com
```
### 7. Configure Systemd Service
Copy and configure the systemd service:
```bash
sudo cp /var/www/bot/deployment/scripts/systemd-template.service /etc/systemd/system/ave-backend.service
sudo nano /etc/systemd/system/ave-backend.service
```
Update `your-user` and `/var/www/bot` paths as needed.
### 8. Start Backend Service
```bash
sudo systemctl daemon-reload
sudo systemctl enable ave-backend
sudo systemctl start ave-backend
sudo systemctl status ave-backend
```
### 9. Configure Firewall
```bash
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```
### 10. Verify Deployment
1. Visit `https://bot.yourdomain.com` - should show frontend
2. Visit `https://bot.yourdomain.com/api/...` - should hit backend API
3. Check backend logs: `sudo journalctl -u ave-backend -f`
## Project Structure
```
/var/www/bot/
├── deployment/ # Deployment scripts and templates
│ ├── DEPLOYMENT.md # This file
│ └── scripts/
│ ├── nginx-template.conf
│ ├── systemd-template.service
│ └── deploy.sh # Automated deployment script
├── src/
│ ├── backend/ # FastAPI application
│ │ ├── app/
│ │ │ ├── api/ # API routes
│ │ │ ├── core/ # Core functionality
│ │ │ ├── db/ # Database models
│ │ │ └── services/ # Business logic
│ │ ├── run.py
│ │ └── requirements.txt
│ └── frontend/ # SvelteKit application
│ ├── src/
│ └── package.json
├── data/ # Runtime data (gitignored)
│ ├── app.db # SQLite database
│ └── .env # Environment variables
└── frontend/ # Built frontend static files
```
## Troubleshooting
### Backend won't start
Check logs:
```bash
sudo journalctl -u ave-backend -n 100
```
Common issues:
- Missing environment variables - check `.env` file
- Port 8000 already in use - check configuration
- Database path incorrect - verify paths
### Nginx errors
Test configuration:
```bash
sudo nginx -t
```
Check error logs:
```bash
sudo tail -f /var/log/nginx/error.log
```
### SSL certificate issues
Renew certificate:
```bash
sudo certbot renew
```
Check certificate status:
```bash
sudo certbot certificates
```
## Useful Commands
| Action | Command |
|--------|---------|
| Restart backend | `sudo systemctl restart ave-backend` |
| View backend logs | `sudo journalctl -u ave-backend -f` |
| Check nginx status | `sudo systemctl status nginx` |
| Reload nginx | `sudo systemctl reload nginx` |
| Check port 8000 | `curl http://localhost:8000/health` |
## Rolling Updates
To update the application:
```bash
cd /var/www/bot
git pull
cd src/backend && source venv/bin/activate && pip install -r requirements.txt
sudo systemctl restart ave-backend
```
For frontend updates, rebuild and copy static files to `/var/www/bot/frontend`.

47
deployment/scripts/deploy.sh Executable file
View File

@@ -0,0 +1,47 @@
#!/bin/bash
set -e
DEPLOY_DIR="/var/www/bot"
DOMAIN="bot.yourdomain.com"
GIT_REPO="https://git.example.com/shoko/randebu.git"
BRANCH="main"
echo "=== Randebu Deployment Script ==="
echo "Deploy directory: $DEPLOY_DIR"
echo "Domain: $DOMAIN"
echo ""
cd "$DEPLOY_DIR"
echo "[1/6] Pulling latest code..."
git pull origin "$BRANCH"
echo "[2/6] Updating backend dependencies..."
cd "$DEPLOY_DIR/src/backend"
source venv/bin/activate
pip install -r requirements.txt
echo "[3/6] Rebuilding frontend..."
cd "$DEPLOY_DIR/src/frontend"
npm install
npm run build
mkdir -p "$DEPLOY_DIR/frontend"
cp -r build/* "$DEPLOY_DIR/frontend/"
echo "[4/6] Restarting backend service..."
sudo systemctl restart ave-backend
sleep 2
sudo systemctl status ave-backend --no-pager
echo "[5/6] Testing endpoints..."
curl -s "http://localhost:8000/health" && echo ""
curl -s -o /dev/null -w "Frontend: %{http_code}\n" "https://$DOMAIN/" || true
echo "[6/6] Verifying SSL..."
sudo certbot certificates 2>/dev/null | grep -A2 "$DOMAIN" || echo "No certificate found for $DOMAIN"
echo ""
echo "=== Deployment Complete ==="
echo "Backend: https://$DOMAIN/api/"
echo "Frontend: https://$DOMAIN/"
echo "Backend logs: sudo journalctl -u ave-backend -f"

View File

@@ -0,0 +1,59 @@
server {
listen 80;
server_name bot.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name bot.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/bot.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/bot.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
root /var/www/bot/frontend;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8000/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
location /ws/ {
proxy_pass http://127.0.0.1:8000/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
}

View File

@@ -0,0 +1,21 @@
[Unit]
Description=Randebu Trading Bot Backend
After=network.target
[Service]
Type=simple
User=your-user
WorkingDirectory=/var/www/bot/src/backend
Environment="PATH=/var/www/bot/src/backend/venv/bin"
ExecStart=/var/www/bot/src/backend/venv/bin/python /var/www/bot/src/backend/run.py
Restart=always
RestartSec=10
EnvironmentFile=/var/www/bot/data/.env
StandardOutput=journal
StandardError=journal
SyslogIdentifier=ave-backend
[Install]
WantedBy=multi-user.target

View File

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

View File

@@ -87,6 +87,7 @@ class SimulationCreate(BaseModel):
token: str token: str
chain: str chain: str
duration_seconds: int = 3600 duration_seconds: int = 3600
check_interval: int = 60
auto_execute: bool = False auto_execute: bool = False

View File

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

View 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
View 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
View File

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

3
src/frontend/.vscode/extensions.json vendored Normal file
View File

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

42
src/frontend/README.md Normal file
View 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

File diff suppressed because it is too large Load Diff

23
src/frontend/package.json Normal file
View 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
View 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
View 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>

View 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);
}
}
};

View File

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

View 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;
}

View 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

View File

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

View 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);
}

View 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);
}

View 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([]);
}

View 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([]);
}

View 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);
}

View 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';

View 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);
}

View 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);
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

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

View 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;

View 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
}

View File

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