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.
79 lines
2 KiB
Python
79 lines
2 KiB
Python
"""RSI mean-reversion — predefined Python strategy."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import itertools
|
|
|
|
import pandas as pd
|
|
import vectorbt as vbt
|
|
|
|
STRATEGY_KEY = "rsi_reversion"
|
|
DISPLAY_NAME = "RSI Mean Reversion"
|
|
DESCRIPTION = "Buy when RSI is oversold; sell when RSI is overbought."
|
|
|
|
PARAM_GRID = {
|
|
"rsi_period": list(range(7, 22, 2)),
|
|
"oversold": list(range(20, 36, 5)),
|
|
"overbought": list(range(65, 81, 5)),
|
|
}
|
|
|
|
DEFAULT_PARAMS = {
|
|
"rsi_period": 14,
|
|
"oversold": 30,
|
|
"overbought": 70,
|
|
}
|
|
|
|
|
|
def generate_signals(
|
|
close: pd.Series,
|
|
high: pd.Series,
|
|
low: pd.Series,
|
|
volume: pd.Series,
|
|
rsi_period: int = 14,
|
|
oversold: float = 30,
|
|
overbought: float = 70,
|
|
**_kwargs,
|
|
) -> tuple[pd.Series, pd.Series]:
|
|
if oversold >= overbought:
|
|
raise ValueError("oversold must be less than overbought")
|
|
|
|
rsi = vbt.RSI.run(close, window=rsi_period).rsi
|
|
entries = (rsi < oversold).fillna(False)
|
|
exits = (rsi > overbought).fillna(False)
|
|
return entries, exits
|
|
|
|
|
|
def optimize_grid(
|
|
close: pd.Series,
|
|
param_grid: dict | None = None,
|
|
init_cash: float = 10_000.0,
|
|
fees: float = 0.001,
|
|
metric: str = "sharpe_ratio",
|
|
) -> pd.DataFrame:
|
|
"""Exhaustive grid over RSI parameter space."""
|
|
from metrics import run_from_signals
|
|
|
|
grid = param_grid or PARAM_GRID
|
|
keys = list(grid.keys())
|
|
rows = []
|
|
|
|
for values in itertools.product(*(grid[k] for k in keys)):
|
|
params = dict(zip(keys, values))
|
|
if params["oversold"] >= params["overbought"]:
|
|
continue
|
|
entries, exits = generate_signals(close, close, close, close, **params)
|
|
result = run_from_signals(
|
|
close=close,
|
|
entries=entries,
|
|
exits=exits,
|
|
init_cash=init_cash,
|
|
fees=fees,
|
|
params=params,
|
|
metric=metric,
|
|
)
|
|
rows.append(result)
|
|
|
|
frame = pd.DataFrame(rows)
|
|
if frame.empty:
|
|
return frame
|
|
return frame.sort_values("score", ascending=False, na_position="last")
|