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:
3
api/models/__init__.py
Normal file
3
api/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Pydantic schemas and database models.
|
||||
"""
|
||||
99
api/models/database.py
Normal file
99
api/models/database.py
Normal 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
162
api/models/schemas.py
Normal 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]]
|
||||
Reference in New Issue
Block a user