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:
225
api/services/storage.py
Normal file
225
api/services/storage.py
Normal file
@@ -0,0 +1,225 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user