Implement FastAPI backend and Vue 3 frontend for Lowkey Backtest UI

- Added FastAPI backend with core API endpoints for strategies, backtests, and data management.
- Introduced Vue 3 frontend with a dark theme, enabling users to run backtests, adjust parameters, and compare results.
- Implemented Pydantic schemas for request/response validation and SQLAlchemy models for database interactions.
- Enhanced project structure with dedicated modules for services, routers, and components.
- Updated dependencies in `pyproject.toml` and `frontend/package.json` to include FastAPI, SQLAlchemy, and Vue-related packages.
- Improved `.gitignore` to exclude unnecessary files and directories.
This commit is contained in:
2026-01-14 21:44:04 +08:00
parent 1e4cb87da3
commit 0c82c4f366
53 changed files with 8328 additions and 0 deletions

3
api/models/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
"""
Pydantic schemas and database models.
"""

99
api/models/database.py Normal file
View File

@@ -0,0 +1,99 @@
"""
SQLAlchemy database models and session management for backtest run persistence.
"""
import json
from datetime import datetime, timezone
from pathlib import Path
from sqlalchemy import JSON, Column, DateTime, Float, Integer, String, Text, create_engine
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
# Database file location
DB_PATH = Path(__file__).parent.parent.parent / "data" / "backtest_runs.db"
DATABASE_URL = f"sqlite:///{DB_PATH}"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class Base(DeclarativeBase):
"""Base class for SQLAlchemy models."""
pass
class BacktestRun(Base):
"""
Persisted backtest run record.
Stores all information needed to display and compare runs.
"""
__tablename__ = "backtest_runs"
id = Column(Integer, primary_key=True, autoincrement=True)
run_id = Column(String(36), unique=True, nullable=False, index=True)
# Configuration
strategy = Column(String(50), nullable=False, index=True)
symbol = Column(String(20), nullable=False, index=True)
exchange = Column(String(20), nullable=False, default="okx")
market_type = Column(String(20), nullable=False)
timeframe = Column(String(10), nullable=False)
leverage = Column(Integer, nullable=False, default=1)
params = Column(JSON, nullable=False, default=dict)
# Date range
start_date = Column(String(20), nullable=True)
end_date = Column(String(20), nullable=True)
# Metrics (denormalized for quick listing)
total_return = Column(Float, nullable=False)
benchmark_return = Column(Float, nullable=False, default=0.0)
alpha = Column(Float, nullable=False, default=0.0)
sharpe_ratio = Column(Float, nullable=False)
max_drawdown = Column(Float, nullable=False)
win_rate = Column(Float, nullable=False)
total_trades = Column(Integer, nullable=False)
profit_factor = Column(Float, nullable=True)
total_fees = Column(Float, nullable=False, default=0.0)
total_funding = Column(Float, nullable=False, default=0.0)
liquidation_count = Column(Integer, nullable=False, default=0)
liquidation_loss = Column(Float, nullable=False, default=0.0)
adjusted_return = Column(Float, nullable=True)
# Full data (JSON serialized)
equity_curve = Column(Text, nullable=False) # JSON array
trades = Column(Text, nullable=False) # JSON array
# Metadata
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
def set_equity_curve(self, data: list[dict]):
"""Serialize equity curve to JSON string."""
self.equity_curve = json.dumps(data)
def get_equity_curve(self) -> list[dict]:
"""Deserialize equity curve from JSON string."""
return json.loads(self.equity_curve) if self.equity_curve else []
def set_trades(self, data: list[dict]):
"""Serialize trades to JSON string."""
self.trades = json.dumps(data)
def get_trades(self) -> list[dict]:
"""Deserialize trades from JSON string."""
return json.loads(self.trades) if self.trades else []
def init_db():
"""Create database tables if they don't exist."""
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
Base.metadata.create_all(bind=engine)
def get_db() -> Session:
"""Get database session (dependency injection)."""
db = SessionLocal()
try:
yield db
finally:
db.close()

162
api/models/schemas.py Normal file
View File

@@ -0,0 +1,162 @@
"""
Pydantic schemas for API request/response models.
"""
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field
# --- Strategy Schemas ---
class StrategyParam(BaseModel):
"""Single strategy parameter definition."""
name: str
value: Any
param_type: str = Field(description="Type: int, float, bool, list")
min_value: float | None = None
max_value: float | None = None
description: str | None = None
class StrategyInfo(BaseModel):
"""Strategy information with parameters."""
name: str
display_name: str
market_type: str
default_leverage: int
default_params: dict[str, Any]
grid_params: dict[str, Any]
class StrategiesResponse(BaseModel):
"""Response for GET /api/strategies."""
strategies: list[StrategyInfo]
# --- Symbol/Data Schemas ---
class SymbolInfo(BaseModel):
"""Available symbol information."""
symbol: str
exchange: str
market_type: str
timeframes: list[str]
start_date: str | None = None
end_date: str | None = None
row_count: int = 0
class DataStatusResponse(BaseModel):
"""Response for GET /api/data/status."""
symbols: list[SymbolInfo]
# --- Backtest Schemas ---
class BacktestRequest(BaseModel):
"""Request body for POST /api/backtest."""
strategy: str
symbol: str
exchange: str = "okx"
timeframe: str = "1h"
market_type: str = "perpetual"
start_date: str | None = None
end_date: str | None = None
init_cash: float = 10000.0
leverage: int | None = None
fees: float | None = None
slippage: float = 0.001
sl_stop: float | None = None
tp_stop: float | None = None
sl_trail: bool = False
params: dict[str, Any] = Field(default_factory=dict)
class TradeRecord(BaseModel):
"""Single trade record."""
entry_time: str
exit_time: str | None = None
entry_price: float
exit_price: float | None = None
size: float
direction: str
pnl: float | None = None
return_pct: float | None = None
status: str = "closed"
class EquityPoint(BaseModel):
"""Single point on equity curve."""
timestamp: str
value: float
drawdown: float = 0.0
class BacktestMetrics(BaseModel):
"""Backtest performance metrics."""
total_return: float
benchmark_return: float = 0.0
alpha: float = 0.0
sharpe_ratio: float
max_drawdown: float
win_rate: float
total_trades: int
profit_factor: float | None = None
avg_trade_return: float | None = None
total_fees: float = 0.0
total_funding: float = 0.0
liquidation_count: int = 0
liquidation_loss: float = 0.0
adjusted_return: float | None = None
class BacktestResult(BaseModel):
"""Complete backtest result."""
run_id: str
strategy: str
symbol: str
market_type: str
timeframe: str
start_date: str
end_date: str
leverage: int
params: dict[str, Any]
metrics: BacktestMetrics
equity_curve: list[EquityPoint]
trades: list[TradeRecord]
created_at: str
class BacktestSummary(BaseModel):
"""Summary for backtest list view."""
run_id: str
strategy: str
symbol: str
market_type: str
timeframe: str
total_return: float
sharpe_ratio: float
max_drawdown: float
total_trades: int
created_at: str
params: dict[str, Any]
class BacktestListResponse(BaseModel):
"""Response for GET /api/backtests."""
runs: list[BacktestSummary]
total: int
# --- Comparison Schemas ---
class CompareRequest(BaseModel):
"""Request body for POST /api/compare."""
run_ids: list[str] = Field(min_length=2, max_length=5)
class CompareResult(BaseModel):
"""Comparison of multiple backtest runs."""
runs: list[BacktestResult]
param_diff: dict[str, list[Any]]