Source code for wraquant.backtest.strategy
"""Strategy abstract base class and common strategies."""
from __future__ import annotations
from abc import ABC, abstractmethod
import numpy as np
import pandas as pd
[docs]
class Strategy(ABC):
"""Abstract base class for trading strategies.
Subclasses must implement ``generate_signals`` which maps prices
to position signals (-1, 0, +1 or fractional).
Example:
>>> class MACrossover(Strategy):
... def __init__(self, fast: int = 10, slow: int = 50):
... self.fast = fast
... self.slow = slow
...
... def generate_signals(self, prices: pd.DataFrame) -> pd.DataFrame:
... fast_ma = prices.rolling(self.fast).mean()
... slow_ma = prices.rolling(self.slow).mean()
... return (fast_ma > slow_ma).astype(float)
"""
[docs]
@abstractmethod
def generate_signals(self, prices: pd.DataFrame) -> pd.DataFrame:
"""Generate position signals from price data.
Parameters:
prices: DataFrame of asset prices (columns = assets).
Returns:
DataFrame of signals with same shape as prices.
Values represent desired position: 1 = long, -1 = short, 0 = flat.
"""
[docs]
class BuyAndHold(Strategy):
"""Buy and hold strategy — always fully invested."""
[docs]
def generate_signals(self, prices: pd.DataFrame) -> pd.DataFrame:
return pd.DataFrame(
np.ones_like(prices.values),
index=prices.index,
columns=prices.columns,
)
[docs]
class MomentumStrategy(Strategy):
"""Simple momentum strategy based on lookback returns.
Parameters:
lookback: Number of periods for momentum calculation.
top_n: Number of top assets to go long.
"""
[docs]
def __init__(self, lookback: int = 20, top_n: int | None = None) -> None:
self.lookback = lookback
self.top_n = top_n
[docs]
def generate_signals(self, prices: pd.DataFrame) -> pd.DataFrame:
mom = prices.pct_change(self.lookback)
if self.top_n is not None and self.top_n < prices.shape[1]:
signals = pd.DataFrame(0.0, index=prices.index, columns=prices.columns)
for i in range(len(prices)):
if i < self.lookback:
continue
row = mom.iloc[i]
top = row.nlargest(self.top_n).index
signals.iloc[i][top] = 1.0 / self.top_n
return signals
return (mom > 0).astype(float)
[docs]
class MeanReversionStrategy(Strategy):
"""Mean reversion strategy using z-score.
Parameters:
window: Lookback window for mean/std.
entry_z: Z-score threshold to enter position.
exit_z: Z-score threshold to exit position.
"""
[docs]
def __init__(
self, window: int = 20, entry_z: float = 2.0, exit_z: float = 0.5
) -> None:
self.window = window
self.entry_z = entry_z
self.exit_z = exit_z
[docs]
def generate_signals(self, prices: pd.DataFrame) -> pd.DataFrame:
rolling_mean = prices.rolling(self.window).mean()
rolling_std = prices.rolling(self.window).std()
z_scores = (prices - rolling_mean) / rolling_std
signals = pd.DataFrame(0.0, index=prices.index, columns=prices.columns)
signals[z_scores < -self.entry_z] = 1.0 # Buy when oversold
signals[z_scores > self.entry_z] = -1.0 # Sell when overbought
return signals