Files
lowkey_backtest/api/services/storage.py
Simon Moisy 0c82c4f366 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.
2026-01-14 21:44:04 +08:00

226 lines
7.1 KiB
Python

"""
Storage service for persisting and retrieving backtest runs.
"""
from sqlalchemy.orm import Session
from api.models.database import BacktestRun
from api.models.schemas import (
BacktestResult,
BacktestSummary,
EquityPoint,
BacktestMetrics,
TradeRecord,
)
class StorageService:
"""
Service for saving and loading backtest runs from SQLite.
"""
def save_run(self, db: Session, result: BacktestResult) -> BacktestRun:
"""
Persist a backtest result to the database.
Args:
db: Database session
result: BacktestResult to save
Returns:
Created BacktestRun record
"""
run = BacktestRun(
run_id=result.run_id,
strategy=result.strategy,
symbol=result.symbol,
market_type=result.market_type,
timeframe=result.timeframe,
leverage=result.leverage,
params=result.params,
start_date=result.start_date,
end_date=result.end_date,
total_return=result.metrics.total_return,
benchmark_return=result.metrics.benchmark_return,
alpha=result.metrics.alpha,
sharpe_ratio=result.metrics.sharpe_ratio,
max_drawdown=result.metrics.max_drawdown,
win_rate=result.metrics.win_rate,
total_trades=result.metrics.total_trades,
profit_factor=result.metrics.profit_factor,
total_fees=result.metrics.total_fees,
total_funding=result.metrics.total_funding,
liquidation_count=result.metrics.liquidation_count,
liquidation_loss=result.metrics.liquidation_loss,
adjusted_return=result.metrics.adjusted_return,
)
# Serialize complex data
run.set_equity_curve([p.model_dump() for p in result.equity_curve])
run.set_trades([t.model_dump() for t in result.trades])
db.add(run)
db.commit()
db.refresh(run)
return run
def get_run(self, db: Session, run_id: str) -> BacktestResult | None:
"""
Retrieve a backtest run by ID.
Args:
db: Database session
run_id: UUID of the run
Returns:
BacktestResult or None if not found
"""
run = db.query(BacktestRun).filter(BacktestRun.run_id == run_id).first()
if not run:
return None
return self._to_result(run)
def list_runs(
self,
db: Session,
limit: int = 50,
offset: int = 0,
strategy: str | None = None,
symbol: str | None = None,
) -> tuple[list[BacktestSummary], int]:
"""
List backtest runs with optional filtering.
Args:
db: Database session
limit: Maximum number of runs to return
offset: Offset for pagination
strategy: Filter by strategy name
symbol: Filter by symbol
Returns:
Tuple of (list of summaries, total count)
"""
query = db.query(BacktestRun)
if strategy:
query = query.filter(BacktestRun.strategy == strategy)
if symbol:
query = query.filter(BacktestRun.symbol == symbol)
total = query.count()
runs = query.order_by(BacktestRun.created_at.desc()).offset(offset).limit(limit).all()
summaries = [self._to_summary(run) for run in runs]
return summaries, total
def get_runs_by_ids(self, db: Session, run_ids: list[str]) -> list[BacktestResult]:
"""
Retrieve multiple runs by their IDs.
Args:
db: Database session
run_ids: List of run UUIDs
Returns:
List of BacktestResults (preserves order)
"""
runs = db.query(BacktestRun).filter(BacktestRun.run_id.in_(run_ids)).all()
# Create lookup and preserve order
run_map = {run.run_id: run for run in runs}
results = []
for run_id in run_ids:
if run_id in run_map:
results.append(self._to_result(run_map[run_id]))
return results
def delete_run(self, db: Session, run_id: str) -> bool:
"""
Delete a backtest run.
Args:
db: Database session
run_id: UUID of the run to delete
Returns:
True if deleted, False if not found
"""
run = db.query(BacktestRun).filter(BacktestRun.run_id == run_id).first()
if not run:
return False
db.delete(run)
db.commit()
return True
def _to_result(self, run: BacktestRun) -> BacktestResult:
"""Convert database record to BacktestResult schema."""
equity_data = run.get_equity_curve()
trades_data = run.get_trades()
return BacktestResult(
run_id=run.run_id,
strategy=run.strategy,
symbol=run.symbol,
market_type=run.market_type,
timeframe=run.timeframe,
start_date=run.start_date or "",
end_date=run.end_date or "",
leverage=run.leverage,
params=run.params or {},
metrics=BacktestMetrics(
total_return=run.total_return,
benchmark_return=run.benchmark_return or 0.0,
alpha=run.alpha or 0.0,
sharpe_ratio=run.sharpe_ratio,
max_drawdown=run.max_drawdown,
win_rate=run.win_rate,
total_trades=run.total_trades,
profit_factor=run.profit_factor,
total_fees=run.total_fees,
total_funding=run.total_funding,
liquidation_count=run.liquidation_count,
liquidation_loss=run.liquidation_loss,
adjusted_return=run.adjusted_return,
),
equity_curve=[EquityPoint(**p) for p in equity_data],
trades=[TradeRecord(**t) for t in trades_data],
created_at=run.created_at.isoformat() if run.created_at else "",
)
def _to_summary(self, run: BacktestRun) -> BacktestSummary:
"""Convert database record to BacktestSummary schema."""
return BacktestSummary(
run_id=run.run_id,
strategy=run.strategy,
symbol=run.symbol,
market_type=run.market_type,
timeframe=run.timeframe,
total_return=run.total_return,
sharpe_ratio=run.sharpe_ratio,
max_drawdown=run.max_drawdown,
total_trades=run.total_trades,
created_at=run.created_at.isoformat() if run.created_at else "",
params=run.params or {},
)
# Singleton instance
_storage: StorageService | None = None
def get_storage() -> StorageService:
"""Get or create the storage service instance."""
global _storage
if _storage is None:
_storage = StorageService()
return _storage