Streamlit + VectorBT dashboard, Parquet harvester with nightly cron, Authentik header auth, SQLite strategy persistence, and Bugsink telemetry. Co-authored-by: Cursor <cursoragent@cursor.com>
88 lines
2.2 KiB
Python
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,
|
|
)
|