Compare commits

..

2 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
23 changed files with 272 additions and 3548 deletions

View File

@@ -1,521 +0,0 @@
# Randebu Trading Bot - Product & Technical Audit Report
> **Date:** 2026-04-09
> **Phase:** Phase 1 Implementation Complete - Pre-Testing Review
> **Purpose:** Document current state, issues found, and recommendations for next steps
---
## 1. Product Overview
### 1.1 What is Randebu?
Randebu is an AI-powered trading bot platform where users create and manage automated trading strategies through natural language chat—similar to ChatGPT, but specialized for creating trading bots.
### 1.2 Core User Flow
```
User Registration → Create Bot → Chat with AI to Define Strategy
→ Backtest Strategy → Simulate Trading → (Future) Live Trading
```
### 1.3 Phase 1 Scope
| Feature | Status |
|---------|--------|
| BNB Chain only | ✅ Intended (not yet enforced) |
| Backtest engine | ✅ Implemented |
| Simulation engine | ✅ Implemented |
| Natural language strategy parsing | ✅ Implemented |
| User authentication | ✅ Implemented |
| Multi-bot support (max 3) | ✅ Implemented |
| Dummy wallet (database record) | ✅ Implemented |
### 1.4 Tech Stack
| Layer | Technology |
|-------|------------|
| Frontend | Svelte 5 + TypeScript |
| Backend | Python FastAPI |
| AI Agent | CrewAI + MiniMax LLM |
| Database | SQLite |
| Trading Data | AVE Cloud API |
---
## 2. Critical Issues (Must Fix Before Testing)
These issues will cause complete pipeline failure if not addressed.
### 2.1 Database Tables Never Created
**Location:** `src/backend/app/main.py`, `src/backend/run.py`
**Problem:** The application starts but never creates the database tables. There is no:
- Alembic migration setup
- `Base.metadata.create_all()` call on startup
- Database initialization script
**Impact:** First database operation will fail with "table not found" error.
**Current State:**
```python
# core/database.py defines Base, but nothing calls:
# Base.metadata.create_all(engine)
```
**Fix Required:** Add database initialization on application startup.
---
### 2.2 Strategy Config Schema Mismatch
**Location:** Multiple files - see mapping below
**Problem:** The LLM outputs one schema format, but the backtest and simulation engines expect a completely different format. This is a **complete pipeline break** - strategies parsed by AI will never trigger any trades in backtesting.
#### Schema Flow Diagram
```
┌─────────────────────────────────────────────────────────────────────────┐
│ LLM OUTPUT (llm_connector.py) - What AI actually produces │
├─────────────────────────────────────────────────────────────────────────┤
│ { │
│ "type": "price_drop", │
│ "params": { │
│ "token": "PEPE", │
│ "threshold_percent": 5 │
│ } │
│ } │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ BACKEND VALIDATOR (crew.py - StrategyValidator.validate()) │
├─────────────────────────────────────────────────────────────────────────┤
│ # Validator expects params.threshold_percent - THIS WORKS │
│ if "threshold_percent" not in params: │
│ errors.append(f"Condition {i}: missing 'threshold_percent'") │
└─────────────────────────────────────────────────────────────────────────┘
▼ (But engines look for flat fields)
┌─────────────────────────────────────────────────────────────────────────┐
│ BACKTEST ENGINE (services/backtest/engine.py - _check_condition()) │
├─────────────────────────────────────────────────────────────────────────┤
│ # What engine actually looks for: │
│ threshold = condition.get("threshold", 0) # ❌ Returns 0! │
│ token = condition.get("token") # ❌ Wrong path! │
│ timeframe = condition.get("timeframe") # ❌ Not in params! │
│ │
│ # Result: Conditions NEVER trigger because field names don't match │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ SIMULATE ENGINE (services/simulate/engine.py - _check_condition()) │
├─────────────────────────────────────────────────────────────────────────┤
│ # Same issue as backtest engine │
│ threshold = condition.get("threshold", 0) # ❌ Returns 0 │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ FRONTEND TYPES (src/frontend/src/lib/api/types.ts) │
├─────────────────────────────────────────────────────────────────────────┤
│ interface Condition { │
│ type: 'price_drop' | 'price_rise' | 'volume_spike' | 'price_level';│
│ token: string; # Flat - no params wrapper │
│ threshold?: number; # Not threshold_percent! │
│ timeframe?: string; # Exists here │
│ } │
└─────────────────────────────────────────────────────────────────────────┘
```
#### Field Mapping Table
| Component | Token Field | Threshold Field | Timeframe Field |
|-----------|-----------|-----------------|-----------------|
| LLM Output | `params.token` | `params.threshold_percent` | N/A |
| Validator | `params.token` | `params.threshold_percent` | N/A |
| Backtest Engine | `token` | `threshold` | `timeframe` |
| Simulate Engine | `token` | `threshold` | `timeframe` |
| Frontend Types | `token` | `threshold` | `timeframe` |
**Fix Required:** Normalize to ONE consistent schema across the entire pipeline. Recommended: Use the flat structure (token, threshold, timeframe) as it's simpler and already used by engines and frontend.
---
### 2.3 Bot Creation Will Fail
**Location:**
- `src/backend/app/db/schemas.py` (BotCreate)
- `src/frontend/src/lib/api/client.ts` (bots.create)
**Problem:**
| Issue | Details |
|-------|---------|
| Backend requires | `strategy_config: dict` (REQUIRED) |
| Backend requires | `llm_config: dict` (REQUIRED) |
| Frontend sends | Only `name` and optional `description` |
**Impact:** Users cannot create bots through the frontend - API will return validation error.
**Fix Required:** Either:
1. Make `strategy_config` and `llm_config` optional in backend with default values
2. OR update frontend to send default config values
---
### 2.4 Config Endpoints Return Empty Data
**Location:** `src/backend/app/api/config.py`
```python
@router.get("/chains")
def get_chains():
return {"chains": []} # ❌ Always empty
@router.get("/tokens")
def get_tokens():
return {"tokens": []} # ❌ Always empty
```
**Impact:** Frontend cannot populate dropdowns for chain/token selection.
**Fix Required:** Return BSC (BNB Chain) as the only supported chain in Phase 1, and query AVE API for available tokens.
---
## 3. Major Issues
### 3.1 Risk Management Not Implemented
**Location:**
- `src/backend/app/db/models.py` (schema supports it)
- `src/backend/app/services/backtest/engine.py`
- `src/backend/app/services/simulate/engine.py`
**Problem:** The database schema and frontend UI support `risk_management` configuration:
```typescript
interface RiskManagement {
stop_loss_percent?: number;
take_profit_percent?: number;
}
```
However, neither the backtest nor simulation engines actually check or use stop-loss/take-profit logic during trade execution. The config is saved but ignored.
**Fix Required:** Implement actual stop-loss and take-profit checks in both engines.
---
### 3.2 Duplicate AveCloudClient Implementations
**Location:**
- `src/backend/app/services/ave/client.py`
- `src/backend/app/services/backtest/ave_client.py`
**Problem:** Two different AveCloudClient classes with different methods:
| `services/ave/client.py` | `services/backtest/ave_client.py` |
|--------------------------|-----------------------------------|
| `get_tokens()` | ❌ Missing |
| `get_batch_prices()` | ✅ `get_batch_prices()` |
| `get_token_details()` | ❌ Missing |
| `get_klines()` | ✅ `get_klines()` |
| `get_trending_tokens()` | ❌ Missing |
| `get_token_risk()` | ❌ Missing |
| `get_chain_quote()` | ❌ Missing |
| `get_chain_swap()` | ❌ Missing |
| ❌ Missing | `get_token_price()` |
Additionally, the simulate engine imports from the wrong location:
```python
# services/simulate/engine.py
from ..backtest.ave_client import AveCloudClient # ❌ Wrong import
```
**Fix Required:** Consolidate into ONE AveCloudClient class.
---
### 3.3 Silent Error Handling in Simulation
**Location:** `src/backend/app/services/simulate/engine.py`
```python
try:
# ... API calls ...
except Exception as e:
pass # ❌ Silently swallows ALL errors!
```
**Impact:** If AVE API fails or returns bad data, the simulation continues silently with no logging or user feedback.
**Fix Required:** Add proper error logging and user-facing error messages.
---
### 3.4 No Chain Validation for Phase 1
**Problem:** You mentioned limiting to BNB Chain only for Phase 1, but:
- No backend validation enforces this
- Users can specify any chain in backtest/simulate config
- The config endpoints return empty arrays
**Fix Required:** Add chain validation that only allows "bsc" for Phase 1.
---
### 3.5 In-Memory Token Blacklist
**Location:** `src/backend/app/api/auth.py`
```python
TOKEN_BLACKLIST = set() # ❌ In-memory only
```
**Problems:**
- Resets when server restarts
- Doesn't work with multiple workers/processes
- Logout doesn't truly invalidate tokens in production
**Fix Required:** Use Redis or database-backed token blacklist for production.
---
### 3.6 Conversation History Not Passed to Crew
**Location:** `src/backend/app/api/bots.py`
```python
history_for_crew = conversation_history[-10:] # Gets history
crew = get_trading_crew() # ❌ Doesn't pass history!
result = crew.chat(user_message, history_for_crew)
```
The history is fetched but not actually used by the agent - each chat starts fresh.
**Fix Required:** Pass conversation history to the crew agent.
---
### 3.7 No Rate Limiting Applied
**Location:** `src/backend/app/main.py`
```python
app.state.limiter = limiter # Set up but not used on most endpoints
```
The rate limiter is initialized but only applied to the login endpoint. Other endpoints have no protection.
**Fix Required:** Apply rate limiting to sensitive endpoints.
---
### 3.8 CORS Wide Open
**Location:** `src/backend/app/main.py`
```python
allow_origins=["*"] # ❌ Should be restricted to frontend domain
```
**Fix Required:** Limit CORS to the frontend domain in production.
---
### 3.9 No WebSocket for Real-Time Updates
**Problem:** Users must poll the API to see:
- Backtest progress
- Simulation signals (new signals only appear on refresh)
**Impact:** Poor UX during long-running operations.
**Fix Required:** Add WebSocket support for real-time updates (Phase 2 or later).
---
## 4. Minor Issues
### 4.1 Unused Dependencies
**Location:** `src/backend/requirements.txt`
```python
anthropic>=0.18.0 # Included but project uses MiniMax
```
**Fix Required:** Remove unused dependency.
---
### 4.2 Missing .env Example
**Problem:** No `.env.example` file to guide deployment.
**Fix Required:** Create `.env.example` with all required variables documented.
---
### 4.3 No Input Sanitization
User-provided data (bot names, chat messages) isn't sanitized before storage or display.
**Fix Required:** Add input validation and sanitization.
---
### 4.4 Inconsistent Error Responses
Some endpoints return `{"detail": "..."}` (FastAPI default), others return custom error shapes.
**Fix Required:** Standardize error response format.
---
### 4.5 No Integration Tests
No tests that verify the full pipeline (chat → config → backtest).
**Fix Required:** Add integration tests.
---
## 5. Missing Documentation Files
The following should be created:
1. **`.env.example`** - All environment variables with descriptions
2. **`docs/STRATEGY_SCHEMA.md`** - Single source of truth for strategy config schema
3. **`docs/API_SCHEMA.md`** - API contract documentation
4. **`init_db.py`** - Database initialization script
---
## 6. Recommendations Summary
### Priority Matrix
| Priority | Issue | Effort | Impact |
|----------|-------|--------|--------|
| **P0** | Database tables not created | Small | App crashes on startup |
| **P0** | Bot creation fails | Small | Users can't create bots |
| **P0** | Strategy schema mismatch | Medium | Backtesting completely broken |
| **P0** | Config endpoints empty | Small | No chain/token selection |
| **P1** | Risk management not implemented | Medium | No stop-loss/take-profit |
| **P1** | Chain validation missing | Small | Can use non-BSC chains |
| **P1** | Silent error handling | Small | Hard to debug issues |
| **P2** | Duplicate AveCloudClient | Medium | Maintenance burden |
| **P2** | CORS restricted | Small | Security hardening |
| **P2** | Token blacklist (production) | Medium | Security |
| **P2** | Rate limiting | Medium | DoS protection |
| **P3** | WebSocket support | Large | UX improvement |
| **P3** | Integration tests | Medium | Code quality |
---
## 7. AVE Cloud Integration Notes
### Rate Limit Strategy
| Tier | TPS | Recommended Approach |
|------|-----|---------------------|
| Free | 1 | Aggressive caching, batch requests |
| Normal | 5 | Moderate caching |
| Pro | 20 | Minimal caching |
### Caching Recommendations
1. **Token prices:** Cache for 30-60 seconds
2. **Trending tokens:** Cache for 5-10 minutes
3. **Token details:** Cache for 5-10 minutes
4. **Risk assessments:** Cache for 15-30 minutes
### No Testnet Warning
AVE Cloud has **no testnet**. All API calls use real money:
- Use quote/dry-run mode for testing
- Start with minimal amounts ($1-10)
- Contact AVE support about sandbox options
---
## 8. Next Steps
### Immediate (Before Testing)
1. Add database initialization to startup
2. Fix bot creation (frontend or backend)
3. **Normalize strategy schema** - Choose flat structure, update all components
4. Populate config endpoints with BSC + default tokens
5. Add BSC-only chain validation
### Short Term
6. Implement risk management (stop-loss/take-profit)
7. Consolidate AveCloudClient
8. Add proper error handling
9. Create .env.example
10. Add input sanitization
### Medium Term
11. Add WebSocket for real-time updates
12. Implement production token blacklist (Redis)
13. Apply rate limiting
14. Restrict CORS
15. Add integration tests
---
## 9. Files Reference
### Key Backend Files
| File | Purpose |
|------|---------|
| `src/backend/app/main.py` | FastAPI app initialization |
| `src/backend/app/api/bots.py` | Bot CRUD + chat endpoint |
| `src/backend/app/api/backtest.py` | Backtest API |
| `src/backend/app/api/simulate.py` | Simulation API |
| `src/backend/app/api/ave.py` | AVE Cloud proxy endpoints |
| `src/backend/app/api/config.py` | Config endpoints |
| `src/backend/app/db/schemas.py` | Pydantic schemas |
| `src/backend/app/db/models.py` | SQLAlchemy models |
| `src/backend/app/services/ai_agent/crew.py` | CrewAI agents |
| `src/backend/app/services/ai_agent/llm_connector.py` | MiniMax LLM |
| `src/backend/app/services/backtest/engine.py` | Backtest logic |
| `src/backend/app/services/simulate/engine.py` | Simulation logic |
| `src/backend/app/services/ave/client.py` | AVE Cloud client |
### Key Frontend Files
| File | Purpose |
|------|---------|
| `src/frontend/src/lib/api/client.ts` | API client |
| `src/frontend/src/lib/api/types.ts` | TypeScript types |
| `src/frontend/src/routes/bot/[id]/+page.svelte` | Bot chat page |
| `src/frontend/src/routes/bot/[id]/backtest/+page.svelte` | Backtest page |
| `src/frontend/src/routes/bot/[id]/simulate/+page.svelte` | Simulation page |
| `src/frontend/src/lib/components/ChatInterface.svelte` | Chat UI |
| `src/frontend/src/lib/components/StrategyPreview.svelte` | Strategy display |
---
## 10. Audit Complete
This audit was conducted by reviewing:
- All source code in `src/backend/` and `src/frontend/`
- Documentation in `docs/`
- Database models and schemas
- API endpoints and their implementations
The product has a **solid architectural foundation** and addresses a real market need. The core issues are manageable - primarily schema standardization and missing initialization code.
---
*End of Audit Report*

View File

@@ -1,279 +0,0 @@
# Strategy Config Schema
> **Status:** DRAFT - Needs to be normalized with implementation
> **Purpose:** Single source of truth for strategy configuration format
---
## 1. Overview
This document defines the structure of the `strategy_config` JSON object that represents a trading bot's strategy. This config is:
- Generated by the AI from natural language input
- Validated by the backend
- Used by backtest and simulation engines
- Displayed in the frontend
---
## 2. Schema Version
**Current Version:** 1.0
**Status:** Flat structure (NOT nested in `params`)
> **IMPORTANT:** The current implementation has a mismatch where the LLM outputs a nested `params` structure but the engines expect flat fields. This document defines the **TARGET** schema to normalize all components.
---
## 3. Full Schema
```json
{
"version": "1.0",
"conditions": [
{
"type": "price_drop",
"token": "PEPE",
"chain": "bsc",
"threshold": 5,
"timeframe": "1h"
}
],
"actions": [
{
"type": "buy",
"amount_percent": 10,
"token": "PEPE"
}
],
"risk_management": {
"stop_loss_percent": 3,
"take_profit_percent": 10
}
}
```
---
## 4. Field Definitions
### 4.1 Root Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `version` | string | No | Schema version (for future compatibility) |
| `conditions` | array | Yes | List of trigger conditions |
| `actions` | array | Yes | List of actions to execute when conditions are met |
| `risk_management` | object | No | Risk management settings |
### 4.2 Condition Object
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | Yes | Condition type (see supported types below) |
| `token` | string | Yes | Token symbol or address (e.g., "PEPE" or "0x123...-bsc") |
| `chain` | string | No | Blockchain chain (default: "bsc") |
| `threshold` | number | For price_drop/rise/volume_spike | Percentage threshold (e.g., 5 = 5%) |
| `price` | number | For price_level | Price level to trigger on |
| `direction` | string | For price_level | "above" or "below" |
| `timeframe` | string | No | Time window for calculation (e.g., "1h", "15m") |
#### Supported Condition Types
| Type | Description | Required Fields |
|------|-------------|-----------------|
| `price_drop` | Triggers when token price drops by threshold % | token, threshold |
| `price_rise` | Triggers when token price rises by threshold % | token, threshold |
| `volume_spike` | Triggers when trading volume increases by threshold % | token, threshold |
| `price_level` | Triggers when price crosses a specific level | token, price, direction |
### 4.3 Action Object
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | Yes | Action type (buy, sell, hold, notify) |
| `amount_percent` | number | For buy/sell | Percentage of portfolio to trade |
| `token` | string | No | Token to trade (defaults to condition token) |
#### Supported Action Types
| Type | Description | Required Fields |
|------|-------------|-----------------|
| `buy` | Purchase tokens | amount_percent |
| `sell` | Sell tokens | amount_percent |
| `hold` | Do nothing (log only) | - |
| `notify` | Send notification to user | - |
### 4.4 Risk Management Object
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `stop_loss_percent` | number | No | Exit trade if loss exceeds this % |
| `take_profit_percent` | number | No | Exit trade if profit reaches this % |
---
## 5. Examples
### 5.1 Simple Buy on Price Drop
> "Buy PEPE when it drops 5% in 1 hour"
```json
{
"conditions": [
{
"type": "price_drop",
"token": "PEPE",
"chain": "bsc",
"threshold": 5,
"timeframe": "1h"
}
],
"actions": [
{
"type": "buy",
"amount_percent": 10
}
]
}
```
### 5.2 Buy on Price Rise with Stop Loss
> "Buy when PEPE rises 10%, but stop loss at 3%"
```json
{
"conditions": [
{
"type": "price_rise",
"token": "PEPE",
"threshold": 10,
"timeframe": "4h"
}
],
"actions": [
{
"type": "buy",
"amount_percent": 20
}
],
"risk_management": {
"stop_loss_percent": 3
}
}
```
### 5.3 Sell on Price Level
> "Sell when PEPE reaches $0.0001"
```json
{
"conditions": [
{
"type": "price_level",
"token": "PEPE",
"price": 0.0001,
"direction": "above"
}
],
"actions": [
{
"type": "sell",
"amount_percent": 100
}
]
}
```
### 5.4 Volume Spike Alert
> "Notify me when PEPE volume spikes 50%"
```json
{
"conditions": [
{
"type": "volume_spike",
"token": "PEPE",
"threshold": 50,
"timeframe": "1h"
}
],
"actions": [
{
"type": "notify"
}
]
}
```
---
## 6. Validation Rules
### 6.1 Conditions
- At least one condition is required
- Each condition must have a valid `type`
- Token must be specified
- Threshold must be positive number (for applicable types)
- Price level must be specified for `price_level` type
- Direction must be "above" or "below" for `price_level` type
### 6.2 Actions
- At least one action is required
- Each action must have a valid `type`
- `amount_percent` must be between 0 and 100
### 6.3 Risk Management
- `stop_loss_percent` must be positive
- `take_profit_percent` must be positive
---
## 7. Implementation Status
### Components Using This Schema
| Component | Status | Notes |
|-----------|--------|-------|
| Backend Validator (crew.py) | ❌ Mismatch | Uses nested `params` structure |
| Backtest Engine | ❌ Mismatch | Uses flat structure (correct) |
| Simulate Engine | ❌ Mismatch | Uses flat structure (correct) |
| Frontend Types | ✅ Match | Uses flat structure |
| Frontend StrategyPreview | ✅ Match | Uses flat structure |
### Normalization Required
The LLM output parser should be updated to output flat structure (not nested in `params`) to match what the engines and frontend expect.
---
## 8. Future Extensions
### Potential Condition Types (Phase 2+)
| Type | Description |
|------|-------------|
| `rsi_oversold` | RSI indicator below threshold |
| `rsi_overbought` | RSI indicator above threshold |
| `ma_crossover` | Moving average crossover |
| `bollinger_breakout` | Bollinger Band breakout |
| `news_sentiment` | Based on news sentiment analysis |
### Potential Action Types (Phase 2+)
| Type | Description |
|------|-------------|
| `dca_buy` | Dollar cost averaging buy |
| `trailing_stop` | Trailing stop loss |
| `smart_rebalance` | Portfolio rebalancing |
---
*Document Version: 1.0*
*Last Updated: 2026-04-09*

View File

@@ -1,68 +1,11 @@
# Randebu Trading Bot - Environment Variables Template
# Copy this file to .env and fill in your values
# =============================================================================
# DATABASE
# =============================================================================
# SQLite database path (relative or absolute)
# Example: sqlite:///./data/app.db
DATABASE_URL=sqlite:///./data/app.db DATABASE_URL=sqlite:///./data/app.db
# =============================================================================
# AUTHENTICATION
# =============================================================================
# Secret key for JWT token signing
# Generate with: python -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=your-super-secret-key-change-in-production SECRET_KEY=your-super-secret-key-change-in-production
# JWT algorithm (HS256 is recommended)
JWT_ALGORITHM=HS256 JWT_ALGORITHM=HS256
# Token expiration time in minutes (1440 = 24 hours)
ACCESS_TOKEN_EXPIRE_MINUTES=1440 ACCESS_TOKEN_EXPIRE_MINUTES=1440
# =============================================================================
# MINIMAX LLM
# =============================================================================
# MiniMax API key (get from https://platform.minimax.chat/)
MINIMAX_API_KEY=your-minimax-api-key MINIMAX_API_KEY=your-minimax-api-key
# MiniMax model to use
# Common options: MiniMax-Text-01, MiniMax-M2.1
MINIMAX_MODEL=MiniMax-Text-01 MINIMAX_MODEL=MiniMax-Text-01
AVE_API_KEY=your-ave-cloud-api-key
# =============================================================================
# AVE CLOUD API
# =============================================================================
# AVE Cloud API key (get from https://cloud.ave.ai/)
AVE_API_KEY=your-ave-api-key
# AVE Cloud plan tier
# Options: free, normal, pro
# Note: Free tier has 1 TPS limit, Pro required for WebSocket
AVE_API_PLAN=free AVE_API_PLAN=free
# =============================================================================
# SERVER CONFIGURATION
# =============================================================================
# Server host (0.0.0.0 for all interfaces)
HOST=0.0.0.0 HOST=0.0.0.0
# Server port
PORT=8000 PORT=8000
# Debug mode (set to false in production)
DEBUG=false DEBUG=false
# =============================================================================
# FRONTEND CONFIGURATION (for reference)
# =============================================================================
# Frontend environment variables (set in frontend .env file):
# VITE_API_URL=https://bot.yourdomain.com/api
# VITE_WS_URL=wss://bot.yourdomain.com/ws

View File

@@ -1,265 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException, status, Request
from sqlalchemy.orm import Session
from typing import Annotated, Optional
import httpx
from .auth import get_current_user
from ..core.database import get_db
from ..core.config import get_settings
from ..db.models import User
from ..services.ave import AveCloudClient, check_tier_access
from ..db.schemas import (
AveBatchPricesRequest,
AveKlinesRequest,
AveChainQuoteRequest,
AveChainSwapRequest,
)
router = APIRouter()
def get_ave_client() -> AveCloudClient:
settings = get_settings()
return AveCloudClient(api_key=settings.AVE_API_KEY, plan=settings.AVE_API_PLAN)
@router.get("/tokens")
async def search_tokens(
query: Optional[str] = None,
chain: Optional[str] = None,
limit: int = 20,
current_user: User = Depends(get_current_user),
):
client = get_ave_client()
try:
tokens = await client.get_tokens(query=query, chain=chain, limit=limit)
return {"tokens": tokens}
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Rate limit exceeded. Please try again later.",
)
raise HTTPException(
status_code=e.response.status_code,
detail=f"AVE API error: {e.response.text}",
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch tokens: {str(e)}",
)
@router.post("/tokens/price")
async def get_batch_prices(
request: AveBatchPricesRequest,
current_user: User = Depends(get_current_user),
):
client = get_ave_client()
try:
prices = await client.get_batch_prices(request.token_ids)
return {"prices": prices}
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Rate limit exceeded. Please try again later.",
)
raise HTTPException(
status_code=e.response.status_code,
detail=f"AVE API error: {e.response.text}",
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch batch prices: {str(e)}",
)
@router.get("/tokens/{token_id}")
async def get_token_details(
token_id: str,
current_user: User = Depends(get_current_user),
):
client = get_ave_client()
try:
token = await client.get_token_details(token_id)
if token is None:
return {"token": None, "upsell_message": None}
return {"token": token}
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Rate limit exceeded. Please try again later.",
)
raise HTTPException(
status_code=e.response.status_code,
detail=f"AVE API error: {e.response.text}",
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch token details: {str(e)}",
)
@router.get("/klines/{token_id}")
async def get_klines(
token_id: str,
interval: str = "1h",
limit: int = 100,
start_time: Optional[int] = None,
end_time: Optional[int] = None,
current_user: User = Depends(get_current_user),
):
client = get_ave_client()
try:
klines = await client.get_klines(
token_id=token_id,
interval=interval,
limit=limit,
start_time=start_time,
end_time=end_time,
)
return {"klines": klines}
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Rate limit exceeded. Please try again later.",
)
raise HTTPException(
status_code=e.response.status_code,
detail=f"AVE API error: {e.response.text}",
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch klines: {str(e)}",
)
@router.get("/tokens/trending")
async def get_trending_tokens(
chain: Optional[str] = None,
limit: int = 20,
current_user: User = Depends(get_current_user),
):
client = get_ave_client()
try:
tokens = await client.get_trending_tokens(chain=chain, limit=limit)
return {"tokens": tokens}
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Rate limit exceeded. Please try again later.",
)
raise HTTPException(
status_code=e.response.status_code,
detail=f"AVE API error: {e.response.text}",
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch trending tokens: {str(e)}",
)
@router.get("/contracts/{contract_id}")
async def get_token_risk(
contract_id: str,
current_user: User = Depends(get_current_user),
):
client = get_ave_client()
try:
risk = await client.get_token_risk(contract_id)
if risk is None:
return {"risk": None, "upsell_message": None}
return {"risk": risk}
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Rate limit exceeded. Please try again later.",
)
raise HTTPException(
status_code=e.response.status_code,
detail=f"AVE API error: {e.response.text}",
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch token risk: {str(e)}",
)
@router.post("/chain/quote")
async def get_chain_quote(
request: AveChainQuoteRequest,
current_user: User = Depends(get_current_user),
):
client = get_ave_client()
try:
quote = await client.get_chain_quote(
chain=request.chain,
from_token=request.from_token,
to_token=request.to_token,
amount=request.amount,
slippage=request.slippage,
)
if quote is None:
return {"quote": None, "upsell_message": None}
return {"quote": quote}
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Rate limit exceeded. Please try again later.",
)
raise HTTPException(
status_code=e.response.status_code,
detail=f"AVE API error: {e.response.text}",
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch chain quote: {str(e)}",
)
@router.post("/chain/swap")
async def get_chain_swap(
request: AveChainSwapRequest,
current_user: User = Depends(get_current_user),
):
client = get_ave_client()
try:
swap = await client.get_chain_swap(
chain=request.chain,
from_token=request.from_token,
to_token=request.to_token,
amount=request.amount,
slippage=request.slippage,
wallet_address=request.wallet_address,
)
if swap is None:
return {"swap": None, "upsell_message": None}
return {"swap": swap}
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Rate limit exceeded. Please try again later.",
)
raise HTTPException(
status_code=e.response.status_code,
detail=f"AVE API error: {e.response.text}",
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to fetch chain swap: {str(e)}",
)

View File

@@ -25,7 +25,6 @@ class User(Base):
id = Column(String, primary_key=True, default=generate_uuid) id = Column(String, primary_key=True, default=generate_uuid)
email = Column(String, unique=True, nullable=False) email = Column(String, unique=True, nullable=False)
password_hash = Column(String, nullable=False) password_hash = Column(String, nullable=False)
tier = Column(String, default="free")
created_at = Column(DateTime, default=datetime.utcnow) created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

View File

@@ -35,8 +35,8 @@ class UserSettingsUpdate(BaseModel):
class BotCreate(BaseModel): class BotCreate(BaseModel):
name: str name: str
description: Optional[str] = None description: Optional[str] = None
strategy_config: Optional[dict] = {} strategy_config: dict
llm_config: Optional[dict] = {} llm_config: dict
class BotUpdate(BaseModel): class BotUpdate(BaseModel):
@@ -144,72 +144,3 @@ class SignalResponse(BaseModel):
class Config: class Config:
from_attributes = True from_attributes = True
class AveTokenSearchResponse(BaseModel):
tokens: List[dict]
upsell_message: Optional[str] = None
class AveBatchPricesRequest(BaseModel):
token_ids: List[str]
class AveBatchPricesResponse(BaseModel):
prices: Dict[str, dict]
upsell_message: Optional[str] = None
class AveTokenDetailsResponse(BaseModel):
token: Optional[dict] = None
upsell_message: Optional[str] = None
class AveKlinesRequest(BaseModel):
token_id: str
interval: str = "1h"
limit: int = 100
start_time: Optional[int] = None
end_time: Optional[int] = None
class AveKlinesResponse(BaseModel):
klines: List[dict]
upsell_message: Optional[str] = None
class AveTrendingTokensResponse(BaseModel):
tokens: List[dict]
upsell_message: Optional[str] = None
class AveTokenRiskResponse(BaseModel):
risk: Optional[dict] = None
upsell_message: Optional[str] = None
class AveChainQuoteRequest(BaseModel):
chain: str
from_token: str
to_token: str
amount: str
slippage: float = 0.5
class AveChainQuoteResponse(BaseModel):
quote: Optional[dict] = None
upsell_message: Optional[str] = None
class AveChainSwapRequest(BaseModel):
chain: str
from_token: str
to_token: str
amount: str
slippage: float = 0.5
wallet_address: Optional[str] = None
class AveChainSwapResponse(BaseModel):
swap: Optional[dict] = None
upsell_message: Optional[str] = None

View File

@@ -1,35 +1,14 @@
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from slowapi import Limiter from slowapi import Limiter
from slowapi.util import get_remote_address from slowapi.util import get_remote_address
from .api import auth, bots, backtest, simulate, config, ave from .api import auth, bots, backtest, simulate, config
from .core.limiter import limiter from .core.limiter import limiter
from .core.database import engine, Base
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Initialize database on startup."""
# Import all models to ensure they're registered
from .db.models import User, Bot, BotConversation, Backtest, Simulation, Signal
# Create tables if they don't exist
Base.metadata.create_all(bind=engine)
logger.info("Database initialized successfully")
yield
# Cleanup on shutdown if needed
app = FastAPI( app = FastAPI(
title="Randebu Trading Bot API", title="Randebu Trading Bot API",
description="AI-powered trading bot platform API", description="AI-powered trading bot platform API",
version="0.1.0", version="0.1.0",
lifespan=lifespan,
) )
app.state.limiter = limiter app.state.limiter = limiter
@@ -47,7 +26,6 @@ app.include_router(bots.router, prefix="/api/bots", tags=["bots"])
app.include_router(backtest.router, prefix="/api", tags=["backtest"]) app.include_router(backtest.router, prefix="/api", tags=["backtest"])
app.include_router(simulate.router, prefix="/api", tags=["simulate"]) app.include_router(simulate.router, prefix="/api", tags=["simulate"])
app.include_router(config.router, prefix="/api/config", tags=["config"]) app.include_router(config.router, prefix="/api/config", tags=["config"])
app.include_router(ave.router, prefix="/api/ave", tags=["ave"])
@app.get("/") @app.get("/")

View File

@@ -1,3 +0,0 @@
from .client import AveCloudClient, check_tier_access
__all__ = ["AveCloudClient", "check_tier_access"]

View File

@@ -1,213 +0,0 @@
import httpx
from typing import List, Dict, Any, Optional
from datetime import datetime
class AveCloudClient:
DATA_API_URL = "https://prod.ave-api.com"
TRADING_API_URL = "https://bot-api.ave.ai"
def __init__(self, api_key: str, plan: str = "free"):
self.api_key = api_key
self.plan = plan
def _data_headers(self) -> Dict[str, str]:
return {"X-API-KEY": self.api_key}
def _trading_headers(self) -> Dict[str, str]:
return {"X-API-KEY": self.api_key, "Content-Type": "application/json"}
async def get_tokens(
self,
query: Optional[str] = None,
chain: Optional[str] = None,
limit: int = 20,
) -> List[Dict[str, Any]]:
url = f"{self.DATA_API_URL}/v2/tokens"
params = {"limit": limit}
if query:
params["query"] = query
if chain:
params["chain"] = chain
async with httpx.AsyncClient() as client:
response = await client.get(
url, headers=self._data_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 tokens: {data}")
async def get_batch_prices(self, token_ids: List[str]) -> Dict[str, Dict[str, Any]]:
url = f"{self.DATA_API_URL}/v2/tokens/price"
async with httpx.AsyncClient() as client:
response = await client.post(
url,
headers=self._data_headers(),
json={"token_ids": token_ids},
timeout=30.0,
)
response.raise_for_status()
data = response.json()
if data.get("status") == 200:
return data.get("data", {})
return {}
async def get_token_details(self, token_id: str) -> Optional[Dict[str, Any]]:
url = f"{self.DATA_API_URL}/v2/tokens/{token_id}"
async with httpx.AsyncClient() as client:
response = await client.get(url, headers=self._data_headers(), timeout=30.0)
response.raise_for_status()
data = response.json()
if data.get("status") == 200:
return data.get("data")
return None
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.DATA_API_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._data_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_trending_tokens(
self, chain: Optional[str] = None, limit: int = 20
) -> List[Dict[str, Any]]:
url = f"{self.DATA_API_URL}/v2/tokens/trending"
params = {"limit": limit}
if chain:
params["chain"] = chain
async with httpx.AsyncClient() as client:
response = await client.get(
url, headers=self._data_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 trending tokens: {data}")
async def get_token_risk(self, contract_id: str) -> Optional[Dict[str, Any]]:
url = f"{self.DATA_API_URL}/v2/contracts/{contract_id}"
async with httpx.AsyncClient() as client:
response = await client.get(url, headers=self._data_headers(), timeout=30.0)
response.raise_for_status()
data = response.json()
if data.get("status") == 200:
return data.get("data")
return None
async def get_chain_quote(
self,
chain: str,
from_token: str,
to_token: str,
amount: str,
slippage: float = 0.5,
) -> Optional[Dict[str, Any]]:
url = f"{self.TRADING_API_URL}/v1/chain/quote"
payload = {
"chain": chain,
"from_token": from_token,
"to_token": to_token,
"amount": amount,
"slippage": slippage,
}
async with httpx.AsyncClient() as client:
response = await client.post(
url, headers=self._trading_headers(), json=payload, timeout=30.0
)
response.raise_for_status()
data = response.json()
if data.get("status") == 200:
return data.get("data")
return None
async def get_chain_swap(
self,
chain: str,
from_token: str,
to_token: str,
amount: str,
slippage: float = 0.5,
wallet_address: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
url = f"{self.TRADING_API_URL}/v1/chain/swap"
payload = {
"chain": chain,
"from_token": from_token,
"to_token": to_token,
"amount": amount,
"slippage": slippage,
}
if wallet_address:
payload["wallet_address"] = wallet_address
async with httpx.AsyncClient() as client:
response = await client.post(
url, headers=self._trading_headers(), json=payload, timeout=60.0
)
response.raise_for_status()
data = response.json()
if data.get("status") == 200:
return data.get("data")
return None
def check_tier_access(user_tier: str, feature: str) -> tuple[bool, Optional[str]]:
tier_access = {
"free": {
"data_rest": True,
"websocket": False,
"chain_wallet": True,
"proxy_wallet": False,
},
"normal": {
"data_rest": True,
"websocket": False,
"chain_wallet": True,
"proxy_wallet": True,
},
"pro": {
"data_rest": True,
"websocket": True,
"chain_wallet": True,
"proxy_wallet": True,
},
}
if user_tier not in tier_access:
user_tier = "free"
access = tier_access[user_tier]
if access.get(feature):
return True, None
upsell_messages = {
"websocket": "Upgrade to Pro plan to access WebSocket streaming data. Visit your account settings.",
"proxy_wallet": "Upgrade to Normal or Pro plan to access Proxy Wallet functionality. Visit your account settings.",
}
return False, upsell_messages.get(
feature, "Upgrade your plan to access this feature."
)

View File

@@ -1,313 +0,0 @@
<script lang="ts">
import type { BacktestResult } from '$lib/api';
interface ChartDataPoint {
timestamp: string;
value: number;
}
interface Props {
results: BacktestResult | null;
signals?: Array<{ created_at: string; signal_type: string; price: number }>;
height?: number;
}
let { results, signals = [], height = 300 }: Props = $props();
let width = $state(800);
let containerEl: HTMLDivElement;
$effect(() => {
if (containerEl) {
width = containerEl.clientWidth;
}
});
function generatePortfolioCurve(): ChartDataPoint[] {
if (!results || signals.length === 0) return [];
const points: ChartDataPoint[] = [];
const startValue = 10000;
let currentValue = startValue;
const sortedSignals = [...signals].sort(
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
);
points.push({
timestamp: sortedSignals[0]?.created_at || new Date().toISOString(),
value: currentValue
});
for (const signal of sortedSignals) {
if (signal.signal_type === 'buy') {
currentValue *= 1.05;
} else if (signal.signal_type === 'sell') {
currentValue *= 0.95;
}
points.push({
timestamp: signal.created_at,
value: currentValue
});
}
return points;
}
function getChartArea(w: number, h: number): { x: number; y: number; width: number; height: number } {
const padding = { top: 20, right: 20, bottom: 40, left: 60 };
return {
x: padding.left,
y: padding.top,
width: w - padding.left - padding.right,
height: h - padding.top - padding.bottom
};
}
function getValueRange(pts: ChartDataPoint[]): { min: number; max: number } {
if (pts.length === 0) return { min: 0, max: 10000 };
const values = pts.map(p => p.value);
const min = Math.min(...values);
const max = Math.max(...values);
const padding = (max - min) * 0.1 || 1000;
return { min: min - padding, max: max + padding };
}
function getPointPosition(point: ChartDataPoint, index: number, total: number, area: { x: number; y: number; width: number; height: number }, range: { min: number; max: number }): { x: number; y: number } {
const x = area.x + (index / Math.max(total - 1, 1)) * area.width;
const normalizedValue = (point.value - range.min) / (range.max - range.min);
const y = area.y + area.height - normalizedValue * area.height;
return { x, y };
}
function getYAxisLabels(area: { x: number; y: number; width: number; height: number }, range: { min: number; max: number }): Array<{ value: number; y: number }> {
const step = (range.max - range.min) / 4;
return [0, 1, 2, 3, 4].map(i => ({
value: range.max - i * step,
y: area.y + (i / 4) * area.height
}));
}
function getXAxisLabels(pts: ChartDataPoint[], area: { x: number; y: number; width: number; height: number }, range: { min: number; max: number }): Array<{ label: string; x: number }> {
if (pts.length === 0) return [];
const step = Math.max(1, Math.floor(pts.length / 5));
return pts
.filter((_, i) => i % step === 0 || i === pts.length - 1)
.map((p, i, arr) => ({
label: new Date(p.timestamp).toLocaleDateString(),
x: getPointPosition(p, pts.indexOf(p), pts.length, area, range).x
}));
}
function getReturnColor(): string {
if (!results) return '#888';
return results.total_return >= 0 ? '#22c55e' : '#ef4444';
}
let points = $derived(generatePortfolioCurve());
let area = $derived(getChartArea(width, height));
let range = $derived(getValueRange(points));
let yAxisLabels = $derived(getYAxisLabels(area, range));
let xAxisLabels = $derived(getXAxisLabels(points, area, range));
</script>
<div class="backtest-chart" bind:this={containerEl}>
{#if !results}
<div class="empty-state">
<p>No backtest results to display</p>
</div>
{:else}
<div class="chart-header">
<div class="metric">
<span class="metric-label">Total Return</span>
<span class="metric-value" style="color: {getReturnColor()}">
{results.total_return >= 0 ? '+' : ''}{results.total_return.toFixed(2)}%
</span>
</div>
<div class="metric">
<span class="metric-label">Win Rate</span>
<span class="metric-value">{results.win_rate.toFixed(1)}%</span>
</div>
<div class="metric">
<span class="metric-label">Total Trades</span>
<span class="metric-value">{results.total_trades}</span>
</div>
<div class="metric">
<span class="metric-label">Sharpe Ratio</span>
<span class="metric-value">{results.sharpe_ratio.toFixed(2)}</span>
</div>
</div>
<svg {width} {height} viewBox="0 0 {width} {height}">
<defs>
<linearGradient id="portfolioGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="rgba(102, 126, 234, 0.4)" />
<stop offset="100%" stop-color="rgba(102, 126, 234, 0)" />
</linearGradient>
</defs>
<g class="grid-lines">
{#each [0, 1, 2, 3, 4] as i}
{@const y = area.y + (i / 4) * area.height}
<line
x1={area.x}
y1={y}
x2={area.x + area.width}
y2={y}
stroke="rgba(255,255,255,0.08)"
stroke-dasharray="4,4"
/>
{/each}
</g>
<g class="y-axis">
{#each yAxisLabels as label}
<text x={area.x - 8} y={label.y + 4} class="axis-label" text-anchor="end">
${label.value.toLocaleString()}
</text>
{/each}
</g>
<g class="x-axis">
{#each xAxisLabels as label}
<text x={label.x} y={height - 8} class="axis-label" text-anchor="middle">
{label.label}
</text>
{/each}
</g>
{#if points.length > 1}
<path
d={points.map((p, i) => {
const pos = getPointPosition(p, i, points.length, area, range);
if (i === 0) {
return `M ${pos.x} ${area.y + area.height} L ${pos.x} ${pos.y}`;
}
return `L ${pos.x} ${pos.y}`;
}).join(' ') + ` L ${getPointPosition(points[points.length - 1], points.length - 1, points.length, area, range).x} ${area.y + area.height} Z`}
fill="url(#portfolioGradient)"
/>
<path
d={points.map((p, i) => {
const pos = getPointPosition(p, i, points.length, area, range);
return `${i === 0 ? 'M' : 'L'} ${pos.x} ${pos.y}`;
}).join(' ')}
fill="none"
stroke="#667eea"
stroke-width="2.5"
/>
{/if}
</svg>
<div class="chart-footer">
<div class="stat">
<span class="stat-label">Buy Signals</span>
<span class="stat-value buy">{results.buy_signals}</span>
</div>
<div class="stat">
<span class="stat-label">Sell Signals</span>
<span class="stat-value sell">{results.sell_signals}</span>
</div>
<div class="stat">
<span class="stat-label">Max Drawdown</span>
<span class="stat-value negative">-{results.max_drawdown.toFixed(2)}%</span>
</div>
</div>
{/if}
</div>
<style>
.backtest-chart {
width: 100%;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1rem;
box-sizing: border-box;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
color: #666;
}
.chart-header {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.metric {
display: flex;
flex-direction: column;
gap: 0.25rem;
text-align: center;
}
.metric-label {
font-size: 0.75rem;
color: #888;
text-transform: uppercase;
}
.metric-value {
font-size: 1.25rem;
font-weight: 600;
}
svg {
display: block;
width: 100%;
height: auto;
}
.axis-label {
font-size: 10px;
fill: #666;
}
.chart-footer {
display: flex;
justify-content: center;
gap: 2rem;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.stat-label {
font-size: 0.75rem;
color: #888;
}
.stat-value {
font-size: 1rem;
font-weight: 500;
}
.buy {
color: #22c55e;
}
.sell {
color: #ef4444;
}
.negative {
color: #ef4444;
}
</style>

View File

@@ -1,127 +0,0 @@
<script lang="ts">
import type { Bot } from '$lib/api';
interface Props {
bot: Bot;
onOpen?: (botId: string) => void;
onDelete?: (botId: string) => void;
showActions?: boolean;
}
let { bot, onOpen, onDelete, showActions = true }: Props = $props();
function handleOpen() {
onOpen?.(bot.id);
}
function handleDelete(e: Event) {
e.stopPropagation();
onDelete?.(bot.id);
}
</script>
<div class="bot-card" onclick={handleOpen} role="button" tabindex="0" onkeydown={(e) => e.key === 'Enter' && handleOpen()}>
<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>
{#if showActions}
<div class="bot-actions" onclick={(e) => e.stopPropagation()} role="group">
<button class="btn btn-primary" onclick={handleOpen}>Open</button>
<button class="btn btn-danger" onclick={handleDelete}>Delete</button>
</div>
{/if}
</div>
<style>
.bot-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
cursor: pointer;
transition: transform 0.2s, border-color 0.2s;
}
.bot-card:hover {
transform: translateY(-2px);
border-color: rgba(255, 255, 255, 0.2);
}
.bot-card:focus {
outline: 2px solid #667eea;
outline-offset: 2px;
}
.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;
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-danger {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.4);
}
.btn:hover {
transform: translateY(-2px);
}
</style>

View File

@@ -1,94 +0,0 @@
<script lang="ts">
import type { Bot } from '$lib/api';
interface Props {
bots: Bot[];
selectedBotId?: string | null;
onSelect: (botId: string) => void;
disabled?: boolean;
label?: string;
}
let { bots, selectedBotId = null, onSelect, disabled = false, label = 'Select Bot' }: Props = $props();
function handleChange(e: Event) {
const target = e.target as HTMLSelectElement;
onSelect(target.value);
}
const MAX_BOTS = 3;
</script>
<div class="bot-selector">
{#if label}
<label for="bot-select">{label}</label>
{/if}
<div class="select-wrapper">
<select
id="bot-select"
onchange={handleChange}
disabled={disabled || bots.length === 0}
value={selectedBotId || ''}
>
{#if bots.length === 0}
<option value="" disabled>No bots available</option>
{:else}
{#each bots as bot}
<option value={bot.id}>{bot.name}</option>
{/each}
{/if}
</select>
<span class="bot-count">{bots.length}/{MAX_BOTS}</span>
</div>
</div>
<style>
.bot-selector {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
label {
font-size: 0.9rem;
color: #888;
}
.select-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
}
select {
flex: 1;
padding: 0.75rem 1rem;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 1rem center;
padding-right: 2.5rem;
}
select:focus {
outline: none;
border-color: #667eea;
}
select:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.bot-count {
font-size: 0.8rem;
color: #666;
white-space: nowrap;
}
</style>

View File

@@ -1,300 +0,0 @@
<script lang="ts">
import type { Bot } from '$lib/api';
import type { ChatMessage } from '$lib/stores/chatStore';
interface Props {
bot: Bot | null;
messages: ChatMessage[];
isSending?: boolean;
onSendMessage: (message: string) => void;
onSelectBot?: (botId: string) => void;
availableBots?: Bot[];
showBotSelector?: boolean;
}
let {
bot,
messages,
isSending = false,
onSendMessage,
onSelectBot,
availableBots = [],
showBotSelector = false
}: Props = $props();
let messageInput = $state('');
let chatContainer: HTMLDivElement;
function handleSend() {
if (!messageInput.trim()) return;
onSendMessage(messageInput);
messageInput = '';
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}
function handleBotChange(e: Event) {
const target = e.target as HTMLSelectElement;
if (onSelectBot && target.value) {
onSelectBot(target.value);
}
}
$effect(() => {
if (messages.length && chatContainer) {
setTimeout(() => {
chatContainer.scrollTop = chatContainer.scrollHeight;
}, 50);
}
});
</script>
<div class="chat-interface">
{#if showBotSelector && availableBots.length > 0}
<div class="bot-selector">
<label for="bot-select">Active Bot:</label>
<select id="bot-select" onchange={handleBotChange}>
{#each availableBots as availableBot}
<option value={availableBot.id} selected={availableBot.id === bot?.id}>
{availableBot.name}
</option>
{/each}
</select>
</div>
{/if}
<div class="chat-messages" bind:this={chatContainer}>
{#if messages.length === 0}
<div class="welcome-message">
<p>Welcome to {bot?.name || 'your bot'}! 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 messages 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 bot}
<div class="input-container">
<textarea
bind:value={messageInput}
onkeydown={handleKeydown}
placeholder="Describe your trading strategy..."
rows="1"
disabled={isSending}
></textarea>
<button onclick={handleSend} disabled={isSending || !messageInput.trim()}>
Send
</button>
</div>
{/if}
</div>
<style>
.chat-interface {
display: flex;
flex-direction: column;
height: 100%;
}
.bot-selector {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.03);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.bot-selector label {
font-size: 0.9rem;
color: #888;
}
.bot-selector select {
padding: 0.5rem 0.75rem;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 0.9rem;
cursor: pointer;
}
.bot-selector select:focus {
outline: none;
border-color: #667eea;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.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.system {
align-items: center;
}
.message-content {
max-width: 70%;
padding: 0.75rem 1rem;
border-radius: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.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.system .message-content {
background: rgba(251, 191, 36, 0.1);
color: #fbbf24;
font-size: 0.9rem;
border: 1px solid rgba(251, 191, 36, 0.3);
}
.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: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
textarea {
flex: 1;
padding: 0.75rem 1rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1rem;
resize: none;
font-family: inherit;
}
textarea:focus {
outline: none;
border-color: #667eea;
}
button {
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s;
}
button:hover:not(:disabled) {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>

View File

@@ -1,348 +0,0 @@
<script lang="ts">
import type { Condition } from '$lib/api';
import TokenPicker from './TokenPicker.svelte';
interface Props {
conditions: Condition[];
onUpdate: (conditions: Condition[]) => void;
disabled?: boolean;
}
let { conditions, onUpdate, disabled = false }: Props = $props();
type ConditionType = Condition['type'];
const conditionTypes: { value: ConditionType; label: string; description: string }[] = [
{ value: 'price_drop', label: 'Price Drop', description: 'Trigger when price falls by X%' },
{ value: 'price_rise', label: 'Price Rise', description: 'Trigger when price rises by X%' },
{ value: 'volume_spike', label: 'Volume Spike', description: 'Trigger when volume increases by X%' },
{ value: 'price_level', label: 'Price Level', description: 'Trigger when price crosses a level' },
];
const timeframes = ['1m', '5m', '15m', '1h', '4h', '1d'];
function addCondition() {
const newCondition: Condition = {
type: 'price_drop',
token: '',
threshold: 5,
timeframe: '1h'
};
onUpdate([...conditions, newCondition]);
}
function removeCondition(index: number) {
onUpdate(conditions.filter((_, i) => i !== index));
}
function updateCondition(index: number, updates: Partial<Condition>) {
const updated = conditions.map((c, i) =>
i === index ? { ...c, ...updates } : c
);
onUpdate(updated);
}
function getConditionDescription(condition: Condition): string {
switch (condition.type) {
case 'price_drop':
return `Price drops ${condition.threshold || 0}% within ${condition.timeframe || '1h'}`;
case 'price_rise':
return `Price rises ${condition.threshold || 0}% within ${condition.timeframe || '1h'}`;
case 'volume_spike':
return `Volume spikes ${condition.threshold || 0}% within ${condition.timeframe || '1h'}`;
case 'price_level':
return `Price crosses ${condition.direction || 'above'} $${condition.price || 0}`;
default:
return 'Unknown condition';
}
}
</script>
<div class="condition-builder">
<div class="conditions-header">
<h4>Conditions</h4>
<button type="button" class="add-btn" onclick={addCondition} {disabled}>
+ Add Condition
</button>
</div>
{#if conditions.length === 0}
<div class="empty-state">
<p>No conditions set</p>
<p class="hint">Add a condition to define when your strategy triggers</p>
</div>
{:else}
<div class="conditions-list">
{#each conditions as condition, index}
<div class="condition-card">
<div class="condition-header">
<span class="condition-number">#{index + 1}</span>
<button
type="button"
class="remove-btn"
onclick={() => removeCondition(index)}
disabled={disabled}
aria-label="Remove condition"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="condition-fields">
<div class="field">
<label for="type-{index}">Type</label>
<select
id="type-{index}"
value={condition.type}
onchange={(e) => updateCondition(index, { type: (e.target as HTMLSelectElement).value as ConditionType })}
disabled={disabled}
>
{#each conditionTypes as ct}
<option value={ct.value}>{ct.label}</option>
{/each}
</select>
</div>
<TokenPicker
label="Token"
selectedToken={condition.token}
selectedChain={condition.chain || ''}
onSelect={(token, chain) => updateCondition(index, { token, chain })}
disabled={disabled}
/>
{#if condition.type === 'price_level'}
<div class="field">
<label for="direction-{index}">Direction</label>
<select
id="direction-{index}"
value={condition.direction || 'above'}
onchange={(e) => updateCondition(index, { direction: (e.target as HTMLSelectElement).value as 'above' | 'below' })}
disabled={disabled}
>
<option value="above">Above</option>
<option value="below">Below</option>
</select>
</div>
<div class="field">
<label for="price-{index}">Price ($)</label>
<input
id="price-{index}"
type="number"
value={condition.price || ''}
oninput={(e) => updateCondition(index, { price: parseFloat((e.target as HTMLInputElement).value) || undefined })}
placeholder="0.000001"
step="any"
min="0"
disabled={disabled}
/>
</div>
{:else}
<div class="field">
<label for="threshold-{index}">Threshold (%)</label>
<input
id="threshold-{index}"
type="number"
value={condition.threshold || ''}
oninput={(e) => updateCondition(index, { threshold: parseFloat((e.target as HTMLInputElement).value) || undefined })}
placeholder="5"
step="any"
min="0"
disabled={disabled}
/>
</div>
<div class="field">
<label for="timeframe-{index}">Timeframe</label>
<select
id="timeframe-{index}"
value={condition.timeframe || '1h'}
onchange={(e) => updateCondition(index, { timeframe: (e.target as HTMLSelectElement).value })}
disabled={disabled}
>
{#each timeframes as tf}
<option value={tf}>{tf}</option>
{/each}
</select>
</div>
{/if}
</div>
<div class="condition-preview">
<span class="preview-label">Summary:</span>
{getConditionDescription(condition)}
</div>
</div>
{/each}
</div>
{/if}
</div>
<style>
.condition-builder {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1rem;
}
.conditions-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
h4 {
margin: 0;
font-size: 0.9rem;
color: #888;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.add-btn {
padding: 0.5rem 1rem;
background: rgba(102, 126, 234, 0.2);
color: #667eea;
border: 1px solid rgba(102, 126, 234, 0.4);
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.add-btn:hover:not(:disabled) {
background: rgba(102, 126, 234, 0.3);
}
.add-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.empty-state {
text-align: center;
padding: 1.5rem;
color: #666;
}
.empty-state .hint {
font-size: 0.85rem;
margin-top: 0.5rem;
}
.conditions-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.condition-card {
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 1rem;
}
.condition-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.condition-number {
font-size: 0.85rem;
font-weight: 600;
color: #667eea;
}
.remove-btn {
background: none;
border: none;
color: #888;
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background 0.2s, color 0.2s;
}
.remove-btn:hover:not(:disabled) {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.remove-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.condition-fields {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.field {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.field label {
font-size: 0.8rem;
color: #888;
}
input,
select {
padding: 0.5rem 0.75rem;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 0.9rem;
}
input:focus,
select:focus {
outline: none;
border-color: #667eea;
}
input:disabled,
select:disabled {
opacity: 0.5;
cursor: not-allowed;
}
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.condition-preview {
padding-top: 0.75rem;
border-top: 1px solid rgba(255, 255, 255, 0.05);
font-size: 0.85rem;
color: #aaa;
}
.preview-label {
color: #666;
margin-right: 0.5rem;
}
</style>

View File

@@ -1,121 +0,0 @@
<script lang="ts">
interface Props {
feature?: string;
dismissible?: boolean;
onDismiss?: () => void;
}
let { feature, dismissible = true, onDismiss }: Props = $props();
</script>
<div class="pro-upgrade-banner">
<div class="banner-content">
<div class="banner-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
</svg>
</div>
<div class="banner-text">
<strong>Upgrade to Pro</strong>
{#if feature}
<p>{feature}</p>
{:else}
<p>Unlock advanced features and unlimited bots</p>
{/if}
</div>
<a href="/settings" class="upgrade-btn">Upgrade Now</a>
</div>
{#if dismissible && onDismiss}
<button class="dismiss-btn" onclick={onDismiss} aria-label="Dismiss">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
{/if}
</div>
<style>
.pro-upgrade-banner {
position: relative;
display: flex;
align-items: center;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 12px;
padding: 1rem 1.5rem;
margin: 1rem 0;
}
.banner-content {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
}
.banner-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: rgba(102, 126, 234, 0.2);
border-radius: 8px;
color: #667eea;
}
.banner-text {
flex: 1;
}
.banner-text strong {
display: block;
color: #fff;
font-size: 1rem;
margin-bottom: 0.25rem;
}
.banner-text p {
margin: 0;
font-size: 0.85rem;
color: #aaa;
}
.upgrade-btn {
padding: 0.5rem 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
font-size: 0.9rem;
transition: transform 0.2s, box-shadow 0.2s;
}
.upgrade-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.dismiss-btn {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: none;
border: none;
color: #888;
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background 0.2s, color 0.2s;
}
.dismiss-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: #fff;
}
</style>

View File

@@ -1,228 +0,0 @@
<script lang="ts">
import type { Signal } from '$lib/api';
interface Props {
signals: Signal[];
height?: number;
}
let { signals, height = 200 }: Props = $props();
let width = $state(800);
let containerEl: HTMLDivElement;
$effect(() => {
if (containerEl) {
width = containerEl.clientWidth;
}
});
function getSignalPosition(signal: Signal, index: number, total: number): { x: number; y: number } {
const padding = 30;
const chartWidth = width - padding * 2;
const chartHeight = height - padding * 2;
const x = padding + (index / Math.max(total - 1, 1)) * chartWidth;
const priceRange = getPriceRange();
const normalizedPrice = priceRange.min === priceRange.max ? 0.5 :
(signal.price - priceRange.min) / (priceRange.max - priceRange.min);
const y = padding + (1 - normalizedPrice) * chartHeight;
return { x, y };
}
function getPriceRange(): { min: number; max: number } {
if (signals.length === 0) return { min: 0, max: 1 };
const prices = signals.map(s => s.price);
const min = Math.min(...prices);
const max = Math.max(...prices);
const padding = (max - min) * 0.1 || 1;
return { min: min - padding, max: max + padding };
}
function getSignalColor(signal: Signal): string {
switch (signal.signal_type) {
case 'buy': return '#22c55e';
case 'sell': return '#ef4444';
case 'hold': return '#fbbf24';
default: return '#888';
}
}
function getYAxisLabels(): string[] {
const range = getPriceRange();
const step = (range.max - range.min) / 4;
return [
range.max.toFixed(6),
(range.max - step).toFixed(6),
(range.min + step).toFixed(6),
range.min.toFixed(6)
];
}
function getXAxisLabels(): string[] {
if (signals.length === 0) return [];
const step = Math.max(1, Math.floor(signals.length / 5));
const labels: string[] = [];
for (let i = 0; i < signals.length; i += step) {
labels.push(new Date(signals[i].created_at).toLocaleTimeString());
}
return labels;
}
</script>
<div class="signal-chart" bind:this={containerEl}>
{#if signals.length === 0}
<div class="empty-state">
<p>No signals to display</p>
</div>
{:else}
<svg {width} {height} viewBox="0 0 {width} {height}">
<defs>
<linearGradient id="chartGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="rgba(102, 126, 234, 0.3)" />
<stop offset="100%" stop-color="rgba(102, 126, 234, 0)" />
</linearGradient>
</defs>
<g class="grid-lines">
{#each [0, 1, 2, 3] as i}
{@const y = 30 + (i / 3) * (height - 60)}
<line
x1="30" y1={y}
x2={width - 30} y2={y}
stroke="rgba(255,255,255,0.1)"
stroke-dasharray="4,4"
/>
{/each}
</g>
<g class="y-axis">
{#each getYAxisLabels() as label, i}
{@const y = 30 + (i / 3) * (height - 60)}
<text x="25" y={y + 4} class="axis-label" text-anchor="end">${label}</text>
{/each}
</g>
<g class="x-axis">
{#each getXAxisLabels() as label, i}
{@const x = 30 + (i / (getXAxisLabels().length - 1 || 1)) * (width - 60)}
<text x={x} y={height - 8} class="axis-label" text-anchor="middle">{label}</text>
{/each}
</g>
<path
d={signals.map((s, i) => {
const pos = getSignalPosition(s, i, signals.length);
return `${i === 0 ? 'M' : 'L'} ${pos.x} ${pos.y}`;
}).join(' ')}
fill="none"
stroke="#667eea"
stroke-width="2"
/>
{#each signals as signal, i}
{@const pos = getSignalPosition(signal, i, signals.length)}
{@const color = getSignalColor(signal)}
<circle
cx={pos.x}
cy={pos.y}
r="6"
fill={color}
stroke={color}
stroke-width="2"
class="signal-dot"
>
<title>{signal.signal_type.toUpperCase()} - ${signal.price.toFixed(6)} - {new Date(signal.created_at).toLocaleString()}</title>
</circle>
{/each}
</svg>
<div class="legend">
<div class="legend-item">
<span class="legend-dot buy"></span>
<span>Buy</span>
</div>
<div class="legend-item">
<span class="legend-dot sell"></span>
<span>Sell</span>
</div>
<div class="legend-item">
<span class="legend-dot hold"></span>
<span>Hold</span>
</div>
</div>
{/if}
</div>
<style>
.signal-chart {
width: 100%;
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 1rem;
box-sizing: border-box;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: #666;
}
svg {
display: block;
width: 100%;
height: auto;
}
.axis-label {
font-size: 10px;
fill: #666;
}
.signal-dot {
cursor: pointer;
transition: r 0.2s;
}
.signal-dot:hover {
r: 8;
}
.legend {
display: flex;
justify-content: center;
gap: 1.5rem;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
color: #888;
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.legend-dot.buy {
background: #22c55e;
}
.legend-dot.sell {
background: #ef4444;
}
.legend-dot.hold {
background: #fbbf24;
}
</style>

View File

@@ -1,227 +0,0 @@
<script lang="ts">
import type { StrategyConfig } from '$lib/api';
interface Props {
config: StrategyConfig | null;
editable?: boolean;
onUpdate?: (config: StrategyConfig) => void;
}
let { config, editable = false, onUpdate }: Props = $props();
function getConditionDescription(condition: StrategyConfig['conditions'][0]): string {
switch (condition.type) {
case 'price_drop':
return `${condition.token} drops by ${condition.threshold}% within ${condition.timeframe}`;
case 'price_rise':
return `${condition.token} rises by ${condition.threshold}% within ${condition.timeframe}`;
case 'volume_spike':
return `${condition.token} volume spikes by ${condition.threshold}% within ${condition.timeframe}`;
case 'price_level':
return `${condition.token} crosses ${condition.direction} $${condition.price}`;
default:
return 'Unknown condition';
}
}
function getActionDescription(action: StrategyConfig['actions'][0]): string {
switch (action.type) {
case 'buy':
return `Buy ${action.amount_percent}% of ${action.token || 'portfolio'}`;
case 'sell':
return `Sell ${action.amount_percent}% of ${action.token || 'portfolio'}`;
case 'hold':
return 'Hold';
default:
return 'Unknown action';
}
}
</script>
<div class="strategy-preview">
{#if !config || (config.conditions.length === 0 && config.actions.length === 0)}
<div class="empty-state">
<p>No strategy configured yet.</p>
<p class="hint">Describe your trading strategy in the chat to create one.</p>
</div>
{:else}
<div class="strategy-section">
<h4>Conditions</h4>
{#if config.conditions.length === 0}
<p class="empty">No conditions set</p>
{:else}
<ul class="items-list">
{#each config.conditions as condition, i}
<li>
<span class="condition-badge">{condition.type.replace('_', ' ')}</span>
{getConditionDescription(condition)}
</li>
{/each}
</ul>
{/if}
</div>
<div class="strategy-section">
<h4>Actions</h4>
{#if config.actions.length === 0}
<p class="empty">No actions set</p>
{:else}
<ul class="items-list">
{#each config.actions as action}
<li>
<span class="action-badge action-{action.type}">{action.type}</span>
{getActionDescription(action)}
</li>
{/each}
</ul>
{/if}
</div>
{#if config.risk_management}
<div class="strategy-section">
<h4>Risk Management</h4>
<div class="risk-items">
{#if config.risk_management.stop_loss_percent}
<div class="risk-item">
<span class="risk-label">Stop Loss</span>
<span class="risk-value negative">{config.risk_management.stop_loss_percent}%</span>
</div>
{/if}
{#if config.risk_management.take_profit_percent}
<div class="risk-item">
<span class="risk-label">Take Profit</span>
<span class="risk-value positive">{config.risk_management.take_profit_percent}%</span>
</div>
{/if}
</div>
</div>
{/if}
{/if}
</div>
<style>
.strategy-preview {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 1rem;
}
.empty-state {
text-align: center;
padding: 1rem;
color: #888;
}
.empty-state .hint {
font-size: 0.85rem;
color: #666;
margin-top: 0.5rem;
}
.strategy-section {
margin-bottom: 1rem;
}
.strategy-section:last-child {
margin-bottom: 0;
}
h4 {
font-size: 0.85rem;
font-weight: 600;
color: #888;
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 0 0 0.75rem;
}
.items-list {
list-style: none;
padding: 0;
margin: 0;
}
.items-list li {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0;
font-size: 0.9rem;
color: #ccc;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.items-list li:last-child {
border-bottom: none;
}
.condition-badge,
.action-badge {
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
.condition-badge {
background: rgba(102, 126, 234, 0.2);
color: #667eea;
}
.action-badge {
min-width: 50px;
text-align: center;
}
.action-buy {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.action-sell {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.action-hold {
background: rgba(251, 191, 36, 0.2);
color: #fbbf24;
}
.empty {
color: #666;
font-size: 0.9rem;
font-style: italic;
}
.risk-items {
display: flex;
gap: 1.5rem;
}
.risk-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.risk-label {
font-size: 0.75rem;
color: #888;
}
.risk-value {
font-size: 1.1rem;
font-weight: 600;
}
.positive {
color: #22c55e;
}
.negative {
color: #ef4444;
}
</style>

View File

@@ -1,256 +0,0 @@
<script lang="ts">
import { api } from '$lib/api';
interface Token {
symbol: string;
chain: string;
name: string;
}
interface Props {
selectedToken?: string;
selectedChain?: string;
onSelect: (token: string, chain: string) => void;
disabled?: boolean;
label?: string;
}
let { selectedToken = '', selectedChain = '', onSelect, disabled = false, label = 'Select Token' }: Props = $props();
let searchQuery = $state('');
let isOpen = $state(false);
let tokens = $state<Token[]>([]);
let isLoading = $state(false);
let inputEl: HTMLInputElement;
let containerEl: HTMLDivElement;
const commonTokens: Token[] = [
{ symbol: 'BTC', chain: 'btc', name: 'Bitcoin' },
{ symbol: 'ETH', chain: 'eth', name: 'Ethereum' },
{ symbol: 'BNB', chain: 'bsc', name: 'BNB' },
{ symbol: 'PEPE', chain: 'bsc', name: 'Pepe' },
{ symbol: 'SHIB', chain: 'eth', name: 'Shiba Inu' },
{ symbol: 'DOGE', chain: 'doge', name: 'Dogecoin' },
{ symbol: 'SOL', chain: 'sol', name: 'Solana' },
{ symbol: 'XRP', chain: 'xrp', name: 'Ripple' },
];
$effect(() => {
function handleClickOutside(event: MouseEvent) {
if (containerEl && !containerEl.contains(event.target as Node)) {
isOpen = false;
}
}
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
});
async function loadTokens() {
isLoading = true;
try {
tokens = await api.config.getTokens();
} catch (e) {
tokens = commonTokens;
} finally {
isLoading = false;
}
}
function getFilteredTokens(): Token[] {
const allTokens = tokens.length > 0 ? tokens : commonTokens;
if (!searchQuery) return allTokens.slice(0, 10);
const query = searchQuery.toLowerCase();
return allTokens.filter(
t => t.symbol.toLowerCase().includes(query) ||
t.name.toLowerCase().includes(query) ||
t.chain.toLowerCase().includes(query)
).slice(0, 10);
}
function handleSelect(token: Token) {
onSelect(token.symbol, token.chain);
searchQuery = '';
isOpen = false;
}
function handleInputFocus() {
isOpen = true;
if (tokens.length === 0 && !isLoading) {
loadTokens();
}
}
</script>
<div class="token-picker" bind:this={containerEl}>
{#if label}
<label>{label}</label>
{/if}
<div class="input-wrapper">
<input
type="text"
bind:this={inputEl}
bind:value={searchQuery}
onfocus={handleInputFocus}
placeholder={selectedToken ? `${selectedToken}${selectedChain ? ` (${selectedChain})` : ''}` : 'Search tokens...'}
{disabled}
class:has-value={selectedToken}
/>
{#if selectedToken}
<button class="clear-btn" onclick={() => onSelect('', '')} disabled={disabled}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
{/if}
</div>
{#if isOpen}
<div class="dropdown">
{#if isLoading}
<div class="loading">Loading tokens...</div>
{:else if getFilteredTokens().length === 0}
<div class="no-results">No tokens found</div>
{:else}
{#each getFilteredTokens() as token}
<button
class="token-option"
class:selected={token.symbol === selectedToken && token.chain === selectedChain}
onclick={() => handleSelect(token)}
>
<span class="token-symbol">{token.symbol}</span>
<span class="token-chain">{token.chain.toUpperCase()}</span>
<span class="token-name">{token.name}</span>
</button>
{/each}
{/if}
</div>
{/if}
</div>
<style>
.token-picker {
position: relative;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
label {
font-size: 0.9rem;
color: #888;
}
.input-wrapper {
position: relative;
}
input {
width: 100%;
padding: 0.75rem 2.5rem 0.75rem 1rem;
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;
}
input.has-value {
border-color: rgba(102, 126, 234, 0.5);
}
input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.clear-btn {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #888;
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
}
.clear-btn:hover:not(:disabled) {
color: #fff;
}
.dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 0.5rem;
background: #1a1a1a;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
max-height: 250px;
overflow-y: auto;
z-index: 50;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
.loading,
.no-results {
padding: 1rem;
text-align: center;
color: #888;
font-size: 0.9rem;
}
.token-option {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
padding: 0.75rem 1rem;
background: none;
border: none;
color: #fff;
text-align: left;
cursor: pointer;
transition: background 0.2s;
}
.token-option:hover {
background: rgba(255, 255, 255, 0.05);
}
.token-option.selected {
background: rgba(102, 126, 234, 0.2);
}
.token-symbol {
font-weight: 600;
min-width: 60px;
}
.token-chain {
font-size: 0.75rem;
padding: 0.15rem 0.4rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
color: #888;
}
.token-name {
flex: 1;
color: #888;
font-size: 0.9rem;
}
</style>

View File

@@ -1,9 +0,0 @@
export { default as ChatInterface } from './ChatInterface.svelte';
export { default as BotCard } from './BotCard.svelte';
export { default as BotSelector } from './BotSelector.svelte';
export { default as StrategyPreview } from './StrategyPreview.svelte';
export { default as SignalChart } from './SignalChart.svelte';
export { default as BacktestChart } from './BacktestChart.svelte';
export { default as ProUpgradeBanner } from './ProUpgradeBanner.svelte';
export { default as TokenPicker } from './TokenPicker.svelte';
export { default as ConditionBuilder } from './ConditionBuilder.svelte';

View File

@@ -4,11 +4,11 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, chatStore, addMessage, setMessages, clearChat, currentBotStore, setCurrentBot } from '$lib/stores'; import { isAuthenticated, isLoading, chatStore, addMessage, setMessages, clearChat, currentBotStore, setCurrentBot } from '$lib/stores';
import { api } from '$lib/api'; import { api } from '$lib/api';
import { ChatInterface, StrategyPreview, ProUpgradeBanner } from '$lib/components';
let botId = $derived($page.params.id); let botId = $derived($page.params.id);
let messageInput = $state('');
let isSending = $state(false); let isSending = $state(false);
let showStrategy = $state(false); let chatContainer: HTMLDivElement;
onMount(async () => { onMount(async () => {
if (!$isAuthenticated && !$isLoading) { if (!$isAuthenticated && !$isLoading) {
@@ -34,18 +34,24 @@
try { try {
const history = await api.bots.getHistory(botId); const history = await api.bots.getHistory(botId);
setMessages(history); setMessages(history);
scrollToBottom();
} catch (e) { } catch (e) {
console.error('Failed to load chat history:', e); console.error('Failed to load chat history:', e);
} }
} }
async function handleSendMessage(message: string) { async function sendMessage() {
if (isSending) return; if (!messageInput.trim() || isSending) return;
const userMessage = messageInput;
messageInput = '';
isSending = true; isSending = true;
addMessage({ role: 'user', content: userMessage });
scrollToBottom();
try { try {
const response = await api.bots.chat(botId, message); const response = await api.bots.chat(botId, userMessage);
addMessage({ role: 'assistant', content: response.response }); addMessage({ role: 'assistant', content: response.response });
if (response.strategy_config) { if (response.strategy_config) {
@@ -56,11 +62,23 @@
addMessage({ role: 'assistant', content: 'Sorry, I encountered an error. Please try again.' }); addMessage({ role: 'assistant', content: 'Sorry, I encountered an error. Please try again.' });
} finally { } finally {
isSending = false; isSending = false;
scrollToBottom();
} }
} }
function toggleStrategy() { function scrollToBottom() {
showStrategy = !showStrategy; setTimeout(() => {
if (chatContainer) {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
}, 50);
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
} }
</script> </script>
@@ -75,32 +93,55 @@
<h1>{$currentBotStore?.name || 'Loading...'}</h1> <h1>{$currentBotStore?.name || 'Loading...'}</h1>
</div> </div>
<div class="header-actions"> <div class="header-actions">
{#if $currentBotStore?.strategy_config}
<button class="btn btn-secondary" onclick={toggleStrategy}>
{showStrategy ? 'Hide' : 'Show'} Strategy
</button>
{/if}
<a href="/bot/{botId}/backtest" class="btn btn-secondary">Backtest</a> <a href="/bot/{botId}/backtest" class="btn btn-secondary">Backtest</a>
<a href="/bot/{botId}/simulate" class="btn btn-secondary">Simulate</a> <a href="/bot/{botId}/simulate" class="btn btn-secondary">Simulate</a>
</div> </div>
</header> </header>
{#if showStrategy && $currentBotStore?.strategy_config} <div class="chat-container" bind:this={chatContainer}>
<div class="strategy-panel"> {#if $chatStore.length === 0}
<StrategyPreview config={$currentBotStore.strategy_config} /> <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> </div>
{/if} {/if}
<div class="chat-wrapper"> {#each $chatStore as message}
<ChatInterface <div class="message {message.role}">
bot={$currentBotStore} <div class="message-content">
messages={$chatStore} {message.content}
{isSending} </div>
onSendMessage={handleSendMessage} <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> </div>
<ProUpgradeBanner feature="Auto-execute trades with your bot" /> {#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> </main>
<style> <style>
@@ -176,14 +217,138 @@
transform: translateY(-2px); transform: translateY(-2px);
} }
.strategy-panel { .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; margin-bottom: 1rem;
} }
.chat-wrapper { .message.user {
flex: 1; align-items: flex-end;
overflow: hidden; }
.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; display: flex;
flex-direction: column; 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> </style>

View File

@@ -4,8 +4,6 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, backtestStore, setCurrentBacktest, addBacktestToHistory, setBacktestLoading, setBacktestError } from '$lib/stores'; import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, backtestStore, setCurrentBacktest, addBacktestToHistory, setBacktestLoading, setBacktestError } from '$lib/stores';
import { api } from '$lib/api'; import { api } from '$lib/api';
import { BacktestChart } from '$lib/components';
import type { Backtest } from '$lib/api';
let botId = $derived($page.params.id); let botId = $derived($page.params.id);
let token = $state('PEPE'); let token = $state('PEPE');
@@ -13,7 +11,6 @@
let startDate = $state(''); let startDate = $state('');
let endDate = $state(''); let endDate = $state('');
let isRunning = $state(false); let isRunning = $state(false);
let selectedBacktest = $state<Backtest | null>(null);
onMount(async () => { onMount(async () => {
if (!$isAuthenticated && !$isLoading) { if (!$isAuthenticated && !$isLoading) {
@@ -79,12 +76,6 @@
function setBacktestHistory(backtests: any[]) { function setBacktestHistory(backtests: any[]) {
backtestStore.update(state => ({ ...state, backtestHistory: backtests })); backtestStore.update(state => ({ ...state, backtestHistory: backtests }));
} }
function selectBacktest(backtest: Backtest) {
if (backtest.status === 'completed' && backtest.result) {
selectedBacktest = backtest;
}
}
</script> </script>
<svelte:head> <svelte:head>
@@ -186,16 +177,6 @@
</div> </div>
{/if} {/if}
</section> </section>
{#if selectedBacktest}
<section class="chart-section">
<div class="chart-header">
<h2>Portfolio Performance</h2>
<button class="close-btn" onclick={() => selectedBacktest = null}>×</button>
</div>
<BacktestChart results={selectedBacktest.result} />
</section>
{/if}
</div> </div>
</main> </main>
@@ -407,36 +388,4 @@
color: #fca5a5; color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.4); border: 1px solid rgba(239, 68, 68, 0.4);
} }
.chart-section {
padding: 1.5rem;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.chart-header h2 {
margin: 0;
}
.close-btn {
width: auto;
padding: 0.25rem 0.75rem;
background: rgba(255, 255, 255, 0.1);
border: none;
color: #888;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
border-radius: 4px;
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
</style> </style>

View File

@@ -4,7 +4,6 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, simulationStore, setCurrentSimulation, addSignals, clearSignals, setSimulationLoading, setSimulationError } from '$lib/stores'; import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, simulationStore, setCurrentSimulation, addSignals, clearSignals, setSimulationLoading, setSimulationError } from '$lib/stores';
import { api } from '$lib/api'; import { api } from '$lib/api';
import { SignalChart, ProUpgradeBanner } from '$lib/components';
let botId = $derived($page.params.id); let botId = $derived($page.params.id);
let token = $state('PEPE'); let token = $state('PEPE');
@@ -143,16 +142,12 @@
</form> </form>
</section> </section>
<ProUpgradeBanner feature="Real-time WebSocket signals for instant trading decisions" />
<section class="signals-section"> <section class="signals-section">
<h2>Signals ({$simulationStore.signals.length})</h2> <h2>Signals ({$simulationStore.signals.length})</h2>
{#if $simulationStore.signals.length === 0} {#if $simulationStore.signals.length === 0}
<p class="empty-state">No signals yet. Start a simulation to see trading signals.</p> <p class="empty-state">No signals yet. Start a simulation to see trading signals.</p>
{:else} {:else}
<SignalChart signals={$simulationStore.signals} height={200} />
<div class="signals-list"> <div class="signals-list">
{#each $simulationStore.signals as signal} {#each $simulationStore.signals as signal}
<div class="signal-card"> <div class="signal-card">

View File

@@ -3,7 +3,6 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { isAuthenticated, isLoading, botsStore, setBots, addBot, removeBot, userStore, logout } from '$lib/stores'; import { isAuthenticated, isLoading, botsStore, setBots, addBot, removeBot, userStore, logout } from '$lib/stores';
import { api } from '$lib/api'; import { api } from '$lib/api';
import { BotCard } from '$lib/components';
let showCreateModal = $state(false); let showCreateModal = $state(false);
let newBotName = $state(''); let newBotName = $state('');
@@ -31,7 +30,7 @@
} }
async function createBot() { async function createBot() {
if (!newBotName.trim()) return; if (!$newBotName.trim()) return;
createError = ''; createError = '';
isCreating = true; isCreating = true;
try { try {
@@ -96,7 +95,19 @@
{:else} {:else}
<div class="bots-grid"> <div class="bots-grid">
{#each $botsStore as bot} {#each $botsStore as bot}
<BotCard {bot} onOpen={(id) => goto(`/bot/${id}`)} onDelete={deleteBot} /> <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} {/each}
</div> </div>
{/if} {/if}
@@ -190,6 +201,57 @@
gap: 1.5rem; 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 { .btn {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 8px; border-radius: 8px;
@@ -212,6 +274,12 @@
border: 1px solid rgba(255, 255, 255, 0.2); 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 { .btn:hover {
transform: translateY(-2px); transform: translateY(-2px);
} }