"""Moving-average crossover — predefined Python strategy.""" from __future__ import annotations import numpy as np import pandas as pd import vectorbt as vbt STRATEGY_KEY = "ma_crossover" DISPLAY_NAME = "MA Crossover" DESCRIPTION = "Enter when fast MA crosses above slow MA; exit on cross below." PARAM_GRID = { "window_pool": list(range(5, 101, 5)), } DEFAULT_PARAMS = { "fast_window": 20, "slow_window": 50, } def generate_signals( close: pd.Series, high: pd.Series, low: pd.Series, volume: pd.Series, fast_window: int = 20, slow_window: int = 50, **_kwargs, ) -> tuple[pd.Series, pd.Series]: if fast_window >= slow_window: raise ValueError("fast_window must be smaller than slow_window") fast_ma = vbt.MA.run(close, fast_window).ma slow_ma = vbt.MA.run(close, slow_window).ma entries = fast_ma.vbt.crossed_above(slow_ma).fillna(False) exits = fast_ma.vbt.crossed_below(slow_ma).fillna(False) return entries, exits def optimize_vectorized( close: pd.Series, window_pool: list[int] | None = None, init_cash: float = 10_000.0, fees: float = 0.001, metric: str = "sharpe_ratio", ) -> pd.DataFrame: """VectorBT combinatorial scan across all fast/slow pairs (fast < slow).""" pool = np.array(window_pool or PARAM_GRID["window_pool"], dtype=int) fast_ma, slow_ma = vbt.MA.run_combs(close, pool, r=2, short_names=["fast", "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", ) metric_fn = { "sharpe_ratio": portfolio.sharpe_ratio, "sortino_ratio": portfolio.sortino_ratio, "total_return": portfolio.total_return, "max_drawdown": lambda **_: portfolio.max_drawdown(group_by=False), }.get(metric, portfolio.sharpe_ratio) scores = metric_fn(group_by=False) max_dd = portfolio.max_drawdown(group_by=False) total_ret = portfolio.total_return(group_by=False) sortino = portfolio.sortino_ratio(group_by=False) trades = portfolio.trades.count(group_by=False) rows = [] for idx, score in scores.items(): fast_w, slow_w = int(idx[0]), int(idx[1]) rows.append( { "fast_window": fast_w, "slow_window": slow_w, "score": float(score) if score == score else float("nan"), "sharpe_ratio": float(scores[idx]) if scores[idx] == scores[idx] else float("nan"), "sortino_ratio": float(sortino[idx]) if sortino[idx] == sortino[idx] else float("nan"), "total_return": float(total_ret[idx]), "max_drawdown": float(max_dd[idx]), "total_trades": int(trades[idx]), } ) return pd.DataFrame(rows).sort_values("score", ascending=False, na_position="last")