Support builtin and custom generate_signals strategies with SQLite persistence, exhaustive grid scans (VectorBT comb optimization for MA crossover), professional backtest/optimize UI, and split harvester/app requirements with BuildKit pip cache.
93 lines
2.9 KiB
Python
93 lines
2.9 KiB
Python
"""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")
|