from abc import ABC, abstractmethod import pandas as pd class Strategy(ABC): """ Abstract base class for all strategies. """ @abstractmethod def generate_signals(self, data): """ Generates trading signals for each stock. :param data: A pandas DataFrame of historical price data. :return: A pandas Series with the same index as the input data, containing the trading signals (e.g., 'buy', 'hold', 'sell'). """ pass class BarStrategy(Strategy): """Strategy on completed daily bars with several fields. ``fields`` names the panels it reads among close/open/high/low/volume. ``generate_signals(bars)`` takes ``{field: DataFrame}`` (a bare close DataFrame is accepted when only "close" is needed) and returns weights whose row t uses bars through t-1, like every Strategy. Backtests may add ``bars["eligible"]`` (bool, point-in-time index membership): prices stay unmasked so signals see full histories, and only eligible names are held. Timing differs from close-price strategies in the live trader: those read the session's open as their latest price, while a BarStrategy only uses completed bars, so the weights of row t trade at the close of t. """ fields: tuple[str, ...] = ("close",) def generate_signals(self, data): bars = data if isinstance(data, dict) else {"close": data} missing = [f for f in self.fields if f not in bars] if missing: raise ValueError(f"{type(self).__name__} needs bar fields {missing}") return self.generate_weights(bars) @abstractmethod def generate_weights(self, bars: dict[str, pd.DataFrame]) -> pd.DataFrame: """Weights (dates x assets), row t using bars through t-1."""