quant-web/backtest.py
epistemophiliac b5db15d6ab Initial QuantTrade stack for Coolify deployment.
Streamlit + VectorBT dashboard, Parquet harvester with nightly cron, Authentik header auth, SQLite strategy persistence, and Bugsink telemetry.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 00:46:51 -04:00

88 lines
2.2 KiB
Python

"""VectorBT backtest engine reading local Parquet OHLCV data."""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
import pandas as pd
import vectorbt as vbt
@dataclass(frozen=True)
class BacktestResult:
ticker: str
fast_window: int
slow_window: int
sharpe_ratio: float
max_drawdown: float
total_return: float
equity_curve: pd.Series
price: pd.Series
def parquet_dir() -> Path:
return Path(os.environ.get("PARQUET_DIR", "/data/parquet"))
def load_ohlcv(ticker: str) -> pd.DataFrame:
path = parquet_dir() / f"{ticker.upper()}.parquet"
if not path.exists():
raise FileNotFoundError(f"No Parquet file for {ticker.upper()} at {path}")
df = pd.read_parquet(path)
if "Date" in df.columns:
df = df.set_index("Date")
df.index = pd.to_datetime(df.index)
df = df.sort_index()
return df
def run_ma_crossover(
ticker: str,
fast_window: int,
slow_window: int,
init_cash: float = 10_000.0,
fees: float = 0.001,
) -> BacktestResult:
if fast_window >= slow_window:
raise ValueError("Fast MA window must be smaller than slow MA window")
ohlcv = load_ohlcv(ticker)
close = ohlcv["Close"].astype(float)
fast_ma = vbt.MA.run(close, fast_window, short_name="fast")
slow_ma = vbt.MA.run(close, slow_window, short_name="slow")
entries = fast_ma.ma_crossed_above(slow_ma)
exits = fast_ma.ma_crossed_below(slow_ma)
portfolio = vbt.Portfolio.from_signals(
close,
entries=entries,
exits=exits,
init_cash=init_cash,
fees=fees,
freq="1D",
)
stats = portfolio.stats()
sharpe = float(stats.get("Sharpe Ratio", 0.0) or 0.0)
max_dd = float(stats.get("Max Drawdown [%]", 0.0) or 0.0) / 100.0
total_return = float(stats.get("Total Return [%]", 0.0) or 0.0) / 100.0
equity = portfolio.value()
if isinstance(equity, pd.DataFrame):
equity = equity.iloc[:, 0]
return BacktestResult(
ticker=ticker.upper(),
fast_window=fast_window,
slow_window=slow_window,
sharpe_ratio=sharpe,
max_drawdown=max_dd,
total_return=total_return,
equity_curve=equity,
price=close,
)